getmyenv 0.11.1 → 0.12.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.
package/README.md CHANGED
@@ -12,6 +12,9 @@ npx getmyenv run -- npm run dev # start with the default context
12
12
  npx getmyenv run staging -- npm start # or pick one
13
13
  ```
14
14
 
15
+ - Run getmyenv where your app starts: your machine, the server, the CI job, or the container's start command. It needs Node 24 or newer there. run is the default. If getmyenv can't run where the app starts, export writes a plaintext .env instead.
16
+ - run fetches the values from getmyenv each time it starts. If it can't reach the server, the command does not start. A running process keeps its values.
17
+
15
18
  ## Commands
16
19
 
17
20
  - `getmyenv`: setup in a new folder, status in a linked one. Without a TTY it prints next steps and exits.
@@ -46,7 +49,7 @@ my-app/
46
49
  ```
47
50
 
48
51
  <!-- cli:help -->
49
- Output of `npx getmyenv --help` (0.11.1):
52
+ Output of `npx getmyenv --help` (0.12.1):
50
53
 
51
54
  ```text
52
55
  Usage: getmyenv [options] [command]
@@ -108,7 +111,7 @@ One Vault password per account unlocks your private key on your device. Each con
108
111
 
109
112
  ## CI and servers
110
113
 
111
- Create a server token for one context on the dashboard CLI page. It holds that context's key.
114
+ Create a server token for one context in Project settings, Tokens. It holds that context's key.
112
115
 
113
116
  ```bash
114
117
  export GETMYENV_TOKEN=... # server token from the dashboard
@@ -117,7 +120,7 @@ npx getmyenv run -- node server.js
117
120
 
118
121
  Server tokens are read-only. `set` needs the folder token from browser sign-in, or a guest project. `printf %s "$VALUE" | npx getmyenv set API_KEY --stdin` keeps the value out of argv.
119
122
 
120
- - `GETMYENV_TOKEN`: a token from the dashboard CLI page.
123
+ - `GETMYENV_TOKEN`: a server token from Project settings, Tokens.
121
124
  - `GETMYENV_CONFIG_DIR`: machine key and cache (default `~/.config/getmyenv`). Use a persistent volume in containers.
122
125
  - `GETMYENV_API_URL`: API origin. https only, http for localhost.
123
126
  - `GETMYENV_MACHINE_LABEL`: the machine name shown in approval requests.
@@ -166,7 +166,7 @@ export async function importCommand(file, opts) {
166
166
  }
167
167
  ensureGitignoreHasEnv();
168
168
  if (imported > 0) {
169
- console.error(pc.dim("Your .env files are unchanged. getmyenv run does not need them. Delete them when you are ready.\n"));
169
+ console.error(pc.dim(`Your .env files are unchanged. ${FACTS.runIgnoresEnvFile}\n`));
170
170
  if (!opts.noRunLine) {
171
171
  console.error("Start your app with:");
172
172
  console.error(pc.cyan(` ${runLine(explicitContext ?? defaultContextSlug(who, meta))}\n`));
@@ -4,6 +4,7 @@ import pc from "picocolors";
4
4
  import { decryptSecret, parsePayload } from "@getmyenv/crypto";
5
5
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
6
6
  import { resolveContext } from "../lib/context.js";
7
+ import { pendingFirstRunHints } from "../lib/first-run.js";
7
8
  import { resolveValues } from "../lib/resolve.js";
8
9
  import { contextKey } from "../lib/vault.js";
9
10
  import { assertVariableNames, explainLines, mergeEnv, runContextLabel, signalExitCode, validationError, windowsCommandLine, } from "../lib/run-env.js";
@@ -51,6 +52,19 @@ export async function runCommand(opts, childArgs) {
51
52
  if (merged.unresolved.length > 0) {
52
53
  log(pc.yellow(`Not set: ${merged.unresolved.map((m) => m.name).join(", ")}`));
53
54
  }
55
+ const hints = opts.explain
56
+ ? { lines: [], markShown: () => undefined }
57
+ : pendingFirstRunHints({
58
+ isTTY: Boolean(process.stderr.isTTY),
59
+ env: process.env,
60
+ tokenKind: who.token.kind,
61
+ apiUrl: linked.apiUrl,
62
+ projectId: who.project.id,
63
+ childArgs,
64
+ cwd: process.cwd(),
65
+ });
66
+ for (const line of hints.lines)
67
+ log(pc.dim(line));
54
68
  const [cmd, ...args] = childArgs;
55
69
  const child = process.platform === "win32"
56
70
  ? spawn(windowsCommandLine(childArgs), { env: merged.env, stdio: "inherit", shell: true })
@@ -64,6 +78,7 @@ export async function runCommand(opts, childArgs) {
64
78
  child.kill(sig);
65
79
  });
66
80
  }
81
+ child.once("spawn", hints.markShown);
67
82
  const code = await new Promise((resolve) => {
68
83
  child.on("error", (err) => {
69
84
  log(pc.red(`Could not start ${cmd}: ${err.message}`));
@@ -0,0 +1,24 @@
1
+ export type HintContext = {
2
+ isTTY: boolean;
3
+ env: NodeJS.ProcessEnv;
4
+ tokenKind: "folder" | "guest" | "server";
5
+ };
6
+ export declare function hintsEnabled({ isTTY, env, tokenKind }: HintContext): boolean;
7
+ /** The package.json script a command like `npm start` or `pnpm run dev` starts, if any. */
8
+ export declare function packageScriptName(childArgs: string[]): string | null;
9
+ /** True when that script passes --env-file to node. */
10
+ export declare function scriptUsesEnvFile(childArgs: string[], packageJson: unknown): boolean;
11
+ export declare function hintLines(childArgs: string[], cwd: string): string[];
12
+ /** One marker per server origin and project, in the CLI config dir. */
13
+ export declare function hintMarkerPath(configDir: string, apiUrl: string, projectId: string): string;
14
+ export type PendingHints = {
15
+ lines: string[];
16
+ markShown: () => void;
17
+ };
18
+ export declare function pendingFirstRunHints(input: HintContext & {
19
+ apiUrl: string;
20
+ projectId: string;
21
+ childArgs: string[];
22
+ cwd: string;
23
+ configDir?: () => string;
24
+ }): PendingHints;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Hints printed by the first `run` of a project on this machine. Never in CI,
3
+ * with GETMYENV_TOKEN or a server token, or when stderr is not a terminal.
4
+ * The marker is written once the child has spawned, so a run that fails before
5
+ * that shows the hints again. Marker errors never change what `run` does.
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { FACTS } from "@getmyenv/shared";
11
+ import { getConfigDir } from "./fs.js";
12
+ import { keychainAccount } from "./keychain.js";
13
+ export function hintsEnabled({ isTTY, env, tokenKind }) {
14
+ if (!isTTY || tokenKind === "server" || env.GETMYENV_TOKEN?.trim())
15
+ return false;
16
+ const ci = env.CI?.trim().toLowerCase();
17
+ return !ci || ci === "false" || ci === "0";
18
+ }
19
+ const MANAGERS = new Set(["npm", "pnpm", "yarn", "bun"]);
20
+ /** The package.json script a command like `npm start` or `pnpm run dev` starts, if any. */
21
+ export function packageScriptName(childArgs) {
22
+ const [cmd, ...rest] = childArgs;
23
+ if (!cmd)
24
+ return null;
25
+ const manager = path.basename(cmd).replace(/\.(cmd|exe|ps1)$/i, "").toLowerCase();
26
+ if (!MANAGERS.has(manager))
27
+ return null;
28
+ const args = rest.filter((a) => !a.startsWith("-"));
29
+ if (args[0] === "run" || args[0] === "run-script")
30
+ return args[1] ?? null;
31
+ if (manager === "npm")
32
+ return args[0] === "start" || args[0] === "test" ? args[0] : null;
33
+ return args[0] ?? null;
34
+ }
35
+ /** True when that script passes --env-file to node. */
36
+ export function scriptUsesEnvFile(childArgs, packageJson) {
37
+ const name = packageScriptName(childArgs);
38
+ if (!name)
39
+ return false;
40
+ const scripts = packageJson?.scripts;
41
+ const script = scripts?.[name];
42
+ return typeof script === "string" && /--env-file\b/.test(script);
43
+ }
44
+ function readPackageJson(cwd) {
45
+ try {
46
+ return JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ export function hintLines(childArgs, cwd) {
53
+ const lines = [FACTS.runExplainHint];
54
+ if (fs.existsSync(path.join(cwd, ".env")))
55
+ lines.push(FACTS.runIgnoresEnvFile);
56
+ if (scriptUsesEnvFile(childArgs, readPackageJson(cwd)))
57
+ lines.push(FACTS.envFileScripts);
58
+ return lines;
59
+ }
60
+ /** One marker per server origin and project, in the CLI config dir. */
61
+ export function hintMarkerPath(configDir, apiUrl, projectId) {
62
+ const id = createHash("sha256").update(keychainAccount(apiUrl, projectId)).digest("hex").slice(0, 32);
63
+ return path.join(configDir, "hints", `${id}.json`);
64
+ }
65
+ const NONE = { lines: [], markShown: () => undefined };
66
+ export function pendingFirstRunHints(input) {
67
+ if (!hintsEnabled(input))
68
+ return NONE;
69
+ let marker = null;
70
+ try {
71
+ marker = hintMarkerPath((input.configDir ?? getConfigDir)(), input.apiUrl, input.projectId);
72
+ if (fs.existsSync(marker))
73
+ return NONE;
74
+ }
75
+ catch {
76
+ marker = null;
77
+ }
78
+ return {
79
+ lines: hintLines(input.childArgs, input.cwd),
80
+ markShown: () => {
81
+ if (!marker)
82
+ return;
83
+ try {
84
+ fs.mkdirSync(path.dirname(marker), { recursive: true, mode: 0o700 });
85
+ fs.writeFileSync(marker, `${JSON.stringify({ shownAt: new Date().toISOString() })}\n`, { mode: 0o600 });
86
+ }
87
+ catch {
88
+ /* the hints show again next time */
89
+ }
90
+ },
91
+ };
92
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getmyenv",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "description": "CLI for getmyenv, an encrypted store for environment variables and secrets. We store ciphertext only.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@getmyenv/crypto": "0.5.0",
37
- "@getmyenv/shared": "0.10.1",
37
+ "@getmyenv/shared": "0.11.0",
38
38
  "@napi-rs/keyring": "^2.1.0",
39
39
  "commander": "^15.0.0",
40
40
  "dotenv": "^16.4.7",