@pipod/cli 0.1.10 → 0.1.12

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/dist/cli.js CHANGED
@@ -1908,6 +1908,14 @@ var AccountClient = class {
1908
1908
  putOrgSettings(body) {
1909
1909
  return this.request(`/orgs/${encodeURIComponent(this.orgId)}/settings`, { method: "PUT", body });
1910
1910
  }
1911
+ /** The organization's policy: its `config` holds the limits; the scripts and Pi files are always empty. */
1912
+ getOrgPolicy() {
1913
+ return this.request(`/orgs/${encodeURIComponent(this.orgId)}/policy`);
1914
+ }
1915
+ /** Replaces the whole policy, compare-and-swap on `version`. The server validates it. */
1916
+ putOrgPolicy(body) {
1917
+ return this.request(`/orgs/${encodeURIComponent(this.orgId)}/policy`, { method: "PUT", body });
1918
+ }
1911
1919
  launch(body) {
1912
1920
  return this.request("/pods", { method: "POST", body });
1913
1921
  }
@@ -2209,10 +2217,12 @@ async function responseError(res, method, apiPath) {
2209
2217
  hint: "an owner of your organization can grant it (`pipod org-admin` opens the console)"
2210
2218
  });
2211
2219
  }
2220
+ const issues = res.status === 400 && Array.isArray(body.detail) ? body.detail.filter((issue) => typeof issue === "string").slice(0, 10).map((issue) => issue.slice(0, 240)) : [];
2212
2221
  return new PiPodError(`the pi pod server refused ${method} ${apiPath}: ${message}`, {
2213
2222
  status: res.status,
2214
2223
  ...code ? { code } : {},
2215
- ...safeDetail !== void 0 ? { detail: safeDetail } : {}
2224
+ ...safeDetail !== void 0 ? { detail: safeDetail } : {},
2225
+ ...issues.length > 0 ? { hint: issues.join("\n") } : {}
2216
2226
  });
2217
2227
  }
2218
2228
  function isSafeCapacityDetail(value) {
@@ -3416,11 +3426,6 @@ async function probeWorkspaceSeed(opts) {
3416
3426
  const branchOut = git(["symbolic-ref", "--quiet", "--short", "HEAD"]);
3417
3427
  const branch = branchOut.stdout.trim();
3418
3428
  if (branchOut.status !== 0 || branch === "") return archive("detached HEAD");
3419
- const status = git(["status", "--porcelain", "--untracked-files=all"]);
3420
- if (status.status !== 0) return archive("cannot tell whether the working tree is clean");
3421
- if (status.stdout.trim() !== "") {
3422
- return archive("uncommitted changes would be lost by a clone");
3423
- }
3424
3429
  const upstream = git(["rev-parse", "--abbrev-ref", "@{u}"]);
3425
3430
  if (upstream.status !== 0) return archive(`${branch} is not pushed`);
3426
3431
  const ahead = git(["rev-list", "@{u}..HEAD"]);
@@ -3535,7 +3540,8 @@ function describeWorkspaceSeed(plan) {
3535
3540
  return plan.reason;
3536
3541
  case "clone": {
3537
3542
  const base = `clone ${plan.url} at ${plan.branch} (${plan.commit.slice(0, 12)})`;
3538
- return plan.access === "credential" ? `${base} with forwarded ${plan.host} credentials` : base;
3543
+ const authenticated = plan.access === "credential" ? `${base} with forwarded ${plan.host} credentials` : base;
3544
+ return `${authenticated} (local uncommitted changes are not copied)`;
3539
3545
  }
3540
3546
  case "archive":
3541
3547
  return `archive ${plan.root}${plan.includeGit ? " with .git history" : ""} (${plan.reason})`;
@@ -3870,6 +3876,9 @@ function orgSourceDir(args) {
3870
3876
  if (!home) throw new PiPodError("cannot locate ~/.pi-pod/org: HOME is unset", { hint: "pass --dir <path>" });
3871
3877
  return path11.join(home, ORG_SOURCE_SUBDIR);
3872
3878
  }
3879
+ function policySourcePath(args) {
3880
+ return path11.join(orgSourceDir(args), ORG_POLICY_FILE);
3881
+ }
3873
3882
  function safePiDir(root, subpath) {
3874
3883
  const directory = path11.join(root, subpath);
3875
3884
  let stat;
@@ -3970,6 +3979,28 @@ async function readEnvSnapshot(envPath, mode, resolver, home) {
3970
3979
  async function compileBundleSource(args) {
3971
3980
  const cwd = args.cwd ?? process.cwd();
3972
3981
  const envMode = args.includeEnv === false ? false : args.includeEnv === "names" ? "names" : true;
3982
+ if (args.source === "policy-file") {
3983
+ const configPath2 = policySourcePath({ dir: args.dir, home: args.home });
3984
+ if (!fs12.existsSync(configPath2)) {
3985
+ throw new PiPodError(`no org policy file at ${configPath2}`, {
3986
+ hint: "run `pipod pull policy` to create it from the server's policy, or pass --dir <path>"
3987
+ });
3988
+ }
3989
+ return {
3990
+ kind: "policy-file",
3991
+ config: parseConfig(configPath2),
3992
+ initScript: "",
3993
+ bakeScript: "",
3994
+ piFiles: {},
3995
+ env: {},
3996
+ warnings: [],
3997
+ name: "org policy",
3998
+ templateRef: null,
3999
+ projectRoot: null,
4000
+ root: path11.dirname(configPath2),
4001
+ configPath: configPath2
4002
+ };
4003
+ }
3973
4004
  if (args.source === "project") {
3974
4005
  const configPath2 = findConfigPath(cwd, { home: args.home });
3975
4006
  if (!configPath2) {
@@ -4069,6 +4100,7 @@ async function compileBundleSource(args) {
4069
4100
  }
4070
4101
  var ORG_SCRIPTS = { init: "init.sh", bake: "bake.sh" };
4071
4102
  var ORG_ENV_FILE = "env";
4103
+ var ORG_POLICY_FILE = "policy.json";
4072
4104
  function readPiFiles(root, config, kind) {
4073
4105
  const files = {};
4074
4106
  const warnings = [];
@@ -4323,6 +4355,15 @@ function writePulledAgents(writer, piDir, agents) {
4323
4355
  }
4324
4356
  function writeBundleSource(args) {
4325
4357
  const { kind, bundle } = args;
4358
+ if (kind === "policy-file") {
4359
+ const policyPath = policySourcePath({ dir: args.dir, home: args.home });
4360
+ const writer2 = new LayerWriter(path11.dirname(policyPath));
4361
+ if (!fs12.existsSync(policyPath) || stable(parseConfig(policyPath)) !== stable(bundle.config)) {
4362
+ writer2.noteComments(policyPath);
4363
+ writer2.json(policyPath, bundle.config);
4364
+ }
4365
+ return { written: writer2.written, removed: writer2.removed, warnings: writer2.warnings, root: writer2.root };
4366
+ }
4326
4367
  let root;
4327
4368
  let configPath;
4328
4369
  if (kind === "project") {
@@ -5217,7 +5258,7 @@ async function cloneIntoPod(opts, plan) {
5217
5258
  if (isMissingRoute(error2)) {
5218
5259
  info("this server predates workspace cloning \u2014 sending an archive instead");
5219
5260
  } else {
5220
- warn(`clone failed: ${reason} \u2014 falling back to an archive of the local tree`);
5261
+ warn(`clone failed: ${reason} \u2014 falling back to an archive of the local working tree (including uncommitted changes)`);
5221
5262
  }
5222
5263
  return archiveIntoPod(
5223
5264
  opts,
@@ -15619,9 +15660,7 @@ function announceWorkspaceSeed(plan) {
15619
15660
  info(`workspace transport: this directory will be copied into the pod (${seed.reason})`);
15620
15661
  return;
15621
15662
  case "clone":
15622
- info(
15623
- `workspace seed: the pod will clone ${seed.url} at ${seed.branch} (${seed.commit.slice(0, 12)})` + (seed.access === "credential" ? ` with forwarded ${seed.host} credentials` : "")
15624
- );
15663
+ info(`workspace seed: the pod will ${describeWorkspaceSeed(seed)}`);
15625
15664
  break;
15626
15665
  case "archive":
15627
15666
  info(
@@ -18863,7 +18902,7 @@ async function rmCmd2(client, args, flags) {
18863
18902
  }
18864
18903
 
18865
18904
  // src/commands/layers.ts
18866
- var LAYER_USAGE = "template [<name>], user, or org";
18905
+ var LAYER_USAGE = "template [<name>], user, org, or policy";
18867
18906
  function parseLayerArgs(verb, args, opts) {
18868
18907
  const positionals = [];
18869
18908
  let dir;
@@ -18890,18 +18929,29 @@ function parseLayerArgs(verb, args, opts) {
18890
18929
  layer = { kind: "template" };
18891
18930
  } else if (first === "template") {
18892
18931
  layer = second === void 0 ? { kind: "template" } : { kind: "template", name: second };
18893
- } else if (first === "user" || first === "org") {
18932
+ } else if (first === "user" || first === "org" || first === "policy") {
18894
18933
  layer = { kind: first };
18895
18934
  if (second !== void 0) extra.unshift(second);
18896
18935
  } else {
18897
18936
  throw new PiPodError(`unknown layer "${first}"`, { hint: `layers: ${LAYER_USAGE}` });
18898
18937
  }
18899
18938
  if (extra.length > 0) throw new PiPodError(`unexpected \`pipod ${verb}\` argument(s): ${extra.join(" ")}`);
18900
- if (dir !== void 0 && layer.kind !== "org") throw new PiPodError("--dir only applies to the org layer");
18939
+ if (dir !== void 0 && layer.kind !== "org" && layer.kind !== "policy") {
18940
+ throw new PiPodError("--dir only applies to the org and policy layers");
18941
+ }
18901
18942
  return { layer, ...dir !== void 0 ? { dir } : {}, yes, withSecrets };
18902
18943
  }
18903
18944
  function sourceKindOf(layer) {
18904
- return layer.kind === "template" ? "project" : layer.kind === "user" ? "user-dir" : "org-dir";
18945
+ switch (layer.kind) {
18946
+ case "template":
18947
+ return "project";
18948
+ case "user":
18949
+ return "user-dir";
18950
+ case "org":
18951
+ return "org-dir";
18952
+ case "policy":
18953
+ return "policy-file";
18954
+ }
18905
18955
  }
18906
18956
  async function fetchRemoteLayer(client, layer, source) {
18907
18957
  if (layer.kind === "template") {
@@ -18924,6 +18974,17 @@ async function fetchRemoteLayer(client, layer, source) {
18924
18974
  scopeId: template.id
18925
18975
  };
18926
18976
  }
18977
+ if (layer.kind === "policy") {
18978
+ const current2 = await client.getOrgPolicy();
18979
+ return {
18980
+ bundle: settingsBundle(current2),
18981
+ version: current2.version,
18982
+ secretNames: [],
18983
+ label: `org policy v${current2.version}`,
18984
+ scope: "policy",
18985
+ scopeId: client.orgId
18986
+ };
18987
+ }
18927
18988
  const current = layer.kind === "user" ? await client.getUserSettings() : await client.getOrgSettings();
18928
18989
  const scopeId = layer.kind === "user" ? client.userId : client.orgId;
18929
18990
  const listed = await client.listSecrets(layer.kind, scopeId).catch(() => ({ secrets: [] }));
@@ -18939,6 +19000,7 @@ async function fetchRemoteLayer(client, layer, source) {
18939
19000
  function localLabel(layer, flags) {
18940
19001
  if (layer.kind === "template") return "project";
18941
19002
  if (layer.kind === "user") return "~/.pi-pod + ~/.pi/agent";
19003
+ if (layer.kind === "policy") return displayPath(policySourcePath({ dir: flags.dir, home: flags.home }), flags.home);
18942
19004
  return displayPath(orgSourceDir({ dir: flags.dir, home: flags.home }), flags.home);
18943
19005
  }
18944
19006
  function printDiff(heading, lines) {
@@ -18972,22 +19034,10 @@ async function runPull(args, flags) {
18972
19034
  const kind = sourceKindOf(parsed.layer);
18973
19035
  let source;
18974
19036
  let firstPull = false;
18975
- if (kind === "org-dir" && !fs31.existsSync(orgSourceDir({ dir: parsed.dir, home: flags.home }))) {
19037
+ const localPath = kind === "org-dir" ? orgSourceDir({ dir: parsed.dir, home: flags.home }) : kind === "policy-file" ? policySourcePath({ dir: parsed.dir, home: flags.home }) : null;
19038
+ if (localPath !== null && !fs31.existsSync(localPath)) {
18976
19039
  firstPull = true;
18977
- source = {
18978
- kind,
18979
- config: {},
18980
- initScript: "",
18981
- bakeScript: "",
18982
- piFiles: {},
18983
- env: {},
18984
- warnings: [],
18985
- name: "org settings",
18986
- templateRef: null,
18987
- projectRoot: null,
18988
- root: orgSourceDir({ dir: parsed.dir, home: flags.home }),
18989
- configPath: ""
18990
- };
19040
+ source = { config: {}, initScript: "", bakeScript: "", piFiles: {}, env: {}, warnings: [], templateRef: null };
18991
19041
  } else {
18992
19042
  source = await compileBundleSource({ source: kind, cwd, home: flags.home, includeEnv: false, dir: parsed.dir });
18993
19043
  }
@@ -19048,6 +19098,11 @@ async function runPush(args, flags) {
19048
19098
  return 0;
19049
19099
  }
19050
19100
  if (remote.version === void 0) throw new PiPodError(`${remote.label} has no version to compare-and-swap against`);
19101
+ if (remote.scope === "policy") {
19102
+ const written2 = await flags.client.putOrgPolicy({ config: source.config, version: remote.version });
19103
+ info(`pushed ${local} to org policy v${written2.version}`);
19104
+ return 0;
19105
+ }
19051
19106
  const body = {
19052
19107
  config: source.config,
19053
19108
  version: remote.version,
@@ -19120,7 +19175,7 @@ async function runSettings(args, flags) {
19120
19175
  const [scope = "user", action = "show", ...rest] = args;
19121
19176
  if (scope !== "user" && scope !== "org") {
19122
19177
  throw new PiPodError(`unknown settings scope "${scope}"`, {
19123
- hint: `${USAGE}; templates are managed with \`pipod templates\``
19178
+ hint: `${USAGE}; templates are managed with \`pipod templates\`, and org policy with \`pipod pull|diff|push policy\``
19124
19179
  });
19125
19180
  }
19126
19181
  if (action === "show") {