linkshell-cli 0.10.59 → 0.10.60
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkshell-cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.60",
|
|
4
4
|
"description": "Follow, steer and approve the coding agents on your computer (Claude Code, Codex, Gemini, Copilot and more) from your phone",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli/src/index.js",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"node-pty": "^1.1.0",
|
|
35
35
|
"qrcode-terminal": "^0.12.0",
|
|
36
36
|
"ws": "^8.21.0",
|
|
37
|
-
"@linkshell/host": "0.4.56",
|
|
38
37
|
"@linkshell/gateway": "0.6.34",
|
|
38
|
+
"@linkshell/host": "0.4.57",
|
|
39
39
|
"@linkshell/wire": "0.2.19"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
package/src/commands/login.ts
CHANGED
|
@@ -7,6 +7,95 @@ import { fetchProMembership } from "../../../client-core/src/pro-membership.js";
|
|
|
7
7
|
|
|
8
8
|
const ITOOL_AUTH_URL = "https://itool.tech/en/auth/linkshell";
|
|
9
9
|
|
|
10
|
+
// Served from the CLI's loopback server, so it has to carry its own styles and
|
|
11
|
+
// copy: it can't load anything from itool.tech.
|
|
12
|
+
const CALLBACK_PAGE = `<!DOCTYPE html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
17
|
+
<title>LinkShell</title>
|
|
18
|
+
<style>
|
|
19
|
+
:root { color-scheme: dark; --canvas:#09090A; --surface:#111113; --border:#1E1E22; --text:#F4F4F2; --muted:#A8A8AF; --dots:rgba(244,244,242,.12);
|
|
20
|
+
--brand:#34D399; --brand-bg:#0E1A15; --error:#F87171; --error-bg:#2A1212; }
|
|
21
|
+
@media (prefers-color-scheme: light) {
|
|
22
|
+
:root { color-scheme: light; --canvas:#F2F2EF; --surface:#FFFFFF; --border:#E2E2DD; --text:#0A0A0A; --muted:#52525B; --dots:rgba(10,10,10,.1);
|
|
23
|
+
--brand:#047857; --brand-bg:#ECFDF5; --error:#B91C1C; --error-bg:#FEF0F0; }
|
|
24
|
+
}
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
html, body { height: 100%; margin: 0; }
|
|
27
|
+
body { display: flex; align-items: center; justify-content: center; padding: 24px; background: var(--canvas); color: var(--text);
|
|
28
|
+
background-image: radial-gradient(var(--dots) 1.6px, transparent 1.6px); background-size: 24px 24px;
|
|
29
|
+
font: 15px/1.6 -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Hiragino Sans GB", "Segoe UI", sans-serif;
|
|
30
|
+
-webkit-font-smoothing: antialiased; }
|
|
31
|
+
main { width: 100%; max-width: 380px; padding: 36px 32px 28px; text-align: center; background: var(--surface); border-radius: 24px;
|
|
32
|
+
box-shadow: inset 0 0 0 1px var(--border), 0 24px 64px -16px rgba(0,0,0,.35); }
|
|
33
|
+
.icon { width: 56px; height: 56px; margin: 0 auto 22px; display: flex; align-items: center; justify-content: center; border-radius: 999px;
|
|
34
|
+
background: var(--brand-bg); color: var(--brand); }
|
|
35
|
+
[data-state="error"] .icon { background: var(--error-bg); color: var(--error); }
|
|
36
|
+
.icon svg { width: 26px; height: 26px; }
|
|
37
|
+
h1 { margin: 0 0 8px; font-size: 22px; font-weight: 600; letter-spacing: -.02em; }
|
|
38
|
+
p { margin: 0; color: var(--muted); }
|
|
39
|
+
code { padding: 2px 6px; border-radius: 6px; font: 13px ui-monospace, "SF Mono", Menlo, monospace; color: var(--text); box-shadow: inset 0 0 0 1px var(--border); }
|
|
40
|
+
footer { margin-top: 28px; padding-top: 16px; font-size: 13px; color: var(--muted); box-shadow: inset 0 1px 0 var(--border); }
|
|
41
|
+
.spin { animation: spin 900ms linear infinite; }
|
|
42
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
43
|
+
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }
|
|
44
|
+
</style>
|
|
45
|
+
</head>
|
|
46
|
+
<body>
|
|
47
|
+
<main id="card" data-state="busy" aria-live="polite">
|
|
48
|
+
<div class="icon" id="icon"></div>
|
|
49
|
+
<h1 id="title"></h1>
|
|
50
|
+
<p id="body"></p>
|
|
51
|
+
<footer>LinkShell</footer>
|
|
52
|
+
</main>
|
|
53
|
+
<script>
|
|
54
|
+
var zh = /^zh/i.test(navigator.language || '');
|
|
55
|
+
document.documentElement.lang = zh ? 'zh-CN' : 'en';
|
|
56
|
+
var retry = zh ? '请回到终端重新运行 <code>linkshell login</code>。' : 'Run <code>linkshell login</code> again in your terminal.';
|
|
57
|
+
var copy = {
|
|
58
|
+
busy: zh ? ['正在完成登录', '正在把账号交给 LinkShell,请稍候。'] : ['Finishing sign-in', 'Handing your account to LinkShell.'],
|
|
59
|
+
done: zh ? ['登录成功', '可以关闭这个标签页,回到终端继续。'] : ['Signed in', 'You can close this tab and return to your terminal.'],
|
|
60
|
+
error: zh ? ['登录失败', retry] : ['Sign-in failed', retry],
|
|
61
|
+
empty: zh ? ['没有收到登录信息', retry] : ['No sign-in details received', retry]
|
|
62
|
+
};
|
|
63
|
+
var icons = {
|
|
64
|
+
busy: '<svg class="spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-6.22-8.56"/></svg>',
|
|
65
|
+
done: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
|
|
66
|
+
error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4.5M12 16h.01"/></svg>'
|
|
67
|
+
};
|
|
68
|
+
function show(state) {
|
|
69
|
+
var card = document.getElementById('card');
|
|
70
|
+
card.setAttribute('data-state', state === 'empty' ? 'error' : state);
|
|
71
|
+
document.getElementById('icon').innerHTML = icons[state === 'empty' ? 'error' : state];
|
|
72
|
+
document.getElementById('title').textContent = copy[state][0];
|
|
73
|
+
document.getElementById('body').innerHTML = copy[state][1];
|
|
74
|
+
document.title = copy[state][0] + ' · LinkShell';
|
|
75
|
+
}
|
|
76
|
+
// The result is shown on iTool so it follows the site's design; this card
|
|
77
|
+
// stays as the fallback if itool.tech can't be reached.
|
|
78
|
+
function finish(state) {
|
|
79
|
+
show(state);
|
|
80
|
+
var status = state === 'done' ? 'ok' : state === 'empty' ? 'empty' : 'failed';
|
|
81
|
+
window.location.replace('https://itool.tech/' + (zh ? 'zh' : 'en') + '/auth/linkshell/done?status=' + status);
|
|
82
|
+
}
|
|
83
|
+
show('busy');
|
|
84
|
+
// iTool returns the tokens in the fragment; older redirects used the query string.
|
|
85
|
+
var data = window.location.hash.substring(1) || window.location.search.substring(1);
|
|
86
|
+
// Drop the tokens from the address bar and history before leaving the page.
|
|
87
|
+
history.replaceState(null, '', '/callback');
|
|
88
|
+
if (data) {
|
|
89
|
+
fetch('/token?' + data, { method: 'POST' })
|
|
90
|
+
.then(function (response) { if (!response.ok) throw new Error('failed'); finish('done'); })
|
|
91
|
+
.catch(function () { finish('error'); });
|
|
92
|
+
} else {
|
|
93
|
+
finish('empty');
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
</body>
|
|
97
|
+
</html>`;
|
|
98
|
+
|
|
10
99
|
/**
|
|
11
100
|
* CLI login via iTool OAuth flow.
|
|
12
101
|
*
|
|
@@ -49,32 +138,8 @@ export async function runLogin(options: { signal?: AbortSignal } = {}): Promise<
|
|
|
49
138
|
|
|
50
139
|
if (url.pathname === "/callback") {
|
|
51
140
|
// Serve a page that extracts the hash fragment and sends it back
|
|
52
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
53
|
-
res.end(
|
|
54
|
-
<html>
|
|
55
|
-
<head><title>LinkShell Login</title></head>
|
|
56
|
-
<body>
|
|
57
|
-
<p>Processing login...</p>
|
|
58
|
-
<script>
|
|
59
|
-
const hash = window.location.hash.substring(1);
|
|
60
|
-
if (hash) {
|
|
61
|
-
fetch('/token?' + hash, { method: 'POST' })
|
|
62
|
-
.then(response => { if (!response.ok) throw new Error('Login failed'); document.body.innerHTML = '<h2>Login successful! You can close this tab.</h2>'; })
|
|
63
|
-
.catch(() => { document.body.innerHTML = '<h2>Login failed. Please try again.</h2>'; });
|
|
64
|
-
} else {
|
|
65
|
-
// Try query params (iTool may pass tokens as query params)
|
|
66
|
-
const params = window.location.search.substring(1);
|
|
67
|
-
if (params) {
|
|
68
|
-
fetch('/token?' + params, { method: 'POST' })
|
|
69
|
-
.then(response => { if (!response.ok) throw new Error('Login failed'); document.body.innerHTML = '<h2>Login successful! You can close this tab.</h2>'; })
|
|
70
|
-
.catch(() => { document.body.innerHTML = '<h2>Login failed. Please try again.</h2>'; });
|
|
71
|
-
} else {
|
|
72
|
-
document.body.innerHTML = '<h2>No auth data received. Please try again.</h2>';
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
</script>
|
|
76
|
-
</body>
|
|
77
|
-
</html>`);
|
|
141
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
142
|
+
res.end(CALLBACK_PAGE);
|
|
78
143
|
return;
|
|
79
144
|
}
|
|
80
145
|
|