@hobble-env/cli 3.0.1

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 (47) hide show
  1. package/README.md +148 -0
  2. package/SKILL.md +91 -0
  3. package/dist/bin/hobble.d.ts +2 -0
  4. package/dist/bin/hobble.js +46 -0
  5. package/dist/commands/delete.d.ts +2 -0
  6. package/dist/commands/delete.js +43 -0
  7. package/dist/commands/env-pull.d.ts +2 -0
  8. package/dist/commands/env-pull.js +56 -0
  9. package/dist/commands/env-push.d.ts +2 -0
  10. package/dist/commands/env-push.js +135 -0
  11. package/dist/commands/get.d.ts +2 -0
  12. package/dist/commands/get.js +43 -0
  13. package/dist/commands/init.d.ts +2 -0
  14. package/dist/commands/init.js +70 -0
  15. package/dist/commands/list.d.ts +4 -0
  16. package/dist/commands/list.js +52 -0
  17. package/dist/commands/login.d.ts +2 -0
  18. package/dist/commands/login.js +223 -0
  19. package/dist/commands/logout.d.ts +2 -0
  20. package/dist/commands/logout.js +25 -0
  21. package/dist/commands/run.d.ts +2 -0
  22. package/dist/commands/run.js +40 -0
  23. package/dist/commands/set.d.ts +2 -0
  24. package/dist/commands/set.js +65 -0
  25. package/dist/commands/shell.d.ts +2 -0
  26. package/dist/commands/shell.js +88 -0
  27. package/dist/commands/switch.d.ts +2 -0
  28. package/dist/commands/switch.js +37 -0
  29. package/dist/commands/tags.d.ts +2 -0
  30. package/dist/commands/tags.js +33 -0
  31. package/dist/commands/whoami.d.ts +2 -0
  32. package/dist/commands/whoami.js +45 -0
  33. package/dist/lib/api.d.ts +10 -0
  34. package/dist/lib/api.js +88 -0
  35. package/dist/lib/browser-login.d.ts +38 -0
  36. package/dist/lib/browser-login.js +88 -0
  37. package/dist/lib/config.d.ts +29 -0
  38. package/dist/lib/config.js +102 -0
  39. package/dist/lib/env-format.d.ts +6 -0
  40. package/dist/lib/env-format.js +35 -0
  41. package/dist/lib/output.d.ts +9 -0
  42. package/dist/lib/output.js +36 -0
  43. package/dist/lib/project-config.d.ts +21 -0
  44. package/dist/lib/project-config.js +55 -0
  45. package/dist/lib/shell-prompt.d.ts +31 -0
  46. package/dist/lib/shell-prompt.js +101 -0
  47. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Hobble CLI
2
+
3
+ A command-line tool for managing secrets stored in your secret management dashboard. Hobble lets you create, read, update, and delete secrets, and inject them into your development workflow as environment variables.
4
+
5
+ ## Installation
6
+
7
+ Install globally from npm:
8
+
9
+ ```bash
10
+ npm install -g @hobble-env/cli
11
+ ```
12
+
13
+ This provides the `hobble` command.
14
+
15
+ ### From source
16
+
17
+ From the project root:
18
+
19
+ ```bash
20
+ cd cli && npm install
21
+ ```
22
+
23
+ Run commands with:
24
+
25
+ ```bash
26
+ npx tsx cli/bin/hobble.ts <command>
27
+ ```
28
+
29
+ Or use the shortcut from the root:
30
+
31
+ ```bash
32
+ npm run hobble -- <command>
33
+ ```
34
+
35
+ ## Getting Started
36
+
37
+ ### 1. Create an API Key
38
+
39
+ Generate an API key from the dashboard under **Settings > API Keys**.
40
+
41
+ ### 2. Log In
42
+
43
+ ```bash
44
+ hobble login
45
+ ```
46
+
47
+ You'll be prompted for your API key and the dashboard URL (defaults to `http://localhost:3000`).
48
+
49
+ ### 3. Initialize a Project
50
+
51
+ ```bash
52
+ hobble init
53
+ ```
54
+
55
+ This walks you through selecting a project and environment, then saves the choice to a `.hobble` file in your current directory. You can switch later with:
56
+
57
+ ```bash
58
+ hobble switch my-project/staging
59
+ ```
60
+
61
+ ## Commands
62
+
63
+ ### Secrets
64
+
65
+ ```bash
66
+ hobble list # List all secrets (names only, no values)
67
+ hobble list --json # List as JSON
68
+
69
+ hobble get DB_HOST # Print a secret's decrypted value
70
+ hobble get DB_HOST --json # Output as JSON
71
+
72
+ hobble set DB_HOST localhost # Create or update a secret
73
+ echo "s3cr3t" | hobble set DB_PASS - # Set from stdin (avoids shell history)
74
+
75
+ hobble delete DB_HOST # Delete a secret (with confirmation)
76
+ hobble delete DB_HOST -y # Skip confirmation
77
+ ```
78
+
79
+ ### Running Commands with Secrets
80
+
81
+ Fetch secrets from the dashboard and inject them as environment variables into a subprocess:
82
+
83
+ ```bash
84
+ hobble run npm run dev
85
+ hobble run -- node server.js --port 8080
86
+ ```
87
+
88
+ Secrets are injected in-memory only -- nothing is written to disk.
89
+
90
+ ### Starting a Subshell with Secrets
91
+
92
+ Start an interactive subshell with the current project/environment's secrets injected as environment variables. Exit the subshell (type `exit` or Ctrl+D) to return to your original shell with its original environment intact.
93
+
94
+ ```bash
95
+ # In a directory with a .hobble file
96
+ hobble shell
97
+
98
+ # Or specify project/env explicitly
99
+ hobble shell --project my-app --env production
100
+ ```
101
+
102
+ Inside the subshell, three extra env vars are set so scripts can detect the context:
103
+
104
+ - `HOBBLE_PROJECT` — project name
105
+ - `HOBBLE_ENVIRONMENT` — environment name
106
+ - `HOBBLE_SHELL` — `<project>/<environment>`
107
+
108
+ For bash and zsh, your prompt is prefixed with `(hobble:<project>/<environment>)` as a visual reminder. Other shells keep their default prompt; use `$HOBBLE_SHELL` to build your own prompt indicator if desired.
109
+
110
+ ### Managing .env Files
111
+
112
+ ```bash
113
+ hobble env pull # Write secrets to .env
114
+ hobble env pull --force # Overwrite existing .env
115
+ hobble env pull -o .env.local # Write to a custom path
116
+
117
+ hobble env push # Push .env contents to the dashboard
118
+ hobble env push -y # Skip confirmation
119
+ hobble env push -i .env.local # Push from a custom path
120
+ ```
121
+
122
+ ### Account
123
+
124
+ ```bash
125
+ hobble whoami # Show current user and org info
126
+ hobble logout # Remove stored credentials
127
+ ```
128
+
129
+ ## Configuration
130
+
131
+ ### Global Config
132
+
133
+ Stored at `~/.config/hobble/config.json` after `hobble login`. Contains your API key and dashboard URL.
134
+
135
+ Override with environment variables:
136
+
137
+ - `HOBBLE_API_KEY` -- override the stored API key
138
+ - `HOBBLE_API_URL` -- override the stored dashboard URL
139
+
140
+ ### Project Config
141
+
142
+ The `.hobble` file in your project directory stores the default project and environment. Add it to `.gitignore` -- `hobble init` will offer to do this for you.
143
+
144
+ Override per-command with flags:
145
+
146
+ ```bash
147
+ hobble list --project my-app --env production
148
+ ```
package/SKILL.md ADDED
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: using-hobble-cli
3
+ description: Use when a repository contains a `.hobble` file, references the hobble CLI in scripts/docs, or you need to run or test code that depends on secrets managed by a hobble dashboard
4
+ ---
5
+
6
+ # Using the hobble CLI
7
+
8
+ ## Overview
9
+
10
+ `hobble` fetches secrets from a remote hobble dashboard and makes them available to local processes. Secrets are never stored on disk unless a user explicitly runs `hobble env pull`. The safe default is in-memory injection via `hobble run`.
11
+
12
+ ## Detecting hobble in a Repo
13
+
14
+ Treat a repo as a hobble consumer if any of these are true:
15
+ - A `.hobble` file exists in the repo root. It is JSON: `{ "project": "...", "environment": "..." }`.
16
+ - `.gitignore` lists `.hobble`.
17
+ - Scripts, Makefiles, or docs invoke `hobble ...`.
18
+ - `package.json` has a script wrapping `hobble run`.
19
+
20
+ If none of these are present, the repo is not a hobble consumer — do not introduce `hobble` without asking.
21
+
22
+ ## Critical Rules
23
+
24
+ 1. **Never commit `.hobble`, `.env`, `.env.local`, or any file produced by `hobble env pull`.** Before running pull, confirm `.gitignore` covers the output path.
25
+ 2. **Prefer `hobble run -- <cmd>` over `hobble env pull`.** `run` injects secrets into a single subprocess in memory. `pull` writes plaintext to disk and must be cleaned up. Only pull when a tool literally cannot read env vars.
26
+ 3. **Never pass secret values as CLI arguments.** They leak into shell history and `ps` output. Use stdin:
27
+ ```bash
28
+ printf '%s' "$value" | hobble set DB_PASS -
29
+ ```
30
+ 4. **Never print, paste, or log the output of `hobble get`.** Do not echo it into chat, a commit message, or a log file. If you must move a value, pipe it directly to the destination.
31
+ 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.
32
+ 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.
33
+
34
+ ## Quick Reference
35
+
36
+ | Task | Command |
37
+ |------|---------|
38
+ | Run code with secrets injected | `hobble run -- <cmd>` |
39
+ | List secret names (no values) | `hobble list` |
40
+ | Read one value | `hobble get KEY` |
41
+ | Create or update a secret | `printf '%s' "$v" \| hobble set KEY -` |
42
+ | Delete a secret | `hobble delete KEY -y` |
43
+ | Write `.env` file (use sparingly) | `hobble env pull` |
44
+ | Upload local `.env` to dashboard | `hobble env push` (review diff before `-y`) |
45
+ | Change bound project/env | `hobble switch <project>/<env>` |
46
+ | Who am I / which org | `hobble whoami` |
47
+
48
+ All data commands accept `--project <name> --env <name>` to override `.hobble` for a single invocation. Prefer flags over `hobble switch` for one-off reads.
49
+
50
+ ## Context Resolution
51
+
52
+ Each data command needs a `project` and `environment`, resolved in order:
53
+ 1. `--project` / `--env` flags on the command
54
+ 2. `.hobble` in the current working directory
55
+ 3. Error: `No project configured. Run \`hobble init\` or pass \`--project\` and \`--env\`.`
56
+
57
+ If you hit that error, stop and ask the user. Do not run `hobble init` on their behalf — it is interactive and binds the directory to a project.
58
+
59
+ ## Authentication
60
+
61
+ - Stored config: `~/.config/hobble/config.json` (written by `hobble login`).
62
+ - Overrides via env vars: `HOBBLE_API_KEY`, `HOBBLE_API_URL`. Prefer these in CI.
63
+ - A 401 error means the key is stale — tell the user to run `hobble login` rather than re-running it yourself.
64
+
65
+ ## Common Mistakes
66
+
67
+ | Mistake | Correct approach |
68
+ |---------|------------------|
69
+ | `hobble set KEY "mysecret"` | `printf '%s' "$v" \| hobble set KEY -` — args leak to shell history |
70
+ | `hobble env pull` to "inspect" secrets | Use `hobble list` for keys; `hobble get KEY` piped to the consumer for one value |
71
+ | `git add -A` after `hobble env pull` | Verify `.gitignore`, then stage files by name |
72
+ | Running `hobble login` / `hobble init` to "set things up" | Ask the user — both are interactive and user-scoped |
73
+ | Committing `.hobble` because it has no secrets | It binds the repo to a project/env and should always be gitignored |
74
+ | `hobble env push -y` on a dirty `.env` | Run without `-y` first to see the create/update/delete diff |
75
+
76
+ ## Running Tests That Need Secrets
77
+
78
+ Prefer:
79
+ ```bash
80
+ hobble run -- npm test
81
+ ```
82
+
83
+ Avoid the pull-then-test pattern — it leaves a plaintext `.env` behind if the test fails:
84
+ ```bash
85
+ # anti-pattern
86
+ hobble env pull && npm test
87
+ ```
88
+
89
+ ## When hobble Is Missing
90
+
91
+ If `hobble` is not installed but the repo needs it, tell the user rather than guessing. Installation is user-specific (global npm vs. per-repo), and the dashboard URL and API key must be set up interactively via `hobble login`.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { setVerbose } from "../lib/output.js";
4
+ import { loginCommand } from "../commands/login.js";
5
+ import { logoutCommand } from "../commands/logout.js";
6
+ import { whoamiCommand } from "../commands/whoami.js";
7
+ import { initCommand } from "../commands/init.js";
8
+ import { switchCommand } from "../commands/switch.js";
9
+ import { listCommand } from "../commands/list.js";
10
+ import { getCommand } from "../commands/get.js";
11
+ import { setCommand } from "../commands/set.js";
12
+ import { tagsCommand } from "../commands/tags.js";
13
+ import { deleteCommand } from "../commands/delete.js";
14
+ import { runCommand } from "../commands/run.js";
15
+ import { shellCommand } from "../commands/shell.js";
16
+ import { envPullCommand } from "../commands/env-pull.js";
17
+ import { envPushCommand } from "../commands/env-push.js";
18
+ const program = new Command();
19
+ program
20
+ .name("hobble")
21
+ .description("CLI for managing secrets via the secret-management-dash API")
22
+ .version("3.0.0")
23
+ .enablePositionalOptions()
24
+ .option("-v, --verbose", "Show debug output for troubleshooting");
25
+ program.hook("preAction", (thisCommand) => {
26
+ const opts = thisCommand.optsWithGlobals();
27
+ if (opts.verbose)
28
+ setVerbose(true);
29
+ });
30
+ program.addCommand(loginCommand);
31
+ program.addCommand(logoutCommand);
32
+ program.addCommand(whoamiCommand);
33
+ program.addCommand(initCommand);
34
+ program.addCommand(switchCommand);
35
+ program.addCommand(listCommand);
36
+ program.addCommand(getCommand);
37
+ program.addCommand(setCommand);
38
+ program.addCommand(tagsCommand);
39
+ program.addCommand(deleteCommand);
40
+ program.addCommand(runCommand);
41
+ program.addCommand(shellCommand);
42
+ const envCommand = new Command("env").description("Manage .env files");
43
+ envCommand.addCommand(envPullCommand);
44
+ envCommand.addCommand(envPushCommand);
45
+ program.addCommand(envCommand);
46
+ program.parse();
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const deleteCommand: Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from "commander";
2
+ import inquirer from "inquirer";
3
+ import { api } from "../lib/api.js";
4
+ import { resolveContext } from "../lib/project-config.js";
5
+ import { resolveProjectId } from "./list.js";
6
+ import { success, error } from "../lib/output.js";
7
+ export const deleteCommand = new Command("delete")
8
+ .description("Delete a secret")
9
+ .argument("<key>", "Secret key name")
10
+ .option("--project <name>", "Project name")
11
+ .option("--env <name>", "Environment name")
12
+ .option("-y, --yes", "Skip confirmation")
13
+ .action(async (key, opts) => {
14
+ try {
15
+ const ctx = await resolveContext(opts);
16
+ const projectId = await resolveProjectId(ctx.project);
17
+ // Find the secret by key name
18
+ const listResponse = (await api.get(`/api/v1/secrets?projectId=${projectId}&environment=${encodeURIComponent(ctx.environment)}`));
19
+ const match = listResponse.data.find((s) => s.key === key);
20
+ if (!match) {
21
+ error(`Secret "${key}" not found.`);
22
+ process.exit(1);
23
+ }
24
+ if (!opts.yes) {
25
+ const { confirm } = await inquirer.prompt([
26
+ {
27
+ type: "confirm",
28
+ name: "confirm",
29
+ message: `Delete secret "${key}"?`,
30
+ default: false,
31
+ },
32
+ ]);
33
+ if (!confirm)
34
+ return;
35
+ }
36
+ await api.delete(`/api/v1/secrets/${match.id}`);
37
+ success(`Deleted "${key}"`);
38
+ }
39
+ catch (err) {
40
+ error(err.message);
41
+ process.exit(1);
42
+ }
43
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const envPullCommand: Command;
@@ -0,0 +1,56 @@
1
+ import { Command } from "commander";
2
+ import { writeFile, access } from "node:fs/promises";
3
+ import { api } from "../lib/api.js";
4
+ import { resolveContext } from "../lib/project-config.js";
5
+ import { resolveProjectId } from "./list.js";
6
+ import { success, warn, error } from "../lib/output.js";
7
+ import { formatEnvComment } from "../lib/env-format.js";
8
+ export const envPullCommand = new Command("pull")
9
+ .description("Write secrets to a .env file")
10
+ .option("--project <name>", "Project name")
11
+ .option("--env <name>", "Environment name")
12
+ .option("--force", "Overwrite existing .env file")
13
+ .option("-o, --output <path>", "Output file path", ".env")
14
+ .action(async (opts) => {
15
+ try {
16
+ // Check if .env already exists
17
+ if (!opts.force) {
18
+ try {
19
+ await access(opts.output);
20
+ error(`${opts.output} already exists. Use --force to overwrite.`);
21
+ process.exit(1);
22
+ }
23
+ catch {
24
+ // File doesn't exist, good
25
+ }
26
+ }
27
+ const ctx = await resolveContext(opts);
28
+ const projectId = await resolveProjectId(ctx.project);
29
+ // Fetch all secrets
30
+ const listResponse = (await api.get(`/api/v1/secrets?projectId=${projectId}&environment=${encodeURIComponent(ctx.environment)}`));
31
+ const header = [
32
+ `# Generated by hobble from ${ctx.project}/${ctx.environment}`,
33
+ `# ${new Date().toISOString()}`,
34
+ ];
35
+ const blocks = [];
36
+ for (const s of listResponse.data) {
37
+ const detailResponse = (await api.get(`/api/v1/secrets/${s.id}`));
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}`;
44
+ const block = [...formatEnvComment(detail.comment), assignment];
45
+ blocks.push(block.join("\n"));
46
+ }
47
+ const body = [header.join("\n"), ...blocks].join("\n\n");
48
+ await writeFile(opts.output, body + "\n", "utf-8");
49
+ success(`Wrote ${listResponse.data.length} secrets to ${opts.output}`);
50
+ warn("Make sure this file is in your .gitignore!");
51
+ }
52
+ catch (err) {
53
+ error(err.message);
54
+ process.exit(1);
55
+ }
56
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const envPushCommand: Command;
@@ -0,0 +1,135 @@
1
+ import { Command } from "commander";
2
+ import { readFile } from "node:fs/promises";
3
+ import { parse } from "dotenv";
4
+ import inquirer from "inquirer";
5
+ import { api } from "../lib/api.js";
6
+ import { resolveContext } from "../lib/project-config.js";
7
+ import { resolveProjectId } from "./list.js";
8
+ import { success, info, error } from "../lib/output.js";
9
+ const HOBBLE_BANNER = /^# Generated by hobble\b/;
10
+ /**
11
+ * Walk the raw .env source and associate each contiguous run of `# ...` lines
12
+ * (separated by no blank lines) with the next KEY=VALUE assignment. The banner
13
+ * comments emitted by `hobble pull` are stripped so they don't get re-pushed.
14
+ */
15
+ function extractComments(raw) {
16
+ const lines = raw.split(/\r?\n/);
17
+ const buffer = [];
18
+ const result = {};
19
+ for (const rawLine of lines) {
20
+ const line = rawLine.trim();
21
+ if (line.startsWith("#")) {
22
+ if (HOBBLE_BANNER.test(line)) {
23
+ buffer.length = 0;
24
+ continue;
25
+ }
26
+ // strip leading "#" and a single optional space
27
+ buffer.push(line.replace(/^#\s?/, ""));
28
+ continue;
29
+ }
30
+ if (line === "") {
31
+ buffer.length = 0;
32
+ continue;
33
+ }
34
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
35
+ if (match && buffer.length > 0) {
36
+ result[match[1]] = buffer.join("\n");
37
+ }
38
+ buffer.length = 0;
39
+ }
40
+ return result;
41
+ }
42
+ export const envPushCommand = new Command("push")
43
+ .description("Push a .env file to the API")
44
+ .option("--project <name>", "Project name")
45
+ .option("--env <name>", "Environment name")
46
+ .option("-y, --yes", "Skip confirmation")
47
+ .option("-i, --input <path>", "Input file path", ".env")
48
+ .action(async (opts) => {
49
+ try {
50
+ const raw = await readFile(opts.input, "utf-8").catch(() => {
51
+ error(`File not found: ${opts.input}`);
52
+ process.exit(1);
53
+ });
54
+ const rawText = raw;
55
+ const parsed = parse(rawText);
56
+ const comments = extractComments(rawText);
57
+ const allKeys = Object.keys(parsed);
58
+ if (allKeys.length === 0) {
59
+ info("No secrets found in .env file.");
60
+ return;
61
+ }
62
+ const keysWithValues = allKeys.filter((k) => parsed[k] !== "");
63
+ const emptyKeys = allKeys.filter((k) => parsed[k] === "");
64
+ const ctx = await resolveContext(opts);
65
+ const projectId = await resolveProjectId(ctx.project);
66
+ // Fetch existing secrets to diff
67
+ const listResponse = (await api.get(`/api/v1/secrets?projectId=${projectId}&environment=${encodeURIComponent(ctx.environment)}`));
68
+ const existingByKey = new Map(listResponse.data.map((s) => [s.key, s]));
69
+ const toCreate = keysWithValues.filter((k) => !existingByKey.has(k));
70
+ const toUpdate = keysWithValues.filter((k) => existingByKey.has(k));
71
+ const toDelete = emptyKeys.filter((k) => existingByKey.has(k));
72
+ info(`${toCreate.length} to create, ${toUpdate.length} to update, ${toDelete.length} to delete`);
73
+ if (toCreate.length > 0) {
74
+ info(`Create: ${toCreate.join(", ")}`);
75
+ }
76
+ if (toUpdate.length > 0) {
77
+ info(`Update: ${toUpdate.join(", ")}`);
78
+ }
79
+ if (toDelete.length > 0) {
80
+ info(`Delete: ${toDelete.join(", ")}`);
81
+ }
82
+ if (toCreate.length === 0 && toUpdate.length === 0 && toDelete.length === 0) {
83
+ info("No changes to apply.");
84
+ return;
85
+ }
86
+ if (!opts.yes) {
87
+ const { confirm } = await inquirer.prompt([
88
+ {
89
+ type: "confirm",
90
+ name: "confirm",
91
+ message: "Apply these changes?",
92
+ default: false,
93
+ },
94
+ ]);
95
+ if (!confirm)
96
+ return;
97
+ }
98
+ // Apply changes
99
+ for (const key of toCreate) {
100
+ const body = {
101
+ projectId,
102
+ environment: ctx.environment,
103
+ key,
104
+ value: parsed[key],
105
+ };
106
+ if (comments[key] !== undefined)
107
+ body.comment = comments[key];
108
+ await api.post("/api/v1/secrets", body);
109
+ }
110
+ for (const key of toUpdate) {
111
+ const existing = existingByKey.get(key);
112
+ const incoming = comments[key];
113
+ const body = {
114
+ value: parsed[key],
115
+ };
116
+ // Only send comment when the file changes it. This prevents push from
117
+ // wiping a server-side comment when the user authored a .env without
118
+ // including comment lines.
119
+ if (incoming !== undefined && incoming !== (existing.comment ?? "")) {
120
+ body.comment = incoming;
121
+ }
122
+ await api.put(`/api/v1/secrets/${existing.id}`, body);
123
+ }
124
+ for (const key of toDelete) {
125
+ const existing = existingByKey.get(key);
126
+ await api.delete(`/api/v1/secrets/${existing.id}`);
127
+ }
128
+ const total = toCreate.length + toUpdate.length + toDelete.length;
129
+ success(`Pushed ${total} changes to ${ctx.project}/${ctx.environment}`);
130
+ }
131
+ catch (err) {
132
+ error(err.message);
133
+ process.exit(1);
134
+ }
135
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const getCommand: Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from "commander";
2
+ import { api } from "../lib/api.js";
3
+ import { resolveContext } from "../lib/project-config.js";
4
+ import { resolveProjectId } from "./list.js";
5
+ import { error, isTTY } from "../lib/output.js";
6
+ export const getCommand = new Command("get")
7
+ .description("Get a decrypted secret value")
8
+ .argument("<key>", "Secret key name")
9
+ .option("--project <name>", "Project name")
10
+ .option("--env <name>", "Environment name")
11
+ .option("--json", "Output as JSON")
12
+ .action(async (key, opts) => {
13
+ try {
14
+ const ctx = await resolveContext(opts);
15
+ const projectId = await resolveProjectId(ctx.project);
16
+ // Find the secret by key name
17
+ const listResponse = (await api.get(`/api/v1/secrets?projectId=${projectId}&environment=${encodeURIComponent(ctx.environment)}`));
18
+ const match = listResponse.data.find((s) => s.key === key);
19
+ if (!match) {
20
+ error(`Secret "${key}" not found.`);
21
+ process.exit(1);
22
+ }
23
+ // Fetch decrypted value
24
+ const secretResponse = (await api.get(`/api/v1/secrets/${match.id}`));
25
+ const secret = secretResponse.data;
26
+ if (opts.json) {
27
+ process.stdout.write(JSON.stringify({ key: secret.key, value: secret.value, tags: (secret.tags ?? []).map((t) => t.name) }, null, 2) + "\n");
28
+ return;
29
+ }
30
+ if (isTTY()) {
31
+ console.log(`${secret.key}=${secret.value}`);
32
+ if (secret.tags && secret.tags.length)
33
+ console.log(`# tags: ${secret.tags.map((t) => t.name).join(", ")}`);
34
+ }
35
+ else {
36
+ process.stdout.write(secret.value);
37
+ }
38
+ }
39
+ catch (err) {
40
+ error(err.message);
41
+ process.exit(1);
42
+ }
43
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const initCommand: Command;
@@ -0,0 +1,70 @@
1
+ import { Command } from "commander";
2
+ import inquirer from "inquirer";
3
+ import { api } from "../lib/api.js";
4
+ import { saveProjectConfig, addToGitignore, isHobbleInGitignore, } from "../lib/project-config.js";
5
+ import { success, error } from "../lib/output.js";
6
+ export const initCommand = new Command("init")
7
+ .description("Configure project and environment for this directory")
8
+ .action(async () => {
9
+ try {
10
+ // Fetch projects
11
+ const projectData = (await api.get("/api/v1/projects"));
12
+ if (projectData.projects.length === 0) {
13
+ error("No projects found. Create one in the dashboard first.");
14
+ process.exit(1);
15
+ }
16
+ const { projectId } = await inquirer.prompt([
17
+ {
18
+ type: "list",
19
+ name: "projectId",
20
+ message: "Select a project:",
21
+ choices: projectData.projects.map((p) => ({
22
+ name: p.name,
23
+ value: p.id,
24
+ })),
25
+ },
26
+ ]);
27
+ const selectedProject = projectData.projects.find((p) => p.id === projectId);
28
+ // Fetch environments
29
+ const envData = (await api.get(`/api/v1/projects/${projectId}/environments`));
30
+ if (envData.environments.length === 0) {
31
+ error("No environments found. Create one in the dashboard first.");
32
+ process.exit(1);
33
+ }
34
+ const { envName } = await inquirer.prompt([
35
+ {
36
+ type: "list",
37
+ name: "envName",
38
+ message: "Select an environment:",
39
+ choices: envData.environments.map((e) => ({
40
+ name: e.name,
41
+ value: e.name,
42
+ })),
43
+ },
44
+ ]);
45
+ await saveProjectConfig({
46
+ project: selectedProject.name,
47
+ environment: envName,
48
+ });
49
+ // Offer to add .hobble to .gitignore — but only if it's not already there
50
+ const alreadyIgnored = await isHobbleInGitignore();
51
+ if (!alreadyIgnored) {
52
+ const { addGitignore } = await inquirer.prompt([
53
+ {
54
+ type: "confirm",
55
+ name: "addGitignore",
56
+ message: "Add .hobble to .gitignore?",
57
+ default: true,
58
+ },
59
+ ]);
60
+ if (addGitignore) {
61
+ await addToGitignore();
62
+ }
63
+ }
64
+ success(`Configured: ${selectedProject.name}/${envName}`);
65
+ }
66
+ catch (err) {
67
+ error(err.message);
68
+ process.exit(1);
69
+ }
70
+ });
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ declare function resolveProjectId(projectName: string): Promise<string>;
3
+ export { resolveProjectId };
4
+ export declare const listCommand: Command;