@dreamor/atlas-cli 0.7.23 → 0.7.24
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.github.md +230 -0
- package/README.md +1 -59
- package/atlas.cjs +226 -0
- package/package.json +7 -9
- package/atlas.js +0 -5
- package/dist/adapters/atlas/auth/browser.js +0 -37
- package/dist/adapters/atlas/auth/index.js +0 -4
- package/dist/adapters/atlas/auth/login.js +0 -187
- package/dist/adapters/atlas/auth/portable.js +0 -193
- package/dist/adapters/atlas/auth/refresh.js +0 -138
- package/dist/adapters/atlas/auth/session.js +0 -167
- package/dist/adapters/atlas/cli.js +0 -561
- package/dist/adapters/atlas/commands/_output_schema.js +0 -379
- package/dist/adapters/atlas/commands/actual/_logic.js +0 -116
- package/dist/adapters/atlas/commands/actual/index.js +0 -138
- package/dist/adapters/atlas/commands/auth.js +0 -1
- package/dist/adapters/atlas/commands/baseline/index.js +0 -137
- package/dist/adapters/atlas/commands/compare/_logic.js +0 -39
- package/dist/adapters/atlas/commands/compare/index.js +0 -89
- package/dist/adapters/atlas/commands/exec.js +0 -81
- package/dist/adapters/atlas/commands/project/index.js +0 -218
- package/dist/adapters/atlas/commands/schema.js +0 -25
- package/dist/adapters/atlas/commands/suggest.js +0 -83
- package/dist/adapters/atlas/commands/update.js +0 -104
- package/dist/adapters/atlas/daemon/index.js +0 -145
- package/dist/adapters/atlas/dict/index.js +0 -41
- package/dist/adapters/atlas/http/client.js +0 -200
- package/dist/adapters/atlas/http/index.js +0 -1
- package/dist/adapters/atlas/schema/actual.js +0 -16
- package/dist/adapters/atlas/schema/baseline.js +0 -34
- package/dist/adapters/atlas/schema/department.js +0 -11
- package/dist/adapters/atlas/schema/index.js +0 -4
- package/dist/adapters/atlas/schema/project.js +0 -13
- package/dist/adapters/atlas/util/cidr.js +0 -114
- package/dist/adapters/atlas/util/constants.js +0 -4
- package/dist/adapters/atlas/util/env.js +0 -56
- package/dist/adapters/atlas/util/environment.js +0 -152
- package/dist/adapters/atlas/util/errors.js +0 -49
- package/dist/adapters/atlas/util/helpers.js +0 -17
- package/dist/adapters/atlas/util/months.js +0 -65
- package/dist/adapters/atlas/util/output-limit.js +0 -21
- package/dist/adapters/atlas/util/output.js +0 -70
- package/dist/adapters/atlas/util/paths.js +0 -40
- package/dist/adapters/atlas/util/portable-store.js +0 -153
- package/dist/adapters/atlas/util/secure-fs.js +0 -41
- package/dist/adapters/atlas/util/time.js +0 -17
- package/dist/adapters/atlas/util/version.js +0 -1
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { ConfigError } from './errors.js';
|
|
2
|
-
const IPV4_OCTET = '(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)';
|
|
3
|
-
const IPV4_RE = new RegExp(`^${IPV4_OCTET}(\\.${IPV4_OCTET}){3}$`);
|
|
4
|
-
const IPV6_FULL_RE = /^[0-9a-fA-F:]+$/;
|
|
5
|
-
/** 是否为 loopback 监听地址(127.0.0.1 / ::1 / localhost) */
|
|
6
|
-
export function isLoopback(host) {
|
|
7
|
-
if (!host)
|
|
8
|
-
return false;
|
|
9
|
-
const h = host.toLowerCase().trim();
|
|
10
|
-
if (h === 'localhost' || h === '::1')
|
|
11
|
-
return true;
|
|
12
|
-
if (IPV4_RE.test(h) && h.startsWith('127.'))
|
|
13
|
-
return true;
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
/** 规范化对端地址:去 IPv4-mapped IPv6 前缀 ::ffff: */
|
|
17
|
-
export function normalizeRemoteAddr(addr) {
|
|
18
|
-
if (!addr)
|
|
19
|
-
return '';
|
|
20
|
-
const trimmed = addr.trim();
|
|
21
|
-
// ::ffff:1.2.3.4 或 ::ffff:a.b.c.d
|
|
22
|
-
const mapped = trimmed.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
23
|
-
if (mapped)
|
|
24
|
-
return mapped[1];
|
|
25
|
-
// 也可能出现 ::ffff:xxx.xxx.xxx.xxx 之外的形式,如 2002:... 退回原值
|
|
26
|
-
return trimmed;
|
|
27
|
-
}
|
|
28
|
-
/** IPv4 点分十进制 → 32 位无符号整数;非法返回 null */
|
|
29
|
-
function ipv4ToInt(ip) {
|
|
30
|
-
if (!IPV4_RE.test(ip))
|
|
31
|
-
return null;
|
|
32
|
-
const parts = ip.split('.').map(Number);
|
|
33
|
-
// 防止前导零或多段异常(正则已挡,此处二次保险)
|
|
34
|
-
return (((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3]) >>> 0;
|
|
35
|
-
}
|
|
36
|
-
function intToIpv4(n) {
|
|
37
|
-
return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join('.');
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* 解析 CIDR/单 IP 列表为结构化条目。
|
|
41
|
-
* - IPv4 单 IP 视作 /32,IPv6 单 IP 视作 /128
|
|
42
|
-
* - IPv6 CIDR 暂不支持(抛 ConfigError),仅允许 IPv6 单 IP
|
|
43
|
-
* - 非法格式抛 ConfigError,message 含原始输入
|
|
44
|
-
*/
|
|
45
|
-
export function parseCidrList(input) {
|
|
46
|
-
if (!input || input.length === 0)
|
|
47
|
-
return [];
|
|
48
|
-
const entries = [];
|
|
49
|
-
for (const raw of input) {
|
|
50
|
-
const item = raw.trim();
|
|
51
|
-
if (!item)
|
|
52
|
-
continue;
|
|
53
|
-
if (item.includes(':')) {
|
|
54
|
-
// IPv6:仅支持单 IP 全等比较
|
|
55
|
-
if (item.includes('/')) {
|
|
56
|
-
throw new ConfigError(`暂不支持 IPv6 CIDR(仅单 IP):${raw}`);
|
|
57
|
-
}
|
|
58
|
-
if (!IPV6_FULL_RE.test(item)) {
|
|
59
|
-
throw new ConfigError(`非法 IPv6 地址:${raw}`);
|
|
60
|
-
}
|
|
61
|
-
entries.push({ raw, family: 'ipv6', network: item.toLowerCase(), prefix: 128 });
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
// IPv4:CIDR 或单 IP
|
|
65
|
-
if (item.includes('/')) {
|
|
66
|
-
const [ip, prefixStr] = item.split('/');
|
|
67
|
-
const ipInt = ipv4ToInt(ip);
|
|
68
|
-
const prefix = Number(prefixStr);
|
|
69
|
-
if (ipInt === null)
|
|
70
|
-
throw new ConfigError(`非法 IPv4 网络地址:${raw}`);
|
|
71
|
-
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) {
|
|
72
|
-
throw new ConfigError(`非法 IPv4 前缀长度:${raw}`);
|
|
73
|
-
}
|
|
74
|
-
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
|
75
|
-
const netInt = (ipInt & mask) >>> 0;
|
|
76
|
-
entries.push({ raw, family: 'ipv4', network: intToIpv4(netInt), prefix });
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
const ipInt = ipv4ToInt(item);
|
|
80
|
-
if (ipInt === null)
|
|
81
|
-
throw new ConfigError(`非法 IPv4 地址:${raw}`);
|
|
82
|
-
entries.push({ raw, family: 'ipv4', network: intToIpv4(ipInt), prefix: 32 });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return entries;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* 判断对端地址是否命中白名单。
|
|
89
|
-
* - allowList 为空 → 放行(向后兼容 loopback 默认)
|
|
90
|
-
* - 未提供地址 → 拒绝(无法判定时安全优先)
|
|
91
|
-
*/
|
|
92
|
-
export function isIpAllowed(remoteAddr, allowList) {
|
|
93
|
-
if (allowList.length === 0)
|
|
94
|
-
return true;
|
|
95
|
-
const addr = normalizeRemoteAddr(remoteAddr);
|
|
96
|
-
if (!addr)
|
|
97
|
-
return false;
|
|
98
|
-
for (const entry of allowList) {
|
|
99
|
-
if (entry.family === 'ipv6') {
|
|
100
|
-
// IPv6 单 IP 全等比较
|
|
101
|
-
if (addr.toLowerCase() === entry.network)
|
|
102
|
-
return true;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
// IPv4 CIDR 匹配
|
|
106
|
-
const ipInt = ipv4ToInt(addr);
|
|
107
|
-
if (ipInt === null)
|
|
108
|
-
continue;
|
|
109
|
-
const mask = entry.prefix === 0 ? 0 : (0xffffffff << (32 - entry.prefix)) >>> 0;
|
|
110
|
-
if (((ipInt & mask) >>> 0) === (ipv4ToInt(entry.network) >>> 0))
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { getLinkFile } from './paths.js';
|
|
3
|
-
import { ConfigError } from './errors.js';
|
|
4
|
-
/** 获取 BANMA_PROJECT_ID 环境变量 */
|
|
5
|
-
export function getBanmaProjectId() {
|
|
6
|
-
return process.env.BANMA_PROJECT_ID;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* 解析项目 ID,优先级:
|
|
10
|
-
* 1. 命令行 --project-id 参数
|
|
11
|
-
* 2. BANMA_PROJECT_ID 环境变量
|
|
12
|
-
* 3. ~/.atlas/link.json(atlas link 绑定的项目)
|
|
13
|
-
*/
|
|
14
|
-
export function resolveProjectId(cliProjectId) {
|
|
15
|
-
if (cliProjectId)
|
|
16
|
-
return cliProjectId;
|
|
17
|
-
if (process.env.BANMA_PROJECT_ID)
|
|
18
|
-
return process.env.BANMA_PROJECT_ID;
|
|
19
|
-
try {
|
|
20
|
-
const linkFile = getLinkFile();
|
|
21
|
-
if (existsSync(linkFile)) {
|
|
22
|
-
const link = JSON.parse(readFileSync(linkFile, 'utf-8'));
|
|
23
|
-
if (link.projectId)
|
|
24
|
-
return link.projectId;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// ignore
|
|
29
|
-
}
|
|
30
|
-
throw new ConfigError('请指定 --project-id、设置 BANMA_PROJECT_ID、或先用 atlas link <project> 绑定项目');
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* 解析项目信息(ID + 名称),优先级同上。名称仅从 link 文件获取。
|
|
34
|
-
*/
|
|
35
|
-
export function resolveProjectInfo(cliProjectId) {
|
|
36
|
-
if (cliProjectId)
|
|
37
|
-
return { id: cliProjectId };
|
|
38
|
-
if (process.env.BANMA_PROJECT_ID)
|
|
39
|
-
return { id: process.env.BANMA_PROJECT_ID };
|
|
40
|
-
try {
|
|
41
|
-
const linkFile = getLinkFile();
|
|
42
|
-
if (existsSync(linkFile)) {
|
|
43
|
-
const link = JSON.parse(readFileSync(linkFile, 'utf-8'));
|
|
44
|
-
if (link.projectId)
|
|
45
|
-
return { id: link.projectId, name: link.projectName };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// ignore
|
|
50
|
-
}
|
|
51
|
-
throw new ConfigError('请指定 --project-id、设置 BANMA_PROJECT_ID、或先用 atlas link <project> 绑定项目');
|
|
52
|
-
}
|
|
53
|
-
/** 是否禁用自动升级 */
|
|
54
|
-
export function isUpdateDisabled() {
|
|
55
|
-
return process.env.ATLAS_DISABLE_UPDATE === '1';
|
|
56
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 沙盒环境识别(对齐 dws agent_code_detect)。
|
|
3
|
-
*
|
|
4
|
-
* 四级信号优先级(高 → 低),命中即返回,绝不猜:
|
|
5
|
-
* T0 ATLAS_AGENT_CODE 环境变量(显式声明,最高优先级)
|
|
6
|
-
* T1 CLI 内嵌签名(claudecode/openclaw/codex/hermes/wukong)
|
|
7
|
-
* T2 VSCODE_BRAND(覆盖 Cursor/Qoder/Trae 等 VS Code fork)
|
|
8
|
-
* T3 macOS __CFBundleIdentifier(qoder/cursor/vscode/wukong)
|
|
9
|
-
* T4 未识别 → 空字符串
|
|
10
|
-
*
|
|
11
|
-
* 同时探测沙盒能力(TTY / DISPLAY / 容器 / 浏览器可拉起 / cookies 就绪 / daemon
|
|
12
|
-
* 可达)。daemon 探测涉及网络 IO,由 detectEnvironmentAsync 单独暴露。
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync } from 'fs';
|
|
15
|
-
import { hostname, platform } from 'os';
|
|
16
|
-
import { getCookieFile, getDaemonTokenFile } from './paths.js';
|
|
17
|
-
/** 已验证的 T1 CLI 签名(envKeys 任一存在即命中) */
|
|
18
|
-
const KNOWN_SIGNATURES = [
|
|
19
|
-
{ code: 'claudecode', envKeys: ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'] },
|
|
20
|
-
{ code: 'openclaw', envKeys: ['OPENCLAW_BUNDLE_ROOT', 'OPENCLAW_RUNTIME_ROLE'] },
|
|
21
|
-
{ code: 'codex', envKeys: ['CODEX_SANDBOX'] },
|
|
22
|
-
{ code: 'hermes', envKeys: ['HERMES_HOME'] },
|
|
23
|
-
{ code: 'wukong', envKeys: ['WUKONG_SANDBOX', 'WUKONG_RUNTIME'] },
|
|
24
|
-
];
|
|
25
|
-
/** T3: macOS bundle id → agent_code 映射(仅取已验证条目) */
|
|
26
|
-
const BUNDLE_ID_MAP = [
|
|
27
|
-
{ code: 'cursor', bundlePrefix: 'com.todesktop.' }, // cursor 包名为 com.todesktop.230313mzl4w4u92
|
|
28
|
-
{ code: 'qoder', bundlePrefix: 'com.qoder.' },
|
|
29
|
-
{ code: 'vscode', bundlePrefix: 'com.microsoft.VSCode' },
|
|
30
|
-
{ code: 'wukong', bundlePrefix: 'com.antgroup.wukong' },
|
|
31
|
-
];
|
|
32
|
-
/** 同步检测 agent_code(不触碰网络/磁盘可变状态,便于单测)。 */
|
|
33
|
-
export function detectAgentCode(env = process.env) {
|
|
34
|
-
// T0: 显式声明最高优先级
|
|
35
|
-
const explicit = env.ATLAS_AGENT_CODE;
|
|
36
|
-
if (typeof explicit === 'string' && explicit.trim()) {
|
|
37
|
-
return { agentCode: explicit.trim(), agentSignal: 'env:ATLAS_AGENT_CODE' };
|
|
38
|
-
}
|
|
39
|
-
// T1: CLI 内嵌签名
|
|
40
|
-
for (const sig of KNOWN_SIGNATURES) {
|
|
41
|
-
for (const k of sig.envKeys) {
|
|
42
|
-
const v = env[k];
|
|
43
|
-
if (typeof v === 'string' && v.length > 0) {
|
|
44
|
-
return { agentCode: sig.code, agentSignal: `env:${k}` };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// T2: VSCODE_BRAND(覆盖所有 VS Code fork:Cursor/Qoder/Trae 等)
|
|
49
|
-
const brand = env.VSCODE_BRAND;
|
|
50
|
-
if (typeof brand === 'string' && brand.trim()) {
|
|
51
|
-
return { agentCode: brand.trim().toLowerCase(), agentSignal: 'env:VSCODE_BRAND' };
|
|
52
|
-
}
|
|
53
|
-
// T3: macOS __CFBundleIdentifier
|
|
54
|
-
const bundle = env.__CFBundleIdentifier;
|
|
55
|
-
if (typeof bundle === 'string' && bundle.length > 0) {
|
|
56
|
-
for (const m of BUNDLE_ID_MAP) {
|
|
57
|
-
if (bundle.startsWith(m.bundlePrefix) || bundle === m.bundlePrefix) {
|
|
58
|
-
return { agentCode: m.code, agentSignal: `bundle:${bundle}` };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
// T4: 未识别 → 空
|
|
63
|
-
return { agentCode: '', agentSignal: '' };
|
|
64
|
-
}
|
|
65
|
-
/** 判断是否处于 Linux 容器内(/.dockerenv 或 $container 变量) */
|
|
66
|
-
export function detectContainer(env = process.env) {
|
|
67
|
-
if (existsSync('/.dockerenv'))
|
|
68
|
-
return true;
|
|
69
|
-
const c = env.container;
|
|
70
|
-
if (typeof c === 'string' && c.trim() && c !== 'off')
|
|
71
|
-
return true;
|
|
72
|
-
// Kubernetes 常见信号
|
|
73
|
-
if (env.KUBERNETES_SERVICE_HOST)
|
|
74
|
-
return true;
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
/** 是否存在 DISPLAY(Linux X11/Wayland)/ macOS 原生 GUI */
|
|
78
|
-
export function detectHasDisplay(env = process.env) {
|
|
79
|
-
if (typeof env.DISPLAY === 'string' && env.DISPLAY.length > 0)
|
|
80
|
-
return true;
|
|
81
|
-
if (typeof env.WAYLAND_DISPLAY === 'string' && env.WAYLAND_DISPLAY.length > 0)
|
|
82
|
-
return true;
|
|
83
|
-
// macOS 即使无 DISPLAY 变量也有原生 GUI
|
|
84
|
-
if (platform() === 'darwin')
|
|
85
|
-
return true;
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
/** ATLAS_SANDBOX=1 可显式强制沙盒行为(测试/调试用) */
|
|
89
|
-
function isForcedSandbox(env) {
|
|
90
|
-
return env.ATLAS_SANDBOX === '1' || env.ATLAS_SANDBOX === 'true';
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* 同步环境能力快照(不探测 daemon,避免阻塞)。
|
|
94
|
-
* daemon 可达性见 detectEnvironmentAsync()。
|
|
95
|
-
*/
|
|
96
|
-
export function detectEnvironment(env = process.env) {
|
|
97
|
-
const { agentCode, agentSignal } = detectAgentCode(env);
|
|
98
|
-
const hasTTY = Boolean(process.stdout.isTTY);
|
|
99
|
-
const hasDisplay = detectHasDisplay(env);
|
|
100
|
-
const isContainer = detectContainer(env);
|
|
101
|
-
const forcedSandbox = isForcedSandbox(env);
|
|
102
|
-
// 受信任的 GUI 拉起:需要 TTY + DISPLAY 且非容器;ATLAS_SANDBOX=1 强制视为不可拉起
|
|
103
|
-
const canLaunchBrowser = !forcedSandbox && hasDisplay && !isContainer && hasTTY;
|
|
104
|
-
const cookiesReady = existsSync(getCookieFile());
|
|
105
|
-
return {
|
|
106
|
-
agentCode,
|
|
107
|
-
agentSignal,
|
|
108
|
-
hasTTY,
|
|
109
|
-
hasDisplay,
|
|
110
|
-
isContainer: isContainer || forcedSandbox,
|
|
111
|
-
canLaunchBrowser,
|
|
112
|
-
cookiesReady,
|
|
113
|
-
daemonReachable: null,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
/** 异步探测 daemon 是否可达(带超时)。失败返回 false,绝不抛。 */
|
|
117
|
-
export async function probeDaemonReachable(url, token, timeoutMs = 1500) {
|
|
118
|
-
if (!url)
|
|
119
|
-
return false;
|
|
120
|
-
try {
|
|
121
|
-
const { request } = await import('undici');
|
|
122
|
-
const headers = {};
|
|
123
|
-
if (token)
|
|
124
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
125
|
-
const resp = await request(`${url.replace(/\/$/, '')}/api/health`, {
|
|
126
|
-
headers,
|
|
127
|
-
headersTimeout: timeoutMs,
|
|
128
|
-
bodyTimeout: timeoutMs,
|
|
129
|
-
});
|
|
130
|
-
return resp.statusCode === 200;
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
/** 从环境变量 + 默认 token 文件解析 daemon 连接配置(不发起请求) */
|
|
137
|
-
export function resolveDaemonConfig(env = process.env) {
|
|
138
|
-
const envUrl = env.ATLAS_DAEMON_URL;
|
|
139
|
-
const envToken = env.ATLAS_DAEMON_TOKEN;
|
|
140
|
-
if (envUrl) {
|
|
141
|
-
return { url: envUrl, token: envToken ?? null, via: 'env' };
|
|
142
|
-
}
|
|
143
|
-
// 回退默认 localhost + 本机 token 文件
|
|
144
|
-
const tokenFileExists = existsSync(getDaemonTokenFile());
|
|
145
|
-
if (tokenFileExists) {
|
|
146
|
-
const envPort = Number(env.ATLAS_DAEMON_PORT);
|
|
147
|
-
const port = Number.isFinite(envPort) && envPort > 0 ? envPort : 8765;
|
|
148
|
-
return { url: `http://localhost:${port}`, token: null, via: 'default' };
|
|
149
|
-
}
|
|
150
|
-
return { url: null, token: null, via: 'none' };
|
|
151
|
-
}
|
|
152
|
-
export const __DEV__ = { hostname, getCookieFile, getDaemonTokenFile };
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { ZodError } from 'zod';
|
|
2
|
-
export class BanmaApiError extends Error {
|
|
3
|
-
errCode;
|
|
4
|
-
errorMsg;
|
|
5
|
-
requestId;
|
|
6
|
-
constructor(errCode, errorMsg, requestId) {
|
|
7
|
-
super(`Banma API error [${errCode}] ${errorMsg}`);
|
|
8
|
-
this.errCode = errCode;
|
|
9
|
-
this.errorMsg = errorMsg;
|
|
10
|
-
this.requestId = requestId;
|
|
11
|
-
this.name = 'BanmaApiError';
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export class ConfigError extends Error {
|
|
15
|
-
constructor(message) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = 'ConfigError';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
export class SessionExpiredError extends Error {
|
|
21
|
-
constructor(message = '会话已过期,请重新登录:atlas auth login') {
|
|
22
|
-
super(message);
|
|
23
|
-
this.name = 'SessionExpiredError';
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
export class OutputTooLargeError extends Error {
|
|
27
|
-
bytes;
|
|
28
|
-
limit;
|
|
29
|
-
constructor(bytes, limit) {
|
|
30
|
-
super(`输出 ${bytes} 字节超过上限 ${limit} 字节`);
|
|
31
|
-
this.bytes = bytes;
|
|
32
|
-
this.limit = limit;
|
|
33
|
-
this.name = 'OutputTooLargeError';
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export class AtlasError extends Error {
|
|
37
|
-
code;
|
|
38
|
-
constructor(message, code) {
|
|
39
|
-
super(message);
|
|
40
|
-
this.code = code;
|
|
41
|
-
this.name = 'AtlasError';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export function isAtlasError(err) {
|
|
45
|
-
return err instanceof AtlasError;
|
|
46
|
-
}
|
|
47
|
-
export function isZodError(err) {
|
|
48
|
-
return err instanceof ZodError;
|
|
49
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** 将 人天 ÷ 22 转换为 人月 */
|
|
2
|
-
export function manhoursToManMonth(hours) {
|
|
3
|
-
const v = typeof hours === 'string' ? parseFloat(hours) : hours;
|
|
4
|
-
if (isNaN(v) || v < 0)
|
|
5
|
-
return 0;
|
|
6
|
-
return Math.round((v / 22) * 100) / 100;
|
|
7
|
-
}
|
|
8
|
-
/** 格式化人力数字 */
|
|
9
|
-
export function fmtManpower(v) {
|
|
10
|
-
if (v == null || isNaN(v))
|
|
11
|
-
return '-';
|
|
12
|
-
return v.toFixed(2);
|
|
13
|
-
}
|
|
14
|
-
/** 安全的 sum */
|
|
15
|
-
export function safeSum(values) {
|
|
16
|
-
return values.reduce((acc, v) => acc + (v ?? 0), 0);
|
|
17
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 月份工具:展开月份范围、格式校验、上限保护。
|
|
3
|
-
*/
|
|
4
|
-
import { ConfigError } from './errors.js';
|
|
5
|
-
import { currentMonthKey } from './time.js';
|
|
6
|
-
/** YYYY-MM 格式正则 */
|
|
7
|
-
const MONTH_RE = /^(\d{4})-(0[1-9]|1[0-2])$/;
|
|
8
|
-
/** 单次查询最大月份数 */
|
|
9
|
-
const MAX_MONTHS = 36;
|
|
10
|
-
/**
|
|
11
|
-
* 展开月份范围 → YYYY-MM 数组。
|
|
12
|
-
* - 同时给 from/to:返回闭区间内的所有月份。
|
|
13
|
-
* - 都不给:返回当前月(单元素数组)。
|
|
14
|
-
* - 只给一个:按"都给"处理,缺失的那个用当前月补齐的语义由调用方决定(此处不单独支持)。
|
|
15
|
-
*
|
|
16
|
-
* 格式校验通过 MONTH_RE;无效格式或范围超过 MAX_MONTHS 抛 ConfigError。
|
|
17
|
-
*/
|
|
18
|
-
export function expandMonths(from, to) {
|
|
19
|
-
if (!from || !to)
|
|
20
|
-
return [currentMonthKey()];
|
|
21
|
-
if (!MONTH_RE.test(from))
|
|
22
|
-
throw new ConfigError(`月份格式应为 YYYY-MM(如 2026-03),实际: "${from}"`);
|
|
23
|
-
if (!MONTH_RE.test(to))
|
|
24
|
-
throw new ConfigError(`月份格式应为 YYYY-MM(如 2026-03),实际: "${to}"`);
|
|
25
|
-
const [fy, fm] = from.split('-').map(Number);
|
|
26
|
-
const [ty, tm] = to.split('-').map(Number);
|
|
27
|
-
const months = [];
|
|
28
|
-
let y = fy, m = fm;
|
|
29
|
-
while (y < ty || (y === ty && m <= tm)) {
|
|
30
|
-
months.push(`${y}-${String(m).padStart(2, '0')}`);
|
|
31
|
-
m++;
|
|
32
|
-
if (m > 12) {
|
|
33
|
-
m = 1;
|
|
34
|
-
y++;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (months.length > MAX_MONTHS) {
|
|
38
|
-
throw new ConfigError(`月份范围过大: ${months.length} 个月(上限 ${MAX_MONTHS} 个月),请缩小查询范围`);
|
|
39
|
-
}
|
|
40
|
-
return months;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* 不传月份时的默认范围:当前月及前后各 12 个月(最多 25 个月,低于 MAX_MONTHS)。
|
|
44
|
-
* 使 actual month 无参数时与 baseline month 行为一致。
|
|
45
|
-
*/
|
|
46
|
-
export function expandMonthsDefault() {
|
|
47
|
-
const now = new Date();
|
|
48
|
-
const cy = now.getFullYear();
|
|
49
|
-
const cm = now.getMonth() + 1;
|
|
50
|
-
// 12 个月前
|
|
51
|
-
let fy = cy, fm = cm - 12;
|
|
52
|
-
while (fm < 1) {
|
|
53
|
-
fm += 12;
|
|
54
|
-
fy--;
|
|
55
|
-
}
|
|
56
|
-
// 12 个月后
|
|
57
|
-
let ty = cy, tm = cm + 12;
|
|
58
|
-
while (tm > 12) {
|
|
59
|
-
tm -= 12;
|
|
60
|
-
ty++;
|
|
61
|
-
}
|
|
62
|
-
const from = `${fy}-${String(fm).padStart(2, '0')}`;
|
|
63
|
-
const to = `${ty}-${String(tm).padStart(2, '0')}`;
|
|
64
|
-
return expandMonths(from, to);
|
|
65
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { OutputTooLargeError } from './errors.js';
|
|
2
|
-
const MAX_OUTPUT_BYTES_ENV = 'ATLAS_MAX_OUTPUT_BYTES';
|
|
3
|
-
/**
|
|
4
|
-
* 在写出文件前校验输出字节数上限。
|
|
5
|
-
*
|
|
6
|
-
* 读取 ATLAS_MAX_OUTPUT_BYTES(字节数,需为正数)。未设置或非法值时放行;
|
|
7
|
-
* 超限时抛 OutputTooLargeError(退出码 65,error.code: OUTPUT_TOO_LARGE),
|
|
8
|
-
* agent 可据此区分"输出过大"和"配置错误"。
|
|
9
|
-
*/
|
|
10
|
-
export function enforceOutputLimit(content) {
|
|
11
|
-
const raw = process.env[MAX_OUTPUT_BYTES_ENV];
|
|
12
|
-
if (!raw)
|
|
13
|
-
return;
|
|
14
|
-
const limit = Number(raw);
|
|
15
|
-
if (!Number.isFinite(limit) || limit <= 0)
|
|
16
|
-
return;
|
|
17
|
-
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
18
|
-
if (bytes > limit) {
|
|
19
|
-
throw new OutputTooLargeError(bytes, limit);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { BanmaApiError, AtlasError, ConfigError, OutputTooLargeError, SessionExpiredError } from './errors.js';
|
|
2
|
-
export function isJsonMode() {
|
|
3
|
-
return process.env.ATLAS_OUTPUT === 'json';
|
|
4
|
-
}
|
|
5
|
-
export function isQuietMode() {
|
|
6
|
-
return process.env.ATLAS_QUIET === '1';
|
|
7
|
-
}
|
|
8
|
-
export function jsonOk(data, meta) {
|
|
9
|
-
const envelope = { ok: true, data };
|
|
10
|
-
if (meta)
|
|
11
|
-
envelope.meta = meta;
|
|
12
|
-
process.stdout.write(JSON.stringify(envelope) + '\n');
|
|
13
|
-
}
|
|
14
|
-
export function jsonError(code, message) {
|
|
15
|
-
const envelope = { ok: false, error: { code, message } };
|
|
16
|
-
process.stdout.write(JSON.stringify(envelope) + '\n');
|
|
17
|
-
}
|
|
18
|
-
export function printError(err, opts) {
|
|
19
|
-
if (opts?.json && isJsonMode()) {
|
|
20
|
-
if (err instanceof BanmaApiError) {
|
|
21
|
-
jsonError(err.errCode, err.errorMsg);
|
|
22
|
-
}
|
|
23
|
-
else if (err instanceof AtlasError) {
|
|
24
|
-
jsonError(err.code, err.message);
|
|
25
|
-
}
|
|
26
|
-
else if (err instanceof OutputTooLargeError) {
|
|
27
|
-
jsonError('OUTPUT_TOO_LARGE', err.message);
|
|
28
|
-
}
|
|
29
|
-
else if (err instanceof Error) {
|
|
30
|
-
jsonError('ERROR', err.message);
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
jsonError('ERROR', String(err));
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
// Non-JSON error printing
|
|
38
|
-
if (err instanceof BanmaApiError) {
|
|
39
|
-
console.error(`Banma API error [${err.errCode}] ${err.errorMsg}`);
|
|
40
|
-
}
|
|
41
|
-
else if (err instanceof SessionExpiredError) {
|
|
42
|
-
console.error(err.message);
|
|
43
|
-
}
|
|
44
|
-
else if (err instanceof ConfigError) {
|
|
45
|
-
console.error(`Config error: ${err.message}`);
|
|
46
|
-
}
|
|
47
|
-
else if (err instanceof OutputTooLargeError) {
|
|
48
|
-
console.error(err.message);
|
|
49
|
-
}
|
|
50
|
-
else if (err instanceof AtlasError) {
|
|
51
|
-
console.error(`[${err.code}] ${err.message}`);
|
|
52
|
-
}
|
|
53
|
-
else if (err instanceof Error) {
|
|
54
|
-
const debug = process.env.DEBUG === '1';
|
|
55
|
-
console.error(debug ? err.stack ?? err.message : err.message);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
console.error(String(err));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
export function log(msg) {
|
|
62
|
-
if (!isQuietMode()) {
|
|
63
|
-
console.error(msg);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
export function table(data) {
|
|
67
|
-
if (!isJsonMode()) {
|
|
68
|
-
console.table(data);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 路径工具:集中管理所有文件路径,统一遵从 ATLAS_HOME 环境变量。
|
|
3
|
-
*
|
|
4
|
-
* 避免各文件重复拼 ~/.atlas,避免未来改目录结构时遗漏调用点。
|
|
5
|
-
*/
|
|
6
|
-
import { homedir } from 'os';
|
|
7
|
-
import { join, resolve } from 'path';
|
|
8
|
-
/** ~/.atlas 或 $ATLAS_HOME(经 path.resolve 防止相对路径误导) */
|
|
9
|
-
export function getAtlasHome() {
|
|
10
|
-
const raw = process.env.ATLAS_HOME || join(homedir(), '.atlas');
|
|
11
|
-
return resolve(raw);
|
|
12
|
-
}
|
|
13
|
-
/** ~/.atlas/cookies.json */
|
|
14
|
-
export function getCookieFile() {
|
|
15
|
-
return join(getAtlasHome(), 'cookies.json');
|
|
16
|
-
}
|
|
17
|
-
/** ~/.atlas/link.json */
|
|
18
|
-
export function getLinkFile() {
|
|
19
|
-
return join(getAtlasHome(), 'link.json');
|
|
20
|
-
}
|
|
21
|
-
/** ~/.atlas/daemon.token */
|
|
22
|
-
export function getDaemonTokenFile() {
|
|
23
|
-
return join(getAtlasHome(), 'daemon.token');
|
|
24
|
-
}
|
|
25
|
-
/** ~/.atlas/bin */
|
|
26
|
-
export function getBinDir() {
|
|
27
|
-
return join(getAtlasHome(), 'bin');
|
|
28
|
-
}
|
|
29
|
-
/** ~/.atlas/bin/atlas.cjs */
|
|
30
|
-
export function getTargetBinary() {
|
|
31
|
-
return join(getBinDir(), 'atlas.cjs');
|
|
32
|
-
}
|
|
33
|
-
/** ~/.atlas/cache/<key>.json */
|
|
34
|
-
export function getCacheFile(key) {
|
|
35
|
-
return join(getAtlasHome(), 'cache', `${key}.json`);
|
|
36
|
-
}
|
|
37
|
-
/** ~/.atlas/cache */
|
|
38
|
-
export function getCacheDir() {
|
|
39
|
-
return join(getAtlasHome(), 'cache');
|
|
40
|
-
}
|