getmyenv 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/commands/backup.js +2 -2
- package/dist/commands/guest.d.ts +6 -1
- package/dist/commands/guest.js +15 -5
- package/dist/commands/import.js +4 -0
- package/dist/commands/ls.js +2 -0
- package/dist/commands/restore.js +2 -0
- package/dist/commands/set.js +3 -0
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +95 -14
- package/dist/commands/status.js +2 -0
- package/dist/commands/template.d.ts +19 -1
- package/dist/commands/template.js +40 -3
- package/dist/lib/authorize.d.ts +10 -0
- package/dist/lib/authorize.js +17 -6
- package/dist/lib/fs.d.ts +1 -0
- package/dist/lib/fs.js +5 -3
- package/dist/lib/template-file.d.ts +34 -0
- package/dist/lib/template-file.js +85 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,10 +32,21 @@ npx getmyenv run staging -- npm start # or pick one
|
|
|
32
32
|
|
|
33
33
|
Prompts and status lines go to stderr, so `export --stdout > .env` stays clean.
|
|
34
34
|
|
|
35
|
-
Commit `.getmyenv/project.json`. `token.json` and `claim.json` stay local and are gitignored.
|
|
35
|
+
Commit `.getmyenv/project.json` and `.getmyenv/template.json`. `token.json` and `claim.json` stay local and are gitignored. `template.json` lists the variable names, never values. In a fresh clone, `npx getmyenv` can start a new project from it and asks for each value.
|
|
36
|
+
|
|
37
|
+
What the CLI writes. Not every folder has all four:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
my-app/
|
|
41
|
+
.getmyenv/
|
|
42
|
+
project.json committed. Project id, name and the folder's default context. Written by setup, guest or sign-in.
|
|
43
|
+
template.json committed. Variable names, never values. Written once the project has a variable name.
|
|
44
|
+
token.json gitignored. The folder token. Written by sign-in or guest. Not used with GETMYENV_TOKEN.
|
|
45
|
+
claim.json gitignored, guest only. Claim code and guest passphrase. Keep it private. Written by guest. Deleted by claim.
|
|
46
|
+
```
|
|
36
47
|
|
|
37
48
|
<!-- cli:help -->
|
|
38
|
-
Output of `npx getmyenv --help` (0.
|
|
49
|
+
Output of `npx getmyenv --help` (0.11.1):
|
|
39
50
|
|
|
40
51
|
```text
|
|
41
52
|
Usage: getmyenv [options] [command]
|
|
@@ -88,7 +99,7 @@ Environment:
|
|
|
88
99
|
|
|
89
100
|
## Vault password
|
|
90
101
|
|
|
91
|
-
Each context has its own key,
|
|
102
|
+
One Vault password per account unlocks your private key on your device. Each context has its own key, and each reader gets their own sealed copy. Forgot the Vault password? Your recovery code sets a new one in the dashboard. For open contexts, after you enter the Vault password in a terminal, the unlocked context keys stay in your OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) for 8 hours. The password is never stored.
|
|
92
103
|
|
|
93
104
|
- `npx getmyenv lock` removes every entry. `unlink` removes this project's.
|
|
94
105
|
- Server tokens hold their context key and never ask.
|
package/dist/commands/backup.js
CHANGED
|
@@ -33,6 +33,6 @@ export async function backupCommand(opts) {
|
|
|
33
33
|
writePrivateFile(out, serializeBackup(envelope), { overwrite: false });
|
|
34
34
|
console.log(`${pc.green("✓")} Encrypted backup: ${out} (${data.values.length} values, ${data.contexts.length} open contexts)`);
|
|
35
35
|
console.log(pc.dim(data.recoveryWrappedKey
|
|
36
|
-
? "It opens with your
|
|
37
|
-
: "It opens with your
|
|
36
|
+
? "It opens with your Vault password or recovery code as they are now, not with newer ones. Read-only contexts are backed up from the dashboard."
|
|
37
|
+
: "It opens with your Vault password as it is now, not with a newer one. Read-only contexts are backed up from the dashboard."));
|
|
38
38
|
}
|
package/dist/commands/guest.d.ts
CHANGED
|
@@ -2,13 +2,17 @@ export type GuestOpts = {
|
|
|
2
2
|
name?: string;
|
|
3
3
|
apiUrl?: string;
|
|
4
4
|
};
|
|
5
|
+
type CreateGuestOpts = GuestOpts & {
|
|
6
|
+
/** New project from the template file: project.json may point to another project, and is replaced once this one exists. */
|
|
7
|
+
replaceProjectMeta?: boolean;
|
|
8
|
+
};
|
|
5
9
|
export declare function folderNameHint(): string;
|
|
6
10
|
/**
|
|
7
11
|
* Create an unclaimed guest project with one open context, Development.
|
|
8
12
|
* Writes token.json and claim.json (gitignored) and project.json (committed).
|
|
9
13
|
* claim.json holds the guest passphrase that wraps the guest key until claim.
|
|
10
14
|
*/
|
|
11
|
-
export declare function createGuestProject(opts?:
|
|
15
|
+
export declare function createGuestProject(opts?: CreateGuestOpts): Promise<{
|
|
12
16
|
projectId: string;
|
|
13
17
|
projectName: string;
|
|
14
18
|
guestExpiresAt: string;
|
|
@@ -16,3 +20,4 @@ export declare function createGuestProject(opts?: GuestOpts): Promise<{
|
|
|
16
20
|
/** The steps printed after guest, in plain text. Runtime first, export only as an alternative. */
|
|
17
21
|
export declare function guestNextLines(): string[];
|
|
18
22
|
export declare function guestCommand(opts?: GuestOpts): Promise<void>;
|
|
23
|
+
export {};
|
package/dist/commands/guest.js
CHANGED
|
@@ -2,10 +2,11 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pc from "picocolors";
|
|
4
4
|
import { createUserKeys, wipe } from "@getmyenv/crypto";
|
|
5
|
-
import { FACTS } from "@getmyenv/shared";
|
|
5
|
+
import { FACTS, TEMPLATE_FILE } from "@getmyenv/shared";
|
|
6
6
|
import { ensureGitignoreHasEnv, readProjectMeta, writeClaimFile, writeProjectMeta, } from "../lib/fs.js";
|
|
7
7
|
import { readFolderToken, writeFolderToken } from "../lib/credentials.js";
|
|
8
8
|
import { ApiClient, resolveApiUrl } from "../lib/api.js";
|
|
9
|
+
import { readTemplateFile } from "../lib/template-file.js";
|
|
9
10
|
export function folderNameHint() {
|
|
10
11
|
return path.basename(process.cwd()) || "project";
|
|
11
12
|
}
|
|
@@ -15,7 +16,7 @@ export function folderNameHint() {
|
|
|
15
16
|
* claim.json holds the guest passphrase that wraps the guest key until claim.
|
|
16
17
|
*/
|
|
17
18
|
export async function createGuestProject(opts = {}) {
|
|
18
|
-
if (readFolderToken() || readProjectMeta()?.projectId) {
|
|
19
|
+
if (readFolderToken() || (readProjectMeta()?.projectId && !opts.replaceProjectMeta)) {
|
|
19
20
|
throw new Error("This folder is already linked. Run: npx getmyenv (or npx getmyenv unlink first)");
|
|
20
21
|
}
|
|
21
22
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
@@ -50,14 +51,14 @@ export async function createGuestProject(opts = {}) {
|
|
|
50
51
|
ensureGitignoreHasEnv();
|
|
51
52
|
const daysLeft = Math.max(0, Math.ceil((new Date(created.guestExpiresAt).getTime() - Date.now()) / 86_400_000));
|
|
52
53
|
console.error(`${pc.green("✓")} Guest project ${pc.bold(created.projectName)}. Context: development.`);
|
|
53
|
-
console.error(pc.dim(`Expires in ${daysLeft} days unless claimed.
|
|
54
|
+
console.error(pc.dim(`Expires in ${daysLeft} days unless claimed. ${FACTS.committedFiles}`));
|
|
54
55
|
if (process.stdin.isTTY) {
|
|
55
56
|
console.error(pc.dim("claim.json holds the claim code and guest passphrase. Shown once, save them to claim later:"));
|
|
56
57
|
console.error(` ${pc.dim("Claim code:")} ${created.claimCode}`);
|
|
57
58
|
console.error(` ${pc.dim("Guest passphrase:")} ${passphrase}\n`);
|
|
58
59
|
}
|
|
59
60
|
else {
|
|
60
|
-
console.error(pc.dim(`claim.json holds the claim code and guest passphrase. It is the only copy. Keep it private. ${FACTS.claimLoss}\n`));
|
|
61
|
+
console.error(pc.dim(`claim.json holds the claim code and guest passphrase. It is the only copy. Keep it private. ${FACTS.claimLoss} ${FACTS.templateFileRecovery}\n`));
|
|
61
62
|
}
|
|
62
63
|
return created;
|
|
63
64
|
}
|
|
@@ -87,7 +88,16 @@ function colorNextLine(line) {
|
|
|
87
88
|
}
|
|
88
89
|
export async function guestCommand(opts = {}) {
|
|
89
90
|
console.error(pc.bold("\ngetmyenv guest\n"));
|
|
90
|
-
|
|
91
|
+
const read = readTemplateFile();
|
|
92
|
+
if (read && !read.ok)
|
|
93
|
+
console.error(pc.yellow(`${TEMPLATE_FILE} is not valid (${read.reason}). Not used.\n`));
|
|
94
|
+
const file = read?.ok ? read.file : null;
|
|
95
|
+
const name = opts.name ?? file?.name;
|
|
96
|
+
await createGuestProject({ ...opts, ...(name ? { name } : {}) });
|
|
97
|
+
if (file && file.keys.length > 0) {
|
|
98
|
+
const { applyTemplateFile } = await import("./template.js");
|
|
99
|
+
await applyTemplateFile(file, { apiUrl: opts.apiUrl, ask: false });
|
|
100
|
+
}
|
|
91
101
|
for (const line of guestNextLines())
|
|
92
102
|
console.error(colorNextLine(line));
|
|
93
103
|
console.error("");
|
package/dist/commands/import.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
|
7
7
|
import { defaultContextSlug, findContext, resolveContext } from "../lib/context.js";
|
|
8
8
|
import { confirmDiff } from "../lib/confirm.js";
|
|
9
9
|
import { printDidYouMean } from "../lib/known-keys.js";
|
|
10
|
+
import { syncFromLinked } from "../lib/template-file.js";
|
|
10
11
|
import { uploadValues } from "../lib/upload.js";
|
|
11
12
|
import { runLine } from "./start.js";
|
|
12
13
|
/** `.env` before `.env.local`, so local files win on merge. */
|
|
@@ -96,6 +97,7 @@ export async function importCommand(file, opts) {
|
|
|
96
97
|
let imported = 0;
|
|
97
98
|
let changed = 0;
|
|
98
99
|
let skipped = 0;
|
|
100
|
+
const added = [];
|
|
99
101
|
for (const target of targets) {
|
|
100
102
|
const label = target.files.map((f) => path.relative(process.cwd(), f) || f).join(" + ");
|
|
101
103
|
const ctx = await ensureTargetContext(linked, target.slug, opts);
|
|
@@ -153,10 +155,12 @@ export async function importCommand(file, opts) {
|
|
|
153
155
|
});
|
|
154
156
|
imported += res.set.length;
|
|
155
157
|
changed += res.set.length + res.declared.length + res.pruned.length;
|
|
158
|
+
added.push(...res.set, ...res.declared);
|
|
156
159
|
console.error(`${pc.green("✓")} ${res.set.length} set, ${res.declared.length} declared${res.pruned.length ? `, ${res.pruned.length} unset` : ""} in ${ctx.slug}\n`);
|
|
157
160
|
}
|
|
158
161
|
if (opts.dryRun)
|
|
159
162
|
return;
|
|
163
|
+
syncFromLinked(linked, { added });
|
|
160
164
|
if (changed === 0 && skipped > 0) {
|
|
161
165
|
throw new Error("Nothing was imported. See the skipped files above.");
|
|
162
166
|
}
|
package/dist/commands/ls.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
3
3
|
import { resolveContext } from "../lib/context.js";
|
|
4
|
+
import { syncFromLinked } from "../lib/template-file.js";
|
|
4
5
|
const CELL = {
|
|
5
6
|
set: "set",
|
|
6
7
|
expiring_soon: "expiring",
|
|
@@ -17,6 +18,7 @@ function paint(status, text) {
|
|
|
17
18
|
/** Key names by context with status. Never values. */
|
|
18
19
|
export async function lsCommand(opts) {
|
|
19
20
|
const { who, meta } = await ensureTokenAndLink({ apiUrl: opts.apiUrl, quiet: true });
|
|
21
|
+
syncFromLinked({ who });
|
|
20
22
|
const contexts = opts.context ? [resolveContext(who, meta, opts.context)] : who.contexts;
|
|
21
23
|
if (who.keys.length === 0) {
|
|
22
24
|
console.log(pc.dim("No variables yet. Add one: npx getmyenv set API_KEY"));
|
package/dist/commands/restore.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
|
5
5
|
import { findContext } from "../lib/context.js";
|
|
6
6
|
import { promptConfirm } from "../lib/fs.js";
|
|
7
7
|
import { resolveVaultPassword } from "../lib/guest-key.js";
|
|
8
|
+
import { syncFromLinked } from "../lib/template-file.js";
|
|
8
9
|
import { writeKey } from "../lib/vault.js";
|
|
9
10
|
/**
|
|
10
11
|
* Restore a backup into this folder's project. The backup opens locally with
|
|
@@ -100,6 +101,7 @@ export async function restoreCommand(file, opts) {
|
|
|
100
101
|
values,
|
|
101
102
|
keyVersions,
|
|
102
103
|
});
|
|
104
|
+
syncFromLinked(linked, { added: [...envelope.keys, ...values.map((v) => v.key)] });
|
|
103
105
|
const skipped = [...perContext.keys()].filter((slug) => !writable(slug));
|
|
104
106
|
console.log(`${pc.green("✓")} Restored ${res.restored} values into ${res.projectName}`);
|
|
105
107
|
if (skipped.length > 0) {
|
package/dist/commands/set.js
CHANGED
|
@@ -4,6 +4,7 @@ import { promptHidden } from "../lib/fs.js";
|
|
|
4
4
|
import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
5
5
|
import { resolveContext } from "../lib/context.js";
|
|
6
6
|
import { printDidYouMean } from "../lib/known-keys.js";
|
|
7
|
+
import { syncFromLinked } from "../lib/template-file.js";
|
|
7
8
|
import { assertWritable, uploadValues } from "../lib/upload.js";
|
|
8
9
|
async function readStdinValue() {
|
|
9
10
|
const chunks = [];
|
|
@@ -75,6 +76,7 @@ export async function setCommand(args, opts) {
|
|
|
75
76
|
for (const name of res.declared)
|
|
76
77
|
console.log(`${pc.yellow("○")} ${name} ${pc.dim(`declared, no value in ${ctx.slug}`)}`);
|
|
77
78
|
printDidYouMean(pairs.map((p) => p.name).filter((n) => !linked.who.keys.some((k) => k.name === n)));
|
|
79
|
+
syncFromLinked(linked, { added: [...res.set, ...res.declared] });
|
|
78
80
|
}
|
|
79
81
|
export async function unsetCommand(names, opts) {
|
|
80
82
|
for (const n of names) {
|
|
@@ -95,4 +97,5 @@ export async function unsetCommand(names, opts) {
|
|
|
95
97
|
const skipped = names.filter((n) => !res.removed.includes(n));
|
|
96
98
|
if (skipped.length > 0)
|
|
97
99
|
console.log(pc.dim(`No change: ${skipped.join(", ")}`));
|
|
100
|
+
syncFromLinked(linked, opts.all ? { removed: res.removed } : {});
|
|
98
101
|
}
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type PublicTemplate } from "@getmyenv/shared";
|
|
1
|
+
import { type PublicTemplate, type TemplateFile } from "@getmyenv/shared";
|
|
2
|
+
import { type ProjectMeta } from "../lib/fs.js";
|
|
2
3
|
/** Best guess at the app's start command, for the printed `run` line. */
|
|
3
4
|
export declare function suggestedStartCommand(cwd?: string): string;
|
|
4
5
|
export declare function runLine(context: string | null, cwd?: string, startCommand?: string | null): string;
|
|
@@ -11,6 +12,8 @@ export declare function startSetup(opts: {
|
|
|
11
12
|
thenContinue?: boolean;
|
|
12
13
|
template?: PublicTemplate;
|
|
13
14
|
}): Promise<void>;
|
|
15
|
+
/** The two ways to start when project.json and template.json both exist. Sign in comes first. */
|
|
16
|
+
export declare function startChoices(existing: ProjectMeta, file: TemplateFile): [string, string];
|
|
14
17
|
/**
|
|
15
18
|
* Bare `getmyenv`: setup when unlinked (terminal only), status when linked.
|
|
16
19
|
* With --template, add the template's names and ask for their values.
|
package/dist/commands/start.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
-
import { DEFAULT_CONTEXT_SLUG, GUEST_TOTAL_DAYS } from "@getmyenv/shared";
|
|
4
|
+
import { DEFAULT_CONTEXT_SLUG, FACTS, GUEST_TOTAL_DAYS, TEMPLATE_FILE, } from "@getmyenv/shared";
|
|
5
5
|
import { promptChoice, promptConfirm, readClaimFile, readLine, readProjectMeta, rootEnvFiles, writeProjectMeta, } from "../lib/fs.js";
|
|
6
6
|
import { ApiClient, getSessionToken, resolveApiUrl } from "../lib/api.js";
|
|
7
7
|
import { authorizeFolder } from "../lib/authorize.js";
|
|
8
8
|
import { NOT_LINKED_STEPS } from "../lib/bootstrap.js";
|
|
9
|
+
import { readTemplateFile } from "../lib/template-file.js";
|
|
9
10
|
import { createGuestProject, folderNameHint } from "./guest.js";
|
|
10
11
|
import { statusCommand } from "./status.js";
|
|
11
|
-
import { applyTemplate, fetchTemplate, printTemplate } from "./template.js";
|
|
12
|
+
import { applyTemplate, applyTemplateFile, fetchTemplate, printTemplate } from "./template.js";
|
|
12
13
|
/** Best guess at the app's start command, for the printed `run` line. */
|
|
13
14
|
export function suggestedStartCommand(cwd = process.cwd()) {
|
|
14
15
|
const pkgFile = path.join(cwd, "package.json");
|
|
@@ -70,6 +71,20 @@ export async function startSetup(opts) {
|
|
|
70
71
|
if (readClaimFile()) {
|
|
71
72
|
throw new Error("This folder has a guest project (claim.json) but no token on this machine. Claim it: npx getmyenv claim");
|
|
72
73
|
}
|
|
74
|
+
const file = opts.template ? null : loadTemplateFile();
|
|
75
|
+
if (file && existing?.projectId) {
|
|
76
|
+
const choices = startChoices(existing, file);
|
|
77
|
+
const pick = await promptChoice("How do you want to start?", choices, 0);
|
|
78
|
+
console.error("");
|
|
79
|
+
if (pick === 1) {
|
|
80
|
+
await startFromTemplateFile(file, { apiUrl, previous: existing, thenContinue: opts.thenContinue });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (file) {
|
|
85
|
+
await startFromTemplateFile(file, { apiUrl, thenContinue: opts.thenContinue });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
73
88
|
if (existing?.projectId) {
|
|
74
89
|
console.error(`This folder uses ${pc.bold(existing.projectName ?? existing.projectId)}. Sign in to get a token.\n`);
|
|
75
90
|
await authorizeFolder({ apiUrl });
|
|
@@ -104,18 +119,7 @@ export async function startSetup(opts) {
|
|
|
104
119
|
printRunLine(applied.context, opts.template.startCommand);
|
|
105
120
|
return;
|
|
106
121
|
}
|
|
107
|
-
|
|
108
|
-
if (files.length > 0) {
|
|
109
|
-
const rel = files.map((f) => path.basename(f)).join(", ");
|
|
110
|
-
const ok = await promptConfirm(`Import ${rel}? Values are encrypted before upload.`, {
|
|
111
|
-
defaultYes: true,
|
|
112
|
-
});
|
|
113
|
-
if (ok) {
|
|
114
|
-
const { importCommand } = await import("./import.js");
|
|
115
|
-
await importCommand(undefined, { apiUrl, yes: true, noRunLine: true });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
122
|
+
if (!(await offerEnvImport(apiUrl))) {
|
|
119
123
|
console.error("No .env file in this folder. Add values with:\n");
|
|
120
124
|
console.error(pc.cyan(" npx getmyenv set API_KEY"));
|
|
121
125
|
if (dashboardUrl)
|
|
@@ -125,6 +129,83 @@ export async function startSetup(opts) {
|
|
|
125
129
|
if (!opts.thenContinue)
|
|
126
130
|
printRunLine(context);
|
|
127
131
|
}
|
|
132
|
+
/** Offer to import the .env files in this folder. False when there are none. */
|
|
133
|
+
async function offerEnvImport(apiUrl) {
|
|
134
|
+
const files = rootEnvFiles();
|
|
135
|
+
if (files.length === 0)
|
|
136
|
+
return false;
|
|
137
|
+
const rel = files.map((f) => path.basename(f)).join(", ");
|
|
138
|
+
const ok = await promptConfirm(`Import ${rel}? Values are encrypted before upload.`, {
|
|
139
|
+
defaultYes: true,
|
|
140
|
+
});
|
|
141
|
+
if (ok) {
|
|
142
|
+
const { importCommand } = await import("./import.js");
|
|
143
|
+
await importCommand(undefined, { apiUrl, yes: true, noRunLine: true });
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
/** A template file that cannot be read is reported and not used. */
|
|
148
|
+
function loadTemplateFile() {
|
|
149
|
+
const read = readTemplateFile();
|
|
150
|
+
if (!read)
|
|
151
|
+
return null;
|
|
152
|
+
if (!read.ok) {
|
|
153
|
+
console.error(pc.yellow(`${TEMPLATE_FILE} is not valid (${read.reason}). Setup continues without it.\n`));
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return read.file;
|
|
157
|
+
}
|
|
158
|
+
/** The two ways to start when project.json and template.json both exist. Sign in comes first. */
|
|
159
|
+
export function startChoices(existing, file) {
|
|
160
|
+
const n = file.keys.length;
|
|
161
|
+
return [
|
|
162
|
+
`Sign in to ${existing.projectName ?? existing.projectId}`,
|
|
163
|
+
`New project from ${TEMPLATE_FILE} (${n} variable${n === 1 ? "" : "s"}). ${FACTS.templateFileNewProject}`,
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A new project with the names in the template file: guest or sign in, then
|
|
168
|
+
* the .env import offer, then the names are declared and each missing value
|
|
169
|
+
* is asked. project.json changes only once the new project exists.
|
|
170
|
+
*/
|
|
171
|
+
async function startFromTemplateFile(file, opts) {
|
|
172
|
+
const n = file.keys.length;
|
|
173
|
+
console.error(`${TEMPLATE_FILE} lists ${n} variable${n === 1 ? "" : "s"}. Values are asked after setup.\n`);
|
|
174
|
+
const choice = await promptChoice("How do you want to start?", [`Start without an account (guest project, claim within ${GUEST_TOTAL_DAYS} days)`, "Sign in"], 0);
|
|
175
|
+
console.error("");
|
|
176
|
+
const suggested = file.name ?? folderNameHint();
|
|
177
|
+
let project;
|
|
178
|
+
let fresh;
|
|
179
|
+
if (choice === 0) {
|
|
180
|
+
const name = (await readLine(`Project name ${pc.dim(`[${suggested}]`)}: `)).trim() || suggested;
|
|
181
|
+
const created = await createGuestProject({
|
|
182
|
+
name,
|
|
183
|
+
apiUrl: opts.apiUrl,
|
|
184
|
+
replaceProjectMeta: Boolean(opts.previous?.projectId),
|
|
185
|
+
});
|
|
186
|
+
project = { id: created.projectId, name: created.projectName };
|
|
187
|
+
fresh = true;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const who = await authorizeFolder({ apiUrl: opts.apiUrl, newProject: { name: suggested } });
|
|
191
|
+
const meta = readProjectMeta();
|
|
192
|
+
if (meta)
|
|
193
|
+
writeProjectMeta({ ...meta, context: defaultContextFor(who) });
|
|
194
|
+
project = who.project;
|
|
195
|
+
fresh = !who.token.member && who.keys.length === 0;
|
|
196
|
+
}
|
|
197
|
+
const before = opts.previous;
|
|
198
|
+
if (before?.projectId && before.projectId !== project.id) {
|
|
199
|
+
console.error(pc.dim(`project.json now points to ${project.name}. It pointed to ${before.projectName ?? before.projectId}.`));
|
|
200
|
+
}
|
|
201
|
+
if (fresh) {
|
|
202
|
+
console.error(`New project ${pc.bold(project.name)} with the names from ${TEMPLATE_FILE}. ${FACTS.templateFileNewProject}\n`);
|
|
203
|
+
}
|
|
204
|
+
await offerEnvImport(opts.apiUrl);
|
|
205
|
+
const applied = await applyTemplateFile(file, { apiUrl: opts.apiUrl });
|
|
206
|
+
if (!opts.thenContinue)
|
|
207
|
+
printRunLine(applied.context, file.startCommand);
|
|
208
|
+
}
|
|
128
209
|
function printRunLine(context, startCommand) {
|
|
129
210
|
console.error("Start your app with:\n");
|
|
130
211
|
console.error(pc.cyan(` ${runLine(context, process.cwd(), startCommand)}\n`));
|
package/dist/commands/status.js
CHANGED
|
@@ -3,6 +3,7 @@ import { NEW_MACHINE_POLICY_LABELS } from "@getmyenv/shared";
|
|
|
3
3
|
import { isDefaultApiUrl } from "../lib/api.js";
|
|
4
4
|
import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
5
5
|
import { defaultContextSlug } from "../lib/context.js";
|
|
6
|
+
import { syncFromLinked } from "../lib/template-file.js";
|
|
6
7
|
import { runLine } from "./start.js";
|
|
7
8
|
const TOKEN_KIND = {
|
|
8
9
|
folder: "folder (open contexts, read and write)",
|
|
@@ -12,6 +13,7 @@ const TOKEN_KIND = {
|
|
|
12
13
|
/** Project, token, contexts with value counts, and the run command. Names only. */
|
|
13
14
|
export async function statusCommand(opts = {}) {
|
|
14
15
|
const { who, apiUrl, meta } = await ensureTokenAndLink({ apiUrl: opts.apiUrl });
|
|
16
|
+
syncFromLinked({ who });
|
|
15
17
|
const current = defaultContextSlug(who, meta);
|
|
16
18
|
console.log(`${pc.bold(who.project.name)}${who.user ? pc.dim(` ${who.user.email}`) : ""}`);
|
|
17
19
|
console.log(pc.dim(who.token.member
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { type PublicTemplate } from "@getmyenv/shared";
|
|
1
|
+
import { type PublicTemplate, type TemplateFile, type TemplateKey } from "@getmyenv/shared";
|
|
2
|
+
import { type ContextInfo } from "../lib/api.js";
|
|
3
|
+
import { type Linked } from "../lib/bootstrap.js";
|
|
2
4
|
/** Load a public template by id or URL. No token needed. */
|
|
3
5
|
export declare function fetchTemplate(ref: string, apiUrl?: string): Promise<PublicTemplate>;
|
|
4
6
|
export declare function printTemplate(template: PublicTemplate): void;
|
|
@@ -11,3 +13,19 @@ export declare function applyTemplate(template: PublicTemplate, opts: {
|
|
|
11
13
|
}): Promise<{
|
|
12
14
|
context: string;
|
|
13
15
|
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Declare the template file's names in the linked project, then ask for each
|
|
18
|
+
* value that is still missing. Without a terminal, list them instead.
|
|
19
|
+
* Values already set, for example from a .env import, are not touched.
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyTemplateFile(file: TemplateFile, opts: {
|
|
22
|
+
apiUrl?: string;
|
|
23
|
+
ask?: boolean;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
context: string;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Ask for each value still missing in ctx, hidden. Enter skips one and sends
|
|
29
|
+
* nothing. `linked` must be fresh, so values set a moment ago are not asked.
|
|
30
|
+
*/
|
|
31
|
+
export declare function askMissingValues(keys: TemplateKey[], linked: Linked, ctx: ContextInfo): Promise<void>;
|
|
@@ -4,6 +4,7 @@ import { promptHidden } from "../lib/fs.js";
|
|
|
4
4
|
import { ApiClient, getSessionToken, resolveApiUrl } from "../lib/api.js";
|
|
5
5
|
import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
6
6
|
import { resolveContext } from "../lib/context.js";
|
|
7
|
+
import { syncFromLinked } from "../lib/template-file.js";
|
|
7
8
|
import { assertWritable, uploadValues } from "../lib/upload.js";
|
|
8
9
|
/** Load a public template by id or URL. No token needed. */
|
|
9
10
|
export async function fetchTemplate(ref, apiUrl) {
|
|
@@ -35,11 +36,48 @@ export async function applyTemplate(template, opts) {
|
|
|
35
36
|
const linked = await ensureTokenAndLink({ apiUrl, quiet: true });
|
|
36
37
|
const ctx = resolveContext(linked.who, linked.meta, undefined);
|
|
37
38
|
assertWritable(linked, ctx);
|
|
39
|
+
syncFromLinked(linked, { added: template.keys.map((k) => k.name) });
|
|
40
|
+
await askMissingValues(template.keys, linked, ctx);
|
|
41
|
+
return { context: ctx.slug };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Declare the template file's names in the linked project, then ask for each
|
|
45
|
+
* value that is still missing. Without a terminal, list them instead.
|
|
46
|
+
* Values already set, for example from a .env import, are not touched.
|
|
47
|
+
*/
|
|
48
|
+
export async function applyTemplateFile(file, opts) {
|
|
49
|
+
const linked = await ensureTokenAndLink({ apiUrl: opts.apiUrl, quiet: true });
|
|
50
|
+
const ctx = resolveContext(linked.who, linked.meta, undefined);
|
|
51
|
+
assertWritable(linked, ctx);
|
|
52
|
+
const names = file.keys.map((k) => k.name);
|
|
53
|
+
if (names.length > 0) {
|
|
54
|
+
await linked.client.request("POST", "/api/cli/values", { context: ctx.slug, values: [], declare: names });
|
|
55
|
+
}
|
|
56
|
+
syncFromLinked(linked, { added: names });
|
|
57
|
+
if (opts.ask ?? process.stdin.isTTY === true) {
|
|
58
|
+
await askMissingValues(file.keys, linked, ctx);
|
|
59
|
+
return { context: ctx.slug };
|
|
60
|
+
}
|
|
61
|
+
const todo = missingIn(file.keys, linked, ctx);
|
|
62
|
+
if (todo.length > 0) {
|
|
63
|
+
console.error(`No value yet in ${ctx.slug}: ${todo.map((k) => k.name).join(", ")}`);
|
|
64
|
+
console.error("Set each one: npx getmyenv set NAME\n");
|
|
65
|
+
}
|
|
66
|
+
return { context: ctx.slug };
|
|
67
|
+
}
|
|
68
|
+
function missingIn(keys, linked, ctx) {
|
|
38
69
|
const status = new Map(linked.who.keys.map((k) => [k.name, k.status[ctx.slug] ?? "missing"]));
|
|
39
|
-
|
|
70
|
+
return keys.filter((k) => (status.get(k.name) ?? "missing") === "missing");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Ask for each value still missing in ctx, hidden. Enter skips one and sends
|
|
74
|
+
* nothing. `linked` must be fresh, so values set a moment ago are not asked.
|
|
75
|
+
*/
|
|
76
|
+
export async function askMissingValues(keys, linked, ctx) {
|
|
77
|
+
const todo = missingIn(keys, linked, ctx);
|
|
40
78
|
if (todo.length === 0) {
|
|
41
79
|
console.error(`${pc.green("✓")} Every template variable has a value in ${ctx.slug}.`);
|
|
42
|
-
return
|
|
80
|
+
return;
|
|
43
81
|
}
|
|
44
82
|
console.error(`Values for ${pc.bold(ctx.slug)}. Hidden as you type. Enter skips one.\n`);
|
|
45
83
|
const pairs = {};
|
|
@@ -71,5 +109,4 @@ export async function applyTemplate(template, opts) {
|
|
|
71
109
|
console.error(pc.dim(`No value yet: ${skipped.join(", ")}. Set later with npx getmyenv set NAME`));
|
|
72
110
|
}
|
|
73
111
|
console.error("");
|
|
74
|
-
return { context: ctx.slug };
|
|
75
112
|
}
|
package/dist/lib/authorize.d.ts
CHANGED
|
@@ -6,10 +6,20 @@ export declare function browserCommand(raw: string, platform?: NodeJS.Platform):
|
|
|
6
6
|
} | null;
|
|
7
7
|
export declare function openBrowser(url: string): void;
|
|
8
8
|
export declare function sleep(ms: number): Promise<void>;
|
|
9
|
+
/** The sign-in page preselects the folder's project, or "New project" with a name. */
|
|
10
|
+
export declare function verifyUrlFor(base: string, pick: {
|
|
11
|
+
projectId?: string;
|
|
12
|
+
newProject?: {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
}): string;
|
|
9
16
|
/**
|
|
10
17
|
* Sign in via the browser and link this folder (writes token.json + project.json).
|
|
11
18
|
* CI uses GETMYENV_TOKEN instead.
|
|
12
19
|
*/
|
|
13
20
|
export declare function authorizeFolder(opts?: {
|
|
14
21
|
apiUrl?: string;
|
|
22
|
+
newProject?: {
|
|
23
|
+
name: string;
|
|
24
|
+
};
|
|
15
25
|
}): Promise<WhoamiResponse>;
|
package/dist/lib/authorize.js
CHANGED
|
@@ -68,7 +68,18 @@ async function authorizeViaPaste(apiUrl) {
|
|
|
68
68
|
}
|
|
69
69
|
return saveVerifiedToken(token, apiUrl);
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
/** The sign-in page preselects the folder's project, or "New project" with a name. */
|
|
72
|
+
export function verifyUrlFor(base, pick) {
|
|
73
|
+
const params = pick.newProject
|
|
74
|
+
? `project=new&name=${encodeURIComponent(pick.newProject.name)}`
|
|
75
|
+
: pick.projectId
|
|
76
|
+
? `project=${encodeURIComponent(pick.projectId)}`
|
|
77
|
+
: "";
|
|
78
|
+
if (!params)
|
|
79
|
+
return base;
|
|
80
|
+
return `${base}${base.includes("?") ? "&" : "?"}${params}`;
|
|
81
|
+
}
|
|
82
|
+
async function authorizeViaBrowser(apiUrl, newProject) {
|
|
72
83
|
const client = new ApiClient(apiUrl);
|
|
73
84
|
let start;
|
|
74
85
|
try {
|
|
@@ -80,10 +91,10 @@ async function authorizeViaBrowser(apiUrl) {
|
|
|
80
91
|
console.error(pc.dim("Falling back to paste.\n"));
|
|
81
92
|
return authorizeViaPaste(apiUrl);
|
|
82
93
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
const verifyUrl = verifyUrlFor(start.verifyUrl, {
|
|
95
|
+
projectId: readProjectMeta()?.projectId,
|
|
96
|
+
newProject,
|
|
97
|
+
});
|
|
87
98
|
console.error("Open this URL to sign in and pick a project:\n");
|
|
88
99
|
console.error(` ${verifyUrl}\n`);
|
|
89
100
|
console.error(`Check the page shows this code: ${pc.bold(start.userCode)}\n`);
|
|
@@ -124,5 +135,5 @@ export async function authorizeFolder(opts = {}) {
|
|
|
124
135
|
if (!process.stdin.isTTY) {
|
|
125
136
|
throw new Error("No token in this folder. Run in a terminal to sign in, or set GETMYENV_TOKEN to a server token.");
|
|
126
137
|
}
|
|
127
|
-
return authorizeViaBrowser(resolveApiUrl(opts.apiUrl));
|
|
138
|
+
return authorizeViaBrowser(resolveApiUrl(opts.apiUrl), opts.newProject);
|
|
128
139
|
}
|
package/dist/lib/fs.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `.getmyenv/project.json`, committed so a clone knows its project.
|
|
3
3
|
* Holds no API URL: a committed file must not redirect where tokens go.
|
|
4
|
+
* `.getmyenv/template.json` is committed too: names only, see template-file.ts.
|
|
4
5
|
*/
|
|
5
6
|
export type ProjectMeta = {
|
|
6
7
|
projectId?: string;
|
package/dist/lib/fs.js
CHANGED
|
@@ -14,7 +14,8 @@ export function getProjectRoot(cwd = process.cwd()) {
|
|
|
14
14
|
for (;;) {
|
|
15
15
|
const local = path.join(dir, LOCAL_DIR);
|
|
16
16
|
if (fs.existsSync(path.join(local, "project.json")) ||
|
|
17
|
-
fs.existsSync(path.join(local, "token.json"))
|
|
17
|
+
fs.existsSync(path.join(local, "token.json")) ||
|
|
18
|
+
fs.existsSync(path.join(local, "template.json"))) {
|
|
18
19
|
return dir;
|
|
19
20
|
}
|
|
20
21
|
const parent = path.dirname(dir);
|
|
@@ -220,8 +221,9 @@ export async function promptChoice(question, options, defaultIndex = 0) {
|
|
|
220
221
|
}
|
|
221
222
|
export function ensureGitignoreHasEnv(cwd = process.cwd()) {
|
|
222
223
|
const gi = path.join(getProjectRoot(cwd), ".gitignore");
|
|
223
|
-
// project.json
|
|
224
|
-
// guest passphrase in claim.json stay on
|
|
224
|
+
// project.json and template.json are committed so a clone knows its project
|
|
225
|
+
// and its names. The token and the guest passphrase in claim.json stay on
|
|
226
|
+
// this machine.
|
|
225
227
|
const entries = [
|
|
226
228
|
".env",
|
|
227
229
|
".env.local",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type TemplateFile } from "@getmyenv/shared";
|
|
2
|
+
import type { Linked } from "./bootstrap.js";
|
|
3
|
+
/**
|
|
4
|
+
* `.getmyenv/template.json`, committed: the project's variable names, never
|
|
5
|
+
* values. The CLI keeps it up to date. A fresh clone can start a new project
|
|
6
|
+
* from it.
|
|
7
|
+
*/
|
|
8
|
+
export type TemplateFileRead = {
|
|
9
|
+
ok: true;
|
|
10
|
+
file: TemplateFile;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function templateFilePath(cwd?: string): string;
|
|
16
|
+
/** Null when there is no file. A file that cannot be read or parsed says why. */
|
|
17
|
+
export declare function readTemplateFile(cwd?: string): TemplateFileRead | null;
|
|
18
|
+
export declare function serializeTemplateFile(file: TemplateFile): string;
|
|
19
|
+
/**
|
|
20
|
+
* Bring the file in line with the project's names. Writes only when the names
|
|
21
|
+
* change. Never overwrites a file it cannot read, so hand edits are not lost,
|
|
22
|
+
* and says so every time it skips.
|
|
23
|
+
*/
|
|
24
|
+
export declare function syncTemplateFile(names: readonly string[], projectName: string, cwd?: string): "written" | "unchanged" | "skipped";
|
|
25
|
+
/** CI and servers must not change the working tree. */
|
|
26
|
+
export declare function mayWriteTemplateFile(linked: Pick<Linked, "who">): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Sync from the key list a command already has, plus what it just added or
|
|
29
|
+
* removed. No extra request. A failure here never fails the command.
|
|
30
|
+
*/
|
|
31
|
+
export declare function syncFromLinked(linked: Pick<Linked, "who">, change?: {
|
|
32
|
+
added?: readonly string[];
|
|
33
|
+
removed?: readonly string[];
|
|
34
|
+
}): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { mergeTemplateFile, templateFileSchema, TEMPLATE_FILE, TEMPLATE_FILE_MAX_KEYS, } from "@getmyenv/shared";
|
|
5
|
+
import { getLocalDir } from "./fs.js";
|
|
6
|
+
export function templateFilePath(cwd = process.cwd()) {
|
|
7
|
+
return path.join(getLocalDir(cwd), "template.json");
|
|
8
|
+
}
|
|
9
|
+
/** Null when there is no file. A file that cannot be read or parsed says why. */
|
|
10
|
+
export function readTemplateFile(cwd = process.cwd()) {
|
|
11
|
+
const file = templateFilePath(cwd);
|
|
12
|
+
let stat;
|
|
13
|
+
try {
|
|
14
|
+
stat = fs.lstatSync(file);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (!stat.isFile())
|
|
20
|
+
return { ok: false, reason: "not a regular file" };
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { ok: false, reason: "not JSON" };
|
|
27
|
+
}
|
|
28
|
+
const parsed = templateFileSchema.safeParse(raw);
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
const issue = parsed.error.issues[0];
|
|
31
|
+
const where = issue?.path.length ? `${issue.path.join(".")}: ` : "";
|
|
32
|
+
return { ok: false, reason: `${where}${issue?.message ?? "invalid"}` };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, file: parsed.data };
|
|
35
|
+
}
|
|
36
|
+
export function serializeTemplateFile(file) {
|
|
37
|
+
return `${JSON.stringify(file, null, 2)}\n`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Bring the file in line with the project's names. Writes only when the names
|
|
41
|
+
* change. Never overwrites a file it cannot read, so hand edits are not lost,
|
|
42
|
+
* and says so every time it skips.
|
|
43
|
+
*/
|
|
44
|
+
export function syncTemplateFile(names, projectName, cwd = process.cwd()) {
|
|
45
|
+
const unique = [...new Set(names)];
|
|
46
|
+
if (unique.length > TEMPLATE_FILE_MAX_KEYS) {
|
|
47
|
+
console.error(pc.yellow(`${TEMPLATE_FILE} not updated. The project has ${unique.length} names, the file holds ${TEMPLATE_FILE_MAX_KEYS}. It may not match the project.`));
|
|
48
|
+
return "skipped";
|
|
49
|
+
}
|
|
50
|
+
const current = readTemplateFile(cwd);
|
|
51
|
+
if (current && !current.ok) {
|
|
52
|
+
console.error(pc.yellow(`${TEMPLATE_FILE} is not valid (${current.reason}). Not updated.`));
|
|
53
|
+
return "skipped";
|
|
54
|
+
}
|
|
55
|
+
if (!current && unique.length === 0)
|
|
56
|
+
return "unchanged";
|
|
57
|
+
const merged = mergeTemplateFile(current?.file ?? null, unique, projectName);
|
|
58
|
+
if (current && serializeTemplateFile(current.file) === serializeTemplateFile(merged))
|
|
59
|
+
return "unchanged";
|
|
60
|
+
const file = templateFilePath(cwd);
|
|
61
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
62
|
+
fs.writeFileSync(file, serializeTemplateFile(merged));
|
|
63
|
+
return "written";
|
|
64
|
+
}
|
|
65
|
+
/** CI and servers must not change the working tree. */
|
|
66
|
+
export function mayWriteTemplateFile(linked) {
|
|
67
|
+
return linked.who.token.kind !== "server" && !process.env.GETMYENV_TOKEN?.trim();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Sync from the key list a command already has, plus what it just added or
|
|
71
|
+
* removed. No extra request. A failure here never fails the command.
|
|
72
|
+
*/
|
|
73
|
+
export function syncFromLinked(linked, change = {}) {
|
|
74
|
+
if (!mayWriteTemplateFile(linked))
|
|
75
|
+
return;
|
|
76
|
+
const removed = new Set(change.removed ?? []);
|
|
77
|
+
const names = [...linked.who.keys.map((k) => k.name), ...(change.added ?? [])].filter((n) => !removed.has(n));
|
|
78
|
+
try {
|
|
79
|
+
syncTemplateFile(names, linked.who.project.name);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
83
|
+
console.error(pc.yellow(`${TEMPLATE_FILE} not updated: ${msg}`));
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getmyenv",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "CLI for getmyenv, an encrypted store for environment variables and secrets. We store ciphertext only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@getmyenv/crypto": "0.5.0",
|
|
37
|
-
"@getmyenv/shared": "0.
|
|
37
|
+
"@getmyenv/shared": "0.10.1",
|
|
38
38
|
"@napi-rs/keyring": "^2.1.0",
|
|
39
39
|
"commander": "^15.0.0",
|
|
40
40
|
"dotenv": "^16.4.7",
|