@spatius/cli 0.1.0-beta.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 +67 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/dist/chunk-YILIK7FE.js +479 -0
- package/dist/cli.js +2523 -0
- package/dist/commands.js +10 -0
- package/docs/deployment.md +277 -0
- package/docs/staging-validation.md +46 -0
- package/docs/workflows.md +134 -0
- package/package.json +45 -0
- package/skills/spatius-avatar/SKILL.md +41 -0
- package/skills/spatius-shared/SKILL.md +50 -0
- package/skills/spatius-shared/references/recovery.md +24 -0
- package/skills/spatius-video/SKILL.md +44 -0
- package/skills/spatius-video/references/inputs-and-recovery.md +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Spatialwalk
|
|
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,67 @@
|
|
|
1
|
+
# Spatius CLI
|
|
2
|
+
|
|
3
|
+
Create avatars and render videos from a coding agent or terminal. The CLI handles
|
|
4
|
+
Studio login, app credentials, temporary uploads, polling, and MP4 downloads.
|
|
5
|
+
|
|
6
|
+
Requires Node.js 22+, a Spatius Studio account, and separately approved Avatar
|
|
7
|
+
and Video API access. Avatar creation uses your existing Avatar Creations balance.
|
|
8
|
+
The CLI is experimental.
|
|
9
|
+
|
|
10
|
+
## Get started
|
|
11
|
+
|
|
12
|
+
After the npm release:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install -g @spatius/cli@beta
|
|
16
|
+
spatius auth login
|
|
17
|
+
spatius setup
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Approve login at `https://app.spatius.ai` in your local browser. Studio API
|
|
21
|
+
requests use `https://api.studio.spatius.ai`; development origin settings are
|
|
22
|
+
documented in the [workflow guide](docs/workflows.md). Credentials stay in private user configuration
|
|
23
|
+
storage; commands never print API keys. Setup reuses a dedicated `Spatius CLI` app.
|
|
24
|
+
Use `spatius setup --app-id <id>` to select another app you own.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
spatius avatars create --image ./portrait.png --name Presenter
|
|
28
|
+
spatius avatars jobs wait <avatar-job-id>
|
|
29
|
+
spatius videos create --avatar-id <avatar-id> --audio ./speech.wav --background ./background.png
|
|
30
|
+
spatius videos wait <video-job-id>
|
|
31
|
+
spatius videos download <video-job-id> --output ./video.mp4
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Inputs accept local files or public HTTP(S) URLs. Local files are uploaded to
|
|
35
|
+
temporary storage and remain available for 24 hours after upload completion.
|
|
36
|
+
Video output follows the service's retention deadline, currently seven days
|
|
37
|
+
after render submission. Save your MP4 before it expires.
|
|
38
|
+
|
|
39
|
+
Commands return JSON on stdout; progress and errors go to stderr. Save the
|
|
40
|
+
`operationId` and `jobId` returned by creation. Resume an interrupted creation with
|
|
41
|
+
`spatius videos create --resume <operation-id>` or
|
|
42
|
+
`spatius avatars create --resume <operation-id>`; do not start a fresh creation
|
|
43
|
+
just because a command timed out.
|
|
44
|
+
|
|
45
|
+
## Use with coding agents
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx skills add spatius-ai/spatius-cli --skill spatius-shared spatius-avatar spatius-video
|
|
49
|
+
spatius schema
|
|
50
|
+
spatius schema videos create
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The three skills cover setup, avatar creation, and video generation. The npm
|
|
54
|
+
package includes the matching skills under `skills/`; use those files when an
|
|
55
|
+
exact version match is needed. The GitHub installer above follows the repository
|
|
56
|
+
default branch. Skills and CLI behavior ship together. `--dry-run` previews a
|
|
57
|
+
creation without uploading or submitting it. `--help` describes the installed CLI's options.
|
|
58
|
+
|
|
59
|
+
See the [workflow guide](docs/workflows.md) for input requirements and recovery,
|
|
60
|
+
and the [Spatius API documentation](https://docs.spatius.ai/api-reference/video-generation)
|
|
61
|
+
for service behavior. Repository development instructions are in
|
|
62
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
63
|
+
|
|
64
|
+
Prereleases use the npm `beta` channel. After a stable release, install it with
|
|
65
|
+
`npm install -g @spatius/cli`. The executable is always `spatius`. Maintainers can
|
|
66
|
+
find the release workflow and first-time setup in the
|
|
67
|
+
[deployment guide](docs/deployment.md).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
The Studio loopback authentication implementation adapts the protocol and code
|
|
4
|
+
from [create-spatius-app](https://github.com/spatius-ai/create-spatius-app).
|
|
5
|
+
|
|
6
|
+
MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 spatialwalk
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// src/commands.ts
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/core/errors.ts
|
|
5
|
+
var CliError = class extends Error {
|
|
6
|
+
constructor(code, message, options = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.name = "CliError";
|
|
11
|
+
}
|
|
12
|
+
code;
|
|
13
|
+
options;
|
|
14
|
+
};
|
|
15
|
+
function asCliError(error) {
|
|
16
|
+
if (error instanceof CliError) return error;
|
|
17
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
18
|
+
return new CliError("INTERRUPTED", "Operation interrupted.", {
|
|
19
|
+
exitCode: 130
|
|
20
|
+
});
|
|
21
|
+
return new CliError(
|
|
22
|
+
"INTERNAL_ERROR",
|
|
23
|
+
"The operation could not be completed.",
|
|
24
|
+
{
|
|
25
|
+
recovery: "Retry a read operation, or inspect the saved operation before retrying a creation."
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/commands.ts
|
|
31
|
+
var timeout = {
|
|
32
|
+
flags: "--timeout <seconds>",
|
|
33
|
+
description: "Wait deadline in seconds (default: 600).",
|
|
34
|
+
type: "number"
|
|
35
|
+
};
|
|
36
|
+
var resume = {
|
|
37
|
+
flags: "--resume <operation-id>",
|
|
38
|
+
description: "Resume a saved operation without supplying new input."
|
|
39
|
+
};
|
|
40
|
+
var wait = {
|
|
41
|
+
flags: "--wait",
|
|
42
|
+
description: "Wait for the admitted job to finish."
|
|
43
|
+
};
|
|
44
|
+
var dryRun = {
|
|
45
|
+
flags: "--dry-run",
|
|
46
|
+
description: "Validate and preview without network requests or local state changes."
|
|
47
|
+
};
|
|
48
|
+
var pages = [
|
|
49
|
+
{
|
|
50
|
+
flags: "--page-size <number>",
|
|
51
|
+
description: "Results per page (1\u2013100).",
|
|
52
|
+
type: "number"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
flags: "--page-token <token>",
|
|
56
|
+
description: "Next page token returned by the preceding list; keep page size unchanged."
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
var status = {
|
|
60
|
+
flags: "--status <statuses>",
|
|
61
|
+
description: "Comma-separated job status filters."
|
|
62
|
+
};
|
|
63
|
+
var str = (v, key) => v[key];
|
|
64
|
+
var num = (v, key) => v[key];
|
|
65
|
+
var bool = (v, key) => v[key] === true;
|
|
66
|
+
function pageOptions(v) {
|
|
67
|
+
return {
|
|
68
|
+
pageSize: num(v, "pageSize"),
|
|
69
|
+
pageToken: str(v, "pageToken"),
|
|
70
|
+
statuses: str(v, "status")?.split(",")
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function settings(v) {
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
[
|
|
76
|
+
"width",
|
|
77
|
+
"height",
|
|
78
|
+
"fit",
|
|
79
|
+
"backgroundColor",
|
|
80
|
+
"backgroundFit",
|
|
81
|
+
"leadInSeconds",
|
|
82
|
+
"leadOutSeconds"
|
|
83
|
+
].filter((key) => v[key] !== void 0).map((key) => [key, v[key]])
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
var definitions = [
|
|
87
|
+
{
|
|
88
|
+
path: "auth login",
|
|
89
|
+
description: "Authorize this local CLI through Spatius Studio.",
|
|
90
|
+
output: "Authenticated user and selected app metadata; no credentials.",
|
|
91
|
+
example: "spatius auth login",
|
|
92
|
+
flags: [
|
|
93
|
+
{
|
|
94
|
+
flags: "--no-browser",
|
|
95
|
+
description: "Print the approval URL without opening a browser."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
...timeout,
|
|
99
|
+
description: "Browser approval timeout in seconds (default: 300)."
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
run: async (_, v, c) => c.auth.login({
|
|
103
|
+
noBrowser: v.browser === false,
|
|
104
|
+
timeoutMs: (num(v, "timeout") ?? 300) * 1e3,
|
|
105
|
+
onAuthorize: (url) => c.progress({
|
|
106
|
+
event: "authorization_required",
|
|
107
|
+
url,
|
|
108
|
+
requiresHuman: true
|
|
109
|
+
}),
|
|
110
|
+
signal: c.signal
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
path: "auth status",
|
|
115
|
+
description: "Inspect the active Studio login.",
|
|
116
|
+
output: "Authentication and account metadata.",
|
|
117
|
+
example: "spatius auth status",
|
|
118
|
+
run: async (_, __, c) => c.auth.status()
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
path: "auth logout",
|
|
122
|
+
description: "Revoke refresh access and remove local credentials.",
|
|
123
|
+
output: "Logged-out state.",
|
|
124
|
+
example: "spatius auth logout",
|
|
125
|
+
run: async (_, __, c) => c.auth.logout()
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
path: "setup",
|
|
129
|
+
description: "Create or reuse a Studio app and API key for this account.",
|
|
130
|
+
output: "appId, userId, and reuse information; no API key.",
|
|
131
|
+
example: "spatius setup",
|
|
132
|
+
flags: [
|
|
133
|
+
{
|
|
134
|
+
flags: "--app-id <id>",
|
|
135
|
+
description: "Select an existing app owned by the authenticated user."
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
flags: "--retry-uncertain",
|
|
139
|
+
description: "After reconciliation, allow one new app/key creation; an earlier request may already have created it."
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
run: async (_, v, c) => c.auth.setup({
|
|
143
|
+
appId: str(v, "appId"),
|
|
144
|
+
retryUncertain: bool(v, "retryUncertain")
|
|
145
|
+
})
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
path: "apps list",
|
|
149
|
+
description: "List owned Studio apps without exposing their API keys.",
|
|
150
|
+
output: "Array of appId, name, and createdAt.",
|
|
151
|
+
example: "spatius apps list",
|
|
152
|
+
run: async (_, __, c) => c.auth.listApps()
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
path: "assets upload",
|
|
156
|
+
description: "Upload a local input to temporary storage.",
|
|
157
|
+
output: "Upload ID, accepted parts, status, and completed URL/expiration.",
|
|
158
|
+
example: "spatius assets upload ./speech.wav --kind audio",
|
|
159
|
+
args: [{ name: "file", required: true }],
|
|
160
|
+
flags: [
|
|
161
|
+
{
|
|
162
|
+
flags: "--kind <kind>",
|
|
163
|
+
description: "Input purpose.",
|
|
164
|
+
choices: ["avatar-image", "audio", "background"]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
flags: "--resume <upload-id>",
|
|
168
|
+
description: "Resume an existing upload of this same local file."
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
run: async (a, v, c) => {
|
|
172
|
+
const kind = str(v, "kind");
|
|
173
|
+
if (!kind)
|
|
174
|
+
throw new CliError("INVALID_ARGUMENT", "--kind is required.", {
|
|
175
|
+
exitCode: 2
|
|
176
|
+
});
|
|
177
|
+
return c.workflows.upload(a[0], {
|
|
178
|
+
kind,
|
|
179
|
+
resume: str(v, "resume")
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
path: "assets get",
|
|
185
|
+
description: "Inspect an owned temporary upload.",
|
|
186
|
+
output: "Upload status and accepted parts.",
|
|
187
|
+
example: "spatius assets get 00000000-0000-4000-8000-000000000001",
|
|
188
|
+
args: [{ name: "id", required: true }],
|
|
189
|
+
run: async (a, _, c) => c.workflows.getUpload(a[0])
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
path: "assets abort",
|
|
193
|
+
description: "Abort an unfinished upload and release storage after cleanup.",
|
|
194
|
+
output: "Aborted upload state.",
|
|
195
|
+
example: "spatius assets abort 00000000-0000-4000-8000-000000000001",
|
|
196
|
+
args: [{ name: "id", required: true }],
|
|
197
|
+
run: async (a, _, c) => c.workflows.abortUpload(a[0])
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
path: "avatars create",
|
|
201
|
+
description: "Create an avatar from a local JPEG/PNG or public URL.",
|
|
202
|
+
output: "operationId, jobId, status, createdAt; optional completed job.",
|
|
203
|
+
example: "spatius avatars create --image ./portrait.png --name Presenter",
|
|
204
|
+
flags: [
|
|
205
|
+
{
|
|
206
|
+
flags: "--image <file-or-url>",
|
|
207
|
+
description: "Opaque JPEG/PNG, at most 5 MiB, shorter side at least 340 pixels."
|
|
208
|
+
},
|
|
209
|
+
{ flags: "--name <name>", description: "Avatar display name." },
|
|
210
|
+
resume,
|
|
211
|
+
wait,
|
|
212
|
+
timeout,
|
|
213
|
+
dryRun
|
|
214
|
+
],
|
|
215
|
+
run: async (_, v, c) => c.workflows.createAvatar({
|
|
216
|
+
image: str(v, "image"),
|
|
217
|
+
name: str(v, "name"),
|
|
218
|
+
resume: str(v, "resume"),
|
|
219
|
+
wait: bool(v, "wait"),
|
|
220
|
+
timeout: num(v, "timeout"),
|
|
221
|
+
dryRun: bool(v, "dryRun")
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
path: "avatars get",
|
|
226
|
+
description: "Read an account avatar.",
|
|
227
|
+
output: "Public Avatar API detail.",
|
|
228
|
+
example: "spatius avatars get 00000000-0000-4000-8000-000000000001",
|
|
229
|
+
args: [{ name: "id", required: true }],
|
|
230
|
+
run: async (a, _, c) => c.workflows.getAvatar(a[0])
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
path: "avatars list",
|
|
234
|
+
description: "List account avatars.",
|
|
235
|
+
output: "Avatars and pagination.nextPageToken.",
|
|
236
|
+
example: "spatius avatars list --page-size 20",
|
|
237
|
+
flags: pages,
|
|
238
|
+
run: async (_, v, c) => c.workflows.listAvatars(pageOptions(v))
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
path: "videos create",
|
|
242
|
+
description: "Render an avatar with audio and an optional background.",
|
|
243
|
+
output: "operationId, jobId, status, createdAt; optional completed job.",
|
|
244
|
+
example: "spatius videos create --avatar-id 00000000-0000-4000-8000-000000000001 --audio ./speech.wav",
|
|
245
|
+
flags: [
|
|
246
|
+
{
|
|
247
|
+
flags: "--avatar-id <id>",
|
|
248
|
+
description: "Public, assigned, or explicitly permitted avatar UUID."
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
flags: "--audio <file-or-url>",
|
|
252
|
+
description: "Supported audio input, at most 500 MiB."
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
flags: "--background <file-or-url>",
|
|
256
|
+
description: "Optional JPEG/PNG/WebP background, at most 50 MiB."
|
|
257
|
+
},
|
|
258
|
+
{ flags: "--name <name>", description: "Video job name." },
|
|
259
|
+
{
|
|
260
|
+
flags: "--request-id <uuid>",
|
|
261
|
+
description: "Retry identity; generated and saved when omitted."
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
flags: "--width <pixels>",
|
|
265
|
+
description: "Even width, 64\u20131920 (default 1024).",
|
|
266
|
+
type: "number"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
flags: "--height <pixels>",
|
|
270
|
+
description: "Even height, 64\u20131920 (default 1024).",
|
|
271
|
+
type: "number"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
flags: "--fit <fit>",
|
|
275
|
+
description: "Avatar fit (default crop).",
|
|
276
|
+
choices: ["crop", "contain"]
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
flags: "--background-color <hex>",
|
|
280
|
+
description: "Six-digit RGB color (default #000000)."
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
flags: "--background-fit <fit>",
|
|
284
|
+
description: "Background fit (default cover).",
|
|
285
|
+
choices: ["cover", "contain", "stretch"]
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
flags: "--lead-in-seconds <seconds>",
|
|
289
|
+
description: "Additional initial idle time, 0\u201360.",
|
|
290
|
+
type: "number"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
flags: "--lead-out-seconds <seconds>",
|
|
294
|
+
description: "Additional final idle time, 0\u201360.",
|
|
295
|
+
type: "number"
|
|
296
|
+
},
|
|
297
|
+
resume,
|
|
298
|
+
wait,
|
|
299
|
+
timeout,
|
|
300
|
+
dryRun
|
|
301
|
+
],
|
|
302
|
+
run: async (_, v, c) => c.workflows.createVideo({
|
|
303
|
+
avatarId: str(v, "avatarId"),
|
|
304
|
+
audio: str(v, "audio"),
|
|
305
|
+
background: str(v, "background"),
|
|
306
|
+
name: str(v, "name"),
|
|
307
|
+
requestId: str(v, "requestId"),
|
|
308
|
+
video: settings(v),
|
|
309
|
+
resume: str(v, "resume"),
|
|
310
|
+
wait: bool(v, "wait"),
|
|
311
|
+
timeout: num(v, "timeout"),
|
|
312
|
+
dryRun: bool(v, "dryRun")
|
|
313
|
+
})
|
|
314
|
+
},
|
|
315
|
+
...["avatar", "video"].flatMap((kind) => {
|
|
316
|
+
const prefix = kind === "avatar" ? "avatars jobs" : "videos";
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
path: `${prefix} get`,
|
|
320
|
+
description: `Read a ${kind} job.`,
|
|
321
|
+
output: "Job detail; a successful video includes a fresh download URL.",
|
|
322
|
+
example: `spatius ${prefix} get 00000000-0000-4000-8000-000000000001`,
|
|
323
|
+
args: [{ name: "id", required: true }],
|
|
324
|
+
run: async (a, _, c) => c.workflows.getJob(kind, a[0])
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
path: `${prefix} list`,
|
|
328
|
+
description: `List ${kind} jobs.`,
|
|
329
|
+
output: "Jobs and pagination.nextPageToken; no download URLs.",
|
|
330
|
+
example: `spatius ${prefix} list --status processing`,
|
|
331
|
+
flags: [...pages, status],
|
|
332
|
+
run: async (_, v, c) => c.workflows.listJobs(kind, pageOptions(v))
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
path: `${prefix} wait`,
|
|
336
|
+
description: `Wait for an existing ${kind} job without submitting another.`,
|
|
337
|
+
output: "Terminal job detail. Deadline reached exits 3 and preserves the job.",
|
|
338
|
+
example: `spatius ${prefix} wait 00000000-0000-4000-8000-000000000001 --timeout 600`,
|
|
339
|
+
args: [{ name: "id", required: true }],
|
|
340
|
+
flags: [timeout],
|
|
341
|
+
run: async (a, v, c) => c.workflows.waitJob(kind, a[0], { timeout: num(v, "timeout") })
|
|
342
|
+
}
|
|
343
|
+
];
|
|
344
|
+
}),
|
|
345
|
+
{
|
|
346
|
+
path: "videos download",
|
|
347
|
+
description: "Fetch a fresh MP4 link and save the output atomically.",
|
|
348
|
+
output: "Saved file path and video job identity.",
|
|
349
|
+
example: "spatius videos download 00000000-0000-4000-8000-000000000001 --output ./video.mp4",
|
|
350
|
+
args: [{ name: "id", required: true }],
|
|
351
|
+
flags: [
|
|
352
|
+
{
|
|
353
|
+
flags: "--output <path>",
|
|
354
|
+
description: "Destination MP4 file (required)."
|
|
355
|
+
},
|
|
356
|
+
{ flags: "--force", description: "Replace an existing destination." }
|
|
357
|
+
],
|
|
358
|
+
run: async (a, v, c) => {
|
|
359
|
+
const output = str(v, "output");
|
|
360
|
+
if (!output)
|
|
361
|
+
throw new CliError("INVALID_ARGUMENT", "--output is required.", {
|
|
362
|
+
exitCode: 2
|
|
363
|
+
});
|
|
364
|
+
return c.workflows.download(a[0], { output, force: bool(v, "force") });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
];
|
|
368
|
+
function commandSchema(path) {
|
|
369
|
+
const found = path ? definitions.filter((d) => d.path === path) : definitions;
|
|
370
|
+
if (!found.length)
|
|
371
|
+
throw new CliError(
|
|
372
|
+
"UNKNOWN_COMMAND",
|
|
373
|
+
"No command matches that schema path.",
|
|
374
|
+
{
|
|
375
|
+
exitCode: 2,
|
|
376
|
+
recovery: "Run spatius schema to discover available commands."
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
return {
|
|
380
|
+
schemaVersion: 1,
|
|
381
|
+
executable: "spatius",
|
|
382
|
+
outputEnvelope: {
|
|
383
|
+
schemaVersion: 1,
|
|
384
|
+
ok: true,
|
|
385
|
+
data: "command-specific result"
|
|
386
|
+
},
|
|
387
|
+
errorEnvelope: {
|
|
388
|
+
schemaVersion: 1,
|
|
389
|
+
ok: false,
|
|
390
|
+
error: {
|
|
391
|
+
code: "string",
|
|
392
|
+
message: "string",
|
|
393
|
+
retryable: "boolean",
|
|
394
|
+
recovery: "string?",
|
|
395
|
+
details: "object?"
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
streams: { result: "stdout", error: "stderr", progress: "stderr" },
|
|
399
|
+
exitCodes: {
|
|
400
|
+
0: "success",
|
|
401
|
+
1: "operation failure",
|
|
402
|
+
2: "invalid arguments",
|
|
403
|
+
3: "wait deadline",
|
|
404
|
+
130: "interrupted"
|
|
405
|
+
},
|
|
406
|
+
commands: found.map(
|
|
407
|
+
({ path: path2, description, args, flags, output, example }) => ({
|
|
408
|
+
path: path2,
|
|
409
|
+
description,
|
|
410
|
+
arguments: args ?? [],
|
|
411
|
+
options: flags ?? [],
|
|
412
|
+
output,
|
|
413
|
+
examples: [example]
|
|
414
|
+
})
|
|
415
|
+
)
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function buildProgram(getContext, emit, version) {
|
|
419
|
+
const root = new Command().name("spatius").description("Avatar and video workflows for coding agents.").version(version).option("--json", "Use structured JSON output (the default).").exitOverride();
|
|
420
|
+
const groups = /* @__PURE__ */ new Map([["", root]]);
|
|
421
|
+
for (const def of definitions) {
|
|
422
|
+
const parts = def.path.split(" ");
|
|
423
|
+
let parent = root;
|
|
424
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
425
|
+
const key = parts.slice(0, i + 1).join(" ");
|
|
426
|
+
let group = groups.get(key);
|
|
427
|
+
if (!group) {
|
|
428
|
+
group = parent.command(parts[i]);
|
|
429
|
+
groups.set(key, group);
|
|
430
|
+
}
|
|
431
|
+
parent = group;
|
|
432
|
+
}
|
|
433
|
+
const cmd = parent.command(parts.at(-1)).description(def.description);
|
|
434
|
+
for (const arg of def.args ?? [])
|
|
435
|
+
cmd.argument(arg.required ? `<${arg.name}>` : `[${arg.name}]`);
|
|
436
|
+
for (const flag of def.flags ?? []) {
|
|
437
|
+
const option = new Option(flag.flags, flag.description);
|
|
438
|
+
if (flag.choices) option.choices(flag.choices);
|
|
439
|
+
if (flag.type === "number")
|
|
440
|
+
option.argParser((value) => {
|
|
441
|
+
const parsed = Number(value);
|
|
442
|
+
if (!value.trim() || !Number.isFinite(parsed))
|
|
443
|
+
throw new CliError(
|
|
444
|
+
"INVALID_ARGUMENT",
|
|
445
|
+
"Expected a finite number.",
|
|
446
|
+
{ exitCode: 2 }
|
|
447
|
+
);
|
|
448
|
+
return parsed;
|
|
449
|
+
});
|
|
450
|
+
if (flag.default !== void 0) option.default(flag.default);
|
|
451
|
+
cmd.addOption(option);
|
|
452
|
+
}
|
|
453
|
+
cmd.action(async (...args) => {
|
|
454
|
+
const count = def.args?.length ?? 0;
|
|
455
|
+
const values = args[count];
|
|
456
|
+
if (typeof values.timeout === "number" && values.timeout <= 0)
|
|
457
|
+
throw new CliError("INVALID_ARGUMENT", "--timeout must be positive.", {
|
|
458
|
+
exitCode: 2
|
|
459
|
+
});
|
|
460
|
+
emit(
|
|
461
|
+
await def.run(args.slice(0, count), values, getContext())
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
root.command("schema").description(
|
|
466
|
+
"Describe commands and structured output for this CLI version."
|
|
467
|
+
).argument("[command...]").action(
|
|
468
|
+
(parts) => emit(commandSchema(parts.length ? parts.join(" ") : void 0))
|
|
469
|
+
);
|
|
470
|
+
return root;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export {
|
|
474
|
+
CliError,
|
|
475
|
+
asCliError,
|
|
476
|
+
definitions,
|
|
477
|
+
commandSchema,
|
|
478
|
+
buildProgram
|
|
479
|
+
};
|