launch.max becomes launch.models plus launch.limits (total, providers, exact provider/model). The launcher re-reads limits on every attempt. Settings and `mosaic limits` write through one locked, journaled organization writer. `mosaic limits migrate` is print-only. Candidate 00b84e311475f97ced0d9a9599fba23a28442971ed15d8d489d9422d84a8812e, Rocko packet4b6e50636, Filbert round 2 approve (comment 28542). Landing gate at0c56cc2d4: 23 suites green. Decision 123 go from Mos. Co-Authored-By: Claude Opus 5.5 <[email protected]>
60 lines
2.7 KiB
JavaScript
60 lines
2.7 KiB
JavaScript
// REQ-LIMIT-1: human settings; decision 145 migration is a separate print-only read.
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { loadSystem } from "./config.mjs";
|
|
import { systemVars } from "../../business/src/index.mjs";
|
|
import { CliError } from "./errors.mjs";
|
|
import { migrateLaunch } from "../../business/src/migrate-launch.mjs";
|
|
|
|
export const LIMITS_USAGE = "mosaic limits [--business ID] [--json] | set --file PATCH [--business ID] | " +
|
|
"migrate --file ORGANIZATION --models MAPPING [--roles-dir DIR]";
|
|
|
|
export function parseLimits(args) {
|
|
const out = { positional: [], flags: new Set(), values: {} };
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === "--json") out.flags.add(arg);
|
|
else if (["--file", "--models", "--roles-dir", "--business"].includes(arg)) {
|
|
if (!args[i + 1] || Object.hasOwn(out.values, arg)) throw new CliError(LIMITS_USAGE, 4);
|
|
out.values[arg] = args[++i];
|
|
} else if (arg.startsWith("--")) throw new CliError(LIMITS_USAGE, 4);
|
|
else out.positional.push(arg);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export async function limitsCommand(args, io, callFor) {
|
|
const parsed = parseLimits(args);
|
|
const verb = parsed.positional[0] ?? "view";
|
|
const values = parsed.values;
|
|
if (parsed.positional.length > 1) throw new CliError(LIMITS_USAGE, 4);
|
|
try {
|
|
if (verb === "migrate") {
|
|
if (!values["--file"] || !values["--models"] || values["--business"]) {
|
|
throw new CliError(LIMITS_USAGE, 4);
|
|
}
|
|
const file = values["--file"];
|
|
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
const mapping = JSON.parse(readFileSync(values["--models"], "utf8"));
|
|
const rolesDir = values["--roles-dir"] ?? fileURLToPath(new URL("../../../roles", import.meta.url));
|
|
const inherits = raw.launch?.max && Object.values(raw.roles ?? {}).some(role => role.vars?.model === undefined);
|
|
const system = inherits ? systemVars(loadSystem({ env: io.env })) : null;
|
|
const converted = migrateLaunch(raw, mapping, file, { rolesDir, system });
|
|
io.stdout.write(JSON.stringify(converted, null, 2) + "\n");
|
|
return;
|
|
}
|
|
if (!["view", "set"].includes(verb) || values["--models"] || values["--roles-dir"] ||
|
|
(verb === "view" && values["--file"]) || (verb === "set" && !values["--file"])) {
|
|
throw new CliError(LIMITS_USAGE, 4);
|
|
}
|
|
const call = callFor(parsed);
|
|
const result = verb === "set"
|
|
? await call("limits.set", JSON.parse(readFileSync(values["--file"], "utf8")))
|
|
: await call("limits", {});
|
|
io.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
} catch (error) {
|
|
if (error instanceof CliError) throw error;
|
|
throw new CliError(error.message, 2);
|
|
}
|
|
}
|