@usenaive-sdk/cli 0.15.0 → 1.0.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.
- package/README.md +43 -354
- package/dist/index.js +51775 -502
- package/package.json +22 -18
- package/dist/client.d.ts +0 -108
- package/dist/client.js +0 -458
- package/dist/client.js.map +0 -1
- package/dist/commands/account-kits.d.ts +0 -2
- package/dist/commands/account-kits.js +0 -71
- package/dist/commands/account-kits.js.map +0 -1
- package/dist/commands/aeo.d.ts +0 -2
- package/dist/commands/aeo.js +0 -451
- package/dist/commands/aeo.js.map +0 -1
- package/dist/commands/agent-profiles.d.ts +0 -11
- package/dist/commands/agent-profiles.js +0 -120
- package/dist/commands/agent-profiles.js.map +0 -1
- package/dist/commands/agents.d.ts +0 -2
- package/dist/commands/agents.js +0 -916
- package/dist/commands/agents.js.map +0 -1
- package/dist/commands/app-data.d.ts +0 -2
- package/dist/commands/app-data.js +0 -445
- package/dist/commands/app-data.js.map +0 -1
- package/dist/commands/approvals.d.ts +0 -2
- package/dist/commands/approvals.js +0 -242
- package/dist/commands/approvals.js.map +0 -1
- package/dist/commands/apps.d.ts +0 -9
- package/dist/commands/apps.js +0 -844
- package/dist/commands/apps.js.map +0 -1
- package/dist/commands/audio.d.ts +0 -9
- package/dist/commands/audio.js +0 -391
- package/dist/commands/audio.js.map +0 -1
- package/dist/commands/auth.d.ts +0 -31
- package/dist/commands/auth.js +0 -428
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/billing.d.ts +0 -2
- package/dist/commands/billing.js +0 -245
- package/dist/commands/billing.js.map +0 -1
- package/dist/commands/brain.d.ts +0 -2
- package/dist/commands/brain.js +0 -1300
- package/dist/commands/brain.js.map +0 -1
- package/dist/commands/browser.d.ts +0 -2
- package/dist/commands/browser.js +0 -378
- package/dist/commands/browser.js.map +0 -1
- package/dist/commands/business.d.ts +0 -2
- package/dist/commands/business.js +0 -566
- package/dist/commands/business.js.map +0 -1
- package/dist/commands/cards.d.ts +0 -2
- package/dist/commands/cards.js +0 -419
- package/dist/commands/cards.js.map +0 -1
- package/dist/commands/ceo.d.ts +0 -2
- package/dist/commands/ceo.js +0 -369
- package/dist/commands/ceo.js.map +0 -1
- package/dist/commands/clone.d.ts +0 -2
- package/dist/commands/clone.js +0 -203
- package/dist/commands/clone.js.map +0 -1
- package/dist/commands/companies.d.ts +0 -10
- package/dist/commands/companies.js +0 -283
- package/dist/commands/companies.js.map +0 -1
- package/dist/commands/company-data.d.ts +0 -2
- package/dist/commands/company-data.js +0 -168
- package/dist/commands/company-data.js.map +0 -1
- package/dist/commands/compute.d.ts +0 -2
- package/dist/commands/compute.js +0 -498
- package/dist/commands/compute.js.map +0 -1
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -36
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/connections.d.ts +0 -8
- package/dist/commands/connections.js +0 -137
- package/dist/commands/connections.js.map +0 -1
- package/dist/commands/cron-jobs.d.ts +0 -2
- package/dist/commands/cron-jobs.js +0 -245
- package/dist/commands/cron-jobs.js.map +0 -1
- package/dist/commands/domains.d.ts +0 -2
- package/dist/commands/domains.js +0 -474
- package/dist/commands/domains.js.map +0 -1
- package/dist/commands/ecommerce.d.ts +0 -2
- package/dist/commands/ecommerce.js +0 -277
- package/dist/commands/ecommerce.js.map +0 -1
- package/dist/commands/email.d.ts +0 -2
- package/dist/commands/email.js +0 -336
- package/dist/commands/email.js.map +0 -1
- package/dist/commands/employees.d.ts +0 -2
- package/dist/commands/employees.js +0 -190
- package/dist/commands/employees.js.map +0 -1
- package/dist/commands/env.d.ts +0 -7
- package/dist/commands/env.js +0 -112
- package/dist/commands/env.js.map +0 -1
- package/dist/commands/formation.d.ts +0 -2
- package/dist/commands/formation.js +0 -273
- package/dist/commands/formation.js.map +0 -1
- package/dist/commands/identity.d.ts +0 -2
- package/dist/commands/identity.js +0 -110
- package/dist/commands/identity.js.map +0 -1
- package/dist/commands/images.d.ts +0 -2
- package/dist/commands/images.js +0 -371
- package/dist/commands/images.js.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.js +0 -43
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/jobs.d.ts +0 -2
- package/dist/commands/jobs.js +0 -144
- package/dist/commands/jobs.js.map +0 -1
- package/dist/commands/keys.d.ts +0 -2
- package/dist/commands/keys.js +0 -156
- package/dist/commands/keys.js.map +0 -1
- package/dist/commands/legacy-auth.d.ts +0 -27
- package/dist/commands/legacy-auth.js +0 -64
- package/dist/commands/legacy-auth.js.map +0 -1
- package/dist/commands/llm.d.ts +0 -2
- package/dist/commands/llm.js +0 -106
- package/dist/commands/llm.js.map +0 -1
- package/dist/commands/login.d.ts +0 -2
- package/dist/commands/login.js +0 -65
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -7
- package/dist/commands/logout.js +0 -55
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/logs.d.ts +0 -2
- package/dist/commands/logs.js +0 -42
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/loops.d.ts +0 -2
- package/dist/commands/loops.js +0 -260
- package/dist/commands/loops.js.map +0 -1
- package/dist/commands/media.d.ts +0 -2
- package/dist/commands/media.js +0 -183
- package/dist/commands/media.js.map +0 -1
- package/dist/commands/memory.d.ts +0 -2
- package/dist/commands/memory.js +0 -151
- package/dist/commands/memory.js.map +0 -1
- package/dist/commands/mobile.d.ts +0 -4
- package/dist/commands/mobile.js +0 -382
- package/dist/commands/mobile.js.map +0 -1
- package/dist/commands/module.d.ts +0 -8
- package/dist/commands/module.js +0 -48
- package/dist/commands/module.js.map +0 -1
- package/dist/commands/objectives.d.ts +0 -2
- package/dist/commands/objectives.js +0 -243
- package/dist/commands/objectives.js.map +0 -1
- package/dist/commands/payments.d.ts +0 -2
- package/dist/commands/payments.js +0 -131
- package/dist/commands/payments.js.map +0 -1
- package/dist/commands/people.d.ts +0 -2
- package/dist/commands/people.js +0 -197
- package/dist/commands/people.js.map +0 -1
- package/dist/commands/phone.d.ts +0 -2
- package/dist/commands/phone.js +0 -289
- package/dist/commands/phone.js.map +0 -1
- package/dist/commands/projects.d.ts +0 -13
- package/dist/commands/projects.js +0 -220
- package/dist/commands/projects.js.map +0 -1
- package/dist/commands/queue.d.ts +0 -2
- package/dist/commands/queue.js +0 -121
- package/dist/commands/queue.js.map +0 -1
- package/dist/commands/register.d.ts +0 -2
- package/dist/commands/register.js +0 -80
- package/dist/commands/register.js.map +0 -1
- package/dist/commands/report.d.ts +0 -17
- package/dist/commands/report.js +0 -109
- package/dist/commands/report.js.map +0 -1
- package/dist/commands/sandbox.d.ts +0 -2
- package/dist/commands/sandbox.js +0 -558
- package/dist/commands/sandbox.js.map +0 -1
- package/dist/commands/search.d.ts +0 -2
- package/dist/commands/search.js +0 -223
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/seo.d.ts +0 -2
- package/dist/commands/seo.js +0 -528
- package/dist/commands/seo.js.map +0 -1
- package/dist/commands/sessions.d.ts +0 -6
- package/dist/commands/sessions.js +0 -109
- package/dist/commands/sessions.js.map +0 -1
- package/dist/commands/social-data.d.ts +0 -2
- package/dist/commands/social-data.js +0 -245
- package/dist/commands/social-data.js.map +0 -1
- package/dist/commands/social.d.ts +0 -2
- package/dist/commands/social.js +0 -407
- package/dist/commands/social.js.map +0 -1
- package/dist/commands/status.d.ts +0 -2
- package/dist/commands/status.js +0 -73
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/support.d.ts +0 -2
- package/dist/commands/support.js +0 -134
- package/dist/commands/support.js.map +0 -1
- package/dist/commands/tasks.d.ts +0 -2
- package/dist/commands/tasks.js +0 -409
- package/dist/commands/tasks.js.map +0 -1
- package/dist/commands/teams.d.ts +0 -69
- package/dist/commands/teams.js +0 -503
- package/dist/commands/teams.js.map +0 -1
- package/dist/commands/trading.d.ts +0 -2
- package/dist/commands/trading.js +0 -333
- package/dist/commands/trading.js.map +0 -1
- package/dist/commands/up.d.ts +0 -551
- package/dist/commands/up.js +0 -1149
- package/dist/commands/up.js.map +0 -1
- package/dist/commands/usage.d.ts +0 -2
- package/dist/commands/usage.js +0 -73
- package/dist/commands/usage.js.map +0 -1
- package/dist/commands/use.d.ts +0 -12
- package/dist/commands/use.js +0 -96
- package/dist/commands/use.js.map +0 -1
- package/dist/commands/users.d.ts +0 -10
- package/dist/commands/users.js +0 -82
- package/dist/commands/users.js.map +0 -1
- package/dist/commands/vault.d.ts +0 -2
- package/dist/commands/vault.js +0 -101
- package/dist/commands/vault.js.map +0 -1
- package/dist/commands/verification.d.ts +0 -2
- package/dist/commands/verification.js +0 -152
- package/dist/commands/verification.js.map +0 -1
- package/dist/commands/verify-email.d.ts +0 -19
- package/dist/commands/verify-email.js +0 -112
- package/dist/commands/verify-email.js.map +0 -1
- package/dist/commands/video.d.ts +0 -2
- package/dist/commands/video.js +0 -417
- package/dist/commands/video.js.map +0 -1
- package/dist/commands/voice.d.ts +0 -2
- package/dist/commands/voice.js +0 -516
- package/dist/commands/voice.js.map +0 -1
- package/dist/commands/wallet.d.ts +0 -2
- package/dist/commands/wallet.js +0 -244
- package/dist/commands/wallet.js.map +0 -1
- package/dist/commands/webhooks.d.ts +0 -13
- package/dist/commands/webhooks.js +0 -119
- package/dist/commands/webhooks.js.map +0 -1
- package/dist/commands/whoami.d.ts +0 -2
- package/dist/commands/whoami.js +0 -53
- package/dist/commands/whoami.js.map +0 -1
- package/dist/config.d.ts +0 -42
- package/dist/config.js +0 -83
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js.map +0 -1
- package/dist/lib/brain-select.d.ts +0 -193
- package/dist/lib/brain-select.js +0 -403
- package/dist/lib/brain-select.js.map +0 -1
- package/dist/lib/cli-validation.d.ts +0 -12
- package/dist/lib/cli-validation.js +0 -46
- package/dist/lib/cli-validation.js.map +0 -1
- package/dist/lib/config-errors.d.ts +0 -4
- package/dist/lib/config-errors.js +0 -13
- package/dist/lib/config-errors.js.map +0 -1
- package/dist/lib/deprecation.d.ts +0 -158
- package/dist/lib/deprecation.js +0 -140
- package/dist/lib/deprecation.js.map +0 -1
- package/dist/lib/duration.d.ts +0 -8
- package/dist/lib/duration.js +0 -11
- package/dist/lib/duration.js.map +0 -1
- package/dist/lib/http-response.d.ts +0 -11
- package/dist/lib/http-response.js +0 -96
- package/dist/lib/http-response.js.map +0 -1
- package/dist/lib/pricing.d.ts +0 -132
- package/dist/lib/pricing.js +0 -148
- package/dist/lib/pricing.js.map +0 -1
- package/dist/lib/starter-config.d.ts +0 -13
- package/dist/lib/starter-config.js +0 -71
- package/dist/lib/starter-config.js.map +0 -1
- package/dist/lib/stream-frames.d.ts +0 -114
- package/dist/lib/stream-frames.js +0 -169
- package/dist/lib/stream-frames.js.map +0 -1
- package/dist/lib/teams-catalog.d.ts +0 -141
- package/dist/lib/teams-catalog.js +0 -841
- package/dist/lib/teams-catalog.js.map +0 -1
- package/dist/lib/unknown-command.d.ts +0 -71
- package/dist/lib/unknown-command.js +0 -132
- package/dist/lib/unknown-command.js.map +0 -1
- package/dist/lib/version-check.d.ts +0 -26
- package/dist/lib/version-check.js +0 -21
- package/dist/lib/version-check.js.map +0 -1
- package/dist/output.d.ts +0 -174
- package/dist/output.js +0 -435
- package/dist/output.js.map +0 -1
- package/dist/prompt.d.ts +0 -17
- package/dist/prompt.js +0 -70
- package/dist/prompt.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.js +0 -14
- package/dist/version.js.map +0 -1
package/dist/commands/up.js
DELETED
|
@@ -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
|