@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 +6 -2
- package/package.json +1 -1
- package/src/main.mjs +32 -0
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.
|
|
55
|
-
|
|
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
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,
|