@usenaive-sdk/cli 0.15.1 → 1.0.1

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 (281) hide show
  1. package/README.md +43 -402
  2. package/dist/index.js +51796 -502
  3. package/package.json +22 -18
  4. package/dist/client.d.ts +0 -108
  5. package/dist/client.js +0 -458
  6. package/dist/client.js.map +0 -1
  7. package/dist/commands/account-kits.d.ts +0 -2
  8. package/dist/commands/account-kits.js +0 -71
  9. package/dist/commands/account-kits.js.map +0 -1
  10. package/dist/commands/aeo.d.ts +0 -2
  11. package/dist/commands/aeo.js +0 -451
  12. package/dist/commands/aeo.js.map +0 -1
  13. package/dist/commands/agent-profiles.d.ts +0 -11
  14. package/dist/commands/agent-profiles.js +0 -120
  15. package/dist/commands/agent-profiles.js.map +0 -1
  16. package/dist/commands/agents.d.ts +0 -2
  17. package/dist/commands/agents.js +0 -916
  18. package/dist/commands/agents.js.map +0 -1
  19. package/dist/commands/app-data.d.ts +0 -2
  20. package/dist/commands/app-data.js +0 -445
  21. package/dist/commands/app-data.js.map +0 -1
  22. package/dist/commands/approvals.d.ts +0 -2
  23. package/dist/commands/approvals.js +0 -242
  24. package/dist/commands/approvals.js.map +0 -1
  25. package/dist/commands/apps.d.ts +0 -9
  26. package/dist/commands/apps.js +0 -844
  27. package/dist/commands/apps.js.map +0 -1
  28. package/dist/commands/audio.d.ts +0 -9
  29. package/dist/commands/audio.js +0 -391
  30. package/dist/commands/audio.js.map +0 -1
  31. package/dist/commands/auth.d.ts +0 -31
  32. package/dist/commands/auth.js +0 -428
  33. package/dist/commands/auth.js.map +0 -1
  34. package/dist/commands/billing.d.ts +0 -2
  35. package/dist/commands/billing.js +0 -250
  36. package/dist/commands/billing.js.map +0 -1
  37. package/dist/commands/brain.d.ts +0 -2
  38. package/dist/commands/brain.js +0 -1300
  39. package/dist/commands/brain.js.map +0 -1
  40. package/dist/commands/browser.d.ts +0 -2
  41. package/dist/commands/browser.js +0 -378
  42. package/dist/commands/browser.js.map +0 -1
  43. package/dist/commands/business.d.ts +0 -2
  44. package/dist/commands/business.js +0 -566
  45. package/dist/commands/business.js.map +0 -1
  46. package/dist/commands/cards.d.ts +0 -2
  47. package/dist/commands/cards.js +0 -419
  48. package/dist/commands/cards.js.map +0 -1
  49. package/dist/commands/ceo.d.ts +0 -2
  50. package/dist/commands/ceo.js +0 -369
  51. package/dist/commands/ceo.js.map +0 -1
  52. package/dist/commands/clone.d.ts +0 -2
  53. package/dist/commands/clone.js +0 -203
  54. package/dist/commands/clone.js.map +0 -1
  55. package/dist/commands/companies.d.ts +0 -10
  56. package/dist/commands/companies.js +0 -283
  57. package/dist/commands/companies.js.map +0 -1
  58. package/dist/commands/company-data.d.ts +0 -2
  59. package/dist/commands/company-data.js +0 -168
  60. package/dist/commands/company-data.js.map +0 -1
  61. package/dist/commands/compute.d.ts +0 -2
  62. package/dist/commands/compute.js +0 -498
  63. package/dist/commands/compute.js.map +0 -1
  64. package/dist/commands/config.d.ts +0 -2
  65. package/dist/commands/config.js +0 -36
  66. package/dist/commands/config.js.map +0 -1
  67. package/dist/commands/connections.d.ts +0 -8
  68. package/dist/commands/connections.js +0 -137
  69. package/dist/commands/connections.js.map +0 -1
  70. package/dist/commands/cron-jobs.d.ts +0 -2
  71. package/dist/commands/cron-jobs.js +0 -245
  72. package/dist/commands/cron-jobs.js.map +0 -1
  73. package/dist/commands/domains.d.ts +0 -2
  74. package/dist/commands/domains.js +0 -474
  75. package/dist/commands/domains.js.map +0 -1
  76. package/dist/commands/ecommerce.d.ts +0 -2
  77. package/dist/commands/ecommerce.js +0 -277
  78. package/dist/commands/ecommerce.js.map +0 -1
  79. package/dist/commands/email.d.ts +0 -2
  80. package/dist/commands/email.js +0 -336
  81. package/dist/commands/email.js.map +0 -1
  82. package/dist/commands/employees.d.ts +0 -2
  83. package/dist/commands/employees.js +0 -190
  84. package/dist/commands/employees.js.map +0 -1
  85. package/dist/commands/env.d.ts +0 -7
  86. package/dist/commands/env.js +0 -112
  87. package/dist/commands/env.js.map +0 -1
  88. package/dist/commands/formation.d.ts +0 -2
  89. package/dist/commands/formation.js +0 -273
  90. package/dist/commands/formation.js.map +0 -1
  91. package/dist/commands/identity.d.ts +0 -2
  92. package/dist/commands/identity.js +0 -110
  93. package/dist/commands/identity.js.map +0 -1
  94. package/dist/commands/images.d.ts +0 -2
  95. package/dist/commands/images.js +0 -371
  96. package/dist/commands/images.js.map +0 -1
  97. package/dist/commands/init.d.ts +0 -5
  98. package/dist/commands/init.js +0 -43
  99. package/dist/commands/init.js.map +0 -1
  100. package/dist/commands/jobs.d.ts +0 -2
  101. package/dist/commands/jobs.js +0 -144
  102. package/dist/commands/jobs.js.map +0 -1
  103. package/dist/commands/keys.d.ts +0 -2
  104. package/dist/commands/keys.js +0 -156
  105. package/dist/commands/keys.js.map +0 -1
  106. package/dist/commands/legacy-auth.d.ts +0 -27
  107. package/dist/commands/legacy-auth.js +0 -64
  108. package/dist/commands/legacy-auth.js.map +0 -1
  109. package/dist/commands/llm.d.ts +0 -2
  110. package/dist/commands/llm.js +0 -125
  111. package/dist/commands/llm.js.map +0 -1
  112. package/dist/commands/login.d.ts +0 -2
  113. package/dist/commands/login.js +0 -65
  114. package/dist/commands/login.js.map +0 -1
  115. package/dist/commands/logout.d.ts +0 -7
  116. package/dist/commands/logout.js +0 -55
  117. package/dist/commands/logout.js.map +0 -1
  118. package/dist/commands/logs.d.ts +0 -2
  119. package/dist/commands/logs.js +0 -42
  120. package/dist/commands/logs.js.map +0 -1
  121. package/dist/commands/loops.d.ts +0 -2
  122. package/dist/commands/loops.js +0 -260
  123. package/dist/commands/loops.js.map +0 -1
  124. package/dist/commands/media.d.ts +0 -2
  125. package/dist/commands/media.js +0 -183
  126. package/dist/commands/media.js.map +0 -1
  127. package/dist/commands/memory.d.ts +0 -2
  128. package/dist/commands/memory.js +0 -151
  129. package/dist/commands/memory.js.map +0 -1
  130. package/dist/commands/mobile.d.ts +0 -4
  131. package/dist/commands/mobile.js +0 -382
  132. package/dist/commands/mobile.js.map +0 -1
  133. package/dist/commands/module.d.ts +0 -8
  134. package/dist/commands/module.js +0 -48
  135. package/dist/commands/module.js.map +0 -1
  136. package/dist/commands/objectives.d.ts +0 -2
  137. package/dist/commands/objectives.js +0 -243
  138. package/dist/commands/objectives.js.map +0 -1
  139. package/dist/commands/payments.d.ts +0 -2
  140. package/dist/commands/payments.js +0 -131
  141. package/dist/commands/payments.js.map +0 -1
  142. package/dist/commands/people.d.ts +0 -2
  143. package/dist/commands/people.js +0 -197
  144. package/dist/commands/people.js.map +0 -1
  145. package/dist/commands/phone.d.ts +0 -2
  146. package/dist/commands/phone.js +0 -289
  147. package/dist/commands/phone.js.map +0 -1
  148. package/dist/commands/projects.d.ts +0 -13
  149. package/dist/commands/projects.js +0 -220
  150. package/dist/commands/projects.js.map +0 -1
  151. package/dist/commands/queue.d.ts +0 -2
  152. package/dist/commands/queue.js +0 -121
  153. package/dist/commands/queue.js.map +0 -1
  154. package/dist/commands/register.d.ts +0 -2
  155. package/dist/commands/register.js +0 -88
  156. package/dist/commands/register.js.map +0 -1
  157. package/dist/commands/report.d.ts +0 -17
  158. package/dist/commands/report.js +0 -109
  159. package/dist/commands/report.js.map +0 -1
  160. package/dist/commands/sandbox.d.ts +0 -2
  161. package/dist/commands/sandbox.js +0 -558
  162. package/dist/commands/sandbox.js.map +0 -1
  163. package/dist/commands/search.d.ts +0 -2
  164. package/dist/commands/search.js +0 -223
  165. package/dist/commands/search.js.map +0 -1
  166. package/dist/commands/seo.d.ts +0 -2
  167. package/dist/commands/seo.js +0 -528
  168. package/dist/commands/seo.js.map +0 -1
  169. package/dist/commands/sessions.d.ts +0 -6
  170. package/dist/commands/sessions.js +0 -109
  171. package/dist/commands/sessions.js.map +0 -1
  172. package/dist/commands/social-data.d.ts +0 -2
  173. package/dist/commands/social-data.js +0 -245
  174. package/dist/commands/social-data.js.map +0 -1
  175. package/dist/commands/social.d.ts +0 -2
  176. package/dist/commands/social.js +0 -407
  177. package/dist/commands/social.js.map +0 -1
  178. package/dist/commands/status.d.ts +0 -2
  179. package/dist/commands/status.js +0 -79
  180. package/dist/commands/status.js.map +0 -1
  181. package/dist/commands/support.d.ts +0 -2
  182. package/dist/commands/support.js +0 -134
  183. package/dist/commands/support.js.map +0 -1
  184. package/dist/commands/tasks.d.ts +0 -2
  185. package/dist/commands/tasks.js +0 -409
  186. package/dist/commands/tasks.js.map +0 -1
  187. package/dist/commands/teams.d.ts +0 -69
  188. package/dist/commands/teams.js +0 -503
  189. package/dist/commands/teams.js.map +0 -1
  190. package/dist/commands/trading.d.ts +0 -2
  191. package/dist/commands/trading.js +0 -333
  192. package/dist/commands/trading.js.map +0 -1
  193. package/dist/commands/up.d.ts +0 -551
  194. package/dist/commands/up.js +0 -1149
  195. package/dist/commands/up.js.map +0 -1
  196. package/dist/commands/usage.d.ts +0 -2
  197. package/dist/commands/usage.js +0 -73
  198. package/dist/commands/usage.js.map +0 -1
  199. package/dist/commands/use.d.ts +0 -12
  200. package/dist/commands/use.js +0 -96
  201. package/dist/commands/use.js.map +0 -1
  202. package/dist/commands/users.d.ts +0 -10
  203. package/dist/commands/users.js +0 -82
  204. package/dist/commands/users.js.map +0 -1
  205. package/dist/commands/vault.d.ts +0 -2
  206. package/dist/commands/vault.js +0 -101
  207. package/dist/commands/vault.js.map +0 -1
  208. package/dist/commands/verification.d.ts +0 -2
  209. package/dist/commands/verification.js +0 -152
  210. package/dist/commands/verification.js.map +0 -1
  211. package/dist/commands/verify-email.d.ts +0 -19
  212. package/dist/commands/verify-email.js +0 -130
  213. package/dist/commands/verify-email.js.map +0 -1
  214. package/dist/commands/video.d.ts +0 -2
  215. package/dist/commands/video.js +0 -417
  216. package/dist/commands/video.js.map +0 -1
  217. package/dist/commands/voice.d.ts +0 -2
  218. package/dist/commands/voice.js +0 -516
  219. package/dist/commands/voice.js.map +0 -1
  220. package/dist/commands/wallet.d.ts +0 -2
  221. package/dist/commands/wallet.js +0 -244
  222. package/dist/commands/wallet.js.map +0 -1
  223. package/dist/commands/webhooks.d.ts +0 -13
  224. package/dist/commands/webhooks.js +0 -119
  225. package/dist/commands/webhooks.js.map +0 -1
  226. package/dist/commands/whoami.d.ts +0 -2
  227. package/dist/commands/whoami.js +0 -53
  228. package/dist/commands/whoami.js.map +0 -1
  229. package/dist/config.d.ts +0 -42
  230. package/dist/config.js +0 -83
  231. package/dist/config.js.map +0 -1
  232. package/dist/index.d.ts +0 -3
  233. package/dist/index.js.map +0 -1
  234. package/dist/lib/brain-select.d.ts +0 -193
  235. package/dist/lib/brain-select.js +0 -403
  236. package/dist/lib/brain-select.js.map +0 -1
  237. package/dist/lib/cli-validation.d.ts +0 -12
  238. package/dist/lib/cli-validation.js +0 -46
  239. package/dist/lib/cli-validation.js.map +0 -1
  240. package/dist/lib/config-errors.d.ts +0 -4
  241. package/dist/lib/config-errors.js +0 -13
  242. package/dist/lib/config-errors.js.map +0 -1
  243. package/dist/lib/deprecation.d.ts +0 -158
  244. package/dist/lib/deprecation.js +0 -140
  245. package/dist/lib/deprecation.js.map +0 -1
  246. package/dist/lib/duration.d.ts +0 -8
  247. package/dist/lib/duration.js +0 -11
  248. package/dist/lib/duration.js.map +0 -1
  249. package/dist/lib/http-response.d.ts +0 -11
  250. package/dist/lib/http-response.js +0 -96
  251. package/dist/lib/http-response.js.map +0 -1
  252. package/dist/lib/llm-paywall.d.ts +0 -82
  253. package/dist/lib/llm-paywall.js +0 -231
  254. package/dist/lib/llm-paywall.js.map +0 -1
  255. package/dist/lib/pricing.d.ts +0 -132
  256. package/dist/lib/pricing.js +0 -148
  257. package/dist/lib/pricing.js.map +0 -1
  258. package/dist/lib/starter-config.d.ts +0 -13
  259. package/dist/lib/starter-config.js +0 -71
  260. package/dist/lib/starter-config.js.map +0 -1
  261. package/dist/lib/stream-frames.d.ts +0 -114
  262. package/dist/lib/stream-frames.js +0 -169
  263. package/dist/lib/stream-frames.js.map +0 -1
  264. package/dist/lib/teams-catalog.d.ts +0 -141
  265. package/dist/lib/teams-catalog.js +0 -841
  266. package/dist/lib/teams-catalog.js.map +0 -1
  267. package/dist/lib/unknown-command.d.ts +0 -71
  268. package/dist/lib/unknown-command.js +0 -132
  269. package/dist/lib/unknown-command.js.map +0 -1
  270. package/dist/lib/version-check.d.ts +0 -26
  271. package/dist/lib/version-check.js +0 -21
  272. package/dist/lib/version-check.js.map +0 -1
  273. package/dist/output.d.ts +0 -246
  274. package/dist/output.js +0 -539
  275. package/dist/output.js.map +0 -1
  276. package/dist/prompt.d.ts +0 -17
  277. package/dist/prompt.js +0 -70
  278. package/dist/prompt.js.map +0 -1
  279. package/dist/version.d.ts +0 -2
  280. package/dist/version.js +0 -14
  281. package/dist/version.js.map +0 -1
@@ -1,1149 +0,0 @@
1
- import { Command } from "commander";
2
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
- import { join, relative } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- import { agentOutput, agentOutputExit, agentError } from "../output.js";
6
- import { apiRequest, handleApiError } from "../client.js";
7
- import { isConfigAuthoringError } from "../lib/config-errors.js";
8
- const WEB_IGNORE = new Set([
9
- "node_modules", ".next", ".git", ".vercel", ".ready", "dist", ".turbo", ".cache", "data", ".DS_Store",
10
- ]);
11
- /** Never upload secrets/local state to the deploy. */
12
- function isExcludedWebFile(name) {
13
- return name.startsWith(".env"); // .env, .env.local, .env.production, …
14
- }
15
- /** Recursively collect a web project's source as base64 files for Vercel deploy. */
16
- function collectWebFiles(root) {
17
- const out = [];
18
- const walk = (dir) => {
19
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
20
- if (WEB_IGNORE.has(entry.name) || isExcludedWebFile(entry.name))
21
- continue;
22
- const full = join(dir, entry.name);
23
- if (entry.isDirectory())
24
- walk(full);
25
- else if (entry.isFile()) {
26
- if (statSync(full).size > 8 * 1024 * 1024)
27
- continue; // skip very large files
28
- out.push({ path: relative(root, full), data: readFileSync(full).toString("base64") });
29
- }
30
- }
31
- };
32
- walk(root);
33
- return out;
34
- }
35
- const CONFIG_CANDIDATES = ["naive.config.ts", "naive.config.js", "naive.config.mjs", "naive.config.json"];
36
- /**
37
- * A module-resolution failure, as opposed to the config refusing itself.
38
- *
39
- * `ERR_MODULE_NOT_FOUND` / `MODULE_NOT_FOUND` is what an absent dependency
40
- * raises. Matched on the CODE rather than the message, because a define-time
41
- * refusal is free to mention a package name in its prose and must not be
42
- * misread as a missing install.
43
- */
44
- function isMissingModule(e) {
45
- const code = e?.code;
46
- return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
47
- }
48
- /** Evaluate naive.config.{ts,js,json} to its full object (the whole NaiveConfig). */
49
- async function loadConfig(path) {
50
- if (path.endsWith(".json")) {
51
- try {
52
- return { ok: true, config: JSON.parse(readFileSync(path, "utf8")) };
53
- }
54
- catch (e) {
55
- // A committed .json needs no toolchain, so a parse failure is only ever
56
- // the file's own fault.
57
- return { ok: false, reason: "invalid", message: e instanceof Error ? e.message : String(e) };
58
- }
59
- }
60
- let jiti;
61
- try {
62
- const { createJiti } = await import("jiti");
63
- jiti = createJiti(pathToFileURL(path).href);
64
- }
65
- catch (e) {
66
- return {
67
- ok: false,
68
- reason: "unloadable",
69
- message: e instanceof Error ? e.message : String(e),
70
- };
71
- }
72
- try {
73
- const mod = (await jiti.import(path));
74
- const cfg = mod?.default ?? mod;
75
- if (!cfg || typeof cfg !== "object") {
76
- return {
77
- ok: false,
78
- reason: "invalid",
79
- message: `${path} evaluated to ${cfg === undefined ? "undefined" : typeof cfg}. It must \`export default defineConfig({...})\`.`,
80
- };
81
- }
82
- return { ok: true, config: cfg };
83
- }
84
- catch (e) {
85
- // Config authoring errors (e.g. system.root !== "ceo") must not look like a
86
- // missing dependency — surface as invalid_input with the author's message.
87
- if (isConfigAuthoringError(e)) {
88
- return {
89
- ok: false,
90
- reason: "invalid",
91
- message: e instanceof Error ? e.message : String(e),
92
- };
93
- }
94
- // The config itself threw. Almost always a NAMED define-time refusal, whose
95
- // message is written to be read by the person who wrote the config — so it
96
- // is surfaced verbatim rather than replaced with a guess.
97
- return {
98
- ok: false,
99
- reason: isMissingModule(e) ? "unloadable" : "invalid",
100
- message: e instanceof Error ? e.message : String(e),
101
- };
102
- }
103
- }
104
- /** Extract top-level keys inside a named object block, e.g. agentProfiles: { a, b }. */
105
- function extractKeys(source, section) {
106
- const re = new RegExp(`${section}\\s*:\\s*\\{`);
107
- const m = re.exec(source);
108
- if (!m)
109
- return [];
110
- let i = m.index + m[0].length;
111
- let depth = 1;
112
- const start = i;
113
- for (; i < source.length && depth > 0; i++) {
114
- if (source[i] === "{")
115
- depth++;
116
- else if (source[i] === "}")
117
- depth--;
118
- }
119
- const block = source.slice(start, i - 1);
120
- const keys = new Set();
121
- let d = 0;
122
- const keyRe = /([A-Za-z0-9_]+)\s*:/g;
123
- for (let j = 0; j < block.length; j++) {
124
- const ch = block[j];
125
- if (ch === "{" || ch === "(" || ch === "[")
126
- d++;
127
- else if (ch === "}" || ch === ")" || ch === "]")
128
- d--;
129
- else if (d === 0) {
130
- keyRe.lastIndex = j;
131
- const km = keyRe.exec(block);
132
- if (km && km.index === j) {
133
- keys.add(km[1]);
134
- j = keyRe.lastIndex - 1;
135
- }
136
- }
137
- }
138
- return [...keys];
139
- }
140
- function findConfig() {
141
- return CONFIG_CANDIDATES.map((c) => join(process.cwd(), c)).find(existsSync) ?? null;
142
- }
143
- /**
144
- * The brain half of the report, or undefined when the config declared none.
145
- *
146
- * Exported for the same reason it exists: `naive up` printed `applied: true` and
147
- * said NOTHING about brains, so a config that declared two of them looked
148
- * identical to a config that declared none. `--plan` showed them (it prints the
149
- * whole plan); apply did not. A provisioned thing that is not reported is the
150
- * same silence as an unprovisioned one, one level up.
151
- *
152
- * 🔴 AND `unpersisted` IS PART OF THE REPORT, NOT AN EXTRA. The apply path
153
- * computed the accept-and-drop list and this projection threw it away, so
154
- * `naive up --plan` told the user which declared brain keys have no column and
155
- * plain `naive up` — the command people actually run — did not. That is the same
156
- * defect one level down: `packages/api/src/services/deployment-brains.ts`'s own
157
- * header calls accept-and-drop "the exact defect this file exists to remove", and
158
- * a report only the dry run prints is a report most operators never see.
159
- */
160
- export function brainReport(brains) {
161
- if (!brains || !brains.declared_in)
162
- return undefined;
163
- const unpersisted = brains.unpersisted ?? {};
164
- return {
165
- declared_in: brains.declared_in,
166
- created: (brains.created ?? []).map((b) => b.name),
167
- converged: (brains.converged ?? []).map((b) => b.name),
168
- default: brains.default?.name ?? null,
169
- retained: brains.retained_undeclared ?? [],
170
- ...(Object.keys(unpersisted).length > 0 ? { unpersisted } : {}),
171
- ...(brains.notes?.length ? { notes: brains.notes } : {}),
172
- };
173
- }
174
- /**
175
- * How a loop's cadence PRINTS — the one place that decides, so the four lines
176
- * below cannot disagree about it.
177
- *
178
- * An interval is spelled `every 3600s` rather than as a bare number: `(3600)`
179
- * next to a name reads as an id, and the whole point of this line is that an
180
- * operator can tell at a glance what is about to spend on a timer.
181
- *
182
- * The empty fallback is reachable only for an {@link AppliedLoop} carrying
183
- * neither arm, which the fold refuses (`loop_cadence_invalid`) — so it exists to
184
- * keep the report printable rather than to describe anything real.
185
- */
186
- export function loopCadence(l) {
187
- if (typeof l.everySeconds === "number")
188
- return `every ${l.everySeconds}s`;
189
- return l.cron ?? "no cadence";
190
- }
191
- /**
192
- * Every loop this apply touched on a team, BOTH LANES FOLDED.
193
- *
194
- * A registered loop is real, recurring work the apply just scheduled, and
195
- * reporting nothing about it made a firing schedule invisible — the reason this
196
- * projection exists at all. It read `hermes?.agents` only, which was correct
197
- * exactly as long as the durable lane registered nothing: a durable role's
198
- * `loop` came back in `unconsumed` and there was no timer to print. Now that
199
- * `registerDurableTeamLoops` (packages/api/src/services/system-staffing.ts)
200
- * registers it against the runtime's own alarm, reading one lane would make a
201
- * schedule the apply had just created invisible on the OTHER runtime — the same
202
- * silence, moved.
203
- *
204
- * Folded rather than `durable ?? hermes` for the reason `agents` is: a MIXED
205
- * team is reported by both materializers and preferring one drops the other's
206
- * half.
207
- */
208
- export function projectLoops(hermes, durable) {
209
- return [...(hermes?.agents ?? []), ...(durable?.agents ?? [])]
210
- .filter((a) => a.loop !== undefined)
211
- .map((a) => ({ role: a.role, ...a.loop }));
212
- }
213
- /**
214
- * One hint line per loop, plus one per loop this apply cancelled.
215
- *
216
- * 🔴 THE LIST COMMAND IS LANE-DEPENDENT AND NAMING THE WRONG ONE IS WORSE THAN
217
- * NAMING NONE. `naive cron list` reads `cron_jobs`, which has no row for a
218
- * durable tenant — an operator who followed it after a durable loop registered
219
- * would find nothing and conclude the line above it had lied. A mixed team has
220
- * both lanes at once, so the lookup is per ROLE and falls back to the team's.
221
- */
222
- export function loopHints(t) {
223
- const lines = (t.loops ?? []).map((l) => {
224
- const lane = t.role_runtimes?.[l.role] ?? t.runtime;
225
- const list = lane === "durable" ? `\`naive teams schedules ${t.name}\` shows it` : "`naive cron list` shows it";
226
- const why = l.reason ? ` — ${l.reason}` : "";
227
- const cadence = loopCadence(l);
228
- return l.status === "registered"
229
- ? `loop "${l.name}" (${cadence}) registered for ${t.name}/${l.role} — ${list}.${why}`
230
- : l.status === "queued"
231
- ? `loop "${l.name}" (${cadence}) for ${t.name}/${l.role} is QUEUED — nothing is firing on this cadence yet${why || " — the runtime could not take it yet; it lands when the runtime is reachable."}`
232
- // THE UNSCHEDULABLE LINE NO LONGER ASSERTS *WHY*. It used to say the
233
- // cadence "is not a cron expression the scheduler can compile", which
234
- // was the only reason that existed when the only arm was cron. An
235
- // interval loop on a hermes role is now also unschedulable, for a
236
- // completely different reason, and that reason arrives in `reason`.
237
- // Stating a cause the field no longer implies would make the report
238
- // wrong exactly when it matters.
239
- : `loop "${l.name}" (${cadence}) for ${t.name}/${l.role} is UNSCHEDULABLE — nothing was registered.${why || ` "${cadence}" is not a cadence this lane's scheduler can compile.`}`;
240
- });
241
- // A CANCELLED TIMER IS WORK THE APPLY DID. Printed for the same reason
242
- // `archived` is: the config stopped declaring it, so something had to happen
243
- // to it, and silence is the reading that leaves an operator unsure whether it
244
- // is still spending.
245
- return [
246
- ...lines,
247
- ...(t.loops_unscheduled ?? []).map((id) => `loop "${id}" UNSCHEDULED on ${t.name} — this config no longer declares it, so the runtime is no longer firing it.`),
248
- ];
249
- }
250
- /**
251
- * 🔴 WHAT THE CONTROL PLANE PUT IN THE DIGEST THAT THE CONFIG DID NOT.
252
- *
253
- * `projectTeamRecord` grants a durable team's LEAD the built-in `delegate` when
254
- * the lane has more than one member. The grant is narrow and argued for at
255
- * length where it is made, and it was reported NOWHERE: `naive up` prints the
256
- * declared `can:`, and so do `/plan` and `/roster`, so the authority surface an
257
- * operator reviews before running `naive teams apply … --expected-digest …`
258
- * contained one capability more than anything on naive listed. A digest that
259
- * moves for a reason no surface names is the same silence the manifest report
260
- * itself exists to end, one field deeper.
261
- *
262
- * SAID PER TEAM AND ONLY WHEN THERE IS SOMETHING TO SAY. A one-member durable
263
- * team gets no grant and no line.
264
- */
265
- export function grantHints(t) {
266
- if (!t.manifest_grants?.length)
267
- return [];
268
- return [
269
- `team "${t.name}" manifest carries ${t.manifest_grants.length} capability the config did not declare: ` +
270
- `${t.manifest_grants.join(", ")} — added by the control plane so the planner can hand sub-tasks to its ` +
271
- `own members and wait for the result. It is IN the digest you review and bind, so it is named here ` +
272
- `rather than left to be discovered in the record.`,
273
- ];
274
- }
275
- /**
276
- * One hint line per declared trigger, one per trigger this apply cancelled, and
277
- * ONE RETAINED LINE for standing schedules this config did not author.
278
- *
279
- * A sibling of {@link loopHints} rather than a branch inside it, printed
280
- * immediately after it, and the split is not cosmetic: a trigger has no ROLE.
281
- * `agent({ loop })` reserves every firing to the member that declared it and
282
- * every loop line says `team/role`; `team({ on })` fires UNRESERVED and the
283
- * board hands the card to whoever is next. A line that named a role would name
284
- * one that does not exist.
285
- *
286
- * 🔴 THE RETAINED LINE IS THE HALF THAT WOULD OTHERWISE BE INVISIBLE, and it is
287
- * the reason this function exists at all. `registerTeamTriggers` refuses to
288
- * cancel a schedule it cannot prove it wrote — the one destructive failure that
289
- * cannot be undone by re-running anything — and then says nothing about it. A
290
- * correct decision nobody is told about is the same defect one level up: the
291
- * operator still has a timer spending on a cadence no config declares, and now
292
- * also has no evidence it exists. The sentence is copied in shape from the
293
- * brains one this file already prints, down to "remove one deliberately", so an
294
- * operator meets one vocabulary for drift rather than two.
295
- *
296
- * 🔴 AND WHERE A FIRING WILL NOT SHOW UP, said once per team that declares any.
297
- * A durable schedule's firing reaches NEITHER existing observability surface:
298
- * `GET /v1/triggers/ledger` reads the `events` table, whose sources are
299
- * `email|objective|cron|task|approval|company_channel` and which a durable alarm
300
- * never writes, and `routeEvent` writes `trigger_deliveries` over a disjoint
301
- * vocabulary it also never writes. The board is the only place the work appears.
302
- * Naming that here is the cheap mitigation; reconciling the two ledgers is its
303
- * own piece of work.
304
- */
305
- export function triggerHints(t) {
306
- const declared = t.triggers ?? [];
307
- const lines = declared.map((tr) => {
308
- const why = tr.reason ? ` — ${tr.reason}` : "";
309
- switch (tr.status) {
310
- case "registered":
311
- return `trigger "${tr.key}" registered for ${t.name} as "${tr.name}" (${tr.cron}, UTC) — \`naive teams schedules ${t.name}\` shows it.${why}`;
312
- case "blocked_unbound":
313
- // NOT `queued`. The repair is a command a human runs, and the runtime
314
- // will refuse every registration until they run it.
315
- return `trigger "${tr.key}" (${tr.cron}, UTC) for ${t.name} is BLOCKED — the team's manifest is not bound, so the runtime refuses to schedule against it and NOTHING was registered${why || " — bind the manifest, then re-run `naive up`."}`;
316
- case "queued":
317
- return `trigger "${tr.key}" (${tr.cron}, UTC) for ${t.name} is QUEUED — nothing is firing on this cadence yet${why || " — the runtime could not take it yet; it lands when the runtime is reachable."}`;
318
- default:
319
- return `trigger "${tr.key}" for ${t.name} is UNSCHEDULABLE — nothing was registered.${why}`;
320
- }
321
- });
322
- return [
323
- ...lines,
324
- ...(t.triggers_unscheduled ?? []).map((id) => `trigger "${id}" UNSCHEDULED on ${t.name} — this config no longer declares it, so the runtime is no longer firing it.`),
325
- // The drift half. One line, all ids, mirroring the brains sentence verbatim
326
- // in shape — including the fact that apply did NOT touch them.
327
- ...(t.schedules_retained?.length
328
- ? [
329
- `schedules on ${t.name} RETAINED, not declared by this config: ${t.schedules_retained.join(", ")} — apply never cancels a schedule it cannot prove it wrote; remove one deliberately with \`naive teams unschedule ${t.name} <id>\`.`,
330
- ]
331
- : []),
332
- ...(declared.length
333
- ? [
334
- // NAMED AS A ROUTE AND NOT AS A COMMAND, deliberately: there is no
335
- // `naive triggers` verb on this build, and pointing an operator at a
336
- // command that does not exist is the defect this line exists to
337
- // avoid, not a smaller version of it.
338
- `a declared trigger's firing appears on ${t.name}'s board and in NO trigger ledger — \`GET /v1/triggers/ledger\` reads the \`events\` table, which a durable alarm never writes to.`,
339
- ]
340
- : []),
341
- ];
342
- }
343
- /**
344
- * The per-team accept-and-drop warning, one line per team that declared any.
345
- *
346
- * The data has been on the wire the whole time (`plan.teams[].unconsumed`,
347
- * computed by `unconsumedTeamKeys` in packages/api) and `naive up` printed none
348
- * of it — `team({ governance: {…} })` applied green and enforced nothing, with
349
- * nothing anywhere for the author to notice. Printed on the PLAN and on the
350
- * APPLY, because a report only the dry run shows is a report most operators
351
- * never see.
352
- */
353
- export function unconsumedWarnings(teams) {
354
- return teams
355
- .filter((t) => (t.unconsumed ?? []).length > 0)
356
- .map((t) => `⚠ team "${t.name}" DECLARED AND UNREAD: ${t.unconsumed.join(", ")} — carried to the ` +
357
- "control plane and consumed by NOTHING. Do not treat these as in force. " +
358
- "`defineProject` refuses them at define time; remove them, or keep the config on " +
359
- "`defineConfig` (lenient) until a consumer lands.");
360
- }
361
- /**
362
- * ONE deprecation line when the config still uses the legacy ECS surface —
363
- * `runtime.pool()` declarations, an agent-level `runtime: "<pool>"` string, or
364
- * `systems:`. All three keep working indefinitely; the line names what was
365
- * found and where new configs should author instead.
366
- */
367
- export function legacyDeprecationNotice(config) {
368
- const found = [];
369
- const pools = Object.keys(config.runtime ?? {});
370
- if (pools.length > 0)
371
- found.push(`runtime.pool() declarations (${pools.join(", ")})`);
372
- const pinned = Object.entries(config.agents ?? {})
373
- .filter(([, a]) => typeof a?.runtime === "string")
374
- .map(([name]) => name);
375
- if (pinned.length > 0)
376
- found.push(`agent-level runtime: "<pool>" strings (${pinned.join(", ")})`);
377
- const systems = Object.keys(config.systems ?? {});
378
- if (systems.length > 0)
379
- found.push(`systems: (${systems.join(", ")})`);
380
- if (found.length === 0)
381
- return null;
382
- return (`DEPRECATED surface in use — ${found.join("; ")}. These keep working indefinitely; new configs ` +
383
- "author `defineProject` with `teams:` on `runtime.durable()` (or `runtime.hermes()` for the hosted lane).");
384
- }
385
- /**
386
- * ONE rename line when the config declares `agents:`. Separate from the legacy
387
- * line above on purpose: `runtime.pool()` / `systems:` are a deprecated
388
- * SURFACE, while `agents:` is a deprecated SPELLING — the block compiles to
389
- * byte-for-byte what `kits:` compiles to, so the honest notice is "rename",
390
- * not "migrate". A notice, never a refusal, on both entrypoints.
391
- */
392
- export function agentsRenameNotice(config) {
393
- const names = Object.keys(config.agents ?? {});
394
- if (names.length === 0)
395
- return null;
396
- return (`DEPRECATED spelling in use — \`agents:\` (${names.join(", ")}). It keeps working indefinitely ` +
397
- "and compiles to exactly what `kits:` compiles to; rename the block to `kits:` (and `agent()` " +
398
- "entries to `kit()`) — an account kit is instantiated per child project via " +
399
- "`naive.forProject(p).forChild(id).provision(\"<kit>\")`.");
400
- }
401
- /**
402
- * The operator credential the DURABLE RUNTIME issued, read from the ENVIRONMENT.
403
- *
404
- * Never from `naive.config.ts`, and that is the whole point rather than a style
405
- * choice. The config is committed, so a credential in it is a credential in git —
406
- * and, more importantly, the config is the one thing that must not by itself be
407
- * able to change where a tenant runs. Pointing a tenant at the durable runtime
408
- * overwrites its `sidecar_url` and `container_auth_token` and nulls its
409
- * `instance_id`, none of which is stashed anywhere. Requiring a second, separate
410
- * act means an edit alone can never do it.
411
- *
412
- * `NAIVE_DURABLE_CREDENTIAL_<TEAM>` names one team; `NAIVE_DURABLE_CREDENTIAL`
413
- * covers them all. Neither is ever printed, and the API never echoes it back.
414
- */
415
- export function durableCredentialEnvVar(team) {
416
- return `NAIVE_DURABLE_CREDENTIAL_${team.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
417
- }
418
- function readDurableCredentials(teams) {
419
- const out = {};
420
- const all = process.env.NAIVE_DURABLE_CREDENTIAL;
421
- if (all)
422
- out["*"] = all;
423
- for (const team of teams) {
424
- const secret = process.env[durableCredentialEnvVar(team)];
425
- if (secret)
426
- out[team] = secret;
427
- }
428
- return Object.keys(out).length > 0 ? out : undefined;
429
- }
430
- /**
431
- * The recovery for ONE refused team — a command that EXISTS, or none.
432
- *
433
- * 🔴 `naive runtime migrate` IS NOT A COMMAND, AND THIS USED TO PRINT IT.
434
- * Every refusal here used to emit
435
- * `naive runtime migrate --tenant … --to durable --credential-from <file>`.
436
- * Measured on the built CLI: `naive runtime migrate --tenant <id> --to hermes`
437
- * answers
438
- * `{"code":"cli_outdated","message":"Unknown command 'runtime'. Your naive CLI
439
- * is v0.12.4; this command may exist in a newer version."}` — and it exists in
440
- * NO version: `packages/cli/src/index.ts` registers no `runtime` group at all.
441
- * So an operator whose apply was refused was handed a command that tells them to
442
- * upgrade, forever. A next_step that cannot be run is worse than none, because it
443
- * ends the search.
444
- *
445
- * The two cases are genuinely different and are answered differently:
446
- *
447
- * · `runtime_credential_required` — there IS a procedure, it works today, and
448
- * it is the one this command already implements: put the operator credential
449
- * the durable runtime issued in the environment and re-run. Never in
450
- * `naive.config.ts`, which is committed.
451
- * · everything else (`runtime_migration_requires_explicit_opt_in`,
452
- * `runtime_claim_in_flight`, `runtime_placement_lost_race`,
453
- * `runtime_not_configured`, `runtime_state_unknown`) — there is NO self-serve
454
- * command on this build. Moving an existing tenant between runtimes
455
- * overwrites `sidecar_url` and `container_auth_token` and nulls `instance_id`
456
- * with nothing stashed, so the control plane refuses it and no client can do
457
- * it either. Say that, and print the server's own message, rather than
458
- * inventing a command to carry the blame.
459
- *
460
- * The API writes the same vapour into `details.migrate_command`
461
- * (packages/api/src/services/placement.ts MIGRATE_COMMAND). That string is not
462
- * this package's to change and is reported as a follow-up; this function is why
463
- * `naive up` no longer repeats it.
464
- */
465
- export function refusalNextStep(t) {
466
- if (t.placement?.code === "runtime_credential_required") {
467
- return {
468
- command: `${durableCredentialEnvVar(t.name)}=<operator-credential> naive up`,
469
- description: `team "${t.name}": the durable runtime issued an operator credential for this tenant and it is not in ` +
470
- `the environment. Export it (or NAIVE_DURABLE_CREDENTIAL for every team) and re-run. Never put it in naive.config.ts.`,
471
- };
472
- }
473
- return null;
474
- }
475
- /**
476
- * A team whose manifest is NOT bound, so its tick dispatches nothing.
477
- *
478
- * `refused` — the runtime (or the projection) refused the record itself.
479
- * `delivered_not_applied` — the record stored, the bind was refused. The team
480
- * is placed, addressable, and inert.
481
- * `unreachable` — the runtime never answered, so nothing was delivered.
482
- *
483
- * `skipped` IS DELIBERATELY ABSENT, and that is the whole reason this is a
484
- * named constant rather than an inline list. The API emits `skipped` only when
485
- * placement already failed — there was nowhere to deliver to — and that team is
486
- * ALREADY in `refusedTeams` and already printed with its placement refusal.
487
- * Including it here would print one failure twice under two names and imply two
488
- * things went wrong, which is the report lying in the generous direction.
489
- *
490
- * `applied` and `noop` are the bound states. `noop` in particular is a SUCCESS:
491
- * the runtime already held that exact digest and rewrote nothing.
492
- */
493
- const UNBOUND_MANIFEST_STATUSES = ["refused", "delivered_not_applied", "unreachable"];
494
- /**
495
- * The `manifest` half of one team's report line, from the API's own team row.
496
- *
497
- * A SIBLING OF `placement`, NEVER NESTED INSIDE IT. The API's comment on the
498
- * field this reads is explicit about why: "the tenant routes to the durable
499
- * runtime" and "the runtime enforces what the config declared" are different
500
- * facts, and a placed team with a refused manifest is exactly the state that
501
- * must not read as success. Nesting one inside the other would collapse a
502
- * distinction the API deliberately kept, and would make the refusal look like a
503
- * detail of a successful placement.
504
- *
505
- * Absent when the API reported nothing — a hermes team has no manifest at all,
506
- * and an older server does not send the field. An empty object rather than
507
- * `{ manifest: undefined }`, so the printed envelope has no key for a fact
508
- * nobody stated.
509
- */
510
- export function projectManifest(durable) {
511
- const m = durable?.manifest;
512
- if (!m)
513
- return {};
514
- return {
515
- manifest: {
516
- status: m.status,
517
- ...(m.digest ? { digest: m.digest } : {}),
518
- ...(m.code ? { code: m.code } : {}),
519
- ...(m.error ? { error: m.error } : {}),
520
- },
521
- };
522
- }
523
- /**
524
- * WHAT `naive up` SAYS ABOUT MANIFEST DELIVERY, AND WHETHER IT MAY STILL EXIT 0.
525
- *
526
- * Extracted for the same reason `brainReport` is: the claim — "a refused
527
- * manifest is reported in the runtime's own words and the apply exits
528
- * non-zero" — is otherwise only assertable against a live API.
529
- *
530
- * Delivering the manifest is the step that turns a placed tenant into a working
531
- * one. Before this existed the CLI decoded the placement and dropped the
532
- * delivery, so a team that routed correctly and enforced NOTHING printed
533
- * exactly like one that worked: `placement: { status: "placed" }`, `applied:
534
- * true`, exit 0.
535
- *
536
- * 🔴 A `next_step` IS NOW OFFERED FOR `delivered_not_applied`, AND FOR NOTHING
537
- * ELSE. This block previously offered none at all, under this reasoning:
538
- *
539
- * The obvious candidate is `naive teams apply <team> --expected-digest
540
- * <digest>` and it is wrong TWICE on this build: `teamsCmd` registers no
541
- * `apply` subcommand, and the control plane routes `POST …/apply` through
542
- * `REFUSED_WRITES` to a 501.
543
- *
544
- * Both halves are now false, and the FIRST ONE WAS NEVER TRUE. `teams.ts`
545
- * declares no subcommands at all — it walks `lib/teams-catalog.ts` — and that
546
- * catalog has carried an `apply` row, with a REQUIRED `--expected-digest` that
547
- * defaults to nothing, since before this branch was written. Verified by
548
- * running the built CLI rather than by reading for it:
549
- *
550
- * $ naive teams --help
551
- * apply [options] <team> Record that an operator applied this exact
552
- * manifest digest. Refuses a stale one
553
- *
554
- * The second half is false as of this change: `POST …/apply` is a
555
- * `DURABLE_WRITES` row and reaches the runtime's own `apply` verb. So the
556
- * command exists, the route exists, and the digest is already in hand — which
557
- * is exactly the condition {@link refusalNextStep} sets for emitting one.
558
- *
559
- * `--tenant` IS SPELLED OUT, AND IT IS NOT OPTIONAL. `naive teams` requires it
560
- * on every subcommand and has NO default, deliberately: "a command that guesses
561
- * the tenant is a command that acts on the wrong tenant exactly once". A durable
562
- * team's subject is its OWN — `ensureTeamSubject` mints one per declared team —
563
- * so `--tenant @self` would address the operator's subject and bind a digest on
564
- * a tenant that is not this team. The apply response carries the right id on
565
- * `subject.tenant_user_id` and that is what is printed. If it is somehow absent
566
- * the next_step is WITHHELD rather than emitted with a placeholder, because a
567
- * command an operator has to guess a field in is the same dead end as a command
568
- * that 501s.
569
- *
570
- * STILL NO next_step FOR `refused` OR `unreachable`, and the distinction is the
571
- * point. `delivered_not_applied` means the record IS stored and only the bind
572
- * failed — a retryable act with a known digest. `refused` means the runtime
573
- * would not take the record at all, so there is nothing to apply and the fix is
574
- * in the config; `unreachable` means nothing was attempted. Offering an apply
575
- * for either would send an operator to run a command that cannot succeed.
576
- */
577
- export function manifestReport(teams) {
578
- const unbound = teams.filter((t) => t.manifest !== undefined && UNBOUND_MANIFEST_STATUSES.includes(t.manifest.status));
579
- const hints = unbound.map((t) => {
580
- const m = t.manifest;
581
- // The refusing side's own sentence, verbatim. The runtime names the slug it
582
- // could not resolve and lists what this deploy does ship; rewording that
583
- // into "manifest delivery failed" throws away the only part an operator can
584
- // act on.
585
- const said = m.error ? `: ${m.error}` : "";
586
- const code = m.code ? ` (${m.code})` : "";
587
- return (`team "${t.name}" manifest ${m.status.toUpperCase()}${code}${said}` +
588
- (m.status === "delivered_not_applied"
589
- ? ` — the record IS stored at digest ${m.digest ?? "(none reported)"}; it is the BIND that was refused, ` +
590
- `so this team routes, accepts requests and dispatches nothing until that digest is applied.`
591
- : m.status === "unreachable"
592
- ? ` — nothing was delivered, so the runtime is enforcing whatever it held before this apply.`
593
- : ` — nothing is bound, so this team's tick dispatches nothing.`));
594
- });
595
- const nextSteps = unbound.flatMap((t) => {
596
- const m = t.manifest;
597
- const tenantId = t.subject?.tenant_user_id;
598
- // Every condition must hold: the record must be STORED (so there is
599
- // something to bind), the digest must be KNOWN (the flag is required and
600
- // has no default), and the team's own subject must be in hand.
601
- if (m.status !== "delivered_not_applied" || !m.digest || !tenantId)
602
- return [];
603
- return [
604
- {
605
- command: `naive teams apply ${t.name} --tenant ${tenantId} --expected-digest ${m.digest}`,
606
- description: `team "${t.name}": the record is stored and the BIND was refused, so this team routes and ` +
607
- `dispatches nothing. Review the authority surface that digest names, then bind it. The digest ` +
608
- `is passed explicitly because applying whatever is current is not a review — a stale one is refused.`,
609
- },
610
- ];
611
- });
612
- return { unbound, hints, nextSteps };
613
- }
614
- /**
615
- * THE ONE PARAGRAPH SAID ABOUT UNBOUND MANIFESTS AS A GROUP, RATHER THAN PER
616
- * TEAM — and it is emitted only for the teams {@link manifestReport} could NOT
617
- * hand a repair command to.
618
- *
619
- * 🔴 IT USED TO CONTRADICT THE NEXT_STEP PRINTED THREE LINES ABOVE IT. Written
620
- * inline in the command's action closure by parity 0, it read:
621
- *
622
- * No `naive` command binds a manifest on this build: `naive teams`
623
- * registers no `apply` subcommand, and the control plane answers
624
- * `POST /v1/teams/.../apply` with 501.
625
- *
626
- * Both halves are false at HEAD and the first was never true — the full
627
- * accounting is in {@link manifestReport}'s own comment, which emits
628
- * `naive teams apply <team> --tenant … --expected-digest …` for
629
- * `delivered_not_applied`. That is the SAME status that put a team in
630
- * `unbound`, so an operator was handed a command and then told, one line later,
631
- * that no such command exists. It survived because it was a string inside a
632
- * 500-line closure that nothing imports and nothing could call.
633
- *
634
- * So it is a function now, for the reason `manifestReport` is one: a claim that
635
- * cannot be called cannot be tested, and this one was wrong for two commits.
636
- *
637
- * WHAT IT SAYS NOW is the fact that is actually true of the teams it fires for:
638
- * `naive teams apply` binds a digest the runtime has ALREADY STORED, so it can
639
- * repair `delivered_not_applied` and nothing else. `refused` means the record
640
- * was never taken; `unreachable` means it was never delivered. For both, the
641
- * runtime's own sentence is printed above and re-running `naive up` re-delivers.
642
- *
643
- * The condition is "an unbound team that got no next_step", not "any unbound
644
- * team": a team whose repair is printed does not need to be told there is none.
645
- */
646
- export function unbindableNote(report) {
647
- if (report.unbound.length <= report.nextSteps.length)
648
- return [];
649
- return [
650
- "A manifest above is unbound with NO bind to offer. `naive teams apply <team> --tenant <id> " +
651
- "--expected-digest <digest>` exists and works, but it binds a digest the runtime has already STORED — " +
652
- "for `refused` nothing was stored, and for `unreachable` nothing was even delivered, so there is no " +
653
- "digest to review. The refusing side's own sentence is printed above and names what it would not take. " +
654
- "Re-running `naive up` re-delivers, which is the only self-serve path and only helps if the cause was " +
655
- "transient.",
656
- ];
657
- }
658
- /** Team names declared in the config, without evaluating anything the API will re-derive. */
659
- function declaredTeamNames(config) {
660
- const teams = config.teams;
661
- return teams && typeof teams === "object" ? Object.keys(teams) : [];
662
- }
663
- /**
664
- * Wrap the config only when there is something to carry alongside it.
665
- *
666
- * `readConfig` on the API takes `body.config ?? body`, so both shapes work — but
667
- * a caller that supplies no credential sends the exact bytes it always sent.
668
- */
669
- function applyBody(config, credentials) {
670
- return credentials ? { config, runtime_credentials: credentials } : config;
671
- }
672
- /**
673
- * Apply asynchronously and poll the job to completion.
674
- *
675
- * An apply can outlive the production gateway timeout (Hermes provisioning
676
- * alone may wait up to two minutes), and the synchronous POST answered 504
677
- * while the apply kept running — partial state with no observable outcome.
678
- * So the CLI asks for async mode (`?async=true`); a server that supports it
679
- * answers `202 {job_id}` and the outcome is read from `GET /v1/jobs/:id`. An
680
- * older server ignores the query and answers synchronously; both shapes are
681
- * returned as the same `{ status, data }` the callers already handle.
682
- */
683
- const APPLY_POLL_INTERVAL_MS = 3_000;
684
- const APPLY_POLL_TIMEOUT_MS = 15 * 60_000;
685
- async function applyProject(project, body) {
686
- const resp = await apiRequest("POST", `/v1/deployments/${encodeURIComponent(project)}/apply?async=true`, body);
687
- const accepted = resp.data;
688
- if (resp.status !== 202 || typeof accepted.job_id !== "string")
689
- return resp;
690
- const deadline = Date.now() + APPLY_POLL_TIMEOUT_MS;
691
- while (Date.now() < deadline) {
692
- await new Promise((r) => setTimeout(r, APPLY_POLL_INTERVAL_MS));
693
- const poll = await apiRequest("GET", `/v1/jobs/${encodeURIComponent(accepted.job_id)}`);
694
- if (poll.status >= 400)
695
- return poll;
696
- const job = poll.data;
697
- if (job.status === "completed")
698
- return { status: 200, data: job.result ?? {} };
699
- if (job.status === "failed" || job.status === "cancelled") {
700
- // The job's error is `code: message` — re-shape it into the standard
701
- // envelope so handleApiError prints it exactly like a synchronous refusal.
702
- const raw = typeof job.error === "string" && job.error ? job.error : "The apply failed";
703
- const m = /^([a-z_]+): ([\s\S]*)$/.exec(raw);
704
- return {
705
- status: 502,
706
- data: { error: { code: m?.[1] ?? "apply_failed", message: m?.[2] ?? raw, job_id: accepted.job_id } },
707
- };
708
- }
709
- }
710
- return {
711
- status: 504,
712
- data: {
713
- error: {
714
- code: "apply_timeout",
715
- message: `The apply job is still running after ${APPLY_POLL_TIMEOUT_MS / 60_000} minutes.`,
716
- hint: `Poll it yourself: naive jobs get ${accepted.job_id} — re-running naive up converges once it settles.`,
717
- job_id: accepted.job_id,
718
- },
719
- },
720
- };
721
- }
722
- export const upCmd = new Command("up")
723
- .description("Plan + apply the infrastructure, runtime pools, agent profiles, and systems declared in naive.config.ts")
724
- .option("--plan", "Preview the plan (diff) without applying")
725
- .option("--local", "Validate the config locally only; do not contact the API")
726
- .option("--yes", "Apply without confirmation (default for non-interactive use)")
727
- .option("--no-deploy", "Provision infrastructure but skip deploying cloud.web source")
728
- .action(async (opts) => {
729
- const found = findConfig();
730
- if (!found) {
731
- agentError({
732
- action: "up",
733
- error: { code: "not_found", message: "No naive.config.ts found in the current directory" },
734
- recovery_steps: [{ command: "naive init", description: "Scaffold a naive.config.ts" }],
735
- });
736
- return;
737
- }
738
- const load = opts.local ? null : await loadConfig(found);
739
- /* ── THE CONFIG REFUSED ITSELF. THAT IS NOT A TOOLCHAIN PROBLEM. ──────────
740
- *
741
- * 🔴 THIS BRANCH IS THE POINT OF THE `ConfigLoad` UNION ABOVE. Everything a
742
- * define-time refusal is for arrives here: `brain_no_partitions`,
743
- * `legacy_multi_team`, `brain_partition_unknown`, `residency_widens_company`,
744
- * `company_brain_and_brains`, `program_undeclared`,
745
- * `workspace_allowlist_empty`, `residency_unsupported_on_runtime`. Every one
746
- * of those messages is written to be read by the person who wrote the config,
747
- * and every one of them used to be swallowed by `catch { return null }`,
748
- * relabelled "install @usenaive-sdk/iac + jiti", printed under
749
- * `success: true`, and exited 0.
750
- *
751
- * So it is surfaced VERBATIM and exits non-zero. No regex fallback: the
752
- * fallback's whole job is to say something useful when the config could not be
753
- * READ, and this config was read perfectly — it said no.
754
- */
755
- if (load && !load.ok && load.reason === "invalid") {
756
- agentError({
757
- action: "up",
758
- error: {
759
- code: "invalid_config",
760
- message: load.message,
761
- hint: `${found} evaluated and refused itself. This is the config's own named refusal, not a missing ` +
762
- "dependency — @usenaive-sdk/iac and jiti both resolved. Fix the declaration it names and re-run.",
763
- },
764
- recovery_steps: [
765
- { command: "naive up --plan", description: "Re-check the config without applying anything" },
766
- ],
767
- });
768
- return;
769
- }
770
- /* ── Fallback: the TOOLCHAIN is missing (or --local was asked for) ─────────
771
- *
772
- * The config could not be evaluated at all, so all that is left is local,
773
- * regex-only validation. The PAYLOAD is deliberately unchanged — extracting
774
- * what can be read and naming the install is genuinely the most useful thing
775
- * to say, and the `unloadable` half of the union stays a soft outcome.
776
- *
777
- * 🔴 WHAT CHANGED IS THE EXIT CODE, AND ONLY WHEN THE USER DID NOT ASK FOR
778
- * LOCAL-ONLY. `naive up` was asked to plan AND apply; on this branch it did
779
- * neither, and it still exited 0. Measured on the published CLI (0.13.1), a
780
- * config importing a package that does not resolve produced
781
- * `success: true`, `applied: false`, exit 0 — so `naive up && echo deployed`
782
- * printed "deployed" over a deployment that was never even planned. That is
783
- * the defect docs/COVERAGE-TRUTH.md §5 recorded as "`naive up` exits 0 with
784
- * `success:true` when it cannot evaluate the config"; the `invalid` half was
785
- * closed above, this is the half that was left.
786
- *
787
- * Exit 3 is this CLI's OWN ladder for exactly this shape — see
788
- * agentOutputExit in output.ts: "success, and the terminal state is not
789
- * success", written because "a CI script reading 0 as 'the work is done'
790
- * would otherwise count an `unverified` task as a pass". Nothing about the
791
- * envelope's content moves, so an agent still gets the extraction and the
792
- * install step; a shell script stops.
793
- *
794
- * `--local` keeps exit 0. There the user asked for local validation only and
795
- * got precisely that — the terminal state IS the success.
796
- */
797
- if (!load || !load.ok) {
798
- const source = readFileSync(found, "utf8");
799
- const projectMatch = /project\s*:\s*["'`]([^"'`]+)["'`]/.exec(source);
800
- const project = projectMatch?.[1] ?? "(unknown)";
801
- agentOutputExit({
802
- exitCode: opts.local ? undefined : 3,
803
- action: "up",
804
- result: {
805
- config: found,
806
- project,
807
- infrastructure: extractKeys(source, "infrastructure"),
808
- runtime_pools: extractKeys(source, "runtime"),
809
- agents: extractKeys(source, "agents").length
810
- ? extractKeys(source, "agents")
811
- : extractKeys(source, "agentProfiles").length
812
- ? extractKeys(source, "agentProfiles")
813
- : extractKeys(source, "templates"),
814
- systems: extractKeys(source, "systems"),
815
- applied: false,
816
- },
817
- next_steps: [{ command: "pnpm add @usenaive-sdk/iac jiti", description: "Install so naive up can evaluate + apply the config" }],
818
- hints: [
819
- opts.local
820
- ? `Local validation only for "${project}".`
821
- : `Validated ${found} locally, but could not evaluate it (install @usenaive-sdk/iac + jiti, or commit a naive.config.json).`,
822
- // Said in the envelope as well as in the exit code, because an agent
823
- // reads this and a shell script reads that, and neither should have to
824
- // infer it from `applied: false`.
825
- ...(opts.local
826
- ? []
827
- : [
828
- "NOTHING WAS PLANNED AND NOTHING WAS APPLIED — the config never evaluated, so no plan was " +
829
- "requested and no apply was sent. Exit code 3 (`success, and the terminal state is not " +
830
- "success`); do not read this as a completed deploy.",
831
- ]),
832
- // Only ever printed for a real resolution failure now — an invalid config
833
- // took the branch above. Said out loud so the two are never conflated again.
834
- ...(load ? [`Module resolution failed: ${load.message}`] : []),
835
- ],
836
- });
837
- return;
838
- }
839
- const config = load.config;
840
- const project = typeof config.project === "string" ? config.project : undefined;
841
- if (!project) {
842
- agentError({ action: "up", error: { code: "invalid_input", message: "naive.config.ts must export a `project` name" }, recovery_steps: [] });
843
- return;
844
- }
845
- const credentials = readDurableCredentials(declaredTeamNames(config));
846
- // The organization's OTHER projects, if the config declares any. Each is a
847
- // separate deployment under its own project key — the control plane keys
848
- // deployments, pools and systems by `(company_id, project)`, so applying two
849
- // projects is two applies of the same shape, not a new one. A config with no
850
- // `projects:` block produces an empty list and everything below is untouched.
851
- const extraProjects = Object.entries(config.projects ?? {})
852
- .map(([key, p]) => p?.slug ?? key)
853
- .filter((slug) => slug !== project);
854
- // 1. Plan (diff desired vs current).
855
- const planResp = await apiRequest("PUT", `/v1/deployments/${encodeURIComponent(project)}`, applyBody(config, credentials));
856
- handleApiError("up", planResp);
857
- const plan = planResp.data?.plan;
858
- const teamPlan = plan?.teams?.plan ?? [];
859
- if (opts.plan) {
860
- // A plan REFUSAL is not a warning to scroll past — it is the reason this
861
- // apply will not do what the config asks. Print it, and exit non-zero so a
862
- // CI step that gates on `naive up --plan` actually gates.
863
- const wouldRefuse = teamPlan.filter((t) => t.placement?.status === "refused");
864
- const extraPlans = [];
865
- for (const extra of extraProjects) {
866
- const resp = await apiRequest("PUT", `/v1/deployments/${encodeURIComponent(extra)}`, applyBody(config, credentials));
867
- handleApiError("up", resp);
868
- extraPlans.push({ project: extra, plan: resp.data?.plan });
869
- }
870
- agentOutput({
871
- action: "up",
872
- result: {
873
- config: found,
874
- project,
875
- plan,
876
- ...(extraPlans.length ? { projects: extraPlans } : {}),
877
- applied: false,
878
- },
879
- next_steps: [{ command: "naive up", description: "Apply the plan" }],
880
- hints: [
881
- "Plan only — nothing was provisioned.",
882
- ...unconsumedWarnings(teamPlan),
883
- ...(legacyDeprecationNotice(config) ? [legacyDeprecationNotice(config)] : []),
884
- ...(agentsRenameNotice(config) ? [agentsRenameNotice(config)] : []),
885
- ...(extraPlans.length
886
- ? [`Planned ${extraPlans.length + 1} projects: ${[project, ...extraProjects].join(", ")}.`]
887
- : []),
888
- ...wouldRefuse.map((t) => `team "${t.name}" WOULD BE REFUSED (${t.placement?.code}): ${t.placement?.message}`),
889
- ],
890
- });
891
- if (wouldRefuse.length > 0)
892
- process.exitCode = 1;
893
- return;
894
- }
895
- // 2. Apply (provision infra, register pools/systems/roles, materialize and
896
- // place declared teams).
897
- const applyResp = await applyProject(project, applyBody(config, credentials));
898
- handleApiError("up", applyResp);
899
- const data = applyResp.data;
900
- const deployment = data.deployment;
901
- const needsAction = (deployment?.resources ?? []).filter((r) => r.status === "needs_action" || r.status === "failed");
902
- // The other declared projects, applied the same way and reported by name.
903
- // Sequential on purpose: two applies of one organization touch the same
904
- // brains and the same business programs, which the control plane serializes
905
- // per company anyway.
906
- const extraResults = [];
907
- for (const extra of extraProjects) {
908
- const resp = await applyProject(extra, applyBody(config, credentials));
909
- handleApiError("up", resp);
910
- const extraData = resp.data;
911
- extraResults.push({
912
- project: extra,
913
- status: extraData.deployment?.status,
914
- resources: extraData.deployment?.resources,
915
- });
916
- }
917
- // ── WHAT WAS PROVISIONED, AND WHAT WAS REFUSED AND WHY ────────────────────
918
- // A declared team that produced nothing used to be indistinguishable from one
919
- // that worked: `naive up` printed `applied: true` and moved on. Every declared
920
- // team now appears here with the agents it materialized, and a durable team
921
- // additionally with its placement — placed, converged, or refused with the
922
- // reason. A refusal exits NON-ZERO: the apply did real work (infra, pools,
923
- // hermes teams) and reporting that honestly means not also claiming the part
924
- // that was refused succeeded.
925
- const durableByTeam = new Map((data.teams ?? []).map((t) => [t.team, t]));
926
- const hermesByTeam = new Map((data.staffing ?? []).map((s) => [s.system, s]));
927
- const teams = teamPlan.map((t) => {
928
- const durable = durableByTeam.get(t.name);
929
- const hermes = hermesByTeam.get(t.name);
930
- // BOTH LANES, not the first one that answered. A MIXED team — a hermes lead
931
- // with a member pinned to `runtime.durable` — is reported by BOTH
932
- // materializers, and `durable ?? hermes` printed one half and silently
933
- // dropped the other, so the agents an apply created did not add up to the
934
- // agents it reported. Each role is tagged with the runtime it landed on when
935
- // the team has two, so "which lane is this agent in" is answerable from the
936
- // output rather than by reading the config back.
937
- const lanes = t.role_runtimes;
938
- const label = (role, status) => lanes?.[role] ? `${role} (${status}, ${lanes[role]})` : `${role} (${status})`;
939
- const agents = [...(hermes?.agents ?? []), ...(durable?.agents ?? [])].map((a) => label(a.role, a.provision_status));
940
- const loops = projectLoops(hermes, durable);
941
- return {
942
- name: t.name,
943
- runtime: t.runtime,
944
- lead: t.lead_role,
945
- ...(lanes ? { role_runtimes: lanes } : {}),
946
- ...(durable?.subject ? { subject: durable.subject } : {}),
947
- agents,
948
- ...(loops.length ? { loops } : {}),
949
- ...(durable?.loops_unscheduled?.length ? { loops_unscheduled: durable.loops_unscheduled } : {}),
950
- // The trigger half, carried WHOLE rather than counted. Each of these is
951
- // a fact an operator may have to act on: a blocked trigger needs a
952
- // manifest bound, a retained schedule needs a human decision, and a
953
- // cancelled one is work this apply already did.
954
- ...(durable?.triggers?.length ? { triggers: durable.triggers } : {}),
955
- ...(durable?.triggers_unscheduled?.length ? { triggers_unscheduled: durable.triggers_unscheduled } : {}),
956
- ...(durable?.schedules_retained?.length ? { schedules_retained: durable.schedules_retained } : {}),
957
- // The authority the config did not declare — see {@link grantHints}.
958
- ...(durable?.manifest_grants?.length ? { manifest_grants: durable.manifest_grants } : {}),
959
- archived: [...(hermes?.archived ?? []), ...(durable?.archived ?? [])],
960
- ...(durable?.placement
961
- ? {
962
- placement: {
963
- status: durable.placement.status,
964
- ...(durable.placement.code ? { code: durable.placement.code } : {}),
965
- ...(durable.placement.message ? { reason: durable.placement.message } : {}),
966
- },
967
- }
968
- : {}),
969
- // A SIBLING OF `placement`, NEVER NESTED INSIDE IT — see
970
- // `projectManifest`, which is where the reason is written down.
971
- ...projectManifest(durable),
972
- };
973
- });
974
- const refusedTeams = teams.filter((t) => t.placement?.status === "refused");
975
- const manifests = manifestReport(teams);
976
- const brains = brainReport(data.brains);
977
- // Deploy cloud.web source to the provisioned web project(s) (Vercel builds + hosts).
978
- const webDeploys = {};
979
- if (opts.deploy !== false) {
980
- // Resolve cloud.web resources from the new nested `infrastructure.cloud`
981
- // shape, falling back to the legacy flat `infrastructure` record.
982
- const infraRaw = config.infrastructure ?? {};
983
- const nestedCloud = infraRaw.cloud;
984
- const cloudResources = (nestedCloud && typeof nestedCloud === "object" && !nestedCloud.kind)
985
- ? nestedCloud
986
- : infraRaw;
987
- for (const r of deployment?.resources ?? []) {
988
- if (r.kind !== "cloud.web" || r.status !== "ready")
989
- continue;
990
- const dir = join(process.cwd(), cloudResources[r.name]?.dir ?? ".");
991
- try {
992
- const files = collectWebFiles(dir);
993
- const dep = await apiRequest("POST", `/v1/deployments/${encodeURIComponent(project)}/web/${encodeURIComponent(r.name)}/deploy`, { files });
994
- const url = dep.data?.url;
995
- if (dep.status >= 200 && dep.status < 300 && url)
996
- webDeploys[r.name] = url;
997
- else
998
- webDeploys[r.name] = `deploy failed (${dep.status})`;
999
- }
1000
- catch (e) {
1001
- webDeploys[r.name] = `deploy error: ${e instanceof Error ? e.message : String(e)}`;
1002
- }
1003
- }
1004
- }
1005
- agentOutput({
1006
- action: "up",
1007
- result: {
1008
- config: found,
1009
- project,
1010
- status: deployment?.status,
1011
- resources: deployment?.resources,
1012
- runtime_pools: Object.keys(config.runtime ?? {}),
1013
- systems: Object.keys(config.systems ?? {}),
1014
- ...(brains ? { brains } : {}),
1015
- agents: Object.keys(config.agents ?? config.agentProfiles ?? config.templates ?? {}),
1016
- teams,
1017
- outputs: deployment?.outputs ?? {},
1018
- web_deployments: webDeploys,
1019
- ...(extraResults.length ? { projects: extraResults } : {}),
1020
- applied: true,
1021
- },
1022
- next_steps: [
1023
- ...needsAction.map((r) => ({ command: `naive status`, description: `${r.name}: ${r.requirement ?? r.status}` })),
1024
- ...refusedTeams.map(refusalNextStep).filter((s) => s !== null),
1025
- // The repair for a team that was placed and never bound. See
1026
- // `manifestReport` for why this is emitted only for
1027
- // `delivered_not_applied` and why `--tenant` is spelled out.
1028
- ...manifests.nextSteps,
1029
- { command: `naive agent-profiles provision <name> --user <tenant>`, description: "Provision an agent profile for a tenant" },
1030
- ],
1031
- hints: [
1032
- `Deployment "${project}" is ${deployment?.status ?? "?"}.`,
1033
- ...teams.flatMap(loopHints),
1034
- // IMMEDIATELY ALONGSIDE THE LOOP LINES, because they are the same kind
1035
- // of fact: a standing timer this apply either created, refused, or found
1036
- // and left alone. Separating them by half a screen would make "what
1037
- // recurs on this team after this apply" two reads instead of one.
1038
- ...teams.flatMap(triggerHints),
1039
- // BESIDE THE MANIFEST LINES, not the schedule ones: this is about the
1040
- // authority surface a digest carries, and the operator reading it is
1041
- // about to run `naive teams apply … --expected-digest …`.
1042
- ...teams.flatMap(grantHints),
1043
- ...unconsumedWarnings(teamPlan),
1044
- ...(legacyDeprecationNotice(config) ? [legacyDeprecationNotice(config)] : []),
1045
- ...(agentsRenameNotice(config) ? [agentsRenameNotice(config)] : []),
1046
- ...extraResults.map((r) => `Deployment "${r.project}" is ${r.status ?? "?"}.`),
1047
- needsAction.length ? `${needsAction.length} resource(s) need action — see next_steps.` : "All declared infrastructure is ready.",
1048
- ...(brains
1049
- ? [
1050
- `brains (${brains.declared_in}): ${brains.created.length} created` +
1051
- `${brains.created.length ? ` (${brains.created.join(", ")})` : ""}, ` +
1052
- `${brains.converged.length} already present` +
1053
- `${brains.converged.length ? ` (${brains.converged.join(", ")})` : ""}` +
1054
- `${brains.default ? `; default is "${brains.default}"` : ""}` +
1055
- `${brains.retained.length ? `. RETAINED, not declared by this config: ${brains.retained.join(", ")} — remove one deliberately with \`naive brain rm\`.` : "."}`,
1056
- // The accept-and-drop list, on the APPLY path and not only in --plan.
1057
- ...(brains.unpersisted
1058
- ? [
1059
- "DECLARED BUT NOT STORED — " +
1060
- Object.entries(brains.unpersisted)
1061
- .map(([name, keys]) => `${name}: ${keys.join(", ")}`)
1062
- .join("; ") +
1063
- ". `brain_knowledge_bases` has no column for these, so they were accepted and dropped. " +
1064
- "Nothing reads them; do not treat them as in force.",
1065
- ]
1066
- : []),
1067
- ...(brains.notes ?? []),
1068
- ]
1069
- : []),
1070
- ...teams
1071
- .filter((t) => t.placement?.status === "placed" || t.placement?.status === "converged")
1072
- .map((t) => t.placement?.status === "placed"
1073
- ? `team "${t.name}" PLACED on the durable runtime (${t.agents.length} agent(s)). Its jurisdiction is fixed permanently by the first control request.`
1074
- : `team "${t.name}" was already on the durable runtime (${t.agents.length} agent(s)); nothing was rewritten.`),
1075
- ...refusedTeams.map((t) => `team "${t.name}" REFUSED (${t.placement?.code}): ${t.placement?.reason}`),
1076
- // Placement and delivery are reported separately because they fail
1077
- // separately: a PLACED team can still be inert. See `manifestReport`.
1078
- ...manifests.hints,
1079
- // Said once, not per team, exactly as the placement paragraph below is.
1080
- // See {@link unbindableNote} for what it now says and what it used to.
1081
- ...unbindableNote(manifests),
1082
- // Said once, not per team. `naive runtime migrate` does not exist in this
1083
- // CLI or any released one — see refusalNextStep — so a refusal with no
1084
- // next_step gets the reason it has none instead of a command that 404s.
1085
- ...(refusedTeams.some((t) => refusalNextStep(t) === null)
1086
- ? [
1087
- "No `naive` command moves an existing tenant between runtimes, on this or any released version. " +
1088
- "Placement rewrites sidecar_url and container_auth_token and nulls instance_id with nothing stashed, " +
1089
- "so the control plane refuses a config edit that would do it. Read the refusal above and, if the move " +
1090
- "is intended, take it to an operator with direct access — there is no self-serve path.",
1091
- ]
1092
- : []),
1093
- ],
1094
- });
1095
- // The apply did real work and said so; it must not ALSO report success for
1096
- // the teams it refused, NOR for the teams it placed and left inert. Both are
1097
- // "the apply ran and part of it did not take", which is exit 1 on this
1098
- // command's ladder — 3 is reserved for "the config could not be evaluated at
1099
- // all, so nothing was even planned", and here the infra, the pools and the
1100
- // hermes teams really were provisioned. Non-zero, so a CI pipeline stops.
1101
- if (refusedTeams.length > 0 || manifests.unbound.length > 0)
1102
- process.exitCode = 1;
1103
- });
1104
- export const downCmd = new Command("down")
1105
- .description("Tear down the infrastructure provisioned for this project (naive.config.ts)")
1106
- .option("--yes", "Confirm the destructive teardown (required to actually destroy)")
1107
- .action(async (opts) => {
1108
- const found = findConfig();
1109
- if (!found) {
1110
- agentError({ action: "down", error: { code: "not_found", message: "No naive.config.ts found in the current directory" }, recovery_steps: [] });
1111
- return;
1112
- }
1113
- const source = readFileSync(found, "utf8");
1114
- const project = /project\s*:\s*["'`]([^"'`]+)["'`]/.exec(source)?.[1];
1115
- if (!project) {
1116
- agentError({ action: "down", error: { code: "invalid_input", message: "Could not read `project` from naive.config.ts" }, recovery_steps: [] });
1117
- return;
1118
- }
1119
- // Without --yes: preview what would be destroyed (no side effects).
1120
- if (!opts.yes) {
1121
- const current = await apiRequest("GET", `/v1/deployments/${encodeURIComponent(project)}`);
1122
- handleApiError("down", current);
1123
- const d = current.data ?? {};
1124
- agentOutput({
1125
- action: "down",
1126
- result: {
1127
- project,
1128
- applied: false,
1129
- would_destroy: {
1130
- resources: (d.resources ?? []).map((r) => `${r.kind}:${r.name}`),
1131
- runtime_pools: (d.runtime_pools ?? []).map((p) => p.name),
1132
- systems: (d.systems ?? []).map((s) => s.name),
1133
- },
1134
- },
1135
- next_steps: [{ command: "naive down --yes", description: "Confirm and permanently destroy this infrastructure" }],
1136
- hints: [`This will DROP databases, delete buckets, and remove web/pools/systems for "${project}". Irreversible. Re-run with --yes to proceed.`],
1137
- });
1138
- return;
1139
- }
1140
- const resp = await apiRequest("DELETE", `/v1/deployments/${encodeURIComponent(project)}?confirm=true`);
1141
- handleApiError("down", resp);
1142
- agentOutput({
1143
- action: "down",
1144
- result: { project, applied: true, ...resp.data },
1145
- next_steps: [{ command: "naive up", description: "Re-provision the project's infrastructure" }],
1146
- hints: [`Tore down infrastructure for "${project}".`],
1147
- });
1148
- });
1149
- //# sourceMappingURL=up.js.map