@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 +95 -80
- package/dist/cli.js.map +1 -1
- package/docs/config-simplification.md +6 -6
- package/docs/workspace-seeding.md +16 -11
- package/man/pipod-config.5 +42 -3
- package/man/pipod-config.5.md +40 -2
- package/man/pipod.1 +33 -20
- package/man/pipod.1.md +32 -19
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
19109
|
-
info(`pushed ${local} to org policy v${
|
|
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
|
|
19157
|
-
|
|
19158
|
-
|
|
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
|
|
19161
|
-
|
|
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
|
|
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(`${
|
|
19241
|
+
for (const warning of report.warnings) warn(`${remote.name}: ${warning}`);
|
|
19174
19242
|
if (report.errors.length > 0) {
|
|
19175
|
-
throw new PiPodError(`invalid ${
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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];
|