getmyenv 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -12
- package/dist/bin.js +8 -0
- package/dist/cli.js +11 -3
- package/dist/commands/backup.js +1 -1
- package/dist/commands/claim.js +3 -2
- package/dist/commands/context.js +4 -4
- package/dist/commands/export.js +17 -11
- package/dist/commands/guest.d.ts +1 -1
- package/dist/commands/guest.js +17 -6
- package/dist/commands/import.js +38 -9
- package/dist/commands/lock.js +1 -1
- package/dist/commands/ls.js +2 -2
- package/dist/commands/push.js +6 -5
- package/dist/commands/restore.js +3 -3
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +61 -12
- package/dist/commands/set.d.ts +16 -0
- package/dist/commands/set.js +91 -12
- package/dist/commands/start.js +7 -7
- package/dist/commands/status.js +3 -3
- package/dist/commands/template.js +3 -3
- package/dist/commands/unlink.d.ts +14 -5
- package/dist/commands/unlink.js +79 -17
- package/dist/lib/api-url.d.ts +2 -0
- package/dist/lib/api-url.js +10 -0
- package/dist/lib/api.d.ts +22 -2
- package/dist/lib/api.js +108 -34
- package/dist/lib/authorize.js +2 -2
- package/dist/lib/bootstrap.d.ts +5 -0
- package/dist/lib/bootstrap.js +32 -8
- package/dist/lib/colors.d.ts +14 -0
- package/dist/lib/colors.js +17 -0
- package/dist/lib/confirm.d.ts +7 -3
- package/dist/lib/confirm.js +21 -12
- package/dist/lib/context.js +1 -1
- package/dist/lib/credentials.d.ts +4 -0
- package/dist/lib/credentials.js +13 -11
- package/dist/lib/fs.d.ts +36 -1
- package/dist/lib/fs.js +102 -23
- package/dist/lib/known-keys-cache.d.ts +3 -2
- package/dist/lib/known-keys-cache.js +16 -4
- package/dist/lib/known-keys.js +1 -1
- package/dist/lib/recovery-code.js +1 -1
- package/dist/lib/resolve.js +1 -1
- package/dist/lib/run-env.d.ts +19 -3
- package/dist/lib/run-env.js +66 -13
- package/dist/lib/template-file.js +1 -1
- package/dist/lib/update-check.js +1 -1
- package/dist/lib/upload.js +2 -2
- package/dist/lib/vault.js +1 -1
- package/dist/providers/http.js +15 -12
- package/dist/providers/netlify.js +3 -1
- package/dist/providers/vercel.js +3 -1
- package/package.json +19 -12
package/dist/lib/api.js
CHANGED
|
@@ -2,25 +2,79 @@
|
|
|
2
2
|
* CLI API client. Never prints plaintext values.
|
|
3
3
|
*/
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { readFolderToken } from "./credentials.js";
|
|
5
|
+
import { readFolderToken, readFolderTokenLenient } from "./credentials.js";
|
|
6
6
|
import { pickApiUrl } from "./api-url.js";
|
|
7
7
|
/** From package.json, which ships with the package: src/lib and dist/lib are both two levels down. */
|
|
8
8
|
export const CLI_VERSION = createRequire(import.meta.url)("../../package.json").version;
|
|
9
9
|
export { isDefaultApiUrl, parseSafeUrl } from "./api-url.js";
|
|
10
|
-
|
|
10
|
+
/** Short on purpose: a stuck process keeps npx's cache lock, so a second `npx getmyenv` waits silently. */
|
|
11
|
+
export const API_TIMEOUT_MS = 8_000;
|
|
12
|
+
/** A request still running after this prints one "Contacting" line on stderr. */
|
|
13
|
+
export const CONTACTING_AFTER_MS = 1_000;
|
|
14
|
+
let contactingPrinted = false;
|
|
15
|
+
/** Error thrown when a request runs past `API_TIMEOUT_MS`, headers and body together. */
|
|
16
|
+
export class RequestTimeoutError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("timed out");
|
|
19
|
+
this.name = "TimeoutError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs `work` with an abort signal and a hard deadline. The deadline rejects even
|
|
24
|
+
* if the socket ignores the abort, and covers reading the body, not only the
|
|
25
|
+
* headers. Timers are cleared when `work` settles, so nothing outlives the request.
|
|
26
|
+
*/
|
|
27
|
+
export async function withDeadline(work, opts = {}) {
|
|
28
|
+
const timeoutMs = opts.timeoutMs ?? API_TIMEOUT_MS;
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
let deadline;
|
|
31
|
+
let contacting;
|
|
32
|
+
const timedOut = new Promise((_, reject) => {
|
|
33
|
+
deadline = setTimeout(() => {
|
|
34
|
+
const err = new RequestTimeoutError();
|
|
35
|
+
controller.abort(err);
|
|
36
|
+
reject(err);
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
});
|
|
39
|
+
if (opts.contacting && !contactingPrinted) {
|
|
40
|
+
const origin = opts.contacting;
|
|
41
|
+
contacting = setTimeout(() => {
|
|
42
|
+
contactingPrinted = true;
|
|
43
|
+
process.stderr.write(`Contacting ${origin}...\n`);
|
|
44
|
+
}, CONTACTING_AFTER_MS);
|
|
45
|
+
contacting.unref?.();
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const running = work(controller.signal);
|
|
49
|
+
// After the deadline wins, the aborted work still rejects. Nobody awaits it then.
|
|
50
|
+
running.catch(() => { });
|
|
51
|
+
return await Promise.race([running, timedOut]);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
clearTimeout(deadline);
|
|
55
|
+
clearTimeout(contacting);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Test hook: lets the "Contacting" line print again. */
|
|
59
|
+
export function resetContactingForTests() {
|
|
60
|
+
contactingPrinted = false;
|
|
61
|
+
}
|
|
11
62
|
export class ApiError extends Error {
|
|
12
63
|
status;
|
|
13
|
-
|
|
64
|
+
/** Machine-readable reason from the server, like `token_ended`. */
|
|
65
|
+
code;
|
|
66
|
+
constructor(message, status, code) {
|
|
14
67
|
super(message);
|
|
15
68
|
this.name = "ApiError";
|
|
16
69
|
this.status = status;
|
|
70
|
+
this.code = code;
|
|
17
71
|
}
|
|
18
72
|
}
|
|
19
73
|
export function resolveApiUrl(cliOverride) {
|
|
20
74
|
return pickApiUrl({
|
|
21
75
|
override: cliOverride,
|
|
22
76
|
env: process.env.GETMYENV_API_URL,
|
|
23
|
-
tokenFile:
|
|
77
|
+
tokenFile: readFolderTokenLenient()?.apiUrl,
|
|
24
78
|
});
|
|
25
79
|
}
|
|
26
80
|
export class ApiClient {
|
|
@@ -42,47 +96,67 @@ export class ApiClient {
|
|
|
42
96
|
return h;
|
|
43
97
|
}
|
|
44
98
|
async request(method, path, body, extraHeaders) {
|
|
45
|
-
|
|
99
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}${path}`;
|
|
100
|
+
let origin = this.baseUrl;
|
|
46
101
|
try {
|
|
47
|
-
|
|
48
|
-
method,
|
|
49
|
-
headers: this.headers(extraHeaders),
|
|
50
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
51
|
-
// The token must never follow a redirect to another host.
|
|
52
|
-
redirect: "error",
|
|
53
|
-
signal: AbortSignal.timeout(API_TIMEOUT_MS),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
throw networkError(err, `Could not reach ${this.baseUrl}`);
|
|
102
|
+
origin = new URL(url).origin;
|
|
58
103
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const data = (await res.json());
|
|
63
|
-
serverMessage = data.error;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
/* ignore */
|
|
67
|
-
}
|
|
68
|
-
const message = res.status >= 500
|
|
69
|
-
? `The getmyenv server had an error (HTTP ${res.status}). Try again in a moment.`
|
|
70
|
-
: (serverMessage ?? `HTTP ${res.status}`);
|
|
71
|
-
throw new ApiError(message, res.status);
|
|
104
|
+
catch {
|
|
105
|
+
/* keep baseUrl */
|
|
72
106
|
}
|
|
73
|
-
if (res.status === 204)
|
|
74
|
-
return undefined;
|
|
75
107
|
try {
|
|
76
|
-
return
|
|
108
|
+
return await withDeadline(async (signal) => {
|
|
109
|
+
let res;
|
|
110
|
+
try {
|
|
111
|
+
res = await fetch(url, {
|
|
112
|
+
method,
|
|
113
|
+
headers: this.headers(extraHeaders),
|
|
114
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
115
|
+
// The token must never follow a redirect to another host.
|
|
116
|
+
redirect: "error",
|
|
117
|
+
signal,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
throw networkError(err, `Could not reach ${this.baseUrl}. Check your connection, or the API URL (GETMYENV_API_URL or --api-url).`);
|
|
122
|
+
}
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
let serverMessage;
|
|
125
|
+
let code;
|
|
126
|
+
try {
|
|
127
|
+
const data = (await res.json());
|
|
128
|
+
serverMessage = data.error;
|
|
129
|
+
if (typeof data.code === "string")
|
|
130
|
+
code = data.code;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* ignore */
|
|
134
|
+
}
|
|
135
|
+
const message = res.status >= 500
|
|
136
|
+
? `The getmyenv server had an error (HTTP ${res.status}). Try again in a moment.`
|
|
137
|
+
: (serverMessage ?? `HTTP ${res.status}`);
|
|
138
|
+
throw new ApiError(message, res.status, code);
|
|
139
|
+
}
|
|
140
|
+
if (res.status === 204)
|
|
141
|
+
return undefined;
|
|
142
|
+
try {
|
|
143
|
+
return (await res.json());
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
throw networkError(err, `The connection to ${this.baseUrl} closed before the answer arrived. Try again in a moment.`);
|
|
147
|
+
}
|
|
148
|
+
}, { contacting: origin });
|
|
77
149
|
}
|
|
78
150
|
catch (err) {
|
|
79
|
-
|
|
151
|
+
if (err instanceof RequestTimeoutError)
|
|
152
|
+
throw networkError(err, "");
|
|
153
|
+
throw err;
|
|
80
154
|
}
|
|
81
155
|
}
|
|
82
156
|
}
|
|
83
157
|
function networkError(err, fallback) {
|
|
84
158
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
85
|
-
return new ApiError(`The getmyenv server did not answer within ${API_TIMEOUT_MS / 1000} seconds. Try again in a moment.`, 0);
|
|
159
|
+
return new ApiError(`The getmyenv server did not answer within ${API_TIMEOUT_MS / 1000} seconds. Try again in a moment.`, 0, "timeout");
|
|
86
160
|
}
|
|
87
161
|
return new ApiError(fallback, 0);
|
|
88
162
|
}
|
package/dist/lib/authorize.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import pc from "
|
|
2
|
+
import { pcErr as pc } from "./colors.js";
|
|
3
3
|
import { ensureGitignoreHasEnv, readLine, readProjectMeta, writeProjectMeta, } from "./fs.js";
|
|
4
4
|
import { writeFolderToken } from "./credentials.js";
|
|
5
5
|
import { ApiClient, parseSafeUrl, resolveApiUrl } from "./api.js";
|
|
@@ -97,7 +97,7 @@ async function authorizeViaBrowser(apiUrl, newProject) {
|
|
|
97
97
|
});
|
|
98
98
|
console.error("Open this URL to sign in and pick a project:\n");
|
|
99
99
|
console.error(` ${verifyUrl}\n`);
|
|
100
|
-
console.error(`
|
|
100
|
+
console.error(`Enter this code on the page: ${pc.bold(start.userCode)}\n`);
|
|
101
101
|
openBrowser(verifyUrl);
|
|
102
102
|
console.error(pc.dim("Opened in your browser. Waiting...\n"));
|
|
103
103
|
const deadline = new Date(start.expiresAt).getTime();
|
package/dist/lib/bootstrap.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export type Linked = {
|
|
|
8
8
|
meta: ProjectMeta | null;
|
|
9
9
|
};
|
|
10
10
|
export declare const NOT_LINKED_STEPS: string;
|
|
11
|
+
/**
|
|
12
|
+
* Next steps for a folder with no token on this machine. A clone with a
|
|
13
|
+
* committed project.json gets its project named: it is not a new folder.
|
|
14
|
+
*/
|
|
15
|
+
export declare function notLinkedMessage(meta?: ProjectMeta | null): string;
|
|
11
16
|
export declare function printGuestBanner(who: WhoamiResponse): void;
|
|
12
17
|
export declare function fetchWhoami(client: ApiClient): Promise<WhoamiResponse>;
|
|
13
18
|
/**
|
package/dist/lib/bootstrap.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import pc from "
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { pcErr as pc } from "./colors.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { GUEST_ACTIVE_DAYS, GUEST_GRACE_MS, TEMPLATE_FILE, TOKEN_ENDED_CODE } from "@getmyenv/shared";
|
|
5
|
+
import { ensureGitignoreHasEnv, getLocalDir, readClaimFile, readProjectMeta, writeProjectMeta, } from "./fs.js";
|
|
4
6
|
import { ApiClient, ApiError, getSessionToken, resolveApiUrl, } from "./api.js";
|
|
5
7
|
import { assertTokenNotCommitted } from "./credentials.js";
|
|
6
8
|
export const NOT_LINKED_STEPS = [
|
|
@@ -9,6 +11,21 @@ export const NOT_LINKED_STEPS = [
|
|
|
9
11
|
"Without an account: npx getmyenv guest",
|
|
10
12
|
"In CI: set GETMYENV_TOKEN",
|
|
11
13
|
].join("\n");
|
|
14
|
+
/**
|
|
15
|
+
* Next steps for a folder with no token on this machine. A clone with a
|
|
16
|
+
* committed project.json gets its project named: it is not a new folder.
|
|
17
|
+
*/
|
|
18
|
+
export function notLinkedMessage(meta = readProjectMeta()) {
|
|
19
|
+
if (!meta?.projectId)
|
|
20
|
+
return NOT_LINKED_STEPS;
|
|
21
|
+
const hasTemplate = fs.existsSync(path.join(getLocalDir(), "template.json"));
|
|
22
|
+
return [
|
|
23
|
+
`This folder belongs to ${meta.projectName ?? meta.projectId}, but this machine has no token for it.`,
|
|
24
|
+
"In a terminal: npx getmyenv (sign in)",
|
|
25
|
+
...(hasTemplate ? [`Or start your own project from ${TEMPLATE_FILE}: npx getmyenv guest`] : []),
|
|
26
|
+
"In CI: set GETMYENV_TOKEN",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
12
29
|
export function printGuestBanner(who) {
|
|
13
30
|
if (!who.isGuest)
|
|
14
31
|
return;
|
|
@@ -22,8 +39,12 @@ export function printGuestBanner(who) {
|
|
|
22
39
|
return;
|
|
23
40
|
}
|
|
24
41
|
const daysLeft = Number.isFinite(expires) ? Math.max(0, Math.ceil((expires - Date.now()) / DAY)) : null;
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
// Each use moves the date to GUEST_ACTIVE_DAYS from now, so a countdown only means
|
|
43
|
+
// something once the cap after creation is closer than that.
|
|
44
|
+
const when = daysLeft != null && daysLeft < GUEST_ACTIVE_DAYS
|
|
45
|
+
? `Expires in ${plural(daysLeft)}.`
|
|
46
|
+
: `Expires after ${plural(GUEST_ACTIVE_DAYS)} without use.`;
|
|
47
|
+
console.error(pc.yellow(`Guest project. ${when} Keep it: npx getmyenv claim.${needClaim}\n`));
|
|
27
48
|
}
|
|
28
49
|
export async function fetchWhoami(client) {
|
|
29
50
|
try {
|
|
@@ -33,6 +54,9 @@ export async function fetchWhoami(client) {
|
|
|
33
54
|
if (err instanceof ApiError && err.status === 410) {
|
|
34
55
|
throw new Error(err.message);
|
|
35
56
|
}
|
|
57
|
+
if (err instanceof ApiError && err.status === 401 && err.code === TOKEN_ENDED_CODE) {
|
|
58
|
+
throw new Error(err.message);
|
|
59
|
+
}
|
|
36
60
|
if (err instanceof ApiError && err.status === 401) {
|
|
37
61
|
throw new Error(process.env.GETMYENV_TOKEN?.trim()
|
|
38
62
|
? "GETMYENV_TOKEN no longer works. It was revoked or expired. Create a new token in Project settings, Tokens."
|
|
@@ -49,14 +73,14 @@ export async function fetchWhoami(client) {
|
|
|
49
73
|
export async function ensureTokenAndLink(opts = {}) {
|
|
50
74
|
if (!getSessionToken()) {
|
|
51
75
|
if (!process.stdin.isTTY) {
|
|
52
|
-
throw new Error(
|
|
76
|
+
throw new Error(notLinkedMessage());
|
|
53
77
|
}
|
|
54
78
|
const { startSetup } = await import("../commands/start.js");
|
|
55
79
|
await startSetup({ apiUrl: opts.apiUrl, thenContinue: true });
|
|
56
80
|
}
|
|
57
81
|
const token = getSessionToken();
|
|
58
82
|
if (!token)
|
|
59
|
-
throw new Error(
|
|
83
|
+
throw new Error(notLinkedMessage());
|
|
60
84
|
if (!process.env.GETMYENV_TOKEN?.trim())
|
|
61
85
|
assertTokenNotCommitted();
|
|
62
86
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
@@ -68,7 +92,7 @@ export async function ensureTokenAndLink(opts = {}) {
|
|
|
68
92
|
if (meta?.projectId && meta.projectId !== who.project.id) {
|
|
69
93
|
throw new Error([
|
|
70
94
|
`project.json points at ${meta.projectName ?? meta.projectId}, but the token is for ${who.project.name}.`,
|
|
71
|
-
"Run: npx getmyenv unlink
|
|
95
|
+
"Run: npx getmyenv unlink, then run the command again.",
|
|
72
96
|
].join("\n"));
|
|
73
97
|
}
|
|
74
98
|
const usingEnvToken = Boolean(process.env.GETMYENV_TOKEN?.trim());
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Stream = {
|
|
2
|
+
isTTY?: boolean;
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* Color only for a terminal. picocolors alone turns color on for every Windows
|
|
6
|
+
* process, so piped or redirected output would hold escape codes.
|
|
7
|
+
* NO_COLOR turns it off, FORCE_COLOR turns it on.
|
|
8
|
+
*/
|
|
9
|
+
export declare function colorEnabled(stream: Stream, env?: Record<string, string | undefined>): boolean;
|
|
10
|
+
/** Colors for text written to stdout. */
|
|
11
|
+
export declare const pc: import("picocolors/types").Colors;
|
|
12
|
+
/** Colors for text written to stderr: prompts, status lines and warnings. */
|
|
13
|
+
export declare const pcErr: import("picocolors/types").Colors;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import picocolors from "picocolors";
|
|
2
|
+
/**
|
|
3
|
+
* Color only for a terminal. picocolors alone turns color on for every Windows
|
|
4
|
+
* process, so piped or redirected output would hold escape codes.
|
|
5
|
+
* NO_COLOR turns it off, FORCE_COLOR turns it on.
|
|
6
|
+
*/
|
|
7
|
+
export function colorEnabled(stream, env = process.env) {
|
|
8
|
+
if (env.NO_COLOR)
|
|
9
|
+
return false;
|
|
10
|
+
if (env.FORCE_COLOR && env.FORCE_COLOR !== "0")
|
|
11
|
+
return true;
|
|
12
|
+
return Boolean(stream.isTTY) && env.TERM !== "dumb";
|
|
13
|
+
}
|
|
14
|
+
/** Colors for text written to stdout. */
|
|
15
|
+
export const pc = picocolors.createColors(colorEnabled(process.stdout));
|
|
16
|
+
/** Colors for text written to stderr: prompts, status lines and warnings. */
|
|
17
|
+
export const pcErr = picocolors.createColors(colorEnabled(process.stderr));
|
package/dist/lib/confirm.d.ts
CHANGED
|
@@ -4,13 +4,17 @@ export type ConflictItem = {
|
|
|
4
4
|
detail?: string;
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
|
-
* Prompt only when
|
|
8
|
-
*
|
|
7
|
+
* Prompt only when something would be lost. Returns true to proceed.
|
|
8
|
+
* `conflicts` are names whose value changes, `removed` names that go away.
|
|
9
|
+
* Each list is counted on its own. Non-TTY: requires `yes`.
|
|
9
10
|
*/
|
|
10
11
|
export declare function confirmDiff(opts: {
|
|
11
12
|
conflicts: ConflictItem[];
|
|
13
|
+
removed?: ConflictItem[];
|
|
14
|
+
/** Heading after the count of `removed`. */
|
|
15
|
+
removedLabel?: string;
|
|
12
16
|
/** What will happen if the user confirms, e.g. "Overwrite local values". */
|
|
13
17
|
action: string;
|
|
14
|
-
/** Skip the prompt when true
|
|
18
|
+
/** Skip the prompt when true. */
|
|
15
19
|
yes?: boolean;
|
|
16
20
|
}): Promise<boolean>;
|
package/dist/lib/confirm.js
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { plural } from "@getmyenv/shared";
|
|
2
|
+
import { pcErr as pc } from "./colors.js";
|
|
2
3
|
import { promptConfirm } from "./fs.js";
|
|
4
|
+
function printItems(title, items) {
|
|
5
|
+
if (items.length === 0)
|
|
6
|
+
return;
|
|
7
|
+
console.error(pc.yellow(`\n${title}:`));
|
|
8
|
+
for (const c of items.slice(0, 20)) {
|
|
9
|
+
console.error(` ${c.name}${c.detail ? pc.dim(` - ${c.detail}`) : ""}`);
|
|
10
|
+
}
|
|
11
|
+
if (items.length > 20) {
|
|
12
|
+
console.error(pc.dim(` …and ${items.length - 20} more`));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
3
15
|
/**
|
|
4
|
-
* Prompt only when
|
|
5
|
-
*
|
|
16
|
+
* Prompt only when something would be lost. Returns true to proceed.
|
|
17
|
+
* `conflicts` are names whose value changes, `removed` names that go away.
|
|
18
|
+
* Each list is counted on its own. Non-TTY: requires `yes`.
|
|
6
19
|
*/
|
|
7
20
|
export async function confirmDiff(opts) {
|
|
8
21
|
const { conflicts, action, yes } = opts;
|
|
9
|
-
|
|
22
|
+
const removed = opts.removed ?? [];
|
|
23
|
+
if (conflicts.length === 0 && removed.length === 0)
|
|
10
24
|
return true;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
console.error(` ${c.name}${c.detail ? pc.dim(` - ${c.detail}`) : ""}`);
|
|
14
|
-
}
|
|
15
|
-
if (conflicts.length > 20) {
|
|
16
|
-
console.error(pc.dim(` …and ${conflicts.length - 20} more`));
|
|
17
|
-
}
|
|
25
|
+
printItems(plural(conflicts.length, "conflict"), conflicts);
|
|
26
|
+
printItems(`${plural(removed.length, "name")} ${opts.removedLabel ?? "will be removed"}`, removed);
|
|
18
27
|
console.error("");
|
|
19
28
|
if (yes)
|
|
20
29
|
return true;
|
|
21
30
|
if (!process.stdin.isTTY) {
|
|
22
|
-
throw new Error(
|
|
31
|
+
throw new Error(`${conflicts.length > 0 ? "Conflicts require" : "Removing them requires"} confirmation. Re-run with --yes to ${action.toLowerCase()}.`);
|
|
23
32
|
}
|
|
24
33
|
return promptConfirm(`${action}?`, { defaultYes: false });
|
|
25
34
|
}
|
package/dist/lib/context.js
CHANGED
|
@@ -35,7 +35,7 @@ export function resolveContext(who, meta, explicit) {
|
|
|
35
35
|
if (found)
|
|
36
36
|
return found;
|
|
37
37
|
if (who.token.kind === "server") {
|
|
38
|
-
throw new Error(`This server token reads ${who.token.context ?? "one
|
|
38
|
+
throw new Error(`This server token reads ${who.token.context ?? "one Read-only context"} only.`);
|
|
39
39
|
}
|
|
40
40
|
const available = who.contexts.map((c) => c.slug).join(", ") || "none";
|
|
41
41
|
const next = who.isGuest
|
|
@@ -15,7 +15,11 @@ export type FolderToken = {
|
|
|
15
15
|
addedAt: string;
|
|
16
16
|
};
|
|
17
17
|
export declare function tokenFilePath(cwd?: string): string;
|
|
18
|
+
export declare const TOKEN_JSON_INVALID = ".getmyenv/token.json is not valid JSON. Fix it, or run: npx getmyenv unlink --yes, then npx getmyenv";
|
|
19
|
+
/** Null when there is no token.json. Throws TOKEN_JSON_INVALID when it is not JSON. */
|
|
18
20
|
export declare function readFolderToken(cwd?: string): FolderToken | null;
|
|
21
|
+
/** `readFolderToken`, with an invalid token.json read as none. For unlink and the API URL only. */
|
|
22
|
+
export declare function readFolderTokenLenient(cwd?: string): FolderToken | null;
|
|
19
23
|
export declare function writeFolderToken(entry: {
|
|
20
24
|
token: string;
|
|
21
25
|
apiUrl?: string;
|
package/dist/lib/credentials.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { getLocalDir, getProjectRoot } from "./fs.js";
|
|
4
|
+
import { getLocalDir, getProjectRoot, readJsonFile } from "./fs.js";
|
|
5
5
|
import { apiUrlForDisk } from "./api-url.js";
|
|
6
6
|
/** True when git tracks this file. False without git or outside a repo. */
|
|
7
7
|
export function isTrackedByGit(file, cwd = process.cwd()) {
|
|
@@ -31,17 +31,10 @@ export function assertTokenNotCommitted(cwd = process.cwd()) {
|
|
|
31
31
|
export function tokenFilePath(cwd = process.cwd()) {
|
|
32
32
|
return path.join(getLocalDir(cwd), "token.json");
|
|
33
33
|
}
|
|
34
|
+
export const TOKEN_JSON_INVALID = ".getmyenv/token.json is not valid JSON. Fix it, or run: npx getmyenv unlink --yes, then npx getmyenv";
|
|
35
|
+
/** Null when there is no token.json. Throws TOKEN_JSON_INVALID when it is not JSON. */
|
|
34
36
|
export function readFolderToken(cwd = process.cwd()) {
|
|
35
|
-
const
|
|
36
|
-
if (!fs.existsSync(file))
|
|
37
|
-
return null;
|
|
38
|
-
let raw;
|
|
39
|
-
try {
|
|
40
|
-
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
37
|
+
const raw = readJsonFile(tokenFilePath(cwd), TOKEN_JSON_INVALID);
|
|
45
38
|
if (!raw || typeof raw !== "object")
|
|
46
39
|
return null;
|
|
47
40
|
const d = raw;
|
|
@@ -63,6 +56,15 @@ export function readFolderToken(cwd = process.cwd()) {
|
|
|
63
56
|
addedAt: d.addedAt,
|
|
64
57
|
};
|
|
65
58
|
}
|
|
59
|
+
/** `readFolderToken`, with an invalid token.json read as none. For unlink and the API URL only. */
|
|
60
|
+
export function readFolderTokenLenient(cwd = process.cwd()) {
|
|
61
|
+
try {
|
|
62
|
+
return readFolderToken(cwd);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
66
68
|
export function writeFolderToken(entry, cwd = process.cwd()) {
|
|
67
69
|
const file = tokenFilePath(cwd);
|
|
68
70
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
package/dist/lib/fs.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
/**
|
|
2
3
|
* `.getmyenv/project.json`, committed so a clone knows its project.
|
|
3
4
|
* Holds no API URL: a committed file must not redirect where tokens go.
|
|
@@ -15,8 +16,22 @@ export type ProjectMeta = {
|
|
|
15
16
|
*/
|
|
16
17
|
export declare function getProjectRoot(cwd?: string): string;
|
|
17
18
|
export declare function getLocalDir(cwd?: string): string;
|
|
19
|
+
/** Text of a file as UTF-8, without a byte order mark (Notepad and PowerShell 5.1 add one). */
|
|
20
|
+
export declare function readTextFile(file: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Parse a JSON file. Null when it does not exist. Invalid JSON throws
|
|
23
|
+
* `invalid`: a broken file is never read as missing, so it is never
|
|
24
|
+
* silently replaced.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readJsonFile(file: string, invalid: string): unknown;
|
|
27
|
+
export declare const PROJECT_JSON_INVALID = ".getmyenv/project.json is not valid JSON. Fix it, or restore it: git checkout -- .getmyenv/project.json";
|
|
18
28
|
export declare function readProjectMeta(cwd?: string): ProjectMeta | null;
|
|
19
29
|
export declare function writeProjectMeta(meta: ProjectMeta, cwd?: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Check that `file` can be written as a plain file: not a symlink, not a
|
|
32
|
+
* folder, and its folder exists. Returns its stats, or null when it is new.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assertFileTarget(file: string): fs.Stats | null;
|
|
20
35
|
/**
|
|
21
36
|
* Write a file only this user can read. Never through a symlink. A new file
|
|
22
37
|
* is created with `wx`; an existing one is replaced only when `overwrite`.
|
|
@@ -58,8 +73,28 @@ export declare function claimFilePath(cwd?: string): string;
|
|
|
58
73
|
export declare function readClaimFile(cwd?: string): ClaimFile | null;
|
|
59
74
|
export declare function writeClaimFile(data: Omit<ClaimFile, "version">, cwd?: string): string;
|
|
60
75
|
export declare function clearClaimFile(cwd?: string): boolean;
|
|
61
|
-
/** dotenv rules (multiline values, inline comments, BOM)
|
|
76
|
+
/** dotenv rules (multiline values, inline comments, BOM). Invalid names land in `skipped`. */
|
|
77
|
+
export declare function readEnvText(text: string): {
|
|
78
|
+
values: Record<string, string>;
|
|
79
|
+
skipped: string[];
|
|
80
|
+
};
|
|
81
|
+
/** `readEnvText`, valid names only. */
|
|
62
82
|
export declare function parseEnvText(text: string): Record<string, string>;
|
|
83
|
+
/**
|
|
84
|
+
* Text from bytes, by byte order mark: UTF-16 LE or BE (PowerShell 5.1 `>`
|
|
85
|
+
* writes UTF-16 LE), else UTF-8 without its BOM.
|
|
86
|
+
*/
|
|
87
|
+
export declare function decodeText(buf: Buffer): string;
|
|
88
|
+
/** `readEnvText` for a file, UTF-8 or UTF-16. A missing file reads as empty. */
|
|
89
|
+
export declare function readEnvFile(filePath: string): {
|
|
90
|
+
values: Record<string, string>;
|
|
91
|
+
skipped: string[];
|
|
92
|
+
};
|
|
93
|
+
/** Names that appear more than once in an env file, with their count. dotenv keeps the last one. */
|
|
94
|
+
export declare function duplicateEnvNames(filePath: string): Array<{
|
|
95
|
+
name: string;
|
|
96
|
+
count: number;
|
|
97
|
+
}>;
|
|
63
98
|
/** `parseEnvText` for a file. A missing file reads as empty. */
|
|
64
99
|
export declare function parseEnvFile(filePath: string): Record<string, string>;
|
|
65
100
|
/** `.example`, `.sample` and `.template` files hold placeholders, not values. */
|