weaver-work-cli 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/dist/cmd/action.js +35 -0
  4. package/dist/cmd/commands/index.js +15 -0
  5. package/dist/cmd/config/index.js +64 -0
  6. package/dist/cmd/docs/index.js +56 -0
  7. package/dist/cmd/doctor/index.js +45 -0
  8. package/dist/cmd/e10/index.js +25 -0
  9. package/dist/cmd/index.js +18 -0
  10. package/dist/cmd/register.js +19 -0
  11. package/dist/cmd/root.js +36 -0
  12. package/dist/cmd/run/index.js +38 -0
  13. package/dist/cmd/setup/index.js +77 -0
  14. package/dist/cmd/skills/index.js +72 -0
  15. package/dist/core/config-store.js +43 -0
  16. package/dist/core/context.js +31 -0
  17. package/dist/core/errors.js +60 -0
  18. package/dist/core/hooks.js +41 -0
  19. package/dist/core/logger.js +27 -0
  20. package/dist/core/output.js +25 -0
  21. package/dist/core/write-with-retry.js +21 -0
  22. package/dist/index.js +11 -0
  23. package/dist/internal/e10/auth/commands.js +594 -0
  24. package/dist/internal/e10/auth/crypto.js +126 -0
  25. package/dist/internal/e10/auth/paths.js +12 -0
  26. package/dist/internal/e10/auth/session.js +449 -0
  27. package/dist/internal/e10/client.js +104 -0
  28. package/dist/internal/e10/context.js +20 -0
  29. package/dist/internal/e10/index.js +2 -0
  30. package/dist/internal/e10/types.js +1 -0
  31. package/dist/internal/skills/install.js +99 -0
  32. package/dist/internal/skills/reader.js +119 -0
  33. package/dist/services/command-runner.js +44 -0
  34. package/dist/shortcuts/index.js +4 -0
  35. package/dist/shortcuts/invoice/continuation.js +70 -0
  36. package/dist/shortcuts/invoice/errors.js +59 -0
  37. package/dist/shortcuts/invoice/host.js +337 -0
  38. package/dist/shortcuts/invoice/index.js +301 -0
  39. package/dist/shortcuts/invoice/manifest.js +288 -0
  40. package/dist/shortcuts/invoice/operations/add.js +106 -0
  41. package/dist/shortcuts/invoice/operations/delete.js +92 -0
  42. package/dist/shortcuts/invoice/operations/download.js +16 -0
  43. package/dist/shortcuts/invoice/operations/get.js +7 -0
  44. package/dist/shortcuts/invoice/operations/import.js +128 -0
  45. package/dist/shortcuts/invoice/operations/list.js +12 -0
  46. package/dist/shortcuts/invoice/operations/ocr-preview.js +40 -0
  47. package/dist/shortcuts/invoice/operations/registry.js +50 -0
  48. package/dist/shortcuts/invoice/operations/shared.js +640 -0
  49. package/dist/shortcuts/invoice/operations/types.js +1 -0
  50. package/dist/shortcuts/invoice/operations/update.js +109 -0
  51. package/dist/shortcuts/invoice/operations/upload.js +8 -0
  52. package/dist/shortcuts/invoice/operations/validate-preview.js +39 -0
  53. package/dist/shortcuts/invoice/operations.js +8 -0
  54. package/dist/types.js +1 -0
  55. package/docs/SKILL.md +36 -0
  56. package/docs/_catalog.md +12 -0
  57. package/docs/agent-invoice.md +53 -0
  58. package/docs/e10-auth.md +51 -0
  59. package/docs/invoice.md +99 -0
  60. package/package.json +51 -0
  61. package/scripts/clean.mjs +4 -0
  62. package/scripts/package-skill.mjs +164 -0
  63. package/scripts/sync-docs.mjs +17 -0
  64. package/skill-template/business-info.json +4 -0
  65. package/skill-template/domains/invoice.md +23 -0
  66. package/skill-template/domains/shared.md +8 -0
  67. package/skill-template/master-skill-template.md +29 -0
  68. package/skill-template/skill-template.md +60 -0
  69. package/skills/weaver-work-cli-invoice/SKILL.md +103 -0
  70. package/skills/weaver-work-cli-invoice/product.json +8 -0
  71. package/skills/weaver-work-cli-invoice/references/invoice-add.md +18 -0
  72. package/skills/weaver-work-cli-invoice/references/invoice-agent-entry.md +38 -0
  73. package/skills/weaver-work-cli-invoice/references/invoice-delete.md +20 -0
  74. package/skills/weaver-work-cli-invoice/references/invoice-detail.md +29 -0
  75. package/skills/weaver-work-cli-invoice/references/invoice-disabled-capabilities.md +26 -0
  76. package/skills/weaver-work-cli-invoice/references/invoice-download.md +22 -0
  77. package/skills/weaver-work-cli-invoice/references/invoice-enterprise-list.md +35 -0
  78. package/skills/weaver-work-cli-invoice/references/invoice-file-upload.md +28 -0
  79. package/skills/weaver-work-cli-invoice/references/invoice-import.md +28 -0
  80. package/skills/weaver-work-cli-invoice/references/invoice-ocr-preview.md +33 -0
  81. package/skills/weaver-work-cli-invoice/references/invoice-personal-list.md +47 -0
  82. package/skills/weaver-work-cli-invoice/references/invoice-update.md +38 -0
  83. package/skills/weaver-work-cli-invoice/references/invoice-validation-preview.md +26 -0
  84. package/skills/weaver-work-cli-shared/SKILL.md +53 -0
  85. package/skills/weaver-work-cli-shared/references/e10-auth-and-session.md +83 -0
  86. package/skills/weaver-work-cli-shared/references/high-risk-write.md +36 -0
  87. package/skills/weaver-work-cli-shared/references/json-output-contract.md +61 -0
  88. package/skills/weaver-work-cli-shared/references/weaver-work-cli-installation.md +61 -0
  89. package/templates/shortcut/index.ts.tpl +19 -0
  90. package/templates/shortcut/operations/ping.ts.tpl +7 -0
@@ -0,0 +1,594 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createServer } from 'node:http';
3
+ import { CliError, ExitCode } from '../../../core/errors.js';
4
+ import { buildE10CookieHeader, E10AuthSession, clearProfileAuthFile, extractRawSetCookie, findAuthFile, getActiveProfile, getProfileAuthPath, getProfileDir, listProfiles, profileNameFromUrl, readProfileConfig, readTeamsCheckUserInfo, saveAuthFile, saveProfileConfig, setActiveProfile, } from './session.js';
5
+ import { getE10AuthRoot } from './paths.js';
6
+ const DEFAULT_AGENT_TYPE = 'weaver-work-cli';
7
+ function asOptions(value) {
8
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
9
+ }
10
+ function optionString(options, ...keys) {
11
+ for (const key of keys) {
12
+ const value = options[key];
13
+ if (typeof value === 'string' && value.trim())
14
+ return value.trim();
15
+ }
16
+ return '';
17
+ }
18
+ function normalizeUrl(value) {
19
+ return value.replace(/\/+$/, '');
20
+ }
21
+ function readString(data, ...keys) {
22
+ for (const key of keys) {
23
+ const value = data[key];
24
+ if (typeof value === 'string' && value)
25
+ return value;
26
+ if (typeof value === 'number')
27
+ return String(value);
28
+ }
29
+ return '';
30
+ }
31
+ function cookieString(value) {
32
+ if (!value)
33
+ return '';
34
+ if (typeof value === 'string')
35
+ return value;
36
+ if (typeof value === 'object' && !Array.isArray(value)) {
37
+ return Object.entries(value)
38
+ .filter(([, cookieValue]) => cookieValue !== undefined && cookieValue !== null && cookieValue !== '')
39
+ .map(([name, cookieValue]) => `${name}=${String(cookieValue)}`)
40
+ .join('; ');
41
+ }
42
+ return '';
43
+ }
44
+ function cookieValue(rawCookie, name) {
45
+ const match = rawCookie.match(new RegExp(`${name}=([^;]+)`));
46
+ return match?.[1] || '';
47
+ }
48
+ function formatAuthStatus(status) {
49
+ if (!status.loggedIn) {
50
+ return [
51
+ 'Not logged in.',
52
+ 'Run: weaver-work-cli auth login --base-url <url>',
53
+ 'Or: weaver-work-cli auth set --eteamsid <id> --base-url <url>',
54
+ ].join('\n');
55
+ }
56
+ const lines = [
57
+ 'Logged in.',
58
+ 'userId: ' + status.userId,
59
+ 'tenantKey: ' + (status.tenantKey || ''),
60
+ 'tenantName: ' + (status.tenantName || ''),
61
+ 'department: ' + (status.department || ''),
62
+ 'subcompany: ' + (status.subcompany || ''),
63
+ 'position: ' + (status.position || ''),
64
+ 'version: ' + (status.version || ''),
65
+ 'agentType: ' + (status.agentType || ''),
66
+ 'baseUrl: ' + status.baseUrl,
67
+ 'profile: ' + status.profile,
68
+ 'authFile: ' + status.authFile,
69
+ ];
70
+ if ('valid' in status) {
71
+ lines.push('valid: ' + String(status.valid));
72
+ }
73
+ return lines.join('\n');
74
+ }
75
+ function formatLoginResult(result) {
76
+ return [
77
+ 'Saved E10 auth.',
78
+ 'userId: ' + result.userId,
79
+ 'tenantKey: ' + result.tenantKey,
80
+ 'tenantName: ' + result.tenantName,
81
+ 'baseUrl: ' + result.baseUrl,
82
+ 'profile: ' + result.profile,
83
+ 'authFile: ' + result.authFile,
84
+ ].join('\n');
85
+ }
86
+ function shellQuote(value) {
87
+ return `'${value.replace(/'/g, "'\\''")}'`;
88
+ }
89
+ function esc(value) {
90
+ return String(value ?? '')
91
+ .replace(/&/g, '&amp;')
92
+ .replace(/</g, '&lt;')
93
+ .replace(/>/g, '&gt;')
94
+ .replace(/"/g, '&quot;')
95
+ .replace(/'/g, '&#39;');
96
+ }
97
+ function openUrlInBrowser(url, ctx) {
98
+ const command = process.platform === 'win32'
99
+ ? { file: 'cmd', args: ['/c', 'start', '', url] }
100
+ : process.platform === 'darwin'
101
+ ? { file: 'open', args: [url] }
102
+ : { file: 'xdg-open', args: [url] };
103
+ execFile(command.file, command.args, (error) => {
104
+ if (error) {
105
+ ctx.logger.warn('Could not open browser automatically. Copy the login URL manually.');
106
+ }
107
+ });
108
+ }
109
+ async function readJsonObject(resp) {
110
+ const data = await resp.json().catch(() => ({}));
111
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
112
+ }
113
+ async function resolveUserInfo(baseUrl, eteamsId, rawCookie = '', agentType = '') {
114
+ const resp = await fetch(`${baseUrl}/api/baseserver/layout/teamsCheck?clientType=not_xinchuang&client=WEB&domainName=${encodeURIComponent(baseUrl)}`, {
115
+ method: 'POST',
116
+ headers: {
117
+ accept: 'application/json',
118
+ 'content-type': 'application/json',
119
+ eteamsid: eteamsId,
120
+ cookie: buildE10CookieHeader(rawCookie, eteamsId, agentType),
121
+ 'user-agent': agentType ? `AgentType=${agentType},IsAgent=true` : 'e10-login',
122
+ origin: baseUrl,
123
+ },
124
+ redirect: 'manual',
125
+ });
126
+ const data = await resp.json().catch(() => ({}));
127
+ return readTeamsCheckUserInfo(resp, data);
128
+ }
129
+ async function oidcExchange(code, redirectUri, baseUrl, tenantKeyFallback, agentTypeFallback, profileHint) {
130
+ const base = normalizeUrl(baseUrl);
131
+ const tokenParams = new URLSearchParams({
132
+ grant_type: 'authorization_code',
133
+ code,
134
+ redirect_uri: redirectUri,
135
+ });
136
+ const tokenResp = await fetch(`${base}/papi/sso/oauth2.0/accessToken`, {
137
+ method: 'POST',
138
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
139
+ body: tokenParams.toString(),
140
+ redirect: 'manual',
141
+ });
142
+ const tokenData = await readJsonObject(tokenResp);
143
+ if (tokenResp.status >= 400) {
144
+ const message = readString(tokenData, 'message', 'msg', 'error_description', 'error') || `OAuth token exchange failed: ${tokenResp.status}`;
145
+ throw new CliError(message, ExitCode.AuthError);
146
+ }
147
+ let eteamsId = readString(tokenData, 'eteamsId', 'eteamsid');
148
+ let userId = readString(tokenData, 'id', 'userId', 'employeeId');
149
+ let rawCookies = cookieString(tokenData.cookies || tokenData.cookie) || extractRawSetCookie(tokenResp);
150
+ let agentType = agentTypeFallback || readString(tokenData, 'agentType', 'agent_type');
151
+ let version = readString(tokenData, 'version', 'baselineVersion');
152
+ const accessToken = readString(tokenData, 'access_token', 'accessToken');
153
+ if (!eteamsId)
154
+ eteamsId = cookieValue(rawCookies, 'ETEAMSID');
155
+ if (accessToken && (!eteamsId || !userId || !rawCookies || !version || !agentType)) {
156
+ const profileResp = await fetch(`${base}/papi/sso/oauth2.0/profile?access_token=${encodeURIComponent(accessToken)}`, {
157
+ method: 'POST',
158
+ headers: { accept: 'application/json' },
159
+ redirect: 'manual',
160
+ });
161
+ const profileData = await readJsonObject(profileResp);
162
+ const profileRawCookies = cookieString(profileData.cookies || profileData.cookie) || extractRawSetCookie(profileResp);
163
+ if (!eteamsId)
164
+ eteamsId = readString(profileData, 'eteamsId', 'eteamsid') || cookieValue(profileRawCookies, 'ETEAMSID');
165
+ if (!userId)
166
+ userId = readString(profileData, 'id', 'userId', 'employeeId');
167
+ if (!rawCookies)
168
+ rawCookies = profileRawCookies;
169
+ if (!agentType)
170
+ agentType = readString(profileData, 'agentType', 'agent_type');
171
+ if (!version)
172
+ version = readString(profileData, 'version', 'baselineVersion');
173
+ }
174
+ if (!eteamsId) {
175
+ throw new CliError('Could not resolve ETEAMSID from OAuth response.', ExitCode.AuthError);
176
+ }
177
+ const userInfo = await resolveUserInfo(base, eteamsId, rawCookies, agentType).catch(() => ({
178
+ userId: '',
179
+ tenantKey: '',
180
+ tenantName: '',
181
+ department: '',
182
+ subcompany: '',
183
+ position: '',
184
+ }));
185
+ const tenantKey = readString(tokenData, 'tenant_key', 'tenantKey')
186
+ || userInfo.tenantKey
187
+ || tenantKeyFallback;
188
+ const authData = {
189
+ cookies: { ETEAMSID: eteamsId, raw: rawCookies },
190
+ userId: userInfo.userId || userId || 'unknown',
191
+ tenantKey,
192
+ tenantName: userInfo.tenantName,
193
+ department: userInfo.department,
194
+ subcompany: userInfo.subcompany,
195
+ position: userInfo.position,
196
+ agentType,
197
+ version,
198
+ baseUrl: base,
199
+ passportUrl: base,
200
+ cookieSavedAt: Date.now(),
201
+ };
202
+ const profile = profileHint || profileNameFromUrl(base);
203
+ setActiveProfile(profile);
204
+ const authFile = saveAuthFile(authData, profile);
205
+ saveProfileConfig(base, base, profile, tenantKey);
206
+ return {
207
+ userId: authData.userId,
208
+ tenantKey,
209
+ tenantName: authData.tenantName || '',
210
+ department: authData.department || '',
211
+ subcompany: authData.subcompany || '',
212
+ position: authData.position || '',
213
+ eteamsId,
214
+ baseUrl: base,
215
+ passportUrl: base,
216
+ profile,
217
+ authFile,
218
+ agentType,
219
+ version,
220
+ };
221
+ }
222
+ async function runBrowserLogin(ctx, options) {
223
+ const commandOpts = asOptions(options);
224
+ const globalOptions = ctx.getGlobalOptions();
225
+ const profileFromGlobal = globalOptions.profile;
226
+ const cfg = readProfileConfig(profileFromGlobal || getActiveProfile());
227
+ const baseUrl = normalizeUrl(optionString(commandOpts, 'baseUrl') || globalOptions.baseUrl || cfg.baseUrl || '');
228
+ const tenantKey = optionString(commandOpts, 'tenantKey');
229
+ const portText = optionString(commandOpts, 'port') || '19800';
230
+ const port = Number.parseInt(portText, 10);
231
+ const explicitAgentType = optionString(commandOpts, 'agentType', 'agent_type');
232
+ const agentType = explicitAgentType || DEFAULT_AGENT_TYPE;
233
+ if (!baseUrl)
234
+ throw new CliError('Missing --base-url', ExitCode.UsageError);
235
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
236
+ throw new CliError('Invalid --port value', ExitCode.UsageError);
237
+ }
238
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
239
+ const authorizeParams = new URLSearchParams({
240
+ redirect_uri: redirectUri,
241
+ response_type: 'code',
242
+ access_type: 'agent',
243
+ agent_type: agentType,
244
+ });
245
+ const loginUrl = `${baseUrl}/papi/sso/oauth2.0/authorize?${authorizeParams.toString()}`;
246
+ ctx.logger.info('Open browser login: ' + loginUrl);
247
+ ctx.logger.info('Waiting for E10 authorization callback...');
248
+ return await new Promise((resolve, reject) => {
249
+ let settled = false;
250
+ let timer = null;
251
+ const finish = (error, result) => {
252
+ if (settled)
253
+ return;
254
+ settled = true;
255
+ if (timer)
256
+ clearTimeout(timer);
257
+ const complete = () => {
258
+ if (error)
259
+ reject(error);
260
+ else
261
+ resolve(result);
262
+ };
263
+ try {
264
+ server.close(complete);
265
+ }
266
+ catch {
267
+ complete();
268
+ }
269
+ };
270
+ const server = createServer(async (req, res) => {
271
+ const callbackUrl = new URL(req.url || '/', `http://127.0.0.1:${port}`);
272
+ if (callbackUrl.pathname !== '/callback') {
273
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
274
+ res.end('Not found');
275
+ return;
276
+ }
277
+ const error = callbackUrl.searchParams.get('error');
278
+ const code = callbackUrl.searchParams.get('code');
279
+ const redirectAgentType = explicitAgentType
280
+ || callbackUrl.searchParams.get('agent_type')
281
+ || callbackUrl.searchParams.get('agentType')
282
+ || agentType;
283
+ if (error) {
284
+ const message = callbackUrl.searchParams.get('error_description') || error;
285
+ res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
286
+ res.end(`<h1>Authorization failed</h1><p>${esc(message)}</p>`);
287
+ finish(new CliError('OAuth authorization failed: ' + message, ExitCode.AuthError));
288
+ return;
289
+ }
290
+ if (!code) {
291
+ res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
292
+ res.end('<h1>Login failed</h1><p>Missing authorization code.</p>');
293
+ finish(new CliError('No authorization code received.', ExitCode.AuthError));
294
+ return;
295
+ }
296
+ try {
297
+ const result = await oidcExchange(code, redirectUri, baseUrl, tenantKey, redirectAgentType, profileFromGlobal);
298
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
299
+ res.end(`<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>weaver-work-cli login</title><style>body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;background:#f6f7f9;color:#20242a;display:grid;place-items:center;min-height:100vh;margin:0}.card{background:#fff;border:1px solid #e6e8ec;border-radius:8px;padding:28px 32px;box-shadow:0 8px 32px rgba(18,24,40,.08);min-width:320px;text-align:center}.title{font-size:20px;font-weight:600;margin-bottom:8px}.desc{color:#667085;font-size:14px;line-height:22px}.user{color:#98a2b3;font-size:12px;margin-top:12px}</style></head><body><main class="card"><div class="title">Login completed</div><div class="desc">You can close this page and return to the terminal.</div><div class="user">${esc(result.userId)}</div></main></body></html>`);
300
+ finish(null, result);
301
+ }
302
+ catch (exchangeError) {
303
+ const message = exchangeError instanceof Error ? exchangeError.message : String(exchangeError);
304
+ res.writeHead(500, { 'content-type': 'text/html; charset=utf-8' });
305
+ res.end(`<h1>Login failed</h1><p>${esc(message)}</p>`);
306
+ finish(exchangeError);
307
+ }
308
+ });
309
+ server.on('error', (error) => {
310
+ const message = error.code === 'EADDRINUSE'
311
+ ? `Port ${port} is already in use. Retry with --port ${port + 1}.`
312
+ : error.code === 'EACCES'
313
+ ? `Port ${port} is not accessible. Retry with another --port.`
314
+ : error.message;
315
+ finish(new CliError(message, ExitCode.UsageError));
316
+ });
317
+ server.listen(port, '127.0.0.1', () => {
318
+ ctx.logger.info('Local callback server: ' + redirectUri);
319
+ openUrlInBrowser(loginUrl, ctx);
320
+ });
321
+ timer = setTimeout(() => {
322
+ finish(new CliError('Login timed out after 5 minutes.', ExitCode.AuthError));
323
+ }, 300_000);
324
+ });
325
+ }
326
+ function sessionToCachedUser(session, profile) {
327
+ return {
328
+ employeeId: session.userId,
329
+ tenantKey: session.tenantKey,
330
+ tenantName: session.tenantName,
331
+ department: session.department,
332
+ subcompany: session.subcompany,
333
+ position: session.position,
334
+ version: session.version,
335
+ baseUrl: session.getBaseUrl(),
336
+ profile,
337
+ eteamsId: session.eteamsId,
338
+ agentType: session.agentType,
339
+ cookie: session.cookieHeader(),
340
+ userAgent: session.uaHeader(),
341
+ };
342
+ }
343
+ function formatWhoami(title, data) {
344
+ return [
345
+ title,
346
+ 'employeeId: ' + (data.employeeId || ''),
347
+ 'tenantKey: ' + (data.tenantKey || ''),
348
+ 'tenantName: ' + (data.tenantName || ''),
349
+ 'department: ' + (data.department || ''),
350
+ 'subcompany: ' + (data.subcompany || ''),
351
+ 'position: ' + (data.position || ''),
352
+ 'version: ' + (data.version || ''),
353
+ 'baseUrl: ' + (data.baseUrl || ''),
354
+ 'profile: ' + (data.profile || ''),
355
+ 'ETEAMSID: ' + (data.eteamsId || ''),
356
+ 'cookie: ' + (data.cookie || ''),
357
+ 'User-Agent: ' + (data.userAgent || ''),
358
+ ].join('\n');
359
+ }
360
+ function registerBrowserLoginCommand(auth, action, name, description) {
361
+ auth.command(name)
362
+ .description(description)
363
+ .option('--base-url <url>', 'E10 base URL')
364
+ .option('--tenant-key <key>', 'Tenant key fallback after login')
365
+ .option('--port <n>', 'Local OAuth callback port', '19800')
366
+ .option('--agent_type <value>', 'Agent type sent to E10 authorize endpoint')
367
+ .option('--agent-type <value>', 'Alias of --agent_type')
368
+ .action(action(async (ctx, options) => {
369
+ const result = await runBrowserLogin(ctx, options);
370
+ ctx.output.write(result, { text: formatLoginResult(result) });
371
+ }));
372
+ }
373
+ export function registerE10AuthCommands(parent, action) {
374
+ const auth = parent.command('auth').description('E10 auth and profile management');
375
+ auth.command('root')
376
+ .description('Print the E10 auth root directory')
377
+ .action(action((ctx) => {
378
+ const root = getE10AuthRoot();
379
+ ctx.output.write({ root }, { text: root });
380
+ }));
381
+ auth.command('status')
382
+ .description('Show E10 login status')
383
+ .option('--no-check', 'skip remote session validity check')
384
+ .action(action(async (ctx, options) => {
385
+ const opts = ctx.getGlobalOptions();
386
+ const commandOpts = asOptions(options);
387
+ const authFile = findAuthFile(ctx.cwd, opts);
388
+ const session = authFile ? E10AuthSession.fromFile(authFile, opts.baseUrl, opts.passportUrl) : null;
389
+ const valid = session && commandOpts.check !== false ? await session.checkValid() : undefined;
390
+ const status = session
391
+ ? {
392
+ loggedIn: true,
393
+ userId: session.userId,
394
+ tenantKey: session.tenantKey,
395
+ tenantName: session.tenantName,
396
+ department: session.department,
397
+ subcompany: session.subcompany,
398
+ position: session.position,
399
+ version: session.version,
400
+ agentType: session.agentType,
401
+ baseUrl: session.getBaseUrl(),
402
+ passportUrl: session.getPassportUrl(),
403
+ profile: opts.profile || getActiveProfile(),
404
+ authFile,
405
+ hasEteamsId: Boolean(session.eteamsId),
406
+ valid,
407
+ }
408
+ : {
409
+ loggedIn: false,
410
+ profile: opts.profile || getActiveProfile(),
411
+ authFile,
412
+ };
413
+ ctx.output.write(status, { text: formatAuthStatus(status) });
414
+ }));
415
+ auth.command('set')
416
+ .description('Save E10 auth from an ETEAMSID cookie')
417
+ .requiredOption('--eteamsid <id>', 'ETEAMSID cookie value')
418
+ .option('--base-url <url>', 'E10 base URL')
419
+ .option('--passport-url <url>', 'E10 passport URL; defaults to base URL')
420
+ .option('--tenant-key <key>', 'Tenant key fallback')
421
+ .option('--agent_type <value>', 'Agent type used in cookie and User-Agent')
422
+ .option('--agent-type <value>', 'Alias of --agent_type')
423
+ .action(action(async (ctx, options) => {
424
+ const globalOptions = ctx.getGlobalOptions();
425
+ const commandOpts = asOptions(options);
426
+ const profileFromGlobal = globalOptions.profile;
427
+ const cfg = readProfileConfig(profileFromGlobal || getActiveProfile());
428
+ const baseUrl = normalizeUrl(optionString(commandOpts, 'baseUrl') || globalOptions.baseUrl || cfg.baseUrl || '');
429
+ const passportUrl = normalizeUrl(optionString(commandOpts, 'passportUrl') || globalOptions.passportUrl || cfg.passportUrl || baseUrl);
430
+ const tenantKeyFallback = optionString(commandOpts, 'tenantKey');
431
+ const agentType = optionString(commandOpts, 'agentType', 'agent_type');
432
+ const eteamsId = optionString(commandOpts, 'eteamsid');
433
+ if (!baseUrl)
434
+ throw new CliError('Missing --base-url', ExitCode.UsageError);
435
+ if (!eteamsId)
436
+ throw new CliError('Missing --eteamsid', ExitCode.UsageError);
437
+ ctx.logger.info('Resolving E10 user information...');
438
+ const userInfo = await resolveUserInfo(baseUrl, eteamsId, '', agentType);
439
+ if (!userInfo.userId) {
440
+ throw new CliError('Could not resolve userId. Check whether ETEAMSID is valid.', ExitCode.AuthError);
441
+ }
442
+ const tenantKey = userInfo.tenantKey || tenantKeyFallback;
443
+ const profile = profileFromGlobal || profileNameFromUrl(baseUrl);
444
+ setActiveProfile(profile);
445
+ const authData = {
446
+ cookies: { ETEAMSID: eteamsId },
447
+ userId: userInfo.userId,
448
+ tenantKey,
449
+ tenantName: userInfo.tenantName,
450
+ department: userInfo.department,
451
+ subcompany: userInfo.subcompany,
452
+ position: userInfo.position,
453
+ agentType,
454
+ baseUrl,
455
+ passportUrl,
456
+ cookieSavedAt: Date.now(),
457
+ };
458
+ const authFile = saveAuthFile(authData, profile);
459
+ saveProfileConfig(baseUrl, passportUrl, profile, tenantKey);
460
+ const result = {
461
+ ...userInfo,
462
+ tenantKey,
463
+ eteamsId,
464
+ baseUrl,
465
+ passportUrl,
466
+ profile,
467
+ authFile,
468
+ agentType,
469
+ version: '',
470
+ };
471
+ ctx.output.write(result, { text: formatLoginResult(result) });
472
+ }));
473
+ registerBrowserLoginCommand(auth, action, 'login', 'Open browser OAuth2.0 login and save E10 auth');
474
+ registerBrowserLoginCommand(auth, action, 'oidc', 'Alias of auth login for OAuth2.0 browser login');
475
+ auth.command('logout')
476
+ .alias('clear')
477
+ .description('Clear saved E10 auth for the current or selected profile')
478
+ .action(action((ctx) => {
479
+ const opts = ctx.getGlobalOptions();
480
+ const result = clearProfileAuthFile(opts.profile || getActiveProfile());
481
+ const text = result.cleared
482
+ ? 'Cleared E10 auth.\nprofile: ' + result.profile + '\nauthFile: ' + result.authFile
483
+ : 'No E10 auth to clear.\nprofile: ' + result.profile + '\nauthFile: ' + result.authFile;
484
+ ctx.output.write(result, { text });
485
+ }));
486
+ auth.command('whoami')
487
+ .description('Show cached or live E10 user information')
488
+ .option('--json', 'print output as JSON')
489
+ .option('--live', 'call teamsCheck for live user information')
490
+ .action(action(async (ctx, options) => {
491
+ const opts = ctx.getGlobalOptions();
492
+ const commandOpts = asOptions(options);
493
+ const json = commandOpts.json === true;
494
+ const live = commandOpts.live === true;
495
+ const authFile = findAuthFile(ctx.cwd, opts);
496
+ const session = authFile ? E10AuthSession.fromFile(authFile, opts.baseUrl, opts.passportUrl) : null;
497
+ if (!session || !session.eteamsId)
498
+ throw new CliError('E10 auth not found.', ExitCode.AuthError);
499
+ const profile = opts.profile || getActiveProfile();
500
+ const cached = sessionToCachedUser(session, profile);
501
+ if (!live) {
502
+ ctx.output.write(cached, {
503
+ json,
504
+ text: formatWhoami('Current E10 user (cached)', cached),
505
+ });
506
+ return;
507
+ }
508
+ const base = session.getBaseUrl();
509
+ const resp = await fetch(`${base}/api/baseserver/layout/teamsCheck?clientType=not_xinchuang&client=WEB&domainName=${encodeURIComponent(base)}`, {
510
+ method: 'POST',
511
+ headers: {
512
+ accept: 'application/json',
513
+ 'content-type': 'application/json',
514
+ eteamsid: session.eteamsId,
515
+ cookie: session.cookieHeader(),
516
+ 'user-agent': session.uaHeader(),
517
+ origin: base,
518
+ },
519
+ redirect: 'manual',
520
+ });
521
+ const raw = await resp.json().catch(() => ({}));
522
+ if (json || ctx.getGlobalOptions().json) {
523
+ ctx.output.write(raw, { json: true });
524
+ return;
525
+ }
526
+ const userInfo = readTeamsCheckUserInfo(resp, raw);
527
+ const liveData = {
528
+ ...cached,
529
+ employeeId: userInfo.userId || cached.employeeId,
530
+ tenantKey: userInfo.tenantKey || cached.tenantKey,
531
+ tenantName: userInfo.tenantName || cached.tenantName,
532
+ department: userInfo.department || cached.department,
533
+ subcompany: userInfo.subcompany || cached.subcompany,
534
+ position: userInfo.position || cached.position,
535
+ };
536
+ ctx.output.write(liveData, { text: formatWhoami('Current E10 user (live)', liveData) });
537
+ }));
538
+ auth.command('export-weaver-env')
539
+ .description('Print WEAVER_* shell exports for the active E10 auth')
540
+ .action(action(async (ctx) => {
541
+ const session = await E10AuthSession.fromOptions(ctx.getGlobalOptions(), ctx.cwd);
542
+ if (!session)
543
+ throw new CliError('E10 auth not found.', ExitCode.AuthError);
544
+ const value = {
545
+ WEAVER_BASE_URL: session.getBaseUrl(),
546
+ WEAVER_ETEAMSID: session.eteamsId,
547
+ WEAVER_COOKIE: session.cookieHeader(),
548
+ WEAVER_USER_AGENT: session.uaHeader(),
549
+ WEAVER_AGENT_TYPE: session.agentType,
550
+ };
551
+ if (ctx.getGlobalOptions().json) {
552
+ ctx.output.write(value);
553
+ return;
554
+ }
555
+ ctx.output.raw(`export WEAVER_BASE_URL=${shellQuote(value.WEAVER_BASE_URL)}`);
556
+ ctx.output.raw(`export WEAVER_ETEAMSID=${shellQuote(value.WEAVER_ETEAMSID)}`);
557
+ ctx.output.raw(`export WEAVER_COOKIE=${shellQuote(value.WEAVER_COOKIE)}`);
558
+ ctx.output.raw(`export WEAVER_USER_AGENT=${shellQuote(value.WEAVER_USER_AGENT)}`);
559
+ ctx.output.raw(`export WEAVER_AGENT_TYPE=${shellQuote(value.WEAVER_AGENT_TYPE)}`);
560
+ }));
561
+ const profile = auth.command('profile').description('Manage E10 profiles');
562
+ profile.command('list')
563
+ .description('List E10 profiles')
564
+ .action(action((ctx) => {
565
+ const active = getActiveProfile();
566
+ const profiles = listProfiles().map((name) => ({
567
+ name,
568
+ active: name === active,
569
+ dir: getProfileDir(name),
570
+ }));
571
+ ctx.output.write({ active, profiles });
572
+ }));
573
+ profile.command('use')
574
+ .description('Switch active E10 profile; creates it if missing')
575
+ .argument('<name>', 'profile name')
576
+ .action(action((ctx, name) => {
577
+ if (typeof name !== 'string')
578
+ throw new CliError('profile name must be a string', ExitCode.UsageError);
579
+ setActiveProfile(name);
580
+ const result = {
581
+ active: name,
582
+ dir: getProfileDir(name),
583
+ authFile: getProfileAuthPath(name),
584
+ };
585
+ ctx.output.write(result, { text: 'Active E10 profile: ' + name + '\n' + 'dir: ' + result.dir });
586
+ }));
587
+ profile.command('current')
588
+ .description('Print active E10 profile')
589
+ .action(action((ctx) => {
590
+ const active = getActiveProfile();
591
+ ctx.output.write({ active, dir: getProfileDir(active) }, { text: active });
592
+ }));
593
+ return auth;
594
+ }