tripo-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/ai/experts.js +115 -0
- package/dist/ai/llm.js +88 -0
- package/dist/ai/plan-card.js +79 -0
- package/dist/ai/wizard.js +105 -0
- package/dist/cli.js +7 -0
- package/dist/commands/account.js +27 -0
- package/dist/commands/ai.js +114 -0
- package/dist/commands/batch.js +148 -0
- package/dist/commands/completion.js +84 -0
- package/dist/commands/config-cmd.js +84 -0
- package/dist/commands/docs.js +30 -0
- package/dist/commands/doctor.js +102 -0
- package/dist/commands/generate.js +81 -0
- package/dist/commands/home.js +68 -0
- package/dist/commands/login.js +309 -0
- package/dist/commands/make.js +142 -0
- package/dist/commands/mcp.js +194 -0
- package/dist/commands/process-groups.js +166 -0
- package/dist/commands/shared.js +75 -0
- package/dist/commands/task.js +128 -0
- package/dist/commands/topup.js +61 -0
- package/dist/commands/view.js +97 -0
- package/dist/config/config.js +99 -0
- package/dist/config/context.js +44 -0
- package/dist/config/history.js +104 -0
- package/dist/core/client.js +129 -0
- package/dist/core/device-auth.js +141 -0
- package/dist/core/download.js +107 -0
- package/dist/core/errors.js +64 -0
- package/dist/core/output.js +72 -0
- package/dist/core/region.js +45 -0
- package/dist/core/requests.js +197 -0
- package/dist/core/task-service.js +167 -0
- package/dist/core/upload.js +42 -0
- package/dist/core/viewer-assets.js +24 -0
- package/dist/core/viewer.js +59 -0
- package/dist/core/watch.js +42 -0
- package/dist/i18n.js +19 -0
- package/dist/knowledge/chains.js +157 -0
- package/dist/knowledge/error-catalog.js +91 -0
- package/dist/knowledge/models.js +158 -0
- package/dist/knowledge/params.js +203 -0
- package/dist/knowledge/scenarios.js +112 -0
- package/dist/pipeline/make.js +233 -0
- package/dist/pipeline/runner.js +73 -0
- package/dist/pipeline/stdin.js +40 -0
- package/dist/program.js +147 -0
- package/dist/src/ai/experts.js +115 -0
- package/dist/src/ai/llm.js +88 -0
- package/dist/src/ai/plan-card.js +79 -0
- package/dist/src/ai/wizard.js +104 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/account.js +27 -0
- package/dist/src/commands/ai.js +113 -0
- package/dist/src/commands/batch.js +148 -0
- package/dist/src/commands/completion.js +84 -0
- package/dist/src/commands/config-cmd.js +84 -0
- package/dist/src/commands/docs.js +30 -0
- package/dist/src/commands/doctor.js +94 -0
- package/dist/src/commands/generate.js +81 -0
- package/dist/src/commands/home.js +68 -0
- package/dist/src/commands/login.js +225 -0
- package/dist/src/commands/make.js +142 -0
- package/dist/src/commands/mcp.js +194 -0
- package/dist/src/commands/process-groups.js +164 -0
- package/dist/src/commands/shared.js +75 -0
- package/dist/src/commands/task.js +127 -0
- package/dist/src/commands/topup.js +61 -0
- package/dist/src/commands/view.js +97 -0
- package/dist/src/config/config.js +79 -0
- package/dist/src/config/context.js +44 -0
- package/dist/src/config/history.js +104 -0
- package/dist/src/core/client.js +120 -0
- package/dist/src/core/download.js +107 -0
- package/dist/src/core/errors.js +64 -0
- package/dist/src/core/output.js +72 -0
- package/dist/src/core/requests.js +169 -0
- package/dist/src/core/task-service.js +161 -0
- package/dist/src/core/upload.js +36 -0
- package/dist/src/core/viewer-assets.js +24 -0
- package/dist/src/core/viewer.js +59 -0
- package/dist/src/core/watch.js +41 -0
- package/dist/src/i18n.js +19 -0
- package/dist/src/knowledge/chains.js +152 -0
- package/dist/src/knowledge/error-catalog.js +91 -0
- package/dist/src/knowledge/models.js +110 -0
- package/dist/src/knowledge/params.js +174 -0
- package/dist/src/knowledge/scenarios.js +112 -0
- package/dist/src/pipeline/make.js +233 -0
- package/dist/src/pipeline/runner.js +73 -0
- package/dist/src/pipeline/stdin.js +40 -0
- package/dist/src/program.js +147 -0
- package/dist/src/types.js +2 -0
- package/dist/src/util/input-detect.js +92 -0
- package/dist/src/util/notify.js +17 -0
- package/dist/src/util/open.js +18 -0
- package/dist/src/util/spinner.js +62 -0
- package/dist/src/util/term-image.js +28 -0
- package/dist/src/version.js +10 -0
- package/dist/types.js +2 -0
- package/dist/util/input-detect.js +93 -0
- package/dist/util/notify.js +17 -0
- package/dist/util/open.js +18 -0
- package/dist/util/spinner.js +62 -0
- package/dist/util/term-image.js +28 -0
- package/dist/version.js +10 -0
- package/package.json +63 -0
- package/skill/SKILL.md +70 -0
- package/skill/commands/account.md +41 -0
- package/skill/commands/batch.md +33 -0
- package/skill/commands/generate.md +34 -0
- package/skill/commands/make.md +47 -0
- package/skill/commands/process.md +50 -0
- package/skill/commands/task.md +27 -0
- package/skill/commands/view.md +25 -0
- package/skill/common-errors.md +38 -0
- package/skill/examples/animation.md +33 -0
- package/skill/examples/ar-web.md +23 -0
- package/skill/examples/film.md +22 -0
- package/skill/examples/game-asset.md +35 -0
- package/skill/examples/pipes.md +39 -0
- package/skill/examples/print.md +31 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tripo batch run <manifest.yaml>` — bulk production with concurrency,
|
|
3
|
+
* retries, and resumable state (skip jobs already succeeded on re-run).
|
|
4
|
+
*
|
|
5
|
+
* Manifest:
|
|
6
|
+
* concurrency: 2
|
|
7
|
+
* defaults: { for: game-mobile, then: "texture,convert:fbx", out: ./assets }
|
|
8
|
+
* jobs:
|
|
9
|
+
* - input: "a sword"
|
|
10
|
+
* - input: cat.png
|
|
11
|
+
* for: print
|
|
12
|
+
* - inputs: [hero-front.png, hero-back.png]
|
|
13
|
+
* name: hero
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
import { parse as parseYaml } from 'yaml';
|
|
18
|
+
import { UsageError } from '../core/errors.js';
|
|
19
|
+
import { emitResult, errorLine, log, success } from '../core/output.js';
|
|
20
|
+
import { t } from '../i18n.js';
|
|
21
|
+
import { parseThenExpression } from '../knowledge/chains.js';
|
|
22
|
+
import { executeMake, planMake } from '../pipeline/make.js';
|
|
23
|
+
import { newClient } from './shared.js';
|
|
24
|
+
export function registerBatchCommand(program) {
|
|
25
|
+
const batch = program.command('batch').description('bulk pipelines from a YAML manifest');
|
|
26
|
+
batch
|
|
27
|
+
.command('run <manifest>')
|
|
28
|
+
.description('run all jobs (resumable: finished jobs are skipped on re-run)')
|
|
29
|
+
.option('--concurrency <n>', 'parallel jobs (default from manifest or 2)', (v) => Number(v))
|
|
30
|
+
.option('--retries <n>', 'retries per failed job', (v) => Number(v), 1)
|
|
31
|
+
.option('--fresh', 'ignore previous state and re-run everything')
|
|
32
|
+
.action(async (manifestPath, opts) => {
|
|
33
|
+
const absPath = resolve(manifestPath);
|
|
34
|
+
const manifest = loadManifest(absPath);
|
|
35
|
+
const statePath = `${absPath}.state.json`;
|
|
36
|
+
const state = opts.fresh ? { jobs: {} } : loadState(statePath);
|
|
37
|
+
const jobs = manifest.jobs.map((job, index) => ({
|
|
38
|
+
key: job.name ?? `job-${index + 1}`,
|
|
39
|
+
spec: { ...manifest.defaults, ...job },
|
|
40
|
+
}));
|
|
41
|
+
// Duplicate names would corrupt state bookkeeping — fail fast.
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const { key } of jobs) {
|
|
44
|
+
if (seen.has(key))
|
|
45
|
+
throw new UsageError(`duplicate job name "${key}" in manifest`);
|
|
46
|
+
seen.add(key);
|
|
47
|
+
}
|
|
48
|
+
const concurrency = Math.max(1, opts.concurrency ?? manifest.concurrency ?? 2);
|
|
49
|
+
const queue = jobs.filter(({ key }) => state.jobs[key]?.status !== 'success');
|
|
50
|
+
const skipped = jobs.length - queue.length;
|
|
51
|
+
if (skipped > 0) {
|
|
52
|
+
log(t({ zh: `跳过 ${skipped} 个已完成任务(断点续跑)`, en: `skipping ${skipped} already-finished job(s) (resume)` }));
|
|
53
|
+
}
|
|
54
|
+
log(t({ zh: `批量执行 ${queue.length} 个任务,并发 ${concurrency}`, en: `running ${queue.length} job(s), concurrency ${concurrency}` }));
|
|
55
|
+
const client = newClient();
|
|
56
|
+
let cursor = 0;
|
|
57
|
+
const runNext = async () => {
|
|
58
|
+
for (;;) {
|
|
59
|
+
const item = queue[cursor++];
|
|
60
|
+
if (!item)
|
|
61
|
+
return;
|
|
62
|
+
const jobState = state.jobs[item.key] ?? { status: 'pending', attempts: 0 };
|
|
63
|
+
state.jobs[item.key] = jobState;
|
|
64
|
+
const maxAttempts = 1 + opts.retries;
|
|
65
|
+
for (let attempt = jobState.attempts + 1; attempt <= maxAttempts; attempt++) {
|
|
66
|
+
jobState.attempts = attempt;
|
|
67
|
+
try {
|
|
68
|
+
const outcome = await runJob(client, item.key, item.spec);
|
|
69
|
+
jobState.status = 'success';
|
|
70
|
+
jobState.task_id = outcome.taskId;
|
|
71
|
+
jobState.output_dir = outcome.outputDir;
|
|
72
|
+
delete jobState.error;
|
|
73
|
+
success(`[${item.key}] done${outcome.outputDir ? ` → ${outcome.outputDir}` : ''}`);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
jobState.status = 'failed';
|
|
79
|
+
jobState.error = message;
|
|
80
|
+
errorLine(`[${item.key}] attempt ${attempt}/${maxAttempts} failed: ${message}`);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
saveState(statePath, state);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, runNext));
|
|
89
|
+
const summary = {
|
|
90
|
+
total: jobs.length,
|
|
91
|
+
success: Object.values(state.jobs).filter((j) => j.status === 'success').length,
|
|
92
|
+
failed: Object.values(state.jobs).filter((j) => j.status === 'failed').length,
|
|
93
|
+
state_file: statePath,
|
|
94
|
+
jobs: state.jobs,
|
|
95
|
+
};
|
|
96
|
+
const failed = summary.failed;
|
|
97
|
+
emitResult(summary, t({
|
|
98
|
+
zh: `批量完成: 成功 ${summary.success}/${summary.total}${failed ? `,失败 ${failed}(重跑同一命令续跑)` : ''}`,
|
|
99
|
+
en: `batch finished: ${summary.success}/${summary.total} succeeded${failed ? `, ${failed} failed (re-run the same command to resume)` : ''}`,
|
|
100
|
+
}));
|
|
101
|
+
if (failed > 0)
|
|
102
|
+
process.exitCode = 6;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async function runJob(client, key, spec) {
|
|
106
|
+
const inputs = spec.inputs ?? (spec.input !== undefined ? [spec.input] : []);
|
|
107
|
+
if (inputs.length === 0)
|
|
108
|
+
throw new UsageError(`job "${key}" has no input`);
|
|
109
|
+
const chain = spec.then ? parseThenExpression(spec.then) : undefined;
|
|
110
|
+
const options = {
|
|
111
|
+
inputs,
|
|
112
|
+
scenario: spec.for,
|
|
113
|
+
chain,
|
|
114
|
+
model: spec.model,
|
|
115
|
+
params: spec.params ?? {},
|
|
116
|
+
name: spec.name ?? key,
|
|
117
|
+
outDir: spec.out,
|
|
118
|
+
};
|
|
119
|
+
const plan = await planMake(client, options);
|
|
120
|
+
const outcome = await executeMake(client, plan, options);
|
|
121
|
+
const finalResult = outcome.chainResults[outcome.chainResults.length - 1] ?? outcome.primary;
|
|
122
|
+
return { taskId: finalResult.task_id, outputDir: finalResult.output_dir ?? outcome.primary.output_dir };
|
|
123
|
+
}
|
|
124
|
+
function loadManifest(path) {
|
|
125
|
+
let raw;
|
|
126
|
+
try {
|
|
127
|
+
raw = readFileSync(path, 'utf8');
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new UsageError(`manifest not found: ${path}`);
|
|
131
|
+
}
|
|
132
|
+
const doc = parseYaml(raw);
|
|
133
|
+
if (!doc || !Array.isArray(doc.jobs) || doc.jobs.length === 0) {
|
|
134
|
+
throw new UsageError('manifest must contain a non-empty "jobs" list');
|
|
135
|
+
}
|
|
136
|
+
return doc;
|
|
137
|
+
}
|
|
138
|
+
function loadState(path) {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return { jobs: {} };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function saveState(path, state) {
|
|
147
|
+
writeFileSync(path, JSON.stringify(state, null, 2) + '\n');
|
|
148
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** `tripo completion <shell>` — static shell completion scripts. */
|
|
2
|
+
import { UsageError } from '../core/errors.js';
|
|
3
|
+
const COMMANDS = [
|
|
4
|
+
'login', 'logout', 'whoami', 'balance', 'usage', 'topup',
|
|
5
|
+
'make', 'view', 'redo', 'ai',
|
|
6
|
+
'generate', 'model', 'anim', 'mesh', 'task', 'files', 'batch', 'history',
|
|
7
|
+
'config', 'doctor', 'docs', 'completion', 'mcp',
|
|
8
|
+
];
|
|
9
|
+
const SUBS = {
|
|
10
|
+
generate: ['text-to-model', 'image-to-model', 'multiview-to-model', 'text-to-image', 'image-to-image', 'image-to-multiview', 'edit-multiview', 'image-to-splat'],
|
|
11
|
+
model: ['refine', 'texture', 'stylize', 'convert', 'import'],
|
|
12
|
+
anim: ['check', 'rig', 'retarget'],
|
|
13
|
+
mesh: ['segment', 'complete', 'decimate', 'smartsegment'],
|
|
14
|
+
task: ['get', 'list', 'watch'],
|
|
15
|
+
files: ['upload'],
|
|
16
|
+
batch: ['run'],
|
|
17
|
+
config: ['list', 'set', 'unset', 'context'],
|
|
18
|
+
};
|
|
19
|
+
export function registerCompletionCommand(program) {
|
|
20
|
+
program
|
|
21
|
+
.command('completion <shell>')
|
|
22
|
+
.description('print a completion script (bash|zsh|fish); e.g. tripo completion zsh >> ~/.zshrc')
|
|
23
|
+
.action((shell) => {
|
|
24
|
+
switch (shell) {
|
|
25
|
+
case 'bash':
|
|
26
|
+
process.stdout.write(bashScript());
|
|
27
|
+
break;
|
|
28
|
+
case 'zsh':
|
|
29
|
+
process.stdout.write(zshScript());
|
|
30
|
+
break;
|
|
31
|
+
case 'fish':
|
|
32
|
+
process.stdout.write(fishScript());
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
throw new UsageError('supported shells: bash, zsh, fish');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function bashScript() {
|
|
40
|
+
const cases = Object.entries(SUBS)
|
|
41
|
+
.map(([cmd, subs]) => ` ${cmd}) COMPREPLY=($(compgen -W "${subs.join(' ')}" -- "$cur"));;`)
|
|
42
|
+
.join('\n');
|
|
43
|
+
return `# tripo bash completion
|
|
44
|
+
_tripo_completions() {
|
|
45
|
+
local cur prev
|
|
46
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
47
|
+
prev="\${COMP_WORDS[1]}"
|
|
48
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
49
|
+
COMPREPLY=($(compgen -W "${COMMANDS.join(' ')}" -- "$cur"))
|
|
50
|
+
return
|
|
51
|
+
fi
|
|
52
|
+
case "$prev" in
|
|
53
|
+
${cases}
|
|
54
|
+
esac
|
|
55
|
+
}
|
|
56
|
+
complete -F _tripo_completions tripo
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
function zshScript() {
|
|
60
|
+
return `# tripo zsh completion
|
|
61
|
+
autoload -Uz compinit && compinit
|
|
62
|
+
_tripo() {
|
|
63
|
+
local -a cmds
|
|
64
|
+
cmds=(${COMMANDS.join(' ')})
|
|
65
|
+
if (( CURRENT == 2 )); then
|
|
66
|
+
compadd -a cmds
|
|
67
|
+
else
|
|
68
|
+
case $words[2] in
|
|
69
|
+
${Object.entries(SUBS)
|
|
70
|
+
.map(([cmd, subs]) => ` ${cmd}) compadd ${subs.join(' ')};;`)
|
|
71
|
+
.join('\n')}
|
|
72
|
+
esac
|
|
73
|
+
fi
|
|
74
|
+
}
|
|
75
|
+
compdef _tripo tripo
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
function fishScript() {
|
|
79
|
+
const lines = [
|
|
80
|
+
`complete -c tripo -f -n '__fish_use_subcommand' -a '${COMMANDS.join(' ')}'`,
|
|
81
|
+
...Object.entries(SUBS).map(([cmd, subs]) => `complete -c tripo -f -n '__fish_seen_subcommand_from ${cmd}' -a '${subs.join(' ')}'`),
|
|
82
|
+
];
|
|
83
|
+
return lines.join('\n') + '\n';
|
|
84
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** `tripo config` — global config + directory context management. */
|
|
2
|
+
import { loadConfig, maskKey, saveConfig } from '../config/config.js';
|
|
3
|
+
import { findContextFile, loadContext, updateContext } from '../config/context.js';
|
|
4
|
+
import { UsageError } from '../core/errors.js';
|
|
5
|
+
import { emitResult, success } from '../core/output.js';
|
|
6
|
+
import { coerceValue } from './shared.js';
|
|
7
|
+
const SETTABLE = new Set([
|
|
8
|
+
'region',
|
|
9
|
+
'api_base_url',
|
|
10
|
+
'platform_base_url',
|
|
11
|
+
'language',
|
|
12
|
+
'default_output_dir',
|
|
13
|
+
'llm.base_url',
|
|
14
|
+
'llm.api_key',
|
|
15
|
+
'llm.model',
|
|
16
|
+
]);
|
|
17
|
+
export function registerConfigCommands(program) {
|
|
18
|
+
const config = program.command('config').description('global config (~/.tripo) and directory context (.tripo)');
|
|
19
|
+
config
|
|
20
|
+
.command('list')
|
|
21
|
+
.description('show effective config (key masked)')
|
|
22
|
+
.action(() => {
|
|
23
|
+
const cfg = loadConfig();
|
|
24
|
+
const shown = {
|
|
25
|
+
...cfg,
|
|
26
|
+
api_key: cfg.api_key ? maskKey(cfg.api_key) : undefined,
|
|
27
|
+
llm: cfg.llm ? { ...cfg.llm, api_key: cfg.llm.api_key ? maskKey(cfg.llm.api_key) : undefined } : undefined,
|
|
28
|
+
};
|
|
29
|
+
const context = loadContext();
|
|
30
|
+
emitResult({ config: shown, context, context_file: findContextFile() }, JSON.stringify({ config: shown, context }, null, 2));
|
|
31
|
+
});
|
|
32
|
+
config
|
|
33
|
+
.command('set <key> <value>')
|
|
34
|
+
.description(`set a config value (${[...SETTABLE].join(', ')})`)
|
|
35
|
+
.action((key, value) => {
|
|
36
|
+
if (!SETTABLE.has(key)) {
|
|
37
|
+
throw new UsageError(`unknown config key "${key}" (valid: ${[...SETTABLE].join(', ')})`);
|
|
38
|
+
}
|
|
39
|
+
const cfg = loadConfig();
|
|
40
|
+
applyPath(cfg, key.split('.'), coerceValue(value));
|
|
41
|
+
saveConfig(cfg);
|
|
42
|
+
success(`${key} = ${key.includes('api_key') ? maskKey(value) : value}`);
|
|
43
|
+
});
|
|
44
|
+
config
|
|
45
|
+
.command('unset <key>')
|
|
46
|
+
.description('remove a config value')
|
|
47
|
+
.action((key) => {
|
|
48
|
+
const cfg = loadConfig();
|
|
49
|
+
const parts = key.split('.');
|
|
50
|
+
const parent = parts.slice(0, -1).reduce((acc, part) => (acc && typeof acc[part] === 'object' ? acc[part] : undefined), cfg);
|
|
51
|
+
if (parent)
|
|
52
|
+
delete parent[parts[parts.length - 1]];
|
|
53
|
+
saveConfig(cfg);
|
|
54
|
+
success(`${key} removed`);
|
|
55
|
+
});
|
|
56
|
+
config
|
|
57
|
+
.command('context [entries...]')
|
|
58
|
+
.description('set directory context: tripo config context default_scenario=game-mobile output_dir=./assets')
|
|
59
|
+
.action((entries) => {
|
|
60
|
+
if (entries.length === 0) {
|
|
61
|
+
const ctx = loadContext();
|
|
62
|
+
emitResult({ context: ctx, file: findContextFile() }, JSON.stringify(ctx, null, 2));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const patch = {};
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const eq = entry.indexOf('=');
|
|
68
|
+
if (eq <= 0)
|
|
69
|
+
throw new UsageError(`context entries are key=value (got "${entry}")`);
|
|
70
|
+
patch[entry.slice(0, eq)] = coerceValue(entry.slice(eq + 1));
|
|
71
|
+
}
|
|
72
|
+
updateContext(patch);
|
|
73
|
+
success(`context updated: ${Object.keys(patch).join(', ')}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function applyPath(obj, path, value) {
|
|
77
|
+
let node = obj;
|
|
78
|
+
for (const part of path.slice(0, -1)) {
|
|
79
|
+
if (typeof node[part] !== 'object' || node[part] === null)
|
|
80
|
+
node[part] = {};
|
|
81
|
+
node = node[part];
|
|
82
|
+
}
|
|
83
|
+
node[path[path.length - 1]] = value;
|
|
84
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
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, resolveRegion, tripoHome } from '../config/config.js';
|
|
6
|
+
import { TripoClient } from '../core/client.js';
|
|
7
|
+
import { CliError } from '../core/errors.js';
|
|
8
|
+
import { detectRegionForKey } from '../core/region.js';
|
|
9
|
+
import { emitResult } from '../core/output.js';
|
|
10
|
+
import { EXIT } from '../knowledge/error-catalog.js';
|
|
11
|
+
import { resolveLlmConfig } from '../ai/llm.js';
|
|
12
|
+
import { terminalSupportsInlineImages } from '../util/term-image.js';
|
|
13
|
+
export function registerDoctorCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('doctor')
|
|
16
|
+
.description('diagnose environment, key, network, and terminal capabilities')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
const checks = [];
|
|
19
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
20
|
+
checks.push({
|
|
21
|
+
name: 'node',
|
|
22
|
+
ok: nodeMajor >= 20,
|
|
23
|
+
detail: `v${process.versions.node} (need >= 20)`,
|
|
24
|
+
critical: true,
|
|
25
|
+
});
|
|
26
|
+
const key = resolveApiKey();
|
|
27
|
+
checks.push({
|
|
28
|
+
name: 'api key',
|
|
29
|
+
ok: Boolean(key),
|
|
30
|
+
detail: key
|
|
31
|
+
? process.env.TRIPO_API_KEY
|
|
32
|
+
? 'from TRIPO_API_KEY env'
|
|
33
|
+
: 'from ~/.tripo/config.json'
|
|
34
|
+
: 'missing — run "tripo login"',
|
|
35
|
+
critical: true,
|
|
36
|
+
});
|
|
37
|
+
if (configFileExists()) {
|
|
38
|
+
try {
|
|
39
|
+
const mode = statSync(join(tripoHome(), 'config.json')).mode & 0o777;
|
|
40
|
+
checks.push({
|
|
41
|
+
name: 'config permissions',
|
|
42
|
+
ok: mode === 0o600,
|
|
43
|
+
detail: `0${mode.toString(8)} (expect 0600)`,
|
|
44
|
+
critical: false,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
const baseUrl = resolveApiBaseUrl();
|
|
50
|
+
const region = resolveRegion();
|
|
51
|
+
if (key) {
|
|
52
|
+
try {
|
|
53
|
+
const client = new TripoClient();
|
|
54
|
+
const balance = await client.getBalance();
|
|
55
|
+
checks.push({ name: 'api reachability', ok: true, detail: `${baseUrl} (region ${region}) · balance ${balance.balance}`, critical: true });
|
|
56
|
+
checks.push({
|
|
57
|
+
name: 'balance',
|
|
58
|
+
ok: Number(balance.balance) > 0,
|
|
59
|
+
detail: `${balance.balance} credits (${balance.frozen} frozen)${Number(balance.balance) <= 0 ? ' — run "tripo topup"' : ''}`,
|
|
60
|
+
critical: false,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
let detail = `${baseUrl}: ${err instanceof Error ? err.message : String(err)}`;
|
|
65
|
+
// Auth failure is the classic region-mismatch symptom: the key may
|
|
66
|
+
// simply live in the other region. Probe and hand over the exact fix.
|
|
67
|
+
if (err instanceof CliError && err.exitCode === EXIT.AUTH && !process.env.TRIPO_API_BASE_URL) {
|
|
68
|
+
const detected = await detectRegionForKey(key);
|
|
69
|
+
if (detected && detected.region !== region) {
|
|
70
|
+
detail += ` — key belongs to region "${detected.region}"; run: tripo config set region ${detected.region}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
checks.push({ name: 'api reachability', ok: false, detail, critical: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
checks.push({ name: 'api reachability', ok: false, detail: `skipped (no key) · would use ${baseUrl} (region ${region})`, critical: false });
|
|
78
|
+
}
|
|
79
|
+
const llm = resolveLlmConfig();
|
|
80
|
+
checks.push({
|
|
81
|
+
name: 'llm (tripo ai conversation mode)',
|
|
82
|
+
ok: true,
|
|
83
|
+
detail: llm ? `${llm.model} @ ${llm.base_url}` : 'not configured — wizard mode only (this is fine)',
|
|
84
|
+
critical: false,
|
|
85
|
+
});
|
|
86
|
+
checks.push({
|
|
87
|
+
name: 'terminal',
|
|
88
|
+
ok: true,
|
|
89
|
+
detail: `${process.stdout.isTTY ? 'interactive TTY' : 'non-TTY (headless mode)'}${terminalSupportsInlineImages() ? ' · inline images supported' : ''}`,
|
|
90
|
+
critical: false,
|
|
91
|
+
});
|
|
92
|
+
const language = loadConfig().language ?? 'auto';
|
|
93
|
+
checks.push({ name: 'language', ok: true, detail: language, critical: false });
|
|
94
|
+
const failedCritical = checks.some((c) => c.critical && !c.ok);
|
|
95
|
+
const human = checks
|
|
96
|
+
.map((c) => `${c.ok ? pc.green('✓') : c.critical ? pc.red('✗') : pc.yellow('!')} ${c.name.padEnd(34)} ${c.detail}`)
|
|
97
|
+
.join('\n');
|
|
98
|
+
emitResult({ ok: !failedCritical, checks }, human);
|
|
99
|
+
if (failedCritical)
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
}
|