@pipod/cli 0.1.12 → 0.1.14

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
@@ -11829,7 +11829,7 @@ function isRecord2(value) {
11829
11829
 
11830
11830
  // src/account/gateway-rpc.ts
11831
11831
  import { randomBytes as randomBytes5, randomUUID as randomUUID4 } from "crypto";
11832
- import WebSocket2 from "ws";
11832
+ import WebSocket3 from "ws";
11833
11833
 
11834
11834
  // src/client/stream-events.ts
11835
11835
  function clone(value) {
@@ -12384,6 +12384,29 @@ function asleepReasonFromError(error2) {
12384
12384
  return "idle_stop";
12385
12385
  }
12386
12386
 
12387
+ // src/account/session-websocket.ts
12388
+ import { HttpsProxyAgent } from "https-proxy-agent";
12389
+ import { getProxyForUrl } from "proxy-from-env";
12390
+ import WebSocket2 from "ws";
12391
+ function createSessionWebSocket(url) {
12392
+ const target = new URL(url);
12393
+ const transportUrl = new URL(url);
12394
+ transportUrl.protocol = target.protocol === "wss:" ? "https:" : "http:";
12395
+ const proxy = getProxyForUrl(target.href) || getProxyForUrl(transportUrl.href);
12396
+ if (!proxy) return new WebSocket2(url);
12397
+ let agent;
12398
+ try {
12399
+ const proxyUrl = new URL(proxy);
12400
+ if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") {
12401
+ throw new Error("unsupported proxy protocol");
12402
+ }
12403
+ agent = new HttpsProxyAgent(proxyUrl);
12404
+ } catch {
12405
+ throw new Error("Invalid session proxy configuration: expected an HTTP(S) proxy URL");
12406
+ }
12407
+ return new WebSocket2(url, { agent });
12408
+ }
12409
+
12387
12410
  // src/account/gateway-rpc.ts
12388
12411
  var GATEWAY_MODELS_TIMEOUT_MS = 5e3;
12389
12412
  var FILE_LIST_MAX_ENTRIES = 50;
@@ -12797,7 +12820,7 @@ var GatewayRpcClient = class _GatewayRpcClient extends RpcClientBase {
12797
12820
  */
12798
12821
  async wake() {
12799
12822
  if (this.userClosed) throw new PiPodError("connection to the pi session was closed");
12800
- if (!this.closed && this.ws?.readyState === WebSocket2.OPEN) return;
12823
+ if (!this.closed && this.ws?.readyState === WebSocket3.OPEN) return;
12801
12824
  if (this.waking) {
12802
12825
  await this.waitUntilConnected();
12803
12826
  return;
@@ -12924,7 +12947,7 @@ var GatewayRpcClient = class _GatewayRpcClient extends RpcClientBase {
12924
12947
  }
12925
12948
  pingWatchdogTick() {
12926
12949
  const ws = this.ws;
12927
- if (!ws || ws.readyState !== WebSocket2.OPEN) return;
12950
+ if (!ws || ws.readyState !== WebSocket3.OPEN) return;
12928
12951
  const deadlineMs = this.opts.pongDeadlineMs ?? PONG_DEADLINE_MS;
12929
12952
  if (this.now() - this.lastActivityAt > deadlineMs) {
12930
12953
  debug("gateway-rpc: pong deadline exceeded; terminating half-open socket");
@@ -13057,7 +13080,7 @@ var GatewayRpcClient = class _GatewayRpcClient extends RpcClientBase {
13057
13080
  ticket,
13058
13081
  ...this.lastSessionId ? { fromSeq: this.lastSeq, fromSession: this.lastSessionId } : {}
13059
13082
  });
13060
- const create = this.opts.createWebSocket ?? ((wsUrl) => new WebSocket2(wsUrl));
13083
+ const create = this.opts.createWebSocket ?? createSessionWebSocket;
13061
13084
  const ws = create(url);
13062
13085
  this.connectingSockets.add(ws);
13063
13086
  let settled = false;
@@ -13542,7 +13565,7 @@ var GatewayRpcClient = class _GatewayRpcClient extends RpcClientBase {
13542
13565
  async waitUntilConnected() {
13543
13566
  const deadline = this.now() + (this.opts.dropRecoveryMs ?? DROP_RECOVERY_MS);
13544
13567
  while (this.now() < deadline) {
13545
- if (!this.closed && this.ws?.readyState === WebSocket2.OPEN) return;
13568
+ if (!this.closed && this.ws?.readyState === WebSocket3.OPEN) return;
13546
13569
  if (this.userClosed) throw new Error("connection to the pi session was closed");
13547
13570
  await new Promise((r) => setTimeout(r, 50));
13548
13571
  }
@@ -13599,7 +13622,7 @@ var GatewayRpcClient = class _GatewayRpcClient extends RpcClientBase {
13599
13622
  this.pendingSemantic.get(replyType)?.accept(value);
13600
13623
  }
13601
13624
  wsSend(message) {
13602
- if (!this.ws || this.ws.readyState !== WebSocket2.OPEN) {
13625
+ if (!this.ws || this.ws.readyState !== WebSocket3.OPEN) {
13603
13626
  throw this.connectionClosedError();
13604
13627
  }
13605
13628
  this.ws.send(JSON.stringify(message));
@@ -18958,7 +18981,7 @@ async function fetchRemoteLayer(client, layer, source) {
18958
18981
  const ref = layer.name ?? source.templateRef;
18959
18982
  if (!ref) {
18960
18983
  throw new PiPodError("this project does not pin a template", {
18961
- hint: "name one: `pipod push template <name>`; a first launch offers to bootstrap and pin one"
18984
+ hint: "name one after `template`, as in `template <name>`; a first launch offers to bootstrap and pin one"
18962
18985
  });
18963
18986
  }
18964
18987
  const template = await findTemplate(client, ref);
@@ -18968,6 +18991,7 @@ async function fetchRemoteLayer(client, layer, source) {
18968
18991
  bundle: templateBundle(template),
18969
18992
  version,
18970
18993
  secretNames: listed2.secrets.map((entry) => entry.name),
18994
+ name: `template ${template.name}`,
18971
18995
  label: `template ${template.name}${version !== void 0 ? ` v${version}` : ""}`,
18972
18996
  template,
18973
18997
  scope: "template",
@@ -18980,6 +19004,7 @@ async function fetchRemoteLayer(client, layer, source) {
18980
19004
  bundle: settingsBundle(current2),
18981
19005
  version: current2.version,
18982
19006
  secretNames: [],
19007
+ name: "org policy",
18983
19008
  label: `org policy v${current2.version}`,
18984
19009
  scope: "policy",
18985
19010
  scopeId: client.orgId
@@ -18992,11 +19017,25 @@ async function fetchRemoteLayer(client, layer, source) {
18992
19017
  bundle: settingsBundle(current),
18993
19018
  version: current.version,
18994
19019
  secretNames: listed.secrets.map((entry) => entry.name),
19020
+ name: `${layer.kind} settings`,
18995
19021
  label: `${layer.kind} settings v${current.version}`,
18996
19022
  scope: layer.kind,
18997
19023
  scopeId
18998
19024
  };
18999
19025
  }
19026
+ async function writeLayerConfig(client, remote, config) {
19027
+ if (remote.scope === "template") {
19028
+ const updated = await client.updateTemplate(remote.scopeId, {
19029
+ ...remote.version !== void 0 ? { expectedVersion: remote.version } : {},
19030
+ config
19031
+ });
19032
+ return updated.version;
19033
+ }
19034
+ if (remote.version === void 0) throw new PiPodError(`${remote.label} has no version to compare-and-swap against`);
19035
+ const body = { config, version: remote.version };
19036
+ const written = remote.scope === "policy" ? await client.putOrgPolicy(body) : remote.scope === "user" ? await client.putUserSettings(body) : await client.putOrgSettings(body);
19037
+ return written.version;
19038
+ }
19000
19039
  function localLabel(layer, flags) {
19001
19040
  if (layer.kind === "template") return "project";
19002
19041
  if (layer.kind === "user") return "~/.pi-pod + ~/.pi/agent";
@@ -19097,12 +19136,12 @@ async function runPush(args, flags) {
19097
19136
  await syncEnvSecrets(flags.client, updated, source.env, { withSecrets: parsed.withSecrets, reportStale: true });
19098
19137
  return 0;
19099
19138
  }
19100
- if (remote.version === void 0) throw new PiPodError(`${remote.label} has no version to compare-and-swap against`);
19101
19139
  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}`);
19140
+ const version = await writeLayerConfig(flags.client, remote, source.config);
19141
+ info(`pushed ${local} to org policy v${version}`);
19104
19142
  return 0;
19105
19143
  }
19144
+ if (remote.version === void 0) throw new PiPodError(`${remote.label} has no version to compare-and-swap against`);
19106
19145
  const body = {
19107
19146
  config: source.config,
19108
19147
  version: remote.version,
@@ -19147,53 +19186,93 @@ import * as fs32 from "fs";
19147
19186
  import * as os7 from "os";
19148
19187
  import * as path30 from "path";
19149
19188
  import { spawnSync as spawnSync2 } from "child_process";
19150
- var USAGE = "usage: pipod settings <user|org> [show | edit | set <key> <value> | unset <key>]";
19151
- function readBundle(client, scope) {
19152
- return scope === "user" ? client.getUserSettings() : client.getOrgSettings();
19189
+ var USAGE = "usage: pipod settings [user | org | policy | template [<name>]] [show | edit | set <key> <value> | unset <key>]";
19190
+ var ACTIONS = ["show", "edit", "set", "unset"];
19191
+ async function runSettings(args, flags) {
19192
+ const { layer, rest } = parseLayer(args);
19193
+ const command = parseCommand(layer, rest);
19194
+ const remote = await fetchRemoteLayer(flags.client, layer, {
19195
+ templateRef: layer.kind === "template" && layer.name === void 0 ? pinnedTemplateRef(flags) : null
19196
+ });
19197
+ if (command.action === "show") {
19198
+ out(JSON.stringify({ ...remote.bundle, version: remote.version }, null, 2));
19199
+ return 0;
19200
+ }
19201
+ const config = structuredClone(remote.bundle.config);
19202
+ if (command.action === "set") setDotPath(config, command.key, command.value);
19203
+ if (command.action === "unset") unsetDotPath(config, command.key);
19204
+ const next = command.action === "edit" ? editInEditor(config, flags) : config;
19205
+ assertConfigSendable(remote, next);
19206
+ const version = await writeLayerConfig(flags.client, remote, next);
19207
+ const done = command.action === "edit" ? "updated" : command.action;
19208
+ const key = command.action === "edit" ? "" : ` ${command.key}`;
19209
+ info(`${done} ${remote.name}${key}${version === void 0 ? "" : ` (v${version})`}`);
19210
+ return 0;
19211
+ }
19212
+ function parseLayer(args) {
19213
+ const [scope = "user", ...rest] = args;
19214
+ if (scope === "user" || scope === "org" || scope === "policy") return { layer: { kind: scope }, rest };
19215
+ if (scope === "template") {
19216
+ const [name, ...afterName] = rest;
19217
+ return name === void 0 || ACTIONS.includes(name) ? { layer: { kind: "template" }, rest } : { layer: { kind: "template", name }, rest: afterName };
19218
+ }
19219
+ throw new PiPodError(`unknown settings layer "${scope}"`, { hint: USAGE });
19220
+ }
19221
+ function parseCommand(layer, args) {
19222
+ const words = layer.kind === "template" && layer.name !== void 0 ? `template ${layer.name}` : layer.kind;
19223
+ const [action = "show", ...rest] = args;
19224
+ switch (action) {
19225
+ case "show":
19226
+ case "edit":
19227
+ if (rest.length > 0) throw new PiPodError(`usage: pipod settings ${words} ${action}`);
19228
+ return { action };
19229
+ case "set": {
19230
+ const [key, raw, ...extra] = rest;
19231
+ if (!key || raw === void 0 || extra.length > 0) throw new PiPodError(`usage: pipod settings ${words} set <key> <value>`);
19232
+ let value;
19233
+ try {
19234
+ value = JSON.parse(raw);
19235
+ } catch {
19236
+ value = raw;
19237
+ }
19238
+ return { action, key, value };
19239
+ }
19240
+ case "unset": {
19241
+ const [key, ...extra] = rest;
19242
+ if (!key || extra.length > 0) throw new PiPodError(`usage: pipod settings ${words} unset <key>`);
19243
+ return { action, key };
19244
+ }
19245
+ default:
19246
+ throw new PiPodError(`unknown settings action "${action}"`, { hint: "actions: show, edit, set, unset" });
19247
+ }
19153
19248
  }
19154
- function writeBundle(client, scope, body) {
19155
- return scope === "user" ? client.putUserSettings(body) : client.putOrgSettings(body);
19249
+ function pinnedTemplateRef(flags) {
19250
+ const configPath = findConfigPath(flags.cwd ?? process.cwd(), { home: flags.home });
19251
+ if (!configPath) return null;
19252
+ const raw = readObject(configPath);
19253
+ return takeClientOnlyKeys(raw).template || null;
19156
19254
  }
19157
- function assertBundleConfigSendable(config) {
19255
+ function assertConfigSendable(remote, config) {
19256
+ if (remote.scope === "policy") return;
19158
19257
  const dropped = nonBundleKeysFound(config);
19159
19258
  if (dropped.length > 0) {
19160
19259
  throw new PiPodError(`not sent: the server bundle no longer accepts ${dropped.join(", ")}`, {
19161
19260
  hint: "local-only keys stay in local files (`template`, `secretResolver`, `pi.chords`); pass --reuse at launch; retired keys must be deleted"
19162
19261
  });
19163
19262
  }
19164
- }
19165
- function assertBundleConfigValid(config, scope) {
19166
19263
  const report = validateConfig(config);
19167
- for (const warning of report.warnings) warn(`${scope} settings: ${warning}`);
19264
+ for (const warning of report.warnings) warn(`${remote.name}: ${warning}`);
19168
19265
  if (report.errors.length > 0) {
19169
- throw new PiPodError(`invalid ${scope} settings config: ${report.errors.join("; ")}`, {
19266
+ throw new PiPodError(`invalid ${remote.name} config: ${report.errors.join("; ")}`, {
19170
19267
  hint: "fix the config or edit it back \u2014 nothing was sent"
19171
19268
  });
19172
19269
  }
19173
19270
  }
19174
- async function runSettings(args, flags) {
19175
- const [scope = "user", action = "show", ...rest] = args;
19176
- if (scope !== "user" && scope !== "org") {
19177
- throw new PiPodError(`unknown settings scope "${scope}"`, {
19178
- hint: `${USAGE}; templates are managed with \`pipod templates\`, and org policy with \`pipod pull|diff|push policy\``
19179
- });
19180
- }
19181
- if (action === "show") {
19182
- if (rest.length > 0) throw new PiPodError(`usage: pipod settings ${scope}`);
19183
- out(JSON.stringify(await readBundle(flags.client, scope), null, 2));
19184
- return 0;
19185
- }
19186
- if (action === "edit") return editSettings(scope, flags);
19187
- if (action === "set") return setSetting(scope, rest, flags.client);
19188
- if (action === "unset") return unsetSetting(scope, rest, flags.client);
19189
- throw new PiPodError(`unknown settings action "${action}"`, { hint: "actions: show, edit, set, unset" });
19190
- }
19191
- async function editSettings(scope, flags) {
19192
- const current = await readBundle(flags.client, scope);
19271
+ function editInEditor(config, flags) {
19193
19272
  const directory = fs32.mkdtempSync(path30.join(os7.tmpdir(), "pi-pod-settings-"));
19194
19273
  const file = path30.join(directory, "config.json");
19195
19274
  try {
19196
- fs32.writeFileSync(file, `${JSON.stringify(current.config, null, 2)}
19275
+ fs32.writeFileSync(file, `${JSON.stringify(config, null, 2)}
19197
19276
  `, { mode: 384 });
19198
19277
  const editor = flags.editor ?? process.env["VISUAL"] ?? process.env["EDITOR"];
19199
19278
  if (!editor) throw new PiPodError("$EDITOR is not set");
@@ -19203,46 +19282,11 @@ async function editSettings(scope, flags) {
19203
19282
  });
19204
19283
  if (edited.error) throw new PiPodError(`could not run editor: ${edited.error.message}`);
19205
19284
  if (edited.status !== 0) throw new PiPodError(`editor exited with status ${edited.status ?? "unknown"}`);
19206
- const config = readObject(file);
19207
- assertBundleConfigSendable(config);
19208
- assertBundleConfigValid(config, scope);
19209
- const result = await writeBundle(flags.client, scope, { config, version: current.version });
19210
- info(`updated ${scope} settings v${result.version}`);
19211
- return 0;
19285
+ return readObject(file);
19212
19286
  } finally {
19213
19287
  fs32.rmSync(directory, { recursive: true, force: true });
19214
19288
  }
19215
19289
  }
19216
- async function setSetting(scope, args, client) {
19217
- const [dotPath, rawValue, ...extra] = args;
19218
- if (!dotPath || rawValue === void 0 || extra.length > 0) {
19219
- throw new PiPodError(`usage: pipod settings ${scope} set <key> <value>`);
19220
- }
19221
- let value;
19222
- try {
19223
- value = JSON.parse(rawValue);
19224
- } catch {
19225
- value = rawValue;
19226
- }
19227
- const version = await updateConfig(scope, client, (config) => setDotPath(config, dotPath, value));
19228
- info(`set ${scope} setting ${dotPath} (v${version})`);
19229
- return 0;
19230
- }
19231
- async function unsetSetting(scope, args, client) {
19232
- const [dotPath, ...extra] = args;
19233
- if (!dotPath || extra.length > 0) throw new PiPodError(`usage: pipod settings ${scope} unset <key>`);
19234
- const version = await updateConfig(scope, client, (config) => unsetDotPath(config, dotPath));
19235
- info(`unset ${scope} setting ${dotPath} (v${version})`);
19236
- return 0;
19237
- }
19238
- async function updateConfig(scope, client, change) {
19239
- const current = await readBundle(client, scope);
19240
- const config = structuredClone(current.config);
19241
- change(config);
19242
- assertBundleConfigSendable(config);
19243
- assertBundleConfigValid(config, scope);
19244
- return (await writeBundle(client, scope, { config, version: current.version })).version;
19245
- }
19246
19290
  function unsetDotPath(target, dotPath) {
19247
19291
  const [head, ...rest] = dotPath.split(".");
19248
19292
  const child = target[head];