@assethub/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) AssetHub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @assethub/cli
2
+
3
+ Command line interface for AssetHub.
4
+
5
+ ```sh
6
+ export ASSETHUB_API_KEY=ah_live_xxx
7
+
8
+ assethub models list
9
+ assethub models list --domain meshGen --ids-only
10
+ assethub files import https://example.com/input.png --media-type image
11
+ assethub image generate --prompt "stylized prop concept" --wait
12
+ assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
13
+ assethub jobs watch job_123
14
+ assethub production analyze --image-url https://example.com/input.png --agent-version ah_part_extractor_v65
15
+ ```
16
+
17
+ Configuration:
18
+
19
+ - `ASSETHUB_API_KEY`: optional when a profile was saved with `auth login`.
20
+ - `ASSETHUB_API_BASE_URL`: optional, defaults to `https://app.assethub.io`.
21
+ - `ASSETHUB_CLI_CONFIG`: optional auth config path.
22
+
23
+ Agent-friendly login:
24
+
25
+ ```sh
26
+ printf '%s' "$ASSETHUB_API_KEY" | assethub auth login --api-key-stdin
27
+ assethub image generate --prompt "small stylized game asset chest" --wait --interval-ms 5000 --timeout-ms 900000
28
+ assethub mesh generate --source-url https://example.com/input.png --model-id meshGen.hunyuan31 --wait --interval-ms 5000 --timeout-ms 900000
29
+ ```
30
+
31
+ Image generation defaults to the deployed `v1` endpoint. Pass `--api-version v2`
32
+ only when the `v2` image endpoint is available in the target environment.
33
+
34
+ The CLI only uses AssetHub's public API surface and is designed so this package
35
+ can be published independently from the private application monorepo.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,594 @@
1
+ #!/usr/bin/env node
2
+ import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { argv, env, exit, stdin, stderr, stdout } from 'node:process';
6
+ import { AssetHubApiError, createAssetHubClient, } from '@assethub/api-client';
7
+ const defaultBaseUrl = 'https://app.assethub.io';
8
+ const defaultImageModelId = 'imageGen.nanoBanana.pro.openrouter';
9
+ const defaultMeshModelId = 'meshGen.hunyuan31';
10
+ const defaultProfileName = 'default';
11
+ const usage = `AssetHub CLI
12
+
13
+ Usage:
14
+ assethub auth login --api-key <key> [--base-url <url>] [--profile <name>]
15
+ assethub auth login --api-key-stdin [--base-url <url>] [--profile <name>]
16
+ assethub auth status [--profile <name>]
17
+ assethub auth logout [--profile <name>]
18
+ assethub models list [--domain <domain>] [--ids-only] [--limit <n>]
19
+ assethub models get <model-id>
20
+ assethub files import <url> --media-type image|mesh
21
+ assethub files upload <file> --media-type image|mesh
22
+ assethub image generate --prompt <text> [--file <path> | --source-url <url>] [--model-id <id>] [--batch-size <n>] [--wait] [--api-version v1|v2]
23
+ assethub image generate --prompt <text> --source-resource-id <id> --api-version v2 [--model-id <id>] [--batch-size <n>] [--wait]
24
+ assethub mesh generate (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json>) [--model-id <id>] [--name <name>] [--wait]
25
+ assethub mesh retopo (--source-resource-id <id> | --source-url <url> | --file-ref-json <json>) --model-id <id> [--wait]
26
+ assethub jobs get <job-id>
27
+ assethub jobs watch <job-id> [--interval-ms <ms>] [--timeout-ms <ms>]
28
+ assethub production analyze --image-url <url> --agent-version <id> [--name <name>]
29
+ assethub production status <order-id>
30
+ assethub production execute --order-id <id> --mission-id <id> --task-id <id> [--task-id <id>...] --agent-version <id> [--part-extraction-mode fast|quality]
31
+
32
+ Global options:
33
+ --api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
34
+ --base-url <url> Defaults to ASSETHUB_API_BASE_URL, saved auth, or ${defaultBaseUrl}.
35
+ --profile <name> Saved auth profile name. Defaults to ${defaultProfileName}.
36
+ --config <path> Auth config path. Defaults to ~/.assethub/config.json.
37
+ --help Show help.
38
+
39
+ Examples:
40
+ ASSETHUB_API_KEY=ah_live_xxx assethub models list
41
+ assethub auth login --api-key-stdin
42
+ assethub models list --domain meshGen --ids-only
43
+ assethub files upload ./input.png --media-type image
44
+ assethub image generate --prompt "stylized prop concept" --wait
45
+ assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
46
+ `;
47
+ const print = (value) => {
48
+ stdout.write(`${JSON.stringify(value, null, 2)}\n`);
49
+ };
50
+ const parseArgs = (args) => {
51
+ const positionals = [];
52
+ const flags = {};
53
+ for (let index = 0; index < args.length; index += 1) {
54
+ const arg = args[index];
55
+ if (!arg.startsWith('--')) {
56
+ positionals.push(arg);
57
+ continue;
58
+ }
59
+ const withoutPrefix = arg.slice(2);
60
+ const [rawKey, inlineValue] = withoutPrefix.split(/=(.*)/s, 2);
61
+ const key = rawKey.trim();
62
+ if (key.length === 0) {
63
+ throw new Error(`Invalid flag: ${arg}`);
64
+ }
65
+ const next = args[index + 1];
66
+ const value = inlineValue != null
67
+ ? inlineValue
68
+ : next != null && !next.startsWith('--')
69
+ ? (index += 1, next)
70
+ : true;
71
+ const previous = flags[key];
72
+ if (previous == null) {
73
+ flags[key] = value;
74
+ }
75
+ else if (Array.isArray(previous)) {
76
+ previous.push(String(value));
77
+ }
78
+ else {
79
+ flags[key] = [String(previous), String(value)];
80
+ }
81
+ }
82
+ return { positionals, flags };
83
+ };
84
+ const getFlag = (flags, name) => {
85
+ const value = flags[name];
86
+ if (value == null || value === false) {
87
+ return undefined;
88
+ }
89
+ if (value === true) {
90
+ return 'true';
91
+ }
92
+ if (Array.isArray(value)) {
93
+ return value[value.length - 1];
94
+ }
95
+ return value;
96
+ };
97
+ const getFlagValues = (flags, name) => {
98
+ const value = flags[name];
99
+ if (value == null || value === false) {
100
+ return [];
101
+ }
102
+ if (value === true) {
103
+ return ['true'];
104
+ }
105
+ return Array.isArray(value) ? value : [value];
106
+ };
107
+ const hasFlag = (flags, name) => flags[name] != null;
108
+ const requireFlag = (flags, name) => {
109
+ const value = getFlag(flags, name);
110
+ if (value == null || value.trim().length === 0 || value === 'true') {
111
+ throw new Error(`Missing required flag: --${name}`);
112
+ }
113
+ return value;
114
+ };
115
+ const parsePositiveIntegerFlag = (flags, name, fallback) => {
116
+ const raw = getFlag(flags, name);
117
+ if (raw == null) {
118
+ return fallback;
119
+ }
120
+ const parsed = Number(raw);
121
+ if (!Number.isFinite(parsed) || parsed <= 0) {
122
+ throw new Error(`--${name} must be a positive number`);
123
+ }
124
+ return Math.floor(parsed);
125
+ };
126
+ const parseBooleanFlag = (flags, name) => {
127
+ const raw = getFlag(flags, name);
128
+ if (raw == null) {
129
+ return undefined;
130
+ }
131
+ if (raw === 'true') {
132
+ return true;
133
+ }
134
+ if (raw === 'false') {
135
+ return false;
136
+ }
137
+ throw new Error(`--${name} must be true or false`);
138
+ };
139
+ const parseMediaType = (value) => {
140
+ if (value === 'image' || value === 'mesh') {
141
+ return value;
142
+ }
143
+ throw new Error('--media-type must be "image" or "mesh"');
144
+ };
145
+ const getProfileName = (flags) => getFlag(flags, 'profile') ?? defaultProfileName;
146
+ const getConfigPath = (flags) => getFlag(flags, 'config') ??
147
+ env.ASSETHUB_CLI_CONFIG ??
148
+ join(homedir(), '.assethub', 'config.json');
149
+ const readAuthConfig = async (configPath) => {
150
+ try {
151
+ const raw = await readFile(configPath, 'utf8');
152
+ const parsed = JSON.parse(raw);
153
+ return {
154
+ defaultProfile: parsed.defaultProfile,
155
+ profiles: parsed.profiles ?? {},
156
+ };
157
+ }
158
+ catch (error) {
159
+ if (error != null &&
160
+ typeof error === 'object' &&
161
+ 'code' in error &&
162
+ error.code === 'ENOENT') {
163
+ return { profiles: {} };
164
+ }
165
+ throw error;
166
+ }
167
+ };
168
+ const writeAuthConfig = async (configPath, config) => {
169
+ await mkdir(dirname(configPath), { recursive: true, mode: 0o700 });
170
+ await writeFile(`${configPath}.tmp`, `${JSON.stringify(config, null, 2)}\n`, {
171
+ mode: 0o600,
172
+ });
173
+ await chmod(`${configPath}.tmp`, 0o600);
174
+ await rename(`${configPath}.tmp`, configPath);
175
+ await chmod(configPath, 0o600);
176
+ };
177
+ const readStdin = async () => await new Promise((resolve, reject) => {
178
+ let value = '';
179
+ stdin.setEncoding('utf8');
180
+ stdin.on('data', chunk => {
181
+ value += chunk;
182
+ });
183
+ stdin.on('end', () => resolve(value));
184
+ stdin.on('error', reject);
185
+ });
186
+ const resolveAuth = async (flags) => {
187
+ const profile = getProfileName(flags);
188
+ const baseUrlFromFlags = getFlag(flags, 'base-url');
189
+ const apiKeyFromFlags = getFlag(flags, 'api-key');
190
+ if (apiKeyFromFlags != null && apiKeyFromFlags !== 'true') {
191
+ return {
192
+ apiKey: apiKeyFromFlags,
193
+ baseUrl: baseUrlFromFlags ?? env.ASSETHUB_API_BASE_URL ?? defaultBaseUrl,
194
+ profile,
195
+ source: 'flag',
196
+ };
197
+ }
198
+ if (env.ASSETHUB_API_KEY != null && env.ASSETHUB_API_KEY.trim() !== '') {
199
+ return {
200
+ apiKey: env.ASSETHUB_API_KEY,
201
+ baseUrl: baseUrlFromFlags ?? env.ASSETHUB_API_BASE_URL ?? defaultBaseUrl,
202
+ profile,
203
+ source: 'env',
204
+ };
205
+ }
206
+ const config = await readAuthConfig(getConfigPath(flags));
207
+ const fallbackProfileName = profile === defaultProfileName ? config.defaultProfile : undefined;
208
+ const storedProfile = config.profiles?.[profile] ??
209
+ (fallbackProfileName != null
210
+ ? config.profiles?.[fallbackProfileName]
211
+ : undefined);
212
+ if (storedProfile != null) {
213
+ return {
214
+ apiKey: storedProfile.apiKey,
215
+ baseUrl: baseUrlFromFlags ?? env.ASSETHUB_API_BASE_URL ?? storedProfile.baseUrl,
216
+ profile: config.profiles?.[profile] != null ? profile : (fallbackProfileName ?? profile),
217
+ source: 'profile',
218
+ };
219
+ }
220
+ throw new Error('Missing API key. Run "assethub auth login --api-key-stdin", set ASSETHUB_API_KEY, or pass --api-key.');
221
+ };
222
+ const createContext = async (flags) => {
223
+ const auth = await resolveAuth(flags);
224
+ return {
225
+ auth,
226
+ flags,
227
+ client: createAssetHubClient({
228
+ apiKey: auth.apiKey,
229
+ baseUrl: auth.baseUrl,
230
+ }),
231
+ };
232
+ };
233
+ const parseFileRef = (value) => {
234
+ const parsed = JSON.parse(value);
235
+ if (parsed == null || typeof parsed !== 'object' || !('type' in parsed)) {
236
+ throw new Error('--file-ref-json must be a BlobLocation JSON object');
237
+ }
238
+ return parsed;
239
+ };
240
+ const readBlob = async (filePath) => {
241
+ const buffer = await readFile(filePath);
242
+ return new Blob([new Uint8Array(buffer)]);
243
+ };
244
+ const resolveSource = async ({ flags, client, fileMediaType, }) => {
245
+ const sourceResourceId = getFlag(flags, 'source-resource-id');
246
+ if (sourceResourceId != null) {
247
+ return { resourceId: sourceResourceId };
248
+ }
249
+ const sourceUrl = getFlag(flags, 'source-url');
250
+ if (sourceUrl != null) {
251
+ return { url: sourceUrl };
252
+ }
253
+ const fileRefJson = getFlag(flags, 'file-ref-json');
254
+ if (fileRefJson != null) {
255
+ return { fileRef: parseFileRef(fileRefJson) };
256
+ }
257
+ const filePath = getFlag(flags, 'file');
258
+ if (filePath != null) {
259
+ const fileRef = await client.v2.uploadFile({
260
+ file: await readBlob(filePath),
261
+ fileName: basename(filePath),
262
+ mediaType: fileMediaType,
263
+ });
264
+ return { fileRef: fileRef.fileRef };
265
+ }
266
+ return undefined;
267
+ };
268
+ const requireSource = async (options) => {
269
+ const source = await resolveSource(options);
270
+ if (source == null) {
271
+ throw new Error('Missing source. Pass --file, --source-url, --source-resource-id, or --file-ref-json.');
272
+ }
273
+ return source;
274
+ };
275
+ const sourceToV1ImageInput = (source) => {
276
+ if (source == null) {
277
+ return {};
278
+ }
279
+ if ('url' in source) {
280
+ return { imageUrl: source.url };
281
+ }
282
+ if ('fileRef' in source) {
283
+ return { imageBlobLocation: source.fileRef };
284
+ }
285
+ throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
286
+ };
287
+ const pollIfRequested = async ({ ctx, jobId, apiVersion = 'v2', }) => {
288
+ if (!hasFlag(ctx.flags, 'wait')) {
289
+ return undefined;
290
+ }
291
+ const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
292
+ const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000);
293
+ const pollJob = apiVersion === 'v1' ? ctx.client.v1.pollJob : ctx.client.v2.pollJob;
294
+ return pollJob(jobId, {
295
+ intervalMs,
296
+ timeoutMs,
297
+ onPoll: job => {
298
+ stderr.write(`[poll] job=${job.id} status=${job.status}\n`);
299
+ },
300
+ });
301
+ };
302
+ const requirePositional = (positionals, index, label) => {
303
+ const value = positionals[index];
304
+ if (value == null || value.trim().length === 0) {
305
+ throw new Error(`Missing required argument: ${label}`);
306
+ }
307
+ return value;
308
+ };
309
+ const commandModels = async (subcommand, positionals, ctx) => {
310
+ if (subcommand === 'list') {
311
+ const domain = getFlag(ctx.flags, 'domain');
312
+ const limit = getFlag(ctx.flags, 'limit') == null
313
+ ? undefined
314
+ : parsePositiveIntegerFlag(ctx.flags, 'limit', 1);
315
+ const models = (await ctx.client.v2.listModels())
316
+ .filter(model => domain == null || model.domain === domain)
317
+ .slice(0, limit);
318
+ print({
319
+ models: hasFlag(ctx.flags, 'ids-only')
320
+ ? models.map(model => model.id)
321
+ : models,
322
+ });
323
+ return;
324
+ }
325
+ if (subcommand === 'get') {
326
+ const modelId = requirePositional(positionals, 2, 'model-id');
327
+ print({ model: await ctx.client.v2.getModel(modelId) });
328
+ return;
329
+ }
330
+ throw new Error('Unknown models command. Use "models list" or "models get".');
331
+ };
332
+ const commandAuth = async (subcommand, flags) => {
333
+ const profile = getProfileName(flags);
334
+ const configPath = getConfigPath(flags);
335
+ if (subcommand === 'login') {
336
+ const apiKey = hasFlag(flags, 'api-key-stdin')
337
+ ? (await readStdin()).trim()
338
+ : (getFlag(flags, 'api-key') ?? env.ASSETHUB_API_KEY ?? '').trim();
339
+ if (apiKey.length === 0) {
340
+ throw new Error('API key is empty. Pass --api-key, use --api-key-stdin, or set ASSETHUB_API_KEY.');
341
+ }
342
+ const baseUrl = getFlag(flags, 'base-url') ?? env.ASSETHUB_API_BASE_URL ?? defaultBaseUrl;
343
+ if (!hasFlag(flags, 'skip-verify')) {
344
+ const client = createAssetHubClient({ apiKey, baseUrl });
345
+ await client.v2.listModels();
346
+ }
347
+ const config = await readAuthConfig(configPath);
348
+ const profiles = config.profiles ?? {};
349
+ profiles[profile] = {
350
+ apiKey,
351
+ baseUrl,
352
+ updatedAt: new Date().toISOString(),
353
+ };
354
+ await writeAuthConfig(configPath, {
355
+ ...config,
356
+ defaultProfile: profile,
357
+ profiles,
358
+ });
359
+ print({
360
+ success: true,
361
+ profile,
362
+ baseUrl,
363
+ configPath,
364
+ verified: !hasFlag(flags, 'skip-verify'),
365
+ });
366
+ return;
367
+ }
368
+ if (subcommand === 'status') {
369
+ const auth = await resolveAuth(flags);
370
+ const client = createAssetHubClient({
371
+ apiKey: auth.apiKey,
372
+ baseUrl: auth.baseUrl,
373
+ });
374
+ const models = await client.v2.listModels();
375
+ print({
376
+ authenticated: true,
377
+ profile: auth.profile,
378
+ source: auth.source,
379
+ baseUrl: auth.baseUrl,
380
+ modelCount: models.length,
381
+ });
382
+ return;
383
+ }
384
+ if (subcommand === 'logout') {
385
+ const config = await readAuthConfig(configPath);
386
+ const profiles = config.profiles ?? {};
387
+ const existed = profiles[profile] != null;
388
+ delete profiles[profile];
389
+ await writeAuthConfig(configPath, {
390
+ defaultProfile: config.defaultProfile === profile ? undefined : config.defaultProfile,
391
+ profiles,
392
+ });
393
+ print({ success: true, profile, removed: existed, configPath });
394
+ return;
395
+ }
396
+ throw new Error('Unknown auth command. Use "auth login", "auth status", or "auth logout".');
397
+ };
398
+ const commandFiles = async (subcommand, positionals, ctx) => {
399
+ const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
400
+ if (subcommand === 'import') {
401
+ const url = requirePositional(positionals, 2, 'url');
402
+ print(await ctx.client.v2.importFile({ url, mediaType }));
403
+ return;
404
+ }
405
+ if (subcommand === 'upload') {
406
+ const filePath = requirePositional(positionals, 2, 'file');
407
+ print(await ctx.client.v2.uploadFile({
408
+ file: await readBlob(filePath),
409
+ fileName: basename(filePath),
410
+ mediaType,
411
+ }));
412
+ return;
413
+ }
414
+ throw new Error('Unknown files command. Use "files import" or "files upload".');
415
+ };
416
+ const commandImage = async (subcommand, ctx) => {
417
+ if (subcommand !== 'generate') {
418
+ throw new Error('Unknown image command. Use "image generate".');
419
+ }
420
+ const source = await resolveSource({
421
+ flags: ctx.flags,
422
+ client: ctx.client,
423
+ fileMediaType: 'image',
424
+ });
425
+ const apiVersion = getFlag(ctx.flags, 'api-version') ?? 'v1';
426
+ if (apiVersion !== 'v1' && apiVersion !== 'v2') {
427
+ throw new Error('--api-version must be "v1" or "v2"');
428
+ }
429
+ const commonInput = {
430
+ prompt: requireFlag(ctx.flags, 'prompt'),
431
+ modelId: getFlag(ctx.flags, 'model-id') ?? defaultImageModelId,
432
+ batchSize: parsePositiveIntegerFlag(ctx.flags, 'batch-size', 1),
433
+ negativePrompt: getFlag(ctx.flags, 'negative-prompt'),
434
+ seed: getFlag(ctx.flags, 'seed') == null
435
+ ? undefined
436
+ : parsePositiveIntegerFlag(ctx.flags, 'seed', 1),
437
+ steps: getFlag(ctx.flags, 'steps') == null
438
+ ? undefined
439
+ : parsePositiveIntegerFlag(ctx.flags, 'steps', 1),
440
+ cfgScale: getFlag(ctx.flags, 'cfg-scale') == null
441
+ ? undefined
442
+ : Number(getFlag(ctx.flags, 'cfg-scale')),
443
+ };
444
+ const queued = apiVersion === 'v1'
445
+ ? await ctx.client.v1.generateImage({
446
+ ...commonInput,
447
+ ...sourceToV1ImageInput(source),
448
+ })
449
+ : await ctx.client.v2.generateImage({
450
+ ...commonInput,
451
+ source,
452
+ });
453
+ const job = await pollIfRequested({ ctx, jobId: queued.jobId, apiVersion });
454
+ print({ queued, job, apiVersion });
455
+ };
456
+ const commandMesh = async (subcommand, ctx) => {
457
+ if (subcommand === 'generate') {
458
+ const queued = await ctx.client.v2.generateMesh({
459
+ source: await requireSource({
460
+ flags: ctx.flags,
461
+ client: ctx.client,
462
+ fileMediaType: 'image',
463
+ }),
464
+ modelId: getFlag(ctx.flags, 'model-id') ?? defaultMeshModelId,
465
+ name: getFlag(ctx.flags, 'name'),
466
+ faceLimit: getFlag(ctx.flags, 'face-limit') == null
467
+ ? undefined
468
+ : parsePositiveIntegerFlag(ctx.flags, 'face-limit', 1),
469
+ isLowPoly: parseBooleanFlag(ctx.flags, 'low-poly'),
470
+ });
471
+ const job = await pollIfRequested({ ctx, jobId: queued.jobId });
472
+ print({ queued, job });
473
+ return;
474
+ }
475
+ if (subcommand === 'retopo') {
476
+ const queued = await ctx.client.v2.retopo({
477
+ source: await requireSource({
478
+ flags: ctx.flags,
479
+ client: ctx.client,
480
+ fileMediaType: 'mesh',
481
+ }),
482
+ modelId: requireFlag(ctx.flags, 'model-id'),
483
+ name: getFlag(ctx.flags, 'name'),
484
+ faceLevel: getFlag(ctx.flags, 'face-level'),
485
+ polygonType: getFlag(ctx.flags, 'polygon-type'),
486
+ });
487
+ const job = await pollIfRequested({ ctx, jobId: queued.jobId });
488
+ print({ queued, job });
489
+ return;
490
+ }
491
+ throw new Error('Unknown mesh command. Use "mesh generate" or "mesh retopo".');
492
+ };
493
+ const commandJobs = async (subcommand, positionals, ctx) => {
494
+ const jobId = requirePositional(positionals, 2, 'job-id');
495
+ if (subcommand === 'get') {
496
+ print({ job: await ctx.client.v2.getJob(jobId) });
497
+ return;
498
+ }
499
+ if (subcommand === 'watch') {
500
+ const job = await ctx.client.v2.pollJob(jobId, {
501
+ intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
502
+ timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000),
503
+ onPoll: current => {
504
+ stderr.write(`[poll] job=${current.id} status=${current.status}\n`);
505
+ },
506
+ });
507
+ print({ job });
508
+ return;
509
+ }
510
+ throw new Error('Unknown jobs command. Use "jobs get" or "jobs watch".');
511
+ };
512
+ const commandProduction = async (subcommand, positionals, ctx) => {
513
+ if (subcommand === 'analyze') {
514
+ print(await ctx.client.v1.analyzeProduction({
515
+ imageUrl: requireFlag(ctx.flags, 'image-url'),
516
+ agentVersion: requireFlag(ctx.flags, 'agent-version'),
517
+ name: getFlag(ctx.flags, 'name'),
518
+ }));
519
+ return;
520
+ }
521
+ if (subcommand === 'status') {
522
+ const orderId = requirePositional(positionals, 2, 'order-id');
523
+ print(await ctx.client.v1.getProductionStatus(orderId));
524
+ return;
525
+ }
526
+ if (subcommand === 'execute') {
527
+ const confirmedTaskIds = getFlagValues(ctx.flags, 'task-id');
528
+ if (confirmedTaskIds.length === 0) {
529
+ throw new Error('Missing required flag: --task-id');
530
+ }
531
+ const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
532
+ if (partExtractionMode != null &&
533
+ partExtractionMode !== 'fast' &&
534
+ partExtractionMode !== 'quality') {
535
+ throw new Error('--part-extraction-mode must be "fast" or "quality"');
536
+ }
537
+ print(await ctx.client.v1.executeProduction({
538
+ orderId: requireFlag(ctx.flags, 'order-id'),
539
+ missionId: requireFlag(ctx.flags, 'mission-id'),
540
+ confirmedTaskIds,
541
+ agentVersion: requireFlag(ctx.flags, 'agent-version'),
542
+ partExtractionMode,
543
+ }));
544
+ return;
545
+ }
546
+ throw new Error('Unknown production command. Use "production analyze", "production status", or "production execute".');
547
+ };
548
+ const run = async () => {
549
+ const parsed = parseArgs(argv.slice(2));
550
+ if (hasFlag(parsed.flags, 'help') || parsed.positionals.length === 0) {
551
+ stdout.write(usage);
552
+ return;
553
+ }
554
+ const [command, subcommand] = parsed.positionals;
555
+ if (command === 'auth') {
556
+ await commandAuth(subcommand, parsed.flags);
557
+ return;
558
+ }
559
+ const ctx = await createContext(parsed.flags);
560
+ switch (command) {
561
+ case 'models':
562
+ await commandModels(subcommand, parsed.positionals, ctx);
563
+ return;
564
+ case 'files':
565
+ await commandFiles(subcommand, parsed.positionals, ctx);
566
+ return;
567
+ case 'image':
568
+ await commandImage(subcommand, ctx);
569
+ return;
570
+ case 'mesh':
571
+ await commandMesh(subcommand, ctx);
572
+ return;
573
+ case 'jobs':
574
+ await commandJobs(subcommand, parsed.positionals, ctx);
575
+ return;
576
+ case 'production':
577
+ await commandProduction(subcommand, parsed.positionals, ctx);
578
+ return;
579
+ default:
580
+ throw new Error(`Unknown command: ${command}`);
581
+ }
582
+ };
583
+ run().catch((error) => {
584
+ if (error instanceof AssetHubApiError) {
585
+ stderr.write(`AssetHub API error [${error.status}] ${error.code}: ${error.message}\n`);
586
+ }
587
+ else if (error instanceof Error) {
588
+ stderr.write(`${error.message}\n`);
589
+ }
590
+ else {
591
+ stderr.write(`${String(error)}\n`);
592
+ }
593
+ exit(1);
594
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@assethub/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command line interface for AssetHub.",
5
+ "type": "module",
6
+ "bin": {
7
+ "assethub": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@assethub/api-client": "^0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "24.10.1",
32
+ "typescript": "5.9.3"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json && chmod +x dist/index.js",
36
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
37
+ }
38
+ }