@aswless_854771076/ai_short_studio_cli 0.1.0-beta.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 +56 -0
- package/bin/run.js +5 -0
- package/dist/auth/credential-store.d.ts +6 -0
- package/dist/auth/credential-store.js +27 -0
- package/dist/auth/google-oauth.d.ts +2 -0
- package/dist/auth/google-oauth.js +72 -0
- package/dist/auth/session-manager.d.ts +44 -0
- package/dist/auth/session-manager.js +100 -0
- package/dist/base-command.d.ts +11 -0
- package/dist/base-command.js +39 -0
- package/dist/client/api-client.d.ts +23 -0
- package/dist/client/api-client.js +64 -0
- package/dist/client/errors.d.ts +14 -0
- package/dist/client/errors.js +34 -0
- package/dist/command-helpers.d.ts +10 -0
- package/dist/command-helpers.js +24 -0
- package/dist/commands/auth/login.d.ts +14 -0
- package/dist/commands/auth/login.js +32 -0
- package/dist/commands/auth/logout.d.ts +11 -0
- package/dist/commands/auth/logout.js +12 -0
- package/dist/commands/auth/status.d.ts +11 -0
- package/dist/commands/auth/status.js +12 -0
- package/dist/commands/canvas/apply.d.ts +12 -0
- package/dist/commands/canvas/apply.js +7 -0
- package/dist/commands/canvas/asset/download.d.ts +16 -0
- package/dist/commands/canvas/asset/download.js +34 -0
- package/dist/commands/canvas/asset/get.d.ts +14 -0
- package/dist/commands/canvas/asset/get.js +12 -0
- package/dist/commands/canvas/asset/list.d.ts +13 -0
- package/dist/commands/canvas/asset/list.js +7 -0
- package/dist/commands/canvas/asset/select.d.ts +15 -0
- package/dist/commands/canvas/asset/select.js +8 -0
- package/dist/commands/canvas/asset/upload.d.ts +14 -0
- package/dist/commands/canvas/asset/upload.js +10 -0
- package/dist/commands/canvas/edge/connect.d.ts +13 -0
- package/dist/commands/canvas/edge/connect.js +9 -0
- package/dist/commands/canvas/edge/disconnect.d.ts +15 -0
- package/dist/commands/canvas/edge/disconnect.js +8 -0
- package/dist/commands/canvas/edge/list.d.ts +11 -0
- package/dist/commands/canvas/edge/list.js +10 -0
- package/dist/commands/canvas/get.d.ts +11 -0
- package/dist/commands/canvas/get.js +6 -0
- package/dist/commands/canvas/node/add.d.ts +18 -0
- package/dist/commands/canvas/node/add.js +8 -0
- package/dist/commands/canvas/node/delete.d.ts +15 -0
- package/dist/commands/canvas/node/delete.js +8 -0
- package/dist/commands/canvas/node/get.d.ts +14 -0
- package/dist/commands/canvas/node/get.js +12 -0
- package/dist/commands/canvas/node/list.d.ts +11 -0
- package/dist/commands/canvas/node/list.js +10 -0
- package/dist/commands/canvas/node/run.d.ts +16 -0
- package/dist/commands/canvas/node/run.js +11 -0
- package/dist/commands/canvas/node/types.d.ts +13 -0
- package/dist/commands/canvas/node/types.js +8 -0
- package/dist/commands/canvas/node/update.d.ts +15 -0
- package/dist/commands/canvas/node/update.js +8 -0
- package/dist/commands/config/get.d.ts +10 -0
- package/dist/commands/config/get.js +6 -0
- package/dist/commands/config/set.d.ts +11 -0
- package/dist/commands/config/set.js +7 -0
- package/dist/commands/project/create.d.ts +12 -0
- package/dist/commands/project/create.js +7 -0
- package/dist/commands/project/delete.d.ts +14 -0
- package/dist/commands/project/delete.js +8 -0
- package/dist/commands/project/get.d.ts +13 -0
- package/dist/commands/project/get.js +8 -0
- package/dist/commands/project/list.d.ts +14 -0
- package/dist/commands/project/list.js +13 -0
- package/dist/commands/project/update.d.ts +15 -0
- package/dist/commands/project/update.js +9 -0
- package/dist/commands/skill/install.d.ts +13 -0
- package/dist/commands/skill/install.js +8 -0
- package/dist/commands/skill/path.d.ts +10 -0
- package/dist/commands/skill/path.js +6 -0
- package/dist/commands/skill/status.d.ts +12 -0
- package/dist/commands/skill/status.js +7 -0
- package/dist/commands/skill/update.d.ts +1 -0
- package/dist/commands/skill/update.js +1 -0
- package/dist/commands/task/cancel.d.ts +14 -0
- package/dist/commands/task/cancel.js +8 -0
- package/dist/commands/task/get.d.ts +13 -0
- package/dist/commands/task/get.js +8 -0
- package/dist/commands/task/wait.d.ts +14 -0
- package/dist/commands/task/wait.js +9 -0
- package/dist/config/profiles.d.ts +15 -0
- package/dist/config/profiles.js +47 -0
- package/dist/domain/vvicat-api.d.ts +84 -0
- package/dist/domain/vvicat-api.js +144 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/output/formatter.d.ts +1 -0
- package/dist/output/formatter.js +3 -0
- package/dist/runtime.d.ts +14 -0
- package/dist/runtime.js +45 -0
- package/dist/skill/command-flags.d.ts +8 -0
- package/dist/skill/command-flags.js +9 -0
- package/dist/skill/skill-manager.d.ts +24 -0
- package/dist/skill/skill-manager.js +100 -0
- package/dist/terminal/prompt.d.ts +2 -0
- package/dist/terminal/prompt.js +49 -0
- package/package.json +55 -0
- package/skills/using-vvicat-cli/SKILL.md +49 -0
- package/skills/using-vvicat-cli/agents/openai.yaml +4 -0
- package/skills/using-vvicat-cli/references/commands.md +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @vvicat/ai_short_studio_cli
|
|
2
|
+
|
|
3
|
+
VVICAT 新链路无限画布的官方命令行客户端。要求 Node.js 22 或更高版本。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install --global @vvicat/ai_short_studio_cli
|
|
7
|
+
vvicat auth login --google
|
|
8
|
+
vvicat project list --json
|
|
9
|
+
vvicat canvas node types --json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
邮箱登录从 stdin 读取密码:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
vvicat auth login --email user@example.com --password-stdin
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
CLI 只把 refresh token 保存到系统钥匙串,access token 仅驻留内存并在到期前自动刷新。服务地址和非敏感 profile 写入系统配置目录;可使用 `--profile`、`--base-url` 或 `VVICAT_BASE_URL` 切换环境。
|
|
19
|
+
|
|
20
|
+
## 命令范围
|
|
21
|
+
|
|
22
|
+
- `vvicat project`:无限画布项目增删改查。
|
|
23
|
+
- `vvicat canvas get|apply`:读取或批量更新画布。
|
|
24
|
+
- `vvicat canvas node`:节点类型/schema 查询、节点读写、执行。
|
|
25
|
+
- `vvicat canvas edge`:连线读写。
|
|
26
|
+
- `vvicat canvas asset`:资产列表、上传、版本选择和下载。
|
|
27
|
+
- `vvicat task`:异步任务读取、等待和取消。
|
|
28
|
+
- `vvicat config`:模型、provider、默认模型、能力默认值和并发配置。
|
|
29
|
+
|
|
30
|
+
运行 `vvicat <topic> --help` 查看精确参数。所有结构化响应保持 JSON;API 错误也返回稳定的 `error.code`、`status`、`retryable` 和 `details`。
|
|
31
|
+
|
|
32
|
+
退出码:`0` 成功,`3` 未认证,`4` 不存在,`5` 禁止或冲突,`6` 参数错误,`7` 其他错误。
|
|
33
|
+
|
|
34
|
+
配套 Agent Skill 位于 `skills/using-vvicat-cli`,随 npm 包发布。
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
vvicat skill install --target codex
|
|
38
|
+
vvicat skill status --target codex
|
|
39
|
+
vvicat skill update --target codex
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
也可使用 `--target agents` 或 `--directory <path>`。CLI 会记录版本和 SHA-256;已安装 Skill 被用户修改后,更新默认停止,只有显式 `--force` 才覆盖。
|
|
43
|
+
|
|
44
|
+
## 发布
|
|
45
|
+
|
|
46
|
+
使用具有发布权限并允许 bypass 2FA 的 npm Granular Access Token:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export NODE_AUTH_TOKEN="<token>"
|
|
50
|
+
npm run release:dry-run
|
|
51
|
+
npm run release:test -- --version 0.1.0-beta.0
|
|
52
|
+
# 加入 @vvicat organization 后发布正式包
|
|
53
|
+
npm run release
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Token 只从环境变量读取,不要放入参数、Git 或日志。
|
package/bin/run.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CredentialStore } from './session-manager.js';
|
|
2
|
+
export declare class KeyringCredentialStore implements CredentialStore {
|
|
3
|
+
delete(profile: string): Promise<void>;
|
|
4
|
+
get(profile: string): Promise<string | null>;
|
|
5
|
+
set(profile: string, refreshToken: string): Promise<void>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AsyncEntry } from '@napi-rs/keyring';
|
|
2
|
+
const SERVICE = 'vvicat-ai-short-studio-cli';
|
|
3
|
+
export class KeyringCredentialStore {
|
|
4
|
+
async delete(profile) {
|
|
5
|
+
await new AsyncEntry(SERVICE, profile).deleteCredential().catch((error) => {
|
|
6
|
+
if (!isMissingCredential(error))
|
|
7
|
+
throw error;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
async get(profile) {
|
|
11
|
+
try {
|
|
12
|
+
return await new AsyncEntry(SERVICE, profile).getPassword() ?? null;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isMissingCredential(error))
|
|
16
|
+
return null;
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async set(profile, refreshToken) {
|
|
21
|
+
await new AsyncEntry(SERVICE, profile).setPassword(refreshToken);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isMissingCredential(error) {
|
|
25
|
+
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
|
26
|
+
return message.includes('no entry') || message.includes('not found') || message.includes('could not be found');
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
export async function loginWithGooglePkce(client) {
|
|
4
|
+
const callback = createCallbackServer();
|
|
5
|
+
const { redirectUrl, waitForCode, close } = await callback.start();
|
|
6
|
+
try {
|
|
7
|
+
const { data, error } = await client.auth.signInWithOAuth({
|
|
8
|
+
provider: 'google',
|
|
9
|
+
options: { redirectTo: redirectUrl, skipBrowserRedirect: true },
|
|
10
|
+
});
|
|
11
|
+
if (error || !data.url)
|
|
12
|
+
throw error ?? new Error('Google 登录未返回授权地址');
|
|
13
|
+
openBrowser(data.url);
|
|
14
|
+
const code = await waitForCode();
|
|
15
|
+
const result = await client.auth.exchangeCodeForSession(code);
|
|
16
|
+
if (result.error || !result.data.session)
|
|
17
|
+
throw result.error ?? new Error('Google 登录未返回会话');
|
|
18
|
+
return result.data.session;
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function createCallbackServer() {
|
|
25
|
+
const server = createServer();
|
|
26
|
+
let redirectUrl = '';
|
|
27
|
+
let resolveCode = null;
|
|
28
|
+
let rejectCode = null;
|
|
29
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
30
|
+
resolveCode = resolve;
|
|
31
|
+
rejectCode = reject;
|
|
32
|
+
});
|
|
33
|
+
server.on('request', (request, response) => {
|
|
34
|
+
const url = new URL(request.url || '/', redirectUrl);
|
|
35
|
+
const code = url.searchParams.get('code');
|
|
36
|
+
const error = url.searchParams.get('error_description') || url.searchParams.get('error');
|
|
37
|
+
response.writeHead(code ? 200 : 400, { 'content-type': 'text/html; charset=utf-8' });
|
|
38
|
+
response.end(code ? '<h1>VVICAT 登录成功,可以关闭此窗口。</h1>' : '<h1>VVICAT 登录失败,请返回终端查看。</h1>');
|
|
39
|
+
if (code)
|
|
40
|
+
resolveCode?.(code);
|
|
41
|
+
else
|
|
42
|
+
rejectCode?.(new Error(error || 'Google 登录回调缺少 code'));
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
async start() {
|
|
46
|
+
await new Promise((resolve, reject) => {
|
|
47
|
+
server.once('error', reject);
|
|
48
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
49
|
+
});
|
|
50
|
+
const address = server.address();
|
|
51
|
+
if (!address || typeof address === 'string')
|
|
52
|
+
throw new Error('无法启动 Google 登录回调');
|
|
53
|
+
redirectUrl = `http://127.0.0.1:${address.port}/callback`;
|
|
54
|
+
return {
|
|
55
|
+
redirectUrl,
|
|
56
|
+
waitForCode: () => withTimeout(codePromise, 300_000),
|
|
57
|
+
close: () => new Promise((resolve) => server.close(() => resolve())),
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function openBrowser(url) {
|
|
63
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
64
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
65
|
+
spawn(command, args, { detached: true, stdio: 'ignore' }).unref();
|
|
66
|
+
}
|
|
67
|
+
function withTimeout(promise, milliseconds) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const timer = setTimeout(() => reject(new Error('Google 登录等待超时')), milliseconds);
|
|
70
|
+
promise.then((value) => { clearTimeout(timer); resolve(value); }, (error) => { clearTimeout(timer); reject(error); });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface SessionTokens {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken: string;
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CredentialStore {
|
|
7
|
+
delete(profile: string): Promise<void>;
|
|
8
|
+
get(profile: string): Promise<string | null>;
|
|
9
|
+
set(profile: string, refreshToken: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface AuthProvider {
|
|
12
|
+
login(email: string, password: string): Promise<SessionTokens>;
|
|
13
|
+
loginWithGoogle(): Promise<SessionTokens>;
|
|
14
|
+
logout(accessToken: string): Promise<void>;
|
|
15
|
+
refresh(refreshToken: string): Promise<SessionTokens>;
|
|
16
|
+
}
|
|
17
|
+
export declare class SupabaseAuthProvider implements AuthProvider {
|
|
18
|
+
private readonly client;
|
|
19
|
+
constructor(url: string, publishableKey: string);
|
|
20
|
+
login(email: string, password: string): Promise<SessionTokens>;
|
|
21
|
+
logout(_accessToken: string): Promise<void>;
|
|
22
|
+
loginWithGoogle(): Promise<SessionTokens>;
|
|
23
|
+
refresh(refreshToken: string): Promise<SessionTokens>;
|
|
24
|
+
}
|
|
25
|
+
export declare class SessionManager {
|
|
26
|
+
private current;
|
|
27
|
+
private refreshPromise;
|
|
28
|
+
private readonly credentials;
|
|
29
|
+
private readonly environment;
|
|
30
|
+
private readonly profile;
|
|
31
|
+
private readonly provider;
|
|
32
|
+
constructor(input: {
|
|
33
|
+
profile: string;
|
|
34
|
+
credentials: CredentialStore;
|
|
35
|
+
provider: AuthProvider;
|
|
36
|
+
environment?: NodeJS.ProcessEnv;
|
|
37
|
+
});
|
|
38
|
+
accessToken(): Promise<string>;
|
|
39
|
+
login(email: string, password: string): Promise<void>;
|
|
40
|
+
loginWithGoogle(): Promise<void>;
|
|
41
|
+
refreshAccessToken(): Promise<string>;
|
|
42
|
+
logout(): Promise<void>;
|
|
43
|
+
private refresh;
|
|
44
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import { loginWithGooglePkce } from './google-oauth.js';
|
|
3
|
+
export class SupabaseAuthProvider {
|
|
4
|
+
client;
|
|
5
|
+
constructor(url, publishableKey) {
|
|
6
|
+
this.client = createClient(url, publishableKey, {
|
|
7
|
+
auth: { autoRefreshToken: false, persistSession: false, flowType: 'pkce' },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
async login(email, password) {
|
|
11
|
+
const { data, error } = await this.client.auth.signInWithPassword({ email, password });
|
|
12
|
+
if (error || !data.session)
|
|
13
|
+
throw error ?? new Error('登录响应未包含会话');
|
|
14
|
+
return sessionTokens(data.session);
|
|
15
|
+
}
|
|
16
|
+
async logout(_accessToken) {
|
|
17
|
+
const { error } = await this.client.auth.signOut();
|
|
18
|
+
if (error)
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
async loginWithGoogle() {
|
|
22
|
+
return sessionTokens(await loginWithGooglePkce(this.client));
|
|
23
|
+
}
|
|
24
|
+
async refresh(refreshToken) {
|
|
25
|
+
const { data, error } = await this.client.auth.refreshSession({ refresh_token: refreshToken });
|
|
26
|
+
if (error || !data.session)
|
|
27
|
+
throw error ?? new Error('刷新响应未包含会话');
|
|
28
|
+
return sessionTokens(data.session);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function sessionTokens(session) {
|
|
32
|
+
if (!session.refresh_token || !session.expires_at)
|
|
33
|
+
throw new Error('认证会话字段不完整');
|
|
34
|
+
return {
|
|
35
|
+
accessToken: session.access_token,
|
|
36
|
+
refreshToken: session.refresh_token,
|
|
37
|
+
expiresAt: session.expires_at * 1000,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export class SessionManager {
|
|
41
|
+
current = null;
|
|
42
|
+
refreshPromise = null;
|
|
43
|
+
credentials;
|
|
44
|
+
environment;
|
|
45
|
+
profile;
|
|
46
|
+
provider;
|
|
47
|
+
constructor(input) {
|
|
48
|
+
this.profile = input.profile;
|
|
49
|
+
this.credentials = input.credentials;
|
|
50
|
+
this.provider = input.provider;
|
|
51
|
+
this.environment = input.environment ?? process.env;
|
|
52
|
+
}
|
|
53
|
+
async accessToken() {
|
|
54
|
+
if (this.environment.VVICAT_ACCESS_TOKEN)
|
|
55
|
+
return this.environment.VVICAT_ACCESS_TOKEN;
|
|
56
|
+
if (this.current && this.current.expiresAt > Date.now() + 60_000)
|
|
57
|
+
return this.current.accessToken;
|
|
58
|
+
if (!this.refreshPromise) {
|
|
59
|
+
this.refreshPromise = this.refresh().finally(() => {
|
|
60
|
+
this.refreshPromise = null;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return this.refreshPromise;
|
|
64
|
+
}
|
|
65
|
+
async login(email, password) {
|
|
66
|
+
this.current = await this.provider.login(email, password);
|
|
67
|
+
await this.credentials.set(this.profile, this.current.refreshToken);
|
|
68
|
+
}
|
|
69
|
+
async loginWithGoogle() {
|
|
70
|
+
this.current = await this.provider.loginWithGoogle();
|
|
71
|
+
await this.credentials.set(this.profile, this.current.refreshToken);
|
|
72
|
+
}
|
|
73
|
+
async refreshAccessToken() {
|
|
74
|
+
if (this.environment.VVICAT_ACCESS_TOKEN)
|
|
75
|
+
return this.environment.VVICAT_ACCESS_TOKEN;
|
|
76
|
+
if (!this.refreshPromise) {
|
|
77
|
+
this.refreshPromise = this.refresh().finally(() => {
|
|
78
|
+
this.refreshPromise = null;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return this.refreshPromise;
|
|
82
|
+
}
|
|
83
|
+
async logout() {
|
|
84
|
+
if (!this.environment.VVICAT_ACCESS_TOKEN) {
|
|
85
|
+
const accessToken = await this.accessToken().catch(() => null);
|
|
86
|
+
if (accessToken)
|
|
87
|
+
await this.provider.logout(accessToken);
|
|
88
|
+
await this.credentials.delete(this.profile);
|
|
89
|
+
}
|
|
90
|
+
this.current = null;
|
|
91
|
+
}
|
|
92
|
+
async refresh() {
|
|
93
|
+
const refreshToken = await this.credentials.get(this.profile);
|
|
94
|
+
if (!refreshToken)
|
|
95
|
+
throw new Error('尚未登录,请先运行 vvicat auth login');
|
|
96
|
+
this.current = await this.provider.refresh(refreshToken);
|
|
97
|
+
await this.credentials.set(this.profile, this.current.refreshToken);
|
|
98
|
+
return this.current.accessToken;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export declare abstract class BaseCommand extends Command {
|
|
3
|
+
static baseFlags: {
|
|
4
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
5
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
protected print(value: unknown): void;
|
|
10
|
+
catch(error: unknown): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { CliApiError } from './client/errors.js';
|
|
3
|
+
import { formatJson } from './output/formatter.js';
|
|
4
|
+
export class BaseCommand extends Command {
|
|
5
|
+
static baseFlags = {
|
|
6
|
+
'base-url': Flags.string({ description: 'VVICAT 服务地址', env: 'VVICAT_BASE_URL' }),
|
|
7
|
+
json: Flags.boolean({ description: '输出稳定 JSON', default: false }),
|
|
8
|
+
locale: Flags.string({ description: '响应语言', default: 'zh', options: ['zh', 'en'] }),
|
|
9
|
+
profile: Flags.string({ description: '配置档案名称' }),
|
|
10
|
+
};
|
|
11
|
+
print(value) {
|
|
12
|
+
this.log(formatJson(value));
|
|
13
|
+
}
|
|
14
|
+
async catch(error) {
|
|
15
|
+
if (error instanceof CliApiError) {
|
|
16
|
+
this.error(formatJson({
|
|
17
|
+
error: {
|
|
18
|
+
code: error.code,
|
|
19
|
+
message: error.message,
|
|
20
|
+
retryable: error.retryable,
|
|
21
|
+
status: error.status,
|
|
22
|
+
details: error.details,
|
|
23
|
+
},
|
|
24
|
+
}), { exit: exitCode(error.status) });
|
|
25
|
+
}
|
|
26
|
+
this.error(error instanceof Error ? error.message : String(error), { exit: 7 });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function exitCode(status) {
|
|
30
|
+
if (status === 401)
|
|
31
|
+
return 3;
|
|
32
|
+
if (status === 403 || status === 409)
|
|
33
|
+
return 5;
|
|
34
|
+
if (status === 404)
|
|
35
|
+
return 4;
|
|
36
|
+
if (status === 400 || status === 422)
|
|
37
|
+
return 6;
|
|
38
|
+
return 7;
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ApiSession {
|
|
2
|
+
accessToken(): Promise<string>;
|
|
3
|
+
refreshAccessToken(): Promise<string>;
|
|
4
|
+
}
|
|
5
|
+
export interface ApiRequestOptions extends Omit<RequestInit, 'body'> {
|
|
6
|
+
auth?: boolean;
|
|
7
|
+
body?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export declare class ApiClient {
|
|
10
|
+
private readonly baseUrl;
|
|
11
|
+
private readonly fetcher;
|
|
12
|
+
private readonly locale;
|
|
13
|
+
private readonly session;
|
|
14
|
+
constructor(input: {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
session: ApiSession;
|
|
17
|
+
fetcher?: typeof fetch;
|
|
18
|
+
locale?: string;
|
|
19
|
+
});
|
|
20
|
+
request<T = unknown>(path: string, options?: ApiRequestOptions): Promise<T>;
|
|
21
|
+
response(path: string, options?: ApiRequestOptions): Promise<Response>;
|
|
22
|
+
private send;
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { apiError } from './errors.js';
|
|
2
|
+
export class ApiClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
fetcher;
|
|
5
|
+
locale;
|
|
6
|
+
session;
|
|
7
|
+
constructor(input) {
|
|
8
|
+
this.baseUrl = input.baseUrl.replace(/\/+$/, '');
|
|
9
|
+
this.session = input.session;
|
|
10
|
+
this.fetcher = input.fetcher ?? fetch;
|
|
11
|
+
this.locale = input.locale ?? 'zh';
|
|
12
|
+
}
|
|
13
|
+
async request(path, options = {}) {
|
|
14
|
+
const firstToken = options.auth === false ? null : await this.session.accessToken();
|
|
15
|
+
let response = await this.send(path, options, firstToken);
|
|
16
|
+
if (response.status === 401 && options.auth !== false) {
|
|
17
|
+
response = await this.send(path, options, await this.session.refreshAccessToken());
|
|
18
|
+
}
|
|
19
|
+
if (!response.ok)
|
|
20
|
+
throw await apiError(response);
|
|
21
|
+
if (response.status === 204)
|
|
22
|
+
return undefined;
|
|
23
|
+
return await response.json();
|
|
24
|
+
}
|
|
25
|
+
async response(path, options = {}) {
|
|
26
|
+
const firstToken = options.auth === false ? null : await this.session.accessToken();
|
|
27
|
+
let response = await this.send(path, options, firstToken);
|
|
28
|
+
if (response.status === 401 && options.auth !== false) {
|
|
29
|
+
response = await this.send(path, options, await this.session.refreshAccessToken());
|
|
30
|
+
}
|
|
31
|
+
if (!response.ok)
|
|
32
|
+
throw await apiError(response);
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
async send(path, options, accessToken) {
|
|
36
|
+
const { auth: _auth, body: inputBody, ...requestInit } = options;
|
|
37
|
+
const headers = new Headers(requestInit.headers);
|
|
38
|
+
headers.set('accept', 'application/json');
|
|
39
|
+
headers.set('accept-language', this.locale);
|
|
40
|
+
if (accessToken)
|
|
41
|
+
headers.set('authorization', `Bearer ${accessToken}`);
|
|
42
|
+
let body = inputBody;
|
|
43
|
+
if (inputBody !== undefined && isJsonBody(inputBody)) {
|
|
44
|
+
headers.set('content-type', 'application/json');
|
|
45
|
+
body = JSON.stringify(inputBody);
|
|
46
|
+
}
|
|
47
|
+
const url = /^https?:\/\//i.test(path)
|
|
48
|
+
? path
|
|
49
|
+
: `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
|
50
|
+
return this.fetcher(url, {
|
|
51
|
+
...requestInit,
|
|
52
|
+
headers,
|
|
53
|
+
body,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function isJsonBody(value) {
|
|
58
|
+
return value !== null
|
|
59
|
+
&& typeof value === 'object'
|
|
60
|
+
&& !(value instanceof ArrayBuffer)
|
|
61
|
+
&& !(value instanceof Blob)
|
|
62
|
+
&& !(value instanceof FormData)
|
|
63
|
+
&& !(value instanceof URLSearchParams);
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class CliApiError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly details: Record<string, unknown>;
|
|
4
|
+
readonly retryable: boolean;
|
|
5
|
+
readonly status: number;
|
|
6
|
+
constructor(input: {
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
status: number;
|
|
10
|
+
retryable?: boolean;
|
|
11
|
+
details?: Record<string, unknown>;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export declare function apiError(response: Response): Promise<CliApiError>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class CliApiError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
retryable;
|
|
5
|
+
status;
|
|
6
|
+
constructor(input) {
|
|
7
|
+
super(input.message);
|
|
8
|
+
this.name = 'CliApiError';
|
|
9
|
+
this.code = input.code;
|
|
10
|
+
this.status = input.status;
|
|
11
|
+
this.retryable = input.retryable ?? false;
|
|
12
|
+
this.details = input.details ?? {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function apiError(response) {
|
|
16
|
+
const payload = await response.json().catch(() => null);
|
|
17
|
+
const nested = payload?.error && typeof payload.error === 'object' && !Array.isArray(payload.error)
|
|
18
|
+
? payload.error
|
|
19
|
+
: null;
|
|
20
|
+
const code = readString(nested?.code) || readString(payload?.code) || `HTTP_${response.status}`;
|
|
21
|
+
const message = readString(nested?.message) || readString(payload?.message) || response.statusText || 'API 请求失败';
|
|
22
|
+
return new CliApiError({
|
|
23
|
+
code,
|
|
24
|
+
message,
|
|
25
|
+
status: response.status,
|
|
26
|
+
retryable: nested?.retryable === true,
|
|
27
|
+
details: nested?.details && typeof nested.details === 'object' && !Array.isArray(nested.details)
|
|
28
|
+
? nested.details
|
|
29
|
+
: {},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function readString(value) {
|
|
33
|
+
return typeof value === 'string' ? value : '';
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createRuntime } from './runtime.js';
|
|
2
|
+
import { VvicatApi } from './domain/vvicat-api.js';
|
|
3
|
+
export declare const projectFlag: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
4
|
+
export declare function commandApi(flags: Parameters<typeof createRuntime>[0]): Promise<{
|
|
5
|
+
api: VvicatApi;
|
|
6
|
+
profile: import("./config/profiles.js").CliProfile;
|
|
7
|
+
session: import("./auth/session-manager.js").SessionManager;
|
|
8
|
+
client: import("./client/api-client.js").ApiClient;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function readJsonInput(file?: string): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { createRuntime } from './runtime.js';
|
|
4
|
+
import { VvicatApi } from './domain/vvicat-api.js';
|
|
5
|
+
export const projectFlag = Flags.string({ description: '无限画布项目 ID', required: true });
|
|
6
|
+
export async function commandApi(flags) {
|
|
7
|
+
const runtime = await createRuntime(flags);
|
|
8
|
+
return { ...runtime, api: new VvicatApi(runtime.client) };
|
|
9
|
+
}
|
|
10
|
+
export async function readJsonInput(file) {
|
|
11
|
+
const source = file ? await readFile(file, 'utf8') : await readStdin();
|
|
12
|
+
const value = JSON.parse(source);
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
14
|
+
throw new Error('输入必须是 JSON 对象');
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
async function readStdin() {
|
|
18
|
+
if (process.stdin.isTTY)
|
|
19
|
+
throw new Error('请使用 --file 或通过 stdin 传入 JSON');
|
|
20
|
+
const chunks = [];
|
|
21
|
+
for await (const chunk of process.stdin)
|
|
22
|
+
chunks.push(Buffer.from(chunk));
|
|
23
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class AuthLogin extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
email: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
google: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
'password-stdin': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base-command.js';
|
|
3
|
+
import { createRuntime } from '../../runtime.js';
|
|
4
|
+
import { promptLine, promptPassword } from '../../terminal/prompt.js';
|
|
5
|
+
export default class AuthLogin extends BaseCommand {
|
|
6
|
+
static description = '登录并将 refresh token 保存到系统钥匙串';
|
|
7
|
+
static flags = {
|
|
8
|
+
...BaseCommand.baseFlags,
|
|
9
|
+
email: Flags.string({ description: '登录邮箱' }),
|
|
10
|
+
google: Flags.boolean({ description: '通过浏览器使用 Google 登录', default: false }),
|
|
11
|
+
'password-stdin': Flags.boolean({ description: '从标准输入读取密码', default: false }),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(AuthLogin);
|
|
15
|
+
const runtime = await createRuntime(flags);
|
|
16
|
+
if (flags.google) {
|
|
17
|
+
await runtime.session.loginWithGoogle();
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const email = flags.email || await promptLine('邮箱: ');
|
|
21
|
+
const password = flags['password-stdin'] ? await readStdin() : await promptPassword('密码: ');
|
|
22
|
+
await runtime.session.login(email, password.trimEnd());
|
|
23
|
+
}
|
|
24
|
+
this.print({ authenticated: true, profile: runtime.profile.name });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readStdin() {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
for await (const chunk of process.stdin)
|
|
30
|
+
chunks.push(Buffer.from(chunk));
|
|
31
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class AuthLogout extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
import { createRuntime } from '../../runtime.js';
|
|
3
|
+
export default class AuthLogout extends BaseCommand {
|
|
4
|
+
static description = '注销并清除本机 refresh token';
|
|
5
|
+
static flags = BaseCommand.baseFlags;
|
|
6
|
+
async run() {
|
|
7
|
+
const { flags } = await this.parse(AuthLogout);
|
|
8
|
+
const runtime = await createRuntime(flags);
|
|
9
|
+
await runtime.session.logout();
|
|
10
|
+
this.print({ authenticated: false, profile: runtime.profile.name });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
export default class AuthStatus extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
locale: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
import { createRuntime } from '../../runtime.js';
|
|
3
|
+
export default class AuthStatus extends BaseCommand {
|
|
4
|
+
static description = '检查当前登录状态';
|
|
5
|
+
static flags = BaseCommand.baseFlags;
|
|
6
|
+
async run() {
|
|
7
|
+
const { flags } = await this.parse(AuthStatus);
|
|
8
|
+
const runtime = await createRuntime(flags);
|
|
9
|
+
const payload = await runtime.client.request('/api/auth/session');
|
|
10
|
+
this.print({ authenticated: true, profile: runtime.profile.name, ...payload });
|
|
11
|
+
}
|
|
12
|
+
}
|