getmyenv 0.11.1 → 0.12.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/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.0):
50
53
 
51
54
  ```text
52
55
  Usage: getmyenv [options] [command]
@@ -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.0",
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",