@ait-co/console-cli 0.1.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 +90 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +845 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# console-cli
|
|
2
|
+
|
|
3
|
+
> ๐ง **Work in Progress** โ not yet published.
|
|
4
|
+
> ์์ง ๊ฐ๋ฐ ์ค์
๋๋ค. ๋ฆด๋ฆฌ์ค ์ ์
๋๋ค.
|
|
5
|
+
|
|
6
|
+
`ait-console` is a community-maintained CLI for automating Apps in Toss developer console operations โ log in once in a browser, then drive subsequent operations from your shell or from an AI coding agent via headless browser automation.
|
|
7
|
+
|
|
8
|
+
์ฑ์ธํ ์ค ์ฝ์์ CLI๋ก ์๋ํํ๋ ์ปค๋ฎค๋ํฐ ๋๊ตฌ. ์ต์ด ๋ก๊ทธ์ธ๋ง ๋ธ๋ผ์ฐ์ ๋ก ํ๊ณ , ์ดํ ์์
์ headless ๋ธ๋ผ์ฐ์ ๋ก ์ฒ๋ฆฌํ๋ค. (MCP ๋ชจ๋๋ ํ์์ โ [TODO.md](./TODO.md) ์ฐธ๊ณ .)
|
|
9
|
+
|
|
10
|
+
> This is an **unofficial, community-maintained** project. Not affiliated with or endorsed by Toss or the Apps in Toss team. It drives the public developer console from a user's authenticated browser session โ it is **not** a client for a blessed, documented API, and behavior may break whenever the console UI changes.
|
|
11
|
+
>
|
|
12
|
+
> ์ด ํ๋ก์ ํธ๋ **๋น๊ณต์ ์ปค๋ฎค๋ํฐ ํ๋ก์ ํธ**์
๋๋ค. ํ ์ค/์ฑ์ธํ ์ค ํ๊ณผ ์ ํด ๊ด๊ณ๊ฐ ์๋๋๋ค. ๊ณต์ API๋ฅผ ํธ์ถํ์ง ์๊ณ ๋ธ๋ผ์ฐ์ ์ธ์
์ ํตํด ์ฝ์์ ์๋ํํ๋ฏ๋ก, ์ฝ์ UI๊ฐ ๋ฐ๋๋ฉด ๋์์ด ๊นจ์ง ์ ์์ต๋๋ค.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
### Platform binary (primary)
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
curl -fsSL https://raw.githubusercontent.com/apps-in-toss-community/console-cli/main/install.sh | sh
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The installer detects OS (`uname -s`) and arch (`uname -m`), downloads the matching binary from the latest GitHub Release, verifies it against `SHA256SUMS`, and installs it to `$HOME/.local/bin/ait-console`. Node is **not** required.
|
|
23
|
+
|
|
24
|
+
Pin a specific version:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
curl -fsSL https://raw.githubusercontent.com/apps-in-toss-community/console-cli/main/install.sh | AIT_CONSOLE_VERSION=v0.1.1 sh
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Override the install directory with `AIT_CONSOLE_INSTALL_DIR=/custom/path` (default `$HOME/.local/bin`).
|
|
31
|
+
|
|
32
|
+
### npm (fallback)
|
|
33
|
+
|
|
34
|
+
If you already have Node 24+ on your PATH:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm i -g @ait-co/console-cli
|
|
38
|
+
# or: pnpm add -g @ait-co/console-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This is the path that `agent-plugin` uses when a project already has Node installed.
|
|
42
|
+
|
|
43
|
+
## Quick usage
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
ait-console --version # print the embedded version
|
|
47
|
+
ait-console login # open the browser, capture the OAuth callback on localhost, save the session
|
|
48
|
+
ait-console login --no-browser # print the authorize URL instead of auto-opening a browser
|
|
49
|
+
ait-console logout # delete the local session file
|
|
50
|
+
ait-console whoami # show the currently logged-in user (exits non-zero if no session)
|
|
51
|
+
ait-console whoami --json # machine-readable output for scripts and agents
|
|
52
|
+
ait-console upgrade # self-update to the latest GitHub Release (binary installs only)
|
|
53
|
+
ait-console upgrade --dry-run # check for an update without downloading or replacing
|
|
54
|
+
ait-console upgrade --force # reinstall the latest release even if versions match
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`ait-console upgrade` respects `GITHUB_TOKEN` to avoid anonymous GitHub API rate limits.
|
|
58
|
+
|
|
59
|
+
Planned commands โ `deploy`, `logs`, `status` โ are tracked in [TODO.md](./TODO.md).
|
|
60
|
+
|
|
61
|
+
### Login details
|
|
62
|
+
|
|
63
|
+
`ait-console login` spawns a short-lived HTTP server on `127.0.0.1:<random-port>` and waits for the OAuth provider to redirect back to `/callback` with a `code` and a `state` parameter. The `state` is a 32-byte crypto-random value generated per attempt and rechecked on arrival โ any mismatch is rejected with a 400 and the login aborts. The server binds to the loopback interface only, listens for exactly one successful callback, and shuts down after either success or a 5-minute timeout (override with `--timeout <seconds>`).
|
|
64
|
+
|
|
65
|
+
The Apps in Toss developer console OAuth authorize URL is not publicly documented yet (see [CLAUDE.md](./CLAUDE.md) ยง "Open questions"). Until it is, set `AIT_CONSOLE_OAUTH_URL` (and optionally `AIT_CONSOLE_OAUTH_CLIENT_ID` / `AIT_CONSOLE_OAUTH_SCOPE`) to point at the real endpoint; without it, `login` exits with a usage error rather than calling a placeholder.
|
|
66
|
+
|
|
67
|
+
## Session storage
|
|
68
|
+
|
|
69
|
+
The local session lives at an XDG-compliant path with file mode `0600`:
|
|
70
|
+
|
|
71
|
+
- Linux/macOS: `$XDG_CONFIG_HOME/ait-console/session.json` (fallback `~/.config/ait-console/session.json`)
|
|
72
|
+
- Windows: `%APPDATA%\ait-console\session.json`
|
|
73
|
+
|
|
74
|
+
The containing directory is created with mode `0700`. Cookies and storage-state origins captured during login are **never** printed, logged, or attached to `--verbose` output โ only `user.email` and `displayName` surface through `whoami`. Playwright screenshots are off by default.
|
|
75
|
+
|
|
76
|
+
See [CLAUDE.md](./CLAUDE.md) for the rationale behind using a plain `0600` file instead of an OS keychain.
|
|
77
|
+
|
|
78
|
+
## Machine-readable output (`--json`)
|
|
79
|
+
|
|
80
|
+
Every command accepts `--json`. When set:
|
|
81
|
+
|
|
82
|
+
- All normal output goes to stdout as a single JSON document on one line.
|
|
83
|
+
- All diagnostics go to stderr as plain text.
|
|
84
|
+
- Exit codes are meaningful and documented per command (see `src/exit.ts`).
|
|
85
|
+
|
|
86
|
+
`agent-plugin` skills shell out with `--json` exclusively and parse stdout.
|
|
87
|
+
|
|
88
|
+
## Status
|
|
89
|
+
|
|
90
|
+
Scaffold complete. `whoami`, `login`, `logout`, and `upgrade` are implemented (`login` still needs the real Toss OAuth endpoint โ override via `AIT_CONSOLE_OAUTH_URL`); `deploy`, `logs`, `status` are not yet โ see [TODO.md](./TODO.md). See the [organization landing page](https://apps-in-toss-community.github.io/) for the full roadmap.
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
//#region src/browser.ts
|
|
10
|
+
function openBrowser(url) {
|
|
11
|
+
if (process.env.AIT_CONSOLE_NO_BROWSER === "1") return Promise.resolve({ launched: false });
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(url);
|
|
14
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return Promise.resolve({ launched: false });
|
|
15
|
+
} catch {
|
|
16
|
+
return Promise.resolve({ launched: false });
|
|
17
|
+
}
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
try {
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
const child = spawn("cmd", [
|
|
22
|
+
"/c",
|
|
23
|
+
"start",
|
|
24
|
+
"\"\"",
|
|
25
|
+
`"${url.replace(/"/g, "%22")}"`
|
|
26
|
+
], {
|
|
27
|
+
stdio: "ignore",
|
|
28
|
+
detached: true,
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
windowsVerbatimArguments: true
|
|
31
|
+
});
|
|
32
|
+
child.once("error", () => resolve({ launched: false }));
|
|
33
|
+
child.once("spawn", () => {
|
|
34
|
+
child.unref();
|
|
35
|
+
resolve({ launched: true });
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const child = spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], {
|
|
40
|
+
stdio: "ignore",
|
|
41
|
+
detached: true
|
|
42
|
+
});
|
|
43
|
+
child.once("error", () => resolve({ launched: false }));
|
|
44
|
+
child.once("spawn", () => {
|
|
45
|
+
child.unref();
|
|
46
|
+
resolve({ launched: true });
|
|
47
|
+
});
|
|
48
|
+
} catch {
|
|
49
|
+
resolve({ launched: false });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/exit.ts
|
|
55
|
+
const ExitCode = {
|
|
56
|
+
Ok: 0,
|
|
57
|
+
Generic: 1,
|
|
58
|
+
Usage: 2,
|
|
59
|
+
NotAuthenticated: 10,
|
|
60
|
+
NetworkError: 11,
|
|
61
|
+
LoginTimeout: 12,
|
|
62
|
+
LoginStateMismatch: 13,
|
|
63
|
+
UpgradeUnavailable: 20,
|
|
64
|
+
UpgradeAlreadyLatest: 21
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/flush.ts
|
|
68
|
+
async function exitAfterFlush(code) {
|
|
69
|
+
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
70
|
+
process.exit(code);
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/oauth.ts
|
|
74
|
+
var CallbackTimeoutError = class extends Error {
|
|
75
|
+
constructor(seconds) {
|
|
76
|
+
super(`Login timed out after ${seconds}s`);
|
|
77
|
+
this.name = "CallbackTimeoutError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var CallbackStateMismatchError = class extends Error {
|
|
81
|
+
constructor() {
|
|
82
|
+
super("Invalid or missing state parameter");
|
|
83
|
+
this.name = "CallbackStateMismatchError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var CallbackMissingCodeError = class extends Error {
|
|
87
|
+
constructor() {
|
|
88
|
+
super("Missing code parameter");
|
|
89
|
+
this.name = "CallbackMissingCodeError";
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function randomState() {
|
|
93
|
+
return randomBytes(32).toString("base64url");
|
|
94
|
+
}
|
|
95
|
+
function constantTimeStringEqual(a, b) {
|
|
96
|
+
const bufA = Buffer.from(a, "utf8");
|
|
97
|
+
const bufB = Buffer.from(b, "utf8");
|
|
98
|
+
if (bufA.length !== bufB.length) return false;
|
|
99
|
+
return timingSafeEqual(bufA, bufB);
|
|
100
|
+
}
|
|
101
|
+
function escapeHtml(s) {
|
|
102
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "/").replace(/`/g, "`");
|
|
103
|
+
}
|
|
104
|
+
function parseCallbackUrl(reqUrl, expectedState) {
|
|
105
|
+
if (!reqUrl) return { kind: "malformed" };
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = new URL(reqUrl, "http://127.0.0.1");
|
|
109
|
+
} catch {
|
|
110
|
+
return { kind: "malformed" };
|
|
111
|
+
}
|
|
112
|
+
if (parsed.pathname !== "/callback") return { kind: "not-found" };
|
|
113
|
+
const raw = {};
|
|
114
|
+
for (const [k, v] of parsed.searchParams) raw[k] = v;
|
|
115
|
+
const state = raw.state ?? "";
|
|
116
|
+
const code = raw.code ?? "";
|
|
117
|
+
if (!state || !constantTimeStringEqual(state, expectedState)) return { kind: "state-mismatch" };
|
|
118
|
+
if (!code) return { kind: "missing-code" };
|
|
119
|
+
return {
|
|
120
|
+
kind: "ok",
|
|
121
|
+
query: {
|
|
122
|
+
code,
|
|
123
|
+
state,
|
|
124
|
+
raw
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const ERROR_MESSAGES = {
|
|
129
|
+
"state-mismatch": "Invalid or missing state parameter",
|
|
130
|
+
"missing-code": "Missing code parameter",
|
|
131
|
+
malformed: "Malformed request URL",
|
|
132
|
+
"not-found": "Not found"
|
|
133
|
+
};
|
|
134
|
+
const ERROR_STATUS = {
|
|
135
|
+
"state-mismatch": 400,
|
|
136
|
+
"missing-code": 400,
|
|
137
|
+
malformed: 400,
|
|
138
|
+
"not-found": 404
|
|
139
|
+
};
|
|
140
|
+
const SUCCESS_HTML = `<!doctype html>
|
|
141
|
+
<html lang="en">
|
|
142
|
+
<head><meta charset="utf-8"><title>ait-console</title>
|
|
143
|
+
<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.25rem}</style>
|
|
144
|
+
</head>
|
|
145
|
+
<body>
|
|
146
|
+
<h1>Logged in to ait-console</h1>
|
|
147
|
+
<p>You can close this window and return to your terminal.</p>
|
|
148
|
+
</body></html>`;
|
|
149
|
+
const GONE_HTML = `<!doctype html>
|
|
150
|
+
<html lang="en">
|
|
151
|
+
<head><meta charset="utf-8"><title>ait-console</title>
|
|
152
|
+
<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.25rem}</style>
|
|
153
|
+
</head>
|
|
154
|
+
<body>
|
|
155
|
+
<h1>This login flow is already complete</h1>
|
|
156
|
+
<p>Return to your terminal.</p>
|
|
157
|
+
</body></html>`;
|
|
158
|
+
function errorHtml(message) {
|
|
159
|
+
return `<!doctype html>
|
|
160
|
+
<html lang="en">
|
|
161
|
+
<head><meta charset="utf-8"><title>ait-console โ error</title>
|
|
162
|
+
<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.25rem;color:#b00020}</style>
|
|
163
|
+
</head>
|
|
164
|
+
<body>
|
|
165
|
+
<h1>Login failed</h1>
|
|
166
|
+
<p>${escapeHtml(message)}</p>
|
|
167
|
+
<p>Return to your terminal for details.</p>
|
|
168
|
+
</body></html>`;
|
|
169
|
+
}
|
|
170
|
+
async function bindServer(server, preferredPort) {
|
|
171
|
+
const tryListen = (port) => new Promise((resolve, reject) => {
|
|
172
|
+
const onError = (err) => {
|
|
173
|
+
server.removeListener("error", onError);
|
|
174
|
+
reject(err);
|
|
175
|
+
};
|
|
176
|
+
server.once("error", onError);
|
|
177
|
+
server.listen(port, "127.0.0.1", () => {
|
|
178
|
+
server.removeListener("error", onError);
|
|
179
|
+
const addr = server.address();
|
|
180
|
+
if (!addr) reject(/* @__PURE__ */ new Error("Failed to bind callback server"));
|
|
181
|
+
else resolve(addr.port);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
if (preferredPort && preferredPort !== 0) try {
|
|
185
|
+
return await tryListen(preferredPort);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (err.code !== "EADDRINUSE") throw err;
|
|
188
|
+
}
|
|
189
|
+
return tryListen(0);
|
|
190
|
+
}
|
|
191
|
+
async function startCallbackServer(options = {}) {
|
|
192
|
+
const timeoutMs = options.timeoutMs ?? 300 * 1e3;
|
|
193
|
+
const expectedState = randomState();
|
|
194
|
+
const server = createServer();
|
|
195
|
+
const boundPort = await bindServer(server, options.preferredPort);
|
|
196
|
+
const redirectUri = `http://127.0.0.1:${boundPort}/callback`;
|
|
197
|
+
let settled = false;
|
|
198
|
+
let closed = false;
|
|
199
|
+
let resolveCb;
|
|
200
|
+
let rejectCb;
|
|
201
|
+
const waiter = new Promise((resolve, reject) => {
|
|
202
|
+
resolveCb = resolve;
|
|
203
|
+
rejectCb = reject;
|
|
204
|
+
});
|
|
205
|
+
waiter.catch(() => {});
|
|
206
|
+
const finish = (outcome) => {
|
|
207
|
+
if (settled) return;
|
|
208
|
+
settled = true;
|
|
209
|
+
if (outcome.kind === "ok") resolveCb(outcome.q);
|
|
210
|
+
else rejectCb(outcome.e);
|
|
211
|
+
};
|
|
212
|
+
server.on("request", (req, res) => {
|
|
213
|
+
if (settled) {
|
|
214
|
+
res.statusCode = 410;
|
|
215
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
216
|
+
res.end(GONE_HTML);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const result = parseCallbackUrl(req.url, expectedState);
|
|
220
|
+
if (result.kind === "ok") {
|
|
221
|
+
res.statusCode = 200;
|
|
222
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
223
|
+
res.end(SUCCESS_HTML, () => finish({
|
|
224
|
+
kind: "ok",
|
|
225
|
+
q: result.query
|
|
226
|
+
}));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const status = ERROR_STATUS[result.kind];
|
|
230
|
+
const message = ERROR_MESSAGES[result.kind];
|
|
231
|
+
res.statusCode = status;
|
|
232
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
233
|
+
const onFlushed = () => {
|
|
234
|
+
switch (result.kind) {
|
|
235
|
+
case "state-mismatch":
|
|
236
|
+
finish({
|
|
237
|
+
kind: "err",
|
|
238
|
+
e: new CallbackStateMismatchError()
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
case "missing-code":
|
|
242
|
+
finish({
|
|
243
|
+
kind: "err",
|
|
244
|
+
e: new CallbackMissingCodeError()
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
case "malformed":
|
|
248
|
+
case "not-found": return;
|
|
249
|
+
default: ((_) => {})(result);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
res.end(errorHtml(message), onFlushed);
|
|
253
|
+
});
|
|
254
|
+
const timer = setTimeout(() => {
|
|
255
|
+
finish({
|
|
256
|
+
kind: "err",
|
|
257
|
+
e: new CallbackTimeoutError(Math.ceil(timeoutMs / 1e3))
|
|
258
|
+
});
|
|
259
|
+
}, timeoutMs);
|
|
260
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
261
|
+
const close = async () => {
|
|
262
|
+
if (closed) return;
|
|
263
|
+
closed = true;
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
await new Promise((resolve) => {
|
|
266
|
+
let done = false;
|
|
267
|
+
const finishClose = () => {
|
|
268
|
+
if (done) return;
|
|
269
|
+
done = true;
|
|
270
|
+
resolve();
|
|
271
|
+
};
|
|
272
|
+
server.close(() => finishClose());
|
|
273
|
+
server.closeAllConnections?.();
|
|
274
|
+
const fallback = setTimeout(finishClose, 1e3);
|
|
275
|
+
if (typeof fallback.unref === "function") fallback.unref();
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
port: boundPort,
|
|
280
|
+
redirectUri,
|
|
281
|
+
expectedState,
|
|
282
|
+
waitForCallback: () => waiter,
|
|
283
|
+
close
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/paths.ts
|
|
288
|
+
const APP_NAME = "ait-console";
|
|
289
|
+
function configDir() {
|
|
290
|
+
if (process.platform === "win32") {
|
|
291
|
+
const appData = process.env.APPDATA;
|
|
292
|
+
if (appData && appData.length > 0) return join(appData, APP_NAME);
|
|
293
|
+
return join(homedir() || ".", "AppData", "Roaming", APP_NAME);
|
|
294
|
+
}
|
|
295
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
296
|
+
if (xdg && xdg.length > 0) return join(xdg, APP_NAME);
|
|
297
|
+
return join(homedir() || ".", ".config", APP_NAME);
|
|
298
|
+
}
|
|
299
|
+
function sessionFilePath() {
|
|
300
|
+
return join(configDir(), "session.json");
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/session.ts
|
|
304
|
+
function summarize(session) {
|
|
305
|
+
return {
|
|
306
|
+
user: session.user,
|
|
307
|
+
capturedAt: session.capturedAt
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async function readSession() {
|
|
311
|
+
try {
|
|
312
|
+
const raw = await readFile(sessionFilePath(), "utf8");
|
|
313
|
+
const parsed = JSON.parse(raw);
|
|
314
|
+
if (parsed.schemaVersion !== 1) return null;
|
|
315
|
+
if (!parsed.user || typeof parsed.user.id !== "string") return null;
|
|
316
|
+
if (typeof parsed.user.email !== "string") return null;
|
|
317
|
+
if (parsed.user.displayName !== void 0 && typeof parsed.user.displayName !== "string") return null;
|
|
318
|
+
return parsed;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
if (err.code === "ENOENT") return null;
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function readSessionSummary() {
|
|
325
|
+
const s = await readSession();
|
|
326
|
+
return s ? summarize(s) : null;
|
|
327
|
+
}
|
|
328
|
+
async function writeSession(session) {
|
|
329
|
+
await mkdir(dirname(sessionFilePath()), {
|
|
330
|
+
recursive: true,
|
|
331
|
+
mode: 448
|
|
332
|
+
});
|
|
333
|
+
await writeFile(sessionFilePath(), JSON.stringify(session, null, 2), { mode: 384 });
|
|
334
|
+
try {
|
|
335
|
+
await chmod(sessionFilePath(), 384);
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
async function clearSession() {
|
|
339
|
+
try {
|
|
340
|
+
await unlink(sessionFilePath());
|
|
341
|
+
return { existed: true };
|
|
342
|
+
} catch (err) {
|
|
343
|
+
if (err.code === "ENOENT") return { existed: false };
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function sessionPathForDiagnostics() {
|
|
348
|
+
return sessionFilePath();
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/commands/login.ts
|
|
352
|
+
const MAX_FIELD_LENGTH = 128;
|
|
353
|
+
const PENDING_USER_ID = "pending:oauth-discovery";
|
|
354
|
+
function sanitizeField(raw) {
|
|
355
|
+
if (typeof raw !== "string") return void 0;
|
|
356
|
+
if (raw.length === 0) return void 0;
|
|
357
|
+
if (raw.length > MAX_FIELD_LENGTH) return void 0;
|
|
358
|
+
if (/[\x00-\x1f\x7f]/.test(raw)) return void 0;
|
|
359
|
+
return raw;
|
|
360
|
+
}
|
|
361
|
+
function buildAuthorizeUrl(params) {
|
|
362
|
+
const url = new URL(params.authorizeUrl);
|
|
363
|
+
url.searchParams.set("response_type", "code");
|
|
364
|
+
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
365
|
+
url.searchParams.set("state", params.state);
|
|
366
|
+
if (params.clientId) url.searchParams.set("client_id", params.clientId);
|
|
367
|
+
if (params.scope) url.searchParams.set("scope", params.scope);
|
|
368
|
+
return url.toString();
|
|
369
|
+
}
|
|
370
|
+
function classifyCallbackError(err) {
|
|
371
|
+
if (err instanceof CallbackTimeoutError) return {
|
|
372
|
+
reason: "timeout",
|
|
373
|
+
exitCode: ExitCode.LoginTimeout
|
|
374
|
+
};
|
|
375
|
+
if (err instanceof CallbackStateMismatchError) return {
|
|
376
|
+
reason: "state-mismatch",
|
|
377
|
+
exitCode: ExitCode.LoginStateMismatch
|
|
378
|
+
};
|
|
379
|
+
if (err instanceof CallbackMissingCodeError) return {
|
|
380
|
+
reason: "missing-code",
|
|
381
|
+
exitCode: ExitCode.Generic
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
reason: "other",
|
|
385
|
+
exitCode: ExitCode.Generic
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const loginCommand = defineCommand({
|
|
389
|
+
meta: {
|
|
390
|
+
name: "login",
|
|
391
|
+
description: "Log in via the browser; starts a localhost callback server."
|
|
392
|
+
},
|
|
393
|
+
args: {
|
|
394
|
+
json: {
|
|
395
|
+
type: "boolean",
|
|
396
|
+
description: "Emit machine-readable JSON to stdout.",
|
|
397
|
+
default: false
|
|
398
|
+
},
|
|
399
|
+
"no-browser": {
|
|
400
|
+
type: "boolean",
|
|
401
|
+
description: "Don't auto-open the browser; print the URL for manual copy.",
|
|
402
|
+
default: false
|
|
403
|
+
},
|
|
404
|
+
timeout: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "Abort the login if no callback arrives within N seconds (default 300).",
|
|
407
|
+
default: "300"
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
async run({ args }) {
|
|
411
|
+
const rawOauthUrl = process.env.AIT_CONSOLE_OAUTH_URL;
|
|
412
|
+
const authorizeUrl = rawOauthUrl && rawOauthUrl.length > 0 ? rawOauthUrl : null;
|
|
413
|
+
const clientId = process.env.AIT_CONSOLE_OAUTH_CLIENT_ID;
|
|
414
|
+
const scope = process.env.AIT_CONSOLE_OAUTH_SCOPE;
|
|
415
|
+
const emitError = (payload, human) => {
|
|
416
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
417
|
+
ok: false,
|
|
418
|
+
...payload
|
|
419
|
+
})}\n`);
|
|
420
|
+
process.stderr.write(`${human}\n`);
|
|
421
|
+
};
|
|
422
|
+
const timeoutNum = Number(args.timeout);
|
|
423
|
+
if (!Number.isFinite(timeoutNum) || timeoutNum < 1) {
|
|
424
|
+
emitError({
|
|
425
|
+
reason: "invalid-timeout",
|
|
426
|
+
given: args.timeout
|
|
427
|
+
}, `Invalid --timeout value: ${args.timeout}`);
|
|
428
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
429
|
+
}
|
|
430
|
+
const timeoutMs = timeoutNum * 1e3;
|
|
431
|
+
if (!authorizeUrl) {
|
|
432
|
+
emitError({
|
|
433
|
+
reason: "oauth-url-not-configured",
|
|
434
|
+
hint: "set AIT_CONSOLE_OAUTH_URL"
|
|
435
|
+
}, [
|
|
436
|
+
"The Toss developer console OAuth URL is not configured.",
|
|
437
|
+
"Discovery is pending โ set AIT_CONSOLE_OAUTH_URL to override,",
|
|
438
|
+
"or track the TODO in CLAUDE.md ยง \"Open questions\"."
|
|
439
|
+
].join("\n"));
|
|
440
|
+
return exitAfterFlush(ExitCode.Usage);
|
|
441
|
+
}
|
|
442
|
+
const server = await startCallbackServer({ timeoutMs });
|
|
443
|
+
const authUrl = buildAuthorizeUrl({
|
|
444
|
+
authorizeUrl,
|
|
445
|
+
redirectUri: server.redirectUri,
|
|
446
|
+
state: server.expectedState,
|
|
447
|
+
clientId,
|
|
448
|
+
scope
|
|
449
|
+
});
|
|
450
|
+
process.stderr.write(`Listening for the OAuth callback on ${server.redirectUri}\n`);
|
|
451
|
+
let launched = false;
|
|
452
|
+
if (!args["no-browser"]) launched = (await openBrowser(authUrl)).launched;
|
|
453
|
+
if (launched) process.stderr.write("Opened your browser. Complete the login there.\n");
|
|
454
|
+
else process.stderr.write(`Open this URL in your browser to continue:\n ${authUrl}\n`);
|
|
455
|
+
let query;
|
|
456
|
+
try {
|
|
457
|
+
try {
|
|
458
|
+
query = await server.waitForCallback();
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const { reason, exitCode } = classifyCallbackError(err);
|
|
461
|
+
emitError({
|
|
462
|
+
reason,
|
|
463
|
+
message: err.message
|
|
464
|
+
}, `Login failed: ${err.message}`);
|
|
465
|
+
return exitAfterFlush(exitCode);
|
|
466
|
+
}
|
|
467
|
+
} finally {
|
|
468
|
+
await server.close();
|
|
469
|
+
}
|
|
470
|
+
const rawUserId = sanitizeField(query.raw.user_id);
|
|
471
|
+
const rawEmail = sanitizeField(query.raw.email);
|
|
472
|
+
const displayName = sanitizeField(query.raw.display_name);
|
|
473
|
+
if (!rawUserId && !rawEmail) {
|
|
474
|
+
emitError({ reason: "oauth-identity-missing" }, [
|
|
475
|
+
"The callback did not carry user_id or email.",
|
|
476
|
+
"This is expected until Toss console OAuth discovery lands.",
|
|
477
|
+
"No session was written."
|
|
478
|
+
].join("\n"));
|
|
479
|
+
return exitAfterFlush(ExitCode.Generic);
|
|
480
|
+
}
|
|
481
|
+
const userId = rawUserId ?? PENDING_USER_ID;
|
|
482
|
+
const email = rawEmail ?? "";
|
|
483
|
+
const session = {
|
|
484
|
+
schemaVersion: 1,
|
|
485
|
+
user: displayName ? {
|
|
486
|
+
id: userId,
|
|
487
|
+
email,
|
|
488
|
+
displayName
|
|
489
|
+
} : {
|
|
490
|
+
id: userId,
|
|
491
|
+
email
|
|
492
|
+
},
|
|
493
|
+
cookies: [],
|
|
494
|
+
origins: [],
|
|
495
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
496
|
+
};
|
|
497
|
+
try {
|
|
498
|
+
await writeSession(session);
|
|
499
|
+
} catch (err) {
|
|
500
|
+
emitError({
|
|
501
|
+
reason: "session-write-failed",
|
|
502
|
+
message: err.message
|
|
503
|
+
}, `Failed to write session file: ${err.message}`);
|
|
504
|
+
return exitAfterFlush(ExitCode.Generic);
|
|
505
|
+
}
|
|
506
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
507
|
+
ok: true,
|
|
508
|
+
status: "logged-in",
|
|
509
|
+
user: session.user,
|
|
510
|
+
capturedAt: session.capturedAt
|
|
511
|
+
})}\n`);
|
|
512
|
+
else {
|
|
513
|
+
const label = displayName ? `${displayName} <${email}>` : email || userId;
|
|
514
|
+
process.stdout.write(`Logged in as ${label}\n`);
|
|
515
|
+
}
|
|
516
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/commands/logout.ts
|
|
521
|
+
const logoutCommand = defineCommand({
|
|
522
|
+
meta: {
|
|
523
|
+
name: "logout",
|
|
524
|
+
description: "Delete the local session file."
|
|
525
|
+
},
|
|
526
|
+
args: { json: {
|
|
527
|
+
type: "boolean",
|
|
528
|
+
description: "Emit machine-readable JSON to stdout.",
|
|
529
|
+
default: false
|
|
530
|
+
} },
|
|
531
|
+
async run({ args }) {
|
|
532
|
+
const path = sessionPathForDiagnostics();
|
|
533
|
+
let existed;
|
|
534
|
+
try {
|
|
535
|
+
existed = (await clearSession()).existed;
|
|
536
|
+
} catch (err) {
|
|
537
|
+
const message = err.message;
|
|
538
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
539
|
+
ok: false,
|
|
540
|
+
reason: "unlink-failed",
|
|
541
|
+
path,
|
|
542
|
+
message
|
|
543
|
+
})}\n`);
|
|
544
|
+
process.stderr.write(`Failed to remove session file at ${path}: ${message}\n`);
|
|
545
|
+
return exitAfterFlush(ExitCode.Generic);
|
|
546
|
+
}
|
|
547
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
548
|
+
ok: true,
|
|
549
|
+
status: existed ? "logged-out" : "no-session",
|
|
550
|
+
path
|
|
551
|
+
})}\n`);
|
|
552
|
+
else if (existed) process.stdout.write(`Logged out. Session removed from ${path}\n`);
|
|
553
|
+
else process.stdout.write(`No active session at ${path}.\n`);
|
|
554
|
+
return exitAfterFlush(ExitCode.Ok);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/github.ts
|
|
559
|
+
const REPO_OWNER = "apps-in-toss-community";
|
|
560
|
+
const REPO_NAME = "console-cli";
|
|
561
|
+
function defaultHeaders() {
|
|
562
|
+
const headers = {
|
|
563
|
+
Accept: "application/vnd.github+json",
|
|
564
|
+
"User-Agent": "ait-console",
|
|
565
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
566
|
+
};
|
|
567
|
+
const token = process.env.GITHUB_TOKEN;
|
|
568
|
+
if (token && token.length > 0) headers.Authorization = `Bearer ${token}`;
|
|
569
|
+
return headers;
|
|
570
|
+
}
|
|
571
|
+
async function fetchLatestRelease() {
|
|
572
|
+
const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;
|
|
573
|
+
const res = await fetch(url, { headers: defaultHeaders() });
|
|
574
|
+
if (!res.ok) throw new Error(`GitHub releases/latest returned ${res.status} ${res.statusText}`);
|
|
575
|
+
return await res.json();
|
|
576
|
+
}
|
|
577
|
+
function versionFromTag(tag) {
|
|
578
|
+
const at = tag.lastIndexOf("@");
|
|
579
|
+
const candidate = at >= 0 ? tag.slice(at + 1) : tag;
|
|
580
|
+
return candidate.startsWith("v") ? candidate.slice(1) : candidate;
|
|
581
|
+
}
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/platform.ts
|
|
584
|
+
function detectPlatform() {
|
|
585
|
+
let os;
|
|
586
|
+
switch (process.platform) {
|
|
587
|
+
case "linux":
|
|
588
|
+
os = "linux";
|
|
589
|
+
break;
|
|
590
|
+
case "darwin":
|
|
591
|
+
os = "darwin";
|
|
592
|
+
break;
|
|
593
|
+
case "win32":
|
|
594
|
+
os = "windows";
|
|
595
|
+
break;
|
|
596
|
+
default: return null;
|
|
597
|
+
}
|
|
598
|
+
let arch;
|
|
599
|
+
switch (process.arch) {
|
|
600
|
+
case "x64":
|
|
601
|
+
arch = "x64";
|
|
602
|
+
break;
|
|
603
|
+
case "arm64":
|
|
604
|
+
arch = "arm64";
|
|
605
|
+
break;
|
|
606
|
+
default: return null;
|
|
607
|
+
}
|
|
608
|
+
if (os === "windows" && arch === "arm64") return null;
|
|
609
|
+
return {
|
|
610
|
+
os,
|
|
611
|
+
arch,
|
|
612
|
+
assetName: `ait-console-${os}-${arch}${os === "windows" ? ".exe" : ""}`
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/semver.ts
|
|
617
|
+
function parseSemver(v) {
|
|
618
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v);
|
|
619
|
+
if (!m) return null;
|
|
620
|
+
return {
|
|
621
|
+
major: +m[1],
|
|
622
|
+
minor: +m[2],
|
|
623
|
+
patch: +m[3],
|
|
624
|
+
pre: m[4] ?? ""
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function compareSemver(a, b) {
|
|
628
|
+
const pa = parseSemver(a);
|
|
629
|
+
const pb = parseSemver(b);
|
|
630
|
+
if (!pa || !pb) return 0;
|
|
631
|
+
if (pa.major !== pb.major) return pa.major > pb.major ? 1 : -1;
|
|
632
|
+
if (pa.minor !== pb.minor) return pa.minor > pb.minor ? 1 : -1;
|
|
633
|
+
if (pa.patch !== pb.patch) return pa.patch > pb.patch ? 1 : -1;
|
|
634
|
+
if (pa.pre === pb.pre) return 0;
|
|
635
|
+
if (pa.pre === "") return 1;
|
|
636
|
+
if (pb.pre === "") return -1;
|
|
637
|
+
return pa.pre > pb.pre ? 1 : -1;
|
|
638
|
+
}
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/version.ts
|
|
641
|
+
function resolveVersion() {
|
|
642
|
+
try {
|
|
643
|
+
const injected = globalThis.AIT_CONSOLE_VERSION;
|
|
644
|
+
if (typeof injected === "string" && injected.length > 0) return injected;
|
|
645
|
+
} catch {}
|
|
646
|
+
try {
|
|
647
|
+
return "0.1.0";
|
|
648
|
+
} catch {}
|
|
649
|
+
return "0.0.0-dev";
|
|
650
|
+
}
|
|
651
|
+
const VERSION = resolveVersion();
|
|
652
|
+
//#endregion
|
|
653
|
+
//#region src/commands/upgrade.ts
|
|
654
|
+
function isStandaloneBinary() {
|
|
655
|
+
return basename(process.execPath).toLowerCase().startsWith("ait-console");
|
|
656
|
+
}
|
|
657
|
+
const upgradeCommand = defineCommand({
|
|
658
|
+
meta: {
|
|
659
|
+
name: "upgrade",
|
|
660
|
+
description: "Download the latest release binary from GitHub and replace the current one."
|
|
661
|
+
},
|
|
662
|
+
args: {
|
|
663
|
+
json: {
|
|
664
|
+
type: "boolean",
|
|
665
|
+
description: "Emit machine-readable JSON to stdout.",
|
|
666
|
+
default: false
|
|
667
|
+
},
|
|
668
|
+
force: {
|
|
669
|
+
type: "boolean",
|
|
670
|
+
description: "Re-install even if already on the latest version.",
|
|
671
|
+
default: false
|
|
672
|
+
},
|
|
673
|
+
"dry-run": {
|
|
674
|
+
type: "boolean",
|
|
675
|
+
description: "Check for updates without downloading or replacing.",
|
|
676
|
+
default: false
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
async run({ args }) {
|
|
680
|
+
const emit = (payload, human) => {
|
|
681
|
+
if (args.json) process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
682
|
+
else process.stdout.write(`${human}\n`);
|
|
683
|
+
};
|
|
684
|
+
const emitError = (payload, human) => {
|
|
685
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
686
|
+
ok: false,
|
|
687
|
+
...payload
|
|
688
|
+
})}\n`);
|
|
689
|
+
else process.stderr.write(`${human}\n`);
|
|
690
|
+
};
|
|
691
|
+
let release;
|
|
692
|
+
try {
|
|
693
|
+
release = await fetchLatestRelease();
|
|
694
|
+
} catch (err) {
|
|
695
|
+
emitError({
|
|
696
|
+
reason: "network-error",
|
|
697
|
+
message: err.message
|
|
698
|
+
}, `Failed to query GitHub releases: ${err.message}`);
|
|
699
|
+
process.exit(ExitCode.NetworkError);
|
|
700
|
+
}
|
|
701
|
+
const latest = versionFromTag(release.tag_name);
|
|
702
|
+
const current = VERSION;
|
|
703
|
+
if (!(compareSemver(latest, current) > 0 || args.force)) {
|
|
704
|
+
emit({
|
|
705
|
+
ok: true,
|
|
706
|
+
status: "already-latest",
|
|
707
|
+
current,
|
|
708
|
+
latest
|
|
709
|
+
}, `Already on the latest version (${current}).`);
|
|
710
|
+
process.exit(ExitCode.UpgradeAlreadyLatest);
|
|
711
|
+
}
|
|
712
|
+
if (args["dry-run"]) {
|
|
713
|
+
emit({
|
|
714
|
+
ok: true,
|
|
715
|
+
status: "update-available",
|
|
716
|
+
current,
|
|
717
|
+
latest,
|
|
718
|
+
url: release.html_url
|
|
719
|
+
}, `Update available: ${current} โ ${latest}\n${release.html_url}`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (!isStandaloneBinary()) {
|
|
723
|
+
emitError({
|
|
724
|
+
reason: "not-standalone",
|
|
725
|
+
current,
|
|
726
|
+
latest,
|
|
727
|
+
hint: "npm i -g @ait-co/console-cli@latest"
|
|
728
|
+
}, [
|
|
729
|
+
"This install was launched via Node, not the standalone binary.",
|
|
730
|
+
"Self-upgrade is only supported for the compiled binary.",
|
|
731
|
+
`Run: npm i -g @ait-co/console-cli@latest (currently ${current}, latest ${latest})`
|
|
732
|
+
].join("\n"));
|
|
733
|
+
process.exit(ExitCode.UpgradeUnavailable);
|
|
734
|
+
}
|
|
735
|
+
const platform = detectPlatform();
|
|
736
|
+
if (!platform) {
|
|
737
|
+
emitError({
|
|
738
|
+
reason: "unsupported-platform",
|
|
739
|
+
platform: process.platform,
|
|
740
|
+
arch: process.arch
|
|
741
|
+
}, `No prebuilt binary for ${process.platform}/${process.arch}.`);
|
|
742
|
+
process.exit(ExitCode.UpgradeUnavailable);
|
|
743
|
+
}
|
|
744
|
+
const asset = release.assets.find((a) => a.name === platform.assetName);
|
|
745
|
+
if (!asset) {
|
|
746
|
+
emitError({
|
|
747
|
+
reason: "asset-missing",
|
|
748
|
+
assetName: platform.assetName,
|
|
749
|
+
tag: release.tag_name
|
|
750
|
+
}, `Release ${release.tag_name} has no asset named ${platform.assetName}. It may still be uploading.`);
|
|
751
|
+
process.exit(ExitCode.UpgradeUnavailable);
|
|
752
|
+
}
|
|
753
|
+
const exePath = process.execPath;
|
|
754
|
+
const stagingPath = `${exePath}.new.${Date.now()}`;
|
|
755
|
+
if (!args.json) process.stdout.write(`Downloading ${asset.name} (${latest})...\n`);
|
|
756
|
+
try {
|
|
757
|
+
const res = await fetch(asset.browser_download_url);
|
|
758
|
+
if (!res.ok || !res.body) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
759
|
+
await writeFile(stagingPath, new Uint8Array(await res.arrayBuffer()), { mode: 493 });
|
|
760
|
+
await chmod(stagingPath, 493);
|
|
761
|
+
} catch (err) {
|
|
762
|
+
emitError({
|
|
763
|
+
reason: "download-failed",
|
|
764
|
+
message: err.message
|
|
765
|
+
}, `Failed to download new binary: ${err.message}`);
|
|
766
|
+
process.exit(ExitCode.NetworkError);
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
if (process.platform === "win32") {
|
|
770
|
+
await rename(exePath, `${exePath}.old`);
|
|
771
|
+
await rename(stagingPath, exePath);
|
|
772
|
+
} else await rename(stagingPath, exePath);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
emitError({
|
|
775
|
+
reason: "replace-failed",
|
|
776
|
+
message: err.message,
|
|
777
|
+
exePath,
|
|
778
|
+
stagingPath
|
|
779
|
+
}, `Failed to replace binary at ${exePath}: ${err.message}`);
|
|
780
|
+
process.exit(ExitCode.Generic);
|
|
781
|
+
}
|
|
782
|
+
emit({
|
|
783
|
+
ok: true,
|
|
784
|
+
status: "upgraded",
|
|
785
|
+
from: current,
|
|
786
|
+
to: latest,
|
|
787
|
+
installedAt: exePath,
|
|
788
|
+
installedIn: dirname(exePath)
|
|
789
|
+
}, `Upgraded ait-console: ${current} โ ${latest}`);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/commands/whoami.ts
|
|
794
|
+
const whoamiCommand = defineCommand({
|
|
795
|
+
meta: {
|
|
796
|
+
name: "whoami",
|
|
797
|
+
description: "Show the currently authenticated user from the local session."
|
|
798
|
+
},
|
|
799
|
+
args: { json: {
|
|
800
|
+
type: "boolean",
|
|
801
|
+
description: "Emit machine-readable JSON to stdout.",
|
|
802
|
+
default: false
|
|
803
|
+
} },
|
|
804
|
+
async run({ args }) {
|
|
805
|
+
const summary = await readSessionSummary();
|
|
806
|
+
if (!summary) {
|
|
807
|
+
if (args.json) process.stdout.write(`${JSON.stringify({ authenticated: false })}\n`);
|
|
808
|
+
else {
|
|
809
|
+
process.stderr.write("Not logged in. Run `ait-console login` to start a session.\n");
|
|
810
|
+
process.stderr.write(`Session file checked: ${sessionPathForDiagnostics()}\n`);
|
|
811
|
+
}
|
|
812
|
+
process.exit(ExitCode.NotAuthenticated);
|
|
813
|
+
}
|
|
814
|
+
if (args.json) {
|
|
815
|
+
process.stdout.write(`${JSON.stringify({
|
|
816
|
+
authenticated: true,
|
|
817
|
+
user: summary.user,
|
|
818
|
+
capturedAt: summary.capturedAt
|
|
819
|
+
})}\n`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const label = summary.user.displayName ? `${summary.user.displayName} <${summary.user.email}>` : summary.user.email;
|
|
823
|
+
process.stdout.write(`Logged in as ${label}\n`);
|
|
824
|
+
process.stdout.write(`Session captured: ${summary.capturedAt}\n`);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/cli.ts
|
|
829
|
+
runMain(defineCommand({
|
|
830
|
+
meta: {
|
|
831
|
+
name: "ait-console",
|
|
832
|
+
version: VERSION,
|
|
833
|
+
description: "Community CLI for the Apps in Toss developer console (unofficial; not affiliated with Toss)."
|
|
834
|
+
},
|
|
835
|
+
subCommands: {
|
|
836
|
+
whoami: whoamiCommand,
|
|
837
|
+
login: loginCommand,
|
|
838
|
+
logout: logoutCommand,
|
|
839
|
+
upgrade: upgradeCommand
|
|
840
|
+
}
|
|
841
|
+
}));
|
|
842
|
+
//#endregion
|
|
843
|
+
export {};
|
|
844
|
+
|
|
845
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/browser.ts","../src/exit.ts","../src/flush.ts","../src/oauth.ts","../src/paths.ts","../src/session.ts","../src/commands/login.ts","../src/commands/logout.ts","../src/github.ts","../src/platform.ts","../src/semver.ts","../src/version.ts","../src/commands/upgrade.ts","../src/commands/whoami.ts","../src/cli.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\n\n// Best-effort cross-platform \"open this URL in the default browser\". On\n// failure the caller prints the URL so the user can copy it manually. We\n// deliberately avoid pulling in an `open`-package dependency โ the matrix we\n// care about (macOS / Linux / Windows) is tiny.\n\nexport interface OpenBrowserResult {\n readonly launched: boolean;\n}\n\nexport function openBrowser(url: string): Promise<OpenBrowserResult> {\n // Allow tests and headless environments to skip the spawn entirely.\n if (process.env.AIT_CONSOLE_NO_BROWSER === '1') {\n return Promise.resolve({ launched: false });\n }\n\n // Refuse anything that isn't a plain http(s) URL โ belt-and-suspenders\n // against accidentally feeding `file://`, `javascript:`, or a shell\n // metacharacter-laden input into the platform opener.\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return Promise.resolve({ launched: false });\n }\n } catch {\n return Promise.resolve({ launched: false });\n }\n\n return new Promise((resolve) => {\n try {\n if (process.platform === 'win32') {\n // `cmd /c start` needs the `\"\"` window-title placeholder so a URL\n // containing `&` isn't reinterpreted. `windowsVerbatimArguments`\n // keeps Node from re-quoting our already-quoted arguments, which\n // would otherwise corrupt URLs with special characters. We also\n // wrap the URL in double-quotes so an intervening literal space\n // (rare but legal through redirects/proxies) is treated as part\n // of the argument.\n const quotedUrl = `\"${url.replace(/\"/g, '%22')}\"`;\n const child = spawn('cmd', ['/c', 'start', '\"\"', quotedUrl], {\n stdio: 'ignore',\n detached: true,\n windowsHide: true,\n windowsVerbatimArguments: true,\n });\n child.once('error', () => resolve({ launched: false }));\n child.once('spawn', () => {\n child.unref();\n resolve({ launched: true });\n });\n return;\n }\n const command = process.platform === 'darwin' ? 'open' : 'xdg-open';\n const child = spawn(command, [url], {\n stdio: 'ignore',\n detached: true,\n });\n child.once('error', () => resolve({ launched: false }));\n child.once('spawn', () => {\n child.unref();\n resolve({ launched: true });\n });\n } catch {\n resolve({ launched: false });\n }\n });\n}\n","// Centralized exit codes so every command and the agent-plugin side agree.\n\nexport const ExitCode = {\n Ok: 0,\n Generic: 1,\n Usage: 2,\n NotAuthenticated: 10,\n NetworkError: 11,\n LoginTimeout: 12,\n LoginStateMismatch: 13,\n UpgradeUnavailable: 20,\n UpgradeAlreadyLatest: 21,\n} as const;\n\nexport type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];\n","// Flush-safe exit: drain stdout before calling `process.exit` so a piped\n// consumer never loses the final JSON line. Callers typically write the\n// JSON payload (or plain-text result) to stdout immediately before\n// calling `return exitAfterFlush(code)`.\n\nexport async function exitAfterFlush(code: number): Promise<never> {\n await new Promise<void>((resolve) => process.stdout.write('', () => resolve()));\n process.exit(code);\n}\n","import { randomBytes, timingSafeEqual } from 'node:crypto';\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\n\n// Localhost OAuth callback server. Binds to 127.0.0.1 on an ephemeral port,\n// waits for exactly one request to `/callback`, validates the `state`\n// parameter, and resolves with the query fields. The server is single-use โ\n// subsequent requests receive 410 Gone and the server closes on settle.\n//\n// The Toss developer console OAuth URL and token-exchange flow are not\n// publicly documented (see CLAUDE.md ยง \"Open questions\"), so this module is\n// deliberately generic: it only knows how to receive a redirect, not how to\n// shape the outbound authorize URL. The caller composes that URL.\n\nexport interface CallbackQuery {\n readonly code: string;\n readonly state: string;\n readonly raw: Record<string, string>;\n}\n\nexport interface CallbackServer {\n readonly port: number;\n readonly redirectUri: string;\n readonly expectedState: string;\n waitForCallback(): Promise<CallbackQuery>;\n close(): Promise<void>;\n}\n\nexport interface StartCallbackServerOptions {\n // Overall timeout for waitForCallback, in ms. Defaults to 5 minutes.\n readonly timeoutMs?: number;\n // Preferred port. 0 = OS-assigned ephemeral. If a preferred port is in use,\n // the server transparently falls back to 0.\n readonly preferredPort?: number;\n}\n\nexport class CallbackTimeoutError extends Error {\n constructor(seconds: number) {\n super(`Login timed out after ${seconds}s`);\n this.name = 'CallbackTimeoutError';\n }\n}\n\nexport class CallbackStateMismatchError extends Error {\n constructor() {\n super('Invalid or missing state parameter');\n this.name = 'CallbackStateMismatchError';\n }\n}\n\nexport class CallbackMissingCodeError extends Error {\n constructor() {\n super('Missing code parameter');\n this.name = 'CallbackMissingCodeError';\n }\n}\n\nexport function randomState(): string {\n // 32 bytes โ 43 chars base64url. Sufficient for CSRF.\n return randomBytes(32).toString('base64url');\n}\n\n// Constant-time string comparison. Falls back to a simple `false` when the\n// lengths differ, matching `crypto.timingSafeEqual`'s own precondition.\nfunction constantTimeStringEqual(a: string, b: string): boolean {\n const bufA = Buffer.from(a, 'utf8');\n const bufB = Buffer.from(b, 'utf8');\n if (bufA.length !== bufB.length) return false;\n return timingSafeEqual(bufA, bufB);\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/\\//g, '/')\n .replace(/`/g, '`');\n}\n\ntype ParseResult =\n | { kind: 'ok'; query: CallbackQuery }\n | { kind: 'state-mismatch' }\n | { kind: 'missing-code' }\n | { kind: 'malformed' }\n | { kind: 'not-found' };\n\nfunction parseCallbackUrl(reqUrl: string | undefined, expectedState: string): ParseResult {\n if (!reqUrl) return { kind: 'malformed' };\n let parsed: URL;\n try {\n parsed = new URL(reqUrl, 'http://127.0.0.1');\n } catch {\n return { kind: 'malformed' };\n }\n if (parsed.pathname !== '/callback') {\n return { kind: 'not-found' };\n }\n const raw: Record<string, string> = {};\n for (const [k, v] of parsed.searchParams) raw[k] = v;\n const state = raw.state ?? '';\n const code = raw.code ?? '';\n if (!state || !constantTimeStringEqual(state, expectedState)) {\n return { kind: 'state-mismatch' };\n }\n if (!code) {\n return { kind: 'missing-code' };\n }\n return { kind: 'ok', query: { code, state, raw } };\n}\n\nconst ERROR_MESSAGES: Record<Exclude<ParseResult['kind'], 'ok'>, string> = {\n 'state-mismatch': 'Invalid or missing state parameter',\n 'missing-code': 'Missing code parameter',\n malformed: 'Malformed request URL',\n 'not-found': 'Not found',\n};\n\nconst ERROR_STATUS: Record<Exclude<ParseResult['kind'], 'ok'>, number> = {\n 'state-mismatch': 400,\n 'missing-code': 400,\n malformed: 400,\n 'not-found': 404,\n};\n\nconst SUCCESS_HTML = `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><title>ait-console</title>\n<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.25rem}</style>\n</head>\n<body>\n<h1>Logged in to ait-console</h1>\n<p>You can close this window and return to your terminal.</p>\n</body></html>`;\n\nconst GONE_HTML = `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><title>ait-console</title>\n<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.25rem}</style>\n</head>\n<body>\n<h1>This login flow is already complete</h1>\n<p>Return to your terminal.</p>\n</body></html>`;\n\nfunction errorHtml(message: string): string {\n // Messages are currently a fixed enum (see ERROR_MESSAGES) but we escape\n // unconditionally so future callers can't introduce a reflected-XSS hole.\n return `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><title>ait-console โ error</title>\n<style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem;color:#222}h1{font-size:1.25rem;color:#b00020}</style>\n</head>\n<body>\n<h1>Login failed</h1>\n<p>${escapeHtml(message)}</p>\n<p>Return to your terminal for details.</p>\n</body></html>`;\n}\n\nasync function bindServer(server: Server, preferredPort: number | undefined): Promise<number> {\n const tryListen = (port: number): Promise<number> =>\n new Promise((resolve, reject) => {\n const onError = (err: Error) => {\n server.removeListener('error', onError);\n reject(err);\n };\n server.once('error', onError);\n server.listen(port, '127.0.0.1', () => {\n server.removeListener('error', onError);\n const addr = server.address() as AddressInfo | null;\n if (!addr) reject(new Error('Failed to bind callback server'));\n else resolve(addr.port);\n });\n });\n\n if (preferredPort && preferredPort !== 0) {\n try {\n return await tryListen(preferredPort);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EADDRINUSE') throw err;\n // Fall through to ephemeral port.\n }\n }\n return tryListen(0);\n}\n\nexport async function startCallbackServer(\n options: StartCallbackServerOptions = {},\n): Promise<CallbackServer> {\n const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;\n const expectedState = randomState();\n\n const server: Server = createServer();\n // Bind to 127.0.0.1 only โ never expose the callback on a routable address.\n // Try `preferredPort` first; on EADDRINUSE fall back to ephemeral (0).\n const boundPort = await bindServer(server, options.preferredPort);\n\n const redirectUri = `http://127.0.0.1:${boundPort}/callback`;\n\n let settled = false;\n let closed = false;\n let resolveCb!: (q: CallbackQuery) => void;\n let rejectCb!: (e: Error) => void;\n const waiter = new Promise<CallbackQuery>((resolve, reject) => {\n resolveCb = resolve;\n rejectCb = reject;\n });\n // Attach a noop catch so an early rejection (e.g. state mismatch fired\n // before the caller calls waitForCallback) is not treated as unhandled.\n // The real error surface is the Promise returned from waitForCallback(),\n // which re-receives the same rejection via its rejectCb. Don't route\n // diagnostics through this handler โ it exists solely to appease the\n // runtime's unhandled-rejection tracker.\n waiter.catch(() => {});\n\n const finish = (outcome: { kind: 'ok'; q: CallbackQuery } | { kind: 'err'; e: Error }) => {\n if (settled) return;\n settled = true;\n if (outcome.kind === 'ok') resolveCb(outcome.q);\n else rejectCb(outcome.e);\n };\n\n server.on('request', (req: IncomingMessage, res: ServerResponse) => {\n // Once the flow is settled, further hits (duplicate redirects, noisy\n // extensions) get 410 Gone rather than another success page. This\n // prevents a late attacker-crafted callback from rendering as \"logged in\".\n if (settled) {\n res.statusCode = 410;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(GONE_HTML);\n return;\n }\n const result = parseCallbackUrl(req.url, expectedState);\n if (result.kind === 'ok') {\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n // Settle only after the response body has actually been flushed to\n // the client โ otherwise `server.close()` / `closeAllConnections()`\n // on the caller's side can tear the socket down mid-write and the\n // user sees \"connection reset\" instead of the success page.\n res.end(SUCCESS_HTML, () => finish({ kind: 'ok', q: result.query }));\n return;\n }\n const status = ERROR_STATUS[result.kind];\n const message = ERROR_MESSAGES[result.kind];\n res.statusCode = status;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n // Don't settle on arbitrary 404s โ the user might have a noisy\n // extension or favicon probe. Only settle on structural errors at the\n // /callback path itself. Once we settle (success or structural error),\n // every subsequent request โ including a legitimate-looking redirect โ\n // gets 410 Gone via the `settled` branch above. The first-wins contract\n // is intentional: a CSRF attacker can't race a real redirect by firing\n // a bad callback first (it'll reject with state-mismatch), and a noisy\n // browser reload after success can't re-render a login page.\n const onFlushed = () => {\n switch (result.kind) {\n case 'state-mismatch':\n finish({ kind: 'err', e: new CallbackStateMismatchError() });\n return;\n case 'missing-code':\n finish({ kind: 'err', e: new CallbackMissingCodeError() });\n return;\n case 'malformed':\n case 'not-found':\n // Don't settle on malformed or non-callback paths โ a noisy\n // extension probing `/\\x00` or `/favicon.ico` shouldn't end\n // the flow. The real redirect will still resolve it.\n return;\n default:\n // Exhaustiveness check โ a new ParseResult kind will fail to\n // compile here because `result` is narrowed to `never`.\n ((_: never) => {})(result);\n }\n };\n res.end(errorHtml(message), onFlushed);\n });\n\n const timer = setTimeout(() => {\n // Ceil so the reported number is an upper bound on the real cap โ\n // a `timeoutMs` of 1500 ms reports \"after 2s\", never \"after 1s\".\n finish({ kind: 'err', e: new CallbackTimeoutError(Math.ceil(timeoutMs / 1000)) });\n }, timeoutMs);\n if (typeof timer.unref === 'function') timer.unref();\n\n const close = async (): Promise<void> => {\n if (closed) return;\n closed = true;\n clearTimeout(timer);\n // Race `server.close()` against a 1s timeout โ a misbehaving keep-alive\n // client shouldn't be able to hold the CLI from exiting.\n await new Promise<void>((resolve) => {\n let done = false;\n const finishClose = () => {\n if (done) return;\n done = true;\n resolve();\n };\n server.close(() => finishClose());\n server.closeAllConnections?.();\n const fallback = setTimeout(finishClose, 1000);\n if (typeof fallback.unref === 'function') fallback.unref();\n });\n };\n\n return {\n port: boundPort,\n redirectUri,\n expectedState,\n waitForCallback: () => waiter,\n close,\n };\n}\n","import { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// Resolve the config directory following the XDG Base Directory spec on\n// POSIX systems and using %APPDATA% on Windows. Falls back gracefully if\n// environment variables are missing (e.g. minimal containers without HOME).\n\nconst APP_NAME = 'ait-console';\n\nexport function configDir(): string {\n if (process.platform === 'win32') {\n const appData = process.env.APPDATA;\n if (appData && appData.length > 0) return join(appData, APP_NAME);\n return join(homedir() || '.', 'AppData', 'Roaming', APP_NAME);\n }\n const xdg = process.env.XDG_CONFIG_HOME;\n if (xdg && xdg.length > 0) return join(xdg, APP_NAME);\n return join(homedir() || '.', '.config', APP_NAME);\n}\n\nexport function sessionFilePath(): string {\n return join(configDir(), 'session.json');\n}\n","import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { configDir, sessionFilePath } from './paths.js';\n\n// Minimal, forward-compatible session shape. `cookies` and `origins` mirror\n// Playwright's `storageState` so a future `login` command can hand them\n// straight to a headless context.\n//\n// SECURITY: this module is the only place that touches the secret material.\n// - Never log raw cookies / origins.\n// - Treat file IO errors as \"no session\" in user-facing commands.\n\nexport interface SessionUser {\n id: string;\n email: string;\n displayName?: string;\n}\n\nexport interface Session {\n schemaVersion: 1;\n user: SessionUser;\n // Opaque Playwright storageState slots. Kept `unknown` to discourage\n // destructuring / logging anywhere except the login/deploy code paths.\n cookies: unknown[];\n origins: unknown[];\n capturedAt: string; // ISO-8601\n}\n\n// Public-safe projection for `whoami` and other diagnostics.\nexport interface SessionSummary {\n user: SessionUser;\n capturedAt: string;\n}\n\nfunction summarize(session: Session): SessionSummary {\n return { user: session.user, capturedAt: session.capturedAt };\n}\n\nexport async function readSession(): Promise<Session | null> {\n try {\n const raw = await readFile(sessionFilePath(), 'utf8');\n const parsed = JSON.parse(raw) as Session;\n if (parsed.schemaVersion !== 1) return null;\n if (!parsed.user || typeof parsed.user.id !== 'string') return null;\n if (typeof parsed.user.email !== 'string') return null;\n if (parsed.user.displayName !== undefined && typeof parsed.user.displayName !== 'string') {\n return null;\n }\n return parsed;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n // Malformed / unreadable file โ treat as no session so commands emit a\n // clean \"not logged in\" error instead of a stack trace.\n return null;\n }\n}\n\nexport async function readSessionSummary(): Promise<SessionSummary | null> {\n const s = await readSession();\n return s ? summarize(s) : null;\n}\n\nexport async function writeSession(session: Session): Promise<void> {\n const dir = dirname(sessionFilePath());\n await mkdir(dir, { recursive: true, mode: 0o700 });\n await writeFile(sessionFilePath(), JSON.stringify(session, null, 2), {\n mode: 0o600,\n });\n // writeFile's mode only applies on creation; tighten existing files too.\n try {\n await chmod(sessionFilePath(), 0o600);\n } catch {\n // Windows / exotic FS: best-effort only.\n }\n}\n\nexport async function clearSession(): Promise<{ existed: boolean }> {\n try {\n await unlink(sessionFilePath());\n return { existed: true };\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return { existed: false };\n throw err;\n }\n}\n\nexport function sessionPathForDiagnostics(): string {\n return sessionFilePath();\n}\n\nexport function configDirForDiagnostics(): string {\n return configDir();\n}\n","import { defineCommand } from 'citty';\nimport { openBrowser } from '../browser.js';\nimport { ExitCode } from '../exit.js';\nimport { exitAfterFlush } from '../flush.js';\nimport {\n CallbackMissingCodeError,\n type CallbackQuery,\n CallbackStateMismatchError,\n CallbackTimeoutError,\n startCallbackServer,\n} from '../oauth.js';\nimport { type Session, writeSession } from '../session.js';\n\n// The Toss developer console OAuth authorize URL and scope are not publicly\n// documented as of 2026-04. Override with `AIT_CONSOLE_OAUTH_URL` (and\n// optionally `AIT_CONSOLE_OAUTH_CLIENT_ID` / `AIT_CONSOLE_OAUTH_SCOPE`) while\n// discovery is in progress. Without the env var we refuse to run rather than\n// silently hit a placeholder endpoint.\n\n// Cap raw callback-query fields before writing them to the session file.\n// The real flow will replace this with a token-endpoint POST; until then,\n// accept only short, control-char-free strings for the user label. 128 is\n// well above the 254-char RFC 5321 email length cap and plausible display\n// names, so anything longer is presumed garbage.\nconst MAX_FIELD_LENGTH = 128;\n\n// Sentinel used when the (still-in-discovery) callback does not carry a\n// user identity. Using a fixed label avoids storing the raw OAuth `code`\n// as a user id, which would then leak through `whoami`.\nconst PENDING_USER_ID = 'pending:oauth-discovery';\n\nfunction sanitizeField(raw: string | undefined): string | undefined {\n if (typeof raw !== 'string') return undefined;\n if (raw.length === 0) return undefined;\n if (raw.length > MAX_FIELD_LENGTH) return undefined;\n // Reject control chars including CR/LF so a pasted value can't forge a log\n // line or break JSON emission.\n // biome-ignore lint/suspicious/noControlCharactersInRegex: explicit control-char filter\n if (/[\\x00-\\x1f\\x7f]/.test(raw)) return undefined;\n return raw;\n}\n\nfunction buildAuthorizeUrl(params: {\n readonly authorizeUrl: string;\n readonly redirectUri: string;\n readonly state: string;\n readonly clientId: string | undefined;\n readonly scope: string | undefined;\n}): string {\n const url = new URL(params.authorizeUrl);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('redirect_uri', params.redirectUri);\n url.searchParams.set('state', params.state);\n if (params.clientId) url.searchParams.set('client_id', params.clientId);\n if (params.scope) url.searchParams.set('scope', params.scope);\n return url.toString();\n}\n\nfunction classifyCallbackError(err: Error): {\n reason: 'timeout' | 'state-mismatch' | 'missing-code' | 'other';\n exitCode: ExitCode;\n} {\n if (err instanceof CallbackTimeoutError) {\n return { reason: 'timeout', exitCode: ExitCode.LoginTimeout };\n }\n if (err instanceof CallbackStateMismatchError) {\n return { reason: 'state-mismatch', exitCode: ExitCode.LoginStateMismatch };\n }\n if (err instanceof CallbackMissingCodeError) {\n return { reason: 'missing-code', exitCode: ExitCode.Generic };\n }\n return { reason: 'other', exitCode: ExitCode.Generic };\n}\n\nexport const loginCommand = defineCommand({\n meta: {\n name: 'login',\n description: 'Log in via the browser; starts a localhost callback server.',\n },\n args: {\n json: {\n type: 'boolean',\n description: 'Emit machine-readable JSON to stdout.',\n default: false,\n },\n 'no-browser': {\n type: 'boolean',\n description: \"Don't auto-open the browser; print the URL for manual copy.\",\n default: false,\n },\n timeout: {\n type: 'string',\n description: 'Abort the login if no callback arrives within N seconds (default 300).',\n default: '300',\n },\n },\n async run({ args }) {\n const rawOauthUrl = process.env.AIT_CONSOLE_OAUTH_URL;\n const authorizeUrl = rawOauthUrl && rawOauthUrl.length > 0 ? rawOauthUrl : null;\n const clientId = process.env.AIT_CONSOLE_OAUTH_CLIENT_ID;\n const scope = process.env.AIT_CONSOLE_OAUTH_SCOPE;\n\n const emitError = (payload: Record<string, unknown>, human: string) => {\n if (args.json) {\n process.stdout.write(`${JSON.stringify({ ok: false, ...payload })}\\n`);\n }\n process.stderr.write(`${human}\\n`);\n };\n\n const timeoutNum = Number(args.timeout);\n // Require โฅ 1 s so `CallbackTimeoutError`'s rounded-to-seconds message\n // can't produce misleading \"Login timed out after 1s\" for a 500 ms cap.\n if (!Number.isFinite(timeoutNum) || timeoutNum < 1) {\n emitError(\n { reason: 'invalid-timeout', given: args.timeout },\n `Invalid --timeout value: ${args.timeout}`,\n );\n return exitAfterFlush(ExitCode.Usage);\n }\n const timeoutMs = timeoutNum * 1000;\n\n if (!authorizeUrl) {\n emitError(\n { reason: 'oauth-url-not-configured', hint: 'set AIT_CONSOLE_OAUTH_URL' },\n [\n 'The Toss developer console OAuth URL is not configured.',\n 'Discovery is pending โ set AIT_CONSOLE_OAUTH_URL to override,',\n 'or track the TODO in CLAUDE.md ยง \"Open questions\".',\n ].join('\\n'),\n );\n return exitAfterFlush(ExitCode.Usage);\n }\n\n const server = await startCallbackServer({ timeoutMs });\n const authUrl = buildAuthorizeUrl({\n authorizeUrl,\n redirectUri: server.redirectUri,\n state: server.expectedState,\n clientId,\n scope,\n });\n\n // Per the --json contract, stdout in JSON mode is strictly a single\n // JSON document. Progress/diagnostic chatter always goes to stderr so\n // behavior is consistent between modes โ in JSON mode it still helps\n // humans watching a terminal and gives tests a way to recover the\n // server's port.\n process.stderr.write(`Listening for the OAuth callback on ${server.redirectUri}\\n`);\n\n let launched = false;\n if (!args['no-browser']) {\n const result = await openBrowser(authUrl);\n launched = result.launched;\n }\n if (launched) {\n process.stderr.write('Opened your browser. Complete the login there.\\n');\n } else {\n process.stderr.write(`Open this URL in your browser to continue:\\n ${authUrl}\\n`);\n }\n\n let query: CallbackQuery;\n try {\n try {\n query = await server.waitForCallback();\n } catch (err) {\n // Emit diagnostics first; the (idempotent) close lands in finally.\n const { reason, exitCode } = classifyCallbackError(err as Error);\n emitError(\n { reason, message: (err as Error).message },\n `Login failed: ${(err as Error).message}`,\n );\n return exitAfterFlush(exitCode);\n }\n } finally {\n await server.close();\n }\n\n // Token exchange / session capture is pending Toss console OAuth\n // discovery (tracked in TODO.md and CLAUDE.md ยง \"Open questions\").\n // Until then we accept optional user-label fields from the callback\n // query string but validate them strictly. If no identity field is\n // present we refuse to write the session rather than smuggling the\n // raw OAuth `code` (a secret) into the user-visible identifier. The\n // real flow will POST to a token endpoint and capture a Playwright\n // `storageState` โ at which point `cookies` and `origins` become\n // non-empty and the identity fallback goes away. Field sanitization\n // (length + control-char guards) stays regardless, since display\n // names and emails still need to be safe to serialize and print.\n const rawUserId = sanitizeField(query.raw.user_id);\n const rawEmail = sanitizeField(query.raw.email);\n const displayName = sanitizeField(query.raw.display_name);\n if (!rawUserId && !rawEmail) {\n // Refuse to write a phantom session during the OAuth-discovery\n // placeholder phase: with no identity we'd have nothing meaningful\n // to show in `whoami`. The real flow will always provide an\n // identity via the token endpoint; this branch goes away then.\n emitError(\n { reason: 'oauth-identity-missing' },\n [\n 'The callback did not carry user_id or email.',\n 'This is expected until Toss console OAuth discovery lands.',\n 'No session was written.',\n ].join('\\n'),\n );\n return exitAfterFlush(ExitCode.Generic);\n }\n const userId = rawUserId ?? PENDING_USER_ID;\n const email = rawEmail ?? '';\n\n const session: Session = {\n schemaVersion: 1,\n user: displayName ? { id: userId, email, displayName } : { id: userId, email },\n // Left empty pending real token-exchange + Playwright storageState.\n cookies: [],\n origins: [],\n capturedAt: new Date().toISOString(),\n };\n try {\n await writeSession(session);\n } catch (err) {\n emitError(\n { reason: 'session-write-failed', message: (err as Error).message },\n `Failed to write session file: ${(err as Error).message}`,\n );\n return exitAfterFlush(ExitCode.Generic);\n }\n\n if (args.json) {\n process.stdout.write(\n `${JSON.stringify({\n ok: true,\n status: 'logged-in',\n user: session.user,\n capturedAt: session.capturedAt,\n })}\\n`,\n );\n } else {\n const label = displayName ? `${displayName} <${email}>` : email || userId;\n process.stdout.write(`Logged in as ${label}\\n`);\n }\n // Flush on the success path too โ pipe consumers reading the JSON line\n // shouldn't be subject to truncation from a natural-exit race.\n return exitAfterFlush(ExitCode.Ok);\n },\n});\n","import { defineCommand } from 'citty';\nimport { ExitCode } from '../exit.js';\nimport { exitAfterFlush } from '../flush.js';\nimport { clearSession, sessionPathForDiagnostics } from '../session.js';\n\nexport const logoutCommand = defineCommand({\n meta: {\n name: 'logout',\n description: 'Delete the local session file.',\n },\n args: {\n json: {\n type: 'boolean',\n description: 'Emit machine-readable JSON to stdout.',\n default: false,\n },\n },\n async run({ args }) {\n const path = sessionPathForDiagnostics();\n\n let existed: boolean;\n try {\n const result = await clearSession();\n existed = result.existed;\n } catch (err) {\n const message = (err as Error).message;\n if (args.json) {\n process.stdout.write(\n `${JSON.stringify({ ok: false, reason: 'unlink-failed', path, message })}\\n`,\n );\n }\n process.stderr.write(`Failed to remove session file at ${path}: ${message}\\n`);\n return exitAfterFlush(ExitCode.Generic);\n }\n\n if (args.json) {\n process.stdout.write(\n `${JSON.stringify({ ok: true, status: existed ? 'logged-out' : 'no-session', path })}\\n`,\n );\n } else if (existed) {\n process.stdout.write(`Logged out. Session removed from ${path}\\n`);\n } else {\n process.stdout.write(`No active session at ${path}.\\n`);\n }\n return exitAfterFlush(ExitCode.Ok);\n },\n});\n","// Thin GitHub Releases API client. Only reads public endpoints, never writes.\n\nconst REPO_OWNER = 'apps-in-toss-community';\nconst REPO_NAME = 'console-cli';\n\nexport interface ReleaseAsset {\n name: string;\n browser_download_url: string;\n size: number;\n}\n\nexport interface Release {\n tag_name: string;\n name: string | null;\n html_url: string;\n assets: ReleaseAsset[];\n}\n\nfunction defaultHeaders(): HeadersInit {\n const headers: Record<string, string> = {\n Accept: 'application/vnd.github+json',\n 'User-Agent': 'ait-console',\n 'X-GitHub-Api-Version': '2022-11-28',\n };\n const token = process.env.GITHUB_TOKEN;\n if (token && token.length > 0) {\n headers.Authorization = `Bearer ${token}`;\n }\n return headers;\n}\n\nexport async function fetchLatestRelease(): Promise<Release> {\n const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`;\n const res = await fetch(url, { headers: defaultHeaders() });\n if (!res.ok) {\n throw new Error(`GitHub releases/latest returned ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as Release;\n}\n\n// Parse `tag_name` into a comparable semver string. Changesets tags this repo\n// as `@ait-co/console-cli@0.1.2`; older ad-hoc tags may be `v0.1.2`. We\n// accept both.\nexport function versionFromTag(tag: string): string {\n const at = tag.lastIndexOf('@');\n const candidate = at >= 0 ? tag.slice(at + 1) : tag;\n return candidate.startsWith('v') ? candidate.slice(1) : candidate;\n}\n","// Map Node's `process.platform` / `process.arch` to the binary asset names\n// produced by `scripts/build-bin.ts` and attached to GitHub Releases.\n\nexport interface PlatformTarget {\n os: 'linux' | 'darwin' | 'windows';\n arch: 'x64' | 'arm64';\n assetName: string;\n}\n\nexport function detectPlatform(): PlatformTarget | null {\n let os: PlatformTarget['os'];\n switch (process.platform) {\n case 'linux':\n os = 'linux';\n break;\n case 'darwin':\n os = 'darwin';\n break;\n case 'win32':\n os = 'windows';\n break;\n default:\n return null;\n }\n\n let arch: PlatformTarget['arch'];\n switch (process.arch) {\n case 'x64':\n arch = 'x64';\n break;\n case 'arm64':\n arch = 'arm64';\n break;\n default:\n return null;\n }\n\n // We don't ship windows-arm64 yet โ Bun's `--compile` target support is still partial.\n if (os === 'windows' && arch === 'arm64') return null;\n\n const suffix = os === 'windows' ? '.exe' : '';\n return { os, arch, assetName: `ait-console-${os}-${arch}${suffix}` };\n}\n","// Minimal semver comparator. We only need \"is A strictly newer than B?\" for\n// the upgrade check. Pulling the full `semver` package would bloat the\n// compiled binary for one function.\n\nexport function parseSemver(\n v: string,\n): { major: number; minor: number; patch: number; pre: string } | null {\n const m = /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v);\n if (!m) return null;\n return { major: +m[1]!, minor: +m[2]!, patch: +m[3]!, pre: m[4] ?? '' };\n}\n\n// Returns 1 if a > b, -1 if a < b, 0 if equal. Returns 0 if either is\n// unparseable (defensive โ upgrade will treat that as \"already latest\").\nexport function compareSemver(a: string, b: string): number {\n const pa = parseSemver(a);\n const pb = parseSemver(b);\n if (!pa || !pb) return 0;\n if (pa.major !== pb.major) return pa.major > pb.major ? 1 : -1;\n if (pa.minor !== pb.minor) return pa.minor > pb.minor ? 1 : -1;\n if (pa.patch !== pb.patch) return pa.patch > pb.patch ? 1 : -1;\n // Treat \"no prerelease\" as greater than \"has prerelease\" (1.0.0 > 1.0.0-rc).\n if (pa.pre === pb.pre) return 0;\n if (pa.pre === '') return 1;\n if (pb.pre === '') return -1;\n return pa.pre > pb.pre ? 1 : -1;\n}\n","// Single source of truth for the embedded CLI version.\n//\n// The value is replaced at build time:\n// - tsdown โ via `--define AIT_CONSOLE_VERSION=...` (see `scripts/build-define.ts`)\n// - bun โ via `--define AIT_CONSOLE_VERSION=...` (see `scripts/build-bin.ts`)\n//\n// During `pnpm test` / `ts-node` execution the define isn't applied, so we fall\n// back to reading `package.json` at runtime. That path is never hit in the\n// shipped artifacts.\n\ndeclare const AIT_CONSOLE_VERSION: string | undefined;\n\nfunction resolveVersion(): string {\n try {\n // biome-ignore lint/suspicious/noExplicitAny: globalThis lookup for optional build-time define\n const injected = (globalThis as any).AIT_CONSOLE_VERSION as string | undefined;\n if (typeof injected === 'string' && injected.length > 0) return injected;\n } catch {\n // ignore\n }\n try {\n if (typeof AIT_CONSOLE_VERSION === 'string' && AIT_CONSOLE_VERSION.length > 0) {\n return AIT_CONSOLE_VERSION;\n }\n } catch {\n // ignore\n }\n return '0.0.0-dev';\n}\n\nexport const VERSION = resolveVersion();\n","import { chmod, rename, writeFile } from 'node:fs/promises';\nimport { basename, dirname } from 'node:path';\nimport { defineCommand } from 'citty';\nimport { ExitCode } from '../exit.js';\nimport { fetchLatestRelease, versionFromTag } from '../github.js';\nimport { detectPlatform } from '../platform.js';\nimport { compareSemver } from '../semver.js';\nimport { VERSION } from '../version.js';\n\n// Distinguishes a Bun-compiled standalone (where `process.execPath` points at\n// the binary itself) from a Node-hosted install (where it points at `node`).\n// Only the former can atomically replace itself; the latter should upgrade\n// via npm.\nfunction isStandaloneBinary(): boolean {\n const exe = basename(process.execPath).toLowerCase();\n return exe.startsWith('ait-console');\n}\n\nexport const upgradeCommand = defineCommand({\n meta: {\n name: 'upgrade',\n description: 'Download the latest release binary from GitHub and replace the current one.',\n },\n args: {\n json: {\n type: 'boolean',\n description: 'Emit machine-readable JSON to stdout.',\n default: false,\n },\n force: {\n type: 'boolean',\n description: 'Re-install even if already on the latest version.',\n default: false,\n },\n 'dry-run': {\n type: 'boolean',\n description: 'Check for updates without downloading or replacing.',\n default: false,\n },\n },\n async run({ args }) {\n const emit = (payload: Record<string, unknown>, human: string) => {\n if (args.json) {\n process.stdout.write(`${JSON.stringify(payload)}\\n`);\n } else {\n process.stdout.write(`${human}\\n`);\n }\n };\n const emitError = (payload: Record<string, unknown>, human: string) => {\n if (args.json) {\n process.stdout.write(`${JSON.stringify({ ok: false, ...payload })}\\n`);\n } else {\n process.stderr.write(`${human}\\n`);\n }\n };\n\n let release: Awaited<ReturnType<typeof fetchLatestRelease>>;\n try {\n release = await fetchLatestRelease();\n } catch (err) {\n emitError(\n { reason: 'network-error', message: (err as Error).message },\n `Failed to query GitHub releases: ${(err as Error).message}`,\n );\n process.exit(ExitCode.NetworkError);\n }\n\n const latest = versionFromTag(release.tag_name);\n const current = VERSION;\n const cmp = compareSemver(latest, current);\n const needsUpdate = cmp > 0 || args.force;\n\n if (!needsUpdate) {\n emit(\n { ok: true, status: 'already-latest', current, latest },\n `Already on the latest version (${current}).`,\n );\n process.exit(ExitCode.UpgradeAlreadyLatest);\n }\n\n if (args['dry-run']) {\n emit(\n { ok: true, status: 'update-available', current, latest, url: release.html_url },\n `Update available: ${current} โ ${latest}\\n${release.html_url}`,\n );\n return;\n }\n\n if (!isStandaloneBinary()) {\n emitError(\n {\n reason: 'not-standalone',\n current,\n latest,\n hint: 'npm i -g @ait-co/console-cli@latest',\n },\n [\n 'This install was launched via Node, not the standalone binary.',\n 'Self-upgrade is only supported for the compiled binary.',\n `Run: npm i -g @ait-co/console-cli@latest (currently ${current}, latest ${latest})`,\n ].join('\\n'),\n );\n process.exit(ExitCode.UpgradeUnavailable);\n }\n\n const platform = detectPlatform();\n if (!platform) {\n emitError(\n {\n reason: 'unsupported-platform',\n platform: process.platform,\n arch: process.arch,\n },\n `No prebuilt binary for ${process.platform}/${process.arch}.`,\n );\n process.exit(ExitCode.UpgradeUnavailable);\n }\n\n const asset = release.assets.find((a) => a.name === platform.assetName);\n if (!asset) {\n emitError(\n { reason: 'asset-missing', assetName: platform.assetName, tag: release.tag_name },\n `Release ${release.tag_name} has no asset named ${platform.assetName}. It may still be uploading.`,\n );\n process.exit(ExitCode.UpgradeUnavailable);\n }\n\n const exePath = process.execPath;\n const stagingPath = `${exePath}.new.${Date.now()}`;\n\n if (!args.json) {\n process.stdout.write(`Downloading ${asset.name} (${latest})...\\n`);\n }\n\n try {\n const res = await fetch(asset.browser_download_url);\n if (!res.ok || !res.body) {\n throw new Error(`Download failed: ${res.status} ${res.statusText}`);\n }\n const buf = new Uint8Array(await res.arrayBuffer());\n await writeFile(stagingPath, buf, { mode: 0o755 });\n await chmod(stagingPath, 0o755);\n } catch (err) {\n emitError(\n { reason: 'download-failed', message: (err as Error).message },\n `Failed to download new binary: ${(err as Error).message}`,\n );\n process.exit(ExitCode.NetworkError);\n }\n\n // Atomic replace. POSIX `rename(2)` on the same filesystem is atomic.\n // On Windows a running exe can't be overwritten directly; the staging\n // path is in the same dir, so rename-over works on most shells, and we\n // leave `<exe>.old` handling to a future refinement.\n try {\n if (process.platform === 'win32') {\n await rename(exePath, `${exePath}.old`);\n await rename(stagingPath, exePath);\n } else {\n await rename(stagingPath, exePath);\n }\n } catch (err) {\n emitError(\n { reason: 'replace-failed', message: (err as Error).message, exePath, stagingPath },\n `Failed to replace binary at ${exePath}: ${(err as Error).message}`,\n );\n process.exit(ExitCode.Generic);\n }\n\n emit(\n {\n ok: true,\n status: 'upgraded',\n from: current,\n to: latest,\n installedAt: exePath,\n installedIn: dirname(exePath),\n },\n `Upgraded ait-console: ${current} โ ${latest}`,\n );\n },\n});\n","import { defineCommand } from 'citty';\nimport { ExitCode } from '../exit.js';\nimport { readSessionSummary, sessionPathForDiagnostics } from '../session.js';\n\nexport const whoamiCommand = defineCommand({\n meta: {\n name: 'whoami',\n description: 'Show the currently authenticated user from the local session.',\n },\n args: {\n json: {\n type: 'boolean',\n description: 'Emit machine-readable JSON to stdout.',\n default: false,\n },\n },\n async run({ args }) {\n const summary = await readSessionSummary();\n\n if (!summary) {\n if (args.json) {\n process.stdout.write(`${JSON.stringify({ authenticated: false })}\\n`);\n } else {\n process.stderr.write('Not logged in. Run `ait-console login` to start a session.\\n');\n process.stderr.write(`Session file checked: ${sessionPathForDiagnostics()}\\n`);\n }\n process.exit(ExitCode.NotAuthenticated);\n }\n\n if (args.json) {\n process.stdout.write(\n `${JSON.stringify({\n authenticated: true,\n user: summary.user,\n capturedAt: summary.capturedAt,\n })}\\n`,\n );\n return;\n }\n\n const label = summary.user.displayName\n ? `${summary.user.displayName} <${summary.user.email}>`\n : summary.user.email;\n process.stdout.write(`Logged in as ${label}\\n`);\n process.stdout.write(`Session captured: ${summary.capturedAt}\\n`);\n },\n});\n","#!/usr/bin/env node\nimport { defineCommand, runMain } from 'citty';\nimport { loginCommand } from './commands/login.js';\nimport { logoutCommand } from './commands/logout.js';\nimport { upgradeCommand } from './commands/upgrade.js';\nimport { whoamiCommand } from './commands/whoami.js';\nimport { VERSION } from './version.js';\n\nconst main = defineCommand({\n meta: {\n name: 'ait-console',\n version: VERSION,\n description:\n 'Community CLI for the Apps in Toss developer console (unofficial; not affiliated with Toss).',\n },\n subCommands: {\n whoami: whoamiCommand,\n login: loginCommand,\n logout: logoutCommand,\n upgrade: upgradeCommand,\n },\n});\n\nrunMain(main);\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ait-co/console-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI and MCP server for the Apps in Toss developer console โ log in once in a browser, then drive builds, deploys, and releases headlessly",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"ait-console": "./dist/cli.mjs"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/cli.mjs",
|
|
13
|
+
"types": "./dist/cli.d.mts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsdown",
|
|
19
|
+
"build:bin": "bun run scripts/build-bin.ts",
|
|
20
|
+
"dev": "tsdown --watch",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"lint": "biome check .",
|
|
24
|
+
"lint:fix": "biome check --write .",
|
|
25
|
+
"format": "biome format --write ."
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "2.4.12",
|
|
29
|
+
"@changesets/cli": "^2.31.0",
|
|
30
|
+
"@types/bun": "^1.2.0",
|
|
31
|
+
"tsdown": "^0.21.7",
|
|
32
|
+
"typescript": "^6.0.2",
|
|
33
|
+
"vitest": "^4.1.4"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"apps-in-toss",
|
|
37
|
+
"toss",
|
|
38
|
+
"mini-app",
|
|
39
|
+
"cli",
|
|
40
|
+
"mcp",
|
|
41
|
+
"deployment"
|
|
42
|
+
],
|
|
43
|
+
"packageManager": "pnpm@10.33.0",
|
|
44
|
+
"license": "BSD-3-Clause",
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/apps-in-toss-community/console-cli"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://www.npmjs.com/package/@ait-co/console-cli",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/apps-in-toss-community/console-cli/issues"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"citty": "^0.2.2"
|
|
58
|
+
}
|
|
59
|
+
}
|