tripo-cli 0.1.0 → 0.2.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 +19 -3
- package/dist/commands/completion.js +1 -1
- package/dist/commands/config-cmd.js +32 -17
- package/dist/commands/doctor.js +6 -5
- package/dist/commands/home.js +4 -2
- package/dist/commands/login.js +129 -32
- package/dist/config/config.js +103 -14
- package/dist/core/client.js +16 -2
- package/dist/core/device-auth.js +41 -9
- package/dist/program.js +21 -1
- package/package.json +2 -2
- package/skill/commands/account.md +15 -4
- package/dist/src/ai/experts.js +0 -115
- package/dist/src/ai/llm.js +0 -88
- package/dist/src/ai/plan-card.js +0 -79
- package/dist/src/ai/wizard.js +0 -104
- package/dist/src/cli.js +0 -7
- package/dist/src/commands/account.js +0 -27
- package/dist/src/commands/ai.js +0 -113
- package/dist/src/commands/batch.js +0 -148
- package/dist/src/commands/completion.js +0 -84
- package/dist/src/commands/config-cmd.js +0 -84
- package/dist/src/commands/docs.js +0 -30
- package/dist/src/commands/doctor.js +0 -94
- package/dist/src/commands/generate.js +0 -81
- package/dist/src/commands/home.js +0 -68
- package/dist/src/commands/login.js +0 -225
- package/dist/src/commands/make.js +0 -142
- package/dist/src/commands/mcp.js +0 -194
- package/dist/src/commands/process-groups.js +0 -164
- package/dist/src/commands/shared.js +0 -75
- package/dist/src/commands/task.js +0 -127
- package/dist/src/commands/topup.js +0 -61
- package/dist/src/commands/view.js +0 -97
- package/dist/src/config/config.js +0 -79
- package/dist/src/config/context.js +0 -44
- package/dist/src/config/history.js +0 -104
- package/dist/src/core/client.js +0 -120
- package/dist/src/core/download.js +0 -107
- package/dist/src/core/errors.js +0 -64
- package/dist/src/core/output.js +0 -72
- package/dist/src/core/requests.js +0 -169
- package/dist/src/core/task-service.js +0 -161
- package/dist/src/core/upload.js +0 -36
- package/dist/src/core/viewer-assets.js +0 -24
- package/dist/src/core/viewer.js +0 -59
- package/dist/src/core/watch.js +0 -41
- package/dist/src/i18n.js +0 -19
- package/dist/src/knowledge/chains.js +0 -152
- package/dist/src/knowledge/error-catalog.js +0 -91
- package/dist/src/knowledge/models.js +0 -110
- package/dist/src/knowledge/params.js +0 -174
- package/dist/src/knowledge/scenarios.js +0 -112
- package/dist/src/pipeline/make.js +0 -233
- package/dist/src/pipeline/runner.js +0 -73
- package/dist/src/pipeline/stdin.js +0 -40
- package/dist/src/program.js +0 -147
- package/dist/src/types.js +0 -2
- package/dist/src/util/input-detect.js +0 -92
- package/dist/src/util/notify.js +0 -17
- package/dist/src/util/open.js +0 -18
- package/dist/src/util/spinner.js +0 -62
- package/dist/src/util/term-image.js +0 -28
- package/dist/src/version.js +0 -10
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Works with both Tripo regions from the same install — international (`openapi.
|
|
|
31
31
|
| `tripo ai [description]` | plan with a wizard (no LLM needed) or multi-expert conversation (BYO LLM), confirm a plan card, execute |
|
|
32
32
|
| `tripo view [task\|file]` | interactive 3D preview in your browser (local server, model-viewer) |
|
|
33
33
|
| `tripo redo [task]` | re-run with a fresh seed |
|
|
34
|
-
| `tripo login / logout / whoami` | device flow: browser approves, key arrives automatically (paste fallback); stored 0600 in `~/.tripo/config.json` |
|
|
34
|
+
| `tripo login / logout / whoami / use` | device flow: browser approves, key arrives automatically (paste fallback); stored 0600 in `~/.tripo/config.json`; `use` switches accounts |
|
|
35
35
|
| `tripo topup / balance / usage` | open the billing page + detect the credits arriving; check spend |
|
|
36
36
|
| `tripo generate <endpoint>` | deterministic access to all 8 generation endpoints |
|
|
37
37
|
| `tripo model / anim / mesh <step>` | refine, texture, stylize, convert, import, rig-check, rig, retarget, segment, complete, decimate, smartsegment |
|
|
@@ -43,7 +43,22 @@ Works with both Tripo regions from the same install — international (`openapi.
|
|
|
43
43
|
| `tripo mcp` | run as an MCP server (Cursor / Claude Desktop) |
|
|
44
44
|
| `tripo completion <shell>` | bash/zsh/fish completion |
|
|
45
45
|
|
|
46
|
-
Every task-producing command supports `-o/--out`, `--no-wait`, `--no-download`, `--name`, `--timeout`, `--notify`, and repeatable `--param key=value` passthrough. Global flags: `--json`, `--yes`, `--quiet`, `--no-open`.
|
|
46
|
+
Every task-producing command supports `-o/--out`, `--no-wait`, `--no-download`, `--name`, `--timeout`, `--notify`, and repeatable `--param key=value` passthrough. Global flags: `--json`, `--yes`, `--quiet`, `--no-open`, `--profile`.
|
|
47
|
+
|
|
48
|
+
## Multiple accounts
|
|
49
|
+
|
|
50
|
+
Credentials live in named profiles (one per account — e.g. a cn account and an ov account, or personal + work). Logging in never overwrites another profile's key:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
tripo login # first account → profile "default"
|
|
54
|
+
tripo login --profile work-cn # second account, kept separately, becomes active
|
|
55
|
+
tripo use # switch interactively (or: tripo use default)
|
|
56
|
+
tripo whoami # shows the active profile and lists the others
|
|
57
|
+
tripo make "a fox" --profile work-cn # one-shot override, nothing sticky
|
|
58
|
+
tripo logout # removes the *active* profile only
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each profile carries its own key + region, so switching accounts switches regions automatically. `TRIPO_PROFILE=work-cn` does the same as `--profile work-cn`; `TRIPO_API_KEY` bypasses profiles entirely (CI/agents). Configs written by older versions are read as the `default` profile — no migration step, nothing breaks.
|
|
47
62
|
|
|
48
63
|
## Scenario presets
|
|
49
64
|
|
|
@@ -81,7 +96,8 @@ tripo batch run assets.yaml --concurrency 2
|
|
|
81
96
|
|
|
82
97
|
| var | meaning |
|
|
83
98
|
| --- | --- |
|
|
84
|
-
| `TRIPO_API_KEY` | API key (`tsk_...`), highest precedence |
|
|
99
|
+
| `TRIPO_API_KEY` | API key (`tsk_...`), highest precedence (bypasses profiles) |
|
|
100
|
+
| `TRIPO_PROFILE` | account profile to use, same as the global `--profile` flag |
|
|
85
101
|
| `TRIPO_REGION` | `ov` or `cn` — normally unnecessary: `tripo login` probes both regions with your key and stores the right one automatically |
|
|
86
102
|
| `TRIPO_API_BASE_URL` / `TRIPO_PLATFORM_BASE_URL` | endpoint overrides |
|
|
87
103
|
| `TRIPO_HOME` | config/history directory (default `~/.tripo`) |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** `tripo completion <shell>` — static shell completion scripts. */
|
|
2
2
|
import { UsageError } from '../core/errors.js';
|
|
3
3
|
const COMMANDS = [
|
|
4
|
-
'login', 'logout', 'whoami', 'balance', 'usage', 'topup',
|
|
4
|
+
'login', 'logout', 'whoami', 'use', 'balance', 'usage', 'topup',
|
|
5
5
|
'make', 'view', 'redo', 'ai',
|
|
6
6
|
'generate', 'model', 'anim', 'mesh', 'task', 'files', 'batch', 'history',
|
|
7
7
|
'config', 'doctor', 'docs', 'completion', 'mcp',
|
|
@@ -1,40 +1,46 @@
|
|
|
1
1
|
/** `tripo config` — global config + directory context management. */
|
|
2
|
-
import { loadConfig, maskKey, saveConfig } from '../config/config.js';
|
|
2
|
+
import { activeProfileName, loadConfig, maskKey, saveConfig, updateProfile, } from '../config/config.js';
|
|
3
3
|
import { findContextFile, loadContext, updateContext } from '../config/context.js';
|
|
4
4
|
import { UsageError } from '../core/errors.js';
|
|
5
5
|
import { emitResult, success } from '../core/output.js';
|
|
6
6
|
import { coerceValue } from './shared.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'default_output_dir',
|
|
13
|
-
'llm.base_url',
|
|
14
|
-
'llm.api_key',
|
|
15
|
-
'llm.model',
|
|
16
|
-
]);
|
|
7
|
+
/** Account-bound settings — these live in the *active profile*. */
|
|
8
|
+
const PROFILE_SETTABLE = new Set(['region', 'api_base_url', 'platform_base_url']);
|
|
9
|
+
/** User preferences — top-level, shared by all profiles. */
|
|
10
|
+
const GLOBAL_SETTABLE = new Set(['language', 'default_output_dir', 'llm.base_url', 'llm.api_key', 'llm.model']);
|
|
11
|
+
const SETTABLE = [...PROFILE_SETTABLE, ...GLOBAL_SETTABLE];
|
|
17
12
|
export function registerConfigCommands(program) {
|
|
18
13
|
const config = program.command('config').description('global config (~/.tripo) and directory context (.tripo)');
|
|
19
14
|
config
|
|
20
15
|
.command('list')
|
|
21
|
-
.description('show effective config (
|
|
16
|
+
.description('show effective config (keys masked)')
|
|
22
17
|
.action(() => {
|
|
23
18
|
const cfg = loadConfig();
|
|
24
19
|
const shown = {
|
|
25
20
|
...cfg,
|
|
26
|
-
|
|
21
|
+
profiles: cfg.profiles
|
|
22
|
+
? Object.fromEntries(Object.entries(cfg.profiles).map(([name, prof]) => [
|
|
23
|
+
name,
|
|
24
|
+
{ ...prof, api_key: prof.api_key ? maskKey(prof.api_key) : undefined },
|
|
25
|
+
]))
|
|
26
|
+
: undefined,
|
|
27
27
|
llm: cfg.llm ? { ...cfg.llm, api_key: cfg.llm.api_key ? maskKey(cfg.llm.api_key) : undefined } : undefined,
|
|
28
28
|
};
|
|
29
29
|
const context = loadContext();
|
|
30
|
-
emitResult({ config: shown, context, context_file: findContextFile() }, JSON.stringify({ config: shown, context }, null, 2));
|
|
30
|
+
emitResult({ config: shown, active_profile: activeProfileName(cfg), context, context_file: findContextFile() }, JSON.stringify({ config: shown, active_profile: activeProfileName(cfg), context }, null, 2));
|
|
31
31
|
});
|
|
32
32
|
config
|
|
33
33
|
.command('set <key> <value>')
|
|
34
|
-
.description(`set a config value (${
|
|
34
|
+
.description(`set a config value (${SETTABLE.join(', ')})`)
|
|
35
35
|
.action((key, value) => {
|
|
36
|
-
if (
|
|
37
|
-
|
|
36
|
+
if (PROFILE_SETTABLE.has(key)) {
|
|
37
|
+
const profile = activeProfileName();
|
|
38
|
+
updateProfile(profile, { [key]: coerceValue(value) }, { activate: false });
|
|
39
|
+
success(`${key} = ${value} (profile "${profile}")`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!GLOBAL_SETTABLE.has(key)) {
|
|
43
|
+
throw new UsageError(`unknown config key "${key}" (valid: ${SETTABLE.join(', ')})`);
|
|
38
44
|
}
|
|
39
45
|
const cfg = loadConfig();
|
|
40
46
|
applyPath(cfg, key.split('.'), coerceValue(value));
|
|
@@ -45,6 +51,15 @@ export function registerConfigCommands(program) {
|
|
|
45
51
|
.command('unset <key>')
|
|
46
52
|
.description('remove a config value')
|
|
47
53
|
.action((key) => {
|
|
54
|
+
if (PROFILE_SETTABLE.has(key)) {
|
|
55
|
+
const cfg = loadConfig();
|
|
56
|
+
const profile = activeProfileName(cfg);
|
|
57
|
+
const prof = { ...cfg.profiles?.[profile] };
|
|
58
|
+
delete prof[key];
|
|
59
|
+
saveConfig({ ...cfg, profiles: { ...cfg.profiles, [profile]: prof } });
|
|
60
|
+
success(`${key} removed (profile "${profile}")`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
48
63
|
const cfg = loadConfig();
|
|
49
64
|
const parts = key.split('.');
|
|
50
65
|
const parent = parts.slice(0, -1).reduce((acc, part) => (acc && typeof acc[part] === 'object' ? acc[part] : undefined), cfg);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { configFileExists, loadConfig, resolveApiBaseUrl, resolveApiKey, resolveRegion, tripoHome } from '../config/config.js';
|
|
5
|
+
import { configFileExists, describeKeySource, listProfiles, loadConfig, resolveApiBaseUrl, resolveApiKey, resolveRegion, tripoHome } from '../config/config.js';
|
|
6
6
|
import { TripoClient } from '../core/client.js';
|
|
7
7
|
import { CliError } from '../core/errors.js';
|
|
8
8
|
import { detectRegionForKey } from '../core/region.js';
|
|
@@ -24,14 +24,15 @@ export function registerDoctorCommand(program) {
|
|
|
24
24
|
critical: true,
|
|
25
25
|
});
|
|
26
26
|
const key = resolveApiKey();
|
|
27
|
+
const profiles = listProfiles();
|
|
27
28
|
checks.push({
|
|
28
29
|
name: 'api key',
|
|
29
30
|
ok: Boolean(key),
|
|
30
31
|
detail: key
|
|
31
|
-
?
|
|
32
|
-
|
|
33
|
-
: '
|
|
34
|
-
|
|
32
|
+
? `from ${describeKeySource()}${profiles.length > 1 ? ` (${profiles.length} profiles: ${profiles.map((x) => x.name).join(', ')})` : ''}`
|
|
33
|
+
: profiles.length > 0
|
|
34
|
+
? `active profile has no key — profiles: ${profiles.map((x) => x.name).join(', ')}; run "tripo use"`
|
|
35
|
+
: 'missing — run "tripo login"',
|
|
35
36
|
critical: true,
|
|
36
37
|
});
|
|
37
38
|
if (configFileExists()) {
|
package/dist/commands/home.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Bare `tripo` — interactive home: status, quick actions, continue last task. */
|
|
2
2
|
import * as p from '@clack/prompts';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { loadConfig, resolveApiKey } from '../config/config.js';
|
|
4
|
+
import { activeProfileName, listProfiles, loadConfig, resolveApiKey } from '../config/config.js';
|
|
5
5
|
import { readHistory } from '../config/history.js';
|
|
6
6
|
import { TripoClient } from '../core/client.js';
|
|
7
7
|
import { isInteractive, log } from '../core/output.js';
|
|
@@ -29,7 +29,9 @@ export function registerHomeAction(program) {
|
|
|
29
29
|
balanceLine = t({ zh: '余额获取失败', en: 'balance unavailable' });
|
|
30
30
|
}
|
|
31
31
|
const last = readHistory().filter((e) => e.status === 'success').pop();
|
|
32
|
-
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
const profileNote = listProfiles(config).length > 1 ? t({ zh: ` · 账号 ${activeProfileName(config)}`, en: ` · profile ${activeProfileName(config)}` }) : '';
|
|
34
|
+
log(`${balanceLine}${profileNote}${last ? t({ zh: ` · 上次任务 @${last.name}`, en: ` · last task @${last.name}` }) : ''}`);
|
|
33
35
|
const action = await p.select({
|
|
34
36
|
message: t({ zh: '想做什么?', en: 'What do you want to do?' }),
|
|
35
37
|
options: [
|
package/dist/commands/login.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Onboarding loop: `tripo login` (device flow: browser approves, key arrives
|
|
3
3
|
* automatically; falls back to open-keys-page-and-paste), `tripo logout`,
|
|
4
|
-
* `tripo whoami
|
|
4
|
+
* `tripo whoami`, `tripo use` (switch between named profiles).
|
|
5
|
+
*
|
|
6
|
+
* Multi-account: every login lands in a profile (default: the active one).
|
|
7
|
+
* `tripo login --profile work` adds a second account without touching the
|
|
8
|
+
* first; `tripo use work` switches. Key precedence stays:
|
|
9
|
+
* TRIPO_API_KEY env > active profile in ~/.tripo/config.json.
|
|
5
10
|
*/
|
|
6
11
|
import * as p from '@clack/prompts';
|
|
7
|
-
import {
|
|
12
|
+
import { activeProfileName, DEFAULT_PROFILE, deleteProfile, describeKeySource, isValidProfileName, listProfiles, loadConfig, maskKey, REGION_DEFAULTS, resolveApiKey, resolvePlatformBaseUrl, resolveRegion, setActiveProfile, updateProfile, } from '../config/config.js';
|
|
8
13
|
import { TripoClient } from '../core/client.js';
|
|
9
14
|
import { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval } from '../core/device-auth.js';
|
|
10
15
|
import { AuthError, CliError, UsageError } from '../core/errors.js';
|
|
@@ -22,20 +27,28 @@ export function registerAuthCommands(program) {
|
|
|
22
27
|
.option('--paste', 'skip the browser flow; just paste an API key')
|
|
23
28
|
.option('--key <key>', 'provide the API key directly (tsk_...); region is auto-detected')
|
|
24
29
|
.option('--region <region>', 'ov or cn — only needed for the browser flow; auto-detected once a key exists')
|
|
30
|
+
.option('--profile <name>', 'save into a named profile (for a second account); becomes the active profile')
|
|
25
31
|
.action(async (opts) => {
|
|
26
32
|
await runLogin(opts);
|
|
27
33
|
});
|
|
28
34
|
program
|
|
29
35
|
.command('logout')
|
|
30
|
-
.description('remove the locally stored API key')
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
.description('remove the locally stored API key (current profile only)')
|
|
37
|
+
.option('--profile <name>', 'log out a specific profile instead of the active one')
|
|
38
|
+
.action((opts) => {
|
|
39
|
+
const name = opts.profile ?? activeProfileName();
|
|
40
|
+
if (!deleteProfile(name)) {
|
|
41
|
+
log(t({ zh: `Profile「${name}」本地没有存储 API Key`, en: `no API key stored locally for profile "${name}"` }));
|
|
35
42
|
}
|
|
36
43
|
else {
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
success(t({ zh: `已删除 profile「${name}」的本地 API Key`, en: `local API key removed (profile "${name}")` }));
|
|
45
|
+
const rest = listProfiles();
|
|
46
|
+
if (rest.length > 0) {
|
|
47
|
+
log(t({
|
|
48
|
+
zh: `其余账号仍在: ${rest.map((x) => x.name).join(', ')}(tripo use 切换)`,
|
|
49
|
+
en: `remaining profiles: ${rest.map((x) => x.name).join(', ')} (switch with "tripo use")`,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
39
52
|
}
|
|
40
53
|
log(t({
|
|
41
54
|
zh: `如需吊销 Key 本身,请前往 ${resolvePlatformBaseUrl()} 的 API Keys 页面`,
|
|
@@ -44,36 +57,115 @@ export function registerAuthCommands(program) {
|
|
|
44
57
|
});
|
|
45
58
|
program
|
|
46
59
|
.command('whoami')
|
|
47
|
-
.description('show the active key (masked), region, and
|
|
60
|
+
.description('show the active key (masked), region, balance, and other profiles')
|
|
48
61
|
.action(async () => {
|
|
49
62
|
const config = loadConfig();
|
|
63
|
+
// TripoClient's constructor is the single authority on "no key": it
|
|
64
|
+
// distinguishes a mistyped profile from not being logged in at all.
|
|
65
|
+
const client = new TripoClient();
|
|
50
66
|
const key = resolveApiKey(config);
|
|
51
|
-
|
|
52
|
-
throw new AuthError(t({ zh: '未配置 API Key', en: 'no API key configured' }));
|
|
53
|
-
const source = process.env.TRIPO_API_KEY ? 'env:TRIPO_API_KEY' : '~/.tripo/config.json';
|
|
67
|
+
const source = describeKeySource(config);
|
|
54
68
|
const region = resolveRegion(config);
|
|
55
|
-
const client = new TripoClient();
|
|
56
69
|
const balance = await client.getBalance();
|
|
70
|
+
const others = listProfiles(config).filter((x) => !x.active);
|
|
71
|
+
const lines = [
|
|
72
|
+
`key ${maskKey(key)} (${source})`,
|
|
73
|
+
`region ${region}`,
|
|
74
|
+
`api ${client.baseUrl}`,
|
|
75
|
+
t({ zh: `余额 ${balance.balance} 积分(冻结 ${balance.frozen})`, en: `balance ${balance.balance} credits (${balance.frozen} frozen)` }),
|
|
76
|
+
];
|
|
77
|
+
if (others.length > 0) {
|
|
78
|
+
lines.push(t({
|
|
79
|
+
zh: `其它账号 ${others.map((x) => profileLabel(x.name, x.profile.region)).join(', ')}(tripo use 切换)`,
|
|
80
|
+
en: `others ${others.map((x) => profileLabel(x.name, x.profile.region)).join(', ')} (switch with "tripo use")`,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
57
83
|
emitResult({
|
|
58
84
|
key: maskKey(key),
|
|
59
85
|
source,
|
|
86
|
+
profile: activeProfileName(config),
|
|
60
87
|
region,
|
|
61
88
|
api_base_url: client.baseUrl,
|
|
62
89
|
balance: balance.balance,
|
|
63
90
|
frozen: balance.frozen,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`region ${region}`,
|
|
67
|
-
`api ${client.baseUrl}`,
|
|
68
|
-
t({ zh: `余额 ${balance.balance} 积分(冻结 ${balance.frozen})`, en: `balance ${balance.balance} credits (${balance.frozen} frozen)` }),
|
|
69
|
-
].join('\n'));
|
|
91
|
+
profiles: listProfiles(config).map((x) => ({ name: x.name, region: x.profile.region, active: x.active })),
|
|
92
|
+
}, lines.join('\n'));
|
|
70
93
|
});
|
|
94
|
+
program
|
|
95
|
+
.command('use [profile]')
|
|
96
|
+
.description('switch the active account profile (no argument: pick interactively)')
|
|
97
|
+
.action(async (name) => {
|
|
98
|
+
await runUse(name);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function profileLabel(name, region) {
|
|
102
|
+
return region ? `${name} (${region})` : name;
|
|
103
|
+
}
|
|
104
|
+
async function runUse(name) {
|
|
105
|
+
const profiles = listProfiles();
|
|
106
|
+
if (profiles.length === 0) {
|
|
107
|
+
throw new AuthError(t({ zh: '还没有任何账号', en: 'no profiles yet' }), t({ zh: '先运行 tripo login 登录', en: 'run "tripo login" first' }));
|
|
108
|
+
}
|
|
109
|
+
if (!name) {
|
|
110
|
+
if (!isInteractive()) {
|
|
111
|
+
throw new UsageError(t({
|
|
112
|
+
zh: `非交互环境请指定名字: tripo use <${profiles.map((x) => x.name).join('|')}>`,
|
|
113
|
+
en: `specify a profile in non-interactive mode: tripo use <${profiles.map((x) => x.name).join('|')}>`,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
const answer = await p.select({
|
|
117
|
+
message: t({ zh: '切换到哪个账号?', en: 'Switch to which account?' }),
|
|
118
|
+
initialValue: profiles.find((x) => x.active)?.name,
|
|
119
|
+
options: profiles.map((x) => ({
|
|
120
|
+
value: x.name,
|
|
121
|
+
label: profileLabel(x.name, x.profile.region) + (x.active ? t({ zh: '(当前)', en: ' (current)' }) : ''),
|
|
122
|
+
hint: x.profile.api_key ? maskKey(x.profile.api_key) : undefined,
|
|
123
|
+
})),
|
|
124
|
+
});
|
|
125
|
+
if (p.isCancel(answer))
|
|
126
|
+
throw new CliError('cancelled', EXIT.FATAL);
|
|
127
|
+
name = answer;
|
|
128
|
+
}
|
|
129
|
+
const match = profiles.find((x) => x.name === name);
|
|
130
|
+
if (!match) {
|
|
131
|
+
throw new UsageError(t({
|
|
132
|
+
zh: `没有叫「${name}」的 profile(现有: ${profiles.map((x) => x.name).join(', ')})`,
|
|
133
|
+
en: `no profile named "${name}" (have: ${profiles.map((x) => x.name).join(', ')})`,
|
|
134
|
+
}), t({ zh: `新账号用 tripo login --profile ${name} 登录`, en: `log a new account in with: tripo login --profile ${name}` }));
|
|
135
|
+
}
|
|
136
|
+
setActiveProfile(name);
|
|
137
|
+
success(t({
|
|
138
|
+
zh: `已切换到「${name}」${match.profile.region ? `(region=${match.profile.region})` : ''}`,
|
|
139
|
+
en: `switched to "${name}"${match.profile.region ? ` (region=${match.profile.region})` : ''}`,
|
|
140
|
+
}));
|
|
141
|
+
if (process.env.TRIPO_API_KEY) {
|
|
142
|
+
warn(t({
|
|
143
|
+
zh: '注意:环境变量 TRIPO_API_KEY 仍然优先于 profile,如需使用 profile 请先 unset',
|
|
144
|
+
en: 'note: TRIPO_API_KEY env var still overrides the profile — unset it to use the profile key',
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
71
147
|
}
|
|
72
148
|
export async function runLogin(opts) {
|
|
73
149
|
let region = normalizeRegion(opts.region);
|
|
150
|
+
// No --profile → the *active* profile: `tripo use work && tripo login`
|
|
151
|
+
// re-authenticates work, it does not silently overwrite default.
|
|
152
|
+
const profile = opts.profile ?? activeProfileName();
|
|
153
|
+
if (!isValidProfileName(profile)) {
|
|
154
|
+
throw new UsageError(t({
|
|
155
|
+
zh: `profile 名「${profile}」不合法(小写字母/数字开头,可含 - _,最长 32)`,
|
|
156
|
+
en: `invalid profile name "${profile}" (lowercase alphanumeric start, may contain - _, max 32 chars)`,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
const replacing = loadConfig().profiles?.[profile]?.api_key;
|
|
160
|
+
if (replacing) {
|
|
161
|
+
log(t({
|
|
162
|
+
zh: `将替换 profile「${profile}」现有的 Key ${maskKey(replacing)}(新账号请用 tripo login --profile 名字)`,
|
|
163
|
+
en: `this will replace the key ${maskKey(replacing)} in profile "${profile}" (for a second account use: tripo login --profile <name>)`,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
74
166
|
if (opts.key) {
|
|
75
167
|
// Key in hand → region is detected, not asked (a key lives in exactly one region).
|
|
76
|
-
await validateAndSave(opts.key, region);
|
|
168
|
+
await validateAndSave(opts.key, region, profile);
|
|
77
169
|
return;
|
|
78
170
|
}
|
|
79
171
|
if (!isInteractive()) {
|
|
@@ -105,8 +197,8 @@ export async function runLogin(opts) {
|
|
|
105
197
|
throw new CliError('cancelled', EXIT.FATAL);
|
|
106
198
|
region = answer;
|
|
107
199
|
}
|
|
108
|
-
const key = opts.paste ? await keyFromConsole(region, false) : await deviceFlowWithFallback(region);
|
|
109
|
-
await validateAndSave(key, region);
|
|
200
|
+
const key = opts.paste ? await keyFromConsole(region, false) : await deviceFlowWithFallback(region, profile);
|
|
201
|
+
await validateAndSave(key, region, profile);
|
|
110
202
|
p.outro(t({
|
|
111
203
|
zh: '登录完成!试试: tripo make "一只可爱的机器人"',
|
|
112
204
|
en: 'logged in! try: tripo make "a cute robot"',
|
|
@@ -119,10 +211,12 @@ export async function runLogin(opts) {
|
|
|
119
211
|
* the backend feature flag is off (endpoints 404) or the console API is
|
|
120
212
|
* unreachable, so login never hard-fails on a backend rollout state.
|
|
121
213
|
*/
|
|
122
|
-
async function deviceFlowWithFallback(region) {
|
|
214
|
+
async function deviceFlowWithFallback(region, profile) {
|
|
123
215
|
let grant;
|
|
124
216
|
try {
|
|
125
|
-
|
|
217
|
+
// The backend rotates keys by name; scoping the name by profile keeps two
|
|
218
|
+
// profiles of the same account from revoking each other's key.
|
|
219
|
+
grant = await requestDeviceCode(region, profile === DEFAULT_PROFILE ? undefined : profile);
|
|
126
220
|
}
|
|
127
221
|
catch (err) {
|
|
128
222
|
if (err instanceof DeviceAuthUnavailableError) {
|
|
@@ -258,12 +352,12 @@ async function keyFromConsole(region, openBrowser) {
|
|
|
258
352
|
* its verdict beats whatever the user selected, so a wrong region choice
|
|
259
353
|
* self-heals instead of producing a config that 401s on every call.
|
|
260
354
|
*/
|
|
261
|
-
async function validateAndSave(key, selectedRegion) {
|
|
355
|
+
async function validateAndSave(key, selectedRegion, profile) {
|
|
262
356
|
if (process.env.TRIPO_API_BASE_URL) {
|
|
263
357
|
const client = new TripoClient({ apiKey: key });
|
|
264
358
|
try {
|
|
265
359
|
const balance = await client.getBalance();
|
|
266
|
-
finishSave(key, selectedRegion ?? 'ov', balance.balance);
|
|
360
|
+
finishSave(key, selectedRegion ?? 'ov', balance.balance, profile);
|
|
267
361
|
}
|
|
268
362
|
catch (err) {
|
|
269
363
|
throw asAuthError(err);
|
|
@@ -283,13 +377,16 @@ async function validateAndSave(key, selectedRegion) {
|
|
|
283
377
|
en: `this key belongs to the ${detected.region} region — auto-switched (your choice "${selectedRegion}" was wrong)`,
|
|
284
378
|
}));
|
|
285
379
|
}
|
|
286
|
-
finishSave(key, detected.region, detected.balance.balance);
|
|
380
|
+
finishSave(key, detected.region, detected.balance.balance, profile);
|
|
287
381
|
}
|
|
288
|
-
function finishSave(key, region, balance) {
|
|
289
|
-
|
|
382
|
+
function finishSave(key, region, balance, profile) {
|
|
383
|
+
// Saving activates the profile: "the account I just logged into is the
|
|
384
|
+
// account I am using" — no second command needed for the common case.
|
|
385
|
+
updateProfile(profile, { api_key: key, region });
|
|
386
|
+
const where = profile === DEFAULT_PROFILE ? '~/.tripo/config.json' : `~/.tripo/config.json · profile "${profile}"`;
|
|
290
387
|
success(t({
|
|
291
|
-
zh: `Key 验证通过(region=${region}),当前余额 ${balance} 积分(已保存到
|
|
292
|
-
en: `key verified (region=${region}); balance ${balance} credits (saved to
|
|
388
|
+
zh: `Key 验证通过(region=${region}),当前余额 ${balance} 积分(已保存到 ${where},权限 0600)`,
|
|
389
|
+
en: `key verified (region=${region}); balance ${balance} credits (saved to ${where}, mode 0600)`,
|
|
293
390
|
}));
|
|
294
391
|
if (Number(balance) === 0) {
|
|
295
392
|
warn(t({
|
package/dist/config/config.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Global config at ~/.tripo/config.json (mode 0600 — it holds
|
|
2
|
+
* Global config at ~/.tripo/config.json (mode 0600 — it holds API keys).
|
|
3
|
+
*
|
|
4
|
+
* Multi-account model: credentials live in named *profiles* (a dict plus an
|
|
5
|
+
* `active_profile` pointer — same shape as AWS CLI profiles / kubectl
|
|
6
|
+
* contexts). Everything account-bound (key, region, URL overrides) is
|
|
7
|
+
* per-profile; user preferences (language, llm, output dir) stay top-level.
|
|
8
|
+
*
|
|
9
|
+
* v0.1.0 stored a single key at the top level. That format is normalized
|
|
10
|
+
* into `profiles.default` on *every* load, so old files keep working
|
|
11
|
+
* untouched and the rest of the codebase only ever sees one shape. Writes
|
|
12
|
+
* always persist the normalized form, so any write migrates the file.
|
|
13
|
+
*
|
|
3
14
|
* Precedence for every setting: env var > directory context > global config.
|
|
15
|
+
* Profile selection: TRIPO_PROFILE env (set by the global --profile flag
|
|
16
|
+
* too) > active_profile > "default". TRIPO_API_KEY bypasses profiles
|
|
17
|
+
* entirely — agent/CI setups never touch the profile machinery.
|
|
4
18
|
*/
|
|
5
19
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
20
|
import { homedir } from 'node:os';
|
|
7
21
|
import { join } from 'node:path';
|
|
22
|
+
export const DEFAULT_PROFILE = 'default';
|
|
8
23
|
/**
|
|
9
24
|
* Everything region-specific reduces to these three URLs: the generation API
|
|
10
25
|
* (`api`), the console website (`platform`), and the console's own backend
|
|
@@ -35,9 +50,33 @@ export function tripoHome() {
|
|
|
35
50
|
function configPath() {
|
|
36
51
|
return join(tripoHome(), 'config.json');
|
|
37
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Fold v0.1.0 top-level credential fields into `profiles.default`, so no
|
|
55
|
+
* other code ever branches on "old format vs new format". If a default
|
|
56
|
+
* profile already exists its fields win (the file was already migrated;
|
|
57
|
+
* stray top-level leftovers must not resurrect a replaced key).
|
|
58
|
+
*/
|
|
59
|
+
export function normalizeConfig(raw) {
|
|
60
|
+
const { api_key, region, api_base_url, platform_base_url, ...rest } = raw;
|
|
61
|
+
const legacy = {};
|
|
62
|
+
if (api_key)
|
|
63
|
+
legacy.api_key = api_key;
|
|
64
|
+
if (region)
|
|
65
|
+
legacy.region = region;
|
|
66
|
+
if (api_base_url)
|
|
67
|
+
legacy.api_base_url = api_base_url;
|
|
68
|
+
if (platform_base_url)
|
|
69
|
+
legacy.platform_base_url = platform_base_url;
|
|
70
|
+
if (Object.keys(legacy).length === 0)
|
|
71
|
+
return rest;
|
|
72
|
+
return {
|
|
73
|
+
...rest,
|
|
74
|
+
profiles: { ...rest.profiles, [DEFAULT_PROFILE]: { ...legacy, ...rest.profiles?.[DEFAULT_PROFILE] } },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
38
77
|
export function loadConfig() {
|
|
39
78
|
try {
|
|
40
|
-
return JSON.parse(readFileSync(configPath(), 'utf8'));
|
|
79
|
+
return normalizeConfig(JSON.parse(readFileSync(configPath(), 'utf8')));
|
|
41
80
|
}
|
|
42
81
|
catch {
|
|
43
82
|
return {};
|
|
@@ -47,7 +86,9 @@ export function saveConfig(config) {
|
|
|
47
86
|
const dir = tripoHome();
|
|
48
87
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
49
88
|
const file = configPath();
|
|
50
|
-
|
|
89
|
+
// Writes always persist the normalized shape — this is what migrates
|
|
90
|
+
// legacy files, and it keeps a single on-disk format going forward.
|
|
91
|
+
writeFileSync(file, JSON.stringify(normalizeConfig(config), null, 2) + '\n', { mode: 0o600 });
|
|
51
92
|
// writeFileSync mode only applies on creation; enforce on every save.
|
|
52
93
|
chmodSync(file, 0o600);
|
|
53
94
|
}
|
|
@@ -56,37 +97,85 @@ export function updateConfig(patch) {
|
|
|
56
97
|
saveConfig(next);
|
|
57
98
|
return next;
|
|
58
99
|
}
|
|
59
|
-
|
|
100
|
+
/** Names are shell-friendly and short — they appear in key names and flags. */
|
|
101
|
+
export function isValidProfileName(name) {
|
|
102
|
+
return /^[a-z0-9][a-z0-9_-]{0,31}$/.test(name);
|
|
103
|
+
}
|
|
104
|
+
/** Selected profile name: TRIPO_PROFILE (or --profile) > config > default. */
|
|
105
|
+
export function activeProfileName(config = loadConfig()) {
|
|
106
|
+
return process.env.TRIPO_PROFILE || config.active_profile || DEFAULT_PROFILE;
|
|
107
|
+
}
|
|
108
|
+
/** The selected profile's data; {} when it does not exist (yet). */
|
|
109
|
+
export function activeProfile(config = loadConfig()) {
|
|
110
|
+
return config.profiles?.[activeProfileName(config)] ?? {};
|
|
111
|
+
}
|
|
112
|
+
export function listProfiles(config = loadConfig()) {
|
|
113
|
+
const active = activeProfileName(config);
|
|
114
|
+
return Object.entries(config.profiles ?? {}).map(([name, profile]) => ({
|
|
115
|
+
name,
|
|
116
|
+
profile,
|
|
117
|
+
active: name === active,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
/** Create/update one profile and (by default) make it the active one. */
|
|
121
|
+
export function updateProfile(name, patch, opts = {}) {
|
|
60
122
|
const config = loadConfig();
|
|
61
|
-
|
|
62
|
-
|
|
123
|
+
const profiles = { ...config.profiles, [name]: { ...config.profiles?.[name], ...patch } };
|
|
124
|
+
saveConfig({ ...config, profiles, ...(opts.activate !== false ? { active_profile: name } : {}) });
|
|
125
|
+
}
|
|
126
|
+
export function setActiveProfile(name) {
|
|
127
|
+
updateConfig({ active_profile: name });
|
|
128
|
+
}
|
|
129
|
+
/** Remove a profile (logout). Active pointer falls back to default. */
|
|
130
|
+
export function deleteProfile(name) {
|
|
131
|
+
const config = loadConfig();
|
|
132
|
+
if (!config.profiles?.[name])
|
|
133
|
+
return false;
|
|
134
|
+
const profiles = { ...config.profiles };
|
|
135
|
+
delete profiles[name];
|
|
136
|
+
const next = { ...config, profiles };
|
|
137
|
+
if (Object.keys(profiles).length === 0)
|
|
138
|
+
delete next.profiles;
|
|
139
|
+
if (next.active_profile === name)
|
|
140
|
+
delete next.active_profile;
|
|
141
|
+
if (Object.keys(next).length === 0) {
|
|
63
142
|
try {
|
|
64
143
|
unlinkSync(configPath());
|
|
65
144
|
}
|
|
66
145
|
catch { }
|
|
146
|
+
return true;
|
|
67
147
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
148
|
+
saveConfig(next);
|
|
149
|
+
return true;
|
|
71
150
|
}
|
|
72
|
-
/** Resolve the API key: env wins, then
|
|
151
|
+
/** Resolve the API key: env wins, then the active profile. */
|
|
73
152
|
export function resolveApiKey(config = loadConfig()) {
|
|
74
|
-
return process.env.TRIPO_API_KEY || config.api_key;
|
|
153
|
+
return process.env.TRIPO_API_KEY || activeProfile(config).api_key;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Where the effective key comes from, for whoami/doctor and for the
|
|
157
|
+
* "no key" error message — so a mistyped --profile says "profile not
|
|
158
|
+
* found", never a misleading "you are not logged in".
|
|
159
|
+
*/
|
|
160
|
+
export function describeKeySource(config = loadConfig()) {
|
|
161
|
+
if (process.env.TRIPO_API_KEY)
|
|
162
|
+
return 'env:TRIPO_API_KEY';
|
|
163
|
+
return `profile:${activeProfileName(config)}`;
|
|
75
164
|
}
|
|
76
165
|
export function resolveRegion(config = loadConfig()) {
|
|
77
166
|
const env = process.env.TRIPO_REGION;
|
|
78
167
|
if (env === 'cn' || env === 'ov')
|
|
79
168
|
return env;
|
|
80
|
-
return config.region ?? 'ov';
|
|
169
|
+
return activeProfile(config).region ?? 'ov';
|
|
81
170
|
}
|
|
82
171
|
export function resolveApiBaseUrl(config = loadConfig()) {
|
|
83
172
|
return (process.env.TRIPO_API_BASE_URL ||
|
|
84
|
-
config.api_base_url ||
|
|
173
|
+
activeProfile(config).api_base_url ||
|
|
85
174
|
REGION_DEFAULTS[resolveRegion(config)].api);
|
|
86
175
|
}
|
|
87
176
|
export function resolvePlatformBaseUrl(config = loadConfig()) {
|
|
88
177
|
return (process.env.TRIPO_PLATFORM_BASE_URL ||
|
|
89
|
-
config.platform_base_url ||
|
|
178
|
+
activeProfile(config).platform_base_url ||
|
|
90
179
|
REGION_DEFAULTS[resolveRegion(config)].platform);
|
|
91
180
|
}
|
|
92
181
|
export function configFileExists() {
|
package/dist/core/client.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Handles the response envelope, error translation, timeouts, and
|
|
4
4
|
* exponential-backoff retries for transient failures (429/5xx/network).
|
|
5
5
|
*/
|
|
6
|
-
import { loadConfig, resolveApiBaseUrl, resolveApiKey } from '../config/config.js';
|
|
6
|
+
import { activeProfileName, DEFAULT_PROFILE, listProfiles, loadConfig, resolveApiBaseUrl, resolveApiKey } from '../config/config.js';
|
|
7
|
+
import { t } from '../i18n.js';
|
|
7
8
|
import { ApiError, AuthError, NetworkError } from './errors.js';
|
|
8
9
|
export class TripoClient {
|
|
9
10
|
baseUrl;
|
|
@@ -16,7 +17,20 @@ export class TripoClient {
|
|
|
16
17
|
this.baseUrl = (options.baseUrl ?? resolveApiBaseUrl(config)).replace(/\/$/, '');
|
|
17
18
|
const key = options.apiKey ?? resolveApiKey(config);
|
|
18
19
|
if (!key) {
|
|
19
|
-
|
|
20
|
+
// A mistyped --profile must say so — "not logged in" would send the
|
|
21
|
+
// user to re-login while their key sits in another profile.
|
|
22
|
+
const wanted = activeProfileName(config);
|
|
23
|
+
const known = listProfiles(config).map((p) => p.name);
|
|
24
|
+
if (wanted !== DEFAULT_PROFILE && !known.includes(wanted)) {
|
|
25
|
+
throw new AuthError(t({
|
|
26
|
+
zh: `没有叫「${wanted}」的 profile${known.length ? `(现有: ${known.join(', ')})` : ''}`,
|
|
27
|
+
en: `profile "${wanted}" not found${known.length ? ` (have: ${known.join(', ')})` : ''}`,
|
|
28
|
+
}), t({
|
|
29
|
+
zh: `用 tripo login --profile ${wanted} 登录,或 tripo use 切换`,
|
|
30
|
+
en: `run "tripo login --profile ${wanted}" to create it, or "tripo use" to switch`,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
throw new AuthError(t({ zh: '未配置 API Key', en: 'no API key configured' }));
|
|
20
34
|
}
|
|
21
35
|
this.apiKey = key;
|
|
22
36
|
this.timeoutMs = options.timeoutMs ?? 60_000;
|