@stage5/lumine 0.2.4 → 0.2.5
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/bin/lumine.js +1 -3158
- package/lib/api.js +381 -0
- package/lib/assets.js +670 -0
- package/lib/auth.js +217 -0
- package/lib/commands.js +1491 -0
- package/lib/constants.js +256 -0
- package/lib/http.js +72 -0
- package/lib/sdk.js +489 -0
- package/lib/util.js +154 -0
- package/lib/workspace.js +577 -0
- package/package.json +5 -1
package/lib/auth.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
|
|
5
|
+
import { requestJson } from "./http.js";
|
|
6
|
+
import { sleep, trimTrailingSlash } from "./util.js";
|
|
7
|
+
|
|
8
|
+
export async function login(options) {
|
|
9
|
+
const start = await requestJson({
|
|
10
|
+
method: "POST",
|
|
11
|
+
url: `${options.apiUrl}/cli/device/start`,
|
|
12
|
+
body: {
|
|
13
|
+
clientName: options.clientName,
|
|
14
|
+
scopes: [
|
|
15
|
+
"build:read",
|
|
16
|
+
"build:write",
|
|
17
|
+
"build:check",
|
|
18
|
+
"build:publish",
|
|
19
|
+
"build:sdk",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
timeoutMs: options.timeoutMs,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const approvalUrl = start.verificationUriComplete || start.verificationUri;
|
|
26
|
+
console.log("Connect Lumine CLI to Twinkle.");
|
|
27
|
+
if (options.openBrowser && approvalUrl) {
|
|
28
|
+
console.log("Opening Twinkle in your browser...");
|
|
29
|
+
const opened = await openBrowser(approvalUrl);
|
|
30
|
+
if (!opened) console.log("Could not open the browser automatically.");
|
|
31
|
+
}
|
|
32
|
+
console.log(`Approval link: ${approvalUrl}`);
|
|
33
|
+
console.log(`Code: ${start.userCode}`);
|
|
34
|
+
console.log("Leave this terminal open. Waiting for approval...");
|
|
35
|
+
|
|
36
|
+
const intervalMs = Math.max(Number(start.interval || 3), 1) * 1000;
|
|
37
|
+
const startedAt = Date.now();
|
|
38
|
+
const expiresInMs = Math.max(Number(start.expiresIn || 600), 1) * 1000;
|
|
39
|
+
|
|
40
|
+
while (Date.now() - startedAt < expiresInMs) {
|
|
41
|
+
await sleep(intervalMs);
|
|
42
|
+
const tokenResponse = await pollToken({
|
|
43
|
+
options,
|
|
44
|
+
deviceCode: start.deviceCode,
|
|
45
|
+
});
|
|
46
|
+
if (!tokenResponse) continue;
|
|
47
|
+
|
|
48
|
+
await writeAuth({
|
|
49
|
+
options,
|
|
50
|
+
token: tokenResponse.accessToken,
|
|
51
|
+
username: tokenResponse.user?.username || "",
|
|
52
|
+
userId: tokenResponse.user?.id || null,
|
|
53
|
+
expiresAt: Date.now() + Number(tokenResponse.expiresIn || 0) * 1000,
|
|
54
|
+
});
|
|
55
|
+
console.log(
|
|
56
|
+
`Logged in${tokenResponse.user?.username ? ` as ${tokenResponse.user.username}` : ""}.`,
|
|
57
|
+
);
|
|
58
|
+
console.log("You can now run `lumine` to choose a project.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error("Login code expired. Run `lumine login` again.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function pollToken({ options, deviceCode }) {
|
|
66
|
+
try {
|
|
67
|
+
return await requestJson({
|
|
68
|
+
method: "POST",
|
|
69
|
+
url: `${options.apiUrl}/cli/device/token`,
|
|
70
|
+
body: { deviceCode },
|
|
71
|
+
timeoutMs: options.timeoutMs,
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.status === 428 || error.data?.error === "authorization_pending") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function logout(options) {
|
|
82
|
+
let removed = false;
|
|
83
|
+
try {
|
|
84
|
+
await fs.unlink(options.authFile);
|
|
85
|
+
removed = true;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error.code !== "ENOENT") throw error;
|
|
88
|
+
}
|
|
89
|
+
console.log(
|
|
90
|
+
removed
|
|
91
|
+
? `Removed Lumine CLI login at ${options.authFile}`
|
|
92
|
+
: `No Lumine CLI login found at ${options.authFile}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function whoami(options) {
|
|
97
|
+
const auth = await resolveAuth(options);
|
|
98
|
+
const session = await requestJson({
|
|
99
|
+
url: `${options.apiUrl}/cli/session`,
|
|
100
|
+
authToken: auth.token,
|
|
101
|
+
timeoutMs: options.timeoutMs,
|
|
102
|
+
});
|
|
103
|
+
console.log(
|
|
104
|
+
`Logged in as ${session.username || auth.username || "unknown"} ` +
|
|
105
|
+
`(userId=${session.userId || auth.userId || "unknown"})`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function ensureAuth(options) {
|
|
110
|
+
try {
|
|
111
|
+
return await resolveAuth(options);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (options.authToken || !isMissingLoginError(error)) throw error;
|
|
114
|
+
}
|
|
115
|
+
await login(options);
|
|
116
|
+
return await resolveAuth(options);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function resolveAuth(options) {
|
|
120
|
+
if (options.authToken) {
|
|
121
|
+
return { token: options.authToken };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const text = await fs.readFile(options.authFile, "utf8");
|
|
125
|
+
const auth = JSON.parse(text);
|
|
126
|
+
if (auth.apiUrl && trimTrailingSlash(auth.apiUrl) !== options.apiUrl) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Saved login is for ${auth.apiUrl}. Run ` +
|
|
129
|
+
`lumine login --api-url ${options.apiUrl}.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (auth.token) return auth;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.code !== "ENOENT") throw error;
|
|
135
|
+
}
|
|
136
|
+
throw new Error("Run `lumine login` before launching a Twinkle build.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function writeAuth({ options, token, username, userId, expiresAt }) {
|
|
140
|
+
let existingAuth = {};
|
|
141
|
+
try {
|
|
142
|
+
existingAuth = JSON.parse(await fs.readFile(options.authFile, "utf8"));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error.code !== "ENOENT") throw error;
|
|
145
|
+
}
|
|
146
|
+
await writeAuthFile(options, {
|
|
147
|
+
...existingAuth,
|
|
148
|
+
token,
|
|
149
|
+
username,
|
|
150
|
+
userId,
|
|
151
|
+
expiresAt,
|
|
152
|
+
apiUrl: options.apiUrl,
|
|
153
|
+
createdAt: new Date().toISOString(),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function writeAuthFile(options, auth) {
|
|
158
|
+
await fs.mkdir(path.dirname(options.authFile), {
|
|
159
|
+
recursive: true,
|
|
160
|
+
mode: 0o700,
|
|
161
|
+
});
|
|
162
|
+
await fs.writeFile(options.authFile, JSON.stringify(auth, null, 2), {
|
|
163
|
+
mode: 0o600,
|
|
164
|
+
});
|
|
165
|
+
await fs.chmod(options.authFile, 0o600);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function saveSelectedBuild({ options, auth, build }) {
|
|
169
|
+
if (options.authToken || !build?.id) return;
|
|
170
|
+
await writeAuthFile(options, {
|
|
171
|
+
...auth,
|
|
172
|
+
selectedBuildId: Number(build.id),
|
|
173
|
+
selectedBuildTitle: build.title || `Build ${build.id}`,
|
|
174
|
+
selectedBuildRole: build.role || "",
|
|
175
|
+
selectedAt: new Date().toISOString(),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function assertAuthScope({ options, auth, scope }) {
|
|
180
|
+
const session = await requestJson({
|
|
181
|
+
url: `${options.apiUrl}/cli/session`,
|
|
182
|
+
authToken: auth.token,
|
|
183
|
+
timeoutMs: options.timeoutMs,
|
|
184
|
+
});
|
|
185
|
+
const scopes = Array.isArray(session.scopes) ? session.scopes : [];
|
|
186
|
+
if (!scopes.includes(scope)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Saved login is missing ${scope}. Run \`lumine login\` again to grant it.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function isMissingLoginError(error) {
|
|
194
|
+
return String(error?.message || "").includes("Run `lumine login`");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function openBrowser(url) {
|
|
198
|
+
const command =
|
|
199
|
+
process.platform === "darwin"
|
|
200
|
+
? "open"
|
|
201
|
+
: process.platform === "win32"
|
|
202
|
+
? "cmd"
|
|
203
|
+
: "xdg-open";
|
|
204
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
205
|
+
|
|
206
|
+
return await new Promise((resolve) => {
|
|
207
|
+
const child = spawn(command, args, {
|
|
208
|
+
detached: true,
|
|
209
|
+
stdio: "ignore",
|
|
210
|
+
});
|
|
211
|
+
child.once("error", () => resolve(false));
|
|
212
|
+
child.once("spawn", () => {
|
|
213
|
+
child.unref();
|
|
214
|
+
resolve(true);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|