@usenaive-sdk/cli 0.12.2 → 0.13.0

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 (193) hide show
  1. package/README.md +76 -5
  2. package/dist/client.d.ts +74 -1
  3. package/dist/client.js +351 -17
  4. package/dist/client.js.map +1 -1
  5. package/dist/commands/aeo.js +69 -27
  6. package/dist/commands/aeo.js.map +1 -1
  7. package/dist/commands/app-data.js +29 -11
  8. package/dist/commands/app-data.js.map +1 -1
  9. package/dist/commands/apps.d.ts +7 -0
  10. package/dist/commands/apps.js +255 -18
  11. package/dist/commands/apps.js.map +1 -1
  12. package/dist/commands/audio.d.ts +9 -0
  13. package/dist/commands/audio.js +391 -0
  14. package/dist/commands/audio.js.map +1 -0
  15. package/dist/commands/auth.d.ts +31 -0
  16. package/dist/commands/auth.js +403 -0
  17. package/dist/commands/auth.js.map +1 -0
  18. package/dist/commands/billing.js +12 -12
  19. package/dist/commands/billing.js.map +1 -1
  20. package/dist/commands/brain.d.ts +2 -0
  21. package/dist/commands/brain.js +1255 -0
  22. package/dist/commands/brain.js.map +1 -0
  23. package/dist/commands/browser.js +35 -8
  24. package/dist/commands/browser.js.map +1 -1
  25. package/dist/commands/business.js +117 -16
  26. package/dist/commands/business.js.map +1 -1
  27. package/dist/commands/clone.d.ts +2 -0
  28. package/dist/commands/clone.js +203 -0
  29. package/dist/commands/clone.js.map +1 -0
  30. package/dist/commands/companies.d.ts +8 -0
  31. package/dist/commands/companies.js +137 -47
  32. package/dist/commands/companies.js.map +1 -1
  33. package/dist/commands/company-data.d.ts +2 -0
  34. package/dist/commands/company-data.js +168 -0
  35. package/dist/commands/company-data.js.map +1 -0
  36. package/dist/commands/compute.js +10 -3
  37. package/dist/commands/compute.js.map +1 -1
  38. package/dist/commands/config.d.ts +2 -0
  39. package/dist/commands/config.js +36 -0
  40. package/dist/commands/config.js.map +1 -0
  41. package/dist/commands/connections.d.ts +6 -0
  42. package/dist/commands/connections.js +29 -1
  43. package/dist/commands/connections.js.map +1 -1
  44. package/dist/commands/domains.js +1 -1
  45. package/dist/commands/ecommerce.js +19 -9
  46. package/dist/commands/ecommerce.js.map +1 -1
  47. package/dist/commands/email.js +56 -9
  48. package/dist/commands/email.js.map +1 -1
  49. package/dist/commands/env.d.ts +3 -3
  50. package/dist/commands/env.js +68 -4
  51. package/dist/commands/env.js.map +1 -1
  52. package/dist/commands/formation.js +3 -3
  53. package/dist/commands/identity.js +4 -3
  54. package/dist/commands/identity.js.map +1 -1
  55. package/dist/commands/images.js +87 -13
  56. package/dist/commands/images.js.map +1 -1
  57. package/dist/commands/init.d.ts +3 -0
  58. package/dist/commands/init.js +39 -36
  59. package/dist/commands/init.js.map +1 -1
  60. package/dist/commands/jobs.js +1 -0
  61. package/dist/commands/jobs.js.map +1 -1
  62. package/dist/commands/keys.js +26 -3
  63. package/dist/commands/keys.js.map +1 -1
  64. package/dist/commands/legacy-auth.d.ts +27 -0
  65. package/dist/commands/legacy-auth.js +64 -0
  66. package/dist/commands/legacy-auth.js.map +1 -0
  67. package/dist/commands/llm.js +13 -0
  68. package/dist/commands/llm.js.map +1 -1
  69. package/dist/commands/login.js +2 -0
  70. package/dist/commands/login.js.map +1 -1
  71. package/dist/commands/loops.d.ts +2 -0
  72. package/dist/commands/loops.js +260 -0
  73. package/dist/commands/loops.js.map +1 -0
  74. package/dist/commands/media.js +3 -3
  75. package/dist/commands/media.js.map +1 -1
  76. package/dist/commands/mobile.d.ts +4 -0
  77. package/dist/commands/mobile.js +382 -0
  78. package/dist/commands/mobile.js.map +1 -0
  79. package/dist/commands/payments.d.ts +2 -0
  80. package/dist/commands/payments.js +131 -0
  81. package/dist/commands/payments.js.map +1 -0
  82. package/dist/commands/people.d.ts +2 -0
  83. package/dist/commands/people.js +193 -0
  84. package/dist/commands/people.js.map +1 -0
  85. package/dist/commands/phone.js +3 -3
  86. package/dist/commands/phone.js.map +1 -1
  87. package/dist/commands/projects.d.ts +13 -0
  88. package/dist/commands/projects.js +220 -0
  89. package/dist/commands/projects.js.map +1 -0
  90. package/dist/commands/register.js +16 -8
  91. package/dist/commands/register.js.map +1 -1
  92. package/dist/commands/sandbox.d.ts +2 -0
  93. package/dist/commands/sandbox.js +245 -0
  94. package/dist/commands/sandbox.js.map +1 -0
  95. package/dist/commands/search.js +26 -12
  96. package/dist/commands/search.js.map +1 -1
  97. package/dist/commands/seo.js +16 -8
  98. package/dist/commands/seo.js.map +1 -1
  99. package/dist/commands/sessions.d.ts +6 -0
  100. package/dist/commands/sessions.js +109 -0
  101. package/dist/commands/sessions.js.map +1 -0
  102. package/dist/commands/social-data.d.ts +2 -0
  103. package/dist/commands/social-data.js +244 -0
  104. package/dist/commands/social-data.js.map +1 -0
  105. package/dist/commands/social.js +8 -6
  106. package/dist/commands/social.js.map +1 -1
  107. package/dist/commands/teams.d.ts +69 -0
  108. package/dist/commands/teams.js +503 -0
  109. package/dist/commands/teams.js.map +1 -0
  110. package/dist/commands/up.d.ts +150 -0
  111. package/dist/commands/up.js +415 -15
  112. package/dist/commands/up.js.map +1 -1
  113. package/dist/commands/usage.js +15 -4
  114. package/dist/commands/usage.js.map +1 -1
  115. package/dist/commands/use.d.ts +9 -0
  116. package/dist/commands/use.js +76 -11
  117. package/dist/commands/use.js.map +1 -1
  118. package/dist/commands/users.d.ts +8 -0
  119. package/dist/commands/users.js +28 -9
  120. package/dist/commands/users.js.map +1 -1
  121. package/dist/commands/vault.js +28 -1
  122. package/dist/commands/vault.js.map +1 -1
  123. package/dist/commands/video.js +190 -270
  124. package/dist/commands/video.js.map +1 -1
  125. package/dist/commands/voice.d.ts +2 -0
  126. package/dist/commands/voice.js +516 -0
  127. package/dist/commands/voice.js.map +1 -0
  128. package/dist/commands/wallet.d.ts +2 -0
  129. package/dist/commands/wallet.js +244 -0
  130. package/dist/commands/wallet.js.map +1 -0
  131. package/dist/commands/whoami.js +17 -1
  132. package/dist/commands/whoami.js.map +1 -1
  133. package/dist/config.d.ts +24 -1
  134. package/dist/config.js +36 -7
  135. package/dist/config.js.map +1 -1
  136. package/dist/index.d.ts +2 -1
  137. package/dist/index.js +290 -20
  138. package/dist/index.js.map +1 -1
  139. package/dist/lib/brain-select.d.ts +193 -0
  140. package/dist/lib/brain-select.js +403 -0
  141. package/dist/lib/brain-select.js.map +1 -0
  142. package/dist/lib/cli-validation.d.ts +12 -0
  143. package/dist/lib/cli-validation.js +46 -0
  144. package/dist/lib/cli-validation.js.map +1 -0
  145. package/dist/lib/deprecation.d.ts +158 -0
  146. package/dist/lib/deprecation.js +140 -0
  147. package/dist/lib/deprecation.js.map +1 -0
  148. package/dist/lib/duration.d.ts +8 -0
  149. package/dist/lib/duration.js +11 -0
  150. package/dist/lib/duration.js.map +1 -0
  151. package/dist/lib/http-response.d.ts +11 -0
  152. package/dist/lib/http-response.js +96 -0
  153. package/dist/lib/http-response.js.map +1 -0
  154. package/dist/lib/pricing.d.ts +132 -0
  155. package/dist/lib/pricing.js +148 -0
  156. package/dist/lib/pricing.js.map +1 -0
  157. package/dist/lib/stream-frames.d.ts +114 -0
  158. package/dist/lib/stream-frames.js +169 -0
  159. package/dist/lib/stream-frames.js.map +1 -0
  160. package/dist/lib/teams-catalog.d.ts +141 -0
  161. package/dist/lib/teams-catalog.js +765 -0
  162. package/dist/lib/teams-catalog.js.map +1 -0
  163. package/dist/lib/unknown-command.d.ts +57 -0
  164. package/dist/lib/unknown-command.js +93 -0
  165. package/dist/lib/unknown-command.js.map +1 -0
  166. package/dist/lib/version-check.d.ts +26 -0
  167. package/dist/lib/version-check.js +21 -0
  168. package/dist/lib/version-check.js.map +1 -0
  169. package/dist/output.d.ts +119 -10
  170. package/dist/output.js +309 -15
  171. package/dist/output.js.map +1 -1
  172. package/dist/prompt.d.ts +17 -0
  173. package/dist/prompt.js +70 -0
  174. package/dist/prompt.js.map +1 -0
  175. package/package.json +11 -6
  176. package/dist/commands/billing-client.d.ts +0 -2
  177. package/dist/commands/billing-client.js +0 -271
  178. package/dist/commands/billing-client.js.map +0 -1
  179. package/dist/commands/crm-app.d.ts +0 -2
  180. package/dist/commands/crm-app.js +0 -306
  181. package/dist/commands/crm-app.js.map +0 -1
  182. package/dist/commands/crm.d.ts +0 -2
  183. package/dist/commands/crm.js +0 -319
  184. package/dist/commands/crm.js.map +0 -1
  185. package/dist/commands/integrations.d.ts +0 -2
  186. package/dist/commands/integrations.js +0 -199
  187. package/dist/commands/integrations.js.map +0 -1
  188. package/dist/commands/link.d.ts +0 -3
  189. package/dist/commands/link.js +0 -91
  190. package/dist/commands/link.js.map +0 -1
  191. package/dist/commands/operators.d.ts +0 -11
  192. package/dist/commands/operators.js +0 -120
  193. package/dist/commands/operators.js.map +0 -1
@@ -1,3 +1,153 @@
1
1
  import { Command } from "commander";
2
+ import { type NextStep } from "../output.js";
3
+ /** What the API reports about the brains `company.brains` declared. */
4
+ interface BrainsResult {
5
+ declared_in?: string | null;
6
+ created?: Array<{
7
+ name: string;
8
+ }>;
9
+ converged?: Array<{
10
+ name: string;
11
+ }>;
12
+ default?: {
13
+ name?: string | null;
14
+ changed?: boolean;
15
+ };
16
+ retained_undeclared?: string[];
17
+ /**
18
+ * Declared brain policy with no column to land in, per brain.
19
+ * `brain_knowledge_bases` has columns for name, is_default, scope, status and
20
+ * provider ids and nothing else, so `retention`, `partitions`, `writes`,
21
+ * `recall`, `lanes`, `visibility` and `bounds` are accepted and not stored.
22
+ */
23
+ unpersisted?: Record<string, string[]>;
24
+ /** The server's own prose for the same, plus the retained-undeclared note. */
25
+ notes?: string[];
26
+ }
27
+ /**
28
+ * The brain half of the report, or undefined when the config declared none.
29
+ *
30
+ * Exported for the same reason it exists: `naive up` printed `applied: true` and
31
+ * said NOTHING about brains, so a config that declared two of them looked
32
+ * identical to a config that declared none. `--plan` showed them (it prints the
33
+ * whole plan); apply did not. A provisioned thing that is not reported is the
34
+ * same silence as an unprovisioned one, one level up.
35
+ *
36
+ * 🔴 AND `unpersisted` IS PART OF THE REPORT, NOT AN EXTRA. The apply path
37
+ * computed the accept-and-drop list and this projection threw it away, so
38
+ * `naive up --plan` told the user which declared brain keys have no column and
39
+ * plain `naive up` — the command people actually run — did not. That is the same
40
+ * defect one level down: `packages/api/src/services/deployment-brains.ts`'s own
41
+ * header calls accept-and-drop "the exact defect this file exists to remove", and
42
+ * a report only the dry run prints is a report most operators never see.
43
+ */
44
+ export declare function brainReport(brains: BrainsResult | undefined): {
45
+ declared_in: string | null;
46
+ created: string[];
47
+ converged: string[];
48
+ default: string | null;
49
+ retained: string[];
50
+ unpersisted?: Record<string, string[]>;
51
+ notes?: string[];
52
+ } | undefined;
53
+ /** What the plan reports per declared team, whichever runtime it asked for. */
54
+ interface TeamPlanEntry {
55
+ name: string;
56
+ runtime: string;
57
+ roles?: string[];
58
+ lead_role?: string;
59
+ /** Role → runtime, present only for a MIXED team. See `role_runtimes` on the API. */
60
+ role_runtimes?: Record<string, string>;
61
+ placement?: {
62
+ status: string;
63
+ code?: string;
64
+ message?: string;
65
+ };
66
+ /** Keys this team declared that NOTHING on the control plane reads. */
67
+ unconsumed?: string[];
68
+ }
69
+ /**
70
+ * The per-team accept-and-drop warning, one line per team that declared any.
71
+ *
72
+ * The data has been on the wire the whole time (`plan.teams[].unconsumed`,
73
+ * computed by `unconsumedTeamKeys` in packages/api) and `naive up` printed none
74
+ * of it — `team({ governance: {…} })` applied green and enforced nothing, with
75
+ * nothing anywhere for the author to notice. Printed on the PLAN and on the
76
+ * APPLY, because a report only the dry run shows is a report most operators
77
+ * never see.
78
+ */
79
+ export declare function unconsumedWarnings(teams: readonly TeamPlanEntry[]): string[];
80
+ /**
81
+ * ONE deprecation line when the config still uses the legacy ECS surface —
82
+ * `runtime.pool()` declarations, an agent-level `runtime: "<pool>"` string, or
83
+ * `systems:`. All three keep working indefinitely; the line names what was
84
+ * found and where new configs should author instead.
85
+ */
86
+ export declare function legacyDeprecationNotice(config: Record<string, unknown>): string | null;
87
+ /**
88
+ * ONE rename line when the config declares `agents:`. Separate from the legacy
89
+ * line above on purpose: `runtime.pool()` / `systems:` are a deprecated
90
+ * SURFACE, while `agents:` is a deprecated SPELLING — the block compiles to
91
+ * byte-for-byte what `kits:` compiles to, so the honest notice is "rename",
92
+ * not "migrate". A notice, never a refusal, on both entrypoints.
93
+ */
94
+ export declare function agentsRenameNotice(config: Record<string, unknown>): string | null;
95
+ /**
96
+ * The operator credential the DURABLE RUNTIME issued, read from the ENVIRONMENT.
97
+ *
98
+ * Never from `naive.config.ts`, and that is the whole point rather than a style
99
+ * choice. The config is committed, so a credential in it is a credential in git —
100
+ * and, more importantly, the config is the one thing that must not by itself be
101
+ * able to change where a tenant runs. Pointing a tenant at the durable runtime
102
+ * overwrites its `sidecar_url` and `container_auth_token` and nulls its
103
+ * `instance_id`, none of which is stashed anywhere. Requiring a second, separate
104
+ * act means an edit alone can never do it.
105
+ *
106
+ * `NAIVE_DURABLE_CREDENTIAL_<TEAM>` names one team; `NAIVE_DURABLE_CREDENTIAL`
107
+ * covers them all. Neither is ever printed, and the API never echoes it back.
108
+ */
109
+ export declare function durableCredentialEnvVar(team: string): string;
110
+ /**
111
+ * The recovery for ONE refused team — a command that EXISTS, or none.
112
+ *
113
+ * 🔴 `naive runtime migrate` IS NOT A COMMAND, AND THIS USED TO PRINT IT.
114
+ * Every refusal here used to emit
115
+ * `naive runtime migrate --tenant … --to durable --credential-from <file>`.
116
+ * Measured on the built CLI: `naive runtime migrate --tenant <id> --to hermes`
117
+ * answers
118
+ * `{"code":"cli_outdated","message":"Unknown command 'runtime'. Your naive CLI
119
+ * is v0.12.4; this command may exist in a newer version."}` — and it exists in
120
+ * NO version: `packages/cli/src/index.ts` registers no `runtime` group at all.
121
+ * So an operator whose apply was refused was handed a command that tells them to
122
+ * upgrade, forever. A next_step that cannot be run is worse than none, because it
123
+ * ends the search.
124
+ *
125
+ * The two cases are genuinely different and are answered differently:
126
+ *
127
+ * · `runtime_credential_required` — there IS a procedure, it works today, and
128
+ * it is the one this command already implements: put the operator credential
129
+ * the durable runtime issued in the environment and re-run. Never in
130
+ * `naive.config.ts`, which is committed.
131
+ * · everything else (`runtime_migration_requires_explicit_opt_in`,
132
+ * `runtime_claim_in_flight`, `runtime_placement_lost_race`,
133
+ * `runtime_not_configured`, `runtime_state_unknown`) — there is NO self-serve
134
+ * command on this build. Moving an existing tenant between runtimes
135
+ * overwrites `sidecar_url` and `container_auth_token` and nulls `instance_id`
136
+ * with nothing stashed, so the control plane refuses it and no client can do
137
+ * it either. Say that, and print the server's own message, rather than
138
+ * inventing a command to carry the blame.
139
+ *
140
+ * The API writes the same vapour into `details.migrate_command`
141
+ * (packages/api/src/services/placement.ts MIGRATE_COMMAND). That string is not
142
+ * this package's to change and is reported as a follow-up; this function is why
143
+ * `naive up` no longer repeats it.
144
+ */
145
+ export declare function refusalNextStep(t: {
146
+ name: string;
147
+ placement?: {
148
+ code?: string;
149
+ };
150
+ }): NextStep | null;
2
151
  export declare const upCmd: Command;
3
152
  export declare const downCmd: Command;
153
+ export {};
@@ -32,25 +32,63 @@ function collectWebFiles(root) {
32
32
  return out;
33
33
  }
34
34
  const CONFIG_CANDIDATES = ["naive.config.ts", "naive.config.js", "naive.config.mjs", "naive.config.json"];
35
+ /**
36
+ * A module-resolution failure, as opposed to the config refusing itself.
37
+ *
38
+ * `ERR_MODULE_NOT_FOUND` / `MODULE_NOT_FOUND` is what an absent dependency
39
+ * raises. Matched on the CODE rather than the message, because a define-time
40
+ * refusal is free to mention a package name in its prose and must not be
41
+ * misread as a missing install.
42
+ */
43
+ function isMissingModule(e) {
44
+ const code = e?.code;
45
+ return code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND";
46
+ }
35
47
  /** Evaluate naive.config.{ts,js,json} to its full object (the whole NaiveConfig). */
36
48
  async function loadConfig(path) {
37
49
  if (path.endsWith(".json")) {
38
50
  try {
39
- return JSON.parse(readFileSync(path, "utf8"));
51
+ return { ok: true, config: JSON.parse(readFileSync(path, "utf8")) };
40
52
  }
41
- catch {
42
- return null;
53
+ catch (e) {
54
+ // A committed .json needs no toolchain, so a parse failure is only ever
55
+ // the file's own fault.
56
+ return { ok: false, reason: "invalid", message: e instanceof Error ? e.message : String(e) };
43
57
  }
44
58
  }
59
+ let jiti;
45
60
  try {
46
61
  const { createJiti } = await import("jiti");
47
- const jiti = createJiti(pathToFileURL(path).href);
62
+ jiti = createJiti(pathToFileURL(path).href);
63
+ }
64
+ catch (e) {
65
+ return {
66
+ ok: false,
67
+ reason: "unloadable",
68
+ message: e instanceof Error ? e.message : String(e),
69
+ };
70
+ }
71
+ try {
48
72
  const mod = (await jiti.import(path));
49
73
  const cfg = mod?.default ?? mod;
50
- return cfg && typeof cfg === "object" ? cfg : null;
74
+ if (!cfg || typeof cfg !== "object") {
75
+ return {
76
+ ok: false,
77
+ reason: "invalid",
78
+ message: `${path} evaluated to ${cfg === undefined ? "undefined" : typeof cfg}. It must \`export default defineConfig({...})\`.`,
79
+ };
80
+ }
81
+ return { ok: true, config: cfg };
51
82
  }
52
- catch {
53
- return null; // jiti/@usenaive-sdk/iac not resolvable — fall back to regex extraction
83
+ catch (e) {
84
+ // The config itself threw. Almost always a NAMED define-time refusal, whose
85
+ // message is written to be read by the person who wrote the config — so it
86
+ // is surfaced verbatim rather than replaced with a guess.
87
+ return {
88
+ ok: false,
89
+ reason: isMissingModule(e) ? "unloadable" : "invalid",
90
+ message: e instanceof Error ? e.message : String(e),
91
+ };
54
92
  }
55
93
  }
56
94
  /** Extract top-level keys inside a named object block, e.g. agentProfiles: { a, b }. */
@@ -92,6 +130,183 @@ function extractKeys(source, section) {
92
130
  function findConfig() {
93
131
  return CONFIG_CANDIDATES.map((c) => join(process.cwd(), c)).find(existsSync) ?? null;
94
132
  }
133
+ /**
134
+ * The brain half of the report, or undefined when the config declared none.
135
+ *
136
+ * Exported for the same reason it exists: `naive up` printed `applied: true` and
137
+ * said NOTHING about brains, so a config that declared two of them looked
138
+ * identical to a config that declared none. `--plan` showed them (it prints the
139
+ * whole plan); apply did not. A provisioned thing that is not reported is the
140
+ * same silence as an unprovisioned one, one level up.
141
+ *
142
+ * 🔴 AND `unpersisted` IS PART OF THE REPORT, NOT AN EXTRA. The apply path
143
+ * computed the accept-and-drop list and this projection threw it away, so
144
+ * `naive up --plan` told the user which declared brain keys have no column and
145
+ * plain `naive up` — the command people actually run — did not. That is the same
146
+ * defect one level down: `packages/api/src/services/deployment-brains.ts`'s own
147
+ * header calls accept-and-drop "the exact defect this file exists to remove", and
148
+ * a report only the dry run prints is a report most operators never see.
149
+ */
150
+ export function brainReport(brains) {
151
+ if (!brains || !brains.declared_in)
152
+ return undefined;
153
+ const unpersisted = brains.unpersisted ?? {};
154
+ return {
155
+ declared_in: brains.declared_in,
156
+ created: (brains.created ?? []).map((b) => b.name),
157
+ converged: (brains.converged ?? []).map((b) => b.name),
158
+ default: brains.default?.name ?? null,
159
+ retained: brains.retained_undeclared ?? [],
160
+ ...(Object.keys(unpersisted).length > 0 ? { unpersisted } : {}),
161
+ ...(brains.notes?.length ? { notes: brains.notes } : {}),
162
+ };
163
+ }
164
+ /**
165
+ * The per-team accept-and-drop warning, one line per team that declared any.
166
+ *
167
+ * The data has been on the wire the whole time (`plan.teams[].unconsumed`,
168
+ * computed by `unconsumedTeamKeys` in packages/api) and `naive up` printed none
169
+ * of it — `team({ governance: {…} })` applied green and enforced nothing, with
170
+ * nothing anywhere for the author to notice. Printed on the PLAN and on the
171
+ * APPLY, because a report only the dry run shows is a report most operators
172
+ * never see.
173
+ */
174
+ export function unconsumedWarnings(teams) {
175
+ return teams
176
+ .filter((t) => (t.unconsumed ?? []).length > 0)
177
+ .map((t) => `⚠ team "${t.name}" DECLARED AND UNREAD: ${t.unconsumed.join(", ")} — carried to the ` +
178
+ "control plane and consumed by NOTHING. Do not treat these as in force. " +
179
+ "`defineProject` refuses them at define time; remove them, or keep the config on " +
180
+ "`defineConfig` (lenient) until a consumer lands.");
181
+ }
182
+ /**
183
+ * ONE deprecation line when the config still uses the legacy ECS surface —
184
+ * `runtime.pool()` declarations, an agent-level `runtime: "<pool>"` string, or
185
+ * `systems:`. All three keep working indefinitely; the line names what was
186
+ * found and where new configs should author instead.
187
+ */
188
+ export function legacyDeprecationNotice(config) {
189
+ const found = [];
190
+ const pools = Object.keys(config.runtime ?? {});
191
+ if (pools.length > 0)
192
+ found.push(`runtime.pool() declarations (${pools.join(", ")})`);
193
+ const pinned = Object.entries(config.agents ?? {})
194
+ .filter(([, a]) => typeof a?.runtime === "string")
195
+ .map(([name]) => name);
196
+ if (pinned.length > 0)
197
+ found.push(`agent-level runtime: "<pool>" strings (${pinned.join(", ")})`);
198
+ const systems = Object.keys(config.systems ?? {});
199
+ if (systems.length > 0)
200
+ found.push(`systems: (${systems.join(", ")})`);
201
+ if (found.length === 0)
202
+ return null;
203
+ return (`DEPRECATED surface in use — ${found.join("; ")}. These keep working indefinitely; new configs ` +
204
+ "author `defineProject` with `teams:` on `runtime.durable()` (or `runtime.hermes()` for the hosted lane).");
205
+ }
206
+ /**
207
+ * ONE rename line when the config declares `agents:`. Separate from the legacy
208
+ * line above on purpose: `runtime.pool()` / `systems:` are a deprecated
209
+ * SURFACE, while `agents:` is a deprecated SPELLING — the block compiles to
210
+ * byte-for-byte what `kits:` compiles to, so the honest notice is "rename",
211
+ * not "migrate". A notice, never a refusal, on both entrypoints.
212
+ */
213
+ export function agentsRenameNotice(config) {
214
+ const names = Object.keys(config.agents ?? {});
215
+ if (names.length === 0)
216
+ return null;
217
+ return (`DEPRECATED spelling in use — \`agents:\` (${names.join(", ")}). It keeps working indefinitely ` +
218
+ "and compiles to exactly what `kits:` compiles to; rename the block to `kits:` (and `agent()` " +
219
+ "entries to `kit()`) — an account kit is instantiated per child project via " +
220
+ "`naive.forProject(p).forChild(id).provision(\"<kit>\")`.");
221
+ }
222
+ /**
223
+ * The operator credential the DURABLE RUNTIME issued, read from the ENVIRONMENT.
224
+ *
225
+ * Never from `naive.config.ts`, and that is the whole point rather than a style
226
+ * choice. The config is committed, so a credential in it is a credential in git —
227
+ * and, more importantly, the config is the one thing that must not by itself be
228
+ * able to change where a tenant runs. Pointing a tenant at the durable runtime
229
+ * overwrites its `sidecar_url` and `container_auth_token` and nulls its
230
+ * `instance_id`, none of which is stashed anywhere. Requiring a second, separate
231
+ * act means an edit alone can never do it.
232
+ *
233
+ * `NAIVE_DURABLE_CREDENTIAL_<TEAM>` names one team; `NAIVE_DURABLE_CREDENTIAL`
234
+ * covers them all. Neither is ever printed, and the API never echoes it back.
235
+ */
236
+ export function durableCredentialEnvVar(team) {
237
+ return `NAIVE_DURABLE_CREDENTIAL_${team.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
238
+ }
239
+ function readDurableCredentials(teams) {
240
+ const out = {};
241
+ const all = process.env.NAIVE_DURABLE_CREDENTIAL;
242
+ if (all)
243
+ out["*"] = all;
244
+ for (const team of teams) {
245
+ const secret = process.env[durableCredentialEnvVar(team)];
246
+ if (secret)
247
+ out[team] = secret;
248
+ }
249
+ return Object.keys(out).length > 0 ? out : undefined;
250
+ }
251
+ /**
252
+ * The recovery for ONE refused team — a command that EXISTS, or none.
253
+ *
254
+ * 🔴 `naive runtime migrate` IS NOT A COMMAND, AND THIS USED TO PRINT IT.
255
+ * Every refusal here used to emit
256
+ * `naive runtime migrate --tenant … --to durable --credential-from <file>`.
257
+ * Measured on the built CLI: `naive runtime migrate --tenant <id> --to hermes`
258
+ * answers
259
+ * `{"code":"cli_outdated","message":"Unknown command 'runtime'. Your naive CLI
260
+ * is v0.12.4; this command may exist in a newer version."}` — and it exists in
261
+ * NO version: `packages/cli/src/index.ts` registers no `runtime` group at all.
262
+ * So an operator whose apply was refused was handed a command that tells them to
263
+ * upgrade, forever. A next_step that cannot be run is worse than none, because it
264
+ * ends the search.
265
+ *
266
+ * The two cases are genuinely different and are answered differently:
267
+ *
268
+ * · `runtime_credential_required` — there IS a procedure, it works today, and
269
+ * it is the one this command already implements: put the operator credential
270
+ * the durable runtime issued in the environment and re-run. Never in
271
+ * `naive.config.ts`, which is committed.
272
+ * · everything else (`runtime_migration_requires_explicit_opt_in`,
273
+ * `runtime_claim_in_flight`, `runtime_placement_lost_race`,
274
+ * `runtime_not_configured`, `runtime_state_unknown`) — there is NO self-serve
275
+ * command on this build. Moving an existing tenant between runtimes
276
+ * overwrites `sidecar_url` and `container_auth_token` and nulls `instance_id`
277
+ * with nothing stashed, so the control plane refuses it and no client can do
278
+ * it either. Say that, and print the server's own message, rather than
279
+ * inventing a command to carry the blame.
280
+ *
281
+ * The API writes the same vapour into `details.migrate_command`
282
+ * (packages/api/src/services/placement.ts MIGRATE_COMMAND). That string is not
283
+ * this package's to change and is reported as a follow-up; this function is why
284
+ * `naive up` no longer repeats it.
285
+ */
286
+ export function refusalNextStep(t) {
287
+ if (t.placement?.code === "runtime_credential_required") {
288
+ return {
289
+ command: `${durableCredentialEnvVar(t.name)}=<operator-credential> naive up`,
290
+ description: `team "${t.name}": the durable runtime issued an operator credential for this tenant and it is not in ` +
291
+ `the environment. Export it (or NAIVE_DURABLE_CREDENTIAL for every team) and re-run. Never put it in naive.config.ts.`,
292
+ };
293
+ }
294
+ return null;
295
+ }
296
+ /** Team names declared in the config, without evaluating anything the API will re-derive. */
297
+ function declaredTeamNames(config) {
298
+ const teams = config.teams;
299
+ return teams && typeof teams === "object" ? Object.keys(teams) : [];
300
+ }
301
+ /**
302
+ * Wrap the config only when there is something to carry alongside it.
303
+ *
304
+ * `readConfig` on the API takes `body.config ?? body`, so both shapes work — but
305
+ * a caller that supplies no credential sends the exact bytes it always sent.
306
+ */
307
+ function applyBody(config, credentials) {
308
+ return credentials ? { config, runtime_credentials: credentials } : config;
309
+ }
95
310
  export const upCmd = new Command("up")
96
311
  .description("Plan + apply the infrastructure, runtime pools, agent profiles, and systems declared in naive.config.ts")
97
312
  .option("--plan", "Preview the plan (diff) without applying")
@@ -108,9 +323,41 @@ export const upCmd = new Command("up")
108
323
  });
109
324
  return;
110
325
  }
111
- const config = opts.local ? null : await loadConfig(found);
112
- // Fallback: config couldn't be evaluated — local validation only.
113
- if (!config) {
326
+ const load = opts.local ? null : await loadConfig(found);
327
+ /* ── THE CONFIG REFUSED ITSELF. THAT IS NOT A TOOLCHAIN PROBLEM. ──────────
328
+ *
329
+ * 🔴 THIS BRANCH IS THE POINT OF THE `ConfigLoad` UNION ABOVE. Everything a
330
+ * define-time refusal is for arrives here: `brain_no_partitions`,
331
+ * `legacy_multi_team`, `brain_partition_unknown`, `residency_widens_company`,
332
+ * `company_brain_and_brains`, `program_undeclared`,
333
+ * `workspace_allowlist_empty`, `residency_unsupported_on_runtime`. Every one
334
+ * of those messages is written to be read by the person who wrote the config,
335
+ * and every one of them used to be swallowed by `catch { return null }`,
336
+ * relabelled "install @usenaive-sdk/iac + jiti", printed under
337
+ * `success: true`, and exited 0.
338
+ *
339
+ * So it is surfaced VERBATIM and exits non-zero. No regex fallback: the
340
+ * fallback's whole job is to say something useful when the config could not be
341
+ * READ, and this config was read perfectly — it said no.
342
+ */
343
+ if (load && !load.ok && load.reason === "invalid") {
344
+ agentError({
345
+ action: "up",
346
+ error: {
347
+ code: "invalid_config",
348
+ message: load.message,
349
+ hint: `${found} evaluated and refused itself. This is the config's own named refusal, not a missing ` +
350
+ "dependency — @usenaive-sdk/iac and jiti both resolved. Fix the declaration it names and re-run.",
351
+ },
352
+ recovery_steps: [
353
+ { command: "naive up --plan", description: "Re-check the config without applying anything" },
354
+ ],
355
+ });
356
+ return;
357
+ }
358
+ // Fallback: the TOOLCHAIN is missing (or --local was asked for), so the config
359
+ // could not be evaluated at all — local, regex-only validation.
360
+ if (!load || !load.ok) {
114
361
  const source = readFileSync(found, "utf8");
115
362
  const projectMatch = /project\s*:\s*["'`]([^"'`]+)["'`]/.exec(source);
116
363
  const project = projectMatch?.[1] ?? "(unknown)";
@@ -134,34 +381,135 @@ export const upCmd = new Command("up")
134
381
  opts.local
135
382
  ? `Local validation only for "${project}".`
136
383
  : `Validated ${found} locally, but could not evaluate it (install @usenaive-sdk/iac + jiti, or commit a naive.config.json).`,
384
+ // Only ever printed for a real resolution failure now — an invalid config
385
+ // took the branch above. Said out loud so the two are never conflated again.
386
+ ...(load ? [`Module resolution failed: ${load.message}`] : []),
137
387
  ],
138
388
  });
139
389
  return;
140
390
  }
391
+ const config = load.config;
141
392
  const project = typeof config.project === "string" ? config.project : undefined;
142
393
  if (!project) {
143
394
  agentError({ action: "up", error: { code: "invalid_input", message: "naive.config.ts must export a `project` name" }, recovery_steps: [] });
144
395
  return;
145
396
  }
397
+ const credentials = readDurableCredentials(declaredTeamNames(config));
398
+ // The organization's OTHER projects, if the config declares any. Each is a
399
+ // separate deployment under its own project key — the control plane keys
400
+ // deployments, pools and systems by `(company_id, project)`, so applying two
401
+ // projects is two applies of the same shape, not a new one. A config with no
402
+ // `projects:` block produces an empty list and everything below is untouched.
403
+ const extraProjects = Object.entries(config.projects ?? {})
404
+ .map(([key, p]) => p?.slug ?? key)
405
+ .filter((slug) => slug !== project);
146
406
  // 1. Plan (diff desired vs current).
147
- const planResp = await apiRequest("PUT", `/v1/deployments/${encodeURIComponent(project)}`, config);
407
+ const planResp = await apiRequest("PUT", `/v1/deployments/${encodeURIComponent(project)}`, applyBody(config, credentials));
148
408
  handleApiError("up", planResp);
149
409
  const plan = planResp.data?.plan;
410
+ const teamPlan = plan?.teams?.plan ?? [];
150
411
  if (opts.plan) {
412
+ // A plan REFUSAL is not a warning to scroll past — it is the reason this
413
+ // apply will not do what the config asks. Print it, and exit non-zero so a
414
+ // CI step that gates on `naive up --plan` actually gates.
415
+ const wouldRefuse = teamPlan.filter((t) => t.placement?.status === "refused");
416
+ const extraPlans = [];
417
+ for (const extra of extraProjects) {
418
+ const resp = await apiRequest("PUT", `/v1/deployments/${encodeURIComponent(extra)}`, applyBody(config, credentials));
419
+ handleApiError("up", resp);
420
+ extraPlans.push({ project: extra, plan: resp.data?.plan });
421
+ }
151
422
  agentOutput({
152
423
  action: "up",
153
- result: { config: found, project, plan, applied: false },
424
+ result: {
425
+ config: found,
426
+ project,
427
+ plan,
428
+ ...(extraPlans.length ? { projects: extraPlans } : {}),
429
+ applied: false,
430
+ },
154
431
  next_steps: [{ command: "naive up", description: "Apply the plan" }],
155
- hints: ["Plan only — nothing was provisioned."],
432
+ hints: [
433
+ "Plan only — nothing was provisioned.",
434
+ ...unconsumedWarnings(teamPlan),
435
+ ...(legacyDeprecationNotice(config) ? [legacyDeprecationNotice(config)] : []),
436
+ ...(agentsRenameNotice(config) ? [agentsRenameNotice(config)] : []),
437
+ ...(extraPlans.length
438
+ ? [`Planned ${extraPlans.length + 1} projects: ${[project, ...extraProjects].join(", ")}.`]
439
+ : []),
440
+ ...wouldRefuse.map((t) => `team "${t.name}" WOULD BE REFUSED (${t.placement?.code}): ${t.placement?.message}`),
441
+ ],
156
442
  });
443
+ if (wouldRefuse.length > 0)
444
+ process.exitCode = 1;
157
445
  return;
158
446
  }
159
- // 2. Apply (provision infra, register pools/systems/agent profiles).
160
- const applyResp = await apiRequest("POST", `/v1/deployments/${encodeURIComponent(project)}/apply`, config);
447
+ // 2. Apply (provision infra, register pools/systems/roles, materialize and
448
+ // place declared teams).
449
+ const applyResp = await apiRequest("POST", `/v1/deployments/${encodeURIComponent(project)}/apply`, applyBody(config, credentials));
161
450
  handleApiError("up", applyResp);
162
451
  const data = applyResp.data;
163
452
  const deployment = data.deployment;
164
453
  const needsAction = (deployment?.resources ?? []).filter((r) => r.status === "needs_action" || r.status === "failed");
454
+ // The other declared projects, applied the same way and reported by name.
455
+ // Sequential on purpose: two applies of one organization touch the same
456
+ // brains and the same business programs, which the control plane serializes
457
+ // per company anyway.
458
+ const extraResults = [];
459
+ for (const extra of extraProjects) {
460
+ const resp = await apiRequest("POST", `/v1/deployments/${encodeURIComponent(extra)}/apply`, applyBody(config, credentials));
461
+ handleApiError("up", resp);
462
+ const extraData = resp.data;
463
+ extraResults.push({
464
+ project: extra,
465
+ status: extraData.deployment?.status,
466
+ resources: extraData.deployment?.resources,
467
+ });
468
+ }
469
+ // ── WHAT WAS PROVISIONED, AND WHAT WAS REFUSED AND WHY ────────────────────
470
+ // A declared team that produced nothing used to be indistinguishable from one
471
+ // that worked: `naive up` printed `applied: true` and moved on. Every declared
472
+ // team now appears here with the agents it materialized, and a durable team
473
+ // additionally with its placement — placed, converged, or refused with the
474
+ // reason. A refusal exits NON-ZERO: the apply did real work (infra, pools,
475
+ // hermes teams) and reporting that honestly means not also claiming the part
476
+ // that was refused succeeded.
477
+ const durableByTeam = new Map((data.teams ?? []).map((t) => [t.team, t]));
478
+ const hermesByTeam = new Map((data.staffing ?? []).map((s) => [s.system, s]));
479
+ const teams = teamPlan.map((t) => {
480
+ const durable = durableByTeam.get(t.name);
481
+ const hermes = hermesByTeam.get(t.name);
482
+ // BOTH LANES, not the first one that answered. A MIXED team — a hermes lead
483
+ // with a member pinned to `runtime.durable` — is reported by BOTH
484
+ // materializers, and `durable ?? hermes` printed one half and silently
485
+ // dropped the other, so the agents an apply created did not add up to the
486
+ // agents it reported. Each role is tagged with the runtime it landed on when
487
+ // the team has two, so "which lane is this agent in" is answerable from the
488
+ // output rather than by reading the config back.
489
+ const lanes = t.role_runtimes;
490
+ const label = (role, status) => lanes?.[role] ? `${role} (${status}, ${lanes[role]})` : `${role} (${status})`;
491
+ const agents = [...(hermes?.agents ?? []), ...(durable?.agents ?? [])].map((a) => label(a.role, a.provision_status));
492
+ return {
493
+ name: t.name,
494
+ runtime: t.runtime,
495
+ lead: t.lead_role,
496
+ ...(lanes ? { role_runtimes: lanes } : {}),
497
+ ...(durable?.subject ? { subject: durable.subject } : {}),
498
+ agents,
499
+ archived: [...(hermes?.archived ?? []), ...(durable?.archived ?? [])],
500
+ ...(durable?.placement
501
+ ? {
502
+ placement: {
503
+ status: durable.placement.status,
504
+ ...(durable.placement.code ? { code: durable.placement.code } : {}),
505
+ ...(durable.placement.message ? { reason: durable.placement.message } : {}),
506
+ },
507
+ }
508
+ : {}),
509
+ };
510
+ });
511
+ const refusedTeams = teams.filter((t) => t.placement?.status === "refused");
512
+ const brains = brainReport(data.brains);
165
513
  // Deploy cloud.web source to the provisioned web project(s) (Vercel builds + hosts).
166
514
  const webDeploys = {};
167
515
  if (opts.deploy !== false) {
@@ -199,20 +547,71 @@ export const upCmd = new Command("up")
199
547
  resources: deployment?.resources,
200
548
  runtime_pools: Object.keys(config.runtime ?? {}),
201
549
  systems: Object.keys(config.systems ?? {}),
550
+ ...(brains ? { brains } : {}),
202
551
  agents: Object.keys(config.agents ?? config.agentProfiles ?? config.templates ?? {}),
552
+ teams,
203
553
  outputs: deployment?.outputs ?? {},
204
554
  web_deployments: webDeploys,
555
+ ...(extraResults.length ? { projects: extraResults } : {}),
205
556
  applied: true,
206
557
  },
207
558
  next_steps: [
208
559
  ...needsAction.map((r) => ({ command: `naive status`, description: `${r.name}: ${r.requirement ?? r.status}` })),
560
+ ...refusedTeams.map(refusalNextStep).filter((s) => s !== null),
209
561
  { command: `naive agent-profiles provision <name> --user <tenant>`, description: "Provision an agent profile for a tenant" },
210
562
  ],
211
563
  hints: [
212
564
  `Deployment "${project}" is ${deployment?.status ?? "?"}.`,
565
+ ...unconsumedWarnings(teamPlan),
566
+ ...(legacyDeprecationNotice(config) ? [legacyDeprecationNotice(config)] : []),
567
+ ...(agentsRenameNotice(config) ? [agentsRenameNotice(config)] : []),
568
+ ...extraResults.map((r) => `Deployment "${r.project}" is ${r.status ?? "?"}.`),
213
569
  needsAction.length ? `${needsAction.length} resource(s) need action — see next_steps.` : "All declared infrastructure is ready.",
570
+ ...(brains
571
+ ? [
572
+ `brains (${brains.declared_in}): ${brains.created.length} created` +
573
+ `${brains.created.length ? ` (${brains.created.join(", ")})` : ""}, ` +
574
+ `${brains.converged.length} already present` +
575
+ `${brains.converged.length ? ` (${brains.converged.join(", ")})` : ""}` +
576
+ `${brains.default ? `; default is "${brains.default}"` : ""}` +
577
+ `${brains.retained.length ? `. RETAINED, not declared by this config: ${brains.retained.join(", ")} — remove one deliberately with \`naive brain rm\`.` : "."}`,
578
+ // The accept-and-drop list, on the APPLY path and not only in --plan.
579
+ ...(brains.unpersisted
580
+ ? [
581
+ "DECLARED BUT NOT STORED — " +
582
+ Object.entries(brains.unpersisted)
583
+ .map(([name, keys]) => `${name}: ${keys.join(", ")}`)
584
+ .join("; ") +
585
+ ". `brain_knowledge_bases` has no column for these, so they were accepted and dropped. " +
586
+ "Nothing reads them; do not treat them as in force.",
587
+ ]
588
+ : []),
589
+ ...(brains.notes ?? []),
590
+ ]
591
+ : []),
592
+ ...teams
593
+ .filter((t) => t.placement?.status === "placed" || t.placement?.status === "converged")
594
+ .map((t) => t.placement?.status === "placed"
595
+ ? `team "${t.name}" PLACED on the durable runtime (${t.agents.length} agent(s)). Its jurisdiction is fixed permanently by the first control request.`
596
+ : `team "${t.name}" was already on the durable runtime (${t.agents.length} agent(s)); nothing was rewritten.`),
597
+ ...refusedTeams.map((t) => `team "${t.name}" REFUSED (${t.placement?.code}): ${t.placement?.reason}`),
598
+ // Said once, not per team. `naive runtime migrate` does not exist in this
599
+ // CLI or any released one — see refusalNextStep — so a refusal with no
600
+ // next_step gets the reason it has none instead of a command that 404s.
601
+ ...(refusedTeams.some((t) => refusalNextStep(t) === null)
602
+ ? [
603
+ "No `naive` command moves an existing tenant between runtimes, on this or any released version. " +
604
+ "Placement rewrites sidecar_url and container_auth_token and nulls instance_id with nothing stashed, " +
605
+ "so the control plane refuses a config edit that would do it. Read the refusal above and, if the move " +
606
+ "is intended, take it to an operator with direct access — there is no self-serve path.",
607
+ ]
608
+ : []),
214
609
  ],
215
610
  });
611
+ // The apply did real work and said so; it must not ALSO report success for
612
+ // the teams it refused. Non-zero, so a CI pipeline stops here.
613
+ if (refusedTeams.length > 0)
614
+ process.exitCode = 1;
216
615
  });
217
616
  export const downCmd = new Command("down")
218
617
  .description("Tear down the infrastructure provisioned for this project (naive.config.ts)")
@@ -232,6 +631,7 @@ export const downCmd = new Command("down")
232
631
  // Without --yes: preview what would be destroyed (no side effects).
233
632
  if (!opts.yes) {
234
633
  const current = await apiRequest("GET", `/v1/deployments/${encodeURIComponent(project)}`);
634
+ handleApiError("down", current);
235
635
  const d = current.data ?? {};
236
636
  agentOutput({
237
637
  action: "down",