getmyenv 0.13.0 → 0.14.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.
Files changed (54) hide show
  1. package/README.md +14 -12
  2. package/dist/bin.js +8 -0
  3. package/dist/cli.js +11 -3
  4. package/dist/commands/backup.js +1 -1
  5. package/dist/commands/claim.js +3 -2
  6. package/dist/commands/context.js +4 -4
  7. package/dist/commands/export.js +17 -11
  8. package/dist/commands/guest.d.ts +1 -1
  9. package/dist/commands/guest.js +17 -6
  10. package/dist/commands/import.js +38 -9
  11. package/dist/commands/lock.js +1 -1
  12. package/dist/commands/ls.js +2 -2
  13. package/dist/commands/push.js +6 -5
  14. package/dist/commands/restore.js +3 -3
  15. package/dist/commands/run.d.ts +1 -0
  16. package/dist/commands/run.js +61 -12
  17. package/dist/commands/set.d.ts +16 -0
  18. package/dist/commands/set.js +91 -12
  19. package/dist/commands/start.js +7 -7
  20. package/dist/commands/status.js +3 -3
  21. package/dist/commands/template.js +3 -3
  22. package/dist/commands/unlink.d.ts +14 -5
  23. package/dist/commands/unlink.js +79 -17
  24. package/dist/lib/api-url.d.ts +2 -0
  25. package/dist/lib/api-url.js +10 -0
  26. package/dist/lib/api.d.ts +22 -2
  27. package/dist/lib/api.js +108 -34
  28. package/dist/lib/authorize.js +2 -2
  29. package/dist/lib/bootstrap.d.ts +5 -0
  30. package/dist/lib/bootstrap.js +32 -8
  31. package/dist/lib/colors.d.ts +14 -0
  32. package/dist/lib/colors.js +17 -0
  33. package/dist/lib/confirm.d.ts +7 -3
  34. package/dist/lib/confirm.js +21 -12
  35. package/dist/lib/context.js +1 -1
  36. package/dist/lib/credentials.d.ts +4 -0
  37. package/dist/lib/credentials.js +13 -11
  38. package/dist/lib/fs.d.ts +36 -1
  39. package/dist/lib/fs.js +102 -23
  40. package/dist/lib/known-keys-cache.d.ts +3 -2
  41. package/dist/lib/known-keys-cache.js +16 -4
  42. package/dist/lib/known-keys.js +1 -1
  43. package/dist/lib/recovery-code.js +1 -1
  44. package/dist/lib/resolve.js +1 -1
  45. package/dist/lib/run-env.d.ts +19 -3
  46. package/dist/lib/run-env.js +66 -13
  47. package/dist/lib/template-file.js +1 -1
  48. package/dist/lib/update-check.js +1 -1
  49. package/dist/lib/upload.js +2 -2
  50. package/dist/lib/vault.js +1 -1
  51. package/dist/providers/http.js +15 -12
  52. package/dist/providers/netlify.js +3 -1
  53. package/dist/providers/vercel.js +3 -1
  54. package/package.json +19 -12
package/dist/lib/fs.js CHANGED
@@ -27,17 +27,29 @@ export function getProjectRoot(cwd = process.cwd()) {
27
27
  export function getLocalDir(cwd = process.cwd()) {
28
28
  return path.join(getProjectRoot(cwd), LOCAL_DIR);
29
29
  }
30
- export function readProjectMeta(cwd = process.cwd()) {
31
- const file = path.join(getLocalDir(cwd), "project.json");
30
+ /** Text of a file as UTF-8, without a byte order mark (Notepad and PowerShell 5.1 add one). */
31
+ export function readTextFile(file) {
32
+ return fs.readFileSync(file, "utf8").replace(/^/, "");
33
+ }
34
+ /**
35
+ * Parse a JSON file. Null when it does not exist. Invalid JSON throws
36
+ * `invalid`: a broken file is never read as missing, so it is never
37
+ * silently replaced.
38
+ */
39
+ export function readJsonFile(file, invalid) {
32
40
  if (!fs.existsSync(file))
33
41
  return null;
34
- let raw;
35
42
  try {
36
- raw = JSON.parse(fs.readFileSync(file, "utf8"));
43
+ return JSON.parse(readTextFile(file));
37
44
  }
38
45
  catch {
39
- return null;
46
+ throw new Error(invalid);
40
47
  }
48
+ }
49
+ export const PROJECT_JSON_INVALID = ".getmyenv/project.json is not valid JSON. Fix it, or restore it: git checkout -- .getmyenv/project.json";
50
+ export function readProjectMeta(cwd = process.cwd()) {
51
+ const file = path.join(getLocalDir(cwd), "project.json");
52
+ const raw = readJsonFile(file, PROJECT_JSON_INVALID);
41
53
  if (!raw || typeof raw !== "object")
42
54
  return null;
43
55
  const d = raw;
@@ -69,21 +81,32 @@ export function writeProjectMeta(meta, cwd = process.cwd()) {
69
81
  fs.writeFileSync(path.join(dir, "project.json"), JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });
70
82
  }
71
83
  /**
72
- * Write a file only this user can read. Never through a symlink. A new file
73
- * is created with `wx`; an existing one is replaced only when `overwrite`.
84
+ * Check that `file` can be written as a plain file: not a symlink, not a
85
+ * folder, and its folder exists. Returns its stats, or null when it is new.
74
86
  */
75
- export function writePrivateFile(file, content, opts) {
76
- let stat = null;
77
- try {
78
- stat = fs.lstatSync(file);
79
- }
80
- catch {
81
- /* does not exist */
82
- }
87
+ export function assertFileTarget(file) {
88
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false }) ?? null;
83
89
  if (stat?.isSymbolicLink())
84
90
  throw new Error(`${file} is a symlink. Refusing to write through it.`);
91
+ if (stat?.isDirectory()) {
92
+ throw new Error(`${file} is a folder. Pass a file name with -o.`);
93
+ }
85
94
  if (stat && !stat.isFile())
86
95
  throw new Error(`${file} is not a regular file.`);
96
+ if (!stat) {
97
+ const dir = path.dirname(path.resolve(file));
98
+ if (!fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) {
99
+ throw new Error(`The folder ${path.dirname(file)} does not exist. Create it first, or pick another path with -o.`);
100
+ }
101
+ }
102
+ return stat;
103
+ }
104
+ /**
105
+ * Write a file only this user can read. Never through a symlink. A new file
106
+ * is created with `wx`; an existing one is replaced only when `overwrite`.
107
+ */
108
+ export function writePrivateFile(file, content, opts) {
109
+ const stat = assertFileTarget(file);
87
110
  if (stat && !opts.overwrite)
88
111
  throw new Error(`${file} already exists. Pick another name with -o.`);
89
112
  fs.writeFileSync(file, content, { mode: 0o600, flag: stat ? "w" : "wx" });
@@ -265,7 +288,7 @@ export function readClaimFile(cwd = process.cwd()) {
265
288
  if (!fs.existsSync(file))
266
289
  return null;
267
290
  try {
268
- const raw = JSON.parse(fs.readFileSync(file, "utf8"));
291
+ const raw = JSON.parse(readTextFile(file));
269
292
  if (typeof raw.claimCode !== "string" ||
270
293
  typeof raw.passphrase !== "string" ||
271
294
  typeof raw.projectId !== "string") {
@@ -303,20 +326,76 @@ export function clearClaimFile(cwd = process.cwd()) {
303
326
  fs.unlinkSync(file);
304
327
  return true;
305
328
  }
306
- /** dotenv rules (multiline values, inline comments, BOM), valid names only. */
307
- export function parseEnvText(text) {
308
- const out = {};
329
+ /** dotenv rules (multiline values, inline comments, BOM). Invalid names land in `skipped`. */
330
+ export function readEnvText(text) {
331
+ const values = {};
332
+ const skipped = [];
309
333
  for (const [key, value] of Object.entries(dotenv.parse(text))) {
310
334
  if (isValidVariableName(key))
311
- out[key] = value;
335
+ values[key] = value;
336
+ else
337
+ skipped.push(key);
338
+ }
339
+ return { values, skipped };
340
+ }
341
+ /** `readEnvText`, valid names only. */
342
+ export function parseEnvText(text) {
343
+ return readEnvText(text).values;
344
+ }
345
+ /**
346
+ * Text from bytes, by byte order mark: UTF-16 LE or BE (PowerShell 5.1 `>`
347
+ * writes UTF-16 LE), else UTF-8 without its BOM.
348
+ */
349
+ export function decodeText(buf) {
350
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
351
+ return buf.subarray(2, 2 + ((buf.length - 2) & ~1)).toString("utf16le");
352
+ }
353
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
354
+ return Buffer.from(buf.subarray(2, 2 + ((buf.length - 2) & ~1))).swap16().toString("utf16le");
355
+ }
356
+ return buf.toString("utf8").replace(/^/, "");
357
+ }
358
+ /** `readEnvText` for a file, UTF-8 or UTF-16. A missing file reads as empty. */
359
+ export function readEnvFile(filePath) {
360
+ if (!fs.existsSync(filePath))
361
+ return { values: {}, skipped: [] };
362
+ return readEnvText(decodeText(fs.readFileSync(filePath)));
363
+ }
364
+ /** Names that appear more than once in an env file, with their count. dotenv keeps the last one. */
365
+ export function duplicateEnvNames(filePath) {
366
+ if (!fs.existsSync(filePath))
367
+ return [];
368
+ const counts = new Map();
369
+ const text = decodeText(fs.readFileSync(filePath));
370
+ for (const line of splitEnvEntries(text)) {
371
+ // Same line rule as dotenv: optional `export`, a name, then = or :.
372
+ const m = line.match(/^\s*(?:export\s+)?([\w.-]+)\s*[=:]/);
373
+ if (m?.[1])
374
+ counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
375
+ }
376
+ return [...counts.entries()].filter(([, n]) => n > 1).map(([name, count]) => ({ name, count }));
377
+ }
378
+ /** Lines of an env file, skipping the inside of quoted multiline values. */
379
+ function splitEnvEntries(text) {
380
+ const out = [];
381
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
382
+ let open = null;
383
+ for (const line of lines) {
384
+ if (open) {
385
+ if (line.includes(open))
386
+ open = null;
387
+ continue;
388
+ }
389
+ out.push(line);
390
+ const m = line.match(/^\s*(?:export\s+)?[\w.-]+\s*[=:]\s*(["'`])(.*)$/);
391
+ if (m && !m[2].includes(m[1]))
392
+ open = m[1];
312
393
  }
313
394
  return out;
314
395
  }
315
396
  /** `parseEnvText` for a file. A missing file reads as empty. */
316
397
  export function parseEnvFile(filePath) {
317
- if (!fs.existsSync(filePath))
318
- return {};
319
- return parseEnvText(fs.readFileSync(filePath, "utf8"));
398
+ return readEnvFile(filePath).values;
320
399
  }
321
400
  const ENV_SKIP_DIRS = new Set([
322
401
  "node_modules",
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Staff additions to the known keys list, cached in the config dir and
3
- * refreshed once a day. Any failure keeps the bundled list.
2
+ * Staff additions to the known keys list, and the defaults staff turned off,
3
+ * cached in the config dir and refreshed once a day. Any failure keeps the
4
+ * bundled list.
4
5
  */
5
6
  import { type KnownKeyIndex } from "@getmyenv/shared";
6
7
  /** The bundled list plus the cached additions. No network. */
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Staff additions to the known keys list, cached in the config dir and
3
- * refreshed once a day. Any failure keeps the bundled list.
2
+ * Staff additions to the known keys list, and the defaults staff turned off,
3
+ * cached in the config dir and refreshed once a day. Any failure keeps the
4
+ * bundled list.
4
5
  */
5
6
  import fs from "node:fs";
6
7
  import path from "node:path";
@@ -25,6 +26,8 @@ function readCache() {
25
26
  etag: typeof data.etag === "string" ? data.etag : null,
26
27
  checkedAt: data.checkedAt,
27
28
  entries: data.entries,
29
+ // Caches from older CLIs have no disabled list.
30
+ disabled: Array.isArray(data.disabled) ? data.disabled : [],
28
31
  };
29
32
  }
30
33
  catch {
@@ -44,7 +47,10 @@ export function knownKeys() {
44
47
  if (index)
45
48
  return index;
46
49
  const cached = readCache();
47
- index = cached?.entries.length ? createKnownKeyIndex(KNOWN_KEYS, cached.entries) : BUNDLED_KNOWN_KEYS;
50
+ index =
51
+ cached && (cached.entries.length || cached.disabled.length)
52
+ ? createKnownKeyIndex(KNOWN_KEYS, cached.entries, cached.disabled)
53
+ : BUNDLED_KNOWN_KEYS;
48
54
  return index;
49
55
  }
50
56
  /**
@@ -77,7 +83,13 @@ export async function refreshKnownKeys(apiUrl) {
77
83
  const data = JSON.parse(text);
78
84
  if (!Array.isArray(data.entries))
79
85
  return;
80
- writeCache({ apiUrl, etag: res.headers.get("etag"), checkedAt, entries: data.entries });
86
+ writeCache({
87
+ apiUrl,
88
+ etag: res.headers.get("etag"),
89
+ checkedAt,
90
+ entries: data.entries,
91
+ disabled: Array.isArray(data.disabled) ? data.disabled : [],
92
+ });
81
93
  index = null;
82
94
  }
83
95
  catch {
@@ -1,4 +1,4 @@
1
- import pc from "picocolors";
1
+ import { pcErr as pc } from "./colors.js";
2
2
  import { publicSecretWarning } from "@getmyenv/shared";
3
3
  import { knownKeys } from "./known-keys-cache.js";
4
4
  /** "Did you mean OPENAI_API_KEY?" for names close to a known one. Nothing is renamed. */
@@ -1,4 +1,4 @@
1
- import pc from "picocolors";
1
+ import { pcErr as pc } from "./colors.js";
2
2
  import { assertRecoveryWrap, createRecoveryWrap, createUserKeys, formatRecoveryCode, recoveryCodesMatch, wipe, } from "@getmyenv/crypto";
3
3
  import { FACTS } from "@getmyenv/shared";
4
4
  import { readLine } from "./fs.js";
@@ -1,4 +1,4 @@
1
- import pc from "picocolors";
1
+ import { pcErr as pc } from "./colors.js";
2
2
  import { ApiError } from "./api.js";
3
3
  import { sleep } from "./authorize.js";
4
4
  import { signMachineProof } from "./identity.js";
@@ -1,12 +1,28 @@
1
1
  export { explainLines, mergeEnv, runContextLabel, validationError, type MissingKey } from "@getmyenv/shared";
2
2
  /** Names from the server go into an environment or a file. Refuse any that are not variable names. */
3
3
  export declare function assertVariableNames(names: Iterable<string>): void;
4
+ /**
5
+ * One argument for a Windows program: quoted by the MSVCRT rules (backslashes
6
+ * before a quote or the end are doubled, inner quotes become \"), then every
7
+ * cmd.exe metacharacter, quotes included, is escaped with ^ so cmd passes the
8
+ * text through untouched and never expands %VAR%. A .cmd or .bat file reads
9
+ * its arguments through cmd a second time, so those get a second ^.
10
+ */
11
+ export declare function windowsArgument(arg: string, batch?: boolean): string;
4
12
  /**
5
13
  * One cmd.exe command line from argv. Windows needs the shell to find `npm.cmd`
6
14
  * and friends, and Node does not quote args passed alongside `shell: true`.
7
- * cmd expands %VAR% even inside quotes, so each % is closed out of the quotes
8
- * and escaped with ^.
15
+ * Arguments follow cross-spawn's rules (see `windowsArgument`). The command
16
+ * is quoted when it holds a space, so a path like C:\Program Files works.
17
+ */
18
+ export declare function windowsCommandLine(args: string[], opts?: {
19
+ batch?: boolean;
20
+ }): string;
21
+ /**
22
+ * True when cmd.exe would start `command` from a .cmd or .bat file (npm, npx,
23
+ * pnpm and most node_modules/.bin shims). Looks in the current folder, then
24
+ * PATH, with PATHEXT, the way cmd does.
9
25
  */
10
- export declare function windowsCommandLine(args: string[]): string;
26
+ export declare function isWindowsBatch(command: string, env: Record<string, string | undefined>, cwd?: string): boolean;
11
27
  /** Exit code for a child that ended by signal: 128 + signal number, like a shell. */
12
28
  export declare function signalExitCode(signal: NodeJS.Signals, numbers: Record<string, number>): number;
@@ -2,6 +2,8 @@
2
2
  * Pure pieces of `getmyenv run`. Merge, validate and explain live in
3
3
  * @getmyenv/shared so the homepage demo prints the same text.
4
4
  */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
5
7
  import { isValidVariableName } from "@getmyenv/shared";
6
8
  export { explainLines, mergeEnv, runContextLabel, validationError } from "@getmyenv/shared";
7
9
  /** Names from the server go into an environment or a file. Refuse any that are not variable names. */
@@ -12,23 +14,74 @@ export function assertVariableNames(names) {
12
14
  }
13
15
  }
14
16
  }
17
+ /** Characters cmd.exe treats as special. Each one gets a ^ in front, outside or inside quotes. */
18
+ const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
19
+ /** Args made only of these go through as they are. */
20
+ const PLAIN_ARG = /^[\w\-.:/\\+@]+$/;
21
+ /**
22
+ * One argument for a Windows program: quoted by the MSVCRT rules (backslashes
23
+ * before a quote or the end are doubled, inner quotes become \"), then every
24
+ * cmd.exe metacharacter, quotes included, is escaped with ^ so cmd passes the
25
+ * text through untouched and never expands %VAR%. A .cmd or .bat file reads
26
+ * its arguments through cmd a second time, so those get a second ^.
27
+ */
28
+ export function windowsArgument(arg, batch = false) {
29
+ if (arg !== "" && PLAIN_ARG.test(arg))
30
+ return arg;
31
+ let quoted = arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
32
+ quoted = `"${quoted}"`.replace(CMD_META, "^$1");
33
+ return batch ? quoted.replace(CMD_META, "^$1") : quoted;
34
+ }
15
35
  /**
16
36
  * One cmd.exe command line from argv. Windows needs the shell to find `npm.cmd`
17
37
  * and friends, and Node does not quote args passed alongside `shell: true`.
18
- * cmd expands %VAR% even inside quotes, so each % is closed out of the quotes
19
- * and escaped with ^.
38
+ * Arguments follow cross-spawn's rules (see `windowsArgument`). The command
39
+ * is quoted when it holds a space, so a path like C:\Program Files works.
40
+ */
41
+ export function windowsCommandLine(args, opts = {}) {
42
+ const [command = "", ...rest] = args;
43
+ return [windowsCommandName(command), ...rest.map((a) => windowsArgument(a, opts.batch === true))].join(" ");
44
+ }
45
+ /**
46
+ * The program to start. cmd finds it by its own quote rules, and the program
47
+ * reads its own name from the same text, so a path with spaces is plainly
48
+ * quoted. A % is closed out of the quotes and escaped, so cmd does not expand it.
20
49
  */
21
- export function windowsCommandLine(args) {
22
- return args
23
- .map((a) => {
24
- if (a === "")
25
- return '""';
26
- if (!/[\s"&|<>^()%]/.test(a))
27
- return a;
28
- const escaped = a.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1");
29
- return `"${escaped.replace(/%/g, '"^%"')}"`;
30
- })
31
- .join(" ");
50
+ function windowsCommandName(command) {
51
+ if (PLAIN_ARG.test(command))
52
+ return command;
53
+ return `"${command.replace(/"/g, "").replace(/%/g, '"^%"')}"`;
54
+ }
55
+ function envValue(env, name) {
56
+ const key = Object.keys(env).find((k) => k.toUpperCase() === name);
57
+ return key ? env[key] : undefined;
58
+ }
59
+ /**
60
+ * True when cmd.exe would start `command` from a .cmd or .bat file (npm, npx,
61
+ * pnpm and most node_modules/.bin shims). Looks in the current folder, then
62
+ * PATH, with PATHEXT, the way cmd does.
63
+ */
64
+ export function isWindowsBatch(command, env, cwd = process.cwd()) {
65
+ if (/\.(cmd|bat)$/i.test(command))
66
+ return true;
67
+ if (path.extname(command))
68
+ return false;
69
+ const exts = (envValue(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
70
+ const hasDir = /[\\/]/.test(command);
71
+ const dirs = hasDir ? [""] : [cwd, ...(envValue(env, "PATH") ?? "").split(";").filter(Boolean)];
72
+ for (const dir of dirs) {
73
+ for (const ext of exts) {
74
+ const file = path.resolve(dir || cwd, `${command}${ext}`);
75
+ try {
76
+ if (fs.statSync(file).isFile())
77
+ return /^\.(cmd|bat)$/i.test(ext);
78
+ }
79
+ catch {
80
+ /* not here */
81
+ }
82
+ }
83
+ }
84
+ return false;
32
85
  }
33
86
  /** Exit code for a child that ended by signal: 128 + signal number, like a shell. */
34
87
  export function signalExitCode(signal, numbers) {
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import pc from "picocolors";
3
+ import { pcErr as pc } from "./colors.js";
4
4
  import { mergeTemplateFile, templateFileSchema, TEMPLATE_FILE, TEMPLATE_FILE_MAX_KEYS, } from "@getmyenv/shared";
5
5
  import { getLocalDir } from "./fs.js";
6
6
  export function templateFilePath(cwd = process.cwd()) {
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
6
6
  import fs from "node:fs";
7
7
  import os from "node:os";
8
8
  import path from "node:path";
9
- import pc from "picocolors";
9
+ import { pcErr as pc } from "./colors.js";
10
10
  import { CLI_VERSION } from "./api.js";
11
11
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
12
12
  const FETCH_TIMEOUT_MS = 2500;
@@ -5,10 +5,10 @@ import { writeKey } from "./vault.js";
5
5
  /** Fail fast before prompting or encrypting. Mirrors the server's `checkWrite`, which still enforces it. */
6
6
  export function assertWritable(linked, ctx) {
7
7
  if (linked.who.token.kind === "server") {
8
- throw new Error(`Server tokens are read-only. ${FACTS.readOnlyWho}`);
8
+ throw new Error(`Server tokens cannot change values. ${FACTS.readOnlyWho}`);
9
9
  }
10
10
  if (ctx.protection === "locked") {
11
- throw new Error(`${ctx.slug} is read-only. ${FACTS.readOnlyWho}`);
11
+ throw new Error(`${ctx.slug} is Read-only. ${FACTS.readOnlyWho}`);
12
12
  }
13
13
  }
14
14
  /**
package/dist/lib/vault.js CHANGED
@@ -1,4 +1,4 @@
1
- import pc from "picocolors";
1
+ import { pcErr as pc } from "./colors.js";
2
2
  import { createUserKeys, generateContextKey, openSealedKey, sealContextKey, splitServerToken, unwrapPrivateKey, wipe, } from "@getmyenv/crypto";
3
3
  import { VAULT_UNLOCK_DURATION } from "@getmyenv/shared";
4
4
  import { ApiError } from "./api.js";
@@ -1,4 +1,4 @@
1
- import { API_TIMEOUT_MS } from "../lib/api.js";
1
+ import { withDeadline } from "../lib/api.js";
2
2
  export class PlatformError extends Error {
3
3
  status;
4
4
  constructor(message, status) {
@@ -29,23 +29,26 @@ function errorMessage(body) {
29
29
  */
30
30
  export async function platformRequest(opts) {
31
31
  let res;
32
+ let text;
32
33
  try {
33
- res = await fetch(`${opts.base}${opts.path}`, {
34
- method: opts.method,
35
- headers: {
36
- authorization: `Bearer ${opts.token}`,
37
- ...(opts.body !== undefined ? { "content-type": "application/json" } : {}),
38
- },
39
- body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
40
- redirect: "error",
41
- signal: AbortSignal.timeout(API_TIMEOUT_MS),
42
- });
34
+ ({ res, text } = await withDeadline(async (signal) => {
35
+ const res = await fetch(`${opts.base}${opts.path}`, {
36
+ method: opts.method,
37
+ headers: {
38
+ authorization: `Bearer ${opts.token}`,
39
+ ...(opts.body !== undefined ? { "content-type": "application/json" } : {}),
40
+ },
41
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
42
+ redirect: "error",
43
+ signal,
44
+ });
45
+ return { res, text: await res.text() };
46
+ }));
43
47
  }
44
48
  catch (err) {
45
49
  const why = err instanceof Error && err.name === "TimeoutError" ? "timed out" : "is unreachable";
46
50
  throw new PlatformError(`${opts.title} ${why}.`, 0);
47
51
  }
48
- const text = await res.text();
49
52
  let parsed = null;
50
53
  if (text) {
51
54
  try {
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { isPublicEnvName, isReservedPushName } from "@getmyenv/shared";
4
+ import { pushApiBase } from "../lib/api-url.js";
4
5
  import { PlatformError, platformRequest } from "./http.js";
5
6
  export const NETLIFY_API = "https://api.netlify.com";
6
7
  const MAX_VALUE_CHARS = 5000;
@@ -21,11 +22,12 @@ export function readNetlifyStateFile(cwd = process.cwd()) {
21
22
  }
22
23
  export function createNetlifyAdapter(opts) {
23
24
  const target = opts.target;
25
+ const base = pushApiBase(NETLIFY_API);
24
26
  let siteId = "";
25
27
  let accountId = "";
26
28
  let vars = null;
27
29
  const secretRefused = [];
28
- const request = (method, p, body) => platformRequest({ title: "Netlify", base: NETLIFY_API, token: opts.token, method, path: p, body });
30
+ const request = (method, p, body) => platformRequest({ title: "Netlify", base, token: opts.token, method, path: p, body });
29
31
  const envPath = (suffix = "") => `/api/v1/accounts/${encodeURIComponent(accountId)}/env${suffix}?site_id=${encodeURIComponent(siteId)}`;
30
32
  const valuesOf = (v) => v.values ?? [];
31
33
  const find = (name) => (vars ?? []).find((v) => v.key === name);
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { isPublicEnvName, isReservedPushName } from "@getmyenv/shared";
4
+ import { pushApiBase } from "../lib/api-url.js";
4
5
  import { PlatformError, platformRequest } from "./http.js";
5
6
  export const VERCEL_API = "https://api.vercel.com";
6
7
  const MAX_TOTAL_BYTES = 64 * 1024;
@@ -31,13 +32,14 @@ function vercelType(type) {
31
32
  }
32
33
  export function createVercelAdapter(opts) {
33
34
  const target = opts.target;
35
+ const base = pushApiBase(VERCEL_API);
34
36
  let projectId = "";
35
37
  let teamId;
36
38
  let envs = null;
37
39
  const request = (method, p, body) => {
38
40
  const sep = p.includes("?") ? "&" : "?";
39
41
  const full = teamId ? `${p}${sep}teamId=${encodeURIComponent(teamId)}` : p;
40
- return platformRequest({ title: "Vercel", base: VERCEL_API, token: opts.token, method, path: full, body });
42
+ return platformRequest({ title: "Vercel", base, token: opts.token, method, path: full, body });
41
43
  };
42
44
  /** Entries that set a value for the pushed target on every branch. Branch-only entries are not ours. */
43
45
  const forTarget = (key) => (envs ?? []).filter((e) => !e.gitBranch && targetsOf(e).includes(target) && (key === undefined || e.key === key));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getmyenv",
3
- "version": "0.13.0",
3
+ "version": "0.14.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": {
@@ -13,35 +13,42 @@
13
13
  "README.md",
14
14
  "LICENSE"
15
15
  ],
16
- "repository": {
17
- "type": "git",
18
- "url": "https://github.com/yobotn/getmyenv.git"
19
- },
20
16
  "homepage": "https://getmyenv.com",
21
17
  "bugs": {
22
- "url": "https://github.com/yobotn/getmyenv/issues"
18
+ "url": "https://getmyenv.com/contact",
19
+ "email": "hello@getmyenv.com"
23
20
  },
24
21
  "keywords": [
25
22
  "env",
26
- "environment",
27
- "secrets",
28
23
  "dotenv",
29
- "cli"
24
+ "environment-variables",
25
+ "secrets",
26
+ "secrets-management",
27
+ "encryption",
28
+ "e2ee",
29
+ "end-to-end-encryption",
30
+ "cli",
31
+ "config",
32
+ "configuration",
33
+ "devops",
34
+ "vault",
35
+ ".env",
36
+ "api-keys"
30
37
  ],
31
38
  "license": "MIT",
32
39
  "publishConfig": {
33
40
  "access": "public"
34
41
  },
35
42
  "dependencies": {
36
- "@getmyenv/crypto": "0.6.0",
37
- "@getmyenv/shared": "0.12.0",
43
+ "@getmyenv/crypto": "0.6.1",
44
+ "@getmyenv/shared": "0.13.0",
38
45
  "@napi-rs/keyring": "^2.1.0",
39
46
  "commander": "^15.0.0",
40
47
  "dotenv": "^16.4.7",
41
48
  "picocolors": "^1.1.1"
42
49
  },
43
50
  "devDependencies": {
44
- "@types/node": "^22.13.10",
51
+ "@types/node": "^24.19.0",
45
52
  "typescript": "^7.0.2",
46
53
  "vitest": "^4.1.11"
47
54
  },