@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.
Files changed (39) hide show
  1. package/README.md +84 -0
  2. package/bin/atlas.js +5 -0
  3. package/dist/adapters/atlas/auth/index.js +2 -0
  4. package/dist/adapters/atlas/auth/login.js +107 -0
  5. package/dist/adapters/atlas/auth/session.js +154 -0
  6. package/dist/adapters/atlas/cli.js +502 -0
  7. package/dist/adapters/atlas/commands/_output_schema.js +100 -0
  8. package/dist/adapters/atlas/commands/actual/_logic.js +41 -0
  9. package/dist/adapters/atlas/commands/actual/index.js +117 -0
  10. package/dist/adapters/atlas/commands/auth.js +1 -0
  11. package/dist/adapters/atlas/commands/baseline/index.js +122 -0
  12. package/dist/adapters/atlas/commands/compare/_logic.js +39 -0
  13. package/dist/adapters/atlas/commands/compare/index.js +72 -0
  14. package/dist/adapters/atlas/commands/exec.js +58 -0
  15. package/dist/adapters/atlas/commands/project/index.js +179 -0
  16. package/dist/adapters/atlas/commands/schema.js +30 -0
  17. package/dist/adapters/atlas/commands/suggest.js +56 -0
  18. package/dist/adapters/atlas/commands/update.js +106 -0
  19. package/dist/adapters/atlas/daemon/index.js +64 -0
  20. package/dist/adapters/atlas/dict/index.js +41 -0
  21. package/dist/adapters/atlas/http/client.js +151 -0
  22. package/dist/adapters/atlas/http/index.js +1 -0
  23. package/dist/adapters/atlas/schema/actual.js +16 -0
  24. package/dist/adapters/atlas/schema/baseline.js +34 -0
  25. package/dist/adapters/atlas/schema/department.js +11 -0
  26. package/dist/adapters/atlas/schema/index.js +4 -0
  27. package/dist/adapters/atlas/schema/project.js +13 -0
  28. package/dist/adapters/atlas/util/constants.js +4 -0
  29. package/dist/adapters/atlas/util/env.js +8 -0
  30. package/dist/adapters/atlas/util/errors.js +45 -0
  31. package/dist/adapters/atlas/util/helpers.js +17 -0
  32. package/dist/adapters/atlas/util/months.js +41 -0
  33. package/dist/adapters/atlas/util/output-limit.js +20 -0
  34. package/dist/adapters/atlas/util/output.js +67 -0
  35. package/dist/adapters/atlas/util/paths.js +40 -0
  36. package/dist/adapters/atlas/util/secure-fs.js +41 -0
  37. package/dist/adapters/atlas/util/time.js +17 -0
  38. package/dist/adapters/atlas/util/version.js +1 -0
  39. package/package.json +54 -0
@@ -0,0 +1,502 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ // Auth
4
+ import { authLoginCmd, authStatusCmd } from './commands/auth.js';
5
+ // Project commands (find, projects, link, unlink)
6
+ import { findCmd, projectsCmd, linkCmd, linkStatusCmd, unlinkCmd, } from './commands/project/index.js';
7
+ // Baseline commands (month, summary, export)
8
+ import { monthCmd as baselineMonthCmd, summaryCmd as baselineSummaryCmd, exportCmd as baselineExportCmd, } from './commands/baseline/index.js';
9
+ // Actual commands (list, show, month, summary, export)
10
+ import { showCmd as actualShowCmd, monthCmd as actualMonthCmd, summaryCmd as actualSummaryCmd, exportCmd as actualExportCmd, } from './commands/actual/index.js';
11
+ // Compare
12
+ import { compareCmd } from './commands/compare/index.js';
13
+ // Utility commands
14
+ import { daemonCmd } from './daemon/index.js';
15
+ import { schemaCommandsCmd, schemaExportCmd } from './commands/schema.js';
16
+ import { execCmd } from './commands/exec.js';
17
+ import { suggestCmd } from './commands/suggest.js';
18
+ import { updateCmd } from './commands/update.js';
19
+ import { BanmaApiError, ConfigError, NotImplementedError, SessionExpiredError, isAtlasError, } from './util/errors.js';
20
+ import { isJsonMode, printError } from './util/output.js';
21
+ import { getOutputSchema } from './commands/_output_schema.js';
22
+ export function handleError(err) {
23
+ if (isJsonMode()) {
24
+ printError(err, { json: true });
25
+ process.exit(exitCodeFor(err));
26
+ }
27
+ if (err instanceof SessionExpiredError) {
28
+ console.error(err.message);
29
+ process.exit(2);
30
+ }
31
+ if (err instanceof ConfigError) {
32
+ console.error(`Config error: ${err.message}`);
33
+ process.exit(64);
34
+ }
35
+ if (err instanceof BanmaApiError) {
36
+ console.error(`Banma API error [${err.errCode}] ${err.errorMsg}`);
37
+ process.exit(3);
38
+ }
39
+ if (err instanceof NotImplementedError) {
40
+ console.error(err.message);
41
+ process.exit(64);
42
+ }
43
+ const debug = process.env.DEBUG === '1';
44
+ console.error(err instanceof Error ? (debug ? err.stack ?? err.message : err.message) : String(err));
45
+ process.exit(1);
46
+ }
47
+ export function exitCodeFor(err) {
48
+ if (err instanceof SessionExpiredError)
49
+ return 2;
50
+ if (err instanceof BanmaApiError)
51
+ return 3;
52
+ if (err instanceof NotImplementedError)
53
+ return 64;
54
+ if (isAtlasError(err)) {
55
+ switch (err.code) {
56
+ case 'AMBIGUOUS_PROJECT': return 4;
57
+ case 'PROJECT_NOT_FOUND': return 5;
58
+ case 'RATE_LIMITED': return 6;
59
+ case 'NETWORK_ERROR': return 7;
60
+ case 'UPDATE_ERROR': return 8;
61
+ case 'CONFIG_ERROR': return 64;
62
+ default: return 1;
63
+ }
64
+ }
65
+ return 1;
66
+ }
67
+ export function emitDescribe(cmd) {
68
+ const path = [];
69
+ let cursor = cmd;
70
+ while (cursor && cursor.name() !== 'atlas') {
71
+ path.unshift(cursor.name());
72
+ cursor = cursor.parent;
73
+ }
74
+ const commandPath = ['atlas', ...path].join(' ');
75
+ const outSchema = getOutputSchema(commandPath);
76
+ const payload = {
77
+ command: commandPath,
78
+ description: cmd.description() ?? '',
79
+ options: cmd.options.map((o) => {
80
+ const opt = o;
81
+ return {
82
+ flags: opt.flags,
83
+ description: opt.description ?? '',
84
+ required: opt.mandatory ?? false,
85
+ ...(opt.defaultValue !== undefined ? { default: opt.defaultValue } : {}),
86
+ };
87
+ }),
88
+ args: cmd.registeredArguments?.map((a) => ({
89
+ name: a.name(),
90
+ required: a.required,
91
+ })) ?? [],
92
+ subcommands: cmd.commands.map((c) => c.name()),
93
+ outputSchema: outSchema.jsonSchema,
94
+ };
95
+ const envelope = { ok: true, data: payload };
96
+ process.stdout.write(JSON.stringify(envelope) + '\n');
97
+ }
98
+ function addProjectOptions(cmd) {
99
+ return cmd
100
+ .option('--project-id <id>', '项目ID,精确名称或唯一子串(或使用 BANMA_PROJECT_ID 环境变量)')
101
+ .option('--refresh-projects', '解析 --project-id 前重新获取项目目录缓存');
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Registration functions
105
+ // ---------------------------------------------------------------------------
106
+ function registerAuthCommands(program) {
107
+ const auth = program.command('auth').description('SSO 会话管理');
108
+ auth
109
+ .command('login')
110
+ .description('打开浏览器完成 SSO 登录并持久化会话')
111
+ .option('--json', '输出 JSON 信封')
112
+ .action(async (opts) => {
113
+ try {
114
+ await authLoginCmd(opts);
115
+ }
116
+ catch (e) {
117
+ handleError(e);
118
+ }
119
+ });
120
+ auth
121
+ .command('status')
122
+ .description('显示当前会话信息')
123
+ .option('--json', '输出 JSON')
124
+ .action(async (opts) => {
125
+ try {
126
+ await authStatusCmd(opts);
127
+ }
128
+ catch (e) {
129
+ handleError(e);
130
+ }
131
+ });
132
+ }
133
+ function registerProjectCommands(program) {
134
+ // atlas find
135
+ program
136
+ .command('find <kind> <query>')
137
+ .description('搜索项目/部门/字典值(kind: project|department|mp-type|line-plan-type|area-code)')
138
+ .option('--json', '输出 JSON 信封')
139
+ .option('--refresh', '刷新字典/部门/项目缓存')
140
+ .option('--limit <n>', '最多返回 N 个候选(默认 20)')
141
+ .action(async (kind, query, opts) => {
142
+ try {
143
+ await findCmd(kind, query, opts);
144
+ }
145
+ catch (e) {
146
+ handleError(e);
147
+ }
148
+ });
149
+ // atlas projects
150
+ program
151
+ .command('projects')
152
+ .description('列出我有权限的所有项目')
153
+ .option('--json', '输出 JSON 信封')
154
+ .option('--refresh', '刷新项目缓存')
155
+ .action(async (opts) => {
156
+ try {
157
+ await projectsCmd(opts);
158
+ }
159
+ catch (e) {
160
+ handleError(e);
161
+ }
162
+ });
163
+ // atlas link
164
+ program
165
+ .command('link [project]')
166
+ .description('绑定当前项目(精确名称/子串/数字ID)。不带参数时显示当前绑定状态')
167
+ .option('--json', '输出 JSON 信封')
168
+ .option('--dry-run', '仅预览,不实际写入绑定')
169
+ .option('--refresh-projects', '解析 project 前重新获取项目目录缓存')
170
+ .action(async (project, opts) => {
171
+ try {
172
+ if (project === undefined) {
173
+ await linkStatusCmd(opts);
174
+ }
175
+ else {
176
+ await linkCmd(project, opts);
177
+ }
178
+ }
179
+ catch (e) {
180
+ handleError(e);
181
+ }
182
+ });
183
+ // atlas unlink
184
+ program
185
+ .command('unlink')
186
+ .description('清除当前项目绑定')
187
+ .option('--json', '输出 JSON 信封')
188
+ .option('--dry-run', '仅预览,不实际清除绑定')
189
+ .action(async (opts) => {
190
+ try {
191
+ await unlinkCmd(opts);
192
+ }
193
+ catch (e) {
194
+ handleError(e);
195
+ }
196
+ });
197
+ }
198
+ function registerBaselineCommands(program) {
199
+ const base = program.command('baseline').description('基线(计划)人力数据');
200
+ // atlas baseline month
201
+ addProjectOptions(base
202
+ .command('month')
203
+ .description('人力基线汇总(按月显示人力投入)'))
204
+ .option('--json', '输出 JSON')
205
+ .option('--department <name>', '按部门名称/ID 筛选(子串,不区分大小写)')
206
+ .option('--role <name>', '按角色/备注筛选(子串,不区分大小写)')
207
+ .option('--area-code <code>', '按地域筛选(子串,不区分大小写)')
208
+ .option('--mp-type <type>', '按人力类型筛选(子串,不区分大小写)')
209
+ .option('--month <yyyymm>', '查询月份(YYYY-MM,与 --from/--to 互斥)')
210
+ .option('--from <yyyymm>', '起始月份(YYYY-MM,包含)')
211
+ .option('--to <yyyymm>', '结束月份(YYYY-MM,包含)')
212
+ .option('--all-months', '显示所有月份(默认:只显示有人力的月份)')
213
+ .action(async (opts) => {
214
+ try {
215
+ await baselineMonthCmd(opts);
216
+ }
217
+ catch (e) {
218
+ handleError(e);
219
+ }
220
+ });
221
+ // atlas baseline summary
222
+ addProjectOptions(base
223
+ .command('summary')
224
+ .description('按月/部门/角色汇总基线人力投入'))
225
+ .option('--by <axis>', 'month | department | role', 'month')
226
+ .option('--department <name>', '按部门名称/ID 筛选(子串,不区分大小写)')
227
+ .option('--role <name>', '按角色/备注筛选(子串,不区分大小写)')
228
+ .option('--area-code <code>', '按地域筛选(子串,不区分大小写)')
229
+ .option('--mp-type <type>', '按人力类型筛选(子串,不区分大小写)')
230
+ .option('--from <yyyymm>', '起始月份(YYYY-MM,包含)')
231
+ .option('--to <yyyymm>', '结束月份(YYYY-MM,包含)')
232
+ .option('--json', '输出 JSON')
233
+ .action(async (opts) => {
234
+ try {
235
+ await baselineSummaryCmd(opts);
236
+ }
237
+ catch (e) {
238
+ handleError(e);
239
+ }
240
+ });
241
+ // atlas baseline export
242
+ addProjectOptions(base
243
+ .command('export')
244
+ .description('导出基线条目到 CSV/JSON'))
245
+ .requiredOption('--format <fmt>', 'csv | json')
246
+ .requiredOption('--out <path>', '输出文件路径')
247
+ .option('--from <yyyymm>', '起始月份(YYYY-MM,包含)')
248
+ .option('--to <yyyymm>', '结束月份(YYYY-MM,包含)')
249
+ .option('--department <name>', '按部门名称/ID 筛选(子串,不区分大小写)')
250
+ .option('--role <name>', '按角色/备注筛选(子串,不区分大小写)')
251
+ .option('--since <iso>', '仅导出指定时间后修改的条目(ISO 时间戳)')
252
+ .option('--json', '输出 JSON 信封(结果摘要)')
253
+ .action(async (opts) => {
254
+ try {
255
+ if (!['csv', 'json'].includes(opts.format)) {
256
+ throw new ConfigError(`--format must be csv|json, got "${opts.format}"`);
257
+ }
258
+ await baselineExportCmd(opts);
259
+ }
260
+ catch (e) {
261
+ handleError(e);
262
+ }
263
+ });
264
+ }
265
+ function registerActualCommands(program) {
266
+ const base = program.command('actual').description('实际人力数据');
267
+ // atlas actual show <staffId>
268
+ addProjectOptions(base
269
+ .command('show <staffId>')
270
+ .description('查看单个人员的实际工时明细'))
271
+ .option('--month <yyyymm>', '查询月份(YYYY-MM,默认当前月)')
272
+ .option('--json', '输出 JSON 信封')
273
+ .action(async (staffId, opts) => {
274
+ try {
275
+ await actualShowCmd(staffId, opts);
276
+ }
277
+ catch (e) {
278
+ handleError(e);
279
+ }
280
+ });
281
+ // atlas actual month
282
+ addProjectOptions(base
283
+ .command('month')
284
+ .description('实际工时明细(人员×周透视表)。无参数时默认查当前自然年'))
285
+ .option('--month <yyyymm>', '查询月份(YYYY-MM,与 --from/--to 互斥)')
286
+ .option('--from <yyyymm>', '起始月份(YYYY-MM,包含,与 --month 互斥)')
287
+ .option('--to <yyyymm>', '结束月份(YYYY-MM,包含,与 --month 互斥)')
288
+ .option('--status <status>', '筛选审批状态: pending | approved | all', 'all')
289
+ .option('--department <name>', '按团队负责人/部门筛选(子串,不区分大小写)')
290
+ .option('--role <name>', '按角色/备注筛选(子串,不区分大小写)')
291
+ .option('--staff-name <name>', '按姓名/工号筛选(子串,不区分大小写)')
292
+ .option('--json', '输出 JSON 信封')
293
+ .action(async (opts) => {
294
+ try {
295
+ await actualMonthCmd(opts);
296
+ }
297
+ catch (e) {
298
+ handleError(e);
299
+ }
300
+ });
301
+ // atlas actual summary
302
+ addProjectOptions(base
303
+ .command('summary')
304
+ .description('按月/部门/角色汇总实际工时'))
305
+ .option('--by <axis>', 'month | department | role', 'month')
306
+ .option('--month <yyyymm>', '查询月份')
307
+ .option('--status <status>', 'pending | approved | all', 'all')
308
+ .option('--department <name>', '按部门筛选')
309
+ .option('--role <name>', '按角色筛选')
310
+ .option('--from <yyyymm>', '起始月份')
311
+ .option('--to <yyyymm>', '结束月份')
312
+ .option('--json', '输出 JSON 信封')
313
+ .action(async (opts) => {
314
+ try {
315
+ await actualSummaryCmd(opts);
316
+ }
317
+ catch (e) {
318
+ handleError(e);
319
+ }
320
+ });
321
+ // atlas actual export
322
+ addProjectOptions(base
323
+ .command('export')
324
+ .description('导出实际工时数据(CSV/JSON)'))
325
+ .requiredOption('--format <fmt>', 'csv | json')
326
+ .requiredOption('--out <path>', '输出文件路径')
327
+ .option('--by <axis>', 'month | department | role', 'month')
328
+ .option('--status <status>', 'pending | approved | all', 'all')
329
+ .option('--department <name>', '按部门筛选')
330
+ .option('--role <name>', '按角色筛选')
331
+ .option('--from <yyyymm>', '起始月份')
332
+ .option('--to <yyyymm>', '结束月份')
333
+ .option('--json', '输出 JSON 信封')
334
+ .action(async (opts) => {
335
+ try {
336
+ await actualExportCmd(opts);
337
+ }
338
+ catch (e) {
339
+ handleError(e);
340
+ }
341
+ });
342
+ }
343
+ function registerCompareCommands(program) {
344
+ addProjectOptions(program
345
+ .command('compare')
346
+ .description('Compare baseline (计划) vs actual (实际) manpower'))
347
+ .option('--by <axis>', 'month | department | role', 'month')
348
+ .option('--from <yyyymm>', '起始月份(YYYY-MM,包含)')
349
+ .option('--to <yyyymm>', '结束月份(YYYY-MM,包含)')
350
+ .option('--month <yyyymm>', '查询月份(YYYY-MM,优先级高于 from/to 用于实际数据 API)')
351
+ .option('--department <name>', '按部门名称/ID 筛选(子串,不区分大小写)')
352
+ .option('--role <name>', '按角色/备注筛选(子串,不区分大小写)')
353
+ .option('--status <status>', '筛选审批状态: pending | approved | all', 'all')
354
+ .option('--threshold <n>', '差异绝对值阈值(人月),低于此值不标记', '0')
355
+ .option('--flag-overrun', '用 ⚠️ 标记实际 > 基线的情况')
356
+ .option('--page <n>', '页码(从 1 开始)')
357
+ .option('--page-size <n>', '每页条目数(大于 0 时启用分页)')
358
+ .option('--json', '输出 JSON 信封')
359
+ .action(async (opts) => {
360
+ try {
361
+ await compareCmd(opts);
362
+ }
363
+ catch (e) {
364
+ handleError(e);
365
+ }
366
+ });
367
+ }
368
+ function registerUtilityCommands(program) {
369
+ // daemon
370
+ program
371
+ .command('daemon')
372
+ .description('启动本地守护进程(沙盒环境使用,保持浏览器会话)')
373
+ .option('--port <n>', '监听端口(默认 8765,也可用 ATLAS_DAEMON_PORT 环境变量)')
374
+ .option('--json', '输出 JSON 信封')
375
+ .action(async (opts) => {
376
+ try {
377
+ await daemonCmd(opts);
378
+ }
379
+ catch (e) {
380
+ handleError(e);
381
+ }
382
+ });
383
+ // schema
384
+ 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
+ schema
400
+ .command('commands')
401
+ .description('列出所有命令的参数 schema 及输出 schema 标注')
402
+ .option('--json', '输出 JSON 信封')
403
+ .option('--describe', '输出完整命令 schema 含 outputSchema(agent 自省用)')
404
+ .action((opts) => {
405
+ try {
406
+ schemaCommandsCmd(program, opts);
407
+ }
408
+ catch (e) {
409
+ handleError(e);
410
+ }
411
+ });
412
+ // exec
413
+ program
414
+ .command('exec')
415
+ .description('按 plan-file 顺序执行多条命令(agent 批处理用)')
416
+ .requiredOption('--plan-file <path>', 'JSON 计划文件路径')
417
+ .option('--json', '输出 JSON 信封')
418
+ .action(async (opts) => {
419
+ try {
420
+ await execCmd(opts);
421
+ }
422
+ catch (e) {
423
+ handleError(e);
424
+ }
425
+ });
426
+ // update
427
+ program
428
+ .command('update')
429
+ .description('检查并自动升级到最新版本(设置 ATLAS_DISABLE_UPDATE=1 禁用)')
430
+ .option('--json', '输出 JSON 信封')
431
+ .action(async (opts) => {
432
+ try {
433
+ await updateCmd(opts);
434
+ }
435
+ catch (e) {
436
+ handleError(e);
437
+ }
438
+ });
439
+ // suggest
440
+ program
441
+ .command('suggest <query...>')
442
+ .description('将自然语言查询翻译为候选 atlas 命令(纯规则,不调 LLM)')
443
+ .option('--json', '输出 JSON 信封')
444
+ .action((tokens, opts) => {
445
+ try {
446
+ suggestCmd(tokens.join(' '), opts);
447
+ }
448
+ catch (e) {
449
+ handleError(e);
450
+ }
451
+ });
452
+ }
453
+ // Version is generated from package.json by scripts/sync-version.mjs (run as
454
+ // prebuild). Inlined as a string constant so both `tsc` and bun --compile
455
+ // single-file binaries can read it without runtime fs access.
456
+ import { ATLAS_VERSION } from './util/version.js';
457
+ export function buildProgram() {
458
+ const program = new Command();
459
+ program
460
+ .name('atlas')
461
+ .description('Atlas CLI - 斑马云图人力基线管理工具')
462
+ .version(ATLAS_VERSION, '-V, --version', '显示版本号')
463
+ .option('--json', '以 JSON 信封输出(也可用环境变量 ATLAS_OUTPUT=json)')
464
+ .option('--quiet', '静默模式:抑制非信封输出(设置 ATLAS_QUIET=1 也可)')
465
+ .option('--describe', '不执行命令,仅输出该命令的参数 schema(agent 自省用)')
466
+ .addHelpText('after', `
467
+ 退出码:
468
+ 0 成功
469
+ 1 通用错误 / exec 中某 step 失败
470
+ 2 会话过期(需重新登录)
471
+ 3 API 返回错误(BanmaApiError)
472
+ 4 项目匹配歧义(AmbiguousProject)
473
+ 5 项目未找到(ProjectNotFound)
474
+ 6 API 限流(RateLimited)
475
+ 7 网络错误(NetworkError)
476
+ 8 版本更新异常(UpdateError)
477
+ 64 配置错误 / 未实现(ConfigError)
478
+ `)
479
+ .showHelpAfterError()
480
+ .hook('preAction', (thisCommand, actionCommand) => {
481
+ const opts = thisCommand.opts();
482
+ if (opts.json === true && process.env.ATLAS_OUTPUT === undefined) {
483
+ process.env.ATLAS_OUTPUT = 'json';
484
+ }
485
+ if (opts.quiet === true) {
486
+ process.env.ATLAS_QUIET = '1';
487
+ }
488
+ if (opts.describe === true) {
489
+ emitDescribe(actionCommand);
490
+ process.exit(0);
491
+ }
492
+ });
493
+ registerAuthCommands(program);
494
+ registerProjectCommands(program);
495
+ registerBaselineCommands(program);
496
+ registerActualCommands(program);
497
+ registerCompareCommands(program);
498
+ registerUtilityCommands(program);
499
+ return program;
500
+ }
501
+ // 注意:auto-run 入口已移至 bin/atlas.ts
502
+ // 此模块仅 export buildProgram / handleError / emitDescribe
@@ -0,0 +1,100 @@
1
+ const schemas = {
2
+ 'atlas auth login': {
3
+ jsonSchema: {
4
+ type: 'object',
5
+ properties: {
6
+ ok: { type: 'boolean', const: true },
7
+ data: {
8
+ type: 'object',
9
+ properties: {
10
+ status: { type: 'string', enum: ['logged_in'] },
11
+ },
12
+ },
13
+ },
14
+ },
15
+ },
16
+ 'atlas auth status': {
17
+ jsonSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ ok: { type: 'boolean' },
21
+ data: {
22
+ type: 'object',
23
+ properties: {
24
+ loggedIn: { type: 'boolean' },
25
+ expiresAt: { type: 'number' },
26
+ },
27
+ },
28
+ },
29
+ },
30
+ },
31
+ 'atlas projects': {
32
+ jsonSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ ok: { type: 'boolean' },
36
+ data: { type: 'array', items: { type: 'object' } },
37
+ },
38
+ },
39
+ },
40
+ 'atlas link': {
41
+ jsonSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ ok: { type: 'boolean' },
45
+ data: {
46
+ type: 'object',
47
+ properties: {
48
+ projectId: { type: 'string' },
49
+ projectName: { type: 'string' },
50
+ linkedAt: { type: 'string' },
51
+ },
52
+ },
53
+ },
54
+ },
55
+ },
56
+ 'atlas baseline month': {
57
+ jsonSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ ok: { type: 'boolean' },
61
+ data: {
62
+ type: 'object',
63
+ properties: {
64
+ months: { type: 'array', items: { type: 'string' } },
65
+ totalManpower: { type: 'number' },
66
+ },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ 'atlas baseline summary': {
72
+ jsonSchema: {
73
+ type: 'object',
74
+ properties: {
75
+ ok: { type: 'boolean' },
76
+ data: {
77
+ type: 'array',
78
+ items: {
79
+ type: 'object',
80
+ properties: {
81
+ month: { type: 'string' },
82
+ manpower: { type: 'number' },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ };
90
+ export function getOutputSchema(commandPath) {
91
+ return schemas[commandPath] ?? {
92
+ jsonSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ ok: { type: 'boolean' },
96
+ data: {},
97
+ },
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * actual 命令的纯逻辑层。
3
+ *
4
+ * 与命令解析层(index.ts)分离:这里不做 IO、不调 API、不输出,
5
+ * 只做数据变换。便于单元测试(参见 tests/actual_logic.test.ts)。
6
+ *
7
+ * 单位约定:manpower 字段为人月(API 直接返回人月)。
8
+ */
9
+ /**
10
+ * expandMonths 已移至 util/months.ts,此处不再导出。
11
+ */
12
+ /** 给每条记录标注 month 字段,返回新数组(不改原数据) */
13
+ export function annotateWithMonth(entries, month) {
14
+ return entries.map((p) => ({ ...p, month }));
15
+ }
16
+ /**
17
+ * 按轴聚合人月。
18
+ * - month:按月份分组(需先用 annotateWithMonth 打标)
19
+ * - department:按 departmentName 分组,缺失归 "其他"
20
+ * - role:按 role 分组,缺失归 "其他"
21
+ * 返回 [{ [axis]: key, manpower }],manpower 四舍五入到 2 位。
22
+ */
23
+ export function aggregateByAxis(rows, axis) {
24
+ const groups = new Map();
25
+ for (const p of rows) {
26
+ const k = axis === 'month' ? p.month
27
+ : axis === 'department' ? (p.departmentName ?? '其他')
28
+ : (p.role ?? '其他');
29
+ groups.set(k, (groups.get(k) ?? 0) + (p.manpower ?? 0));
30
+ }
31
+ return [...groups.entries()].map(([k, v]) => ({
32
+ [axis]: k,
33
+ manpower: Math.round(v * 100) / 100,
34
+ }));
35
+ }
36
+ /**
37
+ * 过滤出指定人员的记录:staffId 精确匹配,或 staffName 包含匹配。
38
+ */
39
+ export function filterByStaff(entries, staffId) {
40
+ return entries.filter((p) => p.staffId === staffId || p.staffName?.includes(staffId));
41
+ }