@hakopod/cli 0.1.0-alpha.1 → 0.1.0-alpha.2

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 CHANGED
@@ -51,8 +51,12 @@ Credentials and resumable deployment state live outside the source repository in
51
51
  `HAKOPOD_CLI_HOME` to use another protected directory. Files are written atomically
52
52
  with owner-only permissions; Windows uses the account profile's directory ACLs.
53
53
  Sessions expire after twelve hours and can be revoked in account security. Logout
54
- attempts server revocation and always removes local credentials. No passwords or
55
- provider tokens are requested by the terminal. Never commit CLI state to Git.
54
+ attempts server revocation and always removes local credentials. Account login happens in the browser. Before deployment, missing application
55
+ secrets can be entered with hidden terminal input or explicitly generated.
56
+ Generation creates a random password, not a provider-issued API credential.
57
+ Noninteractive runs stop with the missing names, including when using `--yes`.
58
+ Secret values are sent directly to scoped storage and never saved in CLI state.
59
+ Never commit CLI state to Git.
56
60
 
57
61
  Use an engine/Cloud server release containing npm CLI device-scope support. Older
58
62
  servers may expose only the image-based Go CLI API. That CLI remains available for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hakopod/cli",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0-alpha.2",
4
4
  "description": "Guided Git deployments to Hakopod Cloud and self-hosted Hakopod",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/main.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { Writable } from "node:stream";
2
3
  import { parseArgs } from "node:util";
3
4
  import { createInterface } from "node:readline/promises";
4
5
  import { stdin, stdout } from "node:process";
@@ -54,6 +55,36 @@ async function ask(label, fallback = "") {
54
55
  ).trim() || fallback
55
56
  );
56
57
  }
58
+ async function askSecret(label) {
59
+ if (!stdin.isTTY) throw new Error("Secret input requires an interactive terminal.");
60
+ rl?.close(); rl = undefined;
61
+ let muted = false;
62
+ const output = new Writable({ write(chunk, encoding, done) { if (!muted) stdout.write(chunk, encoding); done(); } });
63
+ const secretInput = createInterface({ input: stdin, output, terminal: true });
64
+ try {
65
+ const result = secretInput.question(`${label} (hidden): `);
66
+ muted = true;
67
+ return await result;
68
+ } finally { secretInput.close(); stdout.write("\n"); }
69
+ }
70
+ async function setupSecrets(api, config, plan) {
71
+ const missing = plan.missing_secrets || [];
72
+ if (!missing.length) return;
73
+ if (!stdin.isTTY) throw new Error(`Missing application secrets: ${missing.map(clean).join(", ")}. Save them in the dashboard or through POST /api/v1/secrets/{name}, then retry. No deployment was submitted.`);
74
+ for (const name of missing) {
75
+ say(`Missing secret: ${name}`);
76
+ const mode = await choose("Secret value", [
77
+ { label: "Enter an existing value (hidden)", value: "value" },
78
+ { label: "Generate a new random password (not a provider credential)", value: "generate" },
79
+ { label: "Cancel deployment", value: "cancel" },
80
+ ]);
81
+ if (mode === "cancel") throw new Error("Secret setup cancelled. No deployment was submitted.");
82
+ const body = mode === "generate" ? { generate: true } : { value: await askSecret(clean(name)) };
83
+ const query = new URLSearchParams({ project: config.project, environment: config.environment, application: plan.spec.name });
84
+ try { await api.call(`/secrets/${encodeURIComponent(name)}?${query}`, body); }
85
+ finally { delete body.value; }
86
+ }
87
+ }
57
88
  async function choose(label, items) {
58
89
  if (!items.length) throw new Error(`No ${label.toLowerCase()} available.`);
59
90
  items.forEach((item, index) => say(`${index + 1}. ${item.label}`));
@@ -425,6 +456,7 @@ async function main() {
425
456
  show(
426
457
  `Review deployment\n${JSON.stringify({ image: run.image, changes: plan.changes, warnings: plan.warnings }, null, 2)}`,
427
458
  );
459
+ await setupSecrets(api, config, plan);
428
460
  await confirm("Deploy this built image", flags.yes);
429
461
  const deployment = await api.call(`${base}/deploy`, {
430
462
  expected_revision: plan.expected_revision,