@dreamor/atlas-cli 0.7.1 → 0.7.3
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 +45 -68
- package/dist/adapters/atlas/auth/index.js +1 -1
- package/dist/adapters/atlas/auth/login.js +9 -5
- package/dist/adapters/atlas/auth/session.js +0 -39
- package/dist/adapters/atlas/cli.js +12 -17
- package/dist/adapters/atlas/commands/_output_schema.js +268 -4
- package/dist/adapters/atlas/commands/exec.js +31 -8
- package/dist/adapters/atlas/commands/schema.js +0 -5
- package/dist/adapters/atlas/commands/suggest.js +45 -18
- package/dist/adapters/atlas/util/errors.js +10 -0
- package/dist/adapters/atlas/util/output-limit.js +4 -3
- package/dist/adapters/atlas/util/output.js +7 -1
- package/dist/adapters/atlas/util/version.js +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,84 +1,61 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
> 斑马云图(Banma)人力基线管理工具
|
|
4
|
-
|
|
5
|
-
## 安装
|
|
1
|
+
# @dreamor/atlas-cli
|
|
6
2
|
|
|
7
3
|
```bash
|
|
8
4
|
npm i -g @dreamor/atlas-cli
|
|
9
5
|
```
|
|
10
6
|
|
|
11
|
-
> Node 20+
|
|
7
|
+
> Node 20+
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
---
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
npm update -g @dreamor/atlas-cli
|
|
17
|
-
```
|
|
11
|
+
## For Agents
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
本 CLI 设计为 AI agent 可自省调用的接口工具。以下是 agent 需要知道的契约。
|
|
14
|
+
|
|
15
|
+
### 1. JSON 模式
|
|
16
|
+
|
|
17
|
+
所有命令支持 `--json` 或环境变量 `ATLAS_OUTPUT=json`:
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
|
-
atlas
|
|
23
|
-
atlas
|
|
24
|
-
atlas auth status # 检查登录态
|
|
20
|
+
ATLAS_OUTPUT=json atlas auth status
|
|
21
|
+
atlas baseline month --month 2026-06 --json
|
|
25
22
|
```
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
stdout 始终是统一 JSON 信封;进度日志走 stderr,不污染管道。
|
|
25
|
+
|
|
26
|
+
### 2. 发现入口
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
# 命令树 + 每个命令的参数 schema + 输出 schema
|
|
30
|
+
atlas schema commands --describe
|
|
31
|
+
|
|
32
|
+
# 单命令自省
|
|
33
|
+
atlas <cmd> --describe
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
|
44
|
-
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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) — 开发者指南(架构、命令、设计决策)
|
|
36
|
+
### 3. 鉴权
|
|
37
|
+
|
|
38
|
+
- `atlas auth status` 返回 `data.loggedIn: boolean`
|
|
39
|
+
- agent **不能自动执行** `atlas auth login`(弹浏览器做 SSO,非终端环境报 exit 64)
|
|
40
|
+
|
|
41
|
+
### 4. 退出码速查
|
|
42
|
+
|
|
43
|
+
| 码 | 含义 | agent 应对 |
|
|
44
|
+
|----|------|-----------|
|
|
45
|
+
| 0 | 成功 | 继续 |
|
|
46
|
+
| 2 | 会话过期 | 提示用户重新登录 |
|
|
47
|
+
| 3 | API 错误 | 不重试 |
|
|
48
|
+
| 6 | 限流 | 退避 10s 重试 ≤1 次 |
|
|
49
|
+
| 7 | 网络错误 | 退避 5s 重试 ≤1 次 |
|
|
50
|
+
| 64 | 配置/未实现 | 不重试 |
|
|
51
|
+
| 65 | 输出超限 | 改用 `--out <file>` |
|
|
52
|
+
|
|
53
|
+
### 5. Skill 入口
|
|
54
|
+
|
|
55
|
+
完整 agent 行为契约见 [.claude/skills/atlas/SKILL.md](.claude/skills/atlas/SKILL.md)
|
|
56
|
+
|
|
57
|
+
### 6. 数据单位
|
|
58
|
+
|
|
59
|
+
- 所有 `manpower` 字段 = **人月**(不要二次换算)
|
|
60
|
+
- `atlas compare`:`diff = 实际 - 基线`(人月),**不要再除 22**
|
|
61
|
+
- 月份格式 `YYYY-MM`,已修正 CST 偏移
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { loginCmd, statusCmd } from './login.js';
|
|
2
|
-
export { getSessionToken,
|
|
2
|
+
export { getSessionToken, getBanmaIdentity, readBanmaIdentity } from './session.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeCookies, readCookies, fetchCookiesFromDaemon } from './session.js';
|
|
2
2
|
import { isJsonMode, jsonOk, log } from '../util/output.js';
|
|
3
|
+
import { AtlasError } from '../util/errors.js';
|
|
3
4
|
const BANMA_HOST = 'banma-yuntu.alibaba-inc.com';
|
|
4
5
|
// 白名单:只保存 getBanmaIdentity 真正需要的 cookies,避免整域 cookies 落盘
|
|
5
6
|
const WANT_COOKIES = new Set(['access_token', 'buc_username', 'buc_userinfo']);
|
|
@@ -9,19 +10,19 @@ const EMP_ID_NAMES = new Set(['emp_id', 'empId', 'employeeId']);
|
|
|
9
10
|
* 环境下无法启动(连 --version 都 require 失败),故改为运行时动态 import + try/catch
|
|
10
11
|
* 降级:只在真正执行 `auth login` 时才需要 playwright,未安装则给出清晰提示。
|
|
11
12
|
*/
|
|
12
|
-
async function
|
|
13
|
+
async function loadFirefox() {
|
|
13
14
|
try {
|
|
14
15
|
const pw = await import('playwright');
|
|
15
|
-
return pw.
|
|
16
|
+
return pw.firefox;
|
|
16
17
|
}
|
|
17
18
|
catch {
|
|
18
|
-
throw new Error("未安装 playwright。运行时需要:设 ATLAS_AUTO_BOOTSTRAP=1 自动安装,或执行 `npm install playwright && npx playwright install
|
|
19
|
+
throw new Error("未安装 playwright。运行时需要:设 ATLAS_AUTO_BOOTSTRAP=1 自动安装,或执行 `npm install playwright && npx playwright install`");
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
export async function login(_port) {
|
|
22
23
|
log('正在打开浏览器进行 Banma SSO 登录...');
|
|
23
|
-
const
|
|
24
|
-
const browser = await
|
|
24
|
+
const firefox = await loadFirefox();
|
|
25
|
+
const browser = await firefox.launch({
|
|
25
26
|
headless: false,
|
|
26
27
|
});
|
|
27
28
|
const page = await browser.newPage();
|
|
@@ -85,6 +86,9 @@ export async function loginCmd(opts) {
|
|
|
85
86
|
}
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
89
|
+
if (!process.stdout.isTTY) {
|
|
90
|
+
throw new AtlasError('atlas auth login 需要在终端中交互完成 SSO,当前为非终端环境。请手动在终端执行 `atlas auth login` 后重试', 'INTERACTIVE_REQUIRED');
|
|
91
|
+
}
|
|
88
92
|
await login();
|
|
89
93
|
if (opts.json || isJsonMode()) {
|
|
90
94
|
jsonOk({ status: 'logged_in' });
|
|
@@ -113,42 +113,3 @@ export async function readBanmaIdentity() {
|
|
|
113
113
|
const cookies = await readCookies();
|
|
114
114
|
return getBanmaIdentity(cookies);
|
|
115
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
|
-
}
|
|
@@ -12,11 +12,11 @@ import { showCmd as actualShowCmd, monthCmd as actualMonthCmd, summaryCmd as act
|
|
|
12
12
|
import { compareCmd } from './commands/compare/index.js';
|
|
13
13
|
// Utility commands
|
|
14
14
|
import { daemonCmd } from './daemon/index.js';
|
|
15
|
-
import { schemaCommandsCmd
|
|
15
|
+
import { schemaCommandsCmd } from './commands/schema.js';
|
|
16
16
|
import { execCmd } from './commands/exec.js';
|
|
17
17
|
import { suggestCmd } from './commands/suggest.js';
|
|
18
18
|
import { updateCmd } from './commands/update.js';
|
|
19
|
-
import { BanmaApiError, ConfigError, NotImplementedError, SessionExpiredError, isAtlasError, } from './util/errors.js';
|
|
19
|
+
import { BanmaApiError, ConfigError, NotImplementedError, OutputTooLargeError, SessionExpiredError, isAtlasError, } from './util/errors.js';
|
|
20
20
|
import { isJsonMode, printError } from './util/output.js';
|
|
21
21
|
import { getOutputSchema } from './commands/_output_schema.js';
|
|
22
22
|
export function handleError(err) {
|
|
@@ -40,6 +40,11 @@ export function handleError(err) {
|
|
|
40
40
|
console.error(err.message);
|
|
41
41
|
process.exit(64);
|
|
42
42
|
}
|
|
43
|
+
if (err instanceof OutputTooLargeError) {
|
|
44
|
+
const debug = process.env.DEBUG === '1';
|
|
45
|
+
console.error(debug ? err.stack ?? err.message : err.message);
|
|
46
|
+
process.exit(65);
|
|
47
|
+
}
|
|
43
48
|
const debug = process.env.DEBUG === '1';
|
|
44
49
|
console.error(err instanceof Error ? (debug ? err.stack ?? err.message : err.message) : String(err));
|
|
45
50
|
process.exit(1);
|
|
@@ -49,6 +54,8 @@ export function exitCodeFor(err) {
|
|
|
49
54
|
return 2;
|
|
50
55
|
if (err instanceof BanmaApiError)
|
|
51
56
|
return 3;
|
|
57
|
+
if (err instanceof OutputTooLargeError)
|
|
58
|
+
return 65;
|
|
52
59
|
if (err instanceof NotImplementedError)
|
|
53
60
|
return 64;
|
|
54
61
|
if (isAtlasError(err)) {
|
|
@@ -59,6 +66,7 @@ export function exitCodeFor(err) {
|
|
|
59
66
|
case 'NETWORK_ERROR': return 7;
|
|
60
67
|
case 'UPDATE_ERROR': return 8;
|
|
61
68
|
case 'CONFIG_ERROR': return 64;
|
|
69
|
+
case 'INTERACTIVE_REQUIRED': return 64;
|
|
62
70
|
default: return 1;
|
|
63
71
|
}
|
|
64
72
|
}
|
|
@@ -382,20 +390,6 @@ function registerUtilityCommands(program) {
|
|
|
382
390
|
});
|
|
383
391
|
// schema
|
|
384
392
|
const schema = program.command('schema').description('CLI 自省 / 字段字典导出');
|
|
385
|
-
schema
|
|
386
|
-
.command('export')
|
|
387
|
-
.description('导出字典 + 部门树,供 skill 缓存对照')
|
|
388
|
-
.option('--out <path>', '同时写入文件路径')
|
|
389
|
-
.option('--refresh', '刷新缓存')
|
|
390
|
-
.option('--json', '输出 JSON 信封')
|
|
391
|
-
.action(async (opts) => {
|
|
392
|
-
try {
|
|
393
|
-
await schemaExportCmd(opts);
|
|
394
|
-
}
|
|
395
|
-
catch (e) {
|
|
396
|
-
handleError(e);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
393
|
schema
|
|
400
394
|
.command('commands')
|
|
401
395
|
.description('列出所有命令的参数 schema 及输出 schema 标注')
|
|
@@ -474,7 +468,8 @@ export function buildProgram() {
|
|
|
474
468
|
6 API 限流(RateLimited)
|
|
475
469
|
7 网络错误(NetworkError)
|
|
476
470
|
8 版本更新异常(UpdateError)
|
|
477
|
-
64 配置错误 / 未实现(ConfigError)
|
|
471
|
+
64 配置错误 / 未实现(ConfigError / NotImplementedError)
|
|
472
|
+
65 输出超限(OutputTooLargeError)
|
|
478
473
|
`)
|
|
479
474
|
.showHelpAfterError()
|
|
480
475
|
.hook('preAction', (thisCommand, actionCommand) => {
|
|
@@ -8,6 +8,7 @@ const schemas = {
|
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
10
10
|
status: { type: 'string', enum: ['logged_in'] },
|
|
11
|
+
via: { type: 'string', enum: ['daemon'] },
|
|
11
12
|
},
|
|
12
13
|
},
|
|
13
14
|
},
|
|
@@ -37,6 +38,26 @@ const schemas = {
|
|
|
37
38
|
},
|
|
38
39
|
},
|
|
39
40
|
},
|
|
41
|
+
'atlas find': {
|
|
42
|
+
jsonSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
ok: { type: 'boolean' },
|
|
46
|
+
data: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
items: { type: 'object' },
|
|
49
|
+
},
|
|
50
|
+
meta: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
count: { type: 'number' },
|
|
54
|
+
kind: { type: 'string' },
|
|
55
|
+
query: { type: 'string' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
40
61
|
'atlas link': {
|
|
41
62
|
jsonSchema: {
|
|
42
63
|
type: 'object',
|
|
@@ -48,6 +69,24 @@ const schemas = {
|
|
|
48
69
|
projectId: { type: 'string' },
|
|
49
70
|
projectName: { type: 'string' },
|
|
50
71
|
linkedAt: { type: 'string' },
|
|
72
|
+
linked: { type: 'object', description: '已绑定的项目信息(link 无参时)' },
|
|
73
|
+
envProjectId: { type: 'string', description: 'BANMA_PROJECT_ID 环境变量值' },
|
|
74
|
+
dryRun: { type: 'boolean' },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
'atlas unlink': {
|
|
81
|
+
jsonSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
ok: { type: 'boolean' },
|
|
85
|
+
data: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
unlinked: { type: 'boolean' },
|
|
89
|
+
projectName: { type: 'string' },
|
|
51
90
|
},
|
|
52
91
|
},
|
|
53
92
|
},
|
|
@@ -61,8 +100,16 @@ const schemas = {
|
|
|
61
100
|
data: {
|
|
62
101
|
type: 'object',
|
|
63
102
|
properties: {
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
projectId: { type: 'string' },
|
|
104
|
+
months: { type: 'array', items: { type: 'string' }, description: 'YYYY-MM 格式月份列表' },
|
|
105
|
+
entries: {
|
|
106
|
+
type: 'array',
|
|
107
|
+
items: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
description: '单个月份的基线条目',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
totalManpower: { type: 'number', description: '单位:人月' },
|
|
66
113
|
},
|
|
67
114
|
},
|
|
68
115
|
},
|
|
@@ -78,14 +125,231 @@ const schemas = {
|
|
|
78
125
|
items: {
|
|
79
126
|
type: 'object',
|
|
80
127
|
properties: {
|
|
81
|
-
month: { type: 'string' },
|
|
82
|
-
manpower: { type: 'number' },
|
|
128
|
+
month: { type: 'string', description: 'YYYY-MM' },
|
|
129
|
+
manpower: { type: 'number', description: '单位:人月' },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
'atlas baseline export': {
|
|
137
|
+
jsonSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
ok: { type: 'boolean' },
|
|
141
|
+
data: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
exported: { type: 'number', description: '导出行数' },
|
|
145
|
+
format: { type: 'string', enum: ['csv', 'json'] },
|
|
146
|
+
out: { type: 'string', description: '导出文件绝对路径(经过白名单校验)' },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
'atlas actual show': {
|
|
153
|
+
jsonSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
ok: { type: 'boolean' },
|
|
157
|
+
data: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
staffId: { type: 'string' },
|
|
161
|
+
month: { type: 'string', description: 'YYYY-MM' },
|
|
162
|
+
personnel: { type: 'array', items: { type: 'object' } },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
'atlas actual month': {
|
|
169
|
+
jsonSchema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
ok: { type: 'boolean' },
|
|
173
|
+
data: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
projectId: { type: 'string' },
|
|
177
|
+
entries: { type: 'array', items: { type: 'object' } },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
meta: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
failedMonths: { type: 'array', items: { type: 'string' } },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
'atlas actual summary': {
|
|
190
|
+
jsonSchema: {
|
|
191
|
+
type: 'object',
|
|
192
|
+
properties: {
|
|
193
|
+
ok: { type: 'boolean' },
|
|
194
|
+
data: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
rows: { type: 'array', items: { type: 'object' } },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
meta: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
failedMonths: { type: 'array', items: { type: 'string' } },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
'atlas actual export': {
|
|
210
|
+
jsonSchema: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
properties: {
|
|
213
|
+
ok: { type: 'boolean' },
|
|
214
|
+
data: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: {
|
|
217
|
+
exported: { type: 'number', description: '导出行数' },
|
|
218
|
+
format: { type: 'string', enum: ['csv', 'json'] },
|
|
219
|
+
out: { type: 'string', description: '导出文件绝对路径' },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
meta: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: {
|
|
225
|
+
failedMonths: { type: 'array', items: { type: 'string' } },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
'atlas compare': {
|
|
232
|
+
jsonSchema: {
|
|
233
|
+
type: 'object',
|
|
234
|
+
properties: {
|
|
235
|
+
ok: { type: 'boolean' },
|
|
236
|
+
data: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
rows: {
|
|
240
|
+
type: 'array',
|
|
241
|
+
items: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
month: { type: 'string', description: 'YYYY-MM' },
|
|
245
|
+
plan: { type: 'number', description: '基线人月' },
|
|
246
|
+
actual: { type: 'number', description: '实际人月' },
|
|
247
|
+
diff: { type: 'number', description: '实际 - 基线(人月)' },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
meta: {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: {
|
|
256
|
+
failedMonths: { type: 'array', items: { type: 'string' } },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
'atlas schema commands': {
|
|
263
|
+
jsonSchema: {
|
|
264
|
+
type: 'object',
|
|
265
|
+
properties: {
|
|
266
|
+
ok: { type: 'boolean' },
|
|
267
|
+
data: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
commands: { type: 'array', items: { type: 'string' } },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
'atlas exec': {
|
|
277
|
+
jsonSchema: {
|
|
278
|
+
type: 'object',
|
|
279
|
+
properties: {
|
|
280
|
+
ok: { type: 'boolean' },
|
|
281
|
+
data: {
|
|
282
|
+
type: 'object',
|
|
283
|
+
properties: {
|
|
284
|
+
results: {
|
|
285
|
+
type: 'array',
|
|
286
|
+
items: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
step: { type: 'number' },
|
|
290
|
+
command: { type: 'string' },
|
|
291
|
+
status: { type: 'string', description: 'success | failed: <msg>' },
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
'atlas suggest': {
|
|
301
|
+
jsonSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
ok: { type: 'boolean' },
|
|
305
|
+
data: {
|
|
306
|
+
type: 'object',
|
|
307
|
+
properties: {
|
|
308
|
+
query: { type: 'string' },
|
|
309
|
+
suggestions: {
|
|
310
|
+
type: 'array',
|
|
311
|
+
maxItems: 5,
|
|
312
|
+
items: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: {
|
|
315
|
+
command: { type: 'string', description: 'atlas 子命令片段' },
|
|
316
|
+
description: { type: 'string' },
|
|
317
|
+
score: { type: 'number' },
|
|
318
|
+
},
|
|
319
|
+
},
|
|
83
320
|
},
|
|
84
321
|
},
|
|
85
322
|
},
|
|
86
323
|
},
|
|
87
324
|
},
|
|
88
325
|
},
|
|
326
|
+
'atlas update': {
|
|
327
|
+
jsonSchema: {
|
|
328
|
+
type: 'object',
|
|
329
|
+
properties: {
|
|
330
|
+
ok: { type: 'boolean' },
|
|
331
|
+
data: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
mode: { type: 'string', enum: ['npm', 'other'] },
|
|
335
|
+
disabled: { type: 'boolean' },
|
|
336
|
+
current: { type: 'string', description: '当前版本号' },
|
|
337
|
+
updateCommand: { type: 'string', description: 'npm 更新命令' },
|
|
338
|
+
migrationCommand: { type: 'string', description: '迁移到 npm 安装的命令' },
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
'atlas daemon': {
|
|
345
|
+
jsonSchema: {
|
|
346
|
+
type: 'object',
|
|
347
|
+
properties: {
|
|
348
|
+
ok: { type: 'boolean' },
|
|
349
|
+
data: { type: 'object' },
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
89
353
|
};
|
|
90
354
|
export function getOutputSchema(commandPath) {
|
|
91
355
|
return schemas[commandPath] ?? {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { isJsonMode, jsonOk, log } from '../util/output.js';
|
|
4
5
|
import { ConfigError } from '../util/errors.js';
|
|
@@ -9,6 +10,32 @@ const ExecStepSchema = z.object({
|
|
|
9
10
|
const ExecPlanSchema = z.object({
|
|
10
11
|
steps: z.array(ExecStepSchema).max(100, '步骤数不能超过 100'),
|
|
11
12
|
});
|
|
13
|
+
/**
|
|
14
|
+
* 用 spawn 执行一条 atlas 子命令,返回 Promise。
|
|
15
|
+
* 使用 process.execPath + process.argv[1] 确保无论 npx / global / direct 都能正确找到入口。
|
|
16
|
+
*/
|
|
17
|
+
function runStep(command, extraArgs = []) {
|
|
18
|
+
const parts = command.split(/\s+/);
|
|
19
|
+
// parts[0] === 'atlas', 去掉后剩下的才是子命令参数
|
|
20
|
+
const args = [process.argv[1], ...parts.slice(1), ...extraArgs];
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn(process.execPath, args, {
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
env: { ...process.env },
|
|
25
|
+
});
|
|
26
|
+
child.on('close', (code) => {
|
|
27
|
+
if (code === 0) {
|
|
28
|
+
resolve();
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
reject(new Error(`进程退出码 ${code}`));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
child.on('error', (err) => {
|
|
35
|
+
reject(new Error(`无法启动进程: ${err.message}`));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
12
39
|
export async function execCmd(opts) {
|
|
13
40
|
let raw;
|
|
14
41
|
try {
|
|
@@ -33,16 +60,12 @@ export async function execCmd(opts) {
|
|
|
33
60
|
const step = plan.steps[i];
|
|
34
61
|
log(`[${i + 1}/${plan.steps.length}] ${step.command}`);
|
|
35
62
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
results.push({ step: i + 1, command: step.command, status: 'registered' });
|
|
63
|
+
await runStep(step.command, step.args);
|
|
64
|
+
results.push({ step: i + 1, command: step.command, status: 'success' });
|
|
39
65
|
}
|
|
40
66
|
catch (e) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
command: step.command,
|
|
44
|
-
status: `failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
45
|
-
});
|
|
67
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
68
|
+
results.push({ step: i + 1, command: step.command, status: `failed: ${msg}` });
|
|
46
69
|
if (opts.json || isJsonMode()) {
|
|
47
70
|
jsonOk({ results }, { completed: i, total: plan.steps.length });
|
|
48
71
|
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { isJsonMode, jsonOk, log } from '../util/output.js';
|
|
2
|
-
import { NotImplementedError } from '../util/errors.js';
|
|
3
2
|
import { emitDescribe } from '../cli.js';
|
|
4
|
-
export async function schemaExportCmd(_opts) {
|
|
5
|
-
// 后续待实装:调用 dictionary/selectAll 和 department/tree 接口拉取真实数据
|
|
6
|
-
throw new NotImplementedError('schema export 暂未完整实现。当前返回空字典,待后续拉取部门树 + 字典值后补全');
|
|
7
|
-
}
|
|
8
3
|
export function schemaCommandsCmd(program, opts) {
|
|
9
4
|
const commands = [];
|
|
10
5
|
program.commands.forEach((cmd) => {
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import { isJsonMode, jsonOk, log } from '../util/output.js';
|
|
2
2
|
const RULES = [
|
|
3
|
-
|
|
4
|
-
{ pattern:
|
|
5
|
-
{ pattern:
|
|
6
|
-
|
|
7
|
-
{ pattern:
|
|
8
|
-
{ pattern:
|
|
9
|
-
{ pattern:
|
|
10
|
-
{ pattern:
|
|
11
|
-
{ pattern:
|
|
12
|
-
{ pattern:
|
|
13
|
-
|
|
14
|
-
{ pattern:
|
|
15
|
-
{ pattern:
|
|
16
|
-
{ pattern:
|
|
17
|
-
|
|
3
|
+
// ── 鉴权类 ─────────────────────────────
|
|
4
|
+
{ pattern: /登录|login|sso|auth/i, command: 'atlas auth login', description: 'SSO 登录(需终端交互)', weight: 10, requiresAuth: false, exitCodes: [0, 1, 64] },
|
|
5
|
+
{ pattern: /状态|status|会话/i, command: 'atlas auth status', description: '查看会话状态', weight: 10, requiresAuth: false, exitCodes: [0] },
|
|
6
|
+
// ── 项目类 ─────────────────────────────
|
|
7
|
+
{ pattern: /项目.*列表|列出.*项目|projects/i, command: 'atlas projects', description: '列出所有项目', weight: 10, requiresAuth: true, exitCodes: [0, 2] },
|
|
8
|
+
{ pattern: /绑定|link|关联/i, command: 'atlas link <project>', description: '绑定项目(可省后续 --project-id)', weight: 10, requiresAuth: true, exitCodes: [0, 4, 5] },
|
|
9
|
+
{ pattern: /解绑|unlink/i, command: 'atlas unlink', description: '解绑项目', weight: 10, requiresAuth: true, exitCodes: [0] },
|
|
10
|
+
{ pattern: /搜索|找|find|查询项目/i, command: 'atlas find project <query>', description: '搜索项目(也支持 department/mp-type/line-plan-type/area-code)', weight: 10, requiresAuth: true, exitCodes: [0, 2] },
|
|
11
|
+
{ pattern: /部门|department/i, command: 'atlas find department <query>', description: '搜索部门', weight: 7, requiresAuth: true, exitCodes: [0, 2] },
|
|
12
|
+
{ pattern: /字典|类型|mp.type|line.plan/i, command: 'atlas find mp-type <query>', description: '搜索 MP 类型字典值', weight: 7, requiresAuth: true, exitCodes: [0, 2] },
|
|
13
|
+
// ── 基线人力 ────────────────────────────
|
|
14
|
+
{ pattern: /基线|计划|baseline|人月.*计划/i, command: 'atlas baseline month --month YYYY-MM', description: '查看指定月份基线人力', weight: 5, requiresAuth: true, exitCodes: [0, 2, 3, 4, 5] },
|
|
15
|
+
{ pattern: /基线.*汇总|baseline.*summar/i, command: 'atlas baseline summary [--by month|department|role]', description: '按月/部门/角色汇总基线人力', weight: 5, requiresAuth: true, exitCodes: [0, 2, 3] },
|
|
16
|
+
{ pattern: /基线.*导出|baseline.*export/i, command: 'atlas baseline export --from YYYY-MM --to YYYY-MM [--format csv|json] [--out <path>]', description: '导出基线条目', weight: 5, requiresAuth: true, exitCodes: [0, 2, 3, 65] },
|
|
17
|
+
// ── 实际工时 ────────────────────────────
|
|
18
|
+
{ pattern: /实际|actual|工时|人月.*实际/i, command: 'atlas actual month --month YYYY-MM', description: '查看指定月份实际工时(人月)', weight: 5, requiresAuth: true, exitCodes: [0, 2, 3, 4, 5] },
|
|
19
|
+
{ pattern: /实际.*汇总|actual.*summar/i, command: 'atlas actual summary [--by month|department|role]', description: '汇总实际工时', weight: 5, requiresAuth: true, exitCodes: [0, 2, 3] },
|
|
20
|
+
{ pattern: /实际.*导出|actual.*export/i, command: 'atlas actual export --from YYYY-MM --to YYYY-MM [--format csv|json] [--out <path>]', description: '导出实际工时', weight: 5, requiresAuth: true, exitCodes: [0, 2, 3, 65] },
|
|
21
|
+
{ pattern: /实际.*人员|人员.*明细|staff|成员.*工时/i, command: 'atlas actual show <staffId> --month YYYY-MM', description: '查看单人员工实际工时明细', weight: 5, requiresAuth: true, exitCodes: [0, 2, 3] },
|
|
22
|
+
// ── 对比 ───────────────────────────────
|
|
23
|
+
{ pattern: /对比|比较|compare|差异|偏差/i, command: 'atlas compare --from YYYY-MM --to YYYY-MM', description: '基线 vs 实际对比(diff = 实际 - 基线,人月)', weight: 10, requiresAuth: true, exitCodes: [0, 2, 3, 4, 5] },
|
|
24
|
+
// ── 批量执行 ────────────────────────────
|
|
25
|
+
{ pattern: /批量|batch|exec|编排/i, command: 'atlas exec --plan-file <path>', description: '按 plan-file 顺序执行多条 atlas 命令', weight: 7, requiresAuth: false, exitCodes: [0, 1] },
|
|
26
|
+
// ── 自省 / 帮助 ─────────────────────────
|
|
27
|
+
{ pattern: /命令.*列表|命令树|帮助|自省|schema/i, command: 'atlas schema commands [--describe]', description: '列出所有命令 + 输出 schema(agent 自省入口)', weight: 8, requiresAuth: false, exitCodes: [0] },
|
|
28
|
+
{ pattern: /升级|更新|update/i, command: 'atlas update', description: '升级到最新版本', weight: 10, requiresAuth: false, exitCodes: [0] },
|
|
29
|
+
{ pattern: /守护|daemon/i, command: 'atlas daemon', description: '启动本地守护进程(保持浏览器会话)', weight: 5, requiresAuth: false, exitCodes: [0] },
|
|
30
|
+
// ── 模糊匹配:问"某项目" ────────────
|
|
31
|
+
{ pattern: /项目\S+|(\S+项目)/i, command: 'atlas find project <query>', description: '搜索项目(含模糊匹配参数)', weight: 3, requiresAuth: true, exitCodes: [0, 2] },
|
|
32
|
+
// ── 月份推算 ────────────────────────────
|
|
33
|
+
{ pattern: /上个月|本月|上月|这个月|当月|当前月/i, command: 'atlas baseline month --month $(date +%Y-%m)', description: '查看当前/上个月基线(月份参数可推算)', weight: 3, requiresAuth: true, exitCodes: [0, 2, 3] },
|
|
34
|
+
{ pattern: /上季度|上个季度|本季度/i, command: 'atlas compare --from $(date -d "$(date +%Y-%m-01) -3 months" +%Y-%m) --to $(date +%Y-%m)', description: '按季度范围对比(月份参数自动推算)', weight: 3, requiresAuth: true, exitCodes: [0, 2, 3] },
|
|
18
35
|
];
|
|
19
36
|
export function suggestCmd(query, opts) {
|
|
20
37
|
const suggestions = [];
|
|
@@ -24,17 +41,24 @@ export function suggestCmd(query, opts) {
|
|
|
24
41
|
command: rule.command,
|
|
25
42
|
description: rule.description,
|
|
26
43
|
score: rule.weight,
|
|
44
|
+
requiresAuth: rule.requiresAuth,
|
|
45
|
+
exitCodes: rule.exitCodes,
|
|
27
46
|
});
|
|
28
47
|
}
|
|
29
48
|
}
|
|
30
|
-
//
|
|
49
|
+
// 模糊分提升:查询词在命令中出现越多加分
|
|
31
50
|
const queryLower = query.toLowerCase();
|
|
32
51
|
for (const s of suggestions) {
|
|
33
52
|
const cmdLower = s.command.toLowerCase();
|
|
34
53
|
let score = 0;
|
|
35
54
|
for (const word of queryLower.split(/\s+/)) {
|
|
36
|
-
if (cmdLower.includes(word))
|
|
55
|
+
if (word.length >= 2 && cmdLower.includes(word))
|
|
37
56
|
score += 0.5;
|
|
57
|
+
// 月份/具体数值命中额外加分
|
|
58
|
+
if (/^\d{4}[-\/]\d{2}$/.test(word))
|
|
59
|
+
score += 1;
|
|
60
|
+
if (/^\d+$/.test(word) && word.length >= 2 && cmdLower.includes(word))
|
|
61
|
+
score += 1;
|
|
38
62
|
}
|
|
39
63
|
s.score += score;
|
|
40
64
|
}
|
|
@@ -51,6 +75,9 @@ export function suggestCmd(query, opts) {
|
|
|
51
75
|
log(`自然语言查询: "${query}"`);
|
|
52
76
|
log('建议命令:');
|
|
53
77
|
for (const s of suggestions.slice(0, 5)) {
|
|
54
|
-
|
|
78
|
+
const authMark = s.requiresAuth ? '🔑' : ' ';
|
|
79
|
+
log(` ${authMark} ${s.command} — ${s.description}`);
|
|
55
80
|
}
|
|
81
|
+
log('');
|
|
82
|
+
log('标记 🔑=需要登录,无标记=无需鉴权');
|
|
56
83
|
}
|
|
@@ -29,6 +29,16 @@ export class NotImplementedError extends Error {
|
|
|
29
29
|
this.name = 'NotImplementedError';
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
export class OutputTooLargeError extends Error {
|
|
33
|
+
bytes;
|
|
34
|
+
limit;
|
|
35
|
+
constructor(bytes, limit) {
|
|
36
|
+
super(`输出 ${bytes} 字节超过上限 ${limit} 字节`);
|
|
37
|
+
this.bytes = bytes;
|
|
38
|
+
this.limit = limit;
|
|
39
|
+
this.name = 'OutputTooLargeError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
32
42
|
export class AtlasError extends Error {
|
|
33
43
|
code;
|
|
34
44
|
constructor(message, code) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OutputTooLargeError } from './errors.js';
|
|
2
2
|
const MAX_OUTPUT_BYTES_ENV = 'ATLAS_MAX_OUTPUT_BYTES';
|
|
3
3
|
/**
|
|
4
4
|
* 在写出文件前校验输出字节数上限。
|
|
5
5
|
*
|
|
6
6
|
* 读取 ATLAS_MAX_OUTPUT_BYTES(字节数,需为正数)。未设置或非法值时放行;
|
|
7
|
-
* 超限时抛
|
|
7
|
+
* 超限时抛 OutputTooLargeError(退出码 65,error.code: OUTPUT_TOO_LARGE),
|
|
8
|
+
* agent 可据此区分"输出过大"和"配置错误"。
|
|
8
9
|
*/
|
|
9
10
|
export function enforceOutputLimit(content) {
|
|
10
11
|
const raw = process.env[MAX_OUTPUT_BYTES_ENV];
|
|
@@ -15,6 +16,6 @@ export function enforceOutputLimit(content) {
|
|
|
15
16
|
return;
|
|
16
17
|
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
17
18
|
if (bytes > limit) {
|
|
18
|
-
throw new
|
|
19
|
+
throw new OutputTooLargeError(bytes, limit);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BanmaApiError, AtlasError, ConfigError, NotImplementedError, SessionExpiredError } from './errors.js';
|
|
1
|
+
import { BanmaApiError, AtlasError, ConfigError, NotImplementedError, OutputTooLargeError, SessionExpiredError } from './errors.js';
|
|
2
2
|
export function isJsonMode() {
|
|
3
3
|
return process.env.ATLAS_OUTPUT === 'json';
|
|
4
4
|
}
|
|
@@ -23,6 +23,9 @@ export function printError(err, opts) {
|
|
|
23
23
|
else if (err instanceof AtlasError) {
|
|
24
24
|
jsonError(err.code, err.message);
|
|
25
25
|
}
|
|
26
|
+
else if (err instanceof OutputTooLargeError) {
|
|
27
|
+
jsonError('OUTPUT_TOO_LARGE', err.message);
|
|
28
|
+
}
|
|
26
29
|
else if (err instanceof Error) {
|
|
27
30
|
jsonError('ERROR', err.message);
|
|
28
31
|
}
|
|
@@ -44,6 +47,9 @@ export function printError(err, opts) {
|
|
|
44
47
|
else if (err instanceof NotImplementedError) {
|
|
45
48
|
console.error(err.message);
|
|
46
49
|
}
|
|
50
|
+
else if (err instanceof OutputTooLargeError) {
|
|
51
|
+
console.error(err.message);
|
|
52
|
+
}
|
|
47
53
|
else if (err instanceof AtlasError) {
|
|
48
54
|
console.error(`[${err.code}] ${err.message}`);
|
|
49
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ATLAS_VERSION = '0.7.
|
|
1
|
+
export const ATLAS_VERSION = '0.7.3';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamor/atlas-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Atlas CLI - 斑马云图人力基线管理工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"zod": "^3.23.8"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"keytar": "^7.9.0",
|
|
44
43
|
"playwright": "^1.49.0"
|
|
45
44
|
},
|
|
46
45
|
"devDependencies": {
|