@dreamor/atlas-cli 0.7.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 +84 -0
- package/bin/atlas.js +5 -0
- package/dist/adapters/atlas/auth/index.js +2 -0
- package/dist/adapters/atlas/auth/login.js +107 -0
- package/dist/adapters/atlas/auth/session.js +154 -0
- package/dist/adapters/atlas/cli.js +502 -0
- package/dist/adapters/atlas/commands/_output_schema.js +100 -0
- package/dist/adapters/atlas/commands/actual/_logic.js +41 -0
- package/dist/adapters/atlas/commands/actual/index.js +117 -0
- package/dist/adapters/atlas/commands/auth.js +1 -0
- package/dist/adapters/atlas/commands/baseline/index.js +122 -0
- package/dist/adapters/atlas/commands/compare/_logic.js +39 -0
- package/dist/adapters/atlas/commands/compare/index.js +72 -0
- package/dist/adapters/atlas/commands/exec.js +58 -0
- package/dist/adapters/atlas/commands/project/index.js +179 -0
- package/dist/adapters/atlas/commands/schema.js +30 -0
- package/dist/adapters/atlas/commands/suggest.js +56 -0
- package/dist/adapters/atlas/commands/update.js +106 -0
- package/dist/adapters/atlas/daemon/index.js +64 -0
- package/dist/adapters/atlas/dict/index.js +41 -0
- package/dist/adapters/atlas/http/client.js +151 -0
- package/dist/adapters/atlas/http/index.js +1 -0
- package/dist/adapters/atlas/schema/actual.js +16 -0
- package/dist/adapters/atlas/schema/baseline.js +34 -0
- package/dist/adapters/atlas/schema/department.js +11 -0
- package/dist/adapters/atlas/schema/index.js +4 -0
- package/dist/adapters/atlas/schema/project.js +13 -0
- package/dist/adapters/atlas/util/constants.js +4 -0
- package/dist/adapters/atlas/util/env.js +8 -0
- package/dist/adapters/atlas/util/errors.js +45 -0
- package/dist/adapters/atlas/util/helpers.js +17 -0
- package/dist/adapters/atlas/util/months.js +41 -0
- package/dist/adapters/atlas/util/output-limit.js +20 -0
- package/dist/adapters/atlas/util/output.js +67 -0
- package/dist/adapters/atlas/util/paths.js +40 -0
- package/dist/adapters/atlas/util/secure-fs.js +41 -0
- package/dist/adapters/atlas/util/time.js +17 -0
- package/dist/adapters/atlas/util/version.js +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Atlas CLI
|
|
2
|
+
|
|
3
|
+
> 斑马云图(Banma)人力基线管理工具
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @dreamor/atlas-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> Node 20+ 是引擎基线(package.json 已声明 `engines.node: >=20`)。
|
|
12
|
+
|
|
13
|
+
## 升级
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm update -g @dreamor/atlas-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 首次使用
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
atlas --help
|
|
23
|
+
atlas auth login # 打开浏览器完成 SSO 登录
|
|
24
|
+
atlas auth status # 检查登录态
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`auth login` 需要 Playwright 启动无头浏览器(仅首次):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm i -g playwright
|
|
31
|
+
npx playwright install chromium
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
未安装时 CLI 会给清晰提示,不会阻塞主命令的安装与运行。
|
|
35
|
+
|
|
36
|
+
## 会话存储
|
|
37
|
+
|
|
38
|
+
- macOS:默认使用 Keychain(依赖 `keytar`,通常随 Atlas 安装自动就绪)
|
|
39
|
+
- 其他平台或 `keytar` 安装失败:自动降级到文件存储(`~/.atlas/cookies.json`)
|
|
40
|
+
|
|
41
|
+
## 命令速览
|
|
42
|
+
|
|
43
|
+
| 命令 | 说明 |
|
|
44
|
+
|------|------|
|
|
45
|
+
| `atlas --help` | 帮助 |
|
|
46
|
+
| `atlas auth {login,status}` | SSO 登录/状态 |
|
|
47
|
+
| `atlas find <kind> <query>` | 搜索 project / department / mp-type / line-plan-type / area-code |
|
|
48
|
+
| `atlas projects` | 列出你有权限的项目 |
|
|
49
|
+
| `atlas link [project]` / `unlink` | 绑定/解绑默认项目 |
|
|
50
|
+
| `atlas baseline month/summary/export` | 基线(计划)人力 |
|
|
51
|
+
| `atlas actual show/month/summary/export` | 实际工时 |
|
|
52
|
+
| `atlas compare` | 基线 vs 实际对比 |
|
|
53
|
+
| `atlas schema {export,commands}` | CLI 自省(字典/参数 schema) |
|
|
54
|
+
| `atlas daemon` | 本地守护进程 |
|
|
55
|
+
| `atlas exec --plan-file <path>` | 批量执行(agent 用) |
|
|
56
|
+
| `atlas update` | 由 npm registry 管理,CLI 只提示更新命令 |
|
|
57
|
+
| `atlas suggest <query>` | 自然语言→命令翻译 |
|
|
58
|
+
|
|
59
|
+
## 全局选项
|
|
60
|
+
|
|
61
|
+
- `--json` / `ATLAS_OUTPUT=json`:JSON 信封输出
|
|
62
|
+
- `--quiet` / `ATLAS_QUIET=1`:静默
|
|
63
|
+
- `--describe`:输出参数 schema(agent 自省)
|
|
64
|
+
|
|
65
|
+
## 退出码
|
|
66
|
+
|
|
67
|
+
| 码 | 含义 |
|
|
68
|
+
|----|------|
|
|
69
|
+
| 0 | 成功 |
|
|
70
|
+
| 1 | 通用错误 |
|
|
71
|
+
| 2 | 会话过期 |
|
|
72
|
+
| 3 | API 错误 |
|
|
73
|
+
| 4 | 项目匹配歧义 |
|
|
74
|
+
| 5 | 项目未找到 |
|
|
75
|
+
| 6 | API 限流 |
|
|
76
|
+
| 7 | 网络错误 |
|
|
77
|
+
| 8 | 版本更新异常 |
|
|
78
|
+
| 64 | 配置错误/未实现 |
|
|
79
|
+
|
|
80
|
+
## 文档
|
|
81
|
+
|
|
82
|
+
- [INSTALL.md](./INSTALL.md) — 安装/环境变量/故障排查
|
|
83
|
+
- [CHANGELOG.md](./CHANGELOG.md) — 变更日志
|
|
84
|
+
- [CLAUDE.md](./CLAUDE.md) — 开发者指南(架构、命令、设计决策)
|
package/bin/atlas.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { writeCookies, readCookies, fetchCookiesFromDaemon } from './session.js';
|
|
2
|
+
import { isJsonMode, jsonOk, log } from '../util/output.js';
|
|
3
|
+
const BANMA_HOST = 'banma-yuntu.alibaba-inc.com';
|
|
4
|
+
// 白名单:只保存 getBanmaIdentity 真正需要的 cookies,避免整域 cookies 落盘
|
|
5
|
+
const WANT_COOKIES = new Set(['access_token', 'buc_username', 'buc_userinfo']);
|
|
6
|
+
const EMP_ID_NAMES = new Set(['emp_id', 'empId', 'employeeId']);
|
|
7
|
+
/**
|
|
8
|
+
* 动态加载 playwright。顶层静态 import 会让单文件 bundle 在未安装 playwright 的
|
|
9
|
+
* 环境下无法启动(连 --version 都 require 失败),故改为运行时动态 import + try/catch
|
|
10
|
+
* 降级:只在真正执行 `auth login` 时才需要 playwright,未安装则给出清晰提示。
|
|
11
|
+
*/
|
|
12
|
+
async function loadChromium() {
|
|
13
|
+
try {
|
|
14
|
+
const pw = await import('playwright');
|
|
15
|
+
return pw.chromium;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error("未安装 playwright。运行时需要:设 ATLAS_AUTO_BOOTSTRAP=1 自动安装,或执行 `npm install playwright && npx playwright install chromium`");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function login(_port) {
|
|
22
|
+
log('正在打开浏览器进行 Banma SSO 登录...');
|
|
23
|
+
const chromium = await loadChromium();
|
|
24
|
+
const browser = await chromium.launch({
|
|
25
|
+
headless: false,
|
|
26
|
+
});
|
|
27
|
+
const page = await browser.newPage();
|
|
28
|
+
log('请在浏览器中完成 SSO 登录...');
|
|
29
|
+
log('登录成功后页面会自动跳转,CLI 会自动捕获 cookies');
|
|
30
|
+
await page.goto(`https://${BANMA_HOST}/`, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
31
|
+
// 等待 access_token cookie 出现(最多 120s),比脆弱的 url 判断 + 固定 timeout 更可靠
|
|
32
|
+
await page.waitForFunction(() => document.cookie.includes('access_token'), { timeout: 120_000 });
|
|
33
|
+
// 额外等待确保残留 cookie 写入完毕
|
|
34
|
+
await page.waitForTimeout(1000);
|
|
35
|
+
// Extract cookies — 精确白名单,避免整域 cookies 落盘
|
|
36
|
+
const cookies = await page.context().cookies();
|
|
37
|
+
const atlasCookies = cookies
|
|
38
|
+
.filter((c) => WANT_COOKIES.has(c.name))
|
|
39
|
+
.map((c) => ({
|
|
40
|
+
name: c.name,
|
|
41
|
+
value: c.value,
|
|
42
|
+
domain: c.domain,
|
|
43
|
+
}));
|
|
44
|
+
await writeCookies(atlasCookies);
|
|
45
|
+
await browser.close();
|
|
46
|
+
log('SSO 登录成功!');
|
|
47
|
+
log(`已保存 ${atlasCookies.length} 个 cookies`);
|
|
48
|
+
}
|
|
49
|
+
export async function loginStatus() {
|
|
50
|
+
// Try daemon first
|
|
51
|
+
try {
|
|
52
|
+
const daemonCookies = await fetchCookiesFromDaemon();
|
|
53
|
+
if (daemonCookies && daemonCookies.length > 0) {
|
|
54
|
+
const empIdCookie = daemonCookies.find((c) => EMP_ID_NAMES.has(c.name));
|
|
55
|
+
return {
|
|
56
|
+
loggedIn: true,
|
|
57
|
+
account: empIdCookie?.value ?? 'via-daemon',
|
|
58
|
+
mode: 'daemon',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Daemon not running
|
|
64
|
+
}
|
|
65
|
+
// Check local cookies
|
|
66
|
+
const cookies = await readCookies();
|
|
67
|
+
if (cookies && cookies.length > 0) {
|
|
68
|
+
const empIdCookie = cookies.find((c) => EMP_ID_NAMES.has(c.name));
|
|
69
|
+
return {
|
|
70
|
+
loggedIn: true,
|
|
71
|
+
account: empIdCookie?.value ?? 'cookies-present',
|
|
72
|
+
mode: 'local',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { loggedIn: false };
|
|
76
|
+
}
|
|
77
|
+
export async function loginCmd(opts) {
|
|
78
|
+
// Try daemon first if cookies available
|
|
79
|
+
const daemonCookies = await fetchCookiesFromDaemon();
|
|
80
|
+
if (daemonCookies && daemonCookies.length > 0) {
|
|
81
|
+
await writeCookies(daemonCookies);
|
|
82
|
+
log('已从 daemon 获取 cookies');
|
|
83
|
+
if (opts.json || isJsonMode()) {
|
|
84
|
+
jsonOk({ status: 'logged_in', via: 'daemon' });
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await login();
|
|
89
|
+
if (opts.json || isJsonMode()) {
|
|
90
|
+
jsonOk({ status: 'logged_in' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function statusCmd(opts) {
|
|
94
|
+
const status = await loginStatus();
|
|
95
|
+
if (opts.json || isJsonMode()) {
|
|
96
|
+
jsonOk(status);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (status.loggedIn) {
|
|
100
|
+
log(`已登录 (${status.mode === 'daemon' ? 'daemon' : 'local cookies'})`);
|
|
101
|
+
if (status.account)
|
|
102
|
+
log(`账号: ${status.account}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
log('未登录,请执行 atlas auth login');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readFile, unlink } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { secureMkdir, secureWriteFile } from '../util/secure-fs.js';
|
|
4
|
+
import { getAtlasHome, getCookieFile } from '../util/paths.js';
|
|
5
|
+
const SESSION_DIR = getAtlasHome();
|
|
6
|
+
const COOKIE_FILE = getCookieFile();
|
|
7
|
+
let cachedCookies = null;
|
|
8
|
+
/**
|
|
9
|
+
* 读取持久化的 cookies(从本地文件)
|
|
10
|
+
* Playwright SSO 登录后将 cookies 写入此文件
|
|
11
|
+
*/
|
|
12
|
+
export async function readCookies() {
|
|
13
|
+
if (cachedCookies)
|
|
14
|
+
return cachedCookies;
|
|
15
|
+
try {
|
|
16
|
+
if (!existsSync(COOKIE_FILE))
|
|
17
|
+
return null;
|
|
18
|
+
const data = await readFile(COOKIE_FILE, 'utf-8');
|
|
19
|
+
const cookies = JSON.parse(data);
|
|
20
|
+
cachedCookies = cookies;
|
|
21
|
+
return cookies;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function writeCookies(cookies) {
|
|
28
|
+
cachedCookies = cookies;
|
|
29
|
+
await secureMkdir(SESSION_DIR, { recursive: true });
|
|
30
|
+
await secureWriteFile(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
|
31
|
+
}
|
|
32
|
+
export async function clearCookies() {
|
|
33
|
+
cachedCookies = null;
|
|
34
|
+
try {
|
|
35
|
+
await unlink(COOKIE_FILE);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 从 daemon 获取 cookies(附带 daemon.token 鉴权)
|
|
43
|
+
*/
|
|
44
|
+
export async function fetchCookiesFromDaemon(port = 8765) {
|
|
45
|
+
try {
|
|
46
|
+
// 读取 daemon token 用于 Authorization header
|
|
47
|
+
const { getDaemonTokenFile } = await import('../util/paths.js');
|
|
48
|
+
const tokenFile = getDaemonTokenFile();
|
|
49
|
+
let token = '';
|
|
50
|
+
if (existsSync(tokenFile)) {
|
|
51
|
+
token = (await readFile(tokenFile, 'utf-8')).trim();
|
|
52
|
+
}
|
|
53
|
+
const { request } = await import('undici');
|
|
54
|
+
const headers = {};
|
|
55
|
+
if (token)
|
|
56
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
57
|
+
const resp = await request(`http://localhost:${port}/api/cookies`, {
|
|
58
|
+
headers,
|
|
59
|
+
headersTimeout: 5000,
|
|
60
|
+
bodyTimeout: 5000,
|
|
61
|
+
});
|
|
62
|
+
const text = await resp.body.text();
|
|
63
|
+
const json = JSON.parse(text);
|
|
64
|
+
if (json?.cookies && Array.isArray(json.cookies)) {
|
|
65
|
+
return json.cookies;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** 兼容旧接口:从 cookies 中提取第一个可用的 token 类值 */
|
|
74
|
+
export async function getSessionToken() {
|
|
75
|
+
const cookies = await readCookies();
|
|
76
|
+
if (!cookies || cookies.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
// Try to find an auth token cookie
|
|
79
|
+
const tokenCookie = cookies.find((c) => c.name.includes('token') ||
|
|
80
|
+
c.name.includes('sid') ||
|
|
81
|
+
c.name.includes('session') ||
|
|
82
|
+
c.name.includes('SESSION'));
|
|
83
|
+
// 没找到 token cookie 时返回 null,避免返回无效 cookie 导致上层误判为"已登录"
|
|
84
|
+
return tokenCookie?.value ?? null;
|
|
85
|
+
}
|
|
86
|
+
const findCookie = (cookies, name) => cookies.find((c) => c.name === name);
|
|
87
|
+
/**
|
|
88
|
+
* 从已有 cookies 派生 Banma 身份。纯函数,无副作用,便于测试。
|
|
89
|
+
* 任一来源缺失对应字段返回空字符串;cookies 为空数组返回 null。
|
|
90
|
+
*/
|
|
91
|
+
export function getBanmaIdentity(cookies) {
|
|
92
|
+
if (!cookies || cookies.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
const token = findCookie(cookies, 'access_token')?.value ?? '';
|
|
95
|
+
const user = findCookie(cookies, 'buc_username')?.value ?? '';
|
|
96
|
+
// buc_userinfo 是 base64 编码的 JSON,含 emp_id / account / name 等
|
|
97
|
+
let staffId = '';
|
|
98
|
+
const userinfoValue = findCookie(cookies, 'buc_userinfo')?.value;
|
|
99
|
+
if (userinfoValue) {
|
|
100
|
+
try {
|
|
101
|
+
const decoded = JSON.parse(Buffer.from(userinfoValue, 'base64').toString('utf-8'));
|
|
102
|
+
if (decoded && decoded.emp_id != null)
|
|
103
|
+
staffId = String(decoded.emp_id);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// 解码或解析失败:保持空字符串,不阻断请求
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { token, user, staffId };
|
|
110
|
+
}
|
|
111
|
+
/** 读取持久化 cookies 并派生 Banma 身份,供 HttpClient 使用 */
|
|
112
|
+
export async function readBanmaIdentity() {
|
|
113
|
+
const cookies = await readCookies();
|
|
114
|
+
return getBanmaIdentity(cookies);
|
|
115
|
+
}
|
|
116
|
+
/** Keytar session — legacy, keep for backward compat */
|
|
117
|
+
export async function readSession() {
|
|
118
|
+
try {
|
|
119
|
+
const keytar = await import('keytar');
|
|
120
|
+
const stored = await keytar.default.getPassword('atlas-cli', 'session');
|
|
121
|
+
if (stored)
|
|
122
|
+
return JSON.parse(stored);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// keytar not available
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
export async function writeSession(session) {
|
|
130
|
+
try {
|
|
131
|
+
const keytar = await import('keytar');
|
|
132
|
+
await keytar.default.setPassword('atlas-cli', 'session', JSON.stringify(session));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// keytar not available
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function clearSession() {
|
|
140
|
+
cachedCookies = null;
|
|
141
|
+
try {
|
|
142
|
+
const keytar = await import('keytar');
|
|
143
|
+
await keytar.default.deletePassword('atlas-cli', 'session');
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await unlink(COOKIE_FILE);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// ignore
|
|
153
|
+
}
|
|
154
|
+
}
|