tripo-cli 0.7.1 → 0.7.3

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 (40) hide show
  1. package/README.md +8 -8
  2. package/dist/ai/experts.js +4 -4
  3. package/dist/ai/plan-card.js +2 -1
  4. package/dist/ai/wizard.js +8 -4
  5. package/dist/commands/completion.js +1 -1
  6. package/dist/commands/generate.js +16 -5
  7. package/dist/commands/login.js +79 -21
  8. package/dist/commands/make.js +3 -0
  9. package/dist/commands/mcp.js +7 -3
  10. package/dist/commands/process-groups.js +26 -14
  11. package/dist/commands/shared.js +17 -3
  12. package/dist/commands/task.js +2 -0
  13. package/dist/commands/update.js +18 -2
  14. package/dist/core/analytics.js +40 -17
  15. package/dist/core/client.js +49 -8
  16. package/dist/core/device-auth.js +49 -1
  17. package/dist/core/download.js +2 -1
  18. package/dist/core/errors.js +8 -3
  19. package/dist/core/requests.js +36 -17
  20. package/dist/core/task-service.js +2 -0
  21. package/dist/core/upload.js +23 -5
  22. package/dist/core/watch.js +3 -0
  23. package/dist/knowledge/chains.js +4 -5
  24. package/dist/knowledge/error-catalog.js +10 -8
  25. package/dist/knowledge/models.js +28 -7
  26. package/dist/knowledge/params.js +48 -25
  27. package/dist/knowledge/scenarios.js +4 -4
  28. package/dist/pipeline/make.js +2 -2
  29. package/dist/pipeline/runner.js +25 -1
  30. package/dist/util/input-detect.js +10 -1
  31. package/package.json +1 -1
  32. package/skill/SKILL.md +19 -8
  33. package/skill/commands/account.md +7 -2
  34. package/skill/commands/generate.md +24 -15
  35. package/skill/commands/make.md +4 -3
  36. package/skill/commands/process.md +27 -23
  37. package/skill/commands/task.md +2 -2
  38. package/skill/common-errors.md +10 -9
  39. package/skill/examples/animation.md +1 -1
  40. package/skill/examples/game-asset.md +1 -1
package/README.md CHANGED
@@ -15,7 +15,7 @@ tripo login # browser approves, key arrives automatically; or --paste /
15
15
  tripo make knight.png --for game-mobile --then texture,rig,convert:fbx
16
16
  ```
17
17
 
18
- Works with both Tripo regions from the same install — international (`openapi.tripo3d.ai`, console `developers.tripo3d.ai`, email login + Stripe) and China mainland (`openapi.tripo3d.com`, console `developers.tripo3d.com`, SMS login + Alipay). `tripo login` runs an RFC 8628 device flow: the terminal shows a short code, the browser signs in (each console's own method) and approves, and the key lands in the CLI automatically — with graceful fallback to open-the-keys-page-and-paste when the device-auth backend is unavailable. The same flow works without a TTY: `tripo login --region ov|cn` from an agent or CI prints the verification URL + code and waits for the browser approval, so an AI agent can start the login and relay the code to its human. You never pick a region when a key is in hand: the key is probed against both regions and whichever accepts it wins; `tripo topup` then opens the matching billing page (`/billing` on ov, `/billing/recharge` on cn). If a config was moved between machines, `tripo doctor` detects the mismatch and prints the one-line fix.
18
+ Works with both Tripo regions from the same install — international (`openapi.tripo3d.ai`, console `developers.tripo3d.ai`, email login + Stripe) and China mainland (`openapi.tripo3d.com`, console `developers.tripo3d.com`, SMS login + Alipay). `tripo login` runs an RFC 8628 device flow: the terminal shows a short code, the browser signs in (each console's own method) and approves, and the key lands in the CLI automatically — with graceful fallback to open-the-keys-page-and-paste when the device-auth backend is unavailable. The same flow works without a TTY: `tripo login --region ov|cn` from an agent or CI prints the verification URL + code and waits for the browser approval, so an AI agent can start the login and relay the code to its human. Chat agents that cannot keep a command running (e.g. ChatGPT dots) add `--no-wait`: it exits right after printing, and rerunning the same command once the human has approved finishes the login with that same code. You never pick a region when a key is in hand: the key is probed against both regions and whichever accepts it wins; `tripo topup` then opens the matching billing page (`/billing` on ov, `/billing/recharge` on cn). If a config was moved between machines, `tripo doctor` detects the mismatch and prints the one-line fix.
19
19
 
20
20
  ## Why this CLI
21
21
 
@@ -35,7 +35,7 @@ Works with both Tripo regions from the same install — international (`openapi.
35
35
  | `tripo login / logout / whoami / use` | device flow: browser approves, key arrives automatically (paste fallback); stored 0600 in `~/.tripo/config.json`; `use` switches accounts |
36
36
  | `tripo topup / balance / usage` | open the billing page + detect the credits arriving; check spend |
37
37
  | `tripo generate <endpoint>` | deterministic access to all 8 generation endpoints |
38
- | `tripo model / anim / mesh <step>` | refine, texture, convert, import, rig-check, rig, retarget, segment, complete, decimate, uv, smartsegment |
38
+ | `tripo model / anim / mesh <step>` | texture, convert, import, rig-check, rig, retarget, segment, complete, decimate, uv, smartsegment |
39
39
  | `tripo task get/list/watch` | query and stream task progress (NDJSON in `--json` mode) |
40
40
  | `tripo files upload` | upload a file, print the `file_token` |
41
41
  | `tripo batch run <manifest.yaml>` | bulk pipelines with concurrency, retries, and resume |
@@ -60,7 +60,7 @@ tripo make "a fox" --profile work-cn # one-shot override, nothing sticky
60
60
  tripo logout # removes the *active* profile only
61
61
  ```
62
62
 
63
- Each profile carries its own key + region, so switching accounts switches regions automatically. `TRIPO_PROFILE=work-cn` does the same as `--profile work-cn`; `TRIPO_API_KEY` bypasses profiles entirely (CI/agents). Configs written by older versions are read as the `default` profile — no migration step, nothing breaks.
63
+ Each profile carries its own key + region, so switching accounts switches regions automatically. `TRIPO_PROFILE=work-cn` does the same as `--profile work-cn`; `TRIPO_API_KEY` bypasses profiles entirely (CI/agents) and is not probed for its region — pair a China-region key with `TRIPO_REGION=cn`. Configs written by older versions are read as the `default` profile — no migration step, nothing breaks.
64
64
 
65
65
  ## Scenario presets
66
66
 
@@ -91,7 +91,7 @@ tripo batch run assets.yaml --concurrency 2
91
91
 
92
92
  - `make`/`watch` are blocking: run them, wait for exit, read the single JSON line on stdout. Do not poll yourself.
93
93
  - The plan card on stderr lists every call before it runs; the final JSON carries `credits_consumed` (total for the run) plus `credits_breakdown` (per task) when a chain ran.
94
- - Exit codes: `0` ok · `2` usage · `3` auth · `4` insufficient credits · `5` content policy · `6` task failed (credits auto-refunded; a `timeout` means it is still running) · `7` network (a failed submit is never resent — check `tripo balance` before re-running) · `8` not found · `9` rate limit.
94
+ - Exit codes: `0` ok · `2` usage · `3` auth · `4` insufficient credits · `5` content policy · `6` task failed (failed/cancelled tasks are auto-refunded; a `timeout` means it is still running) · `7` network (a failed submit is never resent — check `tripo balance` before re-running) · `8` not found · `9` rate limit.
95
95
  - `preview.png` in every output directory is your eyes: look at it, then decide to `tripo redo` or continue the chain.
96
96
  - Full behavior rules: `tripo docs --llm`, per-command docs via `tripo docs --topic commands/make`.
97
97
 
@@ -99,9 +99,9 @@ tripo batch run assets.yaml --concurrency 2
99
99
 
100
100
  | var | meaning |
101
101
  | --- | --- |
102
- | `TRIPO_API_KEY` | API key (`tsk_...`), highest precedence (bypasses profiles) |
102
+ | `TRIPO_API_KEY` | API key (`tsk_...`), highest precedence (bypasses profiles); its region is not probed — set `TRIPO_REGION=cn` for a China-region key |
103
103
  | `TRIPO_PROFILE` | account profile to use, same as the global `--profile` flag |
104
- | `TRIPO_REGION` | `ov` or `cn` — normally unnecessary: `tripo login` probes both regions with your key and stores the right one automatically |
104
+ | `TRIPO_REGION` | `ov` or `cn` — unnecessary after `tripo login` (it probes both regions with your key and stores the right one); required with a China-region `TRIPO_API_KEY` |
105
105
  | `TRIPO_API_BASE_URL` / `TRIPO_PLATFORM_BASE_URL` | endpoint overrides |
106
106
  | `TRIPO_HOME` | config/history directory (default `~/.tripo`) |
107
107
  | `TRIPO_NO_UPDATE_CHECK` | disable the startup "new version available" hint (also off when `CI` is set, headless, or `--json`) |
@@ -116,8 +116,8 @@ Setting `TRIPO_API_KEY` per shell — bash/zsh: `export TRIPO_API_KEY=tsk_...`
116
116
 
117
117
  Since 0.7.0 the CLI sends usage events to Tripo (`https://sshu-api.tripo3d.ai`) for calls to the international API (`openapi.tripo3d.ai`). It is on by default, needs no setup, prints nothing, and never changes stdout, exit codes, or credit behavior.
118
118
 
119
- - **Sent:** command or MCP tool name and its result, sign-in method and result, error category with HTTP status / API code, task id (and parent task id for chains), task type, model, status, credits consumed, durations, downloaded format and file count, CLI version, and the calling agent with its version — `codex` / `workbuddy` / `zcode` / `kimi` (otherwise `unknown`), from `TRIPO_CLIENT_APP` / `TRIPO_DISTRIBUTION_CHANNEL` (set by agent plugins) or the MCP host's client info.
120
- - **Identity:** a random installation id (`analytics_distinct_id` in `~/.tripo/config.json`) and, since 0.7.1, your Tripo account id (`platform_user_id`) when it can be resolved. It is looked up for the API key actually in use with `POST https://apiplatform.tripo3d.ai/verify`, once per command or MCP tool call, alongside the first API request rather than before it; the lookup gives up after 1 second, is never retried, and the id is never written to disk — on failure events simply go without it. No email or billing details are sent.
119
+ - **Sent:** command or MCP tool name and its result, sign-in method and result, error category with HTTP status / API code, task id (and parent task id for chains), task type, model, status, credits consumed, durations, downloaded format and file count, CLI version, and the calling agent with its version — `codex` / `workbuddy` / `zcode` / `kimi` / `deepseek` / `claude` / `cursor` / `chatgpt` (otherwise `unknown`). Explicit `TRIPO_CLIENT_APP` takes priority over the MCP host's client info. When neither identifies the caller, the CLI can recognize Codex (`CODEX_THREAD_ID`), ZCode (`ZCODE_SESSION_ID`), or WorkBuddy (`CODEBUDDY_HOST=workbuddy-desktop`); only the app name is sent, never these marker values. Conflicting host markers remain `unknown`. `TRIPO_DISTRIBUTION_CHANNEL` names the plugin separately and is never used to guess the host; attribution is resolved per invocation and is not cached against an account or installation.
120
+ - **Identity:** a random installation id (`analytics_distinct_id` in `~/.tripo/config.json`) and, since 0.7.1, your Tripo account id (`platform_user_id`) when it can be resolved. It is looked up for the API key actually in use with `POST https://apiplatform.tripo3d.ai/verify`, at most once per command or per `tripo mcp` session (since 0.7.3 a session reuses it across tool calls), alongside the first API request rather than before it; events wait at most 1 second for it, the lookup is never retried within that command or session, and the id is kept only in memory — never written to disk or printed — so on failure events simply go without it. No email or billing details are sent.
121
121
  - **Never sent:** prompts, file paths, input images or models, API keys, download URLs (the key itself only goes to `/verify` for the account lookup).
122
122
  - **Nothing is sent, and no account lookup is made, for:** China-region (`cn`) calls, custom API endpoints, `--dry-run`, help/version, `docs`, `completion`, `config`, `update`, `use`, `logout`, and the bare `tripo` home screen.
123
123
  - **Opt out:** `TRIPO_ANALYTICS_ENABLED=0` (also skips the account lookup).
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { getLocale, t, tList } from '../i18n.js';
10
10
  import { availableSteps, parseThenExpression, validateChain } from '../knowledge/chains.js';
11
- import { chooseModel, detectLowPolyIntent, RIG_MODEL_V25 } from '../knowledge/models.js';
11
+ import { CAPABILITY_DEFAULTS, chooseModel, detectLowPolyIntent } from '../knowledge/models.js';
12
12
  import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
13
13
  import { chatJson } from './llm.js';
14
14
  const SCENARIO_TABLE = SCENARIOS.map((s) => `- ${s.id}: ${s.description.en} | model=${s.model} | chain=${s.chain.map((c) => c.step).join('→') || 'none'} | deliver=${s.deliverables.join('/')}`).join('\n');
@@ -29,9 +29,9 @@ const SPECIALIST_PROMPTS = {
29
29
  'game-mobile': `You are a senior game technical artist (mobile). Budgets: 500–20000 faces, 1–2K textures, clean topology, FBX/GLB delivery. P1 model is used (no quad/parts/geometry_quality).`,
30
30
  'game-pc': `You are a senior game technical artist (PC/console). PBR detailed textures, optional decimate for LODs (bake=true), FBX delivery with engine presets.`,
31
31
  film: `You are a film/offline-rendering artist. quad=true subdivision-friendly topology (FBX output), geometry_quality=detailed, texture_quality=detailed, 4K textures, USD/OBJ handoff via convert.`,
32
- print: `You are a 3D-printing engineer. Geometry only (texture=false,pbr=false), watertight meshes, flatten_bottom for FDM, STL or colored 3MF via convert, real-world sizing via scale_factor/auto_size.`,
32
+ print: `You are a 3D-printing engineer. Geometry only (texture=false,pbr=false), watertight meshes, flatten_bottom for FDM, STL or colored 3MF via convert, real-world sizing via auto_size at generation or scale_factor at convert.`,
33
33
  'ar-web': `You are a realtime AR/Web 3D engineer. Keep faces <= 20000 via decimate, 2K textures, deliver GLB + USDZ (iOS QuickLook).`,
34
- anim: `You are a character animation TD. Chain: rig-check → rig (v2.5 rig, spec tripo|mixamo) → retarget (<=5 preset animations, billed per animation). Mixamo/Unity Humanoid → spec=mixamo.`,
34
+ anim: `You are a character animation TD. Chain: rig-check → rig (v3.0 rig, spec tripo|mixamo|actorcore|unity|unreal|vrm, biped only) → retarget (<=5 preset animations, billed per animation). Mixamo → spec=mixamo; Unity Humanoid → spec=unity; Unreal → spec=unreal.`,
35
35
  toy: `You are a stylized collectible designer. P1 low-poly. STL via convert for physical prints.`,
36
36
  };
37
37
  function specialistSystem(scenarioId) {
@@ -85,7 +85,7 @@ export async function buildExpertPlan(request, askUser) {
85
85
  chain = [
86
86
  ...chain.filter((s) => s.step !== 'convert'),
87
87
  { step: 'rig-check', args: {} },
88
- { step: 'rig', args: { model: RIG_MODEL_V25 } },
88
+ { step: 'rig', args: { model: CAPABILITY_DEFAULTS.rig } },
89
89
  { step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'] } },
90
90
  ];
91
91
  }
@@ -6,6 +6,7 @@
6
6
  import pc from 'picocolors';
7
7
  import { t } from '../i18n.js';
8
8
  import { getStep } from '../knowledge/chains.js';
9
+ import { isComplexConvert } from '../knowledge/params.js';
9
10
  import { getScenario } from '../knowledge/scenarios.js';
10
11
  const GENERATION_DESCRIBE = {
11
12
  text_to_model: { zh: '文本生成 3D 模型', en: 'generate a 3D model from text' },
@@ -90,7 +91,7 @@ export function costNotes(card) {
90
91
  notes.push(t({ zh: '分件生成:价签更高,出裸几何', en: 'part generation: higher price tier, bare geometry' }));
91
92
  }
92
93
  for (const step of card.chain) {
93
- if (step.step === 'convert' && Object.keys(step.args).length > 1) {
94
+ if (step.step === 'convert' && isComplexConvert(step.args)) {
94
95
  notes.push(t({ zh: 'convert 非默认参数:按 complex 价签计费', en: 'non-default convert options bill as complex convert' }));
95
96
  }
96
97
  if (step.step === 'retarget') {
package/dist/ai/wizard.js CHANGED
@@ -8,7 +8,7 @@ import { CliError, UsageError } from '../core/errors.js';
8
8
  import { isInteractive } from '../core/output.js';
9
9
  import { t, tList } from '../i18n.js';
10
10
  import { EXIT } from '../knowledge/error-catalog.js';
11
- import { RIG_MODEL_V25 } from '../knowledge/models.js';
11
+ import { CAPABILITY_DEFAULTS } from '../knowledge/models.js';
12
12
  import { getScenario, SCENARIOS } from '../knowledge/scenarios.js';
13
13
  export async function runWizard(initialPrompt) {
14
14
  if (!isInteractive()) {
@@ -79,10 +79,14 @@ export function adjustChain(scenarioId, engine, animations) {
79
79
  let chain = scenario.chain.map((s) => ({ step: s.step, args: { ...s.args } }));
80
80
  const notes = [];
81
81
  if (animations && !chain.some((s) => s.step === 'rig')) {
82
- const rigArgs = { model: RIG_MODEL_V25, out_format: 'fbx' };
82
+ const rigArgs = { model: CAPABILITY_DEFAULTS.rig, out_format: 'fbx' };
83
83
  if (engine === 'unity') {
84
- rigArgs.spec = 'mixamo';
85
- notes.push(t({ zh: 'Unity Humanoid → spec=mixamo', en: 'Unity Humanoid → spec=mixamo' }));
84
+ rigArgs.spec = 'unity';
85
+ notes.push(t({ zh: 'Unity Humanoid → spec=unity', en: 'Unity Humanoid → spec=unity' }));
86
+ }
87
+ else if (engine === 'unreal') {
88
+ rigArgs.spec = 'unreal';
89
+ notes.push(t({ zh: 'Unreal Mannequin → spec=unreal', en: 'Unreal Mannequin → spec=unreal' }));
86
90
  }
87
91
  chain = [
88
92
  ...chain.filter((s) => s.step !== 'convert'),
@@ -9,7 +9,7 @@ const COMMANDS = [
9
9
  ];
10
10
  const SUBS = {
11
11
  generate: ['text-to-model', 'image-to-model', 'multiview-to-model', 'text-to-image', 'image-to-image', 'image-to-multiview', 'edit-multiview', 'image-to-splat'],
12
- model: ['refine', 'texture', 'convert', 'import'],
12
+ model: ['texture', 'convert', 'import'],
13
13
  anim: ['check', 'rig', 'retarget'],
14
14
  mesh: ['segment', 'complete', 'decimate', 'uv', ...(isMeshEditEnabled() ? ['edit'] : []), 'smartsegment'],
15
15
  task: ['get', 'list', 'watch'],
@@ -5,6 +5,7 @@ import { buildEditMultiview, buildImageToImage, buildImageToModel, buildImageToM
5
5
  import { resolveInputValue, runTask } from '../core/task-service.js';
6
6
  import { chooseModel } from '../knowledge/models.js';
7
7
  import { assignViews } from '../pipeline/make.js';
8
+ import { detectInput } from '../util/input-detect.js';
8
9
  import { newClient, printTaskResult, withTaskOptions } from './shared.js';
9
10
  export function registerGenerateCommands(program) {
10
11
  const generate = program.command('generate').description('deterministic generation endpoints (text/image/multiview → model/image/splat)');
@@ -46,21 +47,31 @@ export function registerGenerateCommands(program) {
46
47
  const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
47
48
  return buildImageToModel(value, model, flags.param);
48
49
  });
49
- add('multiview-to-model', '2–4 view images → 3D model', 'multi', async (client, inputs, flags) => {
50
- const views = await assignViews(client, inputs);
50
+ add('multiview-to-model', '2–4 view images, or one image-to-multiview/edit-multiview task → 3D model', 'multi', async (client, inputs, flags) => {
51
51
  const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
52
+ const kind = inputs.length === 1 ? detectInput(inputs[0]).kind : undefined;
53
+ if (kind === 'task-id' || kind === 'history-ref') {
54
+ const { value, sourceTaskType } = await resolveInputValue(client, inputs[0]);
55
+ if (sourceTaskType && sourceTaskType !== 'generate_multiview_image' && sourceTaskType !== 'edit_multiview_image') {
56
+ throw new UsageError(`multiview-to-model reuses an image-to-multiview / edit-multiview task, not ${sourceTaskType}`);
57
+ }
58
+ return buildMultiviewToModel({ task_id: value }, model, flags.param);
59
+ }
60
+ const views = await assignViews(client, inputs);
52
61
  return buildMultiviewToModel(views, model, flags.param);
53
62
  });
54
63
  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]);
64
+ add('image-to-image', 'image(s) + prompt → edited image (several images: cite them as image[1], image[2], ...)', 'multi', async (client, inputs, flags) => {
57
65
  const params = { ...(flags.model ? { model: flags.model } : {}), ...flags.param };
58
66
  if (flags.prompt)
59
67
  params.prompt = flags.prompt;
60
68
  if (!params.prompt && !params.template) {
61
69
  throw new UsageError('image-to-image needs --prompt or --param template=...');
62
70
  }
63
- return buildImageToImage(value, params);
71
+ const values = [];
72
+ for (const input of inputs)
73
+ values.push((await resolveInputValue(client, input)).value);
74
+ return buildImageToImage(values, params);
64
75
  });
65
76
  add('image-to-multiview', 'single image → 4-view sheet', 'single', async (client, inputs, flags) => {
66
77
  const { value } = await resolveInputValue(client, inputs[0]);
@@ -12,7 +12,7 @@ import * as p from '@clack/prompts';
12
12
  import { activeProfileName, DEFAULT_PROFILE, deleteProfile, describeKeySource, isValidProfileName, listProfiles, loadConfig, maskKey, REGION_DEFAULTS, resolveApiKey, resolvePlatformBaseUrl, resolveRegion, setActiveProfile, updateProfile, } from '../config/config.js';
13
13
  import { TripoClient } from '../core/client.js';
14
14
  import { trackEvent } from '../core/analytics.js';
15
- import { DeviceAuthUnavailableError, requestDeviceCode, waitForDeviceApproval, } from '../core/device-auth.js';
15
+ import { checkDeviceApproval, clearPendingGrant, DeviceAuthUnavailableError, loadPendingGrant, requestDeviceCode, savePendingGrant, waitForDeviceApproval, } from '../core/device-auth.js';
16
16
  import { AuthError, CliError, NetworkError, UsageError } from '../core/errors.js';
17
17
  import { detectRegionForKey } from '../core/region.js';
18
18
  import { emitResult, isInteractive, log, shouldOpen, success, warn } from '../core/output.js';
@@ -29,6 +29,7 @@ export function registerAuthCommands(program) {
29
29
  .option('--key <key>', 'provide the API key directly (tsk_...); region is auto-detected')
30
30
  .option('--region <region>', 'ov or cn — needed for the browser flow; required for non-interactive login')
31
31
  .option('--profile <name>', 'save into a named profile (for a second account); becomes the active profile')
32
+ .option('--no-wait', 'print the verification URL + code and exit at once; rerun the same command after approval to finish')
32
33
  .action(async (opts) => {
33
34
  await runLogin(opts);
34
35
  });
@@ -169,8 +170,8 @@ export async function runLogin(opts) {
169
170
  await validateAndSave(opts.key, region, profile);
170
171
  return;
171
172
  }
172
- if (!isInteractive()) {
173
- await runHeadlessLogin(region, profile, opts.paste);
173
+ if (!isInteractive() || opts.wait === false) {
174
+ await runHeadlessLogin(region, profile, opts.paste, opts.wait === false);
174
175
  return;
175
176
  }
176
177
  p.intro('tripo login');
@@ -211,8 +212,13 @@ export async function runLogin(opts) {
211
212
  * path are impossible here, so --region must be explicit (there is no key
212
213
  * yet to probe, and the two regions have different console hosts) and
213
214
  * --paste is rejected outright.
215
+ *
216
+ * A rerun resumes the parked grant instead of issuing a new code, so a
217
+ * killed poll never strands the human's approval. --no-wait exits right after
218
+ * printing (chat agents relay the code by ending their turn); rerunning it
219
+ * checks once and finishes the login when the human has approved.
214
220
  */
215
- async function runHeadlessLogin(region, profile, paste) {
221
+ async function runHeadlessLogin(region, profile, paste, noWait) {
216
222
  if (paste) {
217
223
  throw new UsageError(t({
218
224
  zh: '--paste 需要交互终端;非交互环境请用设备码登录(tripo login --region ov|cn)或 TRIPO_API_KEY / --key',
@@ -228,30 +234,66 @@ async function runHeadlessLogin(region, profile, paste) {
228
234
  en: '"tripo login --region cn|ov" — or, with a key in hand: "tripo login --key tsk_..." (site auto-detected)',
229
235
  }));
230
236
  }
231
- let grant;
232
- try {
233
- grant = await requestDeviceCode(region, profile === DEFAULT_PROFILE ? undefined : profile);
237
+ let grant = loadPendingGrant(region, profile);
238
+ const resumed = grant !== undefined;
239
+ if (grant && noWait) {
240
+ let poll;
241
+ try {
242
+ poll = await checkDeviceApproval(region, grant);
243
+ }
244
+ catch (err) {
245
+ throw new NetworkError(t({
246
+ zh: `授权服务暂时连不上(${err.message}),终端码仍然有效`,
247
+ en: `authorization service unavailable (${err.message}); the code is still valid`,
248
+ }), t({ zh: '稍后重新运行同一条命令', en: 'run the same command again shortly' }));
249
+ }
250
+ if (poll.status !== 'pending') {
251
+ clearPendingGrant();
252
+ await finishDeviceLogin(poll, region, profile);
253
+ return;
254
+ }
234
255
  }
235
- catch (err) {
236
- if (err instanceof DeviceAuthUnavailableError) {
237
- if (err.kind === 'unreachable') {
256
+ if (!grant) {
257
+ try {
258
+ grant = await requestDeviceCode(region, profile === DEFAULT_PROFILE ? undefined : profile);
259
+ }
260
+ catch (err) {
261
+ if (err instanceof DeviceAuthUnavailableError) {
262
+ if (err.kind === 'unreachable') {
263
+ throw new CliError(t({
264
+ zh: `连不上授权服务(${err.message})— 请检查本机网络/防火墙,出网走 HTTP 代理时设置 HTTPS_PROXY;或改用 TRIPO_API_KEY / tripo login --key tsk_...`,
265
+ en: `cannot reach the authorization service (${err.message}) — check this machine's network/firewall (set HTTPS_PROXY when behind a proxy), or use TRIPO_API_KEY / "tripo login --key tsk_..."`,
266
+ }), EXIT.NETWORK);
267
+ }
268
+ // No paste fallback without a TTY — fail with every remaining option spelled out.
238
269
  throw new CliError(t({
239
- zh: `连不上授权服务(${err.message})— 请检查本机网络/防火墙,出网走 HTTP 代理时设置 HTTPS_PROXY;或改用 TRIPO_API_KEY / tripo login --key tsk_...`,
240
- en: `cannot reach the authorization service (${err.message}) — check this machine's network/firewall (set HTTPS_PROXY when behind a proxy), or use TRIPO_API_KEY / "tripo login --key tsk_..."`,
241
- }), EXIT.NETWORK);
270
+ zh: `浏览器授权服务不可用(${err.message});请设置 TRIPO_API_KEY,运行 tripo login --key tsk_...,或在交互终端里运行 tripo login`,
271
+ en: `device authorization unavailable (${err.message}) — set TRIPO_API_KEY, run "tripo login --key tsk_...", or run "tripo login" in an interactive terminal`,
272
+ }), EXIT.AUTH);
242
273
  }
243
- // No paste fallback without a TTY — fail with every remaining option spelled out.
244
- throw new CliError(t({
245
- zh: `浏览器授权服务不可用(${err.message});请设置 TRIPO_API_KEY,运行 tripo login --key tsk_...,或在交互终端里运行 tripo login`,
246
- en: `device authorization unavailable (${err.message}) — set TRIPO_API_KEY, run "tripo login --key tsk_...", or run "tripo login" in an interactive terminal`,
247
- }), EXIT.AUTH);
274
+ throw err;
248
275
  }
249
- throw err;
276
+ savePendingGrant(region, profile, grant);
250
277
  }
251
278
  const minutes = Math.max(1, Math.round(grant.expires_in / 60));
279
+ const rerun = `tripo login --region ${region}${profile === DEFAULT_PROFILE ? '' : ` --profile ${profile}`}${noWait ? ' --no-wait' : ''}`;
280
+ const header = noWait
281
+ ? {
282
+ zh: `需要在浏览器完成授权;本命令现在退出。用户说授权完成后,运行 ${rerun} 完成登录(同一个终端码,不要重新发起)。请转告用户:`,
283
+ en: `Authorization happens in a browser; this command exits now. Once the human says it is done, run ${rerun} to finish (same code — do not start over). Relay these steps:`,
284
+ }
285
+ : resumed
286
+ ? {
287
+ zh: '继续等待之前发起的授权(终端码不变),直到授权完成或过期。用户还没操作的话,请转告:',
288
+ en: 'Resuming the authorization started earlier (same code); waiting until approved or expired. If the human has not done it yet, relay:',
289
+ }
290
+ : {
291
+ zh: '需要在浏览器完成授权;本命令会一直等待,直到授权完成或过期。请转告用户:',
292
+ en: 'Authorization happens in a browser; this command keeps waiting until approved or expired. Relay these steps:',
293
+ };
252
294
  log(t({
253
295
  zh: [
254
- '需要在浏览器完成授权;本命令会一直等待,直到授权完成或过期。请转告用户:',
296
+ header.zh,
255
297
  ` 1. 打开: ${grant.verification_url_complete}`,
256
298
  ' (页面如果提示登录,先登录或注册 Tripo 账号 — 新账号有免费额度)',
257
299
  ` 2. 在「Verification code」输入框填入终端码: ${grant.user_code}`,
@@ -259,7 +301,7 @@ async function runHeadlessLogin(region, profile, paste) {
259
301
  ` 4. 点击「Authorize」。此码 ${minutes} 分钟内有效`,
260
302
  ].join('\n'),
261
303
  en: [
262
- 'Authorization happens in a browser; this command keeps waiting until approved or expired. Relay these steps:',
304
+ header.en,
263
305
  ` 1. Open: ${grant.verification_url_complete}`,
264
306
  ' (if the page asks to sign in, sign in or sign up first — new accounts get free credits)',
265
307
  ` 2. Enter this one-time code in the "Verification code" field: ${grant.user_code}`,
@@ -267,7 +309,23 @@ async function runHeadlessLogin(region, profile, paste) {
267
309
  ` 4. Click "Authorize". The code expires in ${minutes} minutes`,
268
310
  ].join('\n'),
269
311
  }));
312
+ if (noWait) {
313
+ emitResult({
314
+ status: 'pending',
315
+ profile,
316
+ region,
317
+ user_code: grant.user_code,
318
+ verification_url: grant.verification_url_complete,
319
+ expires_in: grant.expires_in,
320
+ next: rerun,
321
+ }, t({ zh: `等待用户授权(终端码 ${grant.user_code});完成后运行: ${rerun}`, en: `waiting for approval (code ${grant.user_code}); then run: ${rerun}` }));
322
+ return;
323
+ }
270
324
  const result = await waitForDeviceApproval(region, grant);
325
+ clearPendingGrant();
326
+ await finishDeviceLogin(result, region, profile);
327
+ }
328
+ async function finishDeviceLogin(result, region, profile) {
271
329
  if (result.status === 'denied') {
272
330
  throw new CliError(t({ zh: '授权被拒绝;重新运行 tripo login 再试', en: 'authorization denied — run "tripo login" again to retry' }), EXIT.AUTH);
273
331
  }
@@ -315,6 +315,9 @@ export function emitMakeOutcome(outcome) {
315
315
  : primary;
316
316
  if (outcome.chainResults.length > 0)
317
317
  merged.chain = outcome.chainResults;
318
+ // Chain steps take a task id input, so the API omits normalize_scale on them; the model keeps the import's scale.
319
+ if (primary.normalize_scale !== undefined)
320
+ merged.normalize_scale ??= primary.normalize_scale;
318
321
  if (outcome.stoppedEarly)
319
322
  merged.stopped_early = outcome.stoppedEarly;
320
323
  if (credits.breakdown.length > 1) {
@@ -20,14 +20,17 @@ import { beginOperation, finishOperation } from '../core/analytics.js';
20
20
  const TOOLS = [
21
21
  {
22
22
  name: 'tripo_make',
23
- description: 'Generate a 3D asset from text, an image path/URL, or an existing task id. Blocks until finished; downloads artifacts locally and returns file paths (model_file, preview). Use scenario presets for domain defaults.',
23
+ description: 'Generate a 3D asset from text, an image path/URL, a model file path/URL, or an existing task id. Blocks until finished; downloads artifacts locally and returns file paths (model_file, preview). A model file is imported first and the result adds normalize_scale, the import normalization factor (original size = model size / normalize_scale). Use scenario presets for domain defaults.',
24
24
  inputSchema: {
25
25
  type: 'object',
26
26
  properties: {
27
- input: { type: 'string', description: 'prompt text, image file path, image URL, or task id' },
27
+ input: { type: 'string', description: 'prompt text, image file path or URL, model file path or URL (GLB/GLTF/FBX/OBJ/STL), or task id' },
28
28
  scenario: { type: 'string', enum: scenarioIds(), description: 'production scenario preset' },
29
29
  then: { type: 'string', description: 'processing chain, e.g. "texture,rig,convert:fbx"' },
30
- model: { type: 'string', description: 'force model: tripo-v3.1, tripo-p1 or tripo-p2 (P-series preview with quad)' },
30
+ model: {
31
+ type: 'string',
32
+ description: 'force model: tripo-v3.1 / v3.1-20260211 (default), tripo-p1 / P1-20260311 (low-poly) or tripo-p2 / P2-20260801 (P-series Preview with quad); aliases and dated versions are both accepted',
33
+ },
31
34
  output_dir: { type: 'string', description: 'directory for artifacts' },
32
35
  },
33
36
  required: ['input'],
@@ -166,6 +169,7 @@ async function callTool(params) {
166
169
  output_dir: final.output_dir ?? outcome.primary.output_dir,
167
170
  model_file: final.model_file ?? outcome.primary.model_file,
168
171
  preview: final.preview ?? outcome.primary.preview,
172
+ normalize_scale: final.normalize_scale ?? outcome.primary.normalize_scale,
169
173
  chain: outcome.chainResults.map((r) => ({ task_id: r.task_id, type: r.type })),
170
174
  stopped_early: outcome.stoppedEarly,
171
175
  });
@@ -11,10 +11,13 @@ import { resolveInputValue, runTask } from '../core/task-service.js';
11
11
  import { t } from '../i18n.js';
12
12
  import { getStep, isMeshEditEnabled } from '../knowledge/chains.js';
13
13
  import { readPipedTask } from '../pipeline/stdin.js';
14
+ import { detectInput, IMAGE_EXTS, urlExtension } from '../util/input-detect.js';
14
15
  import { newClient, printTaskResult, withTaskOptions } from './shared.js';
15
16
  async function resolveStepInput(client, raw) {
16
- if (raw)
17
- return resolveInputValue(client, raw);
17
+ if (raw) {
18
+ const { value, sourceTaskType } = await resolveInputValue(client, raw);
19
+ return { value, sourceType: sourceTaskType };
20
+ }
18
21
  const piped = await readPipedTask();
19
22
  if (piped) {
20
23
  log(t({ zh: `使用管道输入任务 ${piped.task_id}`, en: `using piped task ${piped.task_id}` }));
@@ -22,20 +25,29 @@ async function resolveStepInput(client, raw) {
22
25
  }
23
26
  const fallback = await resolveInputValue(client, '@last');
24
27
  log(t({ zh: `未指定输入,使用上一个成功任务 @last`, en: `no input given; defaulting to the last successful task (@last)` }));
25
- return fallback;
28
+ return { value: fallback.value, sourceType: fallback.sourceTaskType };
26
29
  }
27
30
  function makeStepAction(stepName, extraDefaults = {}) {
28
31
  return async (input, flags) => {
29
32
  const client = newClient();
33
+ const def = getStep(stepName);
34
+ const kind = input ? detectInput(input).kind : undefined;
35
+ // Task-only steps (retarget/complete): refuse a file/URL before uploading it.
36
+ if (!def.acceptsFile && kind && kind !== 'task-id' && kind !== 'history-ref' && kind !== 'text') {
37
+ throw new UsageError(`${def.name} takes a task id or @reference, not a file/URL`);
38
+ }
30
39
  const resolved = await resolveStepInput(client, input);
31
40
  // Local chain legality: catch e.g. retarget-on-non-rig before spending credits.
32
- const def = getStep(stepName);
33
41
  if (Array.isArray(def.accepts) && resolved.sourceType && !def.accepts.includes(resolved.sourceType)) {
34
42
  throw new UsageError(def.accepts.length === 0
35
43
  ? `${def.name} takes a file/URL input, not a task (input is a ${resolved.sourceType} task)`
36
44
  : `${def.name} only accepts ${def.accepts.join('/')} tasks (input is a ${resolved.sourceType} task)`);
37
45
  }
38
46
  const args = { ...extraDefaults, ...collectStepArgs(stepName, flags), ...flags.param };
47
+ if (stepName === 'smartsegment' && args.seg_type === undefined
48
+ && (kind === 'image-file' || (kind === 'url' && IMAGE_EXTS.has(urlExtension(input))))) {
49
+ args.seg_type = 'image';
50
+ }
39
51
  const built = buildStepRequest(stepName, resolved.value, args);
40
52
  for (const w of built.warnings)
41
53
  warn(w);
@@ -67,7 +79,7 @@ function collectStepArgs(step, flags) {
67
79
  };
68
80
  switch (step) {
69
81
  case 'convert':
70
- return pick(['format', 'texture_size', 'texture_format', 'face_limit', 'quad', 'flatten_bottom', 'fbx_preset', 'export_orientation', 'scale_factor', 'auto_size', 'pivot_to_center_bottom', 'export_vertex_colors']);
82
+ return pick(['format', 'texture_size', 'texture_format', 'face_limit', 'quad', 'flatten_bottom', 'fbx_preset', 'export_orientation', 'scale_factor', 'pivot_to_center_bottom', 'export_vertex_colors']);
71
83
  case 'texture':
72
84
  return pick(['model', 'texture_quality', 'texture_alignment', 'pbr']);
73
85
  case 'rig':
@@ -92,12 +104,12 @@ function collectStepArgs(step, flags) {
92
104
  }
93
105
  export function registerProcessCommands(program) {
94
106
  // ---- tripo model ----
95
- const model = program.command('model').description('model processing: refine / texture / convert / import');
96
- withTaskOptions(model.command('refine [input]').description('refine a draft text-to-model task (task id / @last)')).action(makeStepAction('refine'));
107
+ const model = program.command('model').description('model processing: texture / convert / import');
97
108
  withTaskOptions(model
98
109
  .command('texture [input]')
99
110
  .description('re-texture a model (task/@last/file/url)')
100
- .option('--texture-quality <q>', 'standard|detailed|extreme')
111
+ .option('--model <v>', 'v3.5-20260815|v3.0-20250812|v2.5-20250123 (default v3.0; fast/delight select v3.5)')
112
+ .option('--texture-quality <q>', 'fast|standard|detailed|extreme (fast needs v3.5)')
101
113
  .option('--texture-alignment <a>', 'original_image|geometry')).action(makeStepAction('texture'));
102
114
  withTaskOptions(model
103
115
  .command('convert [input]')
@@ -108,21 +120,21 @@ export function registerProcessCommands(program) {
108
120
  .option('--face-limit <n>', 'face limit', (v) => Number(v))
109
121
  .option('--quad', 'quad remesh')
110
122
  .option('--flatten-bottom', 'flatten the bottom (3D printing)')
111
- .option('--fbx-preset <p>', 'blender|3dsmax|mixamo')
123
+ .option('--fbx-preset <p>', 'blender|3dsmax|mixamo|bake_scale')
112
124
  .option('--export-orientation <o>', '+x|-x|+y|-y')
113
125
  .option('--scale-factor <n>', 'scale factor', (v) => Number(v))
114
- .option('--auto-size', 'scale to real-world size')
115
126
  .option('--pivot-to-center-bottom', 'pivot at center bottom')
116
127
  .option('--export-vertex-colors', 'include vertex colors (OBJ/GLTF)')).action(makeStepAction('convert'));
117
128
  withTaskOptions(model.command('import <input>').description('import an external model file/url (GLB/GLTF/FBX/OBJ/STL ≤150MB)')).action(makeStepAction('import'));
118
129
  // ---- tripo anim ----
119
130
  const anim = program.command('anim').description('animation: check (riggability) / rig / retarget');
120
- withTaskOptions(anim.command('check [input]').description('rig-check: is this model riggable? (GLB input)')).action(makeStepAction('rig-check'));
131
+ withTaskOptions(anim.command('check [input]').description('rig-check: is this model riggable? (GLB/FBX/OBJ/STL input)')).action(makeStepAction('rig-check'));
121
132
  withTaskOptions(anim
122
133
  .command('rig [input]')
123
134
  .description('rig a model (skeleton binding)')
124
- .option('--rig-type <type>', 'biped|quadruped|hexapod|octopod|avian|serpentine|aquatic')
125
- .option('--spec <spec>', 'tripo|mixamo')
135
+ .option('--model <v>', 'v3.0-20260909|v2.5-20260210|v1.0-20240301 (default v3.0; aliases rig-v3.0/rig-v2.5/rig-v1.0)')
136
+ .option('--rig-type <type>', 'biped|quadruped|hexapod|octopod|avian|serpentine|aquatic|others (others: v3.0)')
137
+ .option('--spec <spec>', 'tripo|mixamo|actorcore|unity|unreal|vrm (biped only; actorcore/unity/unreal/vrm: v3.0)')
126
138
  .option('--out-format <f>', 'glb|fbx')).action(makeStepAction('rig'));
127
139
  withTaskOptions(anim
128
140
  .command('retarget [input]')
@@ -180,7 +192,7 @@ export function registerProcessCommands(program) {
180
192
  withTaskOptions(mesh
181
193
  .command('smartsegment [input]')
182
194
  .description('smart segmentation (image or model pipeline)')
183
- .option('--seg-type <t>', 'image|model', 'model')
195
+ .option('--seg-type <t>', 'image|model (default: image for an image file/URL, else model)')
184
196
  .option('--granularity <g>', 'coarse|medium|fine')
185
197
  .option('--hint <text>', 'hint for the parts to find')).action(makeStepAction('smartsegment'));
186
198
  }
@@ -4,7 +4,7 @@ import { TripoClient } from '../core/client.js';
4
4
  import { UsageError } from '../core/errors.js';
5
5
  import { emitResult } from '../core/output.js';
6
6
  import { t } from '../i18n.js';
7
- import { parseThenExpression } from '../knowledge/chains.js';
7
+ import { ChainError, parseThenExpression } from '../knowledge/chains.js';
8
8
  /** repeatable --param key=value collector */
9
9
  export function collectParam(value, previous) {
10
10
  const eq = value.indexOf('=');
@@ -49,7 +49,14 @@ export function newClient() {
49
49
  export function parseChainFlag(value) {
50
50
  if (!value)
51
51
  return undefined;
52
- return parseThenExpression(value);
52
+ try {
53
+ return parseThenExpression(value);
54
+ }
55
+ catch (error) {
56
+ if (error instanceof ChainError)
57
+ throw new UsageError(error.message);
58
+ throw error;
59
+ }
53
60
  }
54
61
  /**
55
62
  * Sum the spend of every task in a run and keep the per-task lines. Tasks
@@ -64,7 +71,8 @@ export function summarizeCredits(tasks) {
64
71
  breakdown.push({ task_id: task.task_id, type: task.type, credits_consumed: task.credits_consumed });
65
72
  total += task.credits_consumed;
66
73
  }
67
- return { total, breakdown };
74
+ // credits carry up to two decimals; round away float drift from the sum.
75
+ return { total: Math.round(total * 100) / 100, breakdown };
68
76
  }
69
77
  /** Standard result output for a finished task (human + JSON modes). */
70
78
  export function printTaskResult(result) {
@@ -90,6 +98,12 @@ export function printTaskResult(result) {
90
98
  lines.push(t({ zh: `模型文件: ${result.model_file}`, en: `model file: ${result.model_file}` }));
91
99
  if (result.preview)
92
100
  lines.push(t({ zh: `预览图: ${result.preview}`, en: `preview: ${result.preview}` }));
101
+ if (result.normalize_scale !== undefined) {
102
+ lines.push(t({
103
+ zh: `导入缩放系数: ${result.normalize_scale}(原始尺寸 = 导入后尺寸 ÷ ${result.normalize_scale})`,
104
+ en: `import scale: ${result.normalize_scale} (original size = imported size ÷ ${result.normalize_scale})`,
105
+ }));
106
+ }
93
107
  emitResult(result, lines.join('\n'));
94
108
  }
95
109
  export function llmConfigured() {
@@ -27,6 +27,8 @@ function humanTask(task) {
27
27
  if (typeof value === 'string' && value.startsWith('http'))
28
28
  lines.push(`${key}: ${value}`);
29
29
  }
30
+ if (typeof output.normalize_scale === 'number')
31
+ lines.push(`normalize_scale: ${output.normalize_scale}`);
30
32
  return lines.join('\n');
31
33
  }
32
34
  export function registerTaskCommands(program) {
@@ -8,8 +8,9 @@
8
8
  * so agents and pipes are unaffected.
9
9
  */
10
10
  import { spawn } from 'node:child_process';
11
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
12
  import { join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
13
14
  import pc from 'picocolors';
14
15
  import { tripoHome } from '../config/config.js';
15
16
  import { CliError, NetworkError } from '../core/errors.js';
@@ -96,18 +97,33 @@ export function notifyIfOutdated() {
96
97
  fetchLatestVersion().catch(() => { });
97
98
  }
98
99
  }
100
+ /**
101
+ * Prefix of the running global install: `<prefix>/lib/node_modules/tripo-cli` plus `<prefix>/bin/tripo`
102
+ * (`<prefix>/node_modules/tripo-cli` plus `<prefix>/tripo.cmd` on Windows). Version managers such as
103
+ * vite-plus and Volta keep `tripo` linked to the Node version it was installed under, so a bare
104
+ * `npm install -g` can land in the current Node's prefix and leave the `tripo` on PATH untouched.
105
+ */
106
+ function installPrefix() {
107
+ const pkgRoot = fileURLToPath(new URL('../../', import.meta.url));
108
+ const win = process.platform === 'win32';
109
+ const prefix = win ? join(pkgRoot, '..', '..') : join(pkgRoot, '..', '..', '..');
110
+ return existsSync(win ? join(prefix, 'tripo.cmd') : join(prefix, 'bin', 'tripo')) ? prefix : undefined;
111
+ }
99
112
  function runNpmInstall() {
113
+ const prefix = installPrefix();
114
+ const manual = `npm install -g ${prefix ? `--prefix "${prefix}" ` : ''}${PACKAGE_NAME}@latest`;
100
115
  return new Promise((resolve, reject) => {
101
116
  const child = spawn('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
102
117
  stdio: 'inherit',
103
118
  shell: process.platform === 'win32',
119
+ env: prefix ? { ...process.env, npm_config_prefix: prefix } : process.env,
104
120
  });
105
121
  child.on('error', (err) => reject(new CliError(`failed to run npm: ${err.message}`, EXIT.FATAL)));
106
122
  child.on('exit', (code) => {
107
123
  if (code === 0)
108
124
  resolve();
109
125
  else
110
- reject(new CliError(`npm install exited with code ${code}`, EXIT.FATAL, t({ zh: `手动执行: npm install -g ${PACKAGE_NAME}@latest`, en: `run manually: npm install -g ${PACKAGE_NAME}@latest` })));
126
+ reject(new CliError(`npm install exited with code ${code}`, EXIT.FATAL, t({ zh: `手动执行: ${manual}`, en: `run manually: ${manual}` })));
111
127
  });
112
128
  });
113
129
  }