tripo-cli 0.1.0 → 0.2.1
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 +20 -4
- 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 +201 -39
- 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/SKILL.md +13 -1
- package/skill/commands/account.md +19 -5
- 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
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `tripo docs` — print bundled documentation. `--llm` prints the skill map
|
|
3
|
-
* designed for AI coding agents (the npm package ships skill/ alongside dist/).
|
|
4
|
-
*/
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { UsageError } from '../core/errors.js';
|
|
7
|
-
export function skillRoot() {
|
|
8
|
-
return new URL('../../skill/', import.meta.url);
|
|
9
|
-
}
|
|
10
|
-
export function readSkillFile(relative) {
|
|
11
|
-
const url = new URL(relative, skillRoot());
|
|
12
|
-
if (!existsSync(url))
|
|
13
|
-
throw new UsageError(`documentation file not found: ${relative}`);
|
|
14
|
-
return readFileSync(url, 'utf8');
|
|
15
|
-
}
|
|
16
|
-
export function registerDocsCommand(program) {
|
|
17
|
-
program
|
|
18
|
-
.command('docs')
|
|
19
|
-
.description('print bundled docs (default: the LLM/agent skill map)')
|
|
20
|
-
.option('--llm', 'print the agent-facing SKILL.md (default)')
|
|
21
|
-
.option('--topic <name>', 'print a specific doc: commands/<cmd> or examples/<scenario> or common-errors')
|
|
22
|
-
.action((opts) => {
|
|
23
|
-
if (opts.topic) {
|
|
24
|
-
const file = opts.topic.endsWith('.md') ? opts.topic : `${opts.topic}.md`;
|
|
25
|
-
process.stdout.write(readSkillFile(file));
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
process.stdout.write(readSkillFile('SKILL.md'));
|
|
29
|
-
});
|
|
30
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/** `tripo doctor` — environment/config/network self-diagnosis. */
|
|
2
|
-
import { statSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import pc from 'picocolors';
|
|
5
|
-
import { configFileExists, loadConfig, resolveApiBaseUrl, resolveApiKey, tripoHome } from '../config/config.js';
|
|
6
|
-
import { TripoClient } from '../core/client.js';
|
|
7
|
-
import { emitResult } from '../core/output.js';
|
|
8
|
-
import { resolveLlmConfig } from '../ai/llm.js';
|
|
9
|
-
import { terminalSupportsInlineImages } from '../util/term-image.js';
|
|
10
|
-
export function registerDoctorCommand(program) {
|
|
11
|
-
program
|
|
12
|
-
.command('doctor')
|
|
13
|
-
.description('diagnose environment, key, network, and terminal capabilities')
|
|
14
|
-
.action(async () => {
|
|
15
|
-
const checks = [];
|
|
16
|
-
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
17
|
-
checks.push({
|
|
18
|
-
name: 'node',
|
|
19
|
-
ok: nodeMajor >= 20,
|
|
20
|
-
detail: `v${process.versions.node} (need >= 20)`,
|
|
21
|
-
critical: true,
|
|
22
|
-
});
|
|
23
|
-
const key = resolveApiKey();
|
|
24
|
-
checks.push({
|
|
25
|
-
name: 'api key',
|
|
26
|
-
ok: Boolean(key),
|
|
27
|
-
detail: key
|
|
28
|
-
? process.env.TRIPO_API_KEY
|
|
29
|
-
? 'from TRIPO_API_KEY env'
|
|
30
|
-
: 'from ~/.tripo/config.json'
|
|
31
|
-
: 'missing — run "tripo login"',
|
|
32
|
-
critical: true,
|
|
33
|
-
});
|
|
34
|
-
if (configFileExists()) {
|
|
35
|
-
try {
|
|
36
|
-
const mode = statSync(join(tripoHome(), 'config.json')).mode & 0o777;
|
|
37
|
-
checks.push({
|
|
38
|
-
name: 'config permissions',
|
|
39
|
-
ok: mode === 0o600,
|
|
40
|
-
detail: `0${mode.toString(8)} (expect 0600)`,
|
|
41
|
-
critical: false,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
catch { }
|
|
45
|
-
}
|
|
46
|
-
const baseUrl = resolveApiBaseUrl();
|
|
47
|
-
if (key) {
|
|
48
|
-
try {
|
|
49
|
-
const client = new TripoClient();
|
|
50
|
-
const balance = await client.getBalance();
|
|
51
|
-
checks.push({ name: 'api reachability', ok: true, detail: `${baseUrl} · balance ${balance.balance}`, critical: true });
|
|
52
|
-
checks.push({
|
|
53
|
-
name: 'balance',
|
|
54
|
-
ok: Number(balance.balance) > 0,
|
|
55
|
-
detail: `${balance.balance} credits (${balance.frozen} frozen)${Number(balance.balance) <= 0 ? ' — run "tripo topup"' : ''}`,
|
|
56
|
-
critical: false,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
checks.push({
|
|
61
|
-
name: 'api reachability',
|
|
62
|
-
ok: false,
|
|
63
|
-
detail: `${baseUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
64
|
-
critical: true,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
checks.push({ name: 'api reachability', ok: false, detail: `skipped (no key) · would use ${baseUrl}`, critical: false });
|
|
70
|
-
}
|
|
71
|
-
const llm = resolveLlmConfig();
|
|
72
|
-
checks.push({
|
|
73
|
-
name: 'llm (tripo ai conversation mode)',
|
|
74
|
-
ok: true,
|
|
75
|
-
detail: llm ? `${llm.model} @ ${llm.base_url}` : 'not configured — wizard mode only (this is fine)',
|
|
76
|
-
critical: false,
|
|
77
|
-
});
|
|
78
|
-
checks.push({
|
|
79
|
-
name: 'terminal',
|
|
80
|
-
ok: true,
|
|
81
|
-
detail: `${process.stdout.isTTY ? 'interactive TTY' : 'non-TTY (headless mode)'}${terminalSupportsInlineImages() ? ' · inline images supported' : ''}`,
|
|
82
|
-
critical: false,
|
|
83
|
-
});
|
|
84
|
-
const language = loadConfig().language ?? 'auto';
|
|
85
|
-
checks.push({ name: 'language', ok: true, detail: language, critical: false });
|
|
86
|
-
const failedCritical = checks.some((c) => c.critical && !c.ok);
|
|
87
|
-
const human = checks
|
|
88
|
-
.map((c) => `${c.ok ? pc.green('✓') : c.critical ? pc.red('✗') : pc.yellow('!')} ${c.name.padEnd(34)} ${c.detail}`)
|
|
89
|
-
.join('\n');
|
|
90
|
-
emitResult({ ok: !failedCritical, checks }, human);
|
|
91
|
-
if (failedCritical)
|
|
92
|
-
process.exitCode = 1;
|
|
93
|
-
});
|
|
94
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/** `tripo generate <endpoint>` — deterministic 1:1 mapping to the 8 generation endpoints. */
|
|
2
|
-
import { UsageError } from '../core/errors.js';
|
|
3
|
-
import { warn } from '../core/output.js';
|
|
4
|
-
import { buildEditMultiview, buildImageToImage, buildImageToModel, buildImageToMultiview, buildImageToSplat, buildMultiviewToModel, buildTextToImage, buildTextToModel, } from '../core/requests.js';
|
|
5
|
-
import { resolveInputValue, runTask } from '../core/task-service.js';
|
|
6
|
-
import { chooseModel } from '../knowledge/models.js';
|
|
7
|
-
import { assignViews } from '../pipeline/make.js';
|
|
8
|
-
import { newClient, printTaskResult, withTaskOptions } from './shared.js';
|
|
9
|
-
export function registerGenerateCommands(program) {
|
|
10
|
-
const generate = program.command('generate').description('deterministic generation endpoints (text/image/multiview → model/image/splat)');
|
|
11
|
-
const add = (name, description, inputArity, build) => {
|
|
12
|
-
const cmd = generate
|
|
13
|
-
.command(`${name} ${inputArity === 'prompt' ? '<prompt...>' : inputArity === 'single' ? '<input>' : '<inputs...>'}`)
|
|
14
|
-
.description(description)
|
|
15
|
-
.option('--model <model>', 'model version override');
|
|
16
|
-
if (inputArity !== 'prompt')
|
|
17
|
-
cmd.option('--prompt <text>', 'prompt for prompt-guided endpoints');
|
|
18
|
-
withTaskOptions(cmd);
|
|
19
|
-
cmd.action(async (inputs, flags) => {
|
|
20
|
-
const list = Array.isArray(inputs) ? inputs : [inputs];
|
|
21
|
-
const client = newClient();
|
|
22
|
-
const built = await build(client, list, flags);
|
|
23
|
-
for (const w of built.warnings)
|
|
24
|
-
warn(w);
|
|
25
|
-
const result = await runTask({
|
|
26
|
-
client,
|
|
27
|
-
request: built.request,
|
|
28
|
-
taskType: built.taskType,
|
|
29
|
-
name: flags.name ?? (inputArity === 'prompt' ? list.join(' ') : list[0]),
|
|
30
|
-
outDir: flags.out,
|
|
31
|
-
noWait: flags.wait === false,
|
|
32
|
-
noDownload: flags.download === false,
|
|
33
|
-
notify: flags.notify,
|
|
34
|
-
timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
|
|
35
|
-
});
|
|
36
|
-
printTaskResult(result);
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
add('text-to-model', 'text → 3D model', 'prompt', async (_c, inputs, flags) => {
|
|
40
|
-
const prompt = inputs.join(' ');
|
|
41
|
-
const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
|
|
42
|
-
return buildTextToModel(prompt, model, flags.param);
|
|
43
|
-
});
|
|
44
|
-
add('image-to-model', 'single image → 3D model', 'single', async (client, inputs, flags) => {
|
|
45
|
-
const { value } = await resolveInputValue(client, inputs[0]);
|
|
46
|
-
const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
|
|
47
|
-
return buildImageToModel(value, model, flags.param);
|
|
48
|
-
});
|
|
49
|
-
add('multiview-to-model', '2–4 view images → 3D model', 'multi', async (client, inputs, flags) => {
|
|
50
|
-
const views = await assignViews(client, inputs);
|
|
51
|
-
const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
|
|
52
|
-
return buildMultiviewToModel(views, model, flags.param);
|
|
53
|
-
});
|
|
54
|
-
add('text-to-image', 'text → image (concept art / t_pose templates)', 'prompt', async (_c, inputs, flags) => buildTextToImage(inputs.join(' '), { ...(flags.model ? { model: flags.model } : {}), ...flags.param }));
|
|
55
|
-
add('image-to-image', 'image + prompt → edited image', 'single', async (client, inputs, flags) => {
|
|
56
|
-
const { value } = await resolveInputValue(client, inputs[0]);
|
|
57
|
-
const params = { ...(flags.model ? { model: flags.model } : {}), ...flags.param };
|
|
58
|
-
if (flags.prompt)
|
|
59
|
-
params.prompt = flags.prompt;
|
|
60
|
-
if (!params.prompt && !params.template) {
|
|
61
|
-
throw new UsageError('image-to-image needs --prompt or --param template=...');
|
|
62
|
-
}
|
|
63
|
-
return buildImageToImage(value, params);
|
|
64
|
-
});
|
|
65
|
-
add('image-to-multiview', 'single image → 4-view sheet', 'single', async (client, inputs, flags) => {
|
|
66
|
-
const { value } = await resolveInputValue(client, inputs[0]);
|
|
67
|
-
return buildImageToMultiview(value, flags.param);
|
|
68
|
-
});
|
|
69
|
-
add('edit-multiview', 'edit views of a multiview task', 'single', async (client, inputs, flags) => {
|
|
70
|
-
const { value } = await resolveInputValue(client, inputs[0]);
|
|
71
|
-
return buildEditMultiview(value, flags.param);
|
|
72
|
-
});
|
|
73
|
-
add('image-to-splat', 'image → gaussian splat', 'single', async (client, inputs, flags) => {
|
|
74
|
-
const { value } = await resolveInputValue(client, inputs[0]);
|
|
75
|
-
return buildImageToSplat(value, flags.param);
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
function numberParam(flags, key) {
|
|
79
|
-
const v = flags.param[key];
|
|
80
|
-
return typeof v === 'number' ? v : undefined;
|
|
81
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/** Bare `tripo` — interactive home: status, quick actions, continue last task. */
|
|
2
|
-
import * as p from '@clack/prompts';
|
|
3
|
-
import pc from 'picocolors';
|
|
4
|
-
import { loadConfig, resolveApiKey } from '../config/config.js';
|
|
5
|
-
import { readHistory } from '../config/history.js';
|
|
6
|
-
import { TripoClient } from '../core/client.js';
|
|
7
|
-
import { isInteractive, log } from '../core/output.js';
|
|
8
|
-
import { t } from '../i18n.js';
|
|
9
|
-
import { runLogin } from './login.js';
|
|
10
|
-
export function registerHomeAction(program) {
|
|
11
|
-
program.action(async () => {
|
|
12
|
-
if (!isInteractive()) {
|
|
13
|
-
program.outputHelp();
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
p.intro(pc.bold('tripo'));
|
|
17
|
-
const key = resolveApiKey(loadConfig());
|
|
18
|
-
if (!key) {
|
|
19
|
-
log(t({ zh: '还没有配置 API Key,先登录吧(新用户可领免费额度)', en: 'no API key yet — log in first (new accounts get free credits)' }));
|
|
20
|
-
await runLogin({});
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
let balanceLine = '';
|
|
24
|
-
try {
|
|
25
|
-
const balance = await new TripoClient().getBalance();
|
|
26
|
-
balanceLine = t({ zh: `余额 ${balance.balance} 积分`, en: `${balance.balance} credits` });
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
balanceLine = t({ zh: '余额获取失败', en: 'balance unavailable' });
|
|
30
|
-
}
|
|
31
|
-
const last = readHistory().filter((e) => e.status === 'success').pop();
|
|
32
|
-
log(`${balanceLine}${last ? t({ zh: ` · 上次任务 @${last.name}`, en: ` · last task @${last.name}` }) : ''}`);
|
|
33
|
-
const action = await p.select({
|
|
34
|
-
message: t({ zh: '想做什么?', en: 'What do you want to do?' }),
|
|
35
|
-
options: [
|
|
36
|
-
{ value: 'ai', label: t({ zh: '描述需求,让专家帮我定方案 (tripo ai)', en: 'describe what I want, experts plan it (tripo ai)' }) },
|
|
37
|
-
{ value: 'make', label: t({ zh: '直接生成 (tripo make ...)', en: 'generate directly (tripo make ...)' }) },
|
|
38
|
-
...(last ? [{ value: 'view', label: t({ zh: '看看上一个结果 (tripo view)', en: 'view the last result (tripo view)' }) }] : []),
|
|
39
|
-
{ value: 'topup', label: t({ zh: '充值 (tripo topup)', en: 'top up credits (tripo topup)' }) },
|
|
40
|
-
{ value: 'help', label: t({ zh: '看全部命令', en: 'show all commands' }) },
|
|
41
|
-
],
|
|
42
|
-
});
|
|
43
|
-
if (p.isCancel(action))
|
|
44
|
-
return;
|
|
45
|
-
switch (action) {
|
|
46
|
-
case 'ai':
|
|
47
|
-
await program.parseAsync(['ai'], { from: 'user' });
|
|
48
|
-
break;
|
|
49
|
-
case 'make': {
|
|
50
|
-
const promptText = await p.text({
|
|
51
|
-
message: t({ zh: '输入一句描述或文件路径', en: 'enter a description or file path' }),
|
|
52
|
-
});
|
|
53
|
-
if (p.isCancel(promptText) || !promptText.trim())
|
|
54
|
-
return;
|
|
55
|
-
await program.parseAsync(['make', promptText.trim()], { from: 'user' });
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
case 'view':
|
|
59
|
-
await program.parseAsync(['view'], { from: 'user' });
|
|
60
|
-
break;
|
|
61
|
-
case 'topup':
|
|
62
|
-
await program.parseAsync(['topup'], { from: 'user' });
|
|
63
|
-
break;
|
|
64
|
-
default:
|
|
65
|
-
program.outputHelp();
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Onboarding loop: `tripo login` (browser flow with paste fallback),
|
|
3
|
-
* `tripo logout`, `tripo whoami`. Key precedence: TRIPO_API_KEY env >
|
|
4
|
-
* ~/.tripo/config.json.
|
|
5
|
-
*/
|
|
6
|
-
import { randomBytes } from 'node:crypto';
|
|
7
|
-
import { createServer } from 'node:http';
|
|
8
|
-
import { hostname } from 'node:os';
|
|
9
|
-
import * as p from '@clack/prompts';
|
|
10
|
-
import { deleteApiKey, loadConfig, maskKey, REGION_DEFAULTS, resolveApiKey, resolvePlatformBaseUrl, updateConfig, } from '../config/config.js';
|
|
11
|
-
import { TripoClient } from '../core/client.js';
|
|
12
|
-
import { AuthError, CliError, UsageError } from '../core/errors.js';
|
|
13
|
-
import { emitResult, isInteractive, log, shouldOpen, success, warn } from '../core/output.js';
|
|
14
|
-
import { t } from '../i18n.js';
|
|
15
|
-
import { EXIT } from '../knowledge/error-catalog.js';
|
|
16
|
-
import { openExternal } from '../util/open.js';
|
|
17
|
-
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
18
|
-
export function registerAuthCommands(program) {
|
|
19
|
-
program
|
|
20
|
-
.command('login')
|
|
21
|
-
.description('sign in: browser flow (default) or paste an API key')
|
|
22
|
-
.option('--paste', 'paste an API key instead of using the browser')
|
|
23
|
-
.option('--key <key>', 'provide the API key directly (tsk_...)')
|
|
24
|
-
.option('--region <region>', 'ov (international) or cn (China mainland)')
|
|
25
|
-
.action(async (opts) => {
|
|
26
|
-
await runLogin(opts);
|
|
27
|
-
});
|
|
28
|
-
program
|
|
29
|
-
.command('logout')
|
|
30
|
-
.description('remove the locally stored API key')
|
|
31
|
-
.action(() => {
|
|
32
|
-
const config = loadConfig();
|
|
33
|
-
if (!config.api_key) {
|
|
34
|
-
log(t({ zh: '本地没有存储 API Key', en: 'no API key stored locally' }));
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
deleteApiKey();
|
|
38
|
-
success(t({ zh: '已删除本地 API Key', en: 'local API key removed' }));
|
|
39
|
-
}
|
|
40
|
-
log(t({
|
|
41
|
-
zh: `如需吊销 Key 本身,请前往 ${resolvePlatformBaseUrl()} 的 API Keys 页面`,
|
|
42
|
-
en: `to revoke the key itself, visit the API Keys page at ${resolvePlatformBaseUrl()}`,
|
|
43
|
-
}));
|
|
44
|
-
});
|
|
45
|
-
program
|
|
46
|
-
.command('whoami')
|
|
47
|
-
.description('show the active key (masked), region, and balance')
|
|
48
|
-
.action(async () => {
|
|
49
|
-
const config = loadConfig();
|
|
50
|
-
const key = resolveApiKey(config);
|
|
51
|
-
if (!key)
|
|
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';
|
|
54
|
-
const client = new TripoClient();
|
|
55
|
-
const balance = await client.getBalance();
|
|
56
|
-
emitResult({
|
|
57
|
-
key: maskKey(key),
|
|
58
|
-
source,
|
|
59
|
-
region: config.region ?? 'ov',
|
|
60
|
-
api_base_url: client.baseUrl,
|
|
61
|
-
balance: balance.balance,
|
|
62
|
-
frozen: balance.frozen,
|
|
63
|
-
}, [
|
|
64
|
-
`key ${maskKey(key)} (${source})`,
|
|
65
|
-
`region ${config.region ?? 'ov'}`,
|
|
66
|
-
`api ${client.baseUrl}`,
|
|
67
|
-
t({ zh: `余额 ${balance.balance} 积分(冻结 ${balance.frozen})`, en: `balance ${balance.balance} credits (${balance.frozen} frozen)` }),
|
|
68
|
-
].join('\n'));
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
export async function runLogin(opts) {
|
|
72
|
-
let region = normalizeRegion(opts.region);
|
|
73
|
-
if (opts.key) {
|
|
74
|
-
region ??= 'ov';
|
|
75
|
-
await validateAndSave(opts.key, region);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (!isInteractive()) {
|
|
79
|
-
throw new UsageError(t({
|
|
80
|
-
zh: '非交互环境请用 TRIPO_API_KEY 环境变量或 tripo login --key tsk_...',
|
|
81
|
-
en: 'in non-interactive environments use the TRIPO_API_KEY env var or "tripo login --key tsk_..."',
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
p.intro('tripo login');
|
|
85
|
-
if (!region) {
|
|
86
|
-
const answer = await p.select({
|
|
87
|
-
message: t({ zh: '选择服务区域', en: 'Choose your region' }),
|
|
88
|
-
options: [
|
|
89
|
-
{ value: 'ov', label: t({ zh: '海外 (api: openapi.tripo3d.ai)', en: 'International (openapi.tripo3d.ai)' }) },
|
|
90
|
-
{ value: 'cn', label: t({ zh: '中国大陆 (api: openapi.tripo3d.com)', en: 'China mainland (openapi.tripo3d.com)' }) },
|
|
91
|
-
],
|
|
92
|
-
});
|
|
93
|
-
if (p.isCancel(answer))
|
|
94
|
-
throw new CliError('cancelled', EXIT.FATAL);
|
|
95
|
-
region = answer;
|
|
96
|
-
}
|
|
97
|
-
const key = opts.paste ? await pasteFlow(region) : await browserFlow(region);
|
|
98
|
-
await validateAndSave(key, region);
|
|
99
|
-
p.outro(t({
|
|
100
|
-
zh: '登录完成!试试: tripo make "一只可爱的机器人"',
|
|
101
|
-
en: 'logged in! try: tripo make "a cute robot"',
|
|
102
|
-
}));
|
|
103
|
-
}
|
|
104
|
-
function normalizeRegion(value) {
|
|
105
|
-
if (value === undefined)
|
|
106
|
-
return undefined;
|
|
107
|
-
if (value === 'ov' || value === 'cn')
|
|
108
|
-
return value;
|
|
109
|
-
throw new UsageError('--region must be ov or cn');
|
|
110
|
-
}
|
|
111
|
-
async function pasteFlow(region) {
|
|
112
|
-
const platform = platformUrlFor(region);
|
|
113
|
-
log(t({
|
|
114
|
-
zh: `在 ${platform} 的「API Keys」页面创建 Key(新用户注册可领免费额度)`,
|
|
115
|
-
en: `create a key on the "API Keys" page at ${platform} (new accounts get free credits)`,
|
|
116
|
-
}));
|
|
117
|
-
const answer = await p.password({
|
|
118
|
-
message: t({ zh: '粘贴你的 API Key (tsk_...)', en: 'Paste your API key (tsk_...)' }),
|
|
119
|
-
validate: (v) => (v && v.trim().startsWith('tsk_') ? undefined : t({ zh: 'Key 应以 tsk_ 开头', en: 'keys start with tsk_' })),
|
|
120
|
-
});
|
|
121
|
-
if (p.isCancel(answer))
|
|
122
|
-
throw new CliError('cancelled', EXIT.FATAL);
|
|
123
|
-
return answer.trim();
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Browser flow (gh-auth style): open <platform>/cli-auth?state=...&callback=…
|
|
127
|
-
* and wait for the page to POST/GET the new key back to localhost. Races
|
|
128
|
-
* against manual paste so the flow still works before the platform page
|
|
129
|
-
* ships (graceful degradation, zero backend changes required).
|
|
130
|
-
*/
|
|
131
|
-
async function browserFlow(region) {
|
|
132
|
-
const platform = platformUrlFor(region);
|
|
133
|
-
const state = randomBytes(16).toString('hex');
|
|
134
|
-
const { server, port, keyPromise } = await startCallbackServer(state);
|
|
135
|
-
try {
|
|
136
|
-
const authUrl = `${platform}/cli-auth?state=${state}&callback=${encodeURIComponent(`http://127.0.0.1:${port}/callback`)}&name=${encodeURIComponent(`CLI - ${hostname()}`)}`;
|
|
137
|
-
if (shouldOpen()) {
|
|
138
|
-
openExternal(authUrl);
|
|
139
|
-
log(t({ zh: `已打开浏览器登录页(未弹出请手动访问):\n ${authUrl}`, en: `opened the browser login page (visit manually if it didn't open):\n ${authUrl}` }));
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
log(t({ zh: `请访问: ${authUrl}`, en: `visit: ${authUrl}` }));
|
|
143
|
-
}
|
|
144
|
-
log(t({
|
|
145
|
-
zh: '等待浏览器完成登录…(也可以直接在下方粘贴 Key)',
|
|
146
|
-
en: 'waiting for the browser… (or paste a key below instead)',
|
|
147
|
-
}));
|
|
148
|
-
const pastePromise = p
|
|
149
|
-
.password({
|
|
150
|
-
message: t({ zh: '或直接粘贴 API Key (tsk_...)', en: 'or paste an API key (tsk_...)' }),
|
|
151
|
-
})
|
|
152
|
-
.then((v) => {
|
|
153
|
-
if (p.isCancel(v))
|
|
154
|
-
throw new CliError('cancelled', EXIT.FATAL);
|
|
155
|
-
const trimmed = v.trim();
|
|
156
|
-
if (!trimmed.startsWith('tsk_'))
|
|
157
|
-
throw new UsageError(t({ zh: 'Key 应以 tsk_ 开头', en: 'keys start with tsk_' }));
|
|
158
|
-
return trimmed;
|
|
159
|
-
});
|
|
160
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new CliError(t({ zh: '登录超时(5 分钟)', en: 'login timed out (5 minutes)' }), EXIT.AUTH)), LOGIN_TIMEOUT_MS));
|
|
161
|
-
return await Promise.race([keyPromise, pastePromise, timeoutPromise]);
|
|
162
|
-
}
|
|
163
|
-
finally {
|
|
164
|
-
server.close();
|
|
165
|
-
// Ensure the pending clack prompt does not hold the process open.
|
|
166
|
-
process.stdin.pause();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function startCallbackServer(state) {
|
|
170
|
-
let resolveKey;
|
|
171
|
-
const keyPromise = new Promise((resolve) => (resolveKey = resolve));
|
|
172
|
-
const server = createServer((req, res) => {
|
|
173
|
-
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
174
|
-
if (url.pathname !== '/callback') {
|
|
175
|
-
res.writeHead(404);
|
|
176
|
-
res.end();
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const gotState = url.searchParams.get('state');
|
|
180
|
-
const key = url.searchParams.get('key') ?? url.searchParams.get('api_key');
|
|
181
|
-
if (gotState !== state || !key || !key.startsWith('tsk_')) {
|
|
182
|
-
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
183
|
-
res.end('<h3>Invalid login callback. Return to the terminal and try again.</h3>');
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
187
|
-
res.end('<h3>✓ Tripo CLI login complete. You can close this tab and return to the terminal.</h3>');
|
|
188
|
-
resolveKey(key);
|
|
189
|
-
});
|
|
190
|
-
return new Promise((resolve, reject) => {
|
|
191
|
-
server.once('error', reject);
|
|
192
|
-
server.listen(0, '127.0.0.1', () => {
|
|
193
|
-
const address = server.address();
|
|
194
|
-
const port = typeof address === 'object' && address ? address.port : 0;
|
|
195
|
-
resolve({ server, port, keyPromise });
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
async function validateAndSave(key, region) {
|
|
200
|
-
const config = updateConfig({ region });
|
|
201
|
-
const client = new TripoClient({ apiKey: key });
|
|
202
|
-
try {
|
|
203
|
-
const balance = await client.getBalance();
|
|
204
|
-
updateConfig({ ...config, api_key: key, region });
|
|
205
|
-
success(t({
|
|
206
|
-
zh: `Key 验证通过,当前余额 ${balance.balance} 积分(已保存到 ~/.tripo/config.json,权限 0600)`,
|
|
207
|
-
en: `key verified; balance ${balance.balance} credits (saved to ~/.tripo/config.json, mode 0600)`,
|
|
208
|
-
}));
|
|
209
|
-
if (Number(balance.balance) === 0) {
|
|
210
|
-
warn(t({
|
|
211
|
-
zh: '余额为 0:新账号可在控制台领取免费额度,或运行 tripo topup 充值',
|
|
212
|
-
en: 'balance is 0: claim free credits in the console for new accounts, or run "tripo topup"',
|
|
213
|
-
}));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
catch (err) {
|
|
217
|
-
if (err instanceof CliError && err.exitCode === EXIT.AUTH) {
|
|
218
|
-
throw new AuthError(t({ zh: 'Key 验证失败:无效或已被删除', en: 'key validation failed: invalid or deleted' }));
|
|
219
|
-
}
|
|
220
|
-
throw err;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
function platformUrlFor(region) {
|
|
224
|
-
return process.env.TRIPO_PLATFORM_BASE_URL || REGION_DEFAULTS[region].platform;
|
|
225
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `tripo make` — the magic command. Give it anything (a sentence, an image,
|
|
3
|
-
* 2–4 views, a model file, or @last/task id) and it picks the endpoint, the
|
|
4
|
-
* model, the parameters, runs the chain, and downloads the artifacts.
|
|
5
|
-
*/
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
|
-
import { UsageError } from '../core/errors.js';
|
|
8
|
-
import { emitResult, isInteractive, log, warn } from '../core/output.js';
|
|
9
|
-
import { t } from '../i18n.js';
|
|
10
|
-
import { scenarioIds } from '../knowledge/scenarios.js';
|
|
11
|
-
import { executeMake, planMake } from '../pipeline/make.js';
|
|
12
|
-
import { newClient, parseChainFlag, printTaskResult, withTaskOptions } from './shared.js';
|
|
13
|
-
import { printInlineImage } from '../util/term-image.js';
|
|
14
|
-
export function registerMakeCommand(program) {
|
|
15
|
-
const cmd = program
|
|
16
|
-
.command('make')
|
|
17
|
-
.description('turn text / images / a model file / a task ref into 3D artifacts (the magic command)')
|
|
18
|
-
.argument('<input...>', 'prompt text, image file(s), model file, URL, task id, or @last')
|
|
19
|
-
.option('--for <scenario>', `scenario preset: ${scenarioIds().join('|')}`)
|
|
20
|
-
.option('--then <steps>', 'processing chain, e.g. "texture,rig,convert:fbx"')
|
|
21
|
-
.option('--model <model>', 'force model version (tripo-v3.1 / tripo-p1 / ...)')
|
|
22
|
-
.option('-n, --candidates <n>', 'generate N candidates in parallel', (v) => Number(v), 1)
|
|
23
|
-
.option('--seed <n>', 'model_seed for reproducible geometry', (v) => Number(v));
|
|
24
|
-
withTaskOptions(cmd);
|
|
25
|
-
cmd.action(async (inputs, flags) => {
|
|
26
|
-
const result = await runMake(inputs, flags);
|
|
27
|
-
emitMakeOutcome(result);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
export async function runMake(inputs, flags) {
|
|
31
|
-
const client = newClient();
|
|
32
|
-
const params = { ...flags.param };
|
|
33
|
-
if (flags.seed !== undefined)
|
|
34
|
-
params.model_seed = flags.seed;
|
|
35
|
-
const options = {
|
|
36
|
-
inputs,
|
|
37
|
-
scenario: flags.for,
|
|
38
|
-
chain: parseChainFlag(flags.then),
|
|
39
|
-
model: flags.model,
|
|
40
|
-
params,
|
|
41
|
-
name: flags.name,
|
|
42
|
-
outDir: flags.out,
|
|
43
|
-
noWait: flags.wait === false,
|
|
44
|
-
noDownload: flags.download === false,
|
|
45
|
-
notify: flags.notify,
|
|
46
|
-
timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
|
|
47
|
-
};
|
|
48
|
-
const n = flags.candidates;
|
|
49
|
-
if (n <= 1) {
|
|
50
|
-
const plan = await planMake(client, options);
|
|
51
|
-
const outcome = await executeMake(client, plan, options);
|
|
52
|
-
return { results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly };
|
|
53
|
-
}
|
|
54
|
-
// -n candidates: N parallel generations with distinct seeds.
|
|
55
|
-
if (n > 4)
|
|
56
|
-
throw new UsageError('at most 4 candidates per run (-n 4)');
|
|
57
|
-
const basePlan = await planMake(client, options);
|
|
58
|
-
if (!basePlan.generation) {
|
|
59
|
-
throw new UsageError('-n only applies to generation inputs (text/image), not task references');
|
|
60
|
-
}
|
|
61
|
-
const chain = basePlan.chain;
|
|
62
|
-
const interactive = isInteractive();
|
|
63
|
-
if (chain.length > 0 && !interactive) {
|
|
64
|
-
throw new UsageError('-n with --then needs an interactive terminal to pick the winner; run the chain afterwards with "tripo make <task-id> --then ..."');
|
|
65
|
-
}
|
|
66
|
-
log(t({ zh: `并行生成 ${n} 个候选…`, en: `generating ${n} candidates in parallel…` }));
|
|
67
|
-
const seeds = Array.from({ length: n }, () => Math.floor(Math.random() * 2 ** 31));
|
|
68
|
-
const runs = seeds.map((seed, i) => (async () => {
|
|
69
|
-
const candidateOptions = {
|
|
70
|
-
...options,
|
|
71
|
-
chain: [],
|
|
72
|
-
params: { ...params, model_seed: seed },
|
|
73
|
-
name: `${flags.name ?? inputs[0] ?? 'make'}-c${i + 1}`,
|
|
74
|
-
};
|
|
75
|
-
const plan = await planMake(client, candidateOptions);
|
|
76
|
-
plan.chain = [];
|
|
77
|
-
return executeMake(client, plan, candidateOptions);
|
|
78
|
-
})());
|
|
79
|
-
const settled = await Promise.allSettled(runs);
|
|
80
|
-
const results = [];
|
|
81
|
-
for (const s of settled) {
|
|
82
|
-
if (s.status === 'fulfilled')
|
|
83
|
-
results.push(s.value.primary);
|
|
84
|
-
else
|
|
85
|
-
warn(String(s.reason instanceof Error ? s.reason.message : s.reason));
|
|
86
|
-
}
|
|
87
|
-
if (results.length === 0)
|
|
88
|
-
throw new UsageError('all candidates failed');
|
|
89
|
-
for (const r of results) {
|
|
90
|
-
if (r.preview)
|
|
91
|
-
printInlineImage(r.preview);
|
|
92
|
-
}
|
|
93
|
-
let chainResults = [];
|
|
94
|
-
let stoppedEarly;
|
|
95
|
-
if (chain.length > 0 && interactive) {
|
|
96
|
-
const pick = await p.select({
|
|
97
|
-
message: t({ zh: '选择一个候选继续加工链', en: 'pick a candidate to continue the chain' }),
|
|
98
|
-
options: results.map((r, i) => ({
|
|
99
|
-
value: r.task_id,
|
|
100
|
-
label: `#${i + 1} ${r.task_id.slice(0, 8)} ${r.output_dir ?? ''}`,
|
|
101
|
-
})),
|
|
102
|
-
});
|
|
103
|
-
if (!p.isCancel(pick)) {
|
|
104
|
-
const winner = results.find((r) => r.task_id === pick);
|
|
105
|
-
const { runChain } = await import('../pipeline/runner.js');
|
|
106
|
-
const chainRun = await runChain({
|
|
107
|
-
client,
|
|
108
|
-
chain,
|
|
109
|
-
sourceTaskId: winner.task_id,
|
|
110
|
-
sourceType: winner.type,
|
|
111
|
-
namePrefix: flags.name ?? 'make',
|
|
112
|
-
outDir: flags.out,
|
|
113
|
-
timeoutMs: options.timeoutMs,
|
|
114
|
-
});
|
|
115
|
-
chainResults = chainRun.results;
|
|
116
|
-
stoppedEarly = chainRun.stoppedEarly;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return { results, chainResults, stoppedEarly };
|
|
120
|
-
}
|
|
121
|
-
export function emitMakeOutcome(outcome) {
|
|
122
|
-
const finalChain = outcome.chainResults[outcome.chainResults.length - 1];
|
|
123
|
-
if (outcome.results.length === 1) {
|
|
124
|
-
const primary = outcome.results[0];
|
|
125
|
-
const merged = finalChain
|
|
126
|
-
? { ...finalChain, chain: undefined, source_task_id: primary.task_id }
|
|
127
|
-
: primary;
|
|
128
|
-
if (outcome.chainResults.length > 0)
|
|
129
|
-
merged.chain = outcome.chainResults;
|
|
130
|
-
if (outcome.stoppedEarly)
|
|
131
|
-
merged.stopped_early = outcome.stoppedEarly;
|
|
132
|
-
printTaskResult(merged);
|
|
133
|
-
if (merged.preview)
|
|
134
|
-
printInlineImage(merged.preview);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
const human = outcome.results
|
|
138
|
-
.map((r, i) => `#${i + 1} ${r.task_id} ${r.output_dir ?? r.status}`)
|
|
139
|
-
.join('\n');
|
|
140
|
-
emitResult({ candidates: outcome.results, chain: outcome.chainResults }, human);
|
|
141
|
-
}
|
|
142
|
-
}
|