reelforge 0.1.0 → 0.2.1
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/commands/auth.js +113 -8
- package/dist/index.js +6 -1
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -1,30 +1,135 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
1
5
|
import { get, setApiKey, setServer, getServer } from "../client.js";
|
|
2
6
|
import { loadConfig, saveConfig, deleteConfig, getConfigPath } from "../utils/config-file.js";
|
|
3
7
|
import { info, success, print, table } from "../utils/output.js";
|
|
8
|
+
function openBrowser(url) {
|
|
9
|
+
const cmd = process.platform === "darwin"
|
|
10
|
+
? "open"
|
|
11
|
+
: process.platform === "win32"
|
|
12
|
+
? "rundll32"
|
|
13
|
+
: "xdg-open";
|
|
14
|
+
const args = process.platform === "win32"
|
|
15
|
+
? ["url.dll,FileProtocolHandler", url]
|
|
16
|
+
: [url];
|
|
17
|
+
try {
|
|
18
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Browser-based OAuth-style login:
|
|
27
|
+
* 1. Start a local HTTP server on 127.0.0.1:<random>
|
|
28
|
+
* 2. Open the server's /cli-auth page with callback + state
|
|
29
|
+
* 3. After the user approves in the browser, our local server receives
|
|
30
|
+
* the token via redirect, validates the state nonce, and returns it.
|
|
31
|
+
*/
|
|
32
|
+
async function browserOAuthFlow(serverUrl) {
|
|
33
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
34
|
+
const hostname = (os.hostname() || "device").slice(0, 64);
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let timer = null;
|
|
37
|
+
const server = http.createServer((req, res) => {
|
|
38
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
39
|
+
if (url.pathname !== "/cb") {
|
|
40
|
+
res.writeHead(404, { "Content-Type": "text/plain" }).end("Not found");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const gotState = url.searchParams.get("state");
|
|
44
|
+
const token = url.searchParams.get("token");
|
|
45
|
+
const errCode = url.searchParams.get("error");
|
|
46
|
+
const finish = (statusCode, title, body, outcome) => {
|
|
47
|
+
res.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" }).end(`<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><title>ReelForge CLI</title><style>body{font-family:-apple-system,system-ui,"Segoe UI",sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#fafaf7;color:#222}div{text-align:center;max-width:24rem;padding:1.5rem}h1{font-size:1.5rem;margin:0 0 .5rem;font-weight:600}p{color:#666;font-size:.875rem;line-height:1.5}</style></head><body><div><h1>${title}</h1><p>${body}</p></div><script>setTimeout(()=>window.close?.(),2000)</script></body></html>`);
|
|
48
|
+
if (timer)
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
server.close();
|
|
51
|
+
if (outcome.ok)
|
|
52
|
+
resolve(outcome.token);
|
|
53
|
+
else
|
|
54
|
+
reject(outcome.err);
|
|
55
|
+
};
|
|
56
|
+
if (gotState !== state) {
|
|
57
|
+
finish(400, "授权失败", "state 不匹配,可能是 CSRF。请重新跑 reelforge login。", {
|
|
58
|
+
ok: false,
|
|
59
|
+
err: new Error("state mismatch"),
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (errCode || !token) {
|
|
64
|
+
finish(400, "授权被取消", "可以关闭此页面回到 CLI。", {
|
|
65
|
+
ok: false,
|
|
66
|
+
err: new Error(errCode || "no token returned"),
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
finish(200, "✓ 授权成功", "可以关闭此页面,CLI 已自动接收 key。", {
|
|
71
|
+
ok: true,
|
|
72
|
+
token,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
server.listen(0, "127.0.0.1", () => {
|
|
76
|
+
const addr = server.address();
|
|
77
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
78
|
+
const callback = `http://127.0.0.1:${port}/cb`;
|
|
79
|
+
const authUrl = new URL("/cli-auth", serverUrl);
|
|
80
|
+
authUrl.searchParams.set("callback", callback);
|
|
81
|
+
authUrl.searchParams.set("state", state);
|
|
82
|
+
authUrl.searchParams.set("hostname", hostname);
|
|
83
|
+
info(`Listening on ${callback}`);
|
|
84
|
+
info("正在打开浏览器完成授权...");
|
|
85
|
+
const opened = openBrowser(authUrl.toString());
|
|
86
|
+
if (!opened) {
|
|
87
|
+
info("(打开浏览器失败,请手动访问以下 URL):");
|
|
88
|
+
info(authUrl.toString());
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
info(`(如果浏览器没自动弹出,请手动访问: ${authUrl.toString()})`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// 5-minute timeout
|
|
95
|
+
timer = setTimeout(() => {
|
|
96
|
+
server.close();
|
|
97
|
+
reject(new Error("授权超时(5 分钟)。请重新跑 reelforge login"));
|
|
98
|
+
}, 5 * 60 * 1000);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
4
101
|
export function registerAuth(program) {
|
|
5
102
|
program
|
|
6
|
-
.command("login
|
|
7
|
-
.description("
|
|
103
|
+
.command("login [api_key]")
|
|
104
|
+
.description("Login: with no argument opens browser for OAuth; with api_key saves it directly")
|
|
8
105
|
.helpOption("-h, --help", "show help")
|
|
9
106
|
.option("--server <url>", "also persist a custom server URL")
|
|
10
107
|
.addHelpText("after", [
|
|
11
108
|
"",
|
|
12
109
|
"Examples:",
|
|
13
|
-
" reelforge login
|
|
14
|
-
" reelforge login JK1234567890ABCDEF
|
|
110
|
+
" reelforge login # browser OAuth (recommended)",
|
|
111
|
+
" reelforge login JK1234567890ABCDEF # manual paste (SSH / headless)",
|
|
112
|
+
" reelforge login --server http://nas:8501 # OAuth against a self-hosted server",
|
|
15
113
|
"",
|
|
16
|
-
"The
|
|
114
|
+
"The token is verified against /api/v1/me before being saved.",
|
|
17
115
|
].join("\n"))
|
|
18
116
|
.action(async (apiKey, opts) => {
|
|
19
117
|
if (opts.server)
|
|
20
118
|
setServer(opts.server);
|
|
21
|
-
|
|
22
|
-
|
|
119
|
+
let token;
|
|
120
|
+
if (apiKey) {
|
|
121
|
+
token = apiKey;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
info(`Starting browser OAuth against ${getServer()}`);
|
|
125
|
+
token = await browserOAuthFlow(getServer());
|
|
126
|
+
}
|
|
127
|
+
setApiKey(token);
|
|
23
128
|
const me = await get("/api/v1/me");
|
|
24
129
|
const existing = await loadConfig();
|
|
25
130
|
const saved = await saveConfig({
|
|
26
131
|
...existing,
|
|
27
|
-
api_key:
|
|
132
|
+
api_key: token,
|
|
28
133
|
...(opts.server ? { server: opts.server } : {}),
|
|
29
134
|
});
|
|
30
135
|
success(`Saved → ${saved}`);
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* Every leaf command supports `--help`. Sub-groups (e.g. `reelforge llm`) print
|
|
5
5
|
* a list of their sub-commands when invoked without arguments OR with `--help`.
|
|
6
6
|
*/
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import path from "node:path";
|
|
7
10
|
import { Command } from "commander";
|
|
8
11
|
import { setServer, setApiKey } from "./client.js";
|
|
9
12
|
import { setOutputOptions } from "./utils/output.js";
|
|
13
|
+
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
14
|
+
const pkgVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
10
15
|
import { registerAuth } from "./commands/auth.js";
|
|
11
16
|
import { registerCreate } from "./commands/create.js";
|
|
12
17
|
import { registerLlm } from "./commands/llm.js";
|
|
@@ -39,7 +44,7 @@ program
|
|
|
39
44
|
"",
|
|
40
45
|
"Run `reelforge <command> --help` for sub-command details, e.g. `reelforge llm chat --help`.",
|
|
41
46
|
].join("\n"))
|
|
42
|
-
.version(
|
|
47
|
+
.version(pkgVersion, "-v, --version", "show CLI version")
|
|
43
48
|
.helpOption("-h, --help", "show help")
|
|
44
49
|
.addHelpCommand("help [command]", "show help for a command")
|
|
45
50
|
.showHelpAfterError("(use `reelforge <command> --help` to see usage)")
|
package/package.json
CHANGED