@tonbo/cli 0.0.2 → 0.0.3
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/dist/src/app.js +4 -0
- package/dist/src/auth.d.ts +7 -1
- package/dist/src/auth.js +48 -10
- package/dist/src/commands.d.ts +4 -0
- package/dist/src/commands.js +50 -7
- package/dist/src/progress.d.ts +25 -0
- package/dist/src/progress.js +60 -0
- package/package.json +1 -1
package/dist/src/app.js
CHANGED
|
@@ -5,6 +5,8 @@ import { AuthClient } from "./auth.js";
|
|
|
5
5
|
import { deployCommand, runCommand, loginCommand, projectCreateCommand, projectUseCommand, sshCommand, sshKeyAddCommand, sshKeyRemoveCommand, secretListCommand, secretRemoveCommand, secretSetCommand, } from "./commands.js";
|
|
6
6
|
import { FileConfigStore } from "./config.js";
|
|
7
7
|
import { FileCredentialStore } from "./credentials.js";
|
|
8
|
+
import { silentProgress, TerminalProgress } from "./progress.js";
|
|
9
|
+
import { readDefaultSshPublicKeys } from "./ssh-key.js";
|
|
8
10
|
const packageVersion = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
9
11
|
export function createDependencies(json = false) {
|
|
10
12
|
const accountOrigin = process.env.TONBO_ACCOUNT_ORIGIN || "https://tonbo.dev";
|
|
@@ -14,12 +16,14 @@ export function createDependencies(json = false) {
|
|
|
14
16
|
auth: new AuthClient(new FileCredentialStore(), fetch, accountOrigin),
|
|
15
17
|
config: new FileConfigStore(),
|
|
16
18
|
cwd: () => process.cwd(),
|
|
19
|
+
defaultSshPublicKeys: readDefaultSshPublicKeys,
|
|
17
20
|
output: (value) => {
|
|
18
21
|
if (json)
|
|
19
22
|
console.log(JSON.stringify(value));
|
|
20
23
|
else
|
|
21
24
|
console.log(value.message ?? value);
|
|
22
25
|
},
|
|
26
|
+
progress: json ? silentProgress : new TerminalProgress(process.stderr),
|
|
23
27
|
secretValue: async (name, fromEnvironment) => {
|
|
24
28
|
const environmentName = fromEnvironment ?? name;
|
|
25
29
|
const value = process.env[environmentName];
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { CredentialStore } from "./credentials.js";
|
|
2
2
|
import type { OAuthTokenSet } from "./types.js";
|
|
3
|
+
export type LoginStep = "account-config" | "callback-server" | "browser" | "browser-authorization" | "callback-close" | "token-exchange" | "credential-store";
|
|
4
|
+
export interface LoginProgressEvent {
|
|
5
|
+
status: "completed" | "started";
|
|
6
|
+
step: LoginStep;
|
|
7
|
+
}
|
|
8
|
+
export type LoginProgress = (event: LoginProgressEvent) => void;
|
|
3
9
|
export declare class AuthClient {
|
|
4
10
|
private readonly credentials;
|
|
5
11
|
private readonly fetcher;
|
|
@@ -7,7 +13,7 @@ export declare class AuthClient {
|
|
|
7
13
|
private readonly browserOpener;
|
|
8
14
|
constructor(credentials: CredentialStore, fetcher: typeof fetch, accountOrigin?: string, browserOpener?: (url: string) => Promise<void>);
|
|
9
15
|
accessToken(): Promise<string>;
|
|
10
|
-
login(): Promise<OAuthTokenSet>;
|
|
16
|
+
login(progress?: LoginProgress): Promise<OAuthTokenSet>;
|
|
11
17
|
private config;
|
|
12
18
|
private exchange;
|
|
13
19
|
private listenForCode;
|
package/dist/src/auth.js
CHANGED
|
@@ -55,8 +55,10 @@ export class AuthClient {
|
|
|
55
55
|
await this.credentials.save(refreshed);
|
|
56
56
|
return refreshed.access_token;
|
|
57
57
|
}
|
|
58
|
-
async login() {
|
|
58
|
+
async login(progress = () => { }) {
|
|
59
|
+
progress({ status: "started", step: "account-config" });
|
|
59
60
|
const config = await this.config();
|
|
61
|
+
progress({ status: "completed", step: "account-config" });
|
|
60
62
|
if (config.redirect_uri !== LOOPBACK_REDIRECT)
|
|
61
63
|
throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
|
|
62
64
|
const verifier = base64url(randomBytes(48));
|
|
@@ -72,7 +74,8 @@ export class AuthClient {
|
|
|
72
74
|
scope: "openid profile email",
|
|
73
75
|
state,
|
|
74
76
|
}).toString();
|
|
75
|
-
const code = await this.listenForCode(state, () => this.browserOpener(authorization.toString()));
|
|
77
|
+
const code = await this.listenForCode(state, () => this.browserOpener(authorization.toString()), progress);
|
|
78
|
+
progress({ status: "started", step: "token-exchange" });
|
|
76
79
|
const tokens = withAbsoluteExpiry(await this.exchange(config.token_endpoint, new URLSearchParams({
|
|
77
80
|
client_id: config.client_id,
|
|
78
81
|
code,
|
|
@@ -80,7 +83,10 @@ export class AuthClient {
|
|
|
80
83
|
grant_type: "authorization_code",
|
|
81
84
|
redirect_uri: config.redirect_uri,
|
|
82
85
|
})));
|
|
86
|
+
progress({ status: "completed", step: "token-exchange" });
|
|
87
|
+
progress({ status: "started", step: "credential-store" });
|
|
83
88
|
await this.credentials.save(tokens);
|
|
89
|
+
progress({ status: "completed", step: "credential-store" });
|
|
84
90
|
return tokens;
|
|
85
91
|
}
|
|
86
92
|
config() {
|
|
@@ -93,10 +99,19 @@ export class AuthClient {
|
|
|
93
99
|
body,
|
|
94
100
|
});
|
|
95
101
|
}
|
|
96
|
-
listenForCode(expectedState, ready) {
|
|
102
|
+
listenForCode(expectedState, ready, progress) {
|
|
97
103
|
return new Promise((resolve, reject) => {
|
|
98
104
|
let settled = false;
|
|
99
|
-
const
|
|
105
|
+
const sockets = new Set();
|
|
106
|
+
let authorizationStarted = false;
|
|
107
|
+
const startAuthorization = () => {
|
|
108
|
+
if (authorizationStarted)
|
|
109
|
+
return;
|
|
110
|
+
authorizationStarted = true;
|
|
111
|
+
progress({ status: "completed", step: "browser" });
|
|
112
|
+
progress({ status: "started", step: "browser-authorization" });
|
|
113
|
+
};
|
|
114
|
+
const finish = (result, completedResponseSocket) => {
|
|
100
115
|
if (settled)
|
|
101
116
|
return;
|
|
102
117
|
settled = true;
|
|
@@ -104,16 +119,24 @@ export class AuthClient {
|
|
|
104
119
|
const complete = (closeError) => {
|
|
105
120
|
if (closeError)
|
|
106
121
|
reject(closeError);
|
|
107
|
-
else if ("code" in result)
|
|
122
|
+
else if ("code" in result) {
|
|
123
|
+
progress({ status: "completed", step: "callback-close" });
|
|
108
124
|
resolve(result.code);
|
|
125
|
+
}
|
|
109
126
|
else
|
|
110
127
|
reject(result.error);
|
|
111
128
|
};
|
|
129
|
+
if ("code" in result)
|
|
130
|
+
progress({ status: "started", step: "callback-close" });
|
|
112
131
|
if (!server.listening) {
|
|
113
132
|
complete();
|
|
114
133
|
return;
|
|
115
134
|
}
|
|
116
135
|
server.close(complete);
|
|
136
|
+
for (const socket of sockets) {
|
|
137
|
+
if (socket !== completedResponseSocket)
|
|
138
|
+
socket.destroy();
|
|
139
|
+
}
|
|
117
140
|
server.closeIdleConnections();
|
|
118
141
|
};
|
|
119
142
|
const timeout = setTimeout(() => {
|
|
@@ -124,7 +147,8 @@ export class AuthClient {
|
|
|
124
147
|
const code = url.searchParams.get("code");
|
|
125
148
|
const oauthError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
|
|
126
149
|
if (oauthError) {
|
|
127
|
-
|
|
150
|
+
const responseSocket = response.socket;
|
|
151
|
+
respond(response, "denied", () => finish({ error: new Error(oauthError) }, responseSocket));
|
|
128
152
|
return;
|
|
129
153
|
}
|
|
130
154
|
if (url.pathname !== "/callback" ||
|
|
@@ -133,12 +157,26 @@ export class AuthClient {
|
|
|
133
157
|
respond(response, "invalid");
|
|
134
158
|
return;
|
|
135
159
|
}
|
|
136
|
-
|
|
160
|
+
startAuthorization();
|
|
161
|
+
progress({ status: "completed", step: "browser-authorization" });
|
|
162
|
+
const responseSocket = response.socket;
|
|
163
|
+
respond(response, "complete", () => finish({ code }, responseSocket));
|
|
164
|
+
});
|
|
165
|
+
server.on("connection", (socket) => {
|
|
166
|
+
sockets.add(socket);
|
|
167
|
+
socket.once("close", () => sockets.delete(socket));
|
|
137
168
|
});
|
|
138
169
|
server.once("error", (error) => finish({ error }));
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
progress({ status: "started", step: "callback-server" });
|
|
171
|
+
server.listen(17655, "localhost", () => {
|
|
172
|
+
progress({ status: "completed", step: "callback-server" });
|
|
173
|
+
progress({ status: "started", step: "browser" });
|
|
174
|
+
void ready()
|
|
175
|
+
.then(startAuthorization)
|
|
176
|
+
.catch((error) => finish({
|
|
177
|
+
error: error instanceof Error ? error : new Error("Could not open the login browser."),
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
142
180
|
});
|
|
143
181
|
}
|
|
144
182
|
}
|
package/dist/src/commands.d.ts
CHANGED
|
@@ -2,12 +2,16 @@ import type { AuthClient } from "./auth.js";
|
|
|
2
2
|
import type { TonboApi } from "./api.js";
|
|
3
3
|
import type { ConfigStore } from "./config.js";
|
|
4
4
|
import type { ProjectSummary } from "./types.js";
|
|
5
|
+
import { readDefaultSshPublicKeys } from "./ssh-key.js";
|
|
6
|
+
import type { ProgressReporter } from "./progress.js";
|
|
5
7
|
export interface CommandDependencies {
|
|
6
8
|
api: TonboApi;
|
|
7
9
|
auth: AuthClient;
|
|
8
10
|
config: ConfigStore;
|
|
9
11
|
cwd: () => string;
|
|
12
|
+
defaultSshPublicKeys: typeof readDefaultSshPublicKeys;
|
|
10
13
|
output: (value: unknown) => void;
|
|
14
|
+
progress: ProgressReporter;
|
|
11
15
|
executable?: () => string;
|
|
12
16
|
secretValue: (name: string, fromEnvironment?: string) => Promise<string>;
|
|
13
17
|
}
|
package/dist/src/commands.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildRevision, loadDeclaration } from "./declaration.js";
|
|
2
2
|
import { buildSourceBundle, findDeclarationRoot } from "./source.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readSshPublicKey } from "./ssh-key.js";
|
|
4
4
|
import { launchProjectSsh } from "./ssh.js";
|
|
5
5
|
export async function resolveProject(deps, selector) {
|
|
6
6
|
const oauthToken = await deps.auth.accessToken();
|
|
@@ -37,12 +37,55 @@ export function selectProject(projects, selector) {
|
|
|
37
37
|
return matches[0];
|
|
38
38
|
}
|
|
39
39
|
export async function loginCommand(deps) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await deps.api.registerSshKey(
|
|
45
|
-
|
|
40
|
+
try {
|
|
41
|
+
const tokens = await deps.auth.login((event) => reportLoginProgress(deps.progress, event));
|
|
42
|
+
deps.progress.start("Preparing SSH access");
|
|
43
|
+
const keys = await deps.defaultSshPublicKeys();
|
|
44
|
+
await Promise.all(keys.map((key) => deps.api.registerSshKey(tokens.access_token, key)));
|
|
45
|
+
deps.progress.succeed("SSH access ready");
|
|
46
|
+
deps.output({ message: "Logged in to Tonbo." });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
deps.progress.fail();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const LOGIN_PROGRESS_MESSAGES = {
|
|
54
|
+
"account-config": {
|
|
55
|
+
started: "Connecting to Tonbo",
|
|
56
|
+
completed: "Connected to Tonbo",
|
|
57
|
+
},
|
|
58
|
+
"callback-server": {
|
|
59
|
+
started: "Starting local browser callback",
|
|
60
|
+
completed: "Local browser callback ready",
|
|
61
|
+
},
|
|
62
|
+
browser: {
|
|
63
|
+
started: "Opening browser",
|
|
64
|
+
completed: "Browser opened",
|
|
65
|
+
},
|
|
66
|
+
"browser-authorization": {
|
|
67
|
+
started: "Waiting for browser authorization",
|
|
68
|
+
completed: "Browser authorization received",
|
|
69
|
+
},
|
|
70
|
+
"callback-close": {
|
|
71
|
+
started: "Closing local browser callback",
|
|
72
|
+
completed: "Local browser callback closed",
|
|
73
|
+
},
|
|
74
|
+
"token-exchange": {
|
|
75
|
+
started: "Exchanging authorization code",
|
|
76
|
+
completed: "Authorization code exchanged",
|
|
77
|
+
},
|
|
78
|
+
"credential-store": {
|
|
79
|
+
started: "Saving login session",
|
|
80
|
+
completed: "Login session saved",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
function reportLoginProgress(progress, event) {
|
|
84
|
+
const messages = LOGIN_PROGRESS_MESSAGES[event.step];
|
|
85
|
+
if (event.status === "started")
|
|
86
|
+
progress.start(messages.started);
|
|
87
|
+
else
|
|
88
|
+
progress.succeed(messages.completed);
|
|
46
89
|
}
|
|
47
90
|
export async function sshKeyAddCommand(deps, path) {
|
|
48
91
|
const key = await readSshPublicKey(path);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ProgressReporter {
|
|
2
|
+
start(message: string): void;
|
|
3
|
+
succeed(message?: string): void;
|
|
4
|
+
fail(message?: string): void;
|
|
5
|
+
}
|
|
6
|
+
interface ProgressStream {
|
|
7
|
+
isTTY?: boolean;
|
|
8
|
+
write(value: string): unknown;
|
|
9
|
+
}
|
|
10
|
+
export declare class TerminalProgress implements ProgressReporter {
|
|
11
|
+
private readonly stream;
|
|
12
|
+
private readonly intervalMs;
|
|
13
|
+
private activeMessage;
|
|
14
|
+
private frame;
|
|
15
|
+
private lastWidth;
|
|
16
|
+
private timer;
|
|
17
|
+
constructor(stream: ProgressStream, intervalMs?: number);
|
|
18
|
+
start(message: string): void;
|
|
19
|
+
succeed(message?: string | undefined): void;
|
|
20
|
+
fail(message?: string | undefined): void;
|
|
21
|
+
private finish;
|
|
22
|
+
private render;
|
|
23
|
+
}
|
|
24
|
+
export declare const silentProgress: ProgressReporter;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const FRAMES = ["|", "/", "-", "\\"];
|
|
2
|
+
export class TerminalProgress {
|
|
3
|
+
stream;
|
|
4
|
+
intervalMs;
|
|
5
|
+
activeMessage;
|
|
6
|
+
frame = 0;
|
|
7
|
+
lastWidth = 0;
|
|
8
|
+
timer;
|
|
9
|
+
constructor(stream, intervalMs = 80) {
|
|
10
|
+
this.stream = stream;
|
|
11
|
+
this.intervalMs = intervalMs;
|
|
12
|
+
}
|
|
13
|
+
start(message) {
|
|
14
|
+
if (this.activeMessage)
|
|
15
|
+
this.succeed();
|
|
16
|
+
this.activeMessage = message;
|
|
17
|
+
this.frame = 0;
|
|
18
|
+
if (!this.stream.isTTY) {
|
|
19
|
+
this.stream.write(`[..] ${message}\n`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.render(`[${FRAMES[this.frame]}] ${message}`);
|
|
23
|
+
this.timer = setInterval(() => {
|
|
24
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
25
|
+
this.render(`[${FRAMES[this.frame]}] ${this.activeMessage}`);
|
|
26
|
+
}, this.intervalMs);
|
|
27
|
+
this.timer.unref();
|
|
28
|
+
}
|
|
29
|
+
succeed(message = this.activeMessage) {
|
|
30
|
+
this.finish("ok", message);
|
|
31
|
+
}
|
|
32
|
+
fail(message = this.activeMessage) {
|
|
33
|
+
this.finish("!!", message);
|
|
34
|
+
}
|
|
35
|
+
finish(marker, message) {
|
|
36
|
+
if (this.timer)
|
|
37
|
+
clearInterval(this.timer);
|
|
38
|
+
this.timer = undefined;
|
|
39
|
+
this.activeMessage = undefined;
|
|
40
|
+
if (!message)
|
|
41
|
+
return;
|
|
42
|
+
const line = `[${marker}] ${message}`;
|
|
43
|
+
if (this.stream.isTTY) {
|
|
44
|
+
this.render(line);
|
|
45
|
+
this.stream.write("\n");
|
|
46
|
+
this.lastWidth = 0;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.stream.write(`${line}\n`);
|
|
50
|
+
}
|
|
51
|
+
render(value) {
|
|
52
|
+
this.stream.write(`\r${value.padEnd(this.lastWidth)}`);
|
|
53
|
+
this.lastWidth = value.length;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const silentProgress = {
|
|
57
|
+
start: () => { },
|
|
58
|
+
succeed: () => { },
|
|
59
|
+
fail: () => { },
|
|
60
|
+
};
|