@pipod/cli 0.1.11 → 0.1.13

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
@@ -3426,11 +3426,6 @@ async function probeWorkspaceSeed(opts) {
3426
3426
  const branchOut = git(["symbolic-ref", "--quiet", "--short", "HEAD"]);
3427
3427
  const branch = branchOut.stdout.trim();
3428
3428
  if (branchOut.status !== 0 || branch === "") return archive("detached HEAD");
3429
- const status = git(["status", "--porcelain", "--untracked-files=all"]);
3430
- if (status.status !== 0) return archive("cannot tell whether the working tree is clean");
3431
- if (status.stdout.trim() !== "") {
3432
- return archive("uncommitted changes would be lost by a clone");
3433
- }
3434
3429
  const upstream = git(["rev-parse", "--abbrev-ref", "@{u}"]);
3435
3430
  if (upstream.status !== 0) return archive(`${branch} is not pushed`);
3436
3431
  const ahead = git(["rev-list", "@{u}..HEAD"]);
@@ -3545,7 +3540,8 @@ function describeWorkspaceSeed(plan) {
3545
3540
  return plan.reason;
3546
3541
  case "clone": {
3547
3542
  const base = `clone ${plan.url} at ${plan.branch} (${plan.commit.slice(0, 12)})`;
3548
- 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)`;
3549
3545
  }
3550
3546
  case "archive":
3551
3547
  return `archive ${plan.root}${plan.includeGit ? " with .git history" : ""} (${plan.reason})`;
@@ -5262,7 +5258,7 @@ async function cloneIntoPod(opts, plan) {
5262
5258
  if (isMissingRoute(error2)) {
5263
5259
  info("this server predates workspace cloning \u2014 sending an archive instead");
5264
5260
  } else {
5265
- 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)`);
5266
5262
  }
5267
5263
  return archiveIntoPod(
5268
5264
  opts,
@@ -15664,9 +15660,7 @@ function announceWorkspaceSeed(plan) {
15664
15660
  info(`workspace transport: this directory will be copied into the pod (${seed.reason})`);
15665
15661
  return;
15666
15662
  case "clone":
15667
- info(
15668
- `workspace seed: the pod will clone ${seed.url} at ${seed.branch} (${seed.commit.slice(0, 12)})` + (seed.access === "credential" ? ` with forwarded ${seed.host} credentials` : "")
15669
- );
15663
+ info(`workspace seed: the pod will ${describeWorkspaceSeed(seed)}`);
15670
15664
  break;
15671
15665
  case "archive":
15672
15666
  info(
@@ -18964,7 +18958,7 @@ async function fetchRemoteLayer(client, layer, source) {
18964
18958
  const ref = layer.name ?? source.templateRef;
18965
18959
  if (!ref) {
18966
18960
  throw new PiPodError("this project does not pin a template", {
18967
- hint: "name one: `pipod push template <name>`; a first launch offers to bootstrap and pin one"
18961
+ hint: "name one after `template`, as in `template <name>`; a first launch offers to bootstrap and pin one"
18968
18962
  });
18969
18963
  }
18970
18964
  const template = await findTemplate(client, ref);
@@ -18974,6 +18968,7 @@ async function fetchRemoteLayer(client, layer, source) {
18974
18968
  bundle: templateBundle(template),
18975
18969
  version,
18976
18970
  secretNames: listed2.secrets.map((entry) => entry.name),
18971
+ name: `template ${template.name}`,
18977
18972
  label: `template ${template.name}${version !== void 0 ? ` v${version}` : ""}`,
18978
18973
  template,
18979
18974
  scope: "template",
@@ -18986,6 +18981,7 @@ async function fetchRemoteLayer(client, layer, source) {
18986
18981
  bundle: settingsBundle(current2),
18987
18982
  version: current2.version,
18988
18983
  secretNames: [],
18984
+ name: "org policy",
18989
18985
  label: `org policy v${current2.version}`,
18990
18986
  scope: "policy",
18991
18987
  scopeId: client.orgId
@@ -18998,11 +18994,25 @@ async function fetchRemoteLayer(client, layer, source) {
18998
18994
  bundle: settingsBundle(current),
18999
18995
  version: current.version,
19000
18996
  secretNames: listed.secrets.map((entry) => entry.name),
18997
+ name: `${layer.kind} settings`,
19001
18998
  label: `${layer.kind} settings v${current.version}`,
19002
18999
  scope: layer.kind,
19003
19000
  scopeId
19004
19001
  };
19005
19002
  }
19003
+ async function writeLayerConfig(client, remote, config) {
19004
+ if (remote.scope === "template") {
19005
+ const updated = await client.updateTemplate(remote.scopeId, {
19006
+ ...remote.version !== void 0 ? { expectedVersion: remote.version } : {},
19007
+ config
19008
+ });
19009
+ return updated.version;
19010
+ }
19011
+ if (remote.version === void 0) throw new PiPodError(`${remote.label} has no version to compare-and-swap against`);
19012
+ const body = { config, version: remote.version };
19013
+ const written = remote.scope === "policy" ? await client.putOrgPolicy(body) : remote.scope === "user" ? await client.putUserSettings(body) : await client.putOrgSettings(body);
19014
+ return written.version;
19015
+ }
19006
19016
  function localLabel(layer, flags) {
19007
19017
  if (layer.kind === "template") return "project";
19008
19018
  if (layer.kind === "user") return "~/.pi-pod + ~/.pi/agent";
@@ -19103,12 +19113,12 @@ async function runPush(args, flags) {
19103
19113
  await syncEnvSecrets(flags.client, updated, source.env, { withSecrets: parsed.withSecrets, reportStale: true });
19104
19114
  return 0;
19105
19115
  }
19106
- if (remote.version === void 0) throw new PiPodError(`${remote.label} has no version to compare-and-swap against`);
19107
19116
  if (remote.scope === "policy") {
19108
- const written2 = await flags.client.putOrgPolicy({ config: source.config, version: remote.version });
19109
- info(`pushed ${local} to org policy v${written2.version}`);
19117
+ const version = await writeLayerConfig(flags.client, remote, source.config);
19118
+ info(`pushed ${local} to org policy v${version}`);
19110
19119
  return 0;
19111
19120
  }
19121
+ if (remote.version === void 0) throw new PiPodError(`${remote.label} has no version to compare-and-swap against`);
19112
19122
  const body = {
19113
19123
  config: source.config,
19114
19124
  version: remote.version,
@@ -19153,53 +19163,93 @@ import * as fs32 from "fs";
19153
19163
  import * as os7 from "os";
19154
19164
  import * as path30 from "path";
19155
19165
  import { spawnSync as spawnSync2 } from "child_process";
19156
- var USAGE = "usage: pipod settings <user|org> [show | edit | set <key> <value> | unset <key>]";
19157
- function readBundle(client, scope) {
19158
- return scope === "user" ? client.getUserSettings() : client.getOrgSettings();
19166
+ var USAGE = "usage: pipod settings [user | org | policy | template [<name>]] [show | edit | set <key> <value> | unset <key>]";
19167
+ var ACTIONS = ["show", "edit", "set", "unset"];
19168
+ async function runSettings(args, flags) {
19169
+ const { layer, rest } = parseLayer(args);
19170
+ const command = parseCommand(layer, rest);
19171
+ const remote = await fetchRemoteLayer(flags.client, layer, {
19172
+ templateRef: layer.kind === "template" && layer.name === void 0 ? pinnedTemplateRef(flags) : null
19173
+ });
19174
+ if (command.action === "show") {
19175
+ out(JSON.stringify({ ...remote.bundle, version: remote.version }, null, 2));
19176
+ return 0;
19177
+ }
19178
+ const config = structuredClone(remote.bundle.config);
19179
+ if (command.action === "set") setDotPath(config, command.key, command.value);
19180
+ if (command.action === "unset") unsetDotPath(config, command.key);
19181
+ const next = command.action === "edit" ? editInEditor(config, flags) : config;
19182
+ assertConfigSendable(remote, next);
19183
+ const version = await writeLayerConfig(flags.client, remote, next);
19184
+ const done = command.action === "edit" ? "updated" : command.action;
19185
+ const key = command.action === "edit" ? "" : ` ${command.key}`;
19186
+ info(`${done} ${remote.name}${key}${version === void 0 ? "" : ` (v${version})`}`);
19187
+ return 0;
19159
19188
  }
19160
- function writeBundle(client, scope, body) {
19161
- return scope === "user" ? client.putUserSettings(body) : client.putOrgSettings(body);
19189
+ function parseLayer(args) {
19190
+ const [scope = "user", ...rest] = args;
19191
+ if (scope === "user" || scope === "org" || scope === "policy") return { layer: { kind: scope }, rest };
19192
+ if (scope === "template") {
19193
+ const [name, ...afterName] = rest;
19194
+ return name === void 0 || ACTIONS.includes(name) ? { layer: { kind: "template" }, rest } : { layer: { kind: "template", name }, rest: afterName };
19195
+ }
19196
+ throw new PiPodError(`unknown settings layer "${scope}"`, { hint: USAGE });
19162
19197
  }
19163
- function assertBundleConfigSendable(config) {
19198
+ function parseCommand(layer, args) {
19199
+ const words = layer.kind === "template" && layer.name !== void 0 ? `template ${layer.name}` : layer.kind;
19200
+ const [action = "show", ...rest] = args;
19201
+ switch (action) {
19202
+ case "show":
19203
+ case "edit":
19204
+ if (rest.length > 0) throw new PiPodError(`usage: pipod settings ${words} ${action}`);
19205
+ return { action };
19206
+ case "set": {
19207
+ const [key, raw, ...extra] = rest;
19208
+ if (!key || raw === void 0 || extra.length > 0) throw new PiPodError(`usage: pipod settings ${words} set <key> <value>`);
19209
+ let value;
19210
+ try {
19211
+ value = JSON.parse(raw);
19212
+ } catch {
19213
+ value = raw;
19214
+ }
19215
+ return { action, key, value };
19216
+ }
19217
+ case "unset": {
19218
+ const [key, ...extra] = rest;
19219
+ if (!key || extra.length > 0) throw new PiPodError(`usage: pipod settings ${words} unset <key>`);
19220
+ return { action, key };
19221
+ }
19222
+ default:
19223
+ throw new PiPodError(`unknown settings action "${action}"`, { hint: "actions: show, edit, set, unset" });
19224
+ }
19225
+ }
19226
+ function pinnedTemplateRef(flags) {
19227
+ const configPath = findConfigPath(flags.cwd ?? process.cwd(), { home: flags.home });
19228
+ if (!configPath) return null;
19229
+ const raw = readObject(configPath);
19230
+ return takeClientOnlyKeys(raw).template || null;
19231
+ }
19232
+ function assertConfigSendable(remote, config) {
19233
+ if (remote.scope === "policy") return;
19164
19234
  const dropped = nonBundleKeysFound(config);
19165
19235
  if (dropped.length > 0) {
19166
19236
  throw new PiPodError(`not sent: the server bundle no longer accepts ${dropped.join(", ")}`, {
19167
19237
  hint: "local-only keys stay in local files (`template`, `secretResolver`, `pi.chords`); pass --reuse at launch; retired keys must be deleted"
19168
19238
  });
19169
19239
  }
19170
- }
19171
- function assertBundleConfigValid(config, scope) {
19172
19240
  const report = validateConfig(config);
19173
- for (const warning of report.warnings) warn(`${scope} settings: ${warning}`);
19241
+ for (const warning of report.warnings) warn(`${remote.name}: ${warning}`);
19174
19242
  if (report.errors.length > 0) {
19175
- throw new PiPodError(`invalid ${scope} settings config: ${report.errors.join("; ")}`, {
19243
+ throw new PiPodError(`invalid ${remote.name} config: ${report.errors.join("; ")}`, {
19176
19244
  hint: "fix the config or edit it back \u2014 nothing was sent"
19177
19245
  });
19178
19246
  }
19179
19247
  }
19180
- async function runSettings(args, flags) {
19181
- const [scope = "user", action = "show", ...rest] = args;
19182
- if (scope !== "user" && scope !== "org") {
19183
- throw new PiPodError(`unknown settings scope "${scope}"`, {
19184
- hint: `${USAGE}; templates are managed with \`pipod templates\`, and org policy with \`pipod pull|diff|push policy\``
19185
- });
19186
- }
19187
- if (action === "show") {
19188
- if (rest.length > 0) throw new PiPodError(`usage: pipod settings ${scope}`);
19189
- out(JSON.stringify(await readBundle(flags.client, scope), null, 2));
19190
- return 0;
19191
- }
19192
- if (action === "edit") return editSettings(scope, flags);
19193
- if (action === "set") return setSetting(scope, rest, flags.client);
19194
- if (action === "unset") return unsetSetting(scope, rest, flags.client);
19195
- throw new PiPodError(`unknown settings action "${action}"`, { hint: "actions: show, edit, set, unset" });
19196
- }
19197
- async function editSettings(scope, flags) {
19198
- const current = await readBundle(flags.client, scope);
19248
+ function editInEditor(config, flags) {
19199
19249
  const directory = fs32.mkdtempSync(path30.join(os7.tmpdir(), "pi-pod-settings-"));
19200
19250
  const file = path30.join(directory, "config.json");
19201
19251
  try {
19202
- fs32.writeFileSync(file, `${JSON.stringify(current.config, null, 2)}
19252
+ fs32.writeFileSync(file, `${JSON.stringify(config, null, 2)}
19203
19253
  `, { mode: 384 });
19204
19254
  const editor = flags.editor ?? process.env["VISUAL"] ?? process.env["EDITOR"];
19205
19255
  if (!editor) throw new PiPodError("$EDITOR is not set");
@@ -19209,46 +19259,11 @@ async function editSettings(scope, flags) {
19209
19259
  });
19210
19260
  if (edited.error) throw new PiPodError(`could not run editor: ${edited.error.message}`);
19211
19261
  if (edited.status !== 0) throw new PiPodError(`editor exited with status ${edited.status ?? "unknown"}`);
19212
- const config = readObject(file);
19213
- assertBundleConfigSendable(config);
19214
- assertBundleConfigValid(config, scope);
19215
- const result = await writeBundle(flags.client, scope, { config, version: current.version });
19216
- info(`updated ${scope} settings v${result.version}`);
19217
- return 0;
19262
+ return readObject(file);
19218
19263
  } finally {
19219
19264
  fs32.rmSync(directory, { recursive: true, force: true });
19220
19265
  }
19221
19266
  }
19222
- async function setSetting(scope, args, client) {
19223
- const [dotPath, rawValue, ...extra] = args;
19224
- if (!dotPath || rawValue === void 0 || extra.length > 0) {
19225
- throw new PiPodError(`usage: pipod settings ${scope} set <key> <value>`);
19226
- }
19227
- let value;
19228
- try {
19229
- value = JSON.parse(rawValue);
19230
- } catch {
19231
- value = rawValue;
19232
- }
19233
- const version = await updateConfig(scope, client, (config) => setDotPath(config, dotPath, value));
19234
- info(`set ${scope} setting ${dotPath} (v${version})`);
19235
- return 0;
19236
- }
19237
- async function unsetSetting(scope, args, client) {
19238
- const [dotPath, ...extra] = args;
19239
- if (!dotPath || extra.length > 0) throw new PiPodError(`usage: pipod settings ${scope} unset <key>`);
19240
- const version = await updateConfig(scope, client, (config) => unsetDotPath(config, dotPath));
19241
- info(`unset ${scope} setting ${dotPath} (v${version})`);
19242
- return 0;
19243
- }
19244
- async function updateConfig(scope, client, change) {
19245
- const current = await readBundle(client, scope);
19246
- const config = structuredClone(current.config);
19247
- change(config);
19248
- assertBundleConfigSendable(config);
19249
- assertBundleConfigValid(config, scope);
19250
- return (await writeBundle(client, scope, { config, version: current.version })).version;
19251
- }
19252
19267
  function unsetDotPath(target, dotPath) {
19253
19268
  const [head, ...rest] = dotPath.split(".");
19254
19269
  const child = target[head];