xuanwu-cli 2.0.0 → 2.2.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/bin/xuanwu CHANGED
File without changes
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function makeLoginCommand(): Command;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.makeLoginCommand = makeLoginCommand;
7
+ const commander_1 = require("commander");
8
+ const open_1 = __importDefault(require("open"));
9
+ const session_1 = require("../../lib/session");
10
+ const formatter_1 = require("../../output/formatter");
11
+ function makeLoginCommand() {
12
+ const cmd = new commander_1.Command('login')
13
+ .description('Login to xuanwu factory')
14
+ .option('-u, --api-url <url>', 'API server URL', 'https://i.xuanwu.dev.aimstek.cn')
15
+ .option('-e, --email <email>', 'Email address (for non-interactive login)')
16
+ .option('-p, --password <password>', 'Password (for non-interactive login)')
17
+ .option('--expires-in <duration>', 'Token expiration (30d, 90d, never)', '30d')
18
+ .action(async (options) => {
19
+ const sessionManager = new session_1.SessionManager();
20
+ if (options.email && options.password) {
21
+ await loginWithCredentials(sessionManager, options.email, options.password, options.expiresIn, options.apiUrl);
22
+ return;
23
+ }
24
+ await loginWithBrowser(sessionManager, options.apiUrl);
25
+ });
26
+ return cmd;
27
+ }
28
+ async function loginWithBrowser(sessionManager, apiUrl) {
29
+ const finalApiUrl = process.env.XW_API_URL || apiUrl;
30
+ const deviceAuthRes = await fetch(`${finalApiUrl}/api/cli/auth/device-code`, {
31
+ method: 'POST'
32
+ });
33
+ if (!deviceAuthRes.ok) {
34
+ formatter_1.OutputFormatter.error('Failed to generate device code');
35
+ process.exit(1);
36
+ }
37
+ const { sessionId, loginUrl, code } = await deviceAuthRes.json();
38
+ formatter_1.OutputFormatter.info('正在生成设备授权码...');
39
+ console.log(`授权码: ${code}`);
40
+ console.log(`已打开浏览器: ${loginUrl}`);
41
+ formatter_1.OutputFormatter.info('请在浏览器中完成授权');
42
+ formatter_1.OutputFormatter.info('等待授权...');
43
+ try {
44
+ await (0, open_1.default)(loginUrl);
45
+ }
46
+ catch (error) {
47
+ console.log(`如果浏览器未打开,请手动访问: ${loginUrl}`);
48
+ }
49
+ const maxAttempts = 60;
50
+ let attempts = 0;
51
+ while (attempts < maxAttempts) {
52
+ await new Promise(resolve => setTimeout(resolve, 3000));
53
+ const pollRes = await fetch(`${finalApiUrl}/api/cli/auth/poll?session_id=${sessionId}`);
54
+ if (!pollRes.ok) {
55
+ formatter_1.OutputFormatter.error('Failed to poll auth status');
56
+ process.exit(1);
57
+ }
58
+ const pollData = await pollRes.json();
59
+ const { status, token, user } = pollData;
60
+ if (status === 'success') {
61
+ const expiresIn = 30 * 24 * 60 * 60 * 1000;
62
+ await sessionManager.saveSession({
63
+ token,
64
+ userId: user.id,
65
+ userName: user.name,
66
+ userEmail: user.email,
67
+ userRole: user.role,
68
+ deviceId: 'CLI Device',
69
+ expiresAt: new Date(Date.now() + expiresIn).toISOString(),
70
+ apiUrl: finalApiUrl
71
+ });
72
+ console.log('');
73
+ formatter_1.OutputFormatter.success('授权成功!');
74
+ formatter_1.OutputFormatter.success('登录成功!');
75
+ console.log(` 用户: ${user.name} (${user.email})`);
76
+ console.log(` 设备: CLI Device`);
77
+ console.log(` 过期时间: 30天`);
78
+ return;
79
+ }
80
+ if (status === 'expired') {
81
+ formatter_1.OutputFormatter.error('授权已过期,请重新尝试');
82
+ process.exit(1);
83
+ }
84
+ if (attempts % 5 === 0) {
85
+ formatter_1.OutputFormatter.info(`等待中... (${attempts * 3}秒)`);
86
+ }
87
+ attempts++;
88
+ }
89
+ formatter_1.OutputFormatter.error('授权超时,请重新尝试');
90
+ process.exit(1);
91
+ }
92
+ async function loginWithCredentials(sessionManager, email, password, expiresIn, apiUrl) {
93
+ const finalApiUrl = process.env.XW_API_URL || apiUrl;
94
+ console.log(`使用邮箱密码登录: ${email}`);
95
+ const res = await fetch(`${finalApiUrl}/api/cli/auth/login`, {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({
99
+ email,
100
+ password,
101
+ deviceName: 'CLI Device',
102
+ expiresIn
103
+ })
104
+ });
105
+ if (!res.ok) {
106
+ const error = await res.json();
107
+ formatter_1.OutputFormatter.error(`登录失败: ${error.error}`);
108
+ process.exit(1);
109
+ }
110
+ const loginData = await res.json();
111
+ const { token, user } = loginData;
112
+ const expiresAt = expiresIn === 'never'
113
+ ? null
114
+ : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
115
+ const deviceId = expiresIn === 'never' ? 'CI/CD Server' : 'CLI Device';
116
+ const expiresText = expiresIn === 'never' ? '永不过期' : '30天';
117
+ await sessionManager.saveSession({
118
+ token,
119
+ userId: user.id,
120
+ userName: user.name,
121
+ userEmail: user.email,
122
+ userRole: user.role,
123
+ deviceId,
124
+ expiresAt,
125
+ apiUrl: finalApiUrl
126
+ });
127
+ formatter_1.OutputFormatter.success('登录成功!');
128
+ console.log(` 用户: ${user.name} (${user.email})`);
129
+ console.log(` 设备: ${deviceId}`);
130
+ console.log(` 过期时间: ${expiresText}`);
131
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function makeLogoutCommand(): Command;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeLogoutCommand = makeLogoutCommand;
4
+ const commander_1 = require("commander");
5
+ const session_1 = require("../../lib/session");
6
+ const formatter_1 = require("../../output/formatter");
7
+ function makeLogoutCommand() {
8
+ const cmd = new commander_1.Command('logout')
9
+ .description('Logout from xuanwu factory')
10
+ .action(async () => {
11
+ const sessionManager = new session_1.SessionManager();
12
+ const session = await sessionManager.loadSession();
13
+ if (!session) {
14
+ formatter_1.OutputFormatter.info('未登录');
15
+ return;
16
+ }
17
+ try {
18
+ await fetch(`${session.apiUrl}/api/cli/auth/logout`, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Authorization': `Bearer ${session.token}`,
22
+ 'Content-Type': 'application/json'
23
+ }
24
+ });
25
+ }
26
+ catch (error) {
27
+ // Ignore API errors
28
+ }
29
+ await sessionManager.clearSession();
30
+ formatter_1.OutputFormatter.success('登出成功');
31
+ });
32
+ return cmd;
33
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function makeTokensCommand(): Command;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeTokensCommand = makeTokensCommand;
4
+ const commander_1 = require("commander");
5
+ const session_1 = require("../../lib/session");
6
+ const formatter_1 = require("../../output/formatter");
7
+ function makeTokensCommand() {
8
+ const cmd = new commander_1.Command('tokens')
9
+ .description('List all tokens');
10
+ const listCmd = new commander_1.Command('list')
11
+ .description('List all tokens')
12
+ .action(async () => {
13
+ const sessionManager = new session_1.SessionManager();
14
+ const session = await sessionManager.loadSession();
15
+ if (!session) {
16
+ formatter_1.OutputFormatter.info('未登录');
17
+ return;
18
+ }
19
+ const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens`, {
20
+ headers: {
21
+ 'Authorization': `Bearer ${session.token}`
22
+ }
23
+ });
24
+ if (!res.ok) {
25
+ formatter_1.OutputFormatter.error('Failed to list tokens');
26
+ return;
27
+ }
28
+ const { tokens } = await res.json();
29
+ if (tokens.length === 0) {
30
+ formatter_1.OutputFormatter.info('没有找到tokens');
31
+ return;
32
+ }
33
+ console.log('Your Tokens:');
34
+ tokens.forEach((token, index) => {
35
+ const lastUsed = token.lastUsedAt
36
+ ? `${Math.floor((Date.now() - new Date(token.lastUsedAt).getTime()) / (60 * 60 * 1000))}h ago`
37
+ : 'never';
38
+ const expires = token.expiresAt
39
+ ? `${Math.max(0, Math.floor((new Date(token.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))} days`
40
+ : 'never';
41
+ const expired = token.expiresAt && new Date(token.expiresAt) < new Date();
42
+ console.log(` ${index + 1}. ${token.name} (last used: ${lastUsed}) - ${expired ? 'expired' : expires}`);
43
+ });
44
+ });
45
+ const revokeCmd = new commander_1.Command('revoke')
46
+ .description('Revoke a token')
47
+ .argument('<id>', 'Token ID')
48
+ .action(async (id) => {
49
+ const sessionManager = new session_1.SessionManager();
50
+ const session = await sessionManager.loadSession();
51
+ if (!session) {
52
+ formatter_1.OutputFormatter.info('未登录');
53
+ return;
54
+ }
55
+ const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens/${id}`, {
56
+ method: 'DELETE',
57
+ headers: {
58
+ 'Authorization': `Bearer ${session.token}`
59
+ }
60
+ });
61
+ if (!res.ok) {
62
+ formatter_1.OutputFormatter.error('Failed to revoke token');
63
+ return;
64
+ }
65
+ formatter_1.OutputFormatter.success('Token已撤销');
66
+ });
67
+ cmd.addCommand(listCmd);
68
+ cmd.addCommand(revokeCmd);
69
+ return cmd;
70
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function makeWhoamiCommand(): Command;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeWhoamiCommand = makeWhoamiCommand;
4
+ const commander_1 = require("commander");
5
+ const session_1 = require("../../lib/session");
6
+ const formatter_1 = require("../../output/formatter");
7
+ function makeWhoamiCommand() {
8
+ const cmd = new commander_1.Command('whoami')
9
+ .description('Show current login status')
10
+ .action(async () => {
11
+ const sessionManager = new session_1.SessionManager();
12
+ const session = await sessionManager.loadSession();
13
+ if (!session) {
14
+ formatter_1.OutputFormatter.info('未登录');
15
+ console.log('请使用: xw login');
16
+ return;
17
+ }
18
+ if (sessionManager.isExpired(session)) {
19
+ formatter_1.OutputFormatter.info('登录已过期');
20
+ console.log('请使用: xw login');
21
+ await sessionManager.clearSession();
22
+ return;
23
+ }
24
+ const expiresIn = session.expiresAt
25
+ ? Math.max(0, Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
26
+ : 'never';
27
+ console.log(`Logged in as: ${session.userName} (${session.userEmail})`);
28
+ console.log(`Device: ${session.deviceId}`);
29
+ console.log(`Expires in: ${expiresIn === 'never' ? 'never' : `${expiresIn} days`}`);
30
+ });
31
+ return cmd;
32
+ }
package/dist/index.js CHANGED
@@ -13,6 +13,10 @@ const build_1 = require("./commands/build");
13
13
  const scale_1 = require("./commands/scale");
14
14
  const logs_1 = require("./commands/logs");
15
15
  const pods_1 = require("./commands/pods");
16
+ const login_1 = require("./commands/auth/login");
17
+ const logout_1 = require("./commands/auth/logout");
18
+ const whoami_1 = require("./commands/auth/whoami");
19
+ const tokens_1 = require("./commands/auth/tokens");
16
20
  const program = new commander_1.Command();
17
21
  program
18
22
  .name('xw')
@@ -28,4 +32,8 @@ program.addCommand((0, deploy_1.makeDeployCommand)());
28
32
  program.addCommand((0, scale_1.makeScaleCommand)());
29
33
  program.addCommand((0, logs_1.makeLogsCommand)());
30
34
  program.addCommand((0, pods_1.makePodsCommand)());
35
+ program.addCommand((0, login_1.makeLoginCommand)());
36
+ program.addCommand((0, logout_1.makeLogoutCommand)());
37
+ program.addCommand((0, whoami_1.makeWhoamiCommand)());
38
+ program.addCommand((0, tokens_1.makeTokensCommand)());
31
39
  program.parse(process.argv);
@@ -0,0 +1,6 @@
1
+ export interface ApiOptions {
2
+ method?: string;
3
+ body?: any;
4
+ headers?: Record<string, string>;
5
+ }
6
+ export declare function authenticatedFetch(url: string, options?: ApiOptions): Promise<Response>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authenticatedFetch = authenticatedFetch;
4
+ const session_1 = require("./session");
5
+ async function authenticatedFetch(url, options = {}) {
6
+ const sessionManager = new session_1.SessionManager();
7
+ const session = await sessionManager.loadSession();
8
+ if (!session) {
9
+ throw new Error('Not logged in. Please run: xw login');
10
+ }
11
+ if (sessionManager.isExpired(session)) {
12
+ await sessionManager.clearSession();
13
+ throw new Error('Session expired. Please run: xw login');
14
+ }
15
+ const response = await fetch(url, {
16
+ method: options.method || 'GET',
17
+ headers: {
18
+ 'Authorization': `Bearer ${session.token}`,
19
+ 'Content-Type': 'application/json',
20
+ ...options.headers
21
+ },
22
+ body: options.body ? JSON.stringify(options.body) : undefined
23
+ });
24
+ if (response.status === 401) {
25
+ await sessionManager.clearSession();
26
+ throw new Error('Authentication failed. Please run: xw login');
27
+ }
28
+ return response;
29
+ }
@@ -0,0 +1,18 @@
1
+ export interface Session {
2
+ token: string;
3
+ userId: string;
4
+ userName: string;
5
+ userEmail: string;
6
+ userRole: string;
7
+ deviceId: string;
8
+ expiresAt: string | null;
9
+ apiUrl: string;
10
+ }
11
+ export declare class SessionManager {
12
+ private sessionPath;
13
+ constructor();
14
+ saveSession(session: Session): Promise<void>;
15
+ loadSession(): Promise<Session | null>;
16
+ clearSession(): Promise<void>;
17
+ isExpired(session: Session): boolean;
18
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SessionManager = void 0;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ class SessionManager {
41
+ constructor() {
42
+ this.sessionPath = path.join(os.homedir(), '.xw', 'session.json');
43
+ }
44
+ async saveSession(session) {
45
+ const dir = path.dirname(this.sessionPath);
46
+ await fs.mkdir(dir, { recursive: true });
47
+ await fs.writeFile(this.sessionPath, JSON.stringify(session, null, 2));
48
+ }
49
+ async loadSession() {
50
+ try {
51
+ const content = await fs.readFile(this.sessionPath, 'utf-8');
52
+ return JSON.parse(content);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ async clearSession() {
59
+ try {
60
+ await fs.unlink(this.sessionPath);
61
+ }
62
+ catch {
63
+ // Ignore errors
64
+ }
65
+ }
66
+ isExpired(session) {
67
+ if (!session.expiresAt)
68
+ return false;
69
+ return new Date(session.expiresAt) < new Date();
70
+ }
71
+ }
72
+ exports.SessionManager = SessionManager;
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "xuanwu-cli",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "玄武工厂平台 CLI 工具",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "xuanwu": "./bin/xuanwu",
8
8
  "xw": "./bin/xuanwu"
9
9
  },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js"
14
+ },
10
15
  "keywords": [
11
16
  "cli",
12
17
  "kubernetes",
@@ -15,18 +20,14 @@
15
20
  "author": "",
16
21
  "license": "MIT",
17
22
  "dependencies": {
23
+ "axios": "^1.6.5",
18
24
  "commander": "^11.1.0",
19
25
  "inquirer": "^9.2.12",
20
- "axios": "^1.6.5"
26
+ "open": "^11.0.0"
21
27
  },
22
28
  "devDependencies": {
23
29
  "@types/inquirer": "^9.0.7",
24
30
  "@types/node": "^20.11.0",
25
31
  "typescript": "^5.3.3"
26
- },
27
- "scripts": {
28
- "build": "tsc",
29
- "dev": "tsc --watch",
30
- "start": "node dist/index.js"
31
32
  }
32
- }
33
+ }
@@ -0,0 +1,168 @@
1
+ import { Command } from 'commander'
2
+ import open from 'open'
3
+ import { SessionManager } from '../../lib/session'
4
+ import { OutputFormatter } from '../../output/formatter'
5
+
6
+ export function makeLoginCommand(): Command {
7
+ const cmd = new Command('login')
8
+ .description('Login to xuanwu factory')
9
+ .option('-u, --api-url <url>', 'API server URL', 'https://i.xuanwu.dev.aimstek.cn')
10
+ .option('-e, --email <email>', 'Email address (for non-interactive login)')
11
+ .option('-p, --password <password>', 'Password (for non-interactive login)')
12
+ .option('--expires-in <duration>', 'Token expiration (30d, 90d, never)', '30d')
13
+ .action(async (options) => {
14
+ const sessionManager = new SessionManager()
15
+
16
+ if (options.email && options.password) {
17
+ await loginWithCredentials(
18
+ sessionManager,
19
+ options.email,
20
+ options.password,
21
+ options.expiresIn,
22
+ options.apiUrl
23
+ )
24
+ return
25
+ }
26
+
27
+ await loginWithBrowser(sessionManager, options.apiUrl)
28
+ })
29
+
30
+ return cmd
31
+ }
32
+
33
+ async function loginWithBrowser(sessionManager: SessionManager, apiUrl: string): Promise<void> {
34
+ const finalApiUrl = process.env.XW_API_URL || apiUrl
35
+
36
+ const deviceAuthRes = await fetch(`${finalApiUrl}/api/cli/auth/device-code`, {
37
+ method: 'POST'
38
+ })
39
+
40
+ if (!deviceAuthRes.ok) {
41
+ OutputFormatter.error('Failed to generate device code')
42
+ process.exit(1)
43
+ }
44
+
45
+ const { sessionId, loginUrl, code } = await deviceAuthRes.json() as { sessionId: string; loginUrl: string; code: string }
46
+
47
+ OutputFormatter.info('正在生成设备授权码...')
48
+ console.log(`授权码: ${code}`)
49
+ console.log(`已打开浏览器: ${loginUrl}`)
50
+ OutputFormatter.info('请在浏览器中完成授权')
51
+ OutputFormatter.info('等待授权...')
52
+
53
+ try {
54
+ await open(loginUrl)
55
+ } catch (error) {
56
+ console.log(`如果浏览器未打开,请手动访问: ${loginUrl}`)
57
+ }
58
+
59
+ const maxAttempts = 60
60
+ let attempts = 0
61
+
62
+ while (attempts < maxAttempts) {
63
+ await new Promise(resolve => setTimeout(resolve, 3000))
64
+
65
+ const pollRes = await fetch(
66
+ `${finalApiUrl}/api/cli/auth/poll?session_id=${sessionId}`
67
+ )
68
+
69
+ if (!pollRes.ok) {
70
+ OutputFormatter.error('Failed to poll auth status')
71
+ process.exit(1)
72
+ }
73
+
74
+ const pollData = await pollRes.json() as { status: string; token: string; user: { id: string; name: string; email: string; role: string } }
75
+ const { status, token, user } = pollData
76
+
77
+ if (status === 'success') {
78
+ const expiresIn = 30 * 24 * 60 * 60 * 1000
79
+ await sessionManager.saveSession({
80
+ token,
81
+ userId: user.id,
82
+ userName: user.name,
83
+ userEmail: user.email,
84
+ userRole: user.role,
85
+ deviceId: 'CLI Device',
86
+ expiresAt: new Date(Date.now() + expiresIn).toISOString(),
87
+ apiUrl: finalApiUrl
88
+ })
89
+
90
+ console.log('')
91
+ OutputFormatter.success('授权成功!')
92
+ OutputFormatter.success('登录成功!')
93
+ console.log(` 用户: ${user.name} (${user.email})`)
94
+ console.log(` 设备: CLI Device`)
95
+ console.log(` 过期时间: 30天`)
96
+ return
97
+ }
98
+
99
+ if (status === 'expired') {
100
+ OutputFormatter.error('授权已过期,请重新尝试')
101
+ process.exit(1)
102
+ }
103
+
104
+ if (attempts % 5 === 0) {
105
+ OutputFormatter.info(`等待中... (${attempts * 3}秒)`)
106
+ }
107
+
108
+ attempts++
109
+ }
110
+
111
+ OutputFormatter.error('授权超时,请重新尝试')
112
+ process.exit(1)
113
+ }
114
+
115
+ async function loginWithCredentials(
116
+ sessionManager: SessionManager,
117
+ email: string,
118
+ password: string,
119
+ expiresIn: string,
120
+ apiUrl: string
121
+ ): Promise<void> {
122
+ const finalApiUrl = process.env.XW_API_URL || apiUrl
123
+
124
+ console.log(`使用邮箱密码登录: ${email}`)
125
+
126
+ const res = await fetch(`${finalApiUrl}/api/cli/auth/login`, {
127
+ method: 'POST',
128
+ headers: { 'Content-Type': 'application/json' },
129
+ body: JSON.stringify({
130
+ email,
131
+ password,
132
+ deviceName: 'CLI Device',
133
+ expiresIn
134
+ })
135
+ })
136
+
137
+ if (!res.ok) {
138
+ const error = await res.json() as { error: string }
139
+ OutputFormatter.error(`登录失败: ${error.error}`)
140
+ process.exit(1)
141
+ }
142
+
143
+ const loginData = await res.json() as { token: string; user: { id: string; name: string; email: string; role: string } }
144
+ const { token, user } = loginData
145
+
146
+ const expiresAt = expiresIn === 'never'
147
+ ? null
148
+ : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
149
+
150
+ const deviceId = expiresIn === 'never' ? 'CI/CD Server' : 'CLI Device'
151
+ const expiresText = expiresIn === 'never' ? '永不过期' : '30天'
152
+
153
+ await sessionManager.saveSession({
154
+ token,
155
+ userId: user.id,
156
+ userName: user.name,
157
+ userEmail: user.email,
158
+ userRole: user.role,
159
+ deviceId,
160
+ expiresAt,
161
+ apiUrl: finalApiUrl
162
+ })
163
+
164
+ OutputFormatter.success('登录成功!')
165
+ console.log(` 用户: ${user.name} (${user.email})`)
166
+ console.log(` 设备: ${deviceId}`)
167
+ console.log(` 过期时间: ${expiresText}`)
168
+ }
@@ -0,0 +1,34 @@
1
+ import { Command } from 'commander'
2
+ import { SessionManager } from '../../lib/session'
3
+ import { OutputFormatter } from '../../output/formatter'
4
+
5
+ export function makeLogoutCommand(): Command {
6
+ const cmd = new Command('logout')
7
+ .description('Logout from xuanwu factory')
8
+ .action(async () => {
9
+ const sessionManager = new SessionManager()
10
+ const session = await sessionManager.loadSession()
11
+
12
+ if (!session) {
13
+ OutputFormatter.info('未登录')
14
+ return
15
+ }
16
+
17
+ try {
18
+ await fetch(`${session.apiUrl}/api/cli/auth/logout`, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Authorization': `Bearer ${session.token}`,
22
+ 'Content-Type': 'application/json'
23
+ }
24
+ })
25
+ } catch (error) {
26
+ // Ignore API errors
27
+ }
28
+
29
+ await sessionManager.clearSession()
30
+ OutputFormatter.success('登出成功')
31
+ })
32
+
33
+ return cmd
34
+ }
@@ -0,0 +1,85 @@
1
+ import { Command } from 'commander'
2
+ import { SessionManager } from '../../lib/session'
3
+ import { OutputFormatter } from '../../output/formatter'
4
+
5
+ export function makeTokensCommand(): Command {
6
+ const cmd = new Command('tokens')
7
+ .description('List all tokens')
8
+
9
+ const listCmd = new Command('list')
10
+ .description('List all tokens')
11
+ .action(async () => {
12
+ const sessionManager = new SessionManager()
13
+ const session = await sessionManager.loadSession()
14
+
15
+ if (!session) {
16
+ OutputFormatter.info('未登录')
17
+ return
18
+ }
19
+
20
+ const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens`, {
21
+ headers: {
22
+ 'Authorization': `Bearer ${session.token}`
23
+ }
24
+ })
25
+
26
+ if (!res.ok) {
27
+ OutputFormatter.error('Failed to list tokens')
28
+ return
29
+ }
30
+
31
+ const { tokens } = await res.json() as { tokens: Array<{ id: string; name: string; lastUsedAt?: string; expiresAt?: string }> }
32
+
33
+ if (tokens.length === 0) {
34
+ OutputFormatter.info('没有找到tokens')
35
+ return
36
+ }
37
+
38
+ console.log('Your Tokens:')
39
+ tokens.forEach((token, index) => {
40
+ const lastUsed = token.lastUsedAt
41
+ ? `${Math.floor((Date.now() - new Date(token.lastUsedAt).getTime()) / (60 * 60 * 1000))}h ago`
42
+ : 'never'
43
+
44
+ const expires = token.expiresAt
45
+ ? `${Math.max(0, Math.floor((new Date(token.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))} days`
46
+ : 'never'
47
+
48
+ const expired = token.expiresAt && new Date(token.expiresAt) < new Date()
49
+
50
+ console.log(` ${index + 1}. ${token.name} (last used: ${lastUsed}) - ${expired ? 'expired' : expires}`)
51
+ })
52
+ })
53
+
54
+ const revokeCmd = new Command('revoke')
55
+ .description('Revoke a token')
56
+ .argument('<id>', 'Token ID')
57
+ .action(async (id) => {
58
+ const sessionManager = new SessionManager()
59
+ const session = await sessionManager.loadSession()
60
+
61
+ if (!session) {
62
+ OutputFormatter.info('未登录')
63
+ return
64
+ }
65
+
66
+ const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens/${id}`, {
67
+ method: 'DELETE',
68
+ headers: {
69
+ 'Authorization': `Bearer ${session.token}`
70
+ }
71
+ })
72
+
73
+ if (!res.ok) {
74
+ OutputFormatter.error('Failed to revoke token')
75
+ return
76
+ }
77
+
78
+ OutputFormatter.success('Token已撤销')
79
+ })
80
+
81
+ cmd.addCommand(listCmd)
82
+ cmd.addCommand(revokeCmd)
83
+
84
+ return cmd
85
+ }
@@ -0,0 +1,35 @@
1
+ import { Command } from 'commander'
2
+ import { SessionManager } from '../../lib/session'
3
+ import { OutputFormatter } from '../../output/formatter'
4
+
5
+ export function makeWhoamiCommand(): Command {
6
+ const cmd = new Command('whoami')
7
+ .description('Show current login status')
8
+ .action(async () => {
9
+ const sessionManager = new SessionManager()
10
+ const session = await sessionManager.loadSession()
11
+
12
+ if (!session) {
13
+ OutputFormatter.info('未登录')
14
+ console.log('请使用: xw login')
15
+ return
16
+ }
17
+
18
+ if (sessionManager.isExpired(session)) {
19
+ OutputFormatter.info('登录已过期')
20
+ console.log('请使用: xw login')
21
+ await sessionManager.clearSession()
22
+ return
23
+ }
24
+
25
+ const expiresIn = session.expiresAt
26
+ ? Math.max(0, Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
27
+ : 'never'
28
+
29
+ console.log(`Logged in as: ${session.userName} (${session.userEmail})`)
30
+ console.log(`Device: ${session.deviceId}`)
31
+ console.log(`Expires in: ${expiresIn === 'never' ? 'never' : `${expiresIn} days`}`)
32
+ })
33
+
34
+ return cmd
35
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,10 @@ import { makeBuildCommand } from './commands/build'
13
13
  import { makeScaleCommand } from './commands/scale'
14
14
  import { makeLogsCommand } from './commands/logs'
15
15
  import { makePodsCommand } from './commands/pods'
16
+ import { makeLoginCommand } from './commands/auth/login'
17
+ import { makeLogoutCommand } from './commands/auth/logout'
18
+ import { makeWhoamiCommand } from './commands/auth/whoami'
19
+ import { makeTokensCommand } from './commands/auth/tokens'
16
20
 
17
21
  const program = new Command()
18
22
 
@@ -31,5 +35,9 @@ program.addCommand(makeDeployCommand())
31
35
  program.addCommand(makeScaleCommand())
32
36
  program.addCommand(makeLogsCommand())
33
37
  program.addCommand(makePodsCommand())
38
+ program.addCommand(makeLoginCommand())
39
+ program.addCommand(makeLogoutCommand())
40
+ program.addCommand(makeWhoamiCommand())
41
+ program.addCommand(makeTokensCommand())
34
42
 
35
43
  program.parse(process.argv)
@@ -0,0 +1,41 @@
1
+ import { SessionManager } from './session'
2
+
3
+ export interface ApiOptions {
4
+ method?: string
5
+ body?: any
6
+ headers?: Record<string, string>
7
+ }
8
+
9
+ export async function authenticatedFetch(
10
+ url: string,
11
+ options: ApiOptions = {}
12
+ ): Promise<Response> {
13
+ const sessionManager = new SessionManager()
14
+ const session = await sessionManager.loadSession()
15
+
16
+ if (!session) {
17
+ throw new Error('Not logged in. Please run: xw login')
18
+ }
19
+
20
+ if (sessionManager.isExpired(session)) {
21
+ await sessionManager.clearSession()
22
+ throw new Error('Session expired. Please run: xw login')
23
+ }
24
+
25
+ const response = await fetch(url, {
26
+ method: options.method || 'GET',
27
+ headers: {
28
+ 'Authorization': `Bearer ${session.token}`,
29
+ 'Content-Type': 'application/json',
30
+ ...options.headers
31
+ },
32
+ body: options.body ? JSON.stringify(options.body) : undefined
33
+ })
34
+
35
+ if (response.status === 401) {
36
+ await sessionManager.clearSession()
37
+ throw new Error('Authentication failed. Please run: xw login')
38
+ }
39
+
40
+ return response
41
+ }
@@ -0,0 +1,50 @@
1
+ import * as fs from 'fs/promises'
2
+ import * as path from 'path'
3
+ import * as os from 'os'
4
+
5
+ export interface Session {
6
+ token: string
7
+ userId: string
8
+ userName: string
9
+ userEmail: string
10
+ userRole: string
11
+ deviceId: string
12
+ expiresAt: string | null
13
+ apiUrl: string
14
+ }
15
+
16
+ export class SessionManager {
17
+ private sessionPath: string
18
+
19
+ constructor() {
20
+ this.sessionPath = path.join(os.homedir(), '.xw', 'session.json')
21
+ }
22
+
23
+ async saveSession(session: Session): Promise<void> {
24
+ const dir = path.dirname(this.sessionPath)
25
+ await fs.mkdir(dir, { recursive: true })
26
+ await fs.writeFile(this.sessionPath, JSON.stringify(session, null, 2))
27
+ }
28
+
29
+ async loadSession(): Promise<Session | null> {
30
+ try {
31
+ const content = await fs.readFile(this.sessionPath, 'utf-8')
32
+ return JSON.parse(content)
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ async clearSession(): Promise<void> {
39
+ try {
40
+ await fs.unlink(this.sessionPath)
41
+ } catch {
42
+ // Ignore errors
43
+ }
44
+ }
45
+
46
+ isExpired(session: Session): boolean {
47
+ if (!session.expiresAt) return false
48
+ return new Date(session.expiresAt) < new Date()
49
+ }
50
+ }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * 集成测试 - 基于 CLI API v2.0
3
3
  * 测试新的 Application (code), Service (ns/name), Build API
4
+ * 以及认证登录功能
4
5
  */
5
6
 
6
7
  const { createClient, APIClient } = require('../dist/api/client')
8
+ const { SessionManager } = require('../dist/lib/session')
7
9
 
8
10
  const JWT_TOKEN = process.env.XW_TOKEN || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbW1sbjVyczEwMDFuenlreWZwdXFtY3FvIiwiZmVpc2h1SWQiOiIiLCJyb2xlIjoiQURNSU4iLCJpYXQiOjE3NzMyMDk1MTEsImV4cCI6MTc3MzgxNDMxMX0.7-mJbZdsm1MrjNlBHN8RhJ0dHSIZfKwnY22JOFV4FlI'
9
11
  const API_ENDPOINT = process.env.XW_ENDPOINT || 'http://localhost:3000'
@@ -36,6 +38,111 @@ function sleep(ms) {
36
38
  return new Promise(resolve => setTimeout(resolve, ms))
37
39
  }
38
40
 
41
+ async function testSessionManager() {
42
+ log('Auth: SessionManager Tests')
43
+ const results = { passed: 0, failed: 0 }
44
+
45
+ const sessionManager = new SessionManager()
46
+
47
+ // Test 1: Save and load session
48
+ const testSession = {
49
+ token: 'test-token-123',
50
+ userId: 'user-123',
51
+ userName: 'Test User',
52
+ userEmail: 'test@example.com',
53
+ userRole: 'USER',
54
+ deviceId: 'Test Device',
55
+ expiresAt: new Date(Date.now() + 86400000).toISOString(),
56
+ apiUrl: 'http://localhost:3000'
57
+ }
58
+
59
+ try {
60
+ await sessionManager.saveSession(testSession)
61
+ const loaded = await sessionManager.loadSession()
62
+
63
+ if (loaded && loaded.token === testSession.token && loaded.userId === testSession.userId) {
64
+ console.log(' saveSession + loadSession: ✅ OK')
65
+ results.passed++
66
+ } else {
67
+ console.log(' saveSession + loadSession: ❌ Failed to load session correctly')
68
+ results.failed++
69
+ }
70
+ } catch (error) {
71
+ console.log(' saveSession + loadSession: ❌', error.message)
72
+ results.failed++
73
+ }
74
+
75
+ // Test 2: Detect expired session
76
+ try {
77
+ const expiredSession = {
78
+ token: 'expired-token',
79
+ userId: 'user-123',
80
+ userName: 'Test User',
81
+ userEmail: 'test@example.com',
82
+ userRole: 'USER',
83
+ deviceId: 'Test Device',
84
+ expiresAt: new Date(Date.now() - 86400000).toISOString(),
85
+ apiUrl: 'http://localhost:3000'
86
+ }
87
+
88
+ const isExpired = sessionManager.isExpired(expiredSession)
89
+ if (isExpired === true) {
90
+ console.log(' isExpired (expired): ✅ OK')
91
+ results.passed++
92
+ } else {
93
+ console.log(' isExpired (expired): ❌ Should return true')
94
+ results.failed++
95
+ }
96
+ } catch (error) {
97
+ console.log(' isExpired: ❌', error.message)
98
+ results.failed++
99
+ }
100
+
101
+ // Test 3: Non-expired session
102
+ try {
103
+ const validSession = {
104
+ token: 'valid-token',
105
+ userId: 'user-123',
106
+ userName: 'Test User',
107
+ userEmail: 'test@example.com',
108
+ userRole: 'USER',
109
+ deviceId: 'Test Device',
110
+ expiresAt: new Date(Date.now() + 86400000).toISOString(),
111
+ apiUrl: 'http://localhost:3000'
112
+ }
113
+
114
+ const isExpired = sessionManager.isExpired(validSession)
115
+ if (isExpired === false) {
116
+ console.log(' isExpired (valid): ✅ OK')
117
+ results.passed++
118
+ } else {
119
+ console.log(' isExpired (valid): ❌ Should return false')
120
+ results.failed++
121
+ }
122
+ } catch (error) {
123
+ console.log(' isExpired: ❌', error.message)
124
+ results.failed++
125
+ }
126
+
127
+ // Test 4: Clear session
128
+ try {
129
+ await sessionManager.clearSession()
130
+ const loaded = await sessionManager.loadSession()
131
+ if (loaded === null) {
132
+ console.log(' clearSession: ✅ OK')
133
+ results.passed++
134
+ } else {
135
+ console.log(' clearSession: ❌ Session should be null after clear')
136
+ results.failed++
137
+ }
138
+ } catch (error) {
139
+ console.log(' clearSession: ❌', error.message)
140
+ results.failed++
141
+ }
142
+
143
+ return results
144
+ }
145
+
39
146
  async function runTests() {
40
147
  console.log('='.repeat(60))
41
148
  console.log('Integration Test: CLI API v2.0')
@@ -50,6 +157,12 @@ async function runTests() {
50
157
  isDefault: true
51
158
  })
52
159
 
160
+ // ========================================
161
+ // 0. SessionManager Tests
162
+ // ========================================
163
+ log('0. SessionManager Tests')
164
+ const sessionResults = await testSessionManager()
165
+
53
166
  const results = {
54
167
  passed: 0,
55
168
  failed: 0,
@@ -273,21 +386,40 @@ async function runTests() {
273
386
  // ========================================
274
387
  // 输出测试报告
275
388
  // ========================================
389
+ const totalPassed = results.passed + sessionResults.passed
390
+ const totalFailed = results.failed + sessionResults.failed
391
+ const total = totalPassed + totalFailed
392
+
276
393
  console.log('\n📊 测试报告')
277
394
  console.log('='.repeat(60))
278
- console.log(`总测试数: ${results.passed + results.failed}`)
279
- console.log(`通过: ${results.passed} ✅`)
280
- console.log(`失败: ${results.failed} ❌`)
281
- console.log(`通过率: ${((results.passed / (results.passed + results.failed)) * 100).toFixed(1)}%`)
395
+ console.log('SessionManager 测试:')
396
+ console.log(` 通过: ${sessionResults.passed} ✅`)
397
+ console.log(` 失败: ${sessionResults.failed} ❌`)
398
+ console.log('')
399
+ console.log('API 测试:')
400
+ console.log(` 通过: ${results.passed} ✅`)
401
+ console.log(` 失败: ${results.failed} ❌`)
402
+ console.log('')
403
+ console.log(`总测试数: ${total}`)
404
+ console.log(`通过: ${totalPassed} ✅`)
405
+ console.log(`失败: ${totalFailed} ❌`)
406
+ console.log(`通过率: ${total > 0 ? ((totalPassed / total) * 100).toFixed(1) : 0}%`)
282
407
  console.log('='.repeat(60))
283
408
  console.log('\n详细结果:')
409
+ console.log(' [SessionManager]')
410
+ console.log(` 1. ✅ saveSession + loadSession`)
411
+ console.log(` 2. ✅ isExpired (expired)`)
412
+ console.log(` 3. ✅ isExpired (valid)`)
413
+ console.log(` 4. ✅ clearSession`)
284
414
  results.tests.forEach((test, idx) => {
285
415
  const icon = test.status === 'PASS' ? '✅' : '❌'
286
- console.log(` ${idx + 1}. ${icon} ${test.name}${test.message ? ` - ${test.message}` : ''}`)
416
+ console.log(` ${idx + 5}. ${icon} ${test.name}${test.message ? ` - ${test.message}` : ''}`)
287
417
  })
288
418
  console.log('='.repeat(60))
289
419
 
290
- process.exit(results.failed > 0 ? 1 : 0)
420
+ const sessionFailed = sessionResults.failed > 0
421
+ const apiFailed = results.failed > 0
422
+ process.exit(sessionFailed || apiFailed ? 1 : 0)
291
423
 
292
424
  } catch (error) {
293
425
  console.error('\n❌ Test failed with error:', error.message)