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.
Files changed (65) hide show
  1. package/README.md +20 -4
  2. package/dist/commands/completion.js +1 -1
  3. package/dist/commands/config-cmd.js +32 -17
  4. package/dist/commands/doctor.js +6 -5
  5. package/dist/commands/home.js +4 -2
  6. package/dist/commands/login.js +201 -39
  7. package/dist/config/config.js +103 -14
  8. package/dist/core/client.js +16 -2
  9. package/dist/core/device-auth.js +41 -9
  10. package/dist/program.js +21 -1
  11. package/package.json +2 -2
  12. package/skill/SKILL.md +13 -1
  13. package/skill/commands/account.md +19 -5
  14. package/dist/src/ai/experts.js +0 -115
  15. package/dist/src/ai/llm.js +0 -88
  16. package/dist/src/ai/plan-card.js +0 -79
  17. package/dist/src/ai/wizard.js +0 -104
  18. package/dist/src/cli.js +0 -7
  19. package/dist/src/commands/account.js +0 -27
  20. package/dist/src/commands/ai.js +0 -113
  21. package/dist/src/commands/batch.js +0 -148
  22. package/dist/src/commands/completion.js +0 -84
  23. package/dist/src/commands/config-cmd.js +0 -84
  24. package/dist/src/commands/docs.js +0 -30
  25. package/dist/src/commands/doctor.js +0 -94
  26. package/dist/src/commands/generate.js +0 -81
  27. package/dist/src/commands/home.js +0 -68
  28. package/dist/src/commands/login.js +0 -225
  29. package/dist/src/commands/make.js +0 -142
  30. package/dist/src/commands/mcp.js +0 -194
  31. package/dist/src/commands/process-groups.js +0 -164
  32. package/dist/src/commands/shared.js +0 -75
  33. package/dist/src/commands/task.js +0 -127
  34. package/dist/src/commands/topup.js +0 -61
  35. package/dist/src/commands/view.js +0 -97
  36. package/dist/src/config/config.js +0 -79
  37. package/dist/src/config/context.js +0 -44
  38. package/dist/src/config/history.js +0 -104
  39. package/dist/src/core/client.js +0 -120
  40. package/dist/src/core/download.js +0 -107
  41. package/dist/src/core/errors.js +0 -64
  42. package/dist/src/core/output.js +0 -72
  43. package/dist/src/core/requests.js +0 -169
  44. package/dist/src/core/task-service.js +0 -161
  45. package/dist/src/core/upload.js +0 -36
  46. package/dist/src/core/viewer-assets.js +0 -24
  47. package/dist/src/core/viewer.js +0 -59
  48. package/dist/src/core/watch.js +0 -41
  49. package/dist/src/i18n.js +0 -19
  50. package/dist/src/knowledge/chains.js +0 -152
  51. package/dist/src/knowledge/error-catalog.js +0 -91
  52. package/dist/src/knowledge/models.js +0 -110
  53. package/dist/src/knowledge/params.js +0 -174
  54. package/dist/src/knowledge/scenarios.js +0 -112
  55. package/dist/src/pipeline/make.js +0 -233
  56. package/dist/src/pipeline/runner.js +0 -73
  57. package/dist/src/pipeline/stdin.js +0 -40
  58. package/dist/src/program.js +0 -147
  59. package/dist/src/types.js +0 -2
  60. package/dist/src/util/input-detect.js +0 -92
  61. package/dist/src/util/notify.js +0 -17
  62. package/dist/src/util/open.js +0 -18
  63. package/dist/src/util/spinner.js +0 -62
  64. package/dist/src/util/term-image.js +0 -28
  65. package/dist/src/version.js +0 -10
@@ -1,79 +0,0 @@
1
- /**
2
- * The plan card: one shared "here is what I will do and what it costs"
3
- * confirmation format used by both the wizard (zero-LLM) and the expert
4
- * conversation mode. Decisions come from the knowledge base either way.
5
- */
6
- import pc from 'picocolors';
7
- import { t } from '../i18n.js';
8
- import { getScenario } from '../knowledge/scenarios.js';
9
- /** Cost-impact annotations derived from the parameter set. */
10
- export function costNotes(card) {
11
- const notes = [];
12
- const params = card.genParams;
13
- if (params.texture === false) {
14
- notes.push(t({ zh: '白模生成:不消耗贴图积分', en: 'bare geometry: no texture credits' }));
15
- }
16
- if (params.texture_quality === 'detailed' || params.texture_quality === 'extreme') {
17
- notes.push(t({ zh: `贴图质量 ${params.texture_quality}:价签更高`, en: `texture_quality ${params.texture_quality}: higher price tier` }));
18
- }
19
- if (params.geometry_quality === 'detailed') {
20
- notes.push(t({ zh: 'geometry detailed(Ultra):加收 HD credit', en: 'geometry detailed (Ultra): extra HD credit' }));
21
- }
22
- if (params.quad === true) {
23
- notes.push(t({ zh: 'quad 四边面:价签更高,输出 FBX', en: 'quad topology: higher price tier, FBX output' }));
24
- }
25
- if (params.generate_parts === true) {
26
- notes.push(t({ zh: '分件生成:价签更高,出裸几何', en: 'part generation: higher price tier, bare geometry' }));
27
- }
28
- for (const step of card.chain) {
29
- if (step.step === 'convert' && Object.keys(step.args).length > 1) {
30
- notes.push(t({ zh: 'convert 非默认参数:按 complex 价签计费', en: 'non-default convert options bill as complex convert' }));
31
- }
32
- if (step.step === 'retarget') {
33
- const count = Array.isArray(step.args.animations) ? step.args.animations.length : 1;
34
- notes.push(t({ zh: `retarget ${count} 条动画:按条计费`, en: `retarget ${count} animation(s): billed per animation` }));
35
- }
36
- }
37
- return notes;
38
- }
39
- /** Render the card for terminal display (stderr). */
40
- export function renderPlanCard(card) {
41
- const scenario = card.scenario ? getScenario(card.scenario) : undefined;
42
- const lines = [];
43
- const title = t({ zh: '方案卡', en: 'Plan' });
44
- lines.push(pc.bold(`── ${title} ──────────────────────────`));
45
- lines.push(`${t({ zh: '输入', en: 'input' })}: ${card.inputSummary}`);
46
- if (scenario)
47
- lines.push(`${t({ zh: '场景', en: 'scenario' })}: ${scenario.id} (${t(scenario.label)})`);
48
- if (card.model) {
49
- lines.push(`${t({ zh: '模型', en: 'model' })}: ${card.model}${card.modelReason ? pc.dim(` — ${card.modelReason}`) : ''}`);
50
- }
51
- const params = Object.entries(card.genParams);
52
- if (params.length > 0) {
53
- lines.push(`${t({ zh: '参数', en: 'params' })}: ${params.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')}`);
54
- }
55
- if (card.chain.length > 0) {
56
- const chainStr = card.chain
57
- .map((s) => (Object.keys(s.args).length > 0 ? `${s.step}(${Object.entries(s.args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(',')})` : s.step))
58
- .join(' → ');
59
- lines.push(`${t({ zh: '加工链', en: 'chain' })}: ${chainStr}`);
60
- }
61
- if (card.deliverables.length > 0) {
62
- lines.push(`${t({ zh: '交付物', en: 'deliverables' })}: ${card.deliverables.join(', ')}`);
63
- }
64
- const cost = costNotes(card);
65
- if (cost.length > 0) {
66
- lines.push(`${t({ zh: '成本影响', en: 'cost impact' })}:`);
67
- for (const note of cost)
68
- lines.push(` · ${note}`);
69
- }
70
- for (const note of card.notes)
71
- lines.push(pc.dim(` ${note}`));
72
- if (card.expertNotes && card.expertNotes.length > 0) {
73
- lines.push(`${t({ zh: '专家意见', en: 'expert notes' })}:`);
74
- for (const note of card.expertNotes)
75
- lines.push(` · ${note}`);
76
- }
77
- lines.push(pc.bold('──────────────────────────────────'));
78
- return lines.join('\n');
79
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * Wizard mode (zero-LLM): 3–4 multiple-choice questions drive the same
3
- * knowledge-base decision tree that experts use. Every user gets 80% of the
4
- * intelligence with no LLM configured.
5
- */
6
- import * as p from '@clack/prompts';
7
- import { CliError, UsageError } from '../core/errors.js';
8
- import { isInteractive } from '../core/output.js';
9
- import { t, tList } from '../i18n.js';
10
- import { EXIT } from '../knowledge/error-catalog.js';
11
- import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
12
- export async function runWizard(initialPrompt) {
13
- if (!isInteractive()) {
14
- throw new UsageError(t({
15
- zh: '向导需要交互终端;无头环境请直接用 tripo make --for <场景> 或配置 LLM 后用 tripo ai --yes',
16
- en: 'the wizard needs an interactive terminal; headless callers should use "tripo make --for <scenario>" instead',
17
- }));
18
- }
19
- p.intro('tripo ai');
20
- let prompt = initialPrompt?.trim() ?? '';
21
- if (!prompt) {
22
- const answer = await p.text({
23
- message: t({ zh: '想做个什么?一句话描述(或图片路径)', en: 'What do you want to make? one sentence (or an image path)' }),
24
- validate: (v) => (v && v.trim().length > 0 ? undefined : t({ zh: '说点什么吧', en: 'say something' })),
25
- });
26
- if (p.isCancel(answer))
27
- throw new CliError('cancelled', EXIT.FATAL);
28
- prompt = answer.trim();
29
- }
30
- const scenarioAnswer = await p.select({
31
- message: t({ zh: '用在什么地方?', en: 'What is it for?' }),
32
- options: SCENARIOS.map((s) => ({ value: s.id, label: `${t(s.label)} — ${t(s.description)}` })),
33
- });
34
- if (p.isCancel(scenarioAnswer))
35
- throw new CliError('cancelled', EXIT.FATAL);
36
- const scenario = getScenario(scenarioAnswer);
37
- let engine;
38
- if (scenarioAnswer === 'game-mobile' || scenarioAnswer === 'game-pc') {
39
- const engineAnswer = await p.select({
40
- message: t({ zh: '目标引擎/软件?', en: 'Target engine/software?' }),
41
- options: [
42
- { value: 'unity', label: 'Unity' },
43
- { value: 'unreal', label: 'Unreal' },
44
- { value: 'blender', label: 'Blender' },
45
- { value: 'any', label: t({ zh: '不确定/通用', en: 'not sure / generic' }) },
46
- ],
47
- });
48
- if (p.isCancel(engineAnswer))
49
- throw new CliError('cancelled', EXIT.FATAL);
50
- engine = engineAnswer;
51
- }
52
- let animations = false;
53
- if (scenarioAnswer !== 'print' && scenarioAnswer !== 'film') {
54
- const animAnswer = await p.confirm({
55
- message: t({ zh: '需要骨骼/动画吗?', en: 'Do you need a skeleton / animations?' }),
56
- initialValue: scenarioAnswer === 'anim',
57
- });
58
- if (p.isCancel(animAnswer))
59
- throw new CliError('cancelled', EXIT.FATAL);
60
- animations = animAnswer;
61
- }
62
- const { chain, notes } = adjustChain(scenario.id, engine, animations);
63
- const card = {
64
- inputSummary: prompt,
65
- scenario: scenario.id,
66
- model: scenario.model,
67
- modelReason: t({ zh: '场景预设', en: 'scenario preset' }),
68
- genParams: scenario.genParams,
69
- chain,
70
- deliverables: scenario.deliverables,
71
- notes: [...tList(scenario.notes), ...notes],
72
- };
73
- return { card, options: { prompt, scenario: scenario.id, chain } };
74
- }
75
- /** Engine/animation adjustments on top of the scenario chain. */
76
- export function adjustChain(scenarioId, engine, animations) {
77
- const scenario = getScenario(scenarioId);
78
- let chain = scenario.chain.map((s) => ({ step: s.step, args: { ...s.args } }));
79
- const notes = [];
80
- if (animations && !chain.some((s) => s.step === 'rig')) {
81
- const rigArgs = { model: 'rig-v2.0', out_format: 'fbx' };
82
- if (engine === 'unity') {
83
- rigArgs.spec = 'mixamo';
84
- notes.push(t({ zh: 'Unity Humanoid → spec=mixamo', en: 'Unity Humanoid → spec=mixamo' }));
85
- }
86
- chain = [
87
- ...chain.filter((s) => s.step !== 'convert'),
88
- { step: 'rig-check', args: {} },
89
- { step: 'rig', args: rigArgs },
90
- { step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'], out_format: 'fbx' } },
91
- ];
92
- notes.push(t({ zh: '动画链输出 FBX(骨骼+动画)', en: 'animation chain outputs FBX (skeleton + animations)' }));
93
- }
94
- if (engine && !animations) {
95
- const convert = chain.find((s) => s.step === 'convert');
96
- if (convert && convert.args.format === 'FBX') {
97
- if (engine === 'blender')
98
- convert.args.fbx_preset = 'blender';
99
- if (engine === 'unreal' || engine === 'unity')
100
- convert.args.export_orientation = '+x';
101
- }
102
- }
103
- return { chain, notes };
104
- }
package/dist/src/cli.js DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runCli } from './program.js';
3
- process.on('SIGINT', () => {
4
- process.stderr.write('\n');
5
- process.exit(130);
6
- });
7
- void runCli(process.argv);
@@ -1,27 +0,0 @@
1
- /** `tripo balance` / `tripo usage` — account visibility. */
2
- import { TripoClient } from '../core/client.js';
3
- import { emitResult } from '../core/output.js';
4
- import { t } from '../i18n.js';
5
- export function registerAccountCommands(program) {
6
- program
7
- .command('balance')
8
- .description('show available and frozen credits')
9
- .action(async () => {
10
- const data = await new TripoClient().getBalance();
11
- emitResult(data, t({
12
- zh: `余额 ${data.balance} 积分(冻结 ${data.frozen})`,
13
- en: `balance: ${data.balance} credits (${data.frozen} frozen)`,
14
- }));
15
- });
16
- program
17
- .command('usage')
18
- .description('recent task credit usage')
19
- .option('--limit <n>', 'max rows to show', (v) => Number(v), 20)
20
- .action(async (opts) => {
21
- const items = (await new TripoClient().getUsage()).slice(0, opts.limit);
22
- const human = items
23
- .map((i) => `${(i.created_at ?? '').slice(0, 19).padEnd(20)} ${i.type.padEnd(24)} ${String(i.credits_consumed ?? 0).padStart(6)} cr ${i.task_id}`)
24
- .join('\n');
25
- emitResult(items, human || t({ zh: '暂无用量记录', en: 'no usage yet' }));
26
- });
27
- }
@@ -1,113 +0,0 @@
1
- /**
2
- * `tripo ai` — dual mode:
3
- * wizard (default without an LLM): knowledge-base decision-tree Q&A
4
- * conversation (with an LLM configured): producer + specialist agents
5
- * Both produce the same plan card; confirmed plans execute through the same
6
- * deterministic engine as `tripo make`.
7
- */
8
- import * as p from '@clack/prompts';
9
- import { resolveReference } from '../config/history.js';
10
- import { CliError } from '../core/errors.js';
11
- import { isInteractive, isYes, log, warn } from '../core/output.js';
12
- import { t } from '../i18n.js';
13
- import { EXIT } from '../knowledge/error-catalog.js';
14
- import { executeMake, planMake } from '../pipeline/make.js';
15
- import { renderPlanCard } from '../ai/plan-card.js';
16
- import { resolveLlmConfig } from '../ai/llm.js';
17
- import { buildExpertPlan } from '../ai/experts.js';
18
- import { runWizard } from '../ai/wizard.js';
19
- import { emitMakeOutcome } from './make.js';
20
- import { newClient, withTaskOptions } from './shared.js';
21
- export function registerAiCommand(program) {
22
- const cmd = program
23
- .command('ai')
24
- .description('describe what you want in natural language; experts plan it, you confirm, it runs')
25
- .argument('[request...]', 'e.g. "给我的手游做一个低模骑士,要跑步动画"')
26
- .option('--wizard', 'force wizard mode even when an LLM is configured');
27
- withTaskOptions(cmd);
28
- cmd.action(async (request, flags) => {
29
- const text = expandHistoryRefs(request.join(' ').trim());
30
- const llm = resolveLlmConfig();
31
- let card;
32
- let makeOptions;
33
- if (llm && !flags.wizard && text) {
34
- log(t({ zh: `对话模式(${llm.model})…`, en: `conversation mode (${llm.model})…` }));
35
- try {
36
- const plan = await buildExpertPlan(text, isInteractive() ? askUser : undefined);
37
- card = plan.card;
38
- makeOptions = {
39
- inputs: [plan.prompt],
40
- scenario: plan.scenarioId,
41
- chain: plan.chain,
42
- params: plan.params,
43
- name: flags.name,
44
- outDir: flags.out,
45
- notify: flags.notify,
46
- timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
47
- };
48
- }
49
- catch (err) {
50
- warn(t({
51
- zh: `LLM 编排失败(${err instanceof Error ? err.message : err}),回落到向导模式`,
52
- en: `LLM planning failed (${err instanceof Error ? err.message : err}); falling back to the wizard`,
53
- }));
54
- const wizard = await runWizard(text);
55
- card = wizard.card;
56
- makeOptions = wizardToOptions(wizard.options, flags);
57
- }
58
- }
59
- else {
60
- if (llm === undefined && text && !isInteractive()) {
61
- throw new CliError(t({
62
- zh: '无 LLM 配置时 tripo ai 需要交互终端;无头环境请用 tripo make --for <场景>(或配置 llm.base_url/llm.model)',
63
- en: 'without an LLM, "tripo ai" needs an interactive terminal; headless callers should use "tripo make --for <scenario>" (or configure llm.base_url/llm.model)',
64
- }), EXIT.USAGE);
65
- }
66
- const wizard = await runWizard(text || undefined);
67
- card = wizard.card;
68
- makeOptions = wizardToOptions(wizard.options, flags);
69
- }
70
- process.stderr.write(renderPlanCard(card) + '\n');
71
- if (!isYes()) {
72
- const go = await p.confirm({ message: t({ zh: '按方案执行?', en: 'Run this plan?' }) });
73
- if (p.isCancel(go) || !go) {
74
- log(t({ zh: '已取消,未消耗积分', en: 'cancelled; no credits spent' }));
75
- return;
76
- }
77
- }
78
- const client = newClient();
79
- const plan = await planMake(client, makeOptions);
80
- const outcome = await executeMake(client, plan, makeOptions);
81
- emitMakeOutcome({ results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly });
82
- });
83
- }
84
- function wizardToOptions(options, flags) {
85
- return {
86
- inputs: [options.prompt],
87
- scenario: options.scenario,
88
- chain: options.chain,
89
- params: flags.param,
90
- name: flags.name,
91
- outDir: flags.out,
92
- notify: flags.notify,
93
- timeoutMs: flags.timeout ? flags.timeout * 1000 : undefined,
94
- };
95
- }
96
- async function askUser(question) {
97
- const answer = await p.text({ message: question });
98
- if (p.isCancel(answer))
99
- return undefined;
100
- return answer;
101
- }
102
- /** Replace @task-name references in a prompt with their task ids. */
103
- export function expandHistoryRefs(text) {
104
- return text.replace(/@[\w\u4e00-\u9fff-]+/g, (ref) => {
105
- try {
106
- const entry = resolveReference(ref);
107
- return entry ? entry.task_id : ref;
108
- }
109
- catch {
110
- return ref; // ambiguity is reported when the value is actually used
111
- }
112
- });
113
- }
@@ -1,148 +0,0 @@
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
- }
@@ -1,84 +0,0 @@
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
- }
@@ -1,84 +0,0 @@
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
- }