@hobble-env/cli 3.3.15 → 3.4.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.
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.13 — keep in sync with cli/bin/hobble.ts -->
10
+ <!-- documented-version: 3.3.15 — 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. Keys whose value and comment already match the server are not listed or counted, so the update list is exactly what changes.
39
39
 
40
- ## Command Surface (v3.3.13)
40
+ ## Command Surface (v3.3.15)
41
41
 
42
42
  Global options: `-v, --verbose` (debug output on stderr), `-V, --version`.
43
43
 
@@ -46,16 +46,16 @@ Global options: `-v, --verbose` (debug output on stderr), `-V, --version`.
46
46
  | `login` | `--url <url>`, `--api-key`, `--no-browser` | Interactive. Browser device flow by default; `--api-key` for a long-lived key. `--url` targets an instance and skips the URL prompt. Credentials are stored per instance URL in `~/.config/hobble/config.json`; the URL becomes the global default. With no interactive terminal (`< /dev/null`, a piped CI step) the prompt cannot be answered: exits 1 with `Prompt cancelled (no interactive terminal).` |
47
47
  | `logout` | `--all` | Removes stored credentials for the effective instance only; other instances stay logged in. `--all` removes every credential. Its confirmation prompt has the same non-TTY behaviour as `login`: exits 1 with `Prompt cancelled (no interactive terminal).` |
48
48
  | `whoami` | — | User, org, role, effective instance URL, and auth type (api key vs. session + expiry). |
49
- | `init` | `--url <url>` | Interactive one-stop setup: signs in first when not authenticated (or session expired) against the effective instance, then selects project/env, writes `.hobble`, offers to gitignore it. `--url` pins the instance into `.hobble` (`"apiUrl"`). Non-TTY without credentials exits 1 with `Not logged in to <url>`. |
49
+ | `init` | `--url <url>` | Interactive one-stop setup: signs in first when not authenticated (or session expired) against the effective instance, then selects project/env, writes `.hobble`, offers to gitignore it. `--url` pins the instance into `.hobble` (`"apiUrl"`). Exits 1 on a non-TTY, but by two different paths: with no usable credential (and no `HOBBLE_API_KEY`) the sign-in gate refuses first, with `Not logged in to <url>` plus the advice to run `hobble login`; **with** a credential it passes that gate and then dies on the first project/env prompt, reporting whatever the prompt library raised. Either way: exit 1, reason on stderr. |
50
50
  | `switch <project>/<env>` | — | Rewrites `.hobble` after validating both exist. Preserves an `"apiUrl"` pin. |
51
- | `list` | `--project`, `--env`, `--json`, `--tag <name>` (repeatable) | Names + tags + dates, never values. `--tag` is an AND filter, case-insensitive. |
51
+ | `list` | `--project`, `--env`, `--json`, `--tag <name>` (repeatable) | Names + tags + dates, never values. `--tag` is an AND filter, case-insensitive; a `--tag` naming no tag known to the project warns `Unknown tag: <name>` on stderr and still exits 0 with an empty result (the check is skipped, silently, for a token that cannot read the project's tag list). Empty results print `No secrets found.` on stderr (nothing on stdout). |
52
52
  | `get <key>` | `--project`, `--env`, `--json` | Decrypted value. Output shape depends on TTY — see Composition. |
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
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 (values are bare when safe, otherwise dotenv-quoted). 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`. The `# Generated by hobble from <project>/<env>` header carries no timestamp, so two pulls of identical data produce byte-identical files. 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`. A key counts as an update only when its value or its comment actually differs — a push straight after an untouched `env pull` reports `0 to create, 0 to update, 0 to delete` and `No changes to apply.` **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. |
@@ -77,7 +77,7 @@ The CLI is designed to be piped. The contract:
77
77
  - **stdout carries only data** — JSON (with `--json`) or a raw secret value (`get` when piped).
78
78
  - **stderr carries everything human-facing** — tables, `✓`/`ℹ`/`⚠` status lines, and `--verbose` debug output.
79
79
 
80
- So `--json` output and `2>/dev/null`-free piping are always safe; you never need to strip decoration from stdout.
80
+ `--json` does not *redirect* the human-facing output — it **replaces** it. `hobble list --json` writes the JSON array to stdout and prints no table at all; `hobble list --json 1>/dev/null` is therefore silent. So `--json` output and `2>/dev/null`-free piping are always safe; you never need to strip decoration from stdout.
81
81
 
82
82
  `hobble get KEY` is TTY-sensitive:
83
83
  - Piped / captured (non-TTY): writes the **raw value only, no trailing newline** — safe for command substitution.
@@ -106,7 +106,7 @@ hobble get API_KEY --env staging | hobble set API_KEY - --env production
106
106
  ```
107
107
 
108
108
  JSON shapes:
109
- - `list --json`: array of `{ id, key, createdAt, updatedAt, tags: [{ id, name, color }] }` — no values.
109
+ - `list --json`: array of `{ id, key, comment, createdAt, updatedAt, tags: [{ id, name, color }] }` — no values (`comment` is `null` when unset).
110
110
  - `get --json`: `{ key, value, tags: [name, ...] }`.
111
111
  - `tags --json`: array of `{ id, name, color }`.
112
112
  - `credential list --json`: array of `{ id, name, username, url, comment, createdAt, updatedAt, tags: [{ id, name, color }] }` — no values.
@@ -138,8 +138,9 @@ project root is.
138
138
  - Instance URL resolution order: `HOBBLE_API_URL` env var > `"apiUrl"` in `.hobble` > global default in config.json > `http://localhost:3000`. The credential used is the one stored for the resolved URL (`HOBBLE_API_KEY` overrides everything).
139
139
  - Overrides via env vars: `HOBBLE_API_KEY`, `HOBBLE_API_URL`. Prefer these in CI.
140
140
  - A 401 means the credential is stale or expired — tell the user to run `hobble login` rather than re-running it yourself. `hobble whoami` shows the effective instance, auth type, and session expiry.
141
- - `HOBBLE_ASSUME_TTY=1` makes `init` treat piped stdin as interactive (test harness escape hatch — do not use it to bypass Rule 5).
142
141
  - An environment-scoped API token works with `--project`/`--env`: it can resolve its environment's parent project by name and list that project's environments, but any read or write outside the scoped environment(s) fails with a permission error. A project name the credential is not scoped to reports `not found, or your credential is not scoped to it`.
142
+ - `HOBBLE_ASSUME_TTY=1` makes the **sign-in gate** of `init` treat piped stdin as interactive, so it starts the device flow instead of raising `Not logged in`. It does nothing else: with a valid credential already stored the gate returns early, so setting it changes nothing, and it does not make the later project/env prompts survive a pipe. Test-harness escape hatch — do not use it to bypass Rule 5.
143
+ - `HOBBLE_NO_OPEN=1` stops the browser-login flow from actually launching a browser. It does **not** skip the `Press Enter to open <url>…` prompt that precedes it — `hobble login --no-browser` is what skips the prompt and just prints the URL. Test-harness escape hatch.
143
144
 
144
145
  ## Common Mistakes
145
146
 
@@ -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.14")
23
+ .version("3.3.16")
24
24
  .enablePositionalOptions()
25
25
  .option("-v, --verbose", "Show debug output for troubleshooting");
26
26
  program.hook("preAction", (thisCommand) => {
@@ -28,9 +28,10 @@ export const envPullCommand = new Command("pull")
28
28
  const projectId = await resolveProjectId(ctx.project);
29
29
  // Fetch all secrets
30
30
  const listResponse = (await api.get(`/api/v1/secrets?projectId=${projectId}&environment=${encodeURIComponent(ctx.environment)}`));
31
+ // No timestamp: two pulls of identical data must produce byte-identical
32
+ // files, so a re-pull diffs as "no change".
31
33
  const header = [
32
34
  `# Generated by hobble from ${ctx.project}/${ctx.environment}`,
33
- `# ${new Date().toISOString()}`,
34
35
  ];
35
36
  const blocks = [];
36
37
  for (const s of listResponse.data) {
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { api } from "../lib/api.js";
3
3
  import { resolveContext } from "../lib/project-config.js";
4
- import { error, table } from "../lib/output.js";
4
+ import { error, table, warn } from "../lib/output.js";
5
5
  async function resolveProjectId(projectName) {
6
6
  const data = (await api.get("/api/v1/projects"));
7
7
  const proj = data.projects.find((p) => p.name === projectName);
@@ -24,6 +24,23 @@ export const listCommand = new Command("list")
24
24
  const response = (await api.get(`/api/v1/secrets?projectId=${projectId}&environment=${encodeURIComponent(ctx.environment)}`));
25
25
  const secrets = response.data;
26
26
  const tagFilter = opts.tag ?? [];
27
+ if (tagFilter.length) {
28
+ // A typo'd --tag otherwise looks exactly like "no matches". Warn (on
29
+ // stderr) but keep the exit code at 0 — an empty filter result is not
30
+ // an error. The lookup is best-effort: an environment-scoped token can
31
+ // list secrets but is forbidden the project-addressed tags route, and
32
+ // that must not turn a working `list` into a failure.
33
+ const known = await api
34
+ .get(`/api/v1/tags?projectId=${projectId}`)
35
+ .then((r) => r.data.map((t) => t.name.toLowerCase()))
36
+ .catch(() => null);
37
+ if (known) {
38
+ for (const t of tagFilter) {
39
+ if (!known.includes(t.toLowerCase()))
40
+ warn(`Unknown tag: ${t}`);
41
+ }
42
+ }
43
+ }
27
44
  const filtered = tagFilter.length
28
45
  ? secrets.filter((s) => {
29
46
  const names = (s.tags ?? []).map((t) => t.name.toLowerCase());
@@ -35,7 +52,9 @@ export const listCommand = new Command("list")
35
52
  return;
36
53
  }
37
54
  if (filtered.length === 0) {
38
- console.log("No secrets found.");
55
+ // Human-facing, so stderr — stdout carries only data (the table this
56
+ // stands in for goes to stderr too).
57
+ process.stderr.write("No secrets found.\n");
39
58
  return;
40
59
  }
41
60
  table(["Key", "Tags", "Created", "Updated"], filtered.map((s) => [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hobble-env/cli",
3
- "version": "3.3.15",
3
+ "version": "3.4.0",
4
4
  "description": "CLI for managing secrets via hobble",
5
5
  "type": "module",
6
6
  "repository": {