tripo-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/dist/ai/experts.js +115 -0
- package/dist/ai/llm.js +88 -0
- package/dist/ai/plan-card.js +79 -0
- package/dist/ai/wizard.js +105 -0
- package/dist/cli.js +7 -0
- package/dist/commands/account.js +27 -0
- package/dist/commands/ai.js +114 -0
- package/dist/commands/batch.js +148 -0
- package/dist/commands/completion.js +84 -0
- package/dist/commands/config-cmd.js +84 -0
- package/dist/commands/docs.js +30 -0
- package/dist/commands/doctor.js +102 -0
- package/dist/commands/generate.js +81 -0
- package/dist/commands/home.js +68 -0
- package/dist/commands/login.js +309 -0
- package/dist/commands/make.js +142 -0
- package/dist/commands/mcp.js +194 -0
- package/dist/commands/process-groups.js +166 -0
- package/dist/commands/shared.js +75 -0
- package/dist/commands/task.js +128 -0
- package/dist/commands/topup.js +61 -0
- package/dist/commands/view.js +97 -0
- package/dist/config/config.js +99 -0
- package/dist/config/context.js +44 -0
- package/dist/config/history.js +104 -0
- package/dist/core/client.js +129 -0
- package/dist/core/device-auth.js +141 -0
- package/dist/core/download.js +107 -0
- package/dist/core/errors.js +64 -0
- package/dist/core/output.js +72 -0
- package/dist/core/region.js +45 -0
- package/dist/core/requests.js +197 -0
- package/dist/core/task-service.js +167 -0
- package/dist/core/upload.js +42 -0
- package/dist/core/viewer-assets.js +24 -0
- package/dist/core/viewer.js +59 -0
- package/dist/core/watch.js +42 -0
- package/dist/i18n.js +19 -0
- package/dist/knowledge/chains.js +157 -0
- package/dist/knowledge/error-catalog.js +91 -0
- package/dist/knowledge/models.js +158 -0
- package/dist/knowledge/params.js +203 -0
- package/dist/knowledge/scenarios.js +112 -0
- package/dist/pipeline/make.js +233 -0
- package/dist/pipeline/runner.js +73 -0
- package/dist/pipeline/stdin.js +40 -0
- package/dist/program.js +147 -0
- package/dist/src/ai/experts.js +115 -0
- package/dist/src/ai/llm.js +88 -0
- package/dist/src/ai/plan-card.js +79 -0
- package/dist/src/ai/wizard.js +104 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/account.js +27 -0
- package/dist/src/commands/ai.js +113 -0
- package/dist/src/commands/batch.js +148 -0
- package/dist/src/commands/completion.js +84 -0
- package/dist/src/commands/config-cmd.js +84 -0
- package/dist/src/commands/docs.js +30 -0
- package/dist/src/commands/doctor.js +94 -0
- package/dist/src/commands/generate.js +81 -0
- package/dist/src/commands/home.js +68 -0
- package/dist/src/commands/login.js +225 -0
- package/dist/src/commands/make.js +142 -0
- package/dist/src/commands/mcp.js +194 -0
- package/dist/src/commands/process-groups.js +164 -0
- package/dist/src/commands/shared.js +75 -0
- package/dist/src/commands/task.js +127 -0
- package/dist/src/commands/topup.js +61 -0
- package/dist/src/commands/view.js +97 -0
- package/dist/src/config/config.js +79 -0
- package/dist/src/config/context.js +44 -0
- package/dist/src/config/history.js +104 -0
- package/dist/src/core/client.js +120 -0
- package/dist/src/core/download.js +107 -0
- package/dist/src/core/errors.js +64 -0
- package/dist/src/core/output.js +72 -0
- package/dist/src/core/requests.js +169 -0
- package/dist/src/core/task-service.js +161 -0
- package/dist/src/core/upload.js +36 -0
- package/dist/src/core/viewer-assets.js +24 -0
- package/dist/src/core/viewer.js +59 -0
- package/dist/src/core/watch.js +41 -0
- package/dist/src/i18n.js +19 -0
- package/dist/src/knowledge/chains.js +152 -0
- package/dist/src/knowledge/error-catalog.js +91 -0
- package/dist/src/knowledge/models.js +110 -0
- package/dist/src/knowledge/params.js +174 -0
- package/dist/src/knowledge/scenarios.js +112 -0
- package/dist/src/pipeline/make.js +233 -0
- package/dist/src/pipeline/runner.js +73 -0
- package/dist/src/pipeline/stdin.js +40 -0
- package/dist/src/program.js +147 -0
- package/dist/src/types.js +2 -0
- package/dist/src/util/input-detect.js +92 -0
- package/dist/src/util/notify.js +17 -0
- package/dist/src/util/open.js +18 -0
- package/dist/src/util/spinner.js +62 -0
- package/dist/src/util/term-image.js +28 -0
- package/dist/src/version.js +10 -0
- package/dist/types.js +2 -0
- package/dist/util/input-detect.js +93 -0
- package/dist/util/notify.js +17 -0
- package/dist/util/open.js +18 -0
- package/dist/util/spinner.js +62 -0
- package/dist/util/term-image.js +28 -0
- package/dist/version.js +10 -0
- package/package.json +63 -0
- package/skill/SKILL.md +70 -0
- package/skill/commands/account.md +41 -0
- package/skill/commands/batch.md +33 -0
- package/skill/commands/generate.md +34 -0
- package/skill/commands/make.md +47 -0
- package/skill/commands/process.md +50 -0
- package/skill/commands/task.md +27 -0
- package/skill/commands/view.md +25 -0
- package/skill/common-errors.md +38 -0
- package/skill/examples/animation.md +33 -0
- package/skill/examples/ar-web.md +23 -0
- package/skill/examples/film.md +22 -0
- package/skill/examples/game-asset.md +35 -0
- package/skill/examples/pipes.md +39 -0
- package/skill/examples/print.md +31 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain runner: execute a validated sequence of processing steps, feeding
|
|
3
|
+
* each step the previous task id. rig-check gates the rig step (stop early
|
|
4
|
+
* when the model is not riggable, and adopt the recommended rig_type).
|
|
5
|
+
*/
|
|
6
|
+
import { UsageError } from '../core/errors.js';
|
|
7
|
+
import { log, warn } from '../core/output.js';
|
|
8
|
+
import { buildStepRequest } from '../core/requests.js';
|
|
9
|
+
import { runTask } from '../core/task-service.js';
|
|
10
|
+
import { t } from '../i18n.js';
|
|
11
|
+
import { getStep, validateChain } from '../knowledge/chains.js';
|
|
12
|
+
export async function runChain(options) {
|
|
13
|
+
const { client, chain } = options;
|
|
14
|
+
validateChain(chain, options.sourceType);
|
|
15
|
+
const results = [];
|
|
16
|
+
let currentTaskId = options.sourceTaskId;
|
|
17
|
+
let rigTypeRecommendation;
|
|
18
|
+
for (let i = 0; i < chain.length; i++) {
|
|
19
|
+
const spec = chain[i];
|
|
20
|
+
const args = { ...spec.args };
|
|
21
|
+
if (spec.step === 'rig' && rigTypeRecommendation && args.rig_type === undefined) {
|
|
22
|
+
args.rig_type = rigTypeRecommendation;
|
|
23
|
+
log(t({ zh: `采用 rig-check 推荐体型:${rigTypeRecommendation}`, en: `using rig-check recommendation: ${rigTypeRecommendation}` }));
|
|
24
|
+
}
|
|
25
|
+
const built = buildStepRequest(spec.step, currentTaskId, args);
|
|
26
|
+
for (const w of built.warnings)
|
|
27
|
+
warn(w);
|
|
28
|
+
const result = await runTask({
|
|
29
|
+
client,
|
|
30
|
+
request: built.request,
|
|
31
|
+
taskType: built.taskType,
|
|
32
|
+
name: `${options.namePrefix}-${spec.step}`,
|
|
33
|
+
outDir: options.outDir,
|
|
34
|
+
notify: false,
|
|
35
|
+
timeoutMs: options.timeoutMs,
|
|
36
|
+
// Intermediate steps skip download; final step downloads.
|
|
37
|
+
noDownload: i < chain.length - 1 && spec.step !== 'rig-check',
|
|
38
|
+
precheckBalance: false, // checked once by the caller
|
|
39
|
+
});
|
|
40
|
+
results.push(result);
|
|
41
|
+
if (spec.step === 'rig-check') {
|
|
42
|
+
const verdict = await client.getTask(result.task_id);
|
|
43
|
+
const output = verdict.output ?? {};
|
|
44
|
+
const riggable = output.riggable;
|
|
45
|
+
rigTypeRecommendation = typeof output.rig_type === 'string' ? output.rig_type : undefined;
|
|
46
|
+
if (riggable === false) {
|
|
47
|
+
const message = t({
|
|
48
|
+
zh: 'rig-check 判定该模型不可绑骨,动画链止损停止(后续步骤未执行,未扣积分)',
|
|
49
|
+
en: 'rig-check says this model is not riggable; stopping the chain early (no further credits spent)',
|
|
50
|
+
});
|
|
51
|
+
warn(message);
|
|
52
|
+
return { results, stoppedEarly: message };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Gate steps (passthrough) inspect the model; the chain continues from
|
|
56
|
+
// the same upstream task.
|
|
57
|
+
if (!getStep(spec.step)?.passthrough) {
|
|
58
|
+
currentTaskId = result.task_id;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { results };
|
|
62
|
+
}
|
|
63
|
+
/** Ensure a source task exists + succeeded before chaining onto it. */
|
|
64
|
+
export async function assertSourceReady(client, taskId) {
|
|
65
|
+
const task = await client.getTask(taskId);
|
|
66
|
+
if (task.status !== 'success') {
|
|
67
|
+
throw new UsageError(`task ${taskId} is ${task.status}; processing steps need a successful upstream task` +
|
|
68
|
+
(task.status === 'running' || task.status === 'queued'
|
|
69
|
+
? ` (wait with "tripo task watch ${taskId}")`
|
|
70
|
+
: ''));
|
|
71
|
+
}
|
|
72
|
+
return task.type;
|
|
73
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON pipe support: downstream commands read the upstream task result from
|
|
3
|
+
* stdin, enabling `tripo make cat.png | tripo model texture | tripo anim rig`.
|
|
4
|
+
*/
|
|
5
|
+
/** Read piped NDJSON/JSON from stdin; returns undefined on a TTY. */
|
|
6
|
+
export async function readPipedTask() {
|
|
7
|
+
if (process.stdin.isTTY)
|
|
8
|
+
return undefined;
|
|
9
|
+
const raw = await readAll(process.stdin);
|
|
10
|
+
return parsePipedText(raw);
|
|
11
|
+
}
|
|
12
|
+
/** Parse piped text: JSON object, NDJSON (last task line wins), or a bare task id. */
|
|
13
|
+
export function parsePipedText(raw) {
|
|
14
|
+
const text = raw.trim();
|
|
15
|
+
if (!text)
|
|
16
|
+
return undefined;
|
|
17
|
+
const lines = text.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
18
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19
|
+
try {
|
|
20
|
+
const obj = JSON.parse(lines[i]);
|
|
21
|
+
if (typeof obj.task_id === 'string') {
|
|
22
|
+
return { task_id: obj.task_id, type: typeof obj.type === 'string' ? obj.type : undefined };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
}
|
|
27
|
+
const last = lines[lines.length - 1];
|
|
28
|
+
if (/^(task_[A-Za-z0-9_-]+|[0-9a-f-]{36})$/i.test(last))
|
|
29
|
+
return { task_id: last };
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function readAll(stream) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
let data = '';
|
|
35
|
+
stream.setEncoding('utf8');
|
|
36
|
+
stream.on('data', (chunk) => (data += chunk));
|
|
37
|
+
stream.on('end', () => resolve(data));
|
|
38
|
+
stream.on('error', reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Command tree assembly + global flags + the error → exit-code boundary. */
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { registerAccountCommands } from './commands/account.js';
|
|
5
|
+
import { registerAiCommand } from './commands/ai.js';
|
|
6
|
+
import { registerAuthCommands } from './commands/login.js';
|
|
7
|
+
import { registerBatchCommand } from './commands/batch.js';
|
|
8
|
+
import { registerCompletionCommand } from './commands/completion.js';
|
|
9
|
+
import { registerConfigCommands } from './commands/config-cmd.js';
|
|
10
|
+
import { registerDoctorCommand } from './commands/doctor.js';
|
|
11
|
+
import { registerDocsCommand } from './commands/docs.js';
|
|
12
|
+
import { registerGenerateCommands } from './commands/generate.js';
|
|
13
|
+
import { registerHomeAction } from './commands/home.js';
|
|
14
|
+
import { registerMakeCommand } from './commands/make.js';
|
|
15
|
+
import { registerMcpCommand } from './commands/mcp.js';
|
|
16
|
+
import { registerProcessCommands } from './commands/process-groups.js';
|
|
17
|
+
import { registerTaskCommands } from './commands/task.js';
|
|
18
|
+
import { registerTopupCommand } from './commands/topup.js';
|
|
19
|
+
import { registerViewCommands } from './commands/view.js';
|
|
20
|
+
import { loadConfig } from './config/config.js';
|
|
21
|
+
import { RefError } from './config/history.js';
|
|
22
|
+
import { ApiError, CliError, UsageError } from './core/errors.js';
|
|
23
|
+
import { configureOutput, errorLine, isJsonMode } from './core/output.js';
|
|
24
|
+
import { setLocale, t } from './i18n.js';
|
|
25
|
+
import { EXIT } from './knowledge/error-catalog.js';
|
|
26
|
+
import { cliVersion } from './version.js';
|
|
27
|
+
export function buildProgram() {
|
|
28
|
+
const program = new Command();
|
|
29
|
+
program
|
|
30
|
+
.name('tripo')
|
|
31
|
+
.description(t({
|
|
32
|
+
zh: 'Tripo 3D 智能 CLI — 一句话/一张图,直出可进引擎的 3D 资产\n高频: make(生成) ai(专家规划) view(预览) topup(充值)',
|
|
33
|
+
en: 'Tripo 3D intelligent CLI — turn a sentence or an image into engine-ready 3D assets\ntop commands: make · ai · view · topup',
|
|
34
|
+
}))
|
|
35
|
+
.version(cliVersion(), '-v, --version')
|
|
36
|
+
.option('--json', 'machine-readable output on stdout (auto-enabled when piped)')
|
|
37
|
+
.option('--yes', 'skip confirmations (auto-enabled when headless)')
|
|
38
|
+
.option('--quiet', 'suppress progress logs on stderr')
|
|
39
|
+
.option('--no-open', 'never auto-open the browser/viewer')
|
|
40
|
+
.enablePositionalOptions()
|
|
41
|
+
.showSuggestionAfterError(true);
|
|
42
|
+
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
43
|
+
configureOutput({
|
|
44
|
+
json: resolveGlobalFlag(actionCommand, 'json', false),
|
|
45
|
+
yes: resolveGlobalFlag(actionCommand, 'yes', false),
|
|
46
|
+
quiet: resolveGlobalFlag(actionCommand, 'quiet', false),
|
|
47
|
+
noOpen: !resolveGlobalFlag(actionCommand, 'open', true),
|
|
48
|
+
});
|
|
49
|
+
const lang = loadConfig().language;
|
|
50
|
+
if (lang)
|
|
51
|
+
setLocale(lang);
|
|
52
|
+
});
|
|
53
|
+
registerMakeCommand(program);
|
|
54
|
+
registerAiCommand(program);
|
|
55
|
+
registerViewCommands(program);
|
|
56
|
+
registerAuthCommands(program);
|
|
57
|
+
registerTopupCommand(program);
|
|
58
|
+
registerAccountCommands(program);
|
|
59
|
+
registerGenerateCommands(program);
|
|
60
|
+
registerProcessCommands(program);
|
|
61
|
+
registerTaskCommands(program);
|
|
62
|
+
registerBatchCommand(program);
|
|
63
|
+
registerConfigCommands(program);
|
|
64
|
+
registerDoctorCommand(program);
|
|
65
|
+
registerDocsCommand(program);
|
|
66
|
+
registerCompletionCommand(program);
|
|
67
|
+
registerMcpCommand(program);
|
|
68
|
+
registerHomeAction(program);
|
|
69
|
+
attachGlobalFlags(program);
|
|
70
|
+
return program;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The 4 global flags live on every leaf command so `tripo balance --json`
|
|
74
|
+
* works the way people type it (prefix `tripo --json balance` works too).
|
|
75
|
+
*/
|
|
76
|
+
function attachGlobalFlags(cmd) {
|
|
77
|
+
for (const sub of cmd.commands)
|
|
78
|
+
attachGlobalFlags(sub);
|
|
79
|
+
if (cmd.commands.length > 0)
|
|
80
|
+
return;
|
|
81
|
+
const has = (long) => cmd.options.some((o) => o.long === long);
|
|
82
|
+
if (!has('--json'))
|
|
83
|
+
cmd.option('--json', 'machine-readable output on stdout');
|
|
84
|
+
if (!has('--yes'))
|
|
85
|
+
cmd.option('--yes', 'skip confirmations');
|
|
86
|
+
if (!has('--quiet'))
|
|
87
|
+
cmd.option('--quiet', 'suppress progress logs on stderr');
|
|
88
|
+
if (!has('--no-open'))
|
|
89
|
+
cmd.option('--no-open', 'never auto-open the browser/viewer');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a flag along the command chain, innermost explicit value first.
|
|
93
|
+
* (optsWithGlobals can't do this: a leaf's *default* would shadow an explicit
|
|
94
|
+
* `tripo --no-open <cmd>` on the root.)
|
|
95
|
+
*/
|
|
96
|
+
function resolveGlobalFlag(cmd, key, def) {
|
|
97
|
+
for (let c = cmd; c; c = c.parent) {
|
|
98
|
+
const source = c.getOptionValueSource(key);
|
|
99
|
+
if (source !== undefined && source !== 'default')
|
|
100
|
+
return Boolean(c.getOptionValue(key));
|
|
101
|
+
}
|
|
102
|
+
return def;
|
|
103
|
+
}
|
|
104
|
+
/** Run the CLI; converts errors into the stable exit-code contract. */
|
|
105
|
+
export async function runCli(argv) {
|
|
106
|
+
const program = buildProgram();
|
|
107
|
+
try {
|
|
108
|
+
await program.parseAsync(argv);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
handleError(err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function handleError(err) {
|
|
115
|
+
// History reference misses (@last with empty history etc.) are usage errors.
|
|
116
|
+
if (err instanceof RefError)
|
|
117
|
+
err = new UsageError(err.message);
|
|
118
|
+
if (err instanceof CliError) {
|
|
119
|
+
if (isJsonMode()) {
|
|
120
|
+
process.stdout.write(JSON.stringify({
|
|
121
|
+
error: err.message,
|
|
122
|
+
exit_code: err.exitCode,
|
|
123
|
+
...(err instanceof ApiError
|
|
124
|
+
? { api_code: err.apiCode, request_id: err.requestId, http_status: err.httpStatus }
|
|
125
|
+
: {}),
|
|
126
|
+
...(err.suggestion ? { suggestion: err.suggestion } : {}),
|
|
127
|
+
}) + '\n');
|
|
128
|
+
}
|
|
129
|
+
errorLine(err.message);
|
|
130
|
+
if (err.suggestion)
|
|
131
|
+
process.stderr.write(pc.dim(` → ${err.suggestion}\n`));
|
|
132
|
+
if (err instanceof ApiError && err.requestId) {
|
|
133
|
+
process.stderr.write(pc.dim(` request_id: ${err.requestId}\n`));
|
|
134
|
+
}
|
|
135
|
+
process.exit(err.exitCode);
|
|
136
|
+
}
|
|
137
|
+
// commander already printed its own message for help/version pseudo-errors
|
|
138
|
+
if (typeof err === 'object' && err !== null && 'code' in err) {
|
|
139
|
+
const code = err.code ?? '';
|
|
140
|
+
if (code.startsWith('commander.')) {
|
|
141
|
+
process.exit(code === 'commander.help' || code === 'commander.version' || code === 'commander.helpDisplayed' ? 0 : EXIT.USAGE);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
145
|
+
errorLine(message);
|
|
146
|
+
process.exit(EXIT.FATAL);
|
|
147
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input classification for `tripo make` and friends: figure out what the user
|
|
3
|
+
* gave us (text prompt / image / model file / URL / task id / history ref)
|
|
4
|
+
* without asking them to know the difference.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, statSync } from 'node:fs';
|
|
7
|
+
import { extname } from 'node:path';
|
|
8
|
+
export const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.bmp']);
|
|
9
|
+
export const MODEL_EXTS = new Set(['.glb', '.gltf', '.fbx', '.obj', '.stl']);
|
|
10
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
const TASK_RE = /^task_[A-Za-z0-9_-]+$/;
|
|
12
|
+
const FILE_TOKEN_RE = /^file_[A-Za-z0-9_-]+$/;
|
|
13
|
+
export function detectInput(raw) {
|
|
14
|
+
const value = raw.trim();
|
|
15
|
+
if (value.startsWith('@'))
|
|
16
|
+
return { kind: 'history-ref', value };
|
|
17
|
+
if (FILE_TOKEN_RE.test(value))
|
|
18
|
+
return { kind: 'file-token', value };
|
|
19
|
+
if (TASK_RE.test(value) || UUID_RE.test(value))
|
|
20
|
+
return { kind: 'task-id', value };
|
|
21
|
+
if (/^https?:\/\//i.test(value))
|
|
22
|
+
return { kind: 'url', value };
|
|
23
|
+
if (isExistingFile(value)) {
|
|
24
|
+
const ext = extname(value).toLowerCase();
|
|
25
|
+
if (IMAGE_EXTS.has(ext))
|
|
26
|
+
return { kind: 'image-file', value };
|
|
27
|
+
if (MODEL_EXTS.has(ext))
|
|
28
|
+
return { kind: 'model-file', value };
|
|
29
|
+
// A real file with an unknown extension is still most likely an asset;
|
|
30
|
+
// treat by extension guess, otherwise reject early with a clear message.
|
|
31
|
+
throw new InputDetectError(`"${value}" exists but has an unsupported extension "${ext}" (images: ${[...IMAGE_EXTS].join(' ')}; models: ${[...MODEL_EXTS].join(' ')})`);
|
|
32
|
+
}
|
|
33
|
+
// Looks like a path but doesn't exist → almost certainly a typo, not a prompt.
|
|
34
|
+
if (looksLikePath(value)) {
|
|
35
|
+
throw new InputDetectError(`file not found: "${value}" (if you meant a text prompt, quote a sentence instead)`);
|
|
36
|
+
}
|
|
37
|
+
return { kind: 'text', value };
|
|
38
|
+
}
|
|
39
|
+
export function classifyInputs(args) {
|
|
40
|
+
if (args.length === 0)
|
|
41
|
+
throw new InputDetectError('no input given');
|
|
42
|
+
const detected = args.map(detectInput);
|
|
43
|
+
const kinds = new Set(detected.map((d) => d.kind));
|
|
44
|
+
if (kinds.size === 1) {
|
|
45
|
+
const kind = detected[0].kind;
|
|
46
|
+
switch (kind) {
|
|
47
|
+
case 'text':
|
|
48
|
+
// Multiple bare words are one prompt, joined.
|
|
49
|
+
return { kind: 'text', values: [{ kind: 'text', value: args.join(' ') }] };
|
|
50
|
+
case 'image-file':
|
|
51
|
+
return detected.length === 1
|
|
52
|
+
? { kind: 'image', values: detected }
|
|
53
|
+
: { kind: 'multiview', values: detected };
|
|
54
|
+
case 'model-file':
|
|
55
|
+
if (detected.length > 1)
|
|
56
|
+
throw new InputDetectError('only one model file at a time');
|
|
57
|
+
return { kind: 'model', values: detected };
|
|
58
|
+
case 'url':
|
|
59
|
+
if (detected.length > 1)
|
|
60
|
+
return { kind: 'multiview', values: detected };
|
|
61
|
+
return { kind: 'url-image', values: detected };
|
|
62
|
+
case 'task-id':
|
|
63
|
+
case 'history-ref':
|
|
64
|
+
case 'file-token':
|
|
65
|
+
if (detected.length > 1)
|
|
66
|
+
throw new InputDetectError('only one task reference at a time');
|
|
67
|
+
return { kind: 'task', values: detected };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Mixed images + urls → multiview; anything else mixed is ambiguous.
|
|
71
|
+
if ([...kinds].every((k) => k === 'image-file' || k === 'url')) {
|
|
72
|
+
return { kind: 'multiview', values: detected };
|
|
73
|
+
}
|
|
74
|
+
throw new InputDetectError(`mixed input types (${[...kinds].join(' + ')}) — pass one kind of input per command`);
|
|
75
|
+
}
|
|
76
|
+
export class InputDetectError extends Error {
|
|
77
|
+
}
|
|
78
|
+
function isExistingFile(p) {
|
|
79
|
+
try {
|
|
80
|
+
return existsSync(p) && statSync(p).isFile();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function looksLikePath(value) {
|
|
87
|
+
if (/\s/.test(value))
|
|
88
|
+
return false;
|
|
89
|
+
const ext = extname(value).toLowerCase();
|
|
90
|
+
return ((value.includes('/') || value.includes('\\') || IMAGE_EXTS.has(ext) || MODEL_EXTS.has(ext)) &&
|
|
91
|
+
!value.includes(' '));
|
|
92
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Desktop notification + terminal bell for long-running task completion. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export function notifyDone(title, message) {
|
|
4
|
+
try {
|
|
5
|
+
if (process.platform === 'darwin') {
|
|
6
|
+
const script = `display notification ${JSON.stringify(message)} with title ${JSON.stringify(title)}`;
|
|
7
|
+
spawn('osascript', ['-e', script], { stdio: 'ignore', detached: true }).on('error', () => { }); // best-effort
|
|
8
|
+
}
|
|
9
|
+
else if (process.platform === 'linux') {
|
|
10
|
+
spawn('notify-send', [title, message], { stdio: 'ignore', detached: true }).on('error', () => { });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
// Terminal bell as the lowest common denominator.
|
|
15
|
+
if (process.stderr.isTTY)
|
|
16
|
+
process.stderr.write('\x07');
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Cross-platform "open in default app/browser" without extra dependencies. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export function openExternal(target) {
|
|
4
|
+
try {
|
|
5
|
+
const [cmd, args] = process.platform === 'darwin'
|
|
6
|
+
? ['open', [target]]
|
|
7
|
+
: process.platform === 'win32'
|
|
8
|
+
? ['cmd', ['/c', 'start', '', target]]
|
|
9
|
+
: ['xdg-open', [target]];
|
|
10
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
11
|
+
child.on('error', () => { });
|
|
12
|
+
child.unref();
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal stderr spinner. Interactive TTY gets an animated line; headless
|
|
3
|
+
* environments get sparse plain-text progress lines (never control codes).
|
|
4
|
+
*/
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
7
|
+
export function createSpinner(initialText, quiet = false) {
|
|
8
|
+
const isTTY = Boolean(process.stderr.isTTY);
|
|
9
|
+
let text = initialText;
|
|
10
|
+
let frame = 0;
|
|
11
|
+
let timer;
|
|
12
|
+
let lastPlainLog = 0;
|
|
13
|
+
const render = () => {
|
|
14
|
+
process.stderr.write(`\r\x1b[2K${pc.cyan(FRAMES[frame % FRAMES.length])} ${text}`);
|
|
15
|
+
frame++;
|
|
16
|
+
};
|
|
17
|
+
const clearLine = () => {
|
|
18
|
+
if (isTTY)
|
|
19
|
+
process.stderr.write('\r\x1b[2K');
|
|
20
|
+
};
|
|
21
|
+
if (!quiet && isTTY) {
|
|
22
|
+
render();
|
|
23
|
+
timer = setInterval(render, 100);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
}
|
|
26
|
+
else if (!quiet) {
|
|
27
|
+
process.stderr.write(`${initialText}\n`);
|
|
28
|
+
lastPlainLog = Date.now();
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
update(next) {
|
|
32
|
+
text = next;
|
|
33
|
+
if (quiet)
|
|
34
|
+
return;
|
|
35
|
+
if (!isTTY) {
|
|
36
|
+
// Throttle non-TTY progress to one line per 5s to keep logs readable.
|
|
37
|
+
if (Date.now() - lastPlainLog > 5000) {
|
|
38
|
+
process.stderr.write(`${next}\n`);
|
|
39
|
+
lastPlainLog = Date.now();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
succeed(finalText) {
|
|
44
|
+
if (timer)
|
|
45
|
+
clearInterval(timer);
|
|
46
|
+
clearLine();
|
|
47
|
+
if (!quiet)
|
|
48
|
+
process.stderr.write(`${pc.green('✓')} ${finalText}\n`);
|
|
49
|
+
},
|
|
50
|
+
fail(finalText) {
|
|
51
|
+
if (timer)
|
|
52
|
+
clearInterval(timer);
|
|
53
|
+
clearLine();
|
|
54
|
+
process.stderr.write(`${pc.red('✗')} ${finalText}\n`);
|
|
55
|
+
},
|
|
56
|
+
stop() {
|
|
57
|
+
if (timer)
|
|
58
|
+
clearInterval(timer);
|
|
59
|
+
clearLine();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline image display for capable terminals (iTerm2 OSC 1337; Kitty graphics
|
|
3
|
+
* protocol consumers also honor the iTerm sequence via icat shims). Falls back
|
|
4
|
+
* to returning false so callers can open the system viewer instead.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
export function terminalSupportsInlineImages() {
|
|
9
|
+
if (!process.stdout.isTTY)
|
|
10
|
+
return false;
|
|
11
|
+
const program = process.env.TERM_PROGRAM ?? '';
|
|
12
|
+
const term = process.env.TERM ?? '';
|
|
13
|
+
return program === 'iTerm.app' || program === 'WezTerm' || term.includes('kitty');
|
|
14
|
+
}
|
|
15
|
+
export function printInlineImage(filePath, stream = process.stderr) {
|
|
16
|
+
if (!terminalSupportsInlineImages())
|
|
17
|
+
return false;
|
|
18
|
+
try {
|
|
19
|
+
const data = readFileSync(filePath);
|
|
20
|
+
const b64 = data.toString('base64');
|
|
21
|
+
const name = Buffer.from(basename(filePath)).toString('base64');
|
|
22
|
+
stream.write(`\x1b]1337;File=name=${name};size=${data.length};inline=1;width=40%:${b64}\x07\n`);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input classification for `tripo make` and friends: figure out what the user
|
|
3
|
+
* gave us (text prompt / image / model file / URL / task id / history ref)
|
|
4
|
+
* without asking them to know the difference.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, statSync } from 'node:fs';
|
|
7
|
+
import { extname } from 'node:path';
|
|
8
|
+
// Server multipart whitelist: images webp/png/jpeg/bmp/tiff (+jpg), meshes glb/obj/fbx/stl/3mf/usdz.
|
|
9
|
+
export const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tiff', '.tif']);
|
|
10
|
+
export const MODEL_EXTS = new Set(['.glb', '.gltf', '.fbx', '.obj', '.stl']);
|
|
11
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
const TASK_RE = /^task_[A-Za-z0-9_-]+$/;
|
|
13
|
+
const FILE_TOKEN_RE = /^file_[A-Za-z0-9_-]+$/;
|
|
14
|
+
export function detectInput(raw) {
|
|
15
|
+
const value = raw.trim();
|
|
16
|
+
if (value.startsWith('@'))
|
|
17
|
+
return { kind: 'history-ref', value };
|
|
18
|
+
if (FILE_TOKEN_RE.test(value))
|
|
19
|
+
return { kind: 'file-token', value };
|
|
20
|
+
if (TASK_RE.test(value) || UUID_RE.test(value))
|
|
21
|
+
return { kind: 'task-id', value };
|
|
22
|
+
if (/^https?:\/\//i.test(value))
|
|
23
|
+
return { kind: 'url', value };
|
|
24
|
+
if (isExistingFile(value)) {
|
|
25
|
+
const ext = extname(value).toLowerCase();
|
|
26
|
+
if (IMAGE_EXTS.has(ext))
|
|
27
|
+
return { kind: 'image-file', value };
|
|
28
|
+
if (MODEL_EXTS.has(ext))
|
|
29
|
+
return { kind: 'model-file', value };
|
|
30
|
+
// A real file with an unknown extension is still most likely an asset;
|
|
31
|
+
// treat by extension guess, otherwise reject early with a clear message.
|
|
32
|
+
throw new InputDetectError(`"${value}" exists but has an unsupported extension "${ext}" (images: ${[...IMAGE_EXTS].join(' ')}; models: ${[...MODEL_EXTS].join(' ')})`);
|
|
33
|
+
}
|
|
34
|
+
// Looks like a path but doesn't exist → almost certainly a typo, not a prompt.
|
|
35
|
+
if (looksLikePath(value)) {
|
|
36
|
+
throw new InputDetectError(`file not found: "${value}" (if you meant a text prompt, quote a sentence instead)`);
|
|
37
|
+
}
|
|
38
|
+
return { kind: 'text', value };
|
|
39
|
+
}
|
|
40
|
+
export function classifyInputs(args) {
|
|
41
|
+
if (args.length === 0)
|
|
42
|
+
throw new InputDetectError('no input given');
|
|
43
|
+
const detected = args.map(detectInput);
|
|
44
|
+
const kinds = new Set(detected.map((d) => d.kind));
|
|
45
|
+
if (kinds.size === 1) {
|
|
46
|
+
const kind = detected[0].kind;
|
|
47
|
+
switch (kind) {
|
|
48
|
+
case 'text':
|
|
49
|
+
// Multiple bare words are one prompt, joined.
|
|
50
|
+
return { kind: 'text', values: [{ kind: 'text', value: args.join(' ') }] };
|
|
51
|
+
case 'image-file':
|
|
52
|
+
return detected.length === 1
|
|
53
|
+
? { kind: 'image', values: detected }
|
|
54
|
+
: { kind: 'multiview', values: detected };
|
|
55
|
+
case 'model-file':
|
|
56
|
+
if (detected.length > 1)
|
|
57
|
+
throw new InputDetectError('only one model file at a time');
|
|
58
|
+
return { kind: 'model', values: detected };
|
|
59
|
+
case 'url':
|
|
60
|
+
if (detected.length > 1)
|
|
61
|
+
return { kind: 'multiview', values: detected };
|
|
62
|
+
return { kind: 'url-image', values: detected };
|
|
63
|
+
case 'task-id':
|
|
64
|
+
case 'history-ref':
|
|
65
|
+
case 'file-token':
|
|
66
|
+
if (detected.length > 1)
|
|
67
|
+
throw new InputDetectError('only one task reference at a time');
|
|
68
|
+
return { kind: 'task', values: detected };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Mixed images + urls → multiview; anything else mixed is ambiguous.
|
|
72
|
+
if ([...kinds].every((k) => k === 'image-file' || k === 'url')) {
|
|
73
|
+
return { kind: 'multiview', values: detected };
|
|
74
|
+
}
|
|
75
|
+
throw new InputDetectError(`mixed input types (${[...kinds].join(' + ')}) — pass one kind of input per command`);
|
|
76
|
+
}
|
|
77
|
+
export class InputDetectError extends Error {
|
|
78
|
+
}
|
|
79
|
+
function isExistingFile(p) {
|
|
80
|
+
try {
|
|
81
|
+
return existsSync(p) && statSync(p).isFile();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function looksLikePath(value) {
|
|
88
|
+
if (/\s/.test(value))
|
|
89
|
+
return false;
|
|
90
|
+
const ext = extname(value).toLowerCase();
|
|
91
|
+
return ((value.includes('/') || value.includes('\\') || IMAGE_EXTS.has(ext) || MODEL_EXTS.has(ext)) &&
|
|
92
|
+
!value.includes(' '));
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Desktop notification + terminal bell for long-running task completion. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export function notifyDone(title, message) {
|
|
4
|
+
try {
|
|
5
|
+
if (process.platform === 'darwin') {
|
|
6
|
+
const script = `display notification ${JSON.stringify(message)} with title ${JSON.stringify(title)}`;
|
|
7
|
+
spawn('osascript', ['-e', script], { stdio: 'ignore', detached: true }).on('error', () => { }); // best-effort
|
|
8
|
+
}
|
|
9
|
+
else if (process.platform === 'linux') {
|
|
10
|
+
spawn('notify-send', [title, message], { stdio: 'ignore', detached: true }).on('error', () => { });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
// Terminal bell as the lowest common denominator.
|
|
15
|
+
if (process.stderr.isTTY)
|
|
16
|
+
process.stderr.write('\x07');
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Cross-platform "open in default app/browser" without extra dependencies. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export function openExternal(target) {
|
|
4
|
+
try {
|
|
5
|
+
const [cmd, args] = process.platform === 'darwin'
|
|
6
|
+
? ['open', [target]]
|
|
7
|
+
: process.platform === 'win32'
|
|
8
|
+
? ['cmd', ['/c', 'start', '', target]]
|
|
9
|
+
: ['xdg-open', [target]];
|
|
10
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
11
|
+
child.on('error', () => { });
|
|
12
|
+
child.unref();
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|