@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 +21 -0
- package/README.md +35 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +594 -0
- package/package.json +38 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|