@heyditto/cli 2.5.1 → 2.6.0

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.
Files changed (64) hide show
  1. package/README.md +52 -20
  2. package/dist/commands.d.ts +34 -3
  3. package/dist/commands.js +130 -59
  4. package/dist/commands.js.map +1 -1
  5. package/dist/secret-stores/aws.d.ts +8 -0
  6. package/dist/secret-stores/aws.js +59 -0
  7. package/dist/secret-stores/aws.js.map +1 -0
  8. package/dist/secret-stores/azure.d.ts +7 -0
  9. package/dist/secret-stores/azure.js +63 -0
  10. package/dist/secret-stores/azure.js.map +1 -0
  11. package/dist/secret-stores/cloudflare.d.ts +7 -0
  12. package/dist/secret-stores/cloudflare.js +46 -0
  13. package/dist/secret-stores/cloudflare.js.map +1 -0
  14. package/dist/secret-stores/doppler.d.ts +7 -0
  15. package/dist/secret-stores/doppler.js +52 -0
  16. package/dist/secret-stores/doppler.js.map +1 -0
  17. package/dist/secret-stores/exec.d.ts +55 -0
  18. package/dist/secret-stores/exec.js +132 -0
  19. package/dist/secret-stores/exec.js.map +1 -0
  20. package/dist/secret-stores/fly.d.ts +7 -0
  21. package/dist/secret-stores/fly.js +54 -0
  22. package/dist/secret-stores/fly.js.map +1 -0
  23. package/dist/secret-stores/gcloud.d.ts +7 -0
  24. package/dist/secret-stores/gcloud.js +62 -0
  25. package/dist/secret-stores/gcloud.js.map +1 -0
  26. package/dist/secret-stores/github.d.ts +9 -0
  27. package/dist/secret-stores/github.js +74 -0
  28. package/dist/secret-stores/github.js.map +1 -0
  29. package/dist/secret-stores/gitlab.d.ts +7 -0
  30. package/dist/secret-stores/gitlab.js +82 -0
  31. package/dist/secret-stores/gitlab.js.map +1 -0
  32. package/dist/secret-stores/index.d.ts +42 -0
  33. package/dist/secret-stores/index.js +106 -0
  34. package/dist/secret-stores/index.js.map +1 -0
  35. package/dist/secret-stores/kubernetes.d.ts +8 -0
  36. package/dist/secret-stores/kubernetes.js +76 -0
  37. package/dist/secret-stores/kubernetes.js.map +1 -0
  38. package/dist/secret-stores/naming.d.ts +20 -0
  39. package/dist/secret-stores/naming.js +62 -0
  40. package/dist/secret-stores/naming.js.map +1 -0
  41. package/dist/secret-stores/onepassword.d.ts +7 -0
  42. package/dist/secret-stores/onepassword.js +50 -0
  43. package/dist/secret-stores/onepassword.js.map +1 -0
  44. package/dist/secret-stores/types.d.ts +108 -0
  45. package/dist/secret-stores/types.js +25 -0
  46. package/dist/secret-stores/types.js.map +1 -0
  47. package/dist/secret-stores/vault.d.ts +7 -0
  48. package/dist/secret-stores/vault.js +47 -0
  49. package/dist/secret-stores/vault.js.map +1 -0
  50. package/dist/secret-stores/vercel.d.ts +9 -0
  51. package/dist/secret-stores/vercel.js +49 -0
  52. package/dist/secret-stores/vercel.js.map +1 -0
  53. package/dist/teleport/harness.d.ts +5 -0
  54. package/dist/teleport/harness.js +21 -5
  55. package/dist/teleport/harness.js.map +1 -1
  56. package/dist/teleport/types.d.ts +7 -0
  57. package/dist/teleport/types.js +12 -1
  58. package/dist/teleport/types.js.map +1 -1
  59. package/dist/teleport/worktree.js +2 -0
  60. package/dist/teleport/worktree.js.map +1 -1
  61. package/package.json +1 -1
  62. package/dist/gh-secret.d.ts +0 -31
  63. package/dist/gh-secret.js +0 -94
  64. package/dist/gh-secret.js.map +0 -1
package/README.md CHANGED
@@ -406,9 +406,10 @@ heyditto endpoints set <endpoint> --model … --system-prompt … --spend-limit
406
406
  --record on|off --memory-depth <n>
407
407
  heyditto endpoints delete <endpoint> [--yes]
408
408
  heyditto endpoints keys <endpoint> list keys
409
- heyditto endpoints keys create <endpoint> --gh-secret <NAME> [--repo owner/repo] [--env <env>] [--org <org>]
409
+ heyditto endpoints keys create <endpoint> --store <destination> --secret <NAME> [scoping flags]
410
410
  [--name <label>] [--expires <1h…never>] [--budget <tokens>]
411
411
  [--spend-period <p>] [--yes]
412
+ heyditto endpoints keys stores which destinations this machine can write to
412
413
  heyditto endpoints keys revoke <endpoint> <keyId> [--yes]
413
414
  ```
414
415
 
@@ -416,30 +417,57 @@ Endpoints spend your Ditto credits, so deleting one, revoking a key or raising a
416
417
  spend limit asks you to type the slug back; pass `--yes` in scripts. `--output
417
418
  json` is available everywhere and includes the gateway base URL.
418
419
 
419
- #### Put a key in GitHub Actions
420
-
421
- `keys create --gh-secret` mints a long-lived key on an endpoint and stores it as
422
- a GitHub Actions secret through the [`gh` CLI](https://cli.github.com) — **the
423
- key is never shown**. The plaintext goes to `gh secret set` over stdin, so it
424
- does not appear in argv, `ps`, shell history, logs or the CLI's own output, and
425
- the CLI does not keep a copy. If `gh` fails to store it, the freshly minted key
426
- is revoked again.
420
+ #### Put a key straight into a secret manager
421
+
422
+ `keys create` mints a long-lived key on an endpoint and stores it in a secret
423
+ manager by delegating to that platform's own CLI — **the key is never shown**.
424
+ The plaintext goes to the platform CLI over stdin (or through `/dev/stdin`, for
425
+ the CLIs that read a file instead), so it does not appear in argv, `ps`, shell
426
+ history, logs or the CLI's own output, and nothing is kept locally. If the
427
+ platform CLI fails, the freshly minted key is revoked again.
428
+
429
+ | Destination | Shorthand | Scoping flags | CLI it delegates to |
430
+ | --- | --- | --- | --- |
431
+ | GitHub Actions | `--gh-secret NAME` | `--repo` `--env` `--org` | `gh secret set` |
432
+ | GitLab CI/CD | `--gitlab-var NAME` | `--project` `--org` `--env` | `glab variable set --masked` |
433
+ | AWS Secrets Manager | `--aws-secret NAME` | `--region` | `aws secretsmanager create-secret` (then `put-secret-value`) |
434
+ | Google Secret Manager | `--gcp-secret NAME` | `--project` | `gcloud secrets create` (then `versions add`) |
435
+ | Azure Key Vault | `--az-secret NAME` | `--key-vault` (required) | `az keyvault secret set --file` |
436
+ | 1Password | `--op-item TITLE` | `--op-vault` | `op item create` (JSON template on stdin) |
437
+ | HashiCorp Vault | `--vault-secret PATH` | `--mount` `--field` | `vault kv patch` (then `kv put`) |
438
+ | Doppler | `--doppler-secret NAME` | `--project` `--doppler-config` | `doppler secrets set` |
439
+ | Cloudflare Workers | `--cf-secret NAME` | `--worker` | `wrangler secret put` |
440
+ | Vercel | `--vercel-env NAME` | `--vercel-target` | `vercel env add --sensitive` |
441
+ | Kubernetes | `--k8s-secret NAME` | `--namespace` `--k8s-key` | `kubectl patch secret` (then `apply`) |
442
+ | Fly.io | `--fly-secret NAME` | `--app` | `fly secrets import` |
443
+
444
+ Every shorthand is sugar for `--store <id> --secret <NAME>`; use whichever
445
+ reads better. `heyditto endpoints keys stores` lists the destinations with the
446
+ CLI each one needs and whether it is installed here (`--output json` for
447
+ scripts).
427
448
 
428
449
  ```bash
429
450
  cd my-repo
430
451
  heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY # repo from the current directory (like gh)
431
452
  heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY --repo acme/app --budget 5000000
432
- heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY --repo acme/app --env production --yes
433
453
  heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY --org acme --expires 6mo --output json
434
- ```
435
-
436
- Requirements: `gh` on `PATH` and signed in (`gh auth status`) — both are checked
437
- before anything is minted. Without a terminal the command needs `--yes`;
438
- interactively it shows what it will do and asks you to type the secret name.
439
- Defaults: key name `gh:<owner>/<repo>:<NAME>`, expiry `1y`; `--budget` caps the
440
- key's spend (`--spend-period` defaults to `monthly`). The output shows the key's
441
- last four characters, expiry, budget, where the secret was set, and a workflow
442
- snippet:
454
+ heyditto endpoints keys create my-endpoint --store aws --secret DITTO_KEY --region us-east-1
455
+ heyditto endpoints keys create my-endpoint --gcp-secret ditto-key --project my-gcp-project
456
+ heyditto endpoints keys create my-endpoint --az-secret ditto-key --key-vault my-vault
457
+ heyditto endpoints keys create my-endpoint --op-item "Ditto inference" --op-vault Engineering
458
+ heyditto endpoints keys create my-endpoint --vault-secret ditto/inference --mount secret --field token
459
+ heyditto endpoints keys create my-endpoint --k8s-secret ditto-inference --namespace prod
460
+ ```
461
+
462
+ Requirements: the platform CLI on `PATH` and signed in — both are checked
463
+ *before* anything is minted, so a missing CLI costs you nothing. Without a
464
+ terminal the command needs `--yes`; interactively it shows what it will do and
465
+ asks you to type the secret name back. Defaults: expiry `1y`, key label
466
+ `<store>:<target>:<NAME>` (GitHub keeps its original `gh:<owner>/<repo>:<NAME>`);
467
+ `--budget` caps the key's spend (`--spend-period` defaults to `monthly`). A
468
+ scoping flag that belongs to another destination is an error rather than being
469
+ ignored. The output shows the key's last four characters, expiry, budget, where
470
+ it was stored, and how to consume it — for GitHub Actions:
443
471
 
444
472
  ```yaml
445
473
  env:
@@ -537,7 +565,11 @@ the thread URL the backend returns for its linked app (falling back to the
537
565
  production app link on older backends). `pull --resume` always resumes the
538
566
  harness inside the restored tree (add `--dry-run` to see the launch plan), so
539
567
  the next prompt lands in the restored transcript even when the original
540
- directory still exists on the same machine. Worktrees compressed with `zstd`
568
+ directory still exists on the same machine. Restored Claude transcripts are
569
+ filed under Claude Code's own project slug for the destination path (every
570
+ non-alphanumeric character becomes `-`), so `claude --resume` finds them in
571
+ paths with underscores or dots such as `/opt/workspace_base/capsule`. macOS
572
+ AppleDouble `._*` files and `.DS_Store` never enter the worktree archive. Worktrees compressed with `zstd`
541
573
  are decompressed by Node itself when the `zstd` binary is missing; on a Node
542
574
  without zstd support the pull fails with a message naming the binary to install.
543
575
 
@@ -52,10 +52,36 @@ export declare function cmdEndpointKeysRevoke(ref: string, keyId: string, option
52
52
  }): Promise<void>;
53
53
  interface KeysCreateOptions {
54
54
  output?: string;
55
+ /** Canonical destination selector; every store also has a shorthand flag. */
56
+ store?: string;
57
+ secret?: string;
55
58
  ghSecret?: string;
59
+ gitlabVar?: string;
60
+ awsSecret?: string;
61
+ gcpSecret?: string;
62
+ azSecret?: string;
63
+ opItem?: string;
64
+ vaultSecret?: string;
65
+ dopplerSecret?: string;
66
+ cfSecret?: string;
67
+ vercelEnv?: string;
68
+ k8sSecret?: string;
69
+ flySecret?: string;
56
70
  repo?: string;
57
71
  env?: string;
58
72
  org?: string;
73
+ region?: string;
74
+ project?: string;
75
+ keyVault?: string;
76
+ opVault?: string;
77
+ mount?: string;
78
+ field?: string;
79
+ dopplerConfig?: string;
80
+ worker?: string;
81
+ vercelTarget?: string;
82
+ namespace?: string;
83
+ k8sKey?: string;
84
+ app?: string;
59
85
  name?: string;
60
86
  expires?: string;
61
87
  budget?: string;
@@ -63,11 +89,16 @@ interface KeysCreateOptions {
63
89
  yes?: boolean;
64
90
  }
65
91
  /**
66
- * Mints a key on an endpoint and hands the plaintext straight to `gh secret
67
- * set` over stdin. The key is never printed, logged or stored locally; on a
68
- * failed `gh` call it is revoked again so nothing usable is left behind.
92
+ * Mints a key on an endpoint and hands the plaintext straight to the platform
93
+ * CLI over stdin (`gh secret set`, `gcloud secrets create --data-file=-`, …).
94
+ * The key is never printed, logged or stored locally; when the platform CLI
95
+ * fails it is revoked again so nothing usable is left behind.
69
96
  */
70
97
  export declare function cmdEndpointKeysCreate(ref: string, options: KeysCreateOptions): Promise<void>;
98
+ /** Lists every destination and whether this machine can delegate to it. */
99
+ export declare function cmdEndpointKeyStores(options: {
100
+ output?: string;
101
+ }): Promise<void>;
71
102
  /** Registers the `endpoints` group; bare `heyditto endpoints [flags]` still lists. */
72
103
  export declare function registerEndpointCommands(program: Command, addExamples: (c: Command, ex: string) => Command, outputOption: () => Option): void;
73
104
  interface SessionsOptions {
package/dist/commands.js CHANGED
@@ -7,12 +7,20 @@ import { createEndpoint, createKey, deleteEndpoint, findEndpoint, getEndpoint, i
7
7
  import { openInBrowser } from "./browser.js";
8
8
  import { endpointURL } from "./config.js";
9
9
  import { activationLink, formatActivation } from "./endpoint-format.js";
10
- import { describeTarget, preflightGh, resolveRepoFromCwd, setGitHubSecret, validateRepo, validateSecretName, } from "./gh-secret.js";
10
+ import { STORE_IDS, VERCEL_TARGETS, deliver, probeStores, selectStore, } from "./secret-stores/index.js";
11
11
  import { SESSION_ENV, SESSION_ID_HEADER, endSession, readSessionHistory, resolveActiveSession, startSession, useSession, } from "./mcp-session.js";
12
12
  import { readStoredAuth, updateStoredAuth } from "./store.js";
13
13
  function pad(s, n) {
14
14
  return s.length >= n ? s : s + " ".repeat(n - s.length);
15
15
  }
16
+ /** Left-aligned columns sized to their widest cell; the last column is not padded. */
17
+ function printTable(header, rows) {
18
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
19
+ const line = (r) => r.map((c, i) => (i === r.length - 1 ? c : pad(c, widths[i]))).join(" ");
20
+ process.stdout.write(`${line(header)}\n`);
21
+ for (const r of rows)
22
+ process.stdout.write(`${line(r)}\n`);
23
+ }
16
24
  function spendColumn(e) {
17
25
  const used = (e.spentTokens ?? 0).toLocaleString();
18
26
  if (e.spendLimitTokens == null || e.spendLimitTokens < 0)
@@ -305,11 +313,7 @@ export async function cmdEndpointKeys(ref, options) {
305
313
  k.lastUsedAt ? `used ${k.lastUsedAt.slice(0, 16).replace("T", " ")}` : "",
306
314
  ]);
307
315
  const header = ["ID", "KEY", "NAME", "STATE", "LAST USED"];
308
- const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
309
- const line = (r) => r.map((c, i) => (i === r.length - 1 ? c : pad(c, widths[i]))).join(" ");
310
- process.stdout.write(`${line(header)}\n`);
311
- for (const r of rows)
312
- process.stdout.write(`${line(r)}\n`);
316
+ printTable(header, rows);
313
317
  }
314
318
  export async function cmdEndpointKeysRevoke(ref, keyId, options) {
315
319
  const { endpoint } = await getEndpoint(ref);
@@ -321,6 +325,23 @@ export async function cmdEndpointKeysRevoke(ref, keyId, options) {
321
325
  }
322
326
  process.stdout.write(`Revoked key ${keyId} on ${endpoint.slug}.\n`);
323
327
  }
328
+ /** Shorthand flag values keyed by the store they select. */
329
+ function shorthandsOf(options) {
330
+ return {
331
+ github: options.ghSecret,
332
+ gitlab: options.gitlabVar,
333
+ aws: options.awsSecret,
334
+ gcloud: options.gcpSecret,
335
+ azure: options.azSecret,
336
+ "1password": options.opItem,
337
+ vault: options.vaultSecret,
338
+ doppler: options.dopplerSecret,
339
+ cloudflare: options.cfSecret,
340
+ vercel: options.vercelEnv,
341
+ kubernetes: options.k8sSecret,
342
+ fly: options.flySecret,
343
+ };
344
+ }
324
345
  function parseKeyBudget(raw) {
325
346
  if (raw === undefined)
326
347
  return undefined;
@@ -335,53 +356,38 @@ function parseKeyExpiry(raw) {
335
356
  return value;
336
357
  throw new Error(`--expires must be one of: ${KEY_EXPIRIES.join(", ")}`);
337
358
  }
338
- /** Resolves where the secret goes from --repo / --env / --org (repo falls back to the cwd, like gh). */
339
- function resolveSecretTarget(options) {
340
- const org = options.org?.trim();
341
- const env = options.env?.trim();
342
- if (org) {
343
- if (options.repo || env)
344
- throw new Error("--org cannot be combined with --repo or --env (organization secrets are not scoped to one repository)");
345
- if (!/^[A-Za-z0-9_.-]+$/.test(org))
346
- throw new Error(`--org must be an organization login, got "${options.org}"`);
347
- return { kind: "org", org };
348
- }
349
- const repo = options.repo ? validateRepo(options.repo) : resolveRepoFromCwd();
350
- if (env)
351
- return { kind: "env", repo, env };
352
- return { kind: "repo", repo };
353
- }
354
359
  /**
355
- * Mints a key on an endpoint and hands the plaintext straight to `gh secret
356
- * set` over stdin. The key is never printed, logged or stored locally; on a
357
- * failed `gh` call it is revoked again so nothing usable is left behind.
360
+ * Mints a key on an endpoint and hands the plaintext straight to the platform
361
+ * CLI over stdin (`gh secret set`, `gcloud secrets create --data-file=-`, …).
362
+ * The key is never printed, logged or stored locally; when the platform CLI
363
+ * fails it is revoked again so nothing usable is left behind.
358
364
  */
359
365
  export async function cmdEndpointKeysCreate(ref, options) {
360
- if (!options.ghSecret)
361
- throw new Error("--gh-secret <NAME> is required: this command only mints keys straight into a GitHub Actions secret");
362
- const secretName = validateSecretName(options.ghSecret);
366
+ const { store, name: secretName } = selectStore({ ...options, shorthands: shorthandsOf(options) });
363
367
  const budget = parseKeyBudget(options.budget);
364
368
  const expiresIn = parseKeyExpiry(options.expires);
365
369
  if (options.spendPeriod !== undefined && budget === undefined)
366
370
  throw new Error("--spend-period only applies together with --budget");
367
371
  const spendPeriod = budget !== undefined ? (options.spendPeriod ?? "monthly") : undefined;
368
- // Everything that can fail cheaply happens before any write: gh present and
369
- // signed in, target repo known, endpoint exists, operator confirmed.
370
- preflightGh();
371
- const target = resolveSecretTarget(options);
372
+ // Everything that can fail cheaply happens before any write: the platform
373
+ // CLI is present and signed in, its target is known, the endpoint exists,
374
+ // and the operator confirmed.
375
+ store.preflight();
376
+ const target = store.resolveTarget(options);
372
377
  const { endpoint, catalog } = await getEndpoint(ref);
373
- const keyName = options.name?.trim() || (target.kind === "org" ? `gh-secret:${secretName}` : `gh:${target.repo}:${secretName}`);
374
- const plan = `Will mint key "${keyName}" on ${endpoint.slug} (expires ${expiresIn}${budget !== undefined ? `, budget ${budget.toLocaleString()} tokens ${spendPeriod}` : ""}) and set secret ${secretName} on ${describeTarget(target)}.`;
375
- await confirmTyped({ action: `mint a key on ${endpoint.slug} and set secret ${secretName}`, expected: secretName, label: "the secret name", yes: options.yes, preview: plan });
378
+ const keyName = options.name?.trim() || defaultKeyName(store, target, secretName);
379
+ const plan = `Will mint key "${keyName}" on ${endpoint.slug} (expires ${expiresIn}${budget !== undefined ? `, budget ${budget.toLocaleString()} tokens ${spendPeriod}` : ""}) and store ${secretName} in ${target.describe}.`;
380
+ await confirmTyped({ action: `mint a key on ${endpoint.slug} and store ${secretName} in ${store.label}`, expected: secretName, label: `the ${store.nameLabel.toLowerCase()}`, yes: options.yes, preview: plan });
376
381
  const minted = await createKey(endpoint.id, {
377
382
  name: keyName,
378
383
  expiresIn,
379
384
  ...(budget !== undefined ? { spendLimitTokens: budget, spendPeriod } : {}),
380
385
  });
381
- // Split the plaintext off immediately; only `plaintext` may reach gh's stdin.
386
+ // Split the plaintext off immediately; only `plaintext` may reach the
387
+ // platform CLI's stdin.
382
388
  const { key: plaintext, ...key } = minted;
383
389
  try {
384
- setGitHubSecret(secretName, target, plaintext ?? "");
390
+ deliver(store, secretName, store.deliveries(secretName, target), plaintext ?? "");
385
391
  }
386
392
  catch (err) {
387
393
  const reason = err instanceof Error ? err.message : String(err);
@@ -396,7 +402,7 @@ export async function cmdEndpointKeysCreate(ref, options) {
396
402
  }
397
403
  const anthropicBaseUrl = apiRootOf(catalog.baseUrl);
398
404
  const openaiBaseUrl = catalog.baseUrl;
399
- const snippet = `\${{ secrets.${secretName} }}`;
405
+ const gateway = { anthropicBaseUrl, openaiBaseUrl };
400
406
  if (isJSON(options)) {
401
407
  process.stdout.write(`${JSON.stringify({
402
408
  endpoint: { id: endpoint.id, slug: endpoint.slug },
@@ -409,24 +415,57 @@ export async function cmdEndpointKeysCreate(ref, options) {
409
415
  spendLimitTokens: key.spendLimitTokens ?? budget ?? null,
410
416
  spendPeriod: key.spendPeriod ?? spendPeriod ?? null,
411
417
  },
412
- secret: { name: secretName, ...target, snippet },
418
+ store: store.id,
419
+ secret: {
420
+ name: secretName,
421
+ ...target.fields,
422
+ describe: target.describe,
423
+ ...(store.snippet ? { snippet: store.snippet(secretName, target) } : {}),
424
+ },
413
425
  gateway: { baseUrl: catalog.baseUrl, anthropicBaseUrl, openaiBaseUrl },
414
426
  }, null, 2)}\n`);
415
427
  return;
416
428
  }
417
429
  process.stdout.write([
418
430
  `Minted key …${key.keyHint} (${key.name ?? keyName}) on ${endpoint.slug}: expires ${expiresIn}${budget !== undefined ? `, budget ${budget.toLocaleString()} tokens ${spendPeriod}` : ", no spend cap"}.`,
419
- `Stored it as GitHub Actions secret ${secretName} on ${describeTarget(target)}. The key was not printed and is not kept locally.`,
431
+ `Stored ${secretName} in ${target.describe} via ${store.bin}. The key was not printed and is not kept locally.`,
420
432
  "",
421
- "Use it in a workflow step:",
422
- " env:",
423
- ` ANTHROPIC_AUTH_TOKEN: ${snippet}`,
424
- ` ANTHROPIC_BASE_URL: ${anthropicBaseUrl}`,
425
- ` # OpenAI-compatible clients: OPENAI_API_KEY: ${snippet} with OPENAI_BASE_URL: ${openaiBaseUrl}`,
433
+ ...store.usage(secretName, target, gateway),
426
434
  "",
427
435
  `Revoke later with: heyditto endpoints keys revoke ${endpoint.slug} ${key.id}`,
428
436
  ].join("\n") + "\n");
429
437
  }
438
+ /** Key label in the Ditto app: which store, which target, which name. */
439
+ function defaultKeyName(store, target, secretName) {
440
+ if (store.keyName)
441
+ return store.keyName(secretName, target);
442
+ const scope = Object.entries(target.fields)
443
+ .filter(([key, value]) => key !== "kind" && value)
444
+ .map(([, value]) => value)
445
+ .join("/");
446
+ return `${store.id}:${scope ? `${scope}:` : ""}${secretName}`;
447
+ }
448
+ /** Lists every destination and whether this machine can delegate to it. */
449
+ export async function cmdEndpointKeyStores(options) {
450
+ const probes = probeStores();
451
+ if (isJSON(options)) {
452
+ process.stdout.write(`${JSON.stringify(probes, null, 2)}\n`);
453
+ return;
454
+ }
455
+ const header = ["STORE", "DESTINATION", "CLI", "STATUS", "FLAGS"];
456
+ const rows = probes.map((p) => [
457
+ p.id,
458
+ p.label,
459
+ p.resolvedBin,
460
+ p.installed ? "installed" : "not installed",
461
+ [`${p.shorthand} <${p.nameLabel}>`, ...p.flags].join(" "),
462
+ ]);
463
+ printTable(header, rows);
464
+ const missing = probes.filter((p) => !p.installed);
465
+ if (missing.length > 0) {
466
+ process.stdout.write(`\nNot installed: ${missing.map((p) => p.bin).join(", ")}. Install the one you need, or store the key by hand from the console.\n`);
467
+ }
468
+ }
430
469
  /** Registers the `endpoints` group; bare `heyditto endpoints [flags]` still lists. */
431
470
  export function registerEndpointCommands(program, addExamples, outputOption) {
432
471
  const endpoints = program
@@ -437,7 +476,7 @@ export function registerEndpointCommands(program, addExamples, outputOption) {
437
476
  .addHelpText("after", `
438
477
  Endpoint controls spend your Ditto credits, so delete, key revocation and
439
478
  spend-limit increases ask you to type the slug back (or pass --yes).
440
- 'keys create --gh-secret' mints a key straight into a GitHub Actions secret
479
+ 'keys create' mints a key straight into a secret manager via its own CLI
441
480
  through the gh CLI; the plaintext never reaches your terminal.`);
442
481
  addExamples(endpoints
443
482
  .command("list", { isDefault: true })
@@ -513,13 +552,38 @@ through the gh CLI; the plaintext never reaches your terminal.`);
513
552
  .action(cmdEndpointKeys);
514
553
  addExamples(keys
515
554
  .command("create")
516
- .description("mint a key and store it straight into a GitHub Actions secret via gh (the key is never printed)")
555
+ .description("mint a key and store it straight into a secret manager through its own CLI (the key is never printed)")
517
556
  .argument("<endpoint>", "endpoint slug or id")
518
- .requiredOption("--gh-secret <NAME>", "Actions secret name to set with the gh CLI")
519
- .option("--repo <owner/repo>", "repository for the secret (default: the repo of the current directory, as gh resolves it)")
520
- .option("--env <environment>", "set a deployment-environment secret on the repo instead of a repository secret")
521
- .option("--org <org>", "set an organization secret instead (cannot be combined with --repo/--env)")
522
- .option("--name <label>", "key name shown in the Ditto app (default: gh:<owner>/<repo>:<NAME>)")
557
+ .addOption(new Option("--store <destination>", "where the key goes").choices([...STORE_IDS]))
558
+ .option("--secret <NAME>", "name of the secret, variable, item or path to store (with --store)")
559
+ .option("--gh-secret <NAME>", "shorthand for --store github --secret NAME")
560
+ .option("--gitlab-var <NAME>", "shorthand for --store gitlab --secret NAME")
561
+ .option("--aws-secret <NAME>", "shorthand for --store aws --secret NAME")
562
+ .option("--gcp-secret <NAME>", "shorthand for --store gcloud --secret NAME")
563
+ .option("--az-secret <NAME>", "shorthand for --store azure --secret NAME")
564
+ .option("--op-item <TITLE>", "shorthand for --store 1password --secret TITLE")
565
+ .option("--vault-secret <PATH>", "shorthand for --store vault --secret PATH")
566
+ .option("--doppler-secret <NAME>", "shorthand for --store doppler --secret NAME")
567
+ .option("--cf-secret <NAME>", "shorthand for --store cloudflare --secret NAME")
568
+ .option("--vercel-env <NAME>", "shorthand for --store vercel --secret NAME")
569
+ .option("--k8s-secret <NAME>", "shorthand for --store kubernetes --secret NAME")
570
+ .option("--fly-secret <NAME>", "shorthand for --store fly --secret NAME")
571
+ .option("--repo <owner/repo>", "github: repository for the secret (default: the repo of the current directory)")
572
+ .option("--env <environment>", "github: deployment environment; gitlab: variable environment scope")
573
+ .option("--org <org>", "github: organization secret; gitlab: group variable (cannot be combined with --repo/--env)")
574
+ .option("--region <region>", "aws: region for the secret (default: the configured region)")
575
+ .option("--project <project>", "gitlab, gcloud, doppler: project (default: the CLI's configured project)")
576
+ .option("--key-vault <name>", "azure: Key Vault the secret goes into (required)")
577
+ .option("--op-vault <name>", "1password: vault for the item (default: Private)")
578
+ .option("--mount <mount>", "vault: kv mount (default: secret)")
579
+ .option("--field <field>", "vault: field at the path (default: token)")
580
+ .option("--doppler-config <config>", "doppler: config within the project, e.g. prod")
581
+ .option("--worker <name>", "cloudflare: Worker name (default: the local wrangler config)")
582
+ .addOption(new Option("--vercel-target <target>", "vercel: environment").choices([...VERCEL_TARGETS]))
583
+ .option("--namespace <namespace>", "kubernetes: namespace (default: default)")
584
+ .option("--k8s-key <key>", "kubernetes: key within the secret (default: key)")
585
+ .option("--app <app>", "fly: app name (default: the local fly.toml)")
586
+ .option("--name <label>", "key name shown in the Ditto app (default: <store>:<target>:<NAME>)")
523
587
  .addOption(new Option("--expires <duration>", "server-side key expiry").choices([...KEY_EXPIRIES]).default("1y"))
524
588
  .option("--budget <tokens>", "spend cap for the key, in Ditto tokens")
525
589
  .addOption(new Option("--spend-period <period>", "window the key's spend cap resets on (with --budget; default monthly)").choices(["daily", "weekly", "monthly", "yearly", "never"]))
@@ -527,8 +591,19 @@ through the gh CLI; the plaintext never reaches your terminal.`);
527
591
  .addOption(outputOption())
528
592
  .action(cmdEndpointKeysCreate), ` heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY # repo of the current directory
529
593
  heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY --repo acme/app --budget 5000000
530
- heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY --repo acme/app --env production --yes
531
- heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY --org acme --expires 6mo --output json`);
594
+ heyditto endpoints keys create my-endpoint --gh-secret DITTO_KEY --org acme --expires 6mo --output json
595
+ heyditto endpoints keys create my-endpoint --store aws --secret DITTO_KEY --region us-east-1
596
+ heyditto endpoints keys create my-endpoint --gcp-secret DITTO_KEY --project my-gcp-project
597
+ heyditto endpoints keys create my-endpoint --az-secret DITTO_KEY --key-vault my-vault
598
+ heyditto endpoints keys create my-endpoint --op-item "Ditto inference" --op-vault Engineering
599
+ heyditto endpoints keys create my-endpoint --vault-secret ditto/inference --mount secret --field token
600
+ heyditto endpoints keys create my-endpoint --gitlab-var DITTO_KEY --project acme/app
601
+ heyditto endpoints keys create my-endpoint --k8s-secret ditto-inference --namespace prod`);
602
+ keys
603
+ .command("stores")
604
+ .description("list the secret managers keys can be minted into, and whether their CLI is installed here")
605
+ .addOption(outputOption())
606
+ .action(cmdEndpointKeyStores);
532
607
  keys
533
608
  .command("revoke")
534
609
  .description("revoke one key")
@@ -765,10 +840,6 @@ export async function cmdAgents(options) {
765
840
  connectionsColumn(a),
766
841
  ]);
767
842
  const header = ["ID", "KIND", "NAME", "THREADS", "LAST ACTIVITY", "CONNECTIONS"];
768
- const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
769
- const line = (r) => r.map((c, i) => (i === r.length - 1 ? c : pad(c, widths[i]))).join(" ");
770
- process.stdout.write(`${line(header)}\n`);
771
- for (const r of rows)
772
- process.stdout.write(`${line(r)}\n`);
843
+ printTable(header, rows);
773
844
  }
774
845
  //# sourceMappingURL=commands.js.map