@pipod/cli 0.1.4 → 0.1.6
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/README.md +9 -8
- package/dist/cli.js +189 -24
- package/dist/cli.js.map +1 -1
- package/docs/config-simplification.md +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,15 +4,16 @@ Run [pi](https://github.com/earendil-works/pi) coding-agent sessions in server-m
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @pipod/cli # or from a checkout of this directory: npm ci && npm run build && npm install -g .
|
|
7
|
-
pipod login --server <url> # sign in to
|
|
7
|
+
pipod login --server <url> # sign in to your pi pod server
|
|
8
8
|
pipod # launch a pod for the current directory and attach
|
|
9
9
|
pipod --help # every command and launch option
|
|
10
10
|
pipod update # upgrade the way it was installed
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Running your own server: [docs/self-host.md](https://github.com/pi-pod/pipod/blob/main/docs/self-host.md).
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
The npm release works with any server at least as new as it; a CLI newer than its server is
|
|
15
|
+
refused, so upgrade the server first. Building the CLI from the server's own checkout always
|
|
16
|
+
matches.
|
|
16
17
|
|
|
17
18
|
## Secrets
|
|
18
19
|
|
|
@@ -53,12 +54,12 @@ Details, limits and a manual test checklist: [docs/workspace-seeding.md](docs/wo
|
|
|
53
54
|
Local images named in a prompt (clipboard pastes, `@shot.png`, file paths) are
|
|
54
55
|
attached inline for the pod agent: [docs/image-attachments.md](docs/image-attachments.md).
|
|
55
56
|
|
|
56
|
-
## Personal workstations (
|
|
57
|
+
## Personal workstations (hosted service)
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
a number. Nothing is lost while it sleeps: every workspace stays on the workstation's disk.
|
|
59
|
+
This section does not apply to a self-hosted server. On the hosted service (coming soon) each
|
|
60
|
+
user gets a whole machine of their own, which sleeps when idle. The first command after it
|
|
61
|
+
sleeps waits for it to come back — measured at 243 to 708 seconds in production, so the CLI
|
|
62
|
+
shows elapsed progress and says "several minutes" rather than promising a number. Nothing is lost while it sleeps: every workspace stays on the workstation's disk.
|
|
62
63
|
|
|
63
64
|
The hosted service also reports the account's plan, active hours and spend cap, which `pipod
|
|
64
65
|
list` prints under the pods. A self-hosted server reports none of that, and the client then
|
package/dist/cli.js
CHANGED
|
@@ -256,6 +256,7 @@ async function discover(issuer) {
|
|
|
256
256
|
authorization_endpoint: body.authorization_endpoint,
|
|
257
257
|
token_endpoint: body.token_endpoint,
|
|
258
258
|
jwks_uri: body.jwks_uri,
|
|
259
|
+
device_authorization_endpoint: body.device_authorization_endpoint,
|
|
259
260
|
end_session_endpoint: body.end_session_endpoint,
|
|
260
261
|
revocation_endpoint: body.revocation_endpoint
|
|
261
262
|
})) {
|
|
@@ -270,6 +271,7 @@ async function discover(issuer) {
|
|
|
270
271
|
authorization_endpoint: body.authorization_endpoint,
|
|
271
272
|
token_endpoint: body.token_endpoint,
|
|
272
273
|
jwks_uri: body.jwks_uri,
|
|
274
|
+
...body.device_authorization_endpoint ? { device_authorization_endpoint: body.device_authorization_endpoint } : {},
|
|
273
275
|
...body.end_session_endpoint ? { end_session_endpoint: body.end_session_endpoint } : {},
|
|
274
276
|
...body.revocation_endpoint ? { revocation_endpoint: body.revocation_endpoint } : {}
|
|
275
277
|
};
|
|
@@ -293,15 +295,18 @@ function organizationScope(alias) {
|
|
|
293
295
|
}
|
|
294
296
|
return `urn:zitadel:iam:org:domain:primary:${alias}`;
|
|
295
297
|
}
|
|
298
|
+
function loginScopes(orgAlias) {
|
|
299
|
+
const scopes = ["openid", "profile", "email", OFFLINE_ACCESS_SCOPE, RESOURCE_OWNER_SCOPE, PROJECT_ROLES_SCOPE];
|
|
300
|
+
const orgScope = organizationScope(orgAlias);
|
|
301
|
+
if (orgScope) scopes.push(orgScope);
|
|
302
|
+
return scopes.join(" ");
|
|
303
|
+
}
|
|
296
304
|
function authorizeUrl(meta, args) {
|
|
297
305
|
const url = new URL(meta.authorization_endpoint);
|
|
298
306
|
url.searchParams.set("response_type", "code");
|
|
299
307
|
url.searchParams.set("client_id", args.clientId);
|
|
300
308
|
url.searchParams.set("redirect_uri", args.redirectUri);
|
|
301
|
-
|
|
302
|
-
const orgScope = organizationScope(args.orgAlias);
|
|
303
|
-
if (orgScope) scopes.push(orgScope);
|
|
304
|
-
url.searchParams.set("scope", scopes.join(" "));
|
|
309
|
+
url.searchParams.set("scope", loginScopes(args.orgAlias));
|
|
305
310
|
url.searchParams.set("code_challenge", args.challenge);
|
|
306
311
|
url.searchParams.set("code_challenge_method", "S256");
|
|
307
312
|
url.searchParams.set("state", args.state);
|
|
@@ -345,11 +350,105 @@ async function exchangeCode(meta, args) {
|
|
|
345
350
|
hint: String(e instanceof Error ? e.message : e)
|
|
346
351
|
});
|
|
347
352
|
});
|
|
348
|
-
|
|
353
|
+
return verifiedLoginTokens(meta, await readTokenResponse(res), args.clientId, args.nonce);
|
|
354
|
+
}
|
|
355
|
+
async function verifiedLoginTokens(meta, tokens, clientId, nonce) {
|
|
349
356
|
if (!tokens.idToken) throw new PiPodError("the identity provider returned no ID token");
|
|
350
|
-
await verifyIdToken(meta, tokens.idToken,
|
|
357
|
+
await verifyIdToken(meta, tokens.idToken, clientId, tokens.accessToken, nonce);
|
|
351
358
|
return tokens;
|
|
352
359
|
}
|
|
360
|
+
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
361
|
+
var DEVICE_POLL_SECONDS = 5;
|
|
362
|
+
async function startDeviceAuthorization(meta, args) {
|
|
363
|
+
if (!meta.device_authorization_endpoint) return null;
|
|
364
|
+
const res = await fetch(meta.device_authorization_endpoint, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
redirect: "error",
|
|
367
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
368
|
+
body: new URLSearchParams({ client_id: args.clientId, scope: loginScopes(args.orgAlias) }),
|
|
369
|
+
signal: AbortSignal.timeout(OIDC_NETWORK_TIMEOUT_MS)
|
|
370
|
+
}).catch((e) => {
|
|
371
|
+
throw new PiPodError("cannot reach the identity provider device endpoint", {
|
|
372
|
+
hint: String(e instanceof Error ? e.message : e)
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
if (!res.ok) {
|
|
376
|
+
if (await oauthError(res) === "unauthorized_client") return null;
|
|
377
|
+
throw new PiPodError("the identity provider refused to start a device sign-in", {
|
|
378
|
+
hint: `HTTP ${res.status} \u2014 run \`pipod login\` again`
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
const body = await res.json().catch(() => null);
|
|
382
|
+
const deviceCode = body?.["device_code"];
|
|
383
|
+
const userCode = body?.["user_code"];
|
|
384
|
+
const verificationUri = body?.["verification_uri"];
|
|
385
|
+
const verificationUriComplete = body?.["verification_uri_complete"];
|
|
386
|
+
const expiresIn = body?.["expires_in"];
|
|
387
|
+
const interval = body?.["interval"];
|
|
388
|
+
if (typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string" || typeof expiresIn !== "number") {
|
|
389
|
+
throw new PiPodError("the identity provider returned an invalid device sign-in");
|
|
390
|
+
}
|
|
391
|
+
parseSecureOidcUrl(verificationUri, "verification_uri");
|
|
392
|
+
if (typeof verificationUriComplete === "string") {
|
|
393
|
+
parseSecureOidcUrl(verificationUriComplete, "verification_uri_complete");
|
|
394
|
+
}
|
|
395
|
+
const authorization = {
|
|
396
|
+
userCode,
|
|
397
|
+
verificationUri,
|
|
398
|
+
...typeof verificationUriComplete === "string" ? { verificationUriComplete } : {},
|
|
399
|
+
deviceCode,
|
|
400
|
+
intervalSeconds: typeof interval === "number" && interval > 0 ? interval : DEVICE_POLL_SECONDS,
|
|
401
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
402
|
+
};
|
|
403
|
+
const probe = await pollDeviceToken(meta, args.clientId, deviceCode);
|
|
404
|
+
if ("error" in probe && probe.error === "unauthorized_client") return null;
|
|
405
|
+
if ("error" in probe && probe.error === "slow_down") authorization.intervalSeconds += DEVICE_POLL_SECONDS;
|
|
406
|
+
return authorization;
|
|
407
|
+
}
|
|
408
|
+
async function awaitDeviceTokens(meta, args) {
|
|
409
|
+
let intervalSeconds = args.authorization.intervalSeconds;
|
|
410
|
+
while (true) {
|
|
411
|
+
await new Promise((resolve14) => setTimeout(resolve14, intervalSeconds * 1e3));
|
|
412
|
+
if (Date.now() >= args.authorization.expiresAt) throw deviceCodeExpired();
|
|
413
|
+
const poll = await pollDeviceToken(meta, args.clientId, args.authorization.deviceCode);
|
|
414
|
+
if ("tokens" in poll) return verifiedLoginTokens(meta, poll.tokens, args.clientId);
|
|
415
|
+
switch (poll.error) {
|
|
416
|
+
case "authorization_pending":
|
|
417
|
+
case "unreachable":
|
|
418
|
+
continue;
|
|
419
|
+
case "slow_down":
|
|
420
|
+
intervalSeconds += DEVICE_POLL_SECONDS;
|
|
421
|
+
continue;
|
|
422
|
+
case "access_denied":
|
|
423
|
+
throw new PiPodError("the sign-in was declined", { hint: "run `pipod login` again" });
|
|
424
|
+
case "expired_token":
|
|
425
|
+
throw deviceCodeExpired();
|
|
426
|
+
default:
|
|
427
|
+
throw new PiPodError(`the identity provider refused the device sign-in (${poll.error})`, {
|
|
428
|
+
hint: "run `pipod login` again"
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function deviceCodeExpired() {
|
|
434
|
+
return new PiPodError("the sign-in code expired before it was used", { hint: "run `pipod login` again" });
|
|
435
|
+
}
|
|
436
|
+
async function pollDeviceToken(meta, clientId, deviceCode) {
|
|
437
|
+
const res = await fetch(meta.token_endpoint, {
|
|
438
|
+
method: "POST",
|
|
439
|
+
redirect: "error",
|
|
440
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
441
|
+
body: new URLSearchParams({ grant_type: DEVICE_CODE_GRANT, client_id: clientId, device_code: deviceCode }),
|
|
442
|
+
signal: AbortSignal.timeout(OIDC_NETWORK_TIMEOUT_MS)
|
|
443
|
+
}).catch(() => null);
|
|
444
|
+
if (!res) return { error: "unreachable" };
|
|
445
|
+
if (res.ok) return { tokens: await readTokenResponse(res) };
|
|
446
|
+
return { error: await oauthError(res) ?? `HTTP ${res.status}` };
|
|
447
|
+
}
|
|
448
|
+
async function oauthError(res) {
|
|
449
|
+
const body = await res.json().catch(() => null);
|
|
450
|
+
return typeof body?.error === "string" ? body.error : null;
|
|
451
|
+
}
|
|
353
452
|
async function verifyIdToken(meta, token, clientId, accessToken, expectedNonce) {
|
|
354
453
|
let jwks = idTokenJwksCache.get(meta.jwks_uri);
|
|
355
454
|
if (!jwks) {
|
|
@@ -7507,7 +7606,7 @@ function unattestedPackageNotice(podPackages, specs) {
|
|
|
7507
7606
|
const remainder = mismatches.length - shown.length;
|
|
7508
7607
|
const examples = `${shown.join(", ")}${remainder > 0 ? `, and ${remainder} more` : ""}`;
|
|
7509
7608
|
const noun = mismatches.length === 1 ? "package" : "packages";
|
|
7510
|
-
return `pod reports ${mismatches.length} configured ${noun} outside its exact launch-attested set: ${examples}; extension rendering from ${mismatches.length === 1 ? "that package is" : "those packages is"} unavailable locally \u2014 land intentional top-level Pi packages in your bundle's settings.json "packages" (pipod push
|
|
7609
|
+
return `pod reports ${mismatches.length} configured ${noun} outside its exact launch-attested set: ${examples}; extension rendering from ${mismatches.length === 1 ? "that package is" : "those packages is"} unavailable locally \u2014 land intentional top-level Pi packages in your bundle's settings.json "packages" (pipod push); do not add transitive dependencies`;
|
|
7511
7610
|
}
|
|
7512
7611
|
|
|
7513
7612
|
// src/client/runtime/pod-command-overrides.ts
|
|
@@ -17020,7 +17119,7 @@ async function runLogin(flags) {
|
|
|
17020
17119
|
if (flags.token) {
|
|
17021
17120
|
accessToken = flags.token;
|
|
17022
17121
|
} else {
|
|
17023
|
-
const pair = await
|
|
17122
|
+
const pair = await interactiveLogin({ issuer, clientId, orgAlias: flags.org, device: flags.device === true });
|
|
17024
17123
|
accessToken = pair.accessToken;
|
|
17025
17124
|
refreshToken = pair.refreshToken;
|
|
17026
17125
|
idToken = pair.idToken;
|
|
@@ -17162,8 +17261,36 @@ async function openConsole(flags, which) {
|
|
|
17162
17261
|
openBrowser2(url);
|
|
17163
17262
|
return 0;
|
|
17164
17263
|
}
|
|
17165
|
-
async function
|
|
17264
|
+
async function interactiveLogin(args) {
|
|
17166
17265
|
const meta = await discover(args.issuer);
|
|
17266
|
+
const browserHere = canOpenBrowserHere();
|
|
17267
|
+
if (args.device || !browserHere) {
|
|
17268
|
+
const authorization = await startDeviceAuthorization(meta, { clientId: args.clientId, orgAlias: args.orgAlias });
|
|
17269
|
+
if (authorization) {
|
|
17270
|
+
const page = authorization.verificationUriComplete ?? authorization.verificationUri;
|
|
17271
|
+
const verb = authorization.verificationUriComplete ? "confirm" : "enter";
|
|
17272
|
+
info(`to sign in, open this page on any device and ${verb} the code ${authorization.userCode}:
|
|
17273
|
+
${page}`);
|
|
17274
|
+
info("waiting for you to finish signing in\u2026");
|
|
17275
|
+
return awaitDeviceTokens(meta, { clientId: args.clientId, authorization });
|
|
17276
|
+
}
|
|
17277
|
+
if (args.device) {
|
|
17278
|
+
throw new PiPodError(`${args.issuer} does not allow this CLI to sign in with a code`, {
|
|
17279
|
+
hint: "on a self-hosted server the operator enables it with `selfhost/upgrade --apply-zitadel`; until then run `pipod login` without --device"
|
|
17280
|
+
});
|
|
17281
|
+
}
|
|
17282
|
+
info("this identity provider does not offer sign-in with a code; using the browser flow");
|
|
17283
|
+
}
|
|
17284
|
+
return browserLogin(meta, { clientId: args.clientId, orgAlias: args.orgAlias, browserHere });
|
|
17285
|
+
}
|
|
17286
|
+
function canOpenBrowserHere() {
|
|
17287
|
+
const env = process.env;
|
|
17288
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
17289
|
+
return !env["SSH_CONNECTION"] && !env["SSH_TTY"];
|
|
17290
|
+
}
|
|
17291
|
+
return Boolean(env["DISPLAY"] || env["WAYLAND_DISPLAY"] || env["BROWSER"]);
|
|
17292
|
+
}
|
|
17293
|
+
async function browserLogin(meta, args) {
|
|
17167
17294
|
const pkce = createPkce();
|
|
17168
17295
|
const state = createState();
|
|
17169
17296
|
const nonce = createNonce();
|
|
@@ -17178,10 +17305,16 @@ async function browserLogin(args) {
|
|
|
17178
17305
|
nonce,
|
|
17179
17306
|
orgAlias: args.orgAlias
|
|
17180
17307
|
});
|
|
17181
|
-
|
|
17182
|
-
|
|
17308
|
+
if (args.browserHere) {
|
|
17309
|
+
info("opening your browser to sign in\u2026");
|
|
17310
|
+
info(`if nothing opens, visit:
|
|
17183
17311
|
${url}`);
|
|
17184
|
-
|
|
17312
|
+
openBrowser2(url);
|
|
17313
|
+
} else {
|
|
17314
|
+
info(`to sign in, open this page in a browser that reaches port ${port} on this machine:
|
|
17315
|
+
${url}`);
|
|
17316
|
+
hint(`over SSH, forward it first: ssh -L ${port}:127.0.0.1:${port} <this host>`);
|
|
17317
|
+
}
|
|
17185
17318
|
const code = await waitForCallback(server, state);
|
|
17186
17319
|
return exchangeCode(meta, {
|
|
17187
17320
|
clientId: args.clientId,
|
|
@@ -18400,6 +18533,11 @@ async function showCmd(client, args) {
|
|
|
18400
18533
|
info("bake script:");
|
|
18401
18534
|
out(t.bakeScript);
|
|
18402
18535
|
}
|
|
18536
|
+
if (t.agentInstructions) {
|
|
18537
|
+
out("");
|
|
18538
|
+
info("agent instructions:");
|
|
18539
|
+
out(t.agentInstructions);
|
|
18540
|
+
}
|
|
18403
18541
|
if (t.piSettings && Object.keys(t.piSettings).length > 0) {
|
|
18404
18542
|
info(`Pi settings ${describePiSettings(flatPiSettings(t.piSettings))}`);
|
|
18405
18543
|
}
|
|
@@ -18478,6 +18616,12 @@ function parseOpts(args) {
|
|
|
18478
18616
|
opts.bakeScript = fs30.readFileSync(file, "utf8");
|
|
18479
18617
|
break;
|
|
18480
18618
|
}
|
|
18619
|
+
case "--agent-instructions": {
|
|
18620
|
+
const file = next();
|
|
18621
|
+
if (!fs30.existsSync(file)) throw new PiPodError(`no such agent instructions file: ${file}`);
|
|
18622
|
+
opts.agentInstructions = fs30.readFileSync(file, "utf8");
|
|
18623
|
+
break;
|
|
18624
|
+
}
|
|
18481
18625
|
default:
|
|
18482
18626
|
rest.push(arg);
|
|
18483
18627
|
}
|
|
@@ -18531,6 +18675,7 @@ async function createCmd(client, args, flags) {
|
|
|
18531
18675
|
...opts.description ? { description: opts.description } : {},
|
|
18532
18676
|
...initScript ? { initScript } : {},
|
|
18533
18677
|
...bakeScript ? { bakeScript } : {},
|
|
18678
|
+
...opts.agentInstructions ? { agentInstructions: opts.agentInstructions } : {},
|
|
18534
18679
|
// An explicit refresh is replacement, not merge: an empty project file set clears stale custody.
|
|
18535
18680
|
...opts.fromHere ? { piSettings: piSettings ?? {} } : {}
|
|
18536
18681
|
});
|
|
@@ -18637,9 +18782,9 @@ async function editCmd(client, args) {
|
|
|
18637
18782
|
});
|
|
18638
18783
|
}
|
|
18639
18784
|
const t = await findTemplate(client, rest[0]);
|
|
18640
|
-
if (opts.name === void 0 && opts.description === void 0 && !opts.config && opts.initScript === void 0 && opts.bakeScript === void 0) {
|
|
18785
|
+
if (opts.name === void 0 && opts.description === void 0 && !opts.config && opts.initScript === void 0 && opts.bakeScript === void 0 && opts.agentInstructions === void 0) {
|
|
18641
18786
|
throw new PiPodError("nothing to change", {
|
|
18642
|
-
hint: "pass --name, --description, --config <file>, --init-script <file> or --
|
|
18787
|
+
hint: "pass --name, --description, --config <file>, --init-script <file>, --bake-script <file> or --agent-instructions <file>"
|
|
18643
18788
|
});
|
|
18644
18789
|
}
|
|
18645
18790
|
const updated = await client.updateTemplate(t.id, {
|
|
@@ -18650,7 +18795,8 @@ async function editCmd(client, args) {
|
|
|
18650
18795
|
...opts.description !== void 0 ? { description: opts.description } : {},
|
|
18651
18796
|
...opts.config ? { config: opts.config } : {},
|
|
18652
18797
|
...opts.initScript !== void 0 ? { initScript: opts.initScript } : {},
|
|
18653
|
-
...opts.bakeScript !== void 0 ? { bakeScript: opts.bakeScript } : {}
|
|
18798
|
+
...opts.bakeScript !== void 0 ? { bakeScript: opts.bakeScript } : {},
|
|
18799
|
+
...opts.agentInstructions !== void 0 ? { agentInstructions: opts.agentInstructions } : {}
|
|
18654
18800
|
});
|
|
18655
18801
|
info(`updated template ${updated.name}`);
|
|
18656
18802
|
await hintMissingCredentials(client);
|
|
@@ -19429,10 +19575,12 @@ reused pods, \`--on\` co-location and \`--no-seed\` never seed.
|
|
|
19429
19575
|
Run \`pipod <command> --help\` for command-specific usage.
|
|
19430
19576
|
`;
|
|
19431
19577
|
var SUBCOMMAND_HELP = {
|
|
19432
|
-
login: `Usage: pipod login [--server <url>] [--issuer <url>] [--org <alias>] [--token <jwt>]
|
|
19578
|
+
login: `Usage: pipod login [--server <url>] [--issuer <url>] [--org <alias>] [--device] [--token <jwt>]
|
|
19433
19579
|
|
|
19434
19580
|
Sign this machine in to a pi pod server via Zitadel (authorization code + PKCE,
|
|
19435
|
-
in your browser).
|
|
19581
|
+
in your browser). Where no browser can open here (over SSH, on a headless
|
|
19582
|
+
server), it prints a page and a code to confirm on any other device instead.
|
|
19583
|
+
The session is stored in ~/.pi-pod/auth.json.
|
|
19436
19584
|
|
|
19437
19585
|
Options:
|
|
19438
19586
|
--server <url> Server to sign in to (default: the previous session's server,
|
|
@@ -19442,6 +19590,8 @@ Options:
|
|
|
19442
19590
|
publish its own
|
|
19443
19591
|
--org <alias> Only sign in to the organization with this primary domain;
|
|
19444
19592
|
omit it to sign in to your own organization
|
|
19593
|
+
--device Sign in with a code on another device even if a browser
|
|
19594
|
+
could open here
|
|
19445
19595
|
--token <jwt> Use an existing access token instead of the browser flow (dev/CI)
|
|
19446
19596
|
`,
|
|
19447
19597
|
account: `Usage: pipod account
|
|
@@ -19689,15 +19839,23 @@ are never stored on the server.
|
|
|
19689
19839
|
templates: `Usage: pipod templates [action]
|
|
19690
19840
|
|
|
19691
19841
|
Manage pod templates on the pi pod server. A template stores config, init and
|
|
19692
|
-
bake scripts, Pi settings files,
|
|
19693
|
-
default: only their owner sees and launches them.
|
|
19694
|
-
\`templates share\` makes one org-wide.
|
|
19842
|
+
bake scripts, Pi settings files, named secrets, and agent instructions.
|
|
19843
|
+
Templates are personal by default: only their owner sees and launches them.
|
|
19844
|
+
--org at creation or \`templates share\` makes one org-wide.
|
|
19845
|
+
|
|
19846
|
+
Agent instructions are added to the agent's system prompt in every pod launched
|
|
19847
|
+
from the template, with the hosts and secrets the pod actually has: say what
|
|
19848
|
+
access the template is meant to have. They guide the agent and enforce nothing;
|
|
19849
|
+
the template's secrets and egress decide what a pod can reach. A pod's child
|
|
19850
|
+
pods and the jobs it schedules launch from the pod's own template.
|
|
19695
19851
|
|
|
19696
19852
|
Actions:
|
|
19697
19853
|
list List templates with scope and key counts (default)
|
|
19698
|
-
show <name|id> Show config, scripts, Pi settings, and
|
|
19854
|
+
show <name|id> Show config, scripts, agent instructions, Pi settings, and
|
|
19855
|
+
secret names
|
|
19699
19856
|
create <name> Create a template
|
|
19700
|
-
edit <name|id> Update a template's name, description, config, or
|
|
19857
|
+
edit <name|id> Update a template's name, description, config, scripts, or
|
|
19858
|
+
agent instructions
|
|
19701
19859
|
share <name|id> Make a personal template org-wide (cannot be undone)
|
|
19702
19860
|
rm <name|id> Delete a template (alias: delete)
|
|
19703
19861
|
|
|
@@ -19709,6 +19867,9 @@ create/edit options:
|
|
|
19709
19867
|
scripts, and sanitized Pi files (contradicts --config)
|
|
19710
19868
|
--init-script <file> Store this init script
|
|
19711
19869
|
--bake-script <file> Store this bake script
|
|
19870
|
+
--agent-instructions <file>
|
|
19871
|
+
Store this file as the agent instructions (an empty
|
|
19872
|
+
file removes them); \`pipod push\` leaves them alone
|
|
19712
19873
|
--org create only: make the template org-wide from the start
|
|
19713
19874
|
--with-secrets With --from-here: also upload project env values as
|
|
19714
19875
|
template secrets
|
|
@@ -19815,7 +19976,8 @@ var FLAG_SCOPE = {
|
|
|
19815
19976
|
"--server": /* @__PURE__ */ new Set(["login"]),
|
|
19816
19977
|
"--org": /* @__PURE__ */ new Set(["login", "jobs"]),
|
|
19817
19978
|
"--token": /* @__PURE__ */ new Set(["login"]),
|
|
19818
|
-
"--issuer": /* @__PURE__ */ new Set(["login"])
|
|
19979
|
+
"--issuer": /* @__PURE__ */ new Set(["login"]),
|
|
19980
|
+
"--device": /* @__PURE__ */ new Set(["login"])
|
|
19819
19981
|
};
|
|
19820
19982
|
function resolveSubcommand(positional) {
|
|
19821
19983
|
if (positional === void 0) return null;
|
|
@@ -19959,6 +20121,9 @@ function parseArgs(argv) {
|
|
|
19959
20121
|
case "--issuer":
|
|
19960
20122
|
flags.issuer = takeValue();
|
|
19961
20123
|
break;
|
|
20124
|
+
case "--device":
|
|
20125
|
+
flags.device = true;
|
|
20126
|
+
break;
|
|
19962
20127
|
default:
|
|
19963
20128
|
throw new PiPodError(`unknown pi pod option "${name}"`, {
|
|
19964
20129
|
hint: "pass Pi options after `--`, for example: pipod -- --model claude-opus",
|
|
@@ -20025,7 +20190,7 @@ async function main(argv) {
|
|
|
20025
20190
|
case "init":
|
|
20026
20191
|
return runInit(flags);
|
|
20027
20192
|
case "login":
|
|
20028
|
-
return runLogin({ server: flags.server, org: flags.org, token: flags.token, issuer: flags.issuer });
|
|
20193
|
+
return runLogin({ server: flags.server, org: flags.org, token: flags.token, issuer: flags.issuer, device: flags.device });
|
|
20029
20194
|
case "whoami":
|
|
20030
20195
|
return runWhoami({});
|
|
20031
20196
|
case "logout":
|