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,97 +0,0 @@
1
- /**
2
- * `tripo view [task|@last|file.glb]` — local model-viewer preview.
3
- * `tripo redo [task|@last]` — re-roll the same request with a fresh seed.
4
- */
5
- import { existsSync, readdirSync } from 'node:fs';
6
- import { extname, join } from 'node:path';
7
- import { readHistory, resolveReference } from '../config/history.js';
8
- import { TripoClient } from '../core/client.js';
9
- import { downloadArtifacts, defaultOutputDir } from '../core/download.js';
10
- import { UsageError } from '../core/errors.js';
11
- import { emitResult, log, shouldOpen } from '../core/output.js';
12
- import { startViewer } from '../core/viewer.js';
13
- import { t } from '../i18n.js';
14
- import { runTask } from '../core/task-service.js';
15
- import { openExternal } from '../util/open.js';
16
- import { printTaskResult, newClient } from './shared.js';
17
- const VIEWABLE = new Set(['.glb', '.gltf']);
18
- export function registerViewCommands(program) {
19
- program
20
- .command('view')
21
- .description('preview a 3D result in the browser (model-viewer, local server)')
22
- .argument('[target]', 'task id, @last, @name, or a local .glb path (default @last)', '@last')
23
- .option('--keep', 'keep the preview server running until Ctrl+C')
24
- .action(async (target, opts) => {
25
- const modelFile = await resolveModelFile(target);
26
- const handle = await startViewer(modelFile, target);
27
- if (shouldOpen()) {
28
- openExternal(handle.url);
29
- log(t({ zh: `预览已打开: ${handle.url}`, en: `preview opened: ${handle.url}` }));
30
- }
31
- emitResult({ url: handle.url, model_file: modelFile }, `${handle.url}\n${modelFile}`);
32
- if (opts.keep || shouldOpen()) {
33
- log(t({ zh: 'Ctrl+C 关闭预览服务', en: 'Ctrl+C to stop the preview server' }));
34
- await new Promise(() => { }); // hold until interrupted
35
- }
36
- else {
37
- handle.close();
38
- }
39
- });
40
- program
41
- .command('redo')
42
- .description('re-run a previous task with a fresh model_seed (same everything else)')
43
- .argument('[target]', 'task id or @reference (default @last)', '@last')
44
- .option('--seed <n>', 'explicit model_seed', (v) => Number(v))
45
- .action(async (target, opts) => {
46
- const entry = target.startsWith('@')
47
- ? resolveReference(target)
48
- : readHistory().find((e) => e.task_id === target);
49
- if (!entry)
50
- throw new UsageError(`no history entry for ${target} — redo only works for tasks created by this CLI`);
51
- if (!entry.endpoint || !entry.payload) {
52
- throw new UsageError(`history entry ${target} has no recorded request payload`);
53
- }
54
- const payload = { ...entry.payload, model_seed: opts.seed ?? Math.floor(Math.random() * 2 ** 31) };
55
- const client = newClient();
56
- const result = await runTask({
57
- client,
58
- request: { endpoint: entry.endpoint, payload },
59
- taskType: entry.type,
60
- name: `${entry.name ?? 'redo'}-redo`,
61
- });
62
- printTaskResult(result);
63
- });
64
- }
65
- async function resolveModelFile(target) {
66
- // Direct file path
67
- if (VIEWABLE.has(extname(target).toLowerCase()) && existsSync(target))
68
- return target;
69
- const entry = target.startsWith('@')
70
- ? resolveReference(target)
71
- : readHistory().find((e) => e.task_id === target);
72
- // Local artifacts already downloaded?
73
- if (entry?.output_dir && existsSync(entry.output_dir)) {
74
- const glb = readdirSync(entry.output_dir).find((f) => VIEWABLE.has(extname(f).toLowerCase()));
75
- if (glb)
76
- return join(entry.output_dir, glb);
77
- }
78
- // Fetch fresh (URLs expire ~5min, so always re-query then download).
79
- const taskId = entry?.task_id ?? target;
80
- if (!/^(task_[A-Za-z0-9_-]+|[0-9a-f-]{36})$/i.test(taskId)) {
81
- throw new UsageError(`cannot resolve "${target}" to a viewable model (.glb path, task id, or @ref)`);
82
- }
83
- const client = new TripoClient();
84
- const task = await client.getTask(taskId);
85
- if (task.status !== 'success')
86
- throw new UsageError(`task ${taskId} is ${task.status}, nothing to view yet`);
87
- log(t({ zh: '正在下载模型用于预览…', en: 'downloading the model for preview…' }));
88
- const dir = entry?.output_dir ?? defaultOutputDir(process.cwd(), entry?.name ?? 'view', taskId);
89
- const artifacts = await downloadArtifacts(task, dir);
90
- if (!artifacts.modelFile || !VIEWABLE.has(extname(artifacts.modelFile).toLowerCase())) {
91
- const fallback = artifacts.files.find((f) => VIEWABLE.has(extname(f).toLowerCase()));
92
- if (!fallback)
93
- throw new UsageError(`task ${taskId} has no glb/gltf artifact to preview (files: ${artifacts.files.join(', ') || 'none'})`);
94
- return fallback;
95
- }
96
- return artifacts.modelFile;
97
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * Global config at ~/.tripo/config.json (mode 0600 — it holds the API key).
3
- * Precedence for every setting: env var > directory context > global config.
4
- */
5
- import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
6
- import { homedir } from 'node:os';
7
- import { join } from 'node:path';
8
- export const REGION_DEFAULTS = {
9
- ov: { api: 'https://openapi.tripo3d.ai', platform: 'https://platform.tripo3d.ai' },
10
- cn: { api: 'https://openapi.tripo3d.com', platform: 'https://platform.tripo3d.ai' },
11
- };
12
- export function tripoHome() {
13
- return process.env.TRIPO_HOME || join(homedir(), '.tripo');
14
- }
15
- function configPath() {
16
- return join(tripoHome(), 'config.json');
17
- }
18
- export function loadConfig() {
19
- try {
20
- return JSON.parse(readFileSync(configPath(), 'utf8'));
21
- }
22
- catch {
23
- return {};
24
- }
25
- }
26
- export function saveConfig(config) {
27
- const dir = tripoHome();
28
- mkdirSync(dir, { recursive: true, mode: 0o700 });
29
- const file = configPath();
30
- writeFileSync(file, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
31
- // writeFileSync mode only applies on creation; enforce on every save.
32
- chmodSync(file, 0o600);
33
- }
34
- export function updateConfig(patch) {
35
- const next = { ...loadConfig(), ...patch };
36
- saveConfig(next);
37
- return next;
38
- }
39
- export function deleteApiKey() {
40
- const config = loadConfig();
41
- delete config.api_key;
42
- if (Object.keys(config).length === 0) {
43
- try {
44
- unlinkSync(configPath());
45
- }
46
- catch { }
47
- }
48
- else {
49
- saveConfig(config);
50
- }
51
- }
52
- /** Resolve the API key: env wins, then global config. */
53
- export function resolveApiKey(config = loadConfig()) {
54
- return process.env.TRIPO_API_KEY || config.api_key;
55
- }
56
- export function resolveRegion(config = loadConfig()) {
57
- const env = process.env.TRIPO_REGION;
58
- if (env === 'cn' || env === 'ov')
59
- return env;
60
- return config.region ?? 'ov';
61
- }
62
- export function resolveApiBaseUrl(config = loadConfig()) {
63
- return (process.env.TRIPO_API_BASE_URL ||
64
- config.api_base_url ||
65
- REGION_DEFAULTS[resolveRegion(config)].api);
66
- }
67
- export function resolvePlatformBaseUrl(config = loadConfig()) {
68
- return (process.env.TRIPO_PLATFORM_BASE_URL ||
69
- config.platform_base_url ||
70
- REGION_DEFAULTS[resolveRegion(config)].platform);
71
- }
72
- export function configFileExists() {
73
- return existsSync(configPath());
74
- }
75
- export function maskKey(key) {
76
- if (key.length <= 10)
77
- return key.slice(0, 4) + '…';
78
- return `${key.slice(0, 8)}…${key.slice(-4)}`;
79
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Directory-level context at .tripo/context.json — project defaults that
3
- * apply when the CLI runs inside that directory (like .git discovery).
4
- */
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
- import { dirname, join } from 'node:path';
7
- /** Walk up from cwd to find the nearest .tripo/context.json. */
8
- export function findContextFile(from = process.cwd()) {
9
- let dir = from;
10
- for (;;) {
11
- const candidate = join(dir, '.tripo', 'context.json');
12
- if (existsSync(candidate))
13
- return candidate;
14
- const parent = dirname(dir);
15
- if (parent === dir)
16
- return undefined;
17
- dir = parent;
18
- }
19
- }
20
- export function loadContext(from = process.cwd()) {
21
- const file = findContextFile(from);
22
- if (!file)
23
- return {};
24
- try {
25
- return JSON.parse(readFileSync(file, 'utf8'));
26
- }
27
- catch {
28
- return {};
29
- }
30
- }
31
- /** Update (or create) the context file in the current directory. */
32
- export function updateContext(patch, cwd = process.cwd()) {
33
- const existing = findContextFile(cwd);
34
- const file = existing ?? join(cwd, '.tripo', 'context.json');
35
- let current = {};
36
- if (existing) {
37
- try {
38
- current = JSON.parse(readFileSync(existing, 'utf8'));
39
- }
40
- catch { }
41
- }
42
- mkdirSync(dirname(file), { recursive: true });
43
- writeFileSync(file, JSON.stringify({ ...current, ...patch }, null, 2) + '\n');
44
- }
@@ -1,104 +0,0 @@
1
- /**
2
- * Task history at ~/.tripo/history.jsonl — powers implicit context (@last),
3
- * positional references (@2), named references (@my-knight), and redo.
4
- */
5
- import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
6
- import { join } from 'node:path';
7
- import { tripoHome } from './config.js';
8
- function historyPath() {
9
- return join(tripoHome(), 'history.jsonl');
10
- }
11
- export function appendHistory(entry) {
12
- mkdirSync(tripoHome(), { recursive: true, mode: 0o700 });
13
- appendFileSync(historyPath(), JSON.stringify(entry) + '\n');
14
- }
15
- export function readHistory() {
16
- let raw;
17
- try {
18
- raw = readFileSync(historyPath(), 'utf8');
19
- }
20
- catch {
21
- return [];
22
- }
23
- const entries = [];
24
- for (const line of raw.split('\n')) {
25
- if (!line.trim())
26
- continue;
27
- try {
28
- entries.push(JSON.parse(line));
29
- }
30
- catch { }
31
- }
32
- // Later entries for the same task id override earlier ones (status updates).
33
- const merged = new Map();
34
- for (const e of entries)
35
- merged.set(e.task_id, { ...merged.get(e.task_id), ...e });
36
- return [...merged.values()];
37
- }
38
- export function updateHistoryStatus(taskId, status, patch) {
39
- const existing = readHistory().find((e) => e.task_id === taskId);
40
- if (!existing)
41
- return;
42
- appendHistory({ ...existing, ...patch, status, task_id: taskId });
43
- }
44
- export class RefError extends Error {
45
- }
46
- /**
47
- * Resolve a history reference:
48
- * @last → most recent successful task
49
- * @last:any → most recent task regardless of status
50
- * @2 → 2nd most recent successful task
51
- * @some-name → unique match on entry name (slug); ambiguity is an error
52
- * Returns undefined when `ref` is not a reference at all.
53
- */
54
- export function resolveReference(ref) {
55
- if (!ref.startsWith('@'))
56
- return undefined;
57
- const body = ref.slice(1);
58
- const entries = readHistory();
59
- const successes = entries.filter((e) => e.status === 'success');
60
- if (body === 'last' || body === '') {
61
- const hit = successes[successes.length - 1];
62
- if (!hit)
63
- throw new RefError('no successful task in history yet');
64
- return hit;
65
- }
66
- if (body === 'last:any') {
67
- const hit = entries[entries.length - 1];
68
- if (!hit)
69
- throw new RefError('history is empty');
70
- return hit;
71
- }
72
- if (/^\d+$/.test(body)) {
73
- const n = Number(body);
74
- const hit = successes[successes.length - n];
75
- if (!hit)
76
- throw new RefError(`history has only ${successes.length} successful task(s), cannot resolve @${n}`);
77
- return hit;
78
- }
79
- const matches = entries.filter((e) => e.name === body);
80
- if (matches.length === 0) {
81
- const fuzzy = entries.filter((e) => e.name?.includes(body));
82
- if (fuzzy.length === 1)
83
- return fuzzy[0];
84
- if (fuzzy.length > 1) {
85
- throw new RefError(`"@${body}" matches ${fuzzy.length} tasks: ${fuzzy.map((e) => `${e.name} (${e.task_id.slice(0, 8)})`).join(', ')} — use a task id instead`);
86
- }
87
- throw new RefError(`no task named "@${body}" in history`);
88
- }
89
- if (matches.length > 1) {
90
- throw new RefError(`"@${body}" matches ${matches.length} tasks: ${matches.map((e) => e.task_id.slice(0, 8)).join(', ')} — use a task id instead`);
91
- }
92
- return matches[0];
93
- }
94
- /** Build a filesystem/name-safe slug from a prompt or filename. */
95
- export function slugify(text, maxLen = 32) {
96
- const slug = text
97
- .toLowerCase()
98
- .replace(/\.[a-z0-9]+$/i, '')
99
- .replace(/[^a-z0-9\u4e00-\u9fff]+/gi, '-')
100
- .replace(/^-+|-+$/g, '')
101
- .slice(0, maxLen)
102
- .replace(/-+$/, '');
103
- return slug || 'task';
104
- }
@@ -1,120 +0,0 @@
1
- /**
2
- * Lightweight V3 API client on top of global fetch (Node 20+).
3
- * Handles the response envelope, error translation, timeouts, and
4
- * exponential-backoff retries for transient failures (429/5xx/network).
5
- */
6
- import { loadConfig, resolveApiBaseUrl, resolveApiKey } from '../config/config.js';
7
- import { ApiError, AuthError, NetworkError } from './errors.js';
8
- export class TripoClient {
9
- baseUrl;
10
- apiKey;
11
- timeoutMs;
12
- maxRetries;
13
- onRetry;
14
- constructor(options = {}) {
15
- const config = loadConfig();
16
- this.baseUrl = (options.baseUrl ?? resolveApiBaseUrl(config)).replace(/\/$/, '');
17
- const key = options.apiKey ?? resolveApiKey(config);
18
- if (!key) {
19
- throw new AuthError('no API key configured');
20
- }
21
- this.apiKey = key;
22
- this.timeoutMs = options.timeoutMs ?? 60_000;
23
- this.maxRetries = options.maxRetries ?? 3;
24
- this.onRetry = options.onRetry;
25
- }
26
- async request(method, path, body) {
27
- return this.send(method, path, {
28
- headers: { 'Content-Type': 'application/json' },
29
- body: body === undefined ? undefined : JSON.stringify(body),
30
- });
31
- }
32
- async uploadFile(fileName, data) {
33
- const form = new FormData();
34
- form.append('file', data, fileName);
35
- return this.send('POST', '/v3/files', { body: form });
36
- }
37
- async send(method, path, init) {
38
- let lastError;
39
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
40
- if (attempt > 0) {
41
- const delay = Math.min(1000 * 2 ** (attempt - 1), 8000);
42
- this.onRetry?.(attempt, delay, lastError?.message ?? 'retry');
43
- await sleep(delay);
44
- }
45
- try {
46
- return await this.sendOnce(method, path, init);
47
- }
48
- catch (err) {
49
- lastError = err;
50
- const retryable = err instanceof NetworkError || (err instanceof ApiError && err.retryable);
51
- if (!retryable || attempt === this.maxRetries)
52
- throw err;
53
- }
54
- }
55
- /* c8 ignore next */
56
- throw lastError ?? new NetworkError('request failed');
57
- }
58
- async sendOnce(method, path, init) {
59
- const controller = new AbortController();
60
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
61
- let response;
62
- try {
63
- response = await fetch(this.baseUrl + path, {
64
- method,
65
- headers: { Authorization: `Bearer ${this.apiKey}`, ...init.headers },
66
- body: init.body,
67
- signal: controller.signal,
68
- });
69
- }
70
- catch (err) {
71
- const reason = err instanceof Error ? err.message : String(err);
72
- throw new NetworkError(controller.signal.aborted ? `request timed out after ${this.timeoutMs}ms: ${method} ${path}` : `network error: ${reason}`);
73
- }
74
- finally {
75
- clearTimeout(timer);
76
- }
77
- let envelope;
78
- try {
79
- envelope = (await response.json());
80
- }
81
- catch {
82
- if (!response.ok) {
83
- throw new ApiError({ httpStatus: response.status, message: `HTTP ${response.status} with non-JSON body` });
84
- }
85
- throw new NetworkError(`invalid JSON response from ${path}`);
86
- }
87
- if (!response.ok || envelope.code !== 0) {
88
- throw new ApiError({
89
- apiCode: envelope.code !== 0 ? envelope.code : undefined,
90
- httpStatus: response.status,
91
- message: envelope.message,
92
- suggestion: envelope.suggestion,
93
- requestId: envelope.request_id,
94
- });
95
- }
96
- return envelope.data;
97
- }
98
- // ---- typed convenience wrappers ----
99
- getTask(taskId, step = false) {
100
- const suffix = step ? '?step=1' : '';
101
- return this.request('GET', `/v3/tasks/${encodeURIComponent(taskId)}${suffix}`);
102
- }
103
- listTasks(taskIds) {
104
- return this.request('POST', '/v3/tasks/list', { task_ids: taskIds });
105
- }
106
- getBalance() {
107
- return this.request('GET', '/v3/account/balance');
108
- }
109
- getUsage() {
110
- return this.request('GET', '/v3/account/usage');
111
- }
112
- /** Create a task; returns its id. Response may be {task_id} or a full detail. */
113
- async createTask(endpoint, payload) {
114
- const data = await this.request('POST', endpoint, payload);
115
- return data.task_id;
116
- }
117
- }
118
- export function sleep(ms) {
119
- return new Promise((resolve) => setTimeout(resolve, ms));
120
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * Artifact download: walk task.output for URL-bearing fields, fetch each into
3
- * the output directory, and write task.json (the reproducibility record).
4
- * Output URLs expire in ~5 minutes, so downloads happen immediately after
5
- * completion and URLs are never cached.
6
- */
7
- import { createWriteStream } from 'node:fs';
8
- import { mkdir, writeFile } from 'node:fs/promises';
9
- import { basename, extname, join } from 'node:path';
10
- import { Readable } from 'node:stream';
11
- import { pipeline } from 'node:stream/promises';
12
- import { NetworkError } from './errors.js';
13
- const MODEL_EXTS = new Set(['.glb', '.gltf', '.fbx', '.obj', '.stl', '.3mf', '.usdz', '.zip', '.ply']);
14
- const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
15
- /** Collect every downloadable URL in the output map (string or {url} object). */
16
- export function collectOutputUrls(output) {
17
- if (!output)
18
- return [];
19
- const found = [];
20
- const visit = (value, path) => {
21
- if (typeof value === 'string') {
22
- if (/^https?:\/\//.test(value))
23
- found.push({ field: path, url: value });
24
- return;
25
- }
26
- if (Array.isArray(value)) {
27
- value.forEach((v, i) => visit(v, `${path}[${i}]`));
28
- return;
29
- }
30
- if (value && typeof value === 'object') {
31
- for (const [k, v] of Object.entries(value))
32
- visit(v, path ? `${path}.${k}` : k);
33
- }
34
- };
35
- visit(output, '');
36
- return found;
37
- }
38
- function fileNameFor(field, url) {
39
- const urlPath = new URL(url).pathname;
40
- let ext = extname(urlPath).toLowerCase();
41
- const base = field
42
- .replace(/_url$/, '')
43
- .replace(/[^a-z0-9_.\-[\]]/gi, '_')
44
- .replace(/[[\]]/g, '_')
45
- .replace(/_+/g, '_')
46
- .replace(/^_|_$/g, '');
47
- if (!ext) {
48
- // rendered images are usually webp/png; models glb — guess from field name
49
- ext = /image|preview|render/i.test(field) ? '.png' : '.bin';
50
- }
51
- return `${base || basename(urlPath) || 'artifact'}${ext}`;
52
- }
53
- async function downloadOne(url, dest) {
54
- const response = await fetch(url);
55
- if (!response.ok || !response.body) {
56
- throw new NetworkError(`download failed (HTTP ${response.status}): ${dest}`);
57
- }
58
- await pipeline(Readable.fromWeb(response.body), createWriteStream(dest));
59
- }
60
- /** Download all artifacts of a finished task + write task.json. */
61
- export async function downloadArtifacts(task, outputDir, historyEntry, options = {}) {
62
- await mkdir(outputDir, { recursive: true });
63
- const urls = collectOutputUrls(task.output);
64
- const files = [];
65
- let modelFile;
66
- let preview;
67
- for (const { field, url } of urls) {
68
- const name = fileNameFor(field, url);
69
- const dest = join(outputDir, name);
70
- options.onFile?.(name);
71
- await downloadOne(url, dest);
72
- files.push(dest);
73
- const ext = extname(name).toLowerCase();
74
- if (!modelFile && MODEL_EXTS.has(ext))
75
- modelFile = dest;
76
- if (!preview && (IMAGE_EXTS.has(ext) || /rendered|preview/i.test(field)))
77
- preview = dest;
78
- }
79
- // Normalize the preview name so agents can rely on preview.png existing.
80
- if (preview && basename(preview) !== 'preview.png') {
81
- const { copyFile } = await import('node:fs/promises');
82
- const canonical = join(outputDir, 'preview.png');
83
- try {
84
- await copyFile(preview, canonical);
85
- files.push(canonical);
86
- preview = canonical;
87
- }
88
- catch { }
89
- }
90
- const record = {
91
- task_id: task.task_id,
92
- type: task.type,
93
- status: task.status,
94
- input: task.input ?? historyEntry?.payload ?? {},
95
- output_fields: urls.map((u) => u.field),
96
- credits_consumed: task.credits_consumed,
97
- created_at: task.created_at,
98
- completed_at: task.completed_at,
99
- files: files.map((f) => basename(f)),
100
- };
101
- await writeFile(join(outputDir, 'task.json'), JSON.stringify(record, null, 2) + '\n');
102
- return { outputDir, files, modelFile, preview };
103
- }
104
- /** Default output directory: ./tripo-out/<slug>-<id8>/ */
105
- export function defaultOutputDir(baseDir, slug, taskId) {
106
- return join(baseDir, 'tripo-out', `${slug}-${taskId.slice(0, 8)}`);
107
- }
@@ -1,64 +0,0 @@
1
- /** CLI error types carrying stable exit codes and human translations. */
2
- import { t } from '../i18n.js';
3
- import { EXIT, exitCodeFor, isRetryable, lookupError } from '../knowledge/error-catalog.js';
4
- export class CliError extends Error {
5
- exitCode;
6
- suggestion;
7
- constructor(message, exitCode = EXIT.FATAL, suggestion) {
8
- super(message);
9
- this.exitCode = exitCode;
10
- this.suggestion = suggestion;
11
- }
12
- }
13
- export class UsageError extends CliError {
14
- constructor(message, suggestion) {
15
- super(message, EXIT.USAGE, suggestion);
16
- }
17
- }
18
- export class AuthError extends CliError {
19
- constructor(message, suggestion) {
20
- super(message, EXIT.AUTH, suggestion ?? t({ zh: '运行 tripo login 配置 API Key', en: 'run "tripo login" to configure an API key' }));
21
- }
22
- }
23
- export class ApiError extends CliError {
24
- apiCode;
25
- httpStatus;
26
- requestId;
27
- retryable;
28
- constructor(opts) {
29
- const info = opts.apiCode !== undefined ? lookupError(opts.apiCode) : undefined;
30
- const translated = info ? t(info) : undefined;
31
- const parts = [translated ?? opts.message ?? `API error (HTTP ${opts.httpStatus})`];
32
- if (translated && opts.message && opts.message !== translated)
33
- parts.push(`(${opts.message})`);
34
- super(parts.join(' '), exitCodeFor(opts.apiCode, opts.httpStatus), opts.suggestion);
35
- this.apiCode = opts.apiCode;
36
- this.httpStatus = opts.httpStatus;
37
- this.requestId = opts.requestId;
38
- this.retryable = isRetryable(opts.apiCode, opts.httpStatus);
39
- }
40
- }
41
- export class NetworkError extends CliError {
42
- constructor(message) {
43
- super(message, EXIT.NETWORK);
44
- }
45
- }
46
- export class TaskFailedError extends CliError {
47
- taskId;
48
- constructor(taskId, status, detail) {
49
- const refundNote = t({
50
- zh: '预扣积分已自动退还',
51
- en: 'frozen credits have been refunded automatically',
52
- });
53
- super(`task ${taskId} ${status}${detail ? `: ${detail}` : ''} (${refundNote})`, EXIT.TASK_FAILED);
54
- this.taskId = taskId;
55
- }
56
- }
57
- /** Decorate insufficient-balance errors with the topup path and the gap. */
58
- export function balanceSuggestion(needed, balance) {
59
- const gap = needed !== undefined && balance !== undefined ? Math.max(0, needed - balance) : undefined;
60
- return t({
61
- zh: `运行 tripo topup 充值${gap !== undefined ? `(还差约 ${gap} 积分)` : ''}`,
62
- en: `run "tripo topup" to add credits${gap !== undefined ? ` (about ${gap} more needed)` : ''}`,
63
- });
64
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * Output discipline (the iron rule):
3
- * - stdout carries ONLY the final business result (JSON when piped/--json,
4
- * human-readable text when interactive).
5
- * - stderr carries all progress, logs, prompts, and decoration.
6
- * Non-TTY environments automatically behave like --json --yes --no-open.
7
- */
8
- import pc from 'picocolors';
9
- const state = { json: false, yes: false, quiet: false, noOpen: false };
10
- export function stdoutIsTTY() {
11
- return Boolean(process.stdout.isTTY);
12
- }
13
- export function stdinIsTTY() {
14
- return Boolean(process.stdin.isTTY);
15
- }
16
- export function configureOutput(flags) {
17
- Object.assign(state, flags);
18
- // Headless environments (pipes, agents, CI) get machine output + no prompts.
19
- if (!stdoutIsTTY())
20
- state.json = true;
21
- if (!stdinIsTTY())
22
- state.yes = state.yes || !stdoutIsTTY();
23
- if (!stdoutIsTTY() || !stdinIsTTY())
24
- state.noOpen = true;
25
- }
26
- export function isJsonMode() {
27
- return state.json;
28
- }
29
- export function isYes() {
30
- return state.yes;
31
- }
32
- export function isQuiet() {
33
- return state.quiet;
34
- }
35
- export function shouldOpen() {
36
- return !state.noOpen;
37
- }
38
- export function isInteractive() {
39
- return stdinIsTTY() && stdoutIsTTY() && !state.yes;
40
- }
41
- /** Progress/log line → stderr. */
42
- export function log(message) {
43
- if (!state.quiet)
44
- process.stderr.write(message + '\n');
45
- }
46
- export function warn(message) {
47
- process.stderr.write(pc.yellow(`! ${message}`) + '\n');
48
- }
49
- export function errorLine(message) {
50
- process.stderr.write(pc.red(`✗ ${message}`) + '\n');
51
- }
52
- export function success(message) {
53
- if (!state.quiet)
54
- process.stderr.write(pc.green(`✓ ${message}`) + '\n');
55
- }
56
- /**
57
- * Final business result → stdout.
58
- * JSON mode prints a single line (NDJSON-safe); human mode prints the
59
- * formatted text (or a default rendering).
60
- */
61
- export function emitResult(data, human) {
62
- if (state.json) {
63
- process.stdout.write(JSON.stringify(data) + '\n');
64
- }
65
- else {
66
- process.stdout.write((human ?? JSON.stringify(data, null, 2)) + '\n');
67
- }
68
- }
69
- /** Stream one NDJSON event to stdout (used by watch-style streaming output). */
70
- export function emitNdjson(event) {
71
- process.stdout.write(JSON.stringify(event) + '\n');
72
- }