getmyenv 0.6.2 → 0.8.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 (136) hide show
  1. package/README.md +19 -17
  2. package/dist/bin.d.ts +0 -1
  3. package/dist/bin.js +2 -2
  4. package/dist/cli.d.ts +0 -1
  5. package/dist/cli.js +13 -17
  6. package/dist/commands/backup.d.ts +1 -2
  7. package/dist/commands/backup.js +11 -7
  8. package/dist/commands/claim.d.ts +1 -2
  9. package/dist/commands/claim.js +84 -40
  10. package/dist/commands/context.d.ts +0 -1
  11. package/dist/commands/context.js +23 -7
  12. package/dist/commands/export.d.ts +0 -2
  13. package/dist/commands/export.js +12 -15
  14. package/dist/commands/guest.d.ts +2 -3
  15. package/dist/commands/guest.js +10 -11
  16. package/dist/commands/import.d.ts +2 -2
  17. package/dist/commands/import.js +6 -4
  18. package/dist/commands/lock.d.ts +1 -2
  19. package/dist/commands/lock.js +5 -9
  20. package/dist/commands/ls.d.ts +0 -1
  21. package/dist/commands/ls.js +0 -1
  22. package/dist/commands/restore.d.ts +3 -4
  23. package/dist/commands/restore.js +52 -28
  24. package/dist/commands/run.d.ts +0 -2
  25. package/dist/commands/run.js +10 -16
  26. package/dist/commands/set.d.ts +0 -2
  27. package/dist/commands/set.js +4 -17
  28. package/dist/commands/start.d.ts +8 -3
  29. package/dist/commands/start.js +49 -11
  30. package/dist/commands/status.d.ts +0 -1
  31. package/dist/commands/status.js +7 -2
  32. package/dist/commands/template.d.ts +13 -0
  33. package/dist/commands/template.js +75 -0
  34. package/dist/commands/unlink.d.ts +0 -1
  35. package/dist/commands/unlink.js +4 -8
  36. package/dist/lib/api-url.d.ts +0 -1
  37. package/dist/lib/api-url.js +0 -1
  38. package/dist/lib/api.d.ts +14 -6
  39. package/dist/lib/api.js +10 -5
  40. package/dist/lib/authorize.d.ts +0 -1
  41. package/dist/lib/authorize.js +2 -3
  42. package/dist/lib/bootstrap.d.ts +0 -1
  43. package/dist/lib/bootstrap.js +5 -3
  44. package/dist/lib/confirm.d.ts +0 -1
  45. package/dist/lib/confirm.js +0 -1
  46. package/dist/lib/context.d.ts +3 -2
  47. package/dist/lib/context.js +4 -2
  48. package/dist/lib/credentials.d.ts +4 -1
  49. package/dist/lib/credentials.js +28 -2
  50. package/dist/lib/fs.d.ts +10 -7
  51. package/dist/lib/fs.js +37 -22
  52. package/dist/lib/guest-key.d.ts +7 -4
  53. package/dist/lib/guest-key.js +22 -30
  54. package/dist/lib/identity.d.ts +1 -2
  55. package/dist/lib/identity.js +1 -2
  56. package/dist/lib/keychain.d.ts +19 -6
  57. package/dist/lib/keychain.js +69 -19
  58. package/dist/lib/known-keys.d.ts +3 -0
  59. package/dist/lib/known-keys.js +17 -0
  60. package/dist/lib/run-env.d.ts +2 -4
  61. package/dist/lib/run-env.js +9 -1
  62. package/dist/lib/update-check.d.ts +0 -1
  63. package/dist/lib/update-check.js +4 -2
  64. package/dist/lib/upload.d.ts +0 -2
  65. package/dist/lib/upload.js +11 -20
  66. package/dist/lib/vault.d.ts +54 -0
  67. package/dist/lib/vault.js +174 -0
  68. package/package.json +4 -5
  69. package/dist/bin.d.ts.map +0 -1
  70. package/dist/bin.js.map +0 -1
  71. package/dist/cli.d.ts.map +0 -1
  72. package/dist/cli.js.map +0 -1
  73. package/dist/commands/backup.d.ts.map +0 -1
  74. package/dist/commands/backup.js.map +0 -1
  75. package/dist/commands/claim.d.ts.map +0 -1
  76. package/dist/commands/claim.js.map +0 -1
  77. package/dist/commands/context.d.ts.map +0 -1
  78. package/dist/commands/context.js.map +0 -1
  79. package/dist/commands/export.d.ts.map +0 -1
  80. package/dist/commands/export.js.map +0 -1
  81. package/dist/commands/guest.d.ts.map +0 -1
  82. package/dist/commands/guest.js.map +0 -1
  83. package/dist/commands/import.d.ts.map +0 -1
  84. package/dist/commands/import.js.map +0 -1
  85. package/dist/commands/lock.d.ts.map +0 -1
  86. package/dist/commands/lock.js.map +0 -1
  87. package/dist/commands/ls.d.ts.map +0 -1
  88. package/dist/commands/ls.js.map +0 -1
  89. package/dist/commands/restore.d.ts.map +0 -1
  90. package/dist/commands/restore.js.map +0 -1
  91. package/dist/commands/run.d.ts.map +0 -1
  92. package/dist/commands/run.js.map +0 -1
  93. package/dist/commands/set.d.ts.map +0 -1
  94. package/dist/commands/set.js.map +0 -1
  95. package/dist/commands/start.d.ts.map +0 -1
  96. package/dist/commands/start.js.map +0 -1
  97. package/dist/commands/status.d.ts.map +0 -1
  98. package/dist/commands/status.js.map +0 -1
  99. package/dist/commands/unlink.d.ts.map +0 -1
  100. package/dist/commands/unlink.js.map +0 -1
  101. package/dist/lib/api-url.d.ts.map +0 -1
  102. package/dist/lib/api-url.js.map +0 -1
  103. package/dist/lib/api.d.ts.map +0 -1
  104. package/dist/lib/api.js.map +0 -1
  105. package/dist/lib/authorize.d.ts.map +0 -1
  106. package/dist/lib/authorize.js.map +0 -1
  107. package/dist/lib/bootstrap.d.ts.map +0 -1
  108. package/dist/lib/bootstrap.js.map +0 -1
  109. package/dist/lib/confirm.d.ts.map +0 -1
  110. package/dist/lib/confirm.js.map +0 -1
  111. package/dist/lib/context.d.ts.map +0 -1
  112. package/dist/lib/context.js.map +0 -1
  113. package/dist/lib/credentials.d.ts.map +0 -1
  114. package/dist/lib/credentials.js.map +0 -1
  115. package/dist/lib/fs.d.ts.map +0 -1
  116. package/dist/lib/fs.js.map +0 -1
  117. package/dist/lib/guest-key.d.ts.map +0 -1
  118. package/dist/lib/guest-key.js.map +0 -1
  119. package/dist/lib/identity.d.ts.map +0 -1
  120. package/dist/lib/identity.js.map +0 -1
  121. package/dist/lib/keychain.d.ts.map +0 -1
  122. package/dist/lib/keychain.js.map +0 -1
  123. package/dist/lib/pek.d.ts +0 -27
  124. package/dist/lib/pek.d.ts.map +0 -1
  125. package/dist/lib/pek.js +0 -60
  126. package/dist/lib/pek.js.map +0 -1
  127. package/dist/lib/run-env.d.ts.map +0 -1
  128. package/dist/lib/run-env.js.map +0 -1
  129. package/dist/lib/unlock.d.ts +0 -24
  130. package/dist/lib/unlock.d.ts.map +0 -1
  131. package/dist/lib/unlock.js +0 -83
  132. package/dist/lib/unlock.js.map +0 -1
  133. package/dist/lib/update-check.d.ts.map +0 -1
  134. package/dist/lib/update-check.js.map +0 -1
  135. package/dist/lib/upload.d.ts.map +0 -1
  136. package/dist/lib/upload.js.map +0 -1
@@ -1,18 +1,17 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import pc from "picocolors";
4
- import { createWrappedProjectKey, serializeWrappedKey, } from "@getmyenv/crypto";
4
+ import { createUserKeys, wipe } from "@getmyenv/crypto";
5
5
  import { ensureGitignoreHasEnv, readProjectMeta, writeClaimFile, writeProjectMeta, } from "../lib/fs.js";
6
6
  import { readFolderToken, writeFolderToken } from "../lib/credentials.js";
7
- import { writeCachedKey } from "../lib/pek.js";
8
7
  import { ApiClient, resolveApiUrl } from "../lib/api.js";
9
8
  export function folderNameHint() {
10
9
  return path.basename(process.cwd()) || "project";
11
10
  }
12
11
  /**
13
12
  * Create an unclaimed guest project with one open context, Development.
14
- * Writes token.json and key.json (gitignored), project.json and claim.json (committed).
15
- * claim.json holds the guest passphrase inside the project folder until claim.
13
+ * Writes token.json and claim.json (gitignored) and project.json (committed).
14
+ * claim.json holds the guest passphrase that wraps the guest key until claim.
16
15
  */
17
16
  export async function createGuestProject(opts = {}) {
18
17
  if (readFolderToken() || readProjectMeta()?.projectId) {
@@ -21,13 +20,13 @@ export async function createGuestProject(opts = {}) {
21
20
  const apiUrl = resolveApiUrl(opts.apiUrl);
22
21
  const name = (opts.name?.trim() || folderNameHint()).slice(0, 120);
23
22
  const passphrase = randomBytes(32).toString("base64url");
24
- const { wrapped } = await createWrappedProjectKey(passphrase);
25
- const wrappedRaw = serializeWrappedKey(wrapped);
23
+ const guestKeys = await createUserKeys(passphrase);
24
+ wipe(guestKeys.privateKey);
26
25
  let created;
27
26
  try {
28
27
  created = await new ApiClient(apiUrl).request("POST", "/api/anon-projects", {
29
28
  name,
30
- wrappedProjectKey: wrappedRaw,
29
+ keys: { publicKey: guestKeys.publicKey, wrappedPrivateKey: guestKeys.wrapped },
31
30
  });
32
31
  }
33
32
  catch (err) {
@@ -47,12 +46,13 @@ export async function createGuestProject(opts = {}) {
47
46
  projectName: created.projectName,
48
47
  guestExpiresAt: created.guestExpiresAt,
49
48
  });
50
- writeCachedKey(wrappedRaw);
51
49
  ensureGitignoreHasEnv();
52
50
  const daysLeft = Math.max(0, Math.ceil((new Date(created.guestExpiresAt).getTime() - Date.now()) / 86_400_000));
53
51
  console.error(`${pc.green("✓")} Guest project ${pc.bold(created.projectName)}. Context: development.`);
54
- console.error(pc.dim(`Expires in ${daysLeft} days unless claimed. Commit .getmyenv/project.json. Commit claim.json to private repos only. token.json and key.json stay local.`));
55
- console.error(pc.dim("claim.json holds the guest passphrase until you claim.\n"));
52
+ console.error(pc.dim(`Expires in ${daysLeft} days unless claimed. Commit .getmyenv/project.json. token.json and claim.json stay local.`));
53
+ console.error(pc.dim("claim.json holds the claim code and guest passphrase. Shown once, save them to claim later:"));
54
+ console.error(` ${pc.dim("Claim code:")} ${created.claimCode}`);
55
+ console.error(` ${pc.dim("Guest passphrase:")} ${passphrase}\n`);
56
56
  return created;
57
57
  }
58
58
  export async function guestCommand(opts = {}) {
@@ -65,4 +65,3 @@ export async function guestCommand(opts = {}) {
65
65
  console.error(pc.cyan(" npx getmyenv claim ") + pc.dim("# keep it, with an account"));
66
66
  console.error("");
67
67
  }
68
- //# sourceMappingURL=guest.js.map
@@ -4,7 +4,8 @@ export type ImportOpts = {
4
4
  prune?: boolean;
5
5
  dryRun?: boolean;
6
6
  yes?: boolean;
7
- vaultPasswordStdin?: boolean;
7
+ /** The caller prints its own run line. */
8
+ noRunLine?: boolean;
8
9
  };
9
10
  type Target = {
10
11
  slug: string;
@@ -23,4 +24,3 @@ export declare function planImport(files: string[], opts: {
23
24
  /** `import [file]`: encrypt .env values locally and upload them to contexts. */
24
25
  export declare function importCommand(file: string | undefined, opts: ImportOpts): Promise<void>;
25
26
  export {};
26
- //# sourceMappingURL=import.d.ts.map
@@ -5,6 +5,7 @@ import { contextSlugFromEnvFile, ensureGitignoreHasEnv, parseEnvFile, promptConf
5
5
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
6
6
  import { defaultContextSlug, findContext, resolveContext } from "../lib/context.js";
7
7
  import { confirmDiff } from "../lib/confirm.js";
8
+ import { printDidYouMean } from "../lib/known-keys.js";
8
9
  import { uploadValues } from "../lib/upload.js";
9
10
  import { runLine } from "./start.js";
10
11
  /** `.env` before `.env.local`, so local files win on merge. */
@@ -126,6 +127,7 @@ export async function importCommand(file, opts) {
126
127
  for (const name of hostedOnly) {
127
128
  console.error(` ${opts.prune ? pc.red("-") : pc.dim("=")} ${name}${pc.dim(opts.prune ? " (unset, not in file)" : " (kept, not in file)")}`);
128
129
  }
130
+ printDidYouMean(names, " ");
129
131
  if (opts.dryRun) {
130
132
  console.error("");
131
133
  continue;
@@ -147,7 +149,6 @@ export async function importCommand(file, opts) {
147
149
  context: ctx,
148
150
  pairs,
149
151
  prune: opts.prune,
150
- vaultPasswordStdin: opts.vaultPasswordStdin,
151
152
  });
152
153
  imported += res.set.length;
153
154
  changed += res.set.length + res.declared.length + res.pruned.length;
@@ -161,8 +162,9 @@ export async function importCommand(file, opts) {
161
162
  ensureGitignoreHasEnv();
162
163
  if (imported > 0) {
163
164
  console.error(pc.dim("Your .env files are unchanged. getmyenv run does not need them. Delete them when you are ready.\n"));
164
- console.error("Start your app with:");
165
- console.error(pc.cyan(` ${runLine(explicitContext ?? defaultContextSlug(who, meta))}\n`));
165
+ if (!opts.noRunLine) {
166
+ console.error("Start your app with:");
167
+ console.error(pc.cyan(` ${runLine(explicitContext ?? defaultContextSlug(who, meta))}\n`));
168
+ }
166
169
  }
167
170
  }
168
- //# sourceMappingURL=import.js.map
@@ -1,5 +1,4 @@
1
- /** Forget the unlocked project key on this machine. The next run asks for the Vault password. */
1
+ /** Forget every unlocked context key on this machine. The next run asks for the Vault password. */
2
2
  export declare function lockCommand(opts: {
3
3
  apiUrl?: string;
4
4
  }): Promise<void>;
5
- //# sourceMappingURL=lock.d.ts.map
@@ -2,17 +2,13 @@ import pc from "picocolors";
2
2
  import { readProjectMeta } from "../lib/fs.js";
3
3
  import { readFolderToken } from "../lib/credentials.js";
4
4
  import { resolveApiUrl } from "../lib/api.js";
5
- import { clearCachedPek, keychainAccount } from "../lib/keychain.js";
6
- /** Forget the unlocked project key on this machine. The next run asks for the Vault password. */
5
+ import { clearAllCachedKeys, keychainAccount } from "../lib/keychain.js";
6
+ /** Forget every unlocked context key on this machine. The next run asks for the Vault password. */
7
7
  export async function lockCommand(opts) {
8
8
  const projectId = readProjectMeta()?.projectId ?? readFolderToken()?.projectId;
9
- if (!projectId) {
10
- console.log(pc.dim("This folder is not linked."));
11
- return;
12
- }
13
- const removed = await clearCachedPek(keychainAccount(resolveApiUrl(opts.apiUrl), projectId));
14
- console.log(removed
9
+ const fallback = projectId ? [keychainAccount(resolveApiUrl(opts.apiUrl), projectId)] : [];
10
+ const removed = await clearAllCachedKeys(fallback);
11
+ console.log(removed > 0
15
12
  ? `${pc.green("✓")} Locked. The next run asks for the Vault password.`
16
13
  : pc.dim("Already locked."));
17
14
  }
18
- //# sourceMappingURL=lock.js.map
@@ -3,4 +3,3 @@ export declare function lsCommand(opts: {
3
3
  context?: string;
4
4
  apiUrl?: string;
5
5
  }): Promise<void>;
6
- //# sourceMappingURL=ls.d.ts.map
@@ -34,4 +34,3 @@ export async function lsCommand(opts) {
34
34
  console.log([k.name.padEnd(nameWidth), ...cells].join(" "));
35
35
  }
36
36
  }
37
- //# sourceMappingURL=ls.js.map
@@ -1,13 +1,12 @@
1
1
  type RestoreOpts = {
2
2
  apiUrl?: string;
3
- vaultPasswordStdin?: boolean;
4
3
  yes?: boolean;
5
4
  dryRun?: boolean;
6
5
  };
7
6
  /**
8
- * Restore a backup into this folder's project. The password is checked
9
- * locally first. Ciphertext is uploaded as is. Read-only contexts are skipped.
7
+ * Restore a backup into this folder's project. The backup opens locally with
8
+ * the Vault password it was made with. Values are encrypted again with this
9
+ * project's context keys. Read-only and unknown contexts are skipped.
10
10
  */
11
11
  export declare function restoreCommand(file: string, opts: RestoreOpts): Promise<void>;
12
12
  export {};
13
- //# sourceMappingURL=restore.d.ts.map
@@ -1,43 +1,63 @@
1
1
  import fs from "node:fs";
2
2
  import pc from "picocolors";
3
- import { parseBackup, restoreValuesFromBackup, serializePayload, serializeWrappedKey, } from "@getmyenv/crypto";
3
+ import { encryptSecret, parseBackup, restoreValuesFromBackup, serializePayload, } from "@getmyenv/crypto";
4
4
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
5
+ import { findContext } from "../lib/context.js";
5
6
  import { promptConfirm } from "../lib/fs.js";
6
7
  import { resolveVaultPassword } from "../lib/guest-key.js";
7
- import { writeCachedKey } from "../lib/pek.js";
8
+ import { contextKey } from "../lib/vault.js";
8
9
  /**
9
- * Restore a backup into this folder's project. The password is checked
10
- * locally first. Ciphertext is uploaded as is. Read-only contexts are skipped.
10
+ * Restore a backup into this folder's project. The backup opens locally with
11
+ * the Vault password it was made with. Values are encrypted again with this
12
+ * project's context keys. Read-only and unknown contexts are skipped.
11
13
  */
12
14
  export async function restoreCommand(file, opts) {
13
15
  if (!fs.existsSync(file))
14
16
  throw new Error(`File not found: ${file}`);
15
- if (!opts.yes && !opts.dryRun && (!process.stdin.isTTY || opts.vaultPasswordStdin)) {
16
- throw new Error("Restore replaces values and the Vault key. Pass --yes to confirm, or --dry-run to preview.");
17
+ if (!opts.yes && !opts.dryRun && !process.stdin.isTTY) {
18
+ throw new Error("Restore replaces values with the same names. Pass --yes to confirm, or --dry-run to preview.");
17
19
  }
18
- const envelope = parseBackup(fs.readFileSync(file, "utf8"));
19
- const { who, client } = await ensureTokenAndLink({ apiUrl: opts.apiUrl });
20
+ let envelope;
21
+ try {
22
+ envelope = parseBackup(fs.readFileSync(file, "utf8"));
23
+ }
24
+ catch (err) {
25
+ const msg = err instanceof Error ? err.message : "";
26
+ throw new Error(msg.startsWith("Unsupported backup version")
27
+ ? "This backup was made by an older version and cannot be restored."
28
+ : "Not a getmyenv backup file.");
29
+ }
30
+ const linked = await ensureTokenAndLink({ apiUrl: opts.apiUrl });
31
+ const { who, client } = linked;
20
32
  if (who.token.kind !== "folder") {
21
33
  throw new Error("Restore needs a signed-in folder token. Guest and server tokens cannot restore.");
22
34
  }
23
35
  const password = await resolveVaultPassword({
24
36
  isGuest: false,
25
- vaultPasswordStdin: opts.vaultPasswordStdin,
26
37
  prompt: "Vault password for this backup (hidden): ",
27
38
  });
39
+ let opened;
28
40
  try {
29
- await restoreValuesFromBackup(password, envelope);
41
+ opened = await restoreValuesFromBackup(password, envelope);
30
42
  }
31
43
  catch {
32
44
  throw new Error("Could not open the backup. Check the Vault password.");
33
45
  }
34
- console.error(`Backup checked: ${envelope.values.length} values in ${envelope.contexts.length} contexts.\n`);
35
- const locked = new Set(envelope.contexts.filter((c) => c.protection === "locked").map((c) => c.slug));
46
+ console.error(`Backup checked: ${opened.values.length} values in ${envelope.contexts.length} contexts.\n`);
47
+ const targets = new Map();
48
+ for (const c of envelope.contexts)
49
+ targets.set(c.slug, findContext(who.contexts, c.slug));
50
+ const writable = (slug) => targets.get(slug)?.protection === "open";
36
51
  const perContext = new Map();
37
- for (const v of envelope.values)
52
+ for (const v of opened.values)
38
53
  perContext.set(v.context, (perContext.get(v.context) ?? 0) + 1);
39
54
  for (const [slug, count] of perContext) {
40
- const note = locked.has(slug) ? pc.yellow(" (read-only, skipped)") : "";
55
+ const target = targets.get(slug);
56
+ const note = !target
57
+ ? pc.yellow(" (no such context here, skipped)")
58
+ : target.protection === "locked"
59
+ ? pc.yellow(" (read-only, skipped)")
60
+ : "";
41
61
  console.error(` ${slug}: ${count} value${count === 1 ? "" : "s"}${note}`);
42
62
  }
43
63
  console.error("");
@@ -46,29 +66,33 @@ export async function restoreCommand(file, opts) {
46
66
  return;
47
67
  }
48
68
  if (!opts.yes) {
49
- const ok = await promptConfirm(`Restore into ${who.project.name}? Existing values with the same names are replaced, and the project uses this backup's Vault password.`, { defaultYes: false });
69
+ const ok = await promptConfirm(`Restore into ${who.project.name}? Existing values with the same names are replaced.`, { defaultYes: false });
50
70
  if (!ok) {
51
71
  console.error(pc.dim("Nothing restored."));
52
72
  return;
53
73
  }
54
74
  }
55
- const res = await client.request("POST", "/api/cli/restore", {
56
- wrappedProjectKey: envelope.wrappedProjectKey,
57
- contexts: envelope.contexts,
58
- keys: envelope.keys,
59
- ...(envelope.tags ? { tags: envelope.tags } : {}),
60
- values: envelope.values.map((v) => ({
75
+ const values = [];
76
+ for (const v of opened.values) {
77
+ if (!writable(v.context))
78
+ continue;
79
+ const target = targets.get(v.context);
80
+ const key = await contextKey(linked, target, { create: true });
81
+ values.push({
61
82
  key: v.key,
62
83
  context: v.context,
63
84
  expiresAt: v.expiresAt,
64
- ciphertext: serializePayload(v.payload),
65
- })),
85
+ ciphertext: serializePayload(encryptSecret(key, v.key, v.value)),
86
+ });
87
+ }
88
+ const res = await client.request("POST", "/api/cli/restore", {
89
+ keys: envelope.keys,
90
+ tags: envelope.tags ?? {},
91
+ values,
66
92
  });
67
- writeCachedKey(serializeWrappedKey(envelope.wrappedProjectKey));
93
+ const skipped = [...perContext.keys()].filter((slug) => !writable(slug));
68
94
  console.log(`${pc.green("✓")} Restored ${res.restored} values into ${res.projectName}`);
69
- if (res.skippedContexts.length > 0) {
70
- console.log(pc.yellow(`Skipped read-only contexts: ${res.skippedContexts.join(", ")}. Restore those in the dashboard.`));
95
+ if (skipped.length > 0) {
96
+ console.log(pc.yellow(`Skipped: ${skipped.join(", ")}. Restore read-only contexts in the dashboard.`));
71
97
  }
72
- console.log(pc.dim("The project now uses this backup's Vault password."));
73
98
  }
74
- //# sourceMappingURL=restore.js.map
@@ -4,8 +4,6 @@ export type RunOpts = {
4
4
  explain?: boolean;
5
5
  keepExisting?: boolean;
6
6
  allowMissing?: boolean;
7
- vaultPasswordStdin?: boolean;
8
7
  };
9
8
  /** Resolve, validate, merge, spawn. */
10
9
  export declare function runCommand(opts: RunOpts, childArgs: string[]): Promise<void>;
11
- //# sourceMappingURL=run.d.ts.map
@@ -6,9 +6,9 @@ import { ApiError } from "../lib/api.js";
6
6
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
7
7
  import { resolveContext } from "../lib/context.js";
8
8
  import { signMachineProof } from "../lib/identity.js";
9
- import { unlockProjectKey } from "../lib/unlock.js";
9
+ import { contextKey } from "../lib/vault.js";
10
10
  import { sleep } from "../lib/authorize.js";
11
- import { explainLines, mergeEnv, signalExitCode, validationError, windowsCommandLine, } from "../lib/run-env.js";
11
+ import { assertVariableNames, explainLines, mergeEnv, signalExitCode, validationError, windowsCommandLine, } from "../lib/run-env.js";
12
12
  const GRANT_HEADER = "X-Getmyenv-Grant";
13
13
  /** Status lines go to stderr so the child's stdout stays clean for pipes. */
14
14
  const log = (line = "") => console.error(line);
@@ -41,7 +41,8 @@ export async function runCommand(opts, childArgs) {
41
41
  if (childArgs.length === 0 && !opts.explain) {
42
42
  throw new Error("Usage: npx getmyenv run [context] -- <command>");
43
43
  }
44
- const { who, client, meta, apiUrl } = await ensureTokenAndLink({ apiUrl: opts.apiUrl });
44
+ const linked = await ensureTokenAndLink({ apiUrl: opts.apiUrl });
45
+ const { who, client, meta } = linked;
45
46
  const ctx = resolveContext(who, meta, opts.context);
46
47
  let res = await resolveOnce(client, who.project.id, ctx.slug);
47
48
  if (res.status === "pending") {
@@ -58,21 +59,15 @@ export async function runCommand(opts, childArgs) {
58
59
  if (res.status !== "ok")
59
60
  throw new Error("Approval did not complete. Run the command again.");
60
61
  }
62
+ if (res.via === "auto") {
63
+ log(`${pc.green("✓")} This machine is now trusted for ${res.context.slug}.`);
64
+ }
65
+ assertVariableNames([...res.values.map((v) => v.name), ...res.missing.map((m) => m.name)]);
61
66
  const values = {};
62
67
  if (res.values.length > 0) {
63
- if (res.wrappedProjectKey == null) {
64
- throw new Error("This project has values but no key. Restore the key from a backup.");
65
- }
66
- const { pek } = await unlockProjectKey({
67
- isGuest: who.isGuest,
68
- projectId: who.project.id,
69
- wrapped: res.wrappedProjectKey,
70
- cache: res.cacheKey,
71
- vaultPasswordStdin: opts.vaultPasswordStdin,
72
- apiUrl,
73
- });
68
+ const key = await contextKey(linked, { ...res.context, sealedKey: res.sealedKey });
74
69
  for (const v of res.values) {
75
- values[v.name] = decryptSecret(pek, parsePayload(v.ciphertext));
70
+ values[v.name] = decryptSecret(key, v.name, parsePayload(v.ciphertext));
76
71
  }
77
72
  }
78
73
  const merged = mergeEnv(process.env, values, res.missing, opts.keepExisting === true);
@@ -122,4 +117,3 @@ export async function runCommand(opts, childArgs) {
122
117
  });
123
118
  process.exit(code);
124
119
  }
125
- //# sourceMappingURL=run.js.map
@@ -2,7 +2,6 @@ export type SetOpts = {
2
2
  context?: string;
3
3
  apiUrl?: string;
4
4
  expires?: string;
5
- vaultPasswordStdin?: boolean;
6
5
  /** Read the one value from stdin, so it never lands in argv or shell history. */
7
6
  stdin?: boolean;
8
7
  };
@@ -13,4 +12,3 @@ export declare function unsetCommand(names: string[], opts: {
13
12
  apiUrl?: string;
14
13
  all?: boolean;
15
14
  }): Promise<void>;
16
- //# sourceMappingURL=set.d.ts.map
@@ -1,9 +1,9 @@
1
1
  import pc from "picocolors";
2
2
  import { variableNameSchema } from "@getmyenv/shared";
3
3
  import { promptHidden } from "../lib/fs.js";
4
- import { presetVaultPassword } from "../lib/guest-key.js";
5
4
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
6
5
  import { resolveContext } from "../lib/context.js";
6
+ import { printDidYouMean } from "../lib/known-keys.js";
7
7
  import { assertWritable, uploadValues } from "../lib/upload.js";
8
8
  async function readStdinValue() {
9
9
  const chunks = [];
@@ -50,20 +50,8 @@ export async function setCommand(args, opts) {
50
50
  const ctx = resolveContext(linked.who, linked.meta, opts.context);
51
51
  assertWritable(linked, ctx);
52
52
  const values = {};
53
- if (opts.stdin) {
54
- const input = await readStdinValue();
55
- if (opts.vaultPasswordStdin) {
56
- const nl = input.indexOf("\n");
57
- if (nl < 0) {
58
- throw new Error("With --vault-password-stdin, send the password on the first line and the value after it.");
59
- }
60
- presetVaultPassword(input.slice(0, nl).replace(/\r$/, ""));
61
- values[pairs[0].name] = input.slice(nl + 1);
62
- }
63
- else {
64
- values[pairs[0].name] = input;
65
- }
66
- }
53
+ if (opts.stdin)
54
+ values[pairs[0].name] = await readStdinValue();
67
55
  for (const p of pairs) {
68
56
  if (opts.stdin)
69
57
  break;
@@ -81,12 +69,12 @@ export async function setCommand(args, opts) {
81
69
  context: ctx,
82
70
  pairs: values,
83
71
  expiresAt,
84
- vaultPasswordStdin: opts.vaultPasswordStdin,
85
72
  });
86
73
  for (const name of res.set)
87
74
  console.log(`${pc.green("✓")} ${name} ${pc.dim(`set in ${ctx.slug}`)}`);
88
75
  for (const name of res.declared)
89
76
  console.log(`${pc.yellow("○")} ${name} ${pc.dim(`declared, no value in ${ctx.slug}`)}`);
77
+ printDidYouMean(pairs.map((p) => p.name).filter((n) => !linked.who.keys.some((k) => k.name === n)));
90
78
  }
91
79
  export async function unsetCommand(names, opts) {
92
80
  for (const n of names) {
@@ -108,4 +96,3 @@ export async function unsetCommand(names, opts) {
108
96
  if (skipped.length > 0)
109
97
  console.log(pc.dim(`No change: ${skipped.join(", ")}`));
110
98
  }
111
- //# sourceMappingURL=set.js.map
@@ -1,6 +1,7 @@
1
+ import { type PublicTemplate } from "@getmyenv/shared";
1
2
  /** Best guess at the app's start command, for the printed `run` line. */
2
3
  export declare function suggestedStartCommand(cwd?: string): string;
3
- export declare function runLine(context: string | null, cwd?: string): string;
4
+ export declare function runLine(context: string | null, cwd?: string, startCommand?: string | null): string;
4
5
  /**
5
6
  * Guided setup for an unlinked folder, in a terminal.
6
7
  * Start without an account, or sign in. Then offer to import a detected .env.
@@ -8,9 +9,13 @@ export declare function runLine(context: string | null, cwd?: string): string;
8
9
  export declare function startSetup(opts: {
9
10
  apiUrl?: string;
10
11
  thenContinue?: boolean;
12
+ template?: PublicTemplate;
11
13
  }): Promise<void>;
12
- /** Bare `getmyenv`: setup when unlinked (terminal only), status when linked. */
14
+ /**
15
+ * Bare `getmyenv`: setup when unlinked (terminal only), status when linked.
16
+ * With --template, add the template's names and ask for their values.
17
+ */
13
18
  export declare function startCommand(opts: {
14
19
  apiUrl?: string;
20
+ template?: string;
15
21
  }): Promise<void>;
16
- //# sourceMappingURL=start.d.ts.map
@@ -8,6 +8,7 @@ import { authorizeFolder } from "../lib/authorize.js";
8
8
  import { NOT_LINKED_STEPS } from "../lib/bootstrap.js";
9
9
  import { createGuestProject, folderNameHint } from "./guest.js";
10
10
  import { statusCommand } from "./status.js";
11
+ import { applyTemplate, fetchTemplate, printTemplate } from "./template.js";
11
12
  /** Best guess at the app's start command, for the printed `run` line. */
12
13
  export function suggestedStartCommand(cwd = process.cwd()) {
13
14
  const pkgFile = path.join(cwd, "package.json");
@@ -32,9 +33,9 @@ export function suggestedStartCommand(cwd = process.cwd()) {
32
33
  return "cargo run";
33
34
  return "<your start command>";
34
35
  }
35
- export function runLine(context, cwd = process.cwd()) {
36
+ export function runLine(context, cwd = process.cwd(), startCommand) {
36
37
  const ctx = context && context !== DEFAULT_CONTEXT_SLUG ? ` ${context}` : "";
37
- return `npx getmyenv run${ctx} -- ${suggestedStartCommand(cwd)}`;
38
+ return `npx getmyenv run${ctx} -- ${startCommand || suggestedStartCommand(cwd)}`;
38
39
  }
39
40
  async function nameFirstContext(apiUrl) {
40
41
  const answer = (await readLine(`Name your first context ${pc.dim("[Development]")}: `)).trim();
@@ -47,7 +48,7 @@ async function nameFirstContext(apiUrl) {
47
48
  }
48
49
  /** A project that already has keys or its own contexts is set up. Do not ask for a first context. */
49
50
  function isFreshProject(who) {
50
- if (who.keys.length > 0)
51
+ if (who.token.member || who.keys.length > 0)
51
52
  return false;
52
53
  return who.contexts.every((c) => c.slug === DEFAULT_CONTEXT_SLUG || c.protection === "locked");
53
54
  }
@@ -62,6 +63,8 @@ function defaultContextFor(who) {
62
63
  export async function startSetup(opts) {
63
64
  console.error(pc.bold("\ngetmyenv"));
64
65
  console.error(pc.dim("Keep using environment variables. Stop managing them as files.\n"));
66
+ if (opts.template)
67
+ printTemplate(opts.template);
65
68
  const apiUrl = resolveApiUrl(opts.apiUrl);
66
69
  const existing = readProjectMeta();
67
70
  if (readClaimFile()) {
@@ -70,11 +73,17 @@ export async function startSetup(opts) {
70
73
  if (existing?.projectId) {
71
74
  console.error(`This folder uses ${pc.bold(existing.projectName ?? existing.projectId)}. Sign in to get a token.\n`);
72
75
  await authorizeFolder({ apiUrl });
76
+ if (opts.template) {
77
+ const { context } = await applyTemplate(opts.template, { apiUrl });
78
+ if (!opts.thenContinue)
79
+ printRunLine(context, opts.template.startCommand);
80
+ }
73
81
  return;
74
82
  }
75
83
  const choice = await promptChoice("How do you want to start?", ["Start without an account (guest project, claim within 60 days)", "Sign in"], 0);
76
84
  console.error("");
77
85
  let context = DEFAULT_CONTEXT_SLUG;
86
+ let dashboardUrl = null;
78
87
  if (choice === 0) {
79
88
  const name = (await readLine(`Project name ${pc.dim(`[${folderNameHint()}]`)}: `)).trim() ||
80
89
  folderNameHint();
@@ -82,10 +91,18 @@ export async function startSetup(opts) {
82
91
  }
83
92
  else {
84
93
  const who = await authorizeFolder({ apiUrl });
85
- context = isFreshProject(who) ? await nameFirstContext(apiUrl) : defaultContextFor(who);
94
+ context =
95
+ isFreshProject(who) && !opts.template ? await nameFirstContext(apiUrl) : defaultContextFor(who);
86
96
  const meta = readProjectMeta();
87
97
  if (meta)
88
98
  writeProjectMeta({ ...meta, context });
99
+ dashboardUrl = `${apiUrl.replace(/\/$/, "")}/projects/${who.project.id}/${context}`;
100
+ }
101
+ if (opts.template) {
102
+ const applied = await applyTemplate(opts.template, { apiUrl });
103
+ if (!opts.thenContinue)
104
+ printRunLine(applied.context, opts.template.startCommand);
105
+ return;
89
106
  }
90
107
  const files = rootEnvFiles();
91
108
  if (files.length > 0) {
@@ -95,17 +112,39 @@ export async function startSetup(opts) {
95
112
  });
96
113
  if (ok) {
97
114
  const { importCommand } = await import("./import.js");
98
- await importCommand(undefined, { apiUrl, yes: true });
115
+ await importCommand(undefined, { apiUrl, yes: true, noRunLine: true });
99
116
  }
100
117
  }
101
- if (!opts.thenContinue) {
102
- console.error("Start your app with:\n");
103
- console.error(pc.cyan(` ${runLine(context)}\n`));
118
+ else {
119
+ console.error("No .env file in this folder. Add values with:\n");
120
+ console.error(pc.cyan(" npx getmyenv set API_KEY"));
121
+ if (dashboardUrl)
122
+ console.error(pc.dim(` or in the dashboard: ${dashboardUrl}`));
123
+ console.error("");
104
124
  }
125
+ if (!opts.thenContinue)
126
+ printRunLine(context);
127
+ }
128
+ function printRunLine(context, startCommand) {
129
+ console.error("Start your app with:\n");
130
+ console.error(pc.cyan(` ${runLine(context, process.cwd(), startCommand)}\n`));
105
131
  }
106
- /** Bare `getmyenv`: setup when unlinked (terminal only), status when linked. */
132
+ /**
133
+ * Bare `getmyenv`: setup when unlinked (terminal only), status when linked.
134
+ * With --template, add the template's names and ask for their values.
135
+ */
107
136
  export async function startCommand(opts) {
137
+ const template = opts.template ? await fetchTemplate(opts.template, opts.apiUrl) : undefined;
138
+ if (template && !process.stdin.isTTY) {
139
+ throw new Error("start --template asks for values. Run it in a terminal.");
140
+ }
108
141
  if (getSessionToken()) {
142
+ if (template) {
143
+ printTemplate(template);
144
+ const { context } = await applyTemplate(template, opts);
145
+ printRunLine(context, template.startCommand);
146
+ return;
147
+ }
109
148
  await statusCommand(opts);
110
149
  return;
111
150
  }
@@ -113,6 +152,5 @@ export async function startCommand(opts) {
113
152
  console.log(NOT_LINKED_STEPS);
114
153
  return;
115
154
  }
116
- await startSetup({ apiUrl: opts.apiUrl });
155
+ await startSetup({ apiUrl: opts.apiUrl, template });
117
156
  }
118
- //# sourceMappingURL=start.js.map
@@ -2,4 +2,3 @@
2
2
  export declare function statusCommand(opts?: {
3
3
  apiUrl?: string;
4
4
  }): Promise<void>;
5
- //# sourceMappingURL=status.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import pc from "picocolors";
2
+ import { NEW_MACHINE_POLICY_LABELS } from "@getmyenv/shared";
2
3
  import { isDefaultApiUrl } from "../lib/api.js";
3
4
  import { ensureTokenAndLink } from "../lib/bootstrap.js";
4
5
  import { defaultContextSlug } from "../lib/context.js";
@@ -13,7 +14,12 @@ export async function statusCommand(opts = {}) {
13
14
  const { who, apiUrl, meta } = await ensureTokenAndLink({ apiUrl: opts.apiUrl });
14
15
  const current = defaultContextSlug(who, meta);
15
16
  console.log(`${pc.bold(who.project.name)}${who.user ? pc.dim(` ${who.user.email}`) : ""}`);
16
- console.log(pc.dim(`Token: ${TOKEN_KIND[who.token.kind] ?? who.token.kind}`));
17
+ console.log(pc.dim(who.token.member
18
+ ? "Token: folder, shared project (the contexts the owner shared with you)"
19
+ : `Token: ${TOKEN_KIND[who.token.kind] ?? who.token.kind}`));
20
+ if (who.token.kind === "server" && who.token.newMachinePolicy) {
21
+ console.log(pc.dim(`New machines: ${NEW_MACHINE_POLICY_LABELS[who.token.newMachinePolicy]}`));
22
+ }
17
23
  if (process.env.GETMYENV_TOKEN?.trim())
18
24
  console.log(pc.dim("Source: GETMYENV_TOKEN"));
19
25
  if (!isDefaultApiUrl(apiUrl))
@@ -37,4 +43,3 @@ export async function statusCommand(opts = {}) {
37
43
  console.log(pc.cyan(` ${runLine(current)}`));
38
44
  console.log("");
39
45
  }
40
- //# sourceMappingURL=status.js.map
@@ -0,0 +1,13 @@
1
+ import { type PublicTemplate } from "@getmyenv/shared";
2
+ /** Load a public template by id or URL. No token needed. */
3
+ export declare function fetchTemplate(ref: string, apiUrl?: string): Promise<PublicTemplate>;
4
+ export declare function printTemplate(template: PublicTemplate): void;
5
+ /**
6
+ * Add the template's names to the linked project, then ask for each value
7
+ * that is still missing in the folder's default context. Enter skips one.
8
+ */
9
+ export declare function applyTemplate(template: PublicTemplate, opts: {
10
+ apiUrl?: string;
11
+ }): Promise<{
12
+ context: string;
13
+ }>;