foldrun 0.4.0 → 0.6.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 +1 -0
- package/bin/foldrun.mjs +18 -4
- package/package.json +10 -4
- package/src/commands.mjs +731 -116
- package/src/credentials.mjs +137 -30
package/src/credentials.mjs
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
// Where `foldrun login` puts what it was given.
|
|
2
2
|
//
|
|
3
|
-
// `~/.foldrun/credentials.json`, 0600
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// `~/.foldrun/credentials.json`, 0600. A PROFILE is one signed-in account on
|
|
4
|
+
// one platform: a name, the URL, the key, and who that key turned out to be.
|
|
5
|
+
// `current` names the one a bare command talks to. The shape every CLI that
|
|
6
|
+
// signs in keeps, so the person who has used Vercel's or Fly's finds nothing
|
|
7
|
+
// to learn. FOLDRUN_HOME moves the directory, for tests and for machines
|
|
8
|
+
// where HOME is not where things should be kept.
|
|
9
|
+
//
|
|
10
|
+
// It was one credential per URL, which is the same thing until somebody
|
|
11
|
+
// looks after more than one customer on the same platform — an agency, a
|
|
12
|
+
// reseller, us. Then a second account on the same host had nowhere to live
|
|
13
|
+
// and every command needed --token pasted in. Profiles are keyed by name,
|
|
14
|
+
// so two accounts on one platform are two profiles.
|
|
15
|
+
//
|
|
16
|
+
// The old shape is still read, and still written for the newest profile on
|
|
17
|
+
// each URL, so a machine that moves between CLI versions keeps working.
|
|
8
18
|
//
|
|
9
19
|
// The value stored is an ordinary API key, minted at approval and listed on
|
|
10
20
|
// Settings → API keys with the machine's name. Nothing here is a second
|
|
@@ -24,53 +34,150 @@ export const normaliseUrl = (url) => {
|
|
|
24
34
|
return `${u.protocol}//${u.host}${u.pathname.replace(/\/+$/, "")}`;
|
|
25
35
|
};
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
function readRaw() {
|
|
28
38
|
try {
|
|
29
39
|
const parsed = JSON.parse(fs.readFileSync(file(), "utf8"));
|
|
30
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
current: parsed.current ?? null,
|
|
42
|
+
default: parsed.default ?? null,
|
|
43
|
+
platforms: parsed.platforms ?? {},
|
|
44
|
+
profiles: parsed.profiles ?? {},
|
|
45
|
+
};
|
|
31
46
|
} catch {
|
|
32
|
-
return { default: null, platforms: {} };
|
|
47
|
+
return { current: null, default: null, platforms: {}, profiles: {} };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every profile, with the old per-URL entries folded in under the account
|
|
53
|
+
* name they carry. Reading migrates; nothing is written until a command
|
|
54
|
+
* that changes something writes.
|
|
55
|
+
*/
|
|
56
|
+
export function readCredentials() {
|
|
57
|
+
const raw = readRaw();
|
|
58
|
+
const profiles = { ...raw.profiles };
|
|
59
|
+
for (const [url, entry] of Object.entries(raw.platforms)) {
|
|
60
|
+
if (Object.values(profiles).some((p) => normaliseUrl(p.url) === url && p.token === entry.token)) continue;
|
|
61
|
+
profiles[nameFor(profiles, entry.account, url)] = { ...entry, url };
|
|
62
|
+
}
|
|
63
|
+
const names = Object.keys(profiles);
|
|
64
|
+
let current = raw.current && profiles[raw.current] ? raw.current : null;
|
|
65
|
+
if (!current && raw.default) {
|
|
66
|
+
current = names.find((n) => normaliseUrl(profiles[n].url) === normaliseUrl(raw.default)) ?? null;
|
|
33
67
|
}
|
|
68
|
+
return { current: current ?? names[0] ?? null, profiles };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A name nobody is using: the account, else the account and the host —
|
|
72
|
+
* two customers called "acme" on two platforms are two profiles. */
|
|
73
|
+
function nameFor(profiles, account, url) {
|
|
74
|
+
const base = (account ?? "account").trim() || "account";
|
|
75
|
+
if (!profiles[base]) return base;
|
|
76
|
+
const host = new URL(url).host;
|
|
77
|
+
const withHost = `${base}@${host}`;
|
|
78
|
+
if (!profiles[withHost]) return withHost;
|
|
79
|
+
for (let i = 2; ; i++) if (!profiles[`${withHost}-${i}`]) return `${withHost}-${i}`;
|
|
34
80
|
}
|
|
35
81
|
|
|
36
|
-
function
|
|
82
|
+
function write(current, profiles) {
|
|
83
|
+
// The old shape goes on being written for the newest profile on each URL,
|
|
84
|
+
// so a machine that moves between CLI versions is not signed out by the
|
|
85
|
+
// move. It is a mirror, never the source.
|
|
86
|
+
const platforms = {};
|
|
87
|
+
for (const p of Object.values(profiles).sort((a, b) => String(a.loggedInAt ?? "").localeCompare(String(b.loggedInAt ?? "")))) {
|
|
88
|
+
platforms[normaliseUrl(p.url)] = { token: p.token, email: p.email ?? null, account: p.account, role: p.role, ...(p.minted ? { minted: true } : {}), loggedInAt: p.loggedInAt };
|
|
89
|
+
}
|
|
37
90
|
fs.mkdirSync(credentialsDir(), { recursive: true, mode: 0o700 });
|
|
38
|
-
fs.writeFileSync(
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
file(),
|
|
93
|
+
JSON.stringify({ current, default: current && profiles[current] ? normaliseUrl(profiles[current].url) : null, profiles, platforms }, null, 2) + "\n",
|
|
94
|
+
{ mode: 0o600 },
|
|
95
|
+
);
|
|
39
96
|
}
|
|
40
97
|
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
43
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Remember a signed-in account. A second login to the same URL for the same
|
|
100
|
+
* ACCOUNT replaces that profile rather than making a near-duplicate; a
|
|
101
|
+
* different account on the same URL is a new one.
|
|
102
|
+
* @param {string} url
|
|
103
|
+
* @param {Record<string, any>} entry
|
|
104
|
+
* @param {{ makeDefault?: boolean, name?: string }} [opts]
|
|
105
|
+
*/
|
|
106
|
+
export function saveCredential(url, entry, { makeDefault = true, name } = {}) {
|
|
44
107
|
const key = normaliseUrl(url);
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
108
|
+
const { current, profiles } = readCredentials();
|
|
109
|
+
const existing = name ?? Object.keys(profiles).find((n) => normaliseUrl(profiles[n].url) === key && profiles[n].account === entry.account);
|
|
110
|
+
const profileName = existing ?? nameFor(profiles, entry.account, key);
|
|
111
|
+
profiles[profileName] = { ...entry, url: key, loggedInAt: new Date().toISOString() };
|
|
112
|
+
write(makeDefault || !current ? profileName : current, profiles);
|
|
113
|
+
return profileName;
|
|
50
114
|
}
|
|
51
115
|
|
|
52
|
-
/** Forget
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
116
|
+
/** Forget one. By name, else by URL (the newest there). Returns what was
|
|
117
|
+
* stored, for a best-effort revoke. */
|
|
118
|
+
export function removeCredential(urlOrName) {
|
|
119
|
+
const { current, profiles } = readCredentials();
|
|
120
|
+
const key = (() => {
|
|
121
|
+
if (profiles[urlOrName]) return urlOrName;
|
|
122
|
+
try {
|
|
123
|
+
const u = normaliseUrl(urlOrName);
|
|
124
|
+
const here = Object.keys(profiles).filter((n) => normaliseUrl(profiles[n].url) === u);
|
|
125
|
+
return here.sort((a, b) => String(profiles[a].loggedInAt ?? "").localeCompare(String(profiles[b].loggedInAt ?? ""))).at(-1) ?? null;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
if (!key) return null;
|
|
131
|
+
const gone = profiles[key];
|
|
132
|
+
delete profiles[key];
|
|
133
|
+
const names = Object.keys(profiles);
|
|
134
|
+
write(current === key ? names[0] ?? null : current, profiles);
|
|
60
135
|
return gone;
|
|
61
136
|
}
|
|
62
137
|
|
|
63
|
-
/** The stored entry for a URL
|
|
138
|
+
/** The stored entry for a URL — the newest account signed in there. */
|
|
64
139
|
export function credentialFor(url) {
|
|
65
140
|
if (!url) return null;
|
|
66
141
|
try {
|
|
67
|
-
|
|
142
|
+
const { profiles } = readCredentials();
|
|
143
|
+
const here = Object.keys(profiles).filter((n) => normaliseUrl(profiles[n].url) === normaliseUrl(url));
|
|
144
|
+
const newest = here.sort((a, b) => String(profiles[a].loggedInAt ?? "").localeCompare(String(profiles[b].loggedInAt ?? ""))).at(-1);
|
|
145
|
+
return newest ? { ...profiles[newest], name: newest } : null;
|
|
68
146
|
} catch {
|
|
69
147
|
return null;
|
|
70
148
|
}
|
|
71
149
|
}
|
|
72
150
|
|
|
73
|
-
/**
|
|
151
|
+
/** One profile by name. */
|
|
152
|
+
export function profileByName(name) {
|
|
153
|
+
if (!name) return null;
|
|
154
|
+
const { profiles } = readCredentials();
|
|
155
|
+
return profiles[name] ? { ...profiles[name], name } : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The profile a bare command talks as. */
|
|
159
|
+
export function currentProfile() {
|
|
160
|
+
const { current, profiles } = readCredentials();
|
|
161
|
+
return current && profiles[current] ? { ...profiles[current], name: current } : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Make one the profile a bare command talks as. */
|
|
165
|
+
export function useProfile(name) {
|
|
166
|
+
const { profiles } = readCredentials();
|
|
167
|
+
if (!profiles[name]) return null;
|
|
168
|
+
write(name, profiles);
|
|
169
|
+
return { ...profiles[name], name };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** All of them, newest sign-in last. */
|
|
173
|
+
export function listProfiles() {
|
|
174
|
+
const { current, profiles } = readCredentials();
|
|
175
|
+
return Object.entries(profiles)
|
|
176
|
+
.map(([name, p]) => ({ ...p, name, current: name === current }))
|
|
177
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The platform a bare command talks to: the current profile's. */
|
|
74
181
|
export function defaultPlatform() {
|
|
75
|
-
return
|
|
182
|
+
return currentProfile()?.url ?? null;
|
|
76
183
|
}
|