@hobble-env/cli 3.3.11 → 3.3.12

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/SKILL.md CHANGED
@@ -7,7 +7,7 @@ description: Use when a repository contains a `.hobble` file, references the hob
7
7
 
8
8
  > **Living document.** This file is the canonical agent-facing reference for the hobble CLI surface. Per the gate in `AGENTS.md`, any feature work that changes the CLI surface (commands, flags, arguments, output formats, exit behavior, env vars, or config files) MUST update this file in the same change, and it must be verified current before every PR. Source of truth: `cli/bin/hobble.ts` and `cli/commands/*.ts`.
9
9
 
10
- <!-- documented-version: 3.3.8 — keep in sync with cli/bin/hobble.ts -->
10
+ <!-- documented-version: 3.3.10 — keep in sync with cli/bin/hobble.ts -->
11
11
 
12
12
  ## Overview
13
13
 
@@ -37,7 +37,7 @@ In this repo, the CLI runs from source: `npm run hobble -- <command>` (or `npx t
37
37
  5. **Do not run interactive commands unprompted.** `hobble login`, `hobble init`, and `hobble switch` modify user or repo state. Ask the user to run them, or explicitly confirm before doing so.
38
38
  6. **Never run `hobble env push -y` without showing the diff first.** Omit `-y` (or run once without it) so the user can review create/update/delete counts.
39
39
 
40
- ## Command Surface (v3.3.8)
40
+ ## Command Surface (v3.3.10)
41
41
 
42
42
  Global options: `-v, --verbose` (debug output on stderr), `-V, --version`.
43
43
 
@@ -53,9 +53,9 @@ Global options: `-v, --verbose` (debug output on stderr), `-V, --version`.
53
53
  | `set <key> [value\|-]` | `--project`, `--env`, `--comment <text>`, `--tag <name>` (repeatable) | Create-or-update. `-` or omitted value reads stdin (trailing whitespace trimmed). `--tag` **replaces** all existing tags. `--comment ""` clears the comment. |
54
54
  | `tags` | `--project`, `--env`, `--json` | Tags available to the project (name + color). `--env` accepted but unused. |
55
55
  | `delete <key>` | `--project`, `--env`, `-y` | Confirmation prompt unless `-y`. |
56
- | `run <cmd...>` | `--project`, `--env` | Injects all secrets into a subprocess (in-memory, `shell: true`). Use `--` before the command so its flags aren't parsed by hobble. Exits with the child's exit code. |
56
+ | `run <cmd...>` | `--project`, `--env` | Injects all secrets into a subprocess (in-memory, argv passed verbatim, no shell). Use `--` before the command so its flags aren't parsed by hobble. Exits with the child's exit code. |
57
57
  | `shell` | `--project`, `--env` | Interactive subshell with secrets injected. Sets `HOBBLE_PROJECT`, `HOBBLE_ENVIRONMENT`, `HOBBLE_SHELL=<project>/<env>`; prefixes bash/zsh prompts. Interactive — don't run unprompted. |
58
- | `env pull` | `--project`, `--env`, `--force`, `-o <path>` (default `.env`) | Writes plaintext secrets + comments to disk. Refuses to overwrite without `--force`. Use sparingly (Rule 2). |
58
+ | `env pull` | `--project`, `--env`, `--force`, `-o <path>` (default `.env`) | Writes plaintext secrets + comments to disk (values are bare when safe, otherwise dotenv-quoted). Refuses to overwrite without `--force`. Use sparingly (Rule 2). |
59
59
  | `env push` | `--project`, `--env`, `-y`, `-i <path>` (default `.env`) | Diffs file against server, prints create/update/delete lists, confirms unless `-y`. **An empty value (`KEY=`) deletes that secret on the server.** Contiguous `# ...` lines above a key become its comment; comments are only sent when changed. |
60
60
  | `credential list` | `--org <slug\|id>`, `--json` | Org-scoped credentials (alias: `secret list`). Names/usernames/URLs, never values. |
61
61
  | `credential get <name>` | `--org`, `--field <value\|username\|url>`, `--json` | Decrypted credential (alias: `secret get`). Name match is case-insensitive. Non-TTY output is the raw field value only. |
@@ -20,7 +20,7 @@ const program = new Command();
20
20
  program
21
21
  .name("hobble")
22
22
  .description("CLI for managing secrets via the secret-management-dash API")
23
- .version("3.3.10")
23
+ .version("3.3.11")
24
24
  .enablePositionalOptions()
25
25
  .option("-v, --verbose", "Show debug output for troubleshooting");
26
26
  program.hook("preAction", (thisCommand) => {
@@ -4,7 +4,7 @@ import { api } from "../lib/api.js";
4
4
  import { resolveContext } from "../lib/project-config.js";
5
5
  import { resolveProjectId } from "./list.js";
6
6
  import { success, warn, error } from "../lib/output.js";
7
- import { formatEnvComment } from "../lib/env-format.js";
7
+ import { formatEnvComment, formatEnvValue } from "../lib/env-format.js";
8
8
  export const envPullCommand = new Command("pull")
9
9
  .description("Write secrets to a .env file")
10
10
  .option("--project <name>", "Project name")
@@ -36,11 +36,7 @@ export const envPullCommand = new Command("pull")
36
36
  for (const s of listResponse.data) {
37
37
  const detailResponse = (await api.get(`/api/v1/secrets/${s.id}`));
38
38
  const detail = detailResponse.data;
39
- // Quote values that contain spaces or special characters
40
- const needsQuotes = /[\s#=]/.test(detail.value);
41
- const assignment = needsQuotes
42
- ? `${detail.key}="${detail.value}"`
43
- : `${detail.key}=${detail.value}`;
39
+ const assignment = `${detail.key}=${formatEnvValue(detail.value)}`;
44
40
  const block = [...formatEnvComment(detail.comment), assignment];
45
41
  blocks.push(block.join("\n"));
46
42
  }
@@ -27,7 +27,10 @@ export const runCommand = new Command("run")
27
27
  const child = spawn(cmd, args, {
28
28
  env: { ...process.env, ...secrets },
29
29
  stdio: "inherit",
30
- shell: true,
30
+ });
31
+ child.on("error", (e) => {
32
+ error(e.message);
33
+ process.exit(127);
31
34
  });
32
35
  child.on("close", (code) => {
33
36
  process.exit(code ?? 1);
@@ -4,3 +4,20 @@
4
4
  * Mirror of src/lib/env-format.ts (CLI is a separate tsconfig package).
5
5
  */
6
6
  export declare function formatEnvComment(comment: string | null | undefined, maxWidth?: number): string[];
7
+ /**
8
+ * Render a secret value for the right-hand side of a `KEY=value` line so a
9
+ * dotenv reader (node dotenv, godotenv) gets the original value back:
10
+ * - safe values (no whitespace, quotes, backticks, `#`, `$`, `\` or newline)
11
+ * stay bare;
12
+ * - values that only need protecting from splitting (spaces, `#`, `'`) are
13
+ * double-quoted verbatim;
14
+ * - values containing `"`, `\`, `$` or a backtick (which node dotenv cannot
15
+ * unescape inside double quotes, and which `source` would expand) are
16
+ * single-quoted verbatim, which is literal;
17
+ * - values containing `"` or `\` together with `'` or a newline fall back to
18
+ * double quotes with `\`, `"` and newlines escaped — node dotenv reads that
19
+ * form back with the escapes intact (it unescapes just `\n`/`\r`).
20
+ * `$` is never escaped: dotenv readers take it literally; `source` expands it
21
+ * and keeps the `\n` of the escaped form literal.
22
+ */
23
+ export declare function formatEnvValue(value: string): string;
@@ -33,3 +33,29 @@ export function formatEnvComment(comment, maxWidth = 80) {
33
33
  }
34
34
  return out;
35
35
  }
36
+ /**
37
+ * Render a secret value for the right-hand side of a `KEY=value` line so a
38
+ * dotenv reader (node dotenv, godotenv) gets the original value back:
39
+ * - safe values (no whitespace, quotes, backticks, `#`, `$`, `\` or newline)
40
+ * stay bare;
41
+ * - values that only need protecting from splitting (spaces, `#`, `'`) are
42
+ * double-quoted verbatim;
43
+ * - values containing `"`, `\`, `$` or a backtick (which node dotenv cannot
44
+ * unescape inside double quotes, and which `source` would expand) are
45
+ * single-quoted verbatim, which is literal;
46
+ * - values containing `"` or `\` together with `'` or a newline fall back to
47
+ * double quotes with `\`, `"` and newlines escaped — node dotenv reads that
48
+ * form back with the escapes intact (it unescapes just `\n`/`\r`).
49
+ * `$` is never escaped: dotenv readers take it literally; `source` expands it
50
+ * and keeps the `\n` of the escaped form literal.
51
+ */
52
+ export function formatEnvValue(value) {
53
+ if (!/[\s"'`#$\\]/.test(value))
54
+ return value;
55
+ if (!/["\\$`\n]/.test(value))
56
+ return `"${value}"`;
57
+ if (!/['\n]/.test(value))
58
+ return `'${value}'`;
59
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
60
+ return `"${escaped}"`;
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hobble-env/cli",
3
- "version": "3.3.11",
3
+ "version": "3.3.12",
4
4
  "description": "CLI for managing secrets via hobble",
5
5
  "type": "module",
6
6
  "repository": {