meegle-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.
- package/README.md +215 -0
- package/RELEASE.md +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2056 -0
- package/dist/core/auth-policy.d.ts +14 -0
- package/dist/core/auth-policy.js +37 -0
- package/dist/core/cli-error.d.ts +4 -0
- package/dist/core/cli-error.js +9 -0
- package/dist/core/client-factory.d.ts +3 -0
- package/dist/core/client-factory.js +22 -0
- package/dist/core/command-guard.d.ts +16 -0
- package/dist/core/command-guard.js +38 -0
- package/dist/core/config-store.d.ts +15 -0
- package/dist/core/config-store.js +69 -0
- package/dist/core/error-handler.d.ts +1 -0
- package/dist/core/error-handler.js +52 -0
- package/dist/core/json-file.d.ts +1 -0
- package/dist/core/json-file.js +12 -0
- package/dist/core/output.d.ts +1 -0
- package/dist/core/output.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -0
- package/dist/types/config.d.ts +12 -0
- package/dist/types/config.js +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
2
|
+
export type AuthPolicy = 'NONE' | 'PLUGIN_WITH_USER_KEY' | 'PLUGIN_OPTIONAL_USER_KEY' | 'USER_TOKEN_REQUIRED';
|
|
3
|
+
export interface EndpointRef {
|
|
4
|
+
method: HttpMethod;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CommandMeta {
|
|
8
|
+
id: string;
|
|
9
|
+
authPolicy: AuthPolicy;
|
|
10
|
+
endpoint?: EndpointRef;
|
|
11
|
+
}
|
|
12
|
+
export declare function isUserTokenOnlyEndpoint(path: string): boolean;
|
|
13
|
+
export declare function assertCommandSupported(meta: CommandMeta): void;
|
|
14
|
+
export declare function assertEndpointAllowed(endpoint?: EndpointRef): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CliError } from './cli-error.js';
|
|
2
|
+
const USER_TOKEN_ONLY_TEMPLATES = new Set([
|
|
3
|
+
'/open_api/user/search',
|
|
4
|
+
'/open_api/:project_key/user_group',
|
|
5
|
+
'/open_api/:project_key/user_group/members',
|
|
6
|
+
'/open_api/:project_key/user_groups/members/page',
|
|
7
|
+
]);
|
|
8
|
+
const USER_TOKEN_ONLY_PATTERNS = [
|
|
9
|
+
/^\/open_api\/user\/search$/,
|
|
10
|
+
/^\/open_api\/[^/]+\/user_group$/,
|
|
11
|
+
/^\/open_api\/[^/]+\/user_group\/members$/,
|
|
12
|
+
/^\/open_api\/[^/]+\/user_groups\/members\/page$/,
|
|
13
|
+
];
|
|
14
|
+
function normalizePath(path) {
|
|
15
|
+
const noQuery = path.split('?')[0] ?? path;
|
|
16
|
+
return noQuery.replace(/\/+$/, '');
|
|
17
|
+
}
|
|
18
|
+
export function isUserTokenOnlyEndpoint(path) {
|
|
19
|
+
const normalized = normalizePath(path);
|
|
20
|
+
if (USER_TOKEN_ONLY_TEMPLATES.has(normalized)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return USER_TOKEN_ONLY_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
24
|
+
}
|
|
25
|
+
export function assertCommandSupported(meta) {
|
|
26
|
+
if (meta.authPolicy === 'USER_TOKEN_REQUIRED') {
|
|
27
|
+
throw new CliError(`命令 "${meta.id}" 依赖 user_access_token,当前 CLI 运行在 plugin-only 模式,已拒绝执行。`, 2);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function assertEndpointAllowed(endpoint) {
|
|
31
|
+
if (!endpoint) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (isUserTokenOnlyEndpoint(endpoint.path)) {
|
|
35
|
+
throw new CliError(`接口 ${endpoint.method} ${endpoint.path} 在文档中标注为仅支持 user_access_token,plugin-only 模式已拒绝。`, 2);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MeegoClient } from 'meeglesdk';
|
|
2
|
+
const silentLogger = {
|
|
3
|
+
debug: () => { },
|
|
4
|
+
info: () => { },
|
|
5
|
+
warn: () => { },
|
|
6
|
+
error: () => { },
|
|
7
|
+
};
|
|
8
|
+
export function createClient(profile) {
|
|
9
|
+
const options = {
|
|
10
|
+
pluginId: profile.pluginId,
|
|
11
|
+
pluginSecret: profile.pluginSecret,
|
|
12
|
+
baseURL: profile.baseURL,
|
|
13
|
+
tokenType: profile.tokenType,
|
|
14
|
+
retry: {
|
|
15
|
+
maxRetries: 2,
|
|
16
|
+
retryDelay: 1000,
|
|
17
|
+
},
|
|
18
|
+
rateLimit: 'meego-openapi',
|
|
19
|
+
logger: silentLogger,
|
|
20
|
+
};
|
|
21
|
+
return new MeegoClient(options);
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AuthContext, MeegoClient } from 'meeglesdk';
|
|
2
|
+
import { type CommandMeta } from './auth-policy.js';
|
|
3
|
+
import type { CliProfile } from '../types/config.js';
|
|
4
|
+
export interface RuntimeOptions {
|
|
5
|
+
profile?: string;
|
|
6
|
+
userKey?: string;
|
|
7
|
+
compatAuth?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface GuardContext {
|
|
10
|
+
profileName: string;
|
|
11
|
+
profile: CliProfile;
|
|
12
|
+
client: MeegoClient;
|
|
13
|
+
auth: AuthContext;
|
|
14
|
+
userKey?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function runGuarded<T>(meta: CommandMeta, runtime: RuntimeOptions, action: (ctx: GuardContext) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CliError } from './cli-error.js';
|
|
2
|
+
import { assertCommandSupported, assertEndpointAllowed } from './auth-policy.js';
|
|
3
|
+
import { getProfile } from './config-store.js';
|
|
4
|
+
import { createClient } from './client-factory.js';
|
|
5
|
+
function resolveUserKey(runtimeUserKey, defaultUserKey) {
|
|
6
|
+
if (runtimeUserKey?.trim()) {
|
|
7
|
+
return runtimeUserKey.trim();
|
|
8
|
+
}
|
|
9
|
+
if (defaultUserKey?.trim()) {
|
|
10
|
+
return defaultUserKey.trim();
|
|
11
|
+
}
|
|
12
|
+
if (process.env.MEEGLE_USER_KEY?.trim()) {
|
|
13
|
+
return process.env.MEEGLE_USER_KEY.trim();
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
function buildPluginAuth(userKey, compatAuth) {
|
|
18
|
+
if (!userKey) {
|
|
19
|
+
return { type: 'plugin' };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
type: 'plugin',
|
|
23
|
+
userKey,
|
|
24
|
+
authMode: compatAuth ? 0 : 1,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function runGuarded(meta, runtime, action) {
|
|
28
|
+
assertCommandSupported(meta);
|
|
29
|
+
assertEndpointAllowed(meta.endpoint);
|
|
30
|
+
const { name: profileName, profile } = await getProfile(runtime.profile);
|
|
31
|
+
const userKey = resolveUserKey(runtime.userKey, profile.defaultUserKey);
|
|
32
|
+
if (meta.authPolicy === 'PLUGIN_WITH_USER_KEY' && !userKey) {
|
|
33
|
+
throw new CliError(`命令 "${meta.id}" 需要 userKey,请传 --user-key 或先在 auth init 配置 defaultUserKey。`, 2);
|
|
34
|
+
}
|
|
35
|
+
const client = createClient(profile);
|
|
36
|
+
const auth = buildPluginAuth(userKey, runtime.compatAuth);
|
|
37
|
+
return action({ profileName, profile, client, auth, userKey });
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CliConfig, CliProfile } from '../types/config.js';
|
|
2
|
+
export declare function getConfigFilePath(): string;
|
|
3
|
+
export declare function loadConfig(): Promise<CliConfig>;
|
|
4
|
+
export declare function saveConfig(config: CliConfig): Promise<void>;
|
|
5
|
+
export declare function upsertProfile(profileName: string, profileInput: {
|
|
6
|
+
pluginId: string;
|
|
7
|
+
pluginSecret: string;
|
|
8
|
+
baseURL?: string;
|
|
9
|
+
tokenType?: 0 | 1;
|
|
10
|
+
defaultUserKey?: string;
|
|
11
|
+
}): Promise<CliConfig>;
|
|
12
|
+
export declare function getProfile(profileName?: string): Promise<{
|
|
13
|
+
name: string;
|
|
14
|
+
profile: CliProfile;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { CliError } from './cli-error.js';
|
|
5
|
+
const DEFAULT_BASE_URL = 'https://project.feishu.cn';
|
|
6
|
+
const CONFIG_DIR = process.env.MEEGLE_CLI_CONFIG_DIR ?? path.join(os.homedir(), '.meegle-cli');
|
|
7
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
8
|
+
const EMPTY_CONFIG = {
|
|
9
|
+
version: 1,
|
|
10
|
+
activeProfile: 'default',
|
|
11
|
+
profiles: {},
|
|
12
|
+
};
|
|
13
|
+
export function getConfigFilePath() {
|
|
14
|
+
return CONFIG_FILE;
|
|
15
|
+
}
|
|
16
|
+
async function ensureConfigDir() {
|
|
17
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
18
|
+
}
|
|
19
|
+
export async function loadConfig() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(CONFIG_FILE, 'utf8');
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (parsed.version !== 1 || typeof parsed.activeProfile !== 'string' || !parsed.profiles) {
|
|
24
|
+
throw new CliError(`配置文件格式不兼容: ${CONFIG_FILE}`, 2);
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === 'ENOENT') {
|
|
30
|
+
return { ...EMPTY_CONFIG };
|
|
31
|
+
}
|
|
32
|
+
if (error instanceof CliError) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
throw new CliError(`读取配置失败: ${CONFIG_FILE}`, 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function saveConfig(config) {
|
|
39
|
+
await ensureConfigDir();
|
|
40
|
+
const content = `${JSON.stringify(config, null, 2)}\n`;
|
|
41
|
+
await writeFile(CONFIG_FILE, content, { encoding: 'utf8', mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
export async function upsertProfile(profileName, profileInput) {
|
|
44
|
+
const config = await loadConfig();
|
|
45
|
+
const normalizedName = profileName.trim();
|
|
46
|
+
if (!normalizedName) {
|
|
47
|
+
throw new CliError('profile 名称不能为空', 2);
|
|
48
|
+
}
|
|
49
|
+
const profile = {
|
|
50
|
+
pluginId: profileInput.pluginId,
|
|
51
|
+
pluginSecret: profileInput.pluginSecret,
|
|
52
|
+
baseURL: profileInput.baseURL ?? DEFAULT_BASE_URL,
|
|
53
|
+
tokenType: profileInput.tokenType ?? 0,
|
|
54
|
+
defaultUserKey: profileInput.defaultUserKey,
|
|
55
|
+
};
|
|
56
|
+
config.profiles[normalizedName] = profile;
|
|
57
|
+
config.activeProfile = normalizedName;
|
|
58
|
+
await saveConfig(config);
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
61
|
+
export async function getProfile(profileName) {
|
|
62
|
+
const config = await loadConfig();
|
|
63
|
+
const name = profileName?.trim() || config.activeProfile;
|
|
64
|
+
const profile = config.profiles[name];
|
|
65
|
+
if (!profile) {
|
|
66
|
+
throw new CliError(`未找到 profile "${name}",请先执行 meegle auth init --target-profile ${name} ...`, 2);
|
|
67
|
+
}
|
|
68
|
+
return { name, profile };
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleCliError(error: unknown): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CommanderError } from 'commander';
|
|
2
|
+
import { ErrorCodes, MeegoError } from 'meeglesdk';
|
|
3
|
+
import { CliError } from './cli-error.js';
|
|
4
|
+
const TOKEN_TYPE_WRONG = 1000052755;
|
|
5
|
+
function print(message) {
|
|
6
|
+
process.stderr.write(`${message}\n`);
|
|
7
|
+
}
|
|
8
|
+
function handleMeegoError(error) {
|
|
9
|
+
switch (error.errCode) {
|
|
10
|
+
case TOKEN_TYPE_WRONG:
|
|
11
|
+
print('接口仅支持 user_access_token;当前 CLI 为 plugin-only 模式,命令已拒绝。');
|
|
12
|
+
return 2;
|
|
13
|
+
case ErrorCodes.PLUGIN_TOKEN_MUST_HAVE_USER_KEY:
|
|
14
|
+
print('缺少 userKey。请传 --user-key 或在 auth init 中配置 defaultUserKey。');
|
|
15
|
+
return 2;
|
|
16
|
+
case ErrorCodes.X_USER_KEY_WRONG:
|
|
17
|
+
print('X-User-Key 无效。请检查 --user-key 是否正确。');
|
|
18
|
+
return 2;
|
|
19
|
+
case ErrorCodes.CHECK_TOKEN_FAILED:
|
|
20
|
+
case ErrorCodes.TOKEN_NOT_EXIST:
|
|
21
|
+
case ErrorCodes.CHECK_TOKEN_PERM_FAILED:
|
|
22
|
+
print(`鉴权失败(${error.errCode}):${error.errMsg}`);
|
|
23
|
+
return 3;
|
|
24
|
+
default:
|
|
25
|
+
print(`API 错误(${error.errCode}):${error.errMsg}`);
|
|
26
|
+
if (error.logId) {
|
|
27
|
+
print(`log_id: ${error.logId}`);
|
|
28
|
+
}
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function handleCliError(error) {
|
|
33
|
+
if (error instanceof CommanderError) {
|
|
34
|
+
if (error.code !== 'commander.helpDisplayed') {
|
|
35
|
+
print(error.message);
|
|
36
|
+
}
|
|
37
|
+
return error.exitCode;
|
|
38
|
+
}
|
|
39
|
+
if (error instanceof CliError) {
|
|
40
|
+
print(error.message);
|
|
41
|
+
return error.exitCode;
|
|
42
|
+
}
|
|
43
|
+
if (error instanceof MeegoError) {
|
|
44
|
+
return handleMeegoError(error);
|
|
45
|
+
}
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
print(error.message);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
print('未知错误');
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readJsonFile<T>(filePath: string): Promise<T>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { CliError } from './cli-error.js';
|
|
3
|
+
export async function readJsonFile(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = await readFile(filePath, 'utf8');
|
|
6
|
+
return JSON.parse(raw);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
const message = error instanceof Error ? error.message : 'unknown error';
|
|
10
|
+
throw new CliError(`读取 JSON 文件失败: ${filePath} (${message})`, 2);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function printData(data: unknown, asJson: boolean): void;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildCli } from './cli.js';
|
|
3
|
+
import { handleCliError } from './core/error-handler.js';
|
|
4
|
+
async function main() {
|
|
5
|
+
const program = buildCli();
|
|
6
|
+
program.showHelpAfterError();
|
|
7
|
+
program.exitOverride();
|
|
8
|
+
try {
|
|
9
|
+
await program.parseAsync(process.argv);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
process.exitCode = handleCliError(error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
await main();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CliProfile {
|
|
2
|
+
pluginId: string;
|
|
3
|
+
pluginSecret: string;
|
|
4
|
+
baseURL: string;
|
|
5
|
+
tokenType: 0 | 1;
|
|
6
|
+
defaultUserKey?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CliConfig {
|
|
9
|
+
version: 1;
|
|
10
|
+
activeProfile: string;
|
|
11
|
+
profiles: Record<string, CliProfile>;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "meegle-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Plugin-only CLI for Feishu Project (Meegle) based on meeglesdk",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"meegle": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"RELEASE.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"clean": "rm -rf dist",
|
|
16
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
17
|
+
"build:all": "npm run clean && tsc -p tsconfig.json",
|
|
18
|
+
"dev": "tsc -w -p tsconfig.json",
|
|
19
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
20
|
+
"test": "npm run build:all && node --test dist/tests/auth-policy.test.js dist/tests/e2e-cli.test.js",
|
|
21
|
+
"test:smoke:real": "npm run build:all && node --test dist/tests/smoke-real.test.js",
|
|
22
|
+
"release:check": "npm test && npm run build && npm pack --dry-run",
|
|
23
|
+
"prepublishOnly": "npm run release:check"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"meegle",
|
|
27
|
+
"feishu",
|
|
28
|
+
"lark",
|
|
29
|
+
"project",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"meeglesdk": "0.1.7"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.15.21",
|
|
38
|
+
"typescript": "^5.8.3"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|