@sealant/mend 0.8.0 → 0.9.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 (2) hide show
  1. package/dist/main.js +78 -5
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -1450,33 +1450,106 @@ const envLoad = async (config, args) => {
1450
1450
  };
1451
1451
  const envShow = async (config, args) => {
1452
1452
  const project = await findProject(config, takeFlagValue(args, "--project"));
1453
- const [environment, secrets] = await Promise.all([
1453
+ const [environment, secrets, cluster] = await Promise.all([
1454
1454
  api(config, "GET", `/projects/${project.id}/environment`),
1455
1455
  api(config, "GET", `/projects/${project.id}/secrets`),
1456
+ api(config, "GET", `/projects/${project.id}/cluster-bindings`),
1456
1457
  ]);
1457
- say(`${project.name} ${dim(`· configuration r${environment.revision} · secrets r${secrets.revision}`)}`);
1458
- if (environment.variables.length === 0 && secrets.secrets.length === 0) {
1458
+ say(`${project.name} ${dim(`· configuration r${environment.revision} · secrets r${secrets.revision} · cluster r${cluster.revision}`)}`);
1459
+ if (environment.variables.length === 0 &&
1460
+ secrets.secrets.length === 0 &&
1461
+ cluster.bindings.length === 0 &&
1462
+ cluster.serviceAccount === null) {
1459
1463
  say(dim(` nothing stored — load a file: ${cobalt("mend env load")}`));
1460
1464
  return;
1461
1465
  }
1462
- const width = Math.max(...environment.variables.map((v) => v.name.length), ...secrets.secrets.map((s) => s.name.length));
1466
+ const bindingNames = cluster.bindings.map((b) => `${b.kind}/${b.objectName}`);
1467
+ const width = Math.max(0, ...environment.variables.map((v) => v.name.length), ...secrets.secrets.map((s) => s.name.length), ...bindingNames.map((name) => name.length));
1463
1468
  for (const variable of environment.variables) {
1464
1469
  say(` ${variable.name.padEnd(width)} configuration ${dim("· plaintext")}`);
1465
1470
  }
1466
1471
  for (const secret of secrets.secrets) {
1467
1472
  say(` ${secret.name.padEnd(width)} secret ${dim("· value set, never shown")}`);
1468
1473
  }
1474
+ for (const name of bindingNames) {
1475
+ say(` ${name.padEnd(width)} cluster binding ${dim("· resolved by the platform at launch · contents unknown to Mend")}`);
1476
+ }
1477
+ if (cluster.serviceAccount !== null) {
1478
+ say(` ${cluster.serviceAccount.padEnd(width)} service account ${dim("· workspace pod identity · allowlisted by the operator")}`);
1479
+ }
1480
+ if (!cluster.clusterCapable && (cluster.bindings.length > 0 || cluster.serviceAccount !== null)) {
1481
+ say(amber(` ${cluster.bindings.length} cluster binding${cluster.bindings.length === 1 ? "" : "s"}${cluster.serviceAccount === null ? "" : " · service account set"} · local runner — cluster bindings do not resolve here`));
1482
+ }
1483
+ };
1484
+ /**
1485
+ * Cluster bindings (`.plans/cluster-env-sources.md`): NAMES of Kubernetes Secrets/ConfigMaps the
1486
+ * platform resolves at launch — Mend never holds the values. Every verb works on every install
1487
+ * (a non-cluster install must be able to remove bindings to launch); only resolution is
1488
+ * Kubernetes-only, and the platform refuses launches there, readably.
1489
+ */
1490
+ const envCluster = async (config, args) => {
1491
+ const explicitProject = takeFlagValue(args, "--project");
1492
+ const positional = args.filter((arg, i) => !arg.startsWith("--") && args[i - 1] !== "--project");
1493
+ const [verb, ...rest] = positional;
1494
+ const usage = "try: mend env cluster add secret <name> | add configmap <name> | remove <kind>/<name> | sa <name> | sa --clear";
1495
+ const project = await findProject(config, explicitProject);
1496
+ const route = `/projects/${project.id}/cluster-bindings`;
1497
+ const appliesLine = () => say(dim(" applies from the next workspace launch; running workspaces keep what they started with"));
1498
+ if (verb === "add") {
1499
+ const [kind, objectName] = rest;
1500
+ if ((kind !== "secret" && kind !== "configmap") || objectName === undefined) {
1501
+ return fail(`env cluster add takes a kind and an object name — ${usage}`);
1502
+ }
1503
+ const result = await api(config, "POST", route, {
1504
+ kind,
1505
+ objectName,
1506
+ });
1507
+ say(`${green("✓")} bound ${kind}/${objectName} ${dim(`· cluster r${result.revision}`)}`);
1508
+ say(dim(" resolved by the platform at launch · contents unknown to Mend"));
1509
+ return appliesLine();
1510
+ }
1511
+ if (verb === "remove") {
1512
+ const [ref] = rest;
1513
+ const [kind, objectName] = ref?.split("/", 2) ?? [];
1514
+ if ((kind !== "secret" && kind !== "configmap") || objectName === undefined) {
1515
+ return fail(`env cluster remove takes <kind>/<name>, e.g. secret/app-env — ${usage}`);
1516
+ }
1517
+ const snapshot = await api(config, "GET", route);
1518
+ const binding = snapshot.bindings.find((b) => b.kind === kind && b.objectName === objectName);
1519
+ if (binding === undefined)
1520
+ return fail(`${kind}/${objectName} is not bound on ${project.name}`);
1521
+ const result = await api(config, "DELETE", `${route}/${binding.id}`);
1522
+ say(`${green("✓")} removed ${kind}/${objectName} ${dim(`· cluster r${result.revision}`)}`);
1523
+ return appliesLine();
1524
+ }
1525
+ if (verb === "sa") {
1526
+ const clear = args.includes("--clear");
1527
+ const [name] = rest;
1528
+ if (!clear && name === undefined)
1529
+ return fail(`env cluster sa takes a name or --clear — ${usage}`);
1530
+ const result = await api(config, "PUT", `${route}/service-account`, { serviceAccount: clear ? null : name });
1531
+ say(result.serviceAccount === null
1532
+ ? `${green("✓")} cleared the workspace service account ${dim(`· cluster r${result.revision}`)}`
1533
+ : `${green("✓")} service account ${result.serviceAccount} ${dim(`· cluster r${result.revision}`)}`);
1534
+ if (result.serviceAccount !== null) {
1535
+ say(amber(" the session agent holds this role's full permissions for the whole session — bind a least-privilege role intended for untrusted code; names outside the platform allowlist fail the launch"));
1536
+ }
1537
+ return appliesLine();
1538
+ }
1539
+ return fail(`unknown env cluster command "${verb ?? ""}" — ${usage}`);
1469
1540
  };
1470
1541
  const envCommand = async (config, args) => {
1471
1542
  const [verb, ...rest] = args;
1472
1543
  switch (verb) {
1473
1544
  case "load":
1474
1545
  return envLoad(config, rest);
1546
+ case "cluster":
1547
+ return envCluster(config, rest);
1475
1548
  case "show":
1476
1549
  case undefined:
1477
1550
  return envShow(config, rest);
1478
1551
  default:
1479
- return fail(`unknown env command "${verb}" — try: mend env load | show`);
1552
+ return fail(`unknown env command "${verb}" — try: mend env load | show | cluster`);
1480
1553
  }
1481
1554
  };
1482
1555
  const dotfilesCommand = async (config, args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/mend",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "The mend CLI: adopt a repository into Mend's store, launch your coding agent in a recorded session worktree, detach and reattach from any terminal",
5
5
  "keywords": [
6
6
  "claude-code",