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.
- package/README.md +19 -17
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +2 -2
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +13 -17
- package/dist/commands/backup.d.ts +1 -2
- package/dist/commands/backup.js +11 -7
- package/dist/commands/claim.d.ts +1 -2
- package/dist/commands/claim.js +84 -40
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +23 -7
- package/dist/commands/export.d.ts +0 -2
- package/dist/commands/export.js +12 -15
- package/dist/commands/guest.d.ts +2 -3
- package/dist/commands/guest.js +10 -11
- package/dist/commands/import.d.ts +2 -2
- package/dist/commands/import.js +6 -4
- package/dist/commands/lock.d.ts +1 -2
- package/dist/commands/lock.js +5 -9
- package/dist/commands/ls.d.ts +0 -1
- package/dist/commands/ls.js +0 -1
- package/dist/commands/restore.d.ts +3 -4
- package/dist/commands/restore.js +52 -28
- package/dist/commands/run.d.ts +0 -2
- package/dist/commands/run.js +10 -16
- package/dist/commands/set.d.ts +0 -2
- package/dist/commands/set.js +4 -17
- package/dist/commands/start.d.ts +8 -3
- package/dist/commands/start.js +49 -11
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/status.js +7 -2
- package/dist/commands/template.d.ts +13 -0
- package/dist/commands/template.js +75 -0
- package/dist/commands/unlink.d.ts +0 -1
- package/dist/commands/unlink.js +4 -8
- package/dist/lib/api-url.d.ts +0 -1
- package/dist/lib/api-url.js +0 -1
- package/dist/lib/api.d.ts +14 -6
- package/dist/lib/api.js +10 -5
- package/dist/lib/authorize.d.ts +0 -1
- package/dist/lib/authorize.js +2 -3
- package/dist/lib/bootstrap.d.ts +0 -1
- package/dist/lib/bootstrap.js +5 -3
- package/dist/lib/confirm.d.ts +0 -1
- package/dist/lib/confirm.js +0 -1
- package/dist/lib/context.d.ts +3 -2
- package/dist/lib/context.js +4 -2
- package/dist/lib/credentials.d.ts +4 -1
- package/dist/lib/credentials.js +28 -2
- package/dist/lib/fs.d.ts +10 -7
- package/dist/lib/fs.js +37 -22
- package/dist/lib/guest-key.d.ts +7 -4
- package/dist/lib/guest-key.js +22 -30
- package/dist/lib/identity.d.ts +1 -2
- package/dist/lib/identity.js +1 -2
- package/dist/lib/keychain.d.ts +19 -6
- package/dist/lib/keychain.js +69 -19
- package/dist/lib/known-keys.d.ts +3 -0
- package/dist/lib/known-keys.js +17 -0
- package/dist/lib/run-env.d.ts +2 -4
- package/dist/lib/run-env.js +9 -1
- package/dist/lib/update-check.d.ts +0 -1
- package/dist/lib/update-check.js +4 -2
- package/dist/lib/upload.d.ts +0 -2
- package/dist/lib/upload.js +11 -20
- package/dist/lib/vault.d.ts +54 -0
- package/dist/lib/vault.js +174 -0
- package/package.json +4 -5
- package/dist/bin.d.ts.map +0 -1
- package/dist/bin.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/backup.d.ts.map +0 -1
- package/dist/commands/backup.js.map +0 -1
- package/dist/commands/claim.d.ts.map +0 -1
- package/dist/commands/claim.js.map +0 -1
- package/dist/commands/context.d.ts.map +0 -1
- package/dist/commands/context.js.map +0 -1
- package/dist/commands/export.d.ts.map +0 -1
- package/dist/commands/export.js.map +0 -1
- package/dist/commands/guest.d.ts.map +0 -1
- package/dist/commands/guest.js.map +0 -1
- package/dist/commands/import.d.ts.map +0 -1
- package/dist/commands/import.js.map +0 -1
- package/dist/commands/lock.d.ts.map +0 -1
- package/dist/commands/lock.js.map +0 -1
- package/dist/commands/ls.d.ts.map +0 -1
- package/dist/commands/ls.js.map +0 -1
- package/dist/commands/restore.d.ts.map +0 -1
- package/dist/commands/restore.js.map +0 -1
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands/run.js.map +0 -1
- package/dist/commands/set.d.ts.map +0 -1
- package/dist/commands/set.js.map +0 -1
- package/dist/commands/start.d.ts.map +0 -1
- package/dist/commands/start.js.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/unlink.d.ts.map +0 -1
- package/dist/commands/unlink.js.map +0 -1
- package/dist/lib/api-url.d.ts.map +0 -1
- package/dist/lib/api-url.js.map +0 -1
- package/dist/lib/api.d.ts.map +0 -1
- package/dist/lib/api.js.map +0 -1
- package/dist/lib/authorize.d.ts.map +0 -1
- package/dist/lib/authorize.js.map +0 -1
- package/dist/lib/bootstrap.d.ts.map +0 -1
- package/dist/lib/bootstrap.js.map +0 -1
- package/dist/lib/confirm.d.ts.map +0 -1
- package/dist/lib/confirm.js.map +0 -1
- package/dist/lib/context.d.ts.map +0 -1
- package/dist/lib/context.js.map +0 -1
- package/dist/lib/credentials.d.ts.map +0 -1
- package/dist/lib/credentials.js.map +0 -1
- package/dist/lib/fs.d.ts.map +0 -1
- package/dist/lib/fs.js.map +0 -1
- package/dist/lib/guest-key.d.ts.map +0 -1
- package/dist/lib/guest-key.js.map +0 -1
- package/dist/lib/identity.d.ts.map +0 -1
- package/dist/lib/identity.js.map +0 -1
- package/dist/lib/keychain.d.ts.map +0 -1
- package/dist/lib/keychain.js.map +0 -1
- package/dist/lib/pek.d.ts +0 -27
- package/dist/lib/pek.d.ts.map +0 -1
- package/dist/lib/pek.js +0 -60
- package/dist/lib/pek.js.map +0 -1
- package/dist/lib/run-env.d.ts.map +0 -1
- package/dist/lib/run-env.js.map +0 -1
- package/dist/lib/unlock.d.ts +0 -24
- package/dist/lib/unlock.d.ts.map +0 -1
- package/dist/lib/unlock.js +0 -83
- package/dist/lib/unlock.js.map +0 -1
- package/dist/lib/update-check.d.ts.map +0 -1
- package/dist/lib/update-check.js.map +0 -1
- package/dist/lib/upload.d.ts.map +0 -1
- package/dist/lib/upload.js.map +0 -1
package/dist/commands/guest.js
CHANGED
|
@@ -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 {
|
|
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
|
|
15
|
-
* claim.json holds the guest passphrase
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
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.
|
|
55
|
-
console.error(pc.dim("claim.json holds the guest passphrase
|
|
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
|
-
|
|
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
|
package/dist/commands/import.js
CHANGED
|
@@ -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
|
-
|
|
165
|
-
|
|
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
|
package/dist/commands/lock.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
/** Forget
|
|
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
|
package/dist/commands/lock.js
CHANGED
|
@@ -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 {
|
|
6
|
-
/** Forget
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
package/dist/commands/ls.d.ts
CHANGED
package/dist/commands/ls.js
CHANGED
|
@@ -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
|
|
9
|
-
*
|
|
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
|
package/dist/commands/restore.js
CHANGED
|
@@ -1,43 +1,63 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import pc from "picocolors";
|
|
3
|
-
import { parseBackup, restoreValuesFromBackup, serializePayload,
|
|
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 {
|
|
8
|
+
import { contextKey } from "../lib/vault.js";
|
|
8
9
|
/**
|
|
9
|
-
* Restore a backup into this folder's project. The
|
|
10
|
-
*
|
|
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 &&
|
|
16
|
-
throw new Error("Restore replaces values
|
|
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
|
-
|
|
19
|
-
|
|
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: ${
|
|
35
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
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
|
-
|
|
93
|
+
const skipped = [...perContext.keys()].filter((slug) => !writable(slug));
|
|
68
94
|
console.log(`${pc.green("✓")} Restored ${res.restored} values into ${res.projectName}`);
|
|
69
|
-
if (
|
|
70
|
-
console.log(pc.yellow(`Skipped
|
|
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
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/run.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
package/dist/commands/set.d.ts
CHANGED
|
@@ -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
|
package/dist/commands/set.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
package/dist/commands/start.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
102
|
-
console.error("
|
|
103
|
-
console.error(pc.cyan(
|
|
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
|
-
/**
|
|
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
|
package/dist/commands/status.js
CHANGED
|
@@ -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(
|
|
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
|
+
}>;
|