@wavespeed/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 +158 -0
- package/dist/cli.js +78 -0
- package/dist/commands/aliases.js +53 -0
- package/dist/commands/auth.js +175 -0
- package/dist/commands/download.js +73 -0
- package/dist/commands/init.js +63 -0
- package/dist/commands/models.js +101 -0
- package/dist/commands/run.js +143 -0
- package/dist/commands/schema.js +155 -0
- package/dist/commands/skill.js +40 -0
- package/dist/commands/upload.js +51 -0
- package/dist/lib/api.js +67 -0
- package/dist/lib/banner.js +13 -0
- package/dist/lib/client.js +19 -0
- package/dist/lib/config.js +86 -0
- package/dist/lib/download-flag.js +24 -0
- package/dist/lib/dynamic-help.js +162 -0
- package/dist/lib/inputs.js +52 -0
- package/dist/lib/log.js +27 -0
- package/dist/lib/output.js +77 -0
- package/package.json +66 -0
- package/skills/SKILL.md +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WaveSpeedAI
|
|
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,158 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://wavespeed.ai" target="_blank">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/WaveSpeedAI/waverless/main/docs/images/wavespeed-dark-logo.png" alt="WaveSpeedAI Logo" width="200"/>
|
|
4
|
+
</a>
|
|
5
|
+
|
|
6
|
+
<h1>WaveSpeed CLI</h1>
|
|
7
|
+
<p><strong>One command, every WaveSpeed model. Image, video, audio, 3D — from your terminal.</strong></p>
|
|
8
|
+
<p>
|
|
9
|
+
<a href="https://wavespeed.ai">🌐 wavespeed.ai</a> •
|
|
10
|
+
<a href="https://wavespeed.ai/models">📚 Models</a> •
|
|
11
|
+
<a href="https://wavespeed.ai/docs">📖 Docs</a> •
|
|
12
|
+
<a href="https://github.com/WaveSpeedAI/cli/issues">🐛 Issues</a>
|
|
13
|
+
</p>
|
|
14
|
+
<p>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@wavespeed/cli"><img alt="npm" src="https://img.shields.io/npm/v/@wavespeed/cli?style=flat-square&color=7c5cff"></a>
|
|
16
|
+
<a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-7c5cff?style=flat-square"></a>
|
|
17
|
+
<img alt="node" src="https://img.shields.io/badge/node-%E2%89%A518-7c5cff?style=flat-square">
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- 🚀 **Every WaveSpeed model** — image, video, audio, 3D; the full live catalog, not a curated subset
|
|
26
|
+
- 🧬 **Dynamic per-model help** — `wavespeed run <id> -h` introspects the real input schema
|
|
27
|
+
- 🔗 **Aliases** — named shortcuts in `wavespeed.json` that bundle a model + default inputs
|
|
28
|
+
- 🤖 **Agent-native** — pipe-safe `--json`, drops a `SKILL.md` for Claude Code / Cursor / Codex
|
|
29
|
+
- ✨ **Zero-friction auth** — browser-launching, clipboard-aware login with live key validation
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# npm
|
|
35
|
+
npm install -g @wavespeed/cli
|
|
36
|
+
|
|
37
|
+
# or curl
|
|
38
|
+
curl -fsSL https://wavespeed.ai/install | bash
|
|
39
|
+
|
|
40
|
+
# sign in (opens the access-key page, paste to confirm)
|
|
41
|
+
wavespeed login
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 1. Browse the catalog
|
|
48
|
+
wavespeed models # browse the full live catalog, grouped by type
|
|
49
|
+
wavespeed models "nano banana" # search
|
|
50
|
+
wavespeed models --type text-to-video # filter by modality
|
|
51
|
+
|
|
52
|
+
# 2. Inspect a model's inputs (dynamic — fetched live)
|
|
53
|
+
wavespeed run google/nano-banana-2/text-to-image -h
|
|
54
|
+
|
|
55
|
+
# 3. Run it (URLs print to stdout; no files unless you ask)
|
|
56
|
+
wavespeed run google/nano-banana-2/text-to-image \
|
|
57
|
+
-p "a cyberpunk skyline at golden hour" \
|
|
58
|
+
-i aspect_ratio="16:9" -i resolution="2k"
|
|
59
|
+
|
|
60
|
+
# Save outputs locally
|
|
61
|
+
wavespeed run ... -p "..." --download # → ./wavespeed-output/
|
|
62
|
+
wavespeed run ... -p "..." --download "./hero.png" # exact path
|
|
63
|
+
wavespeed run ... -p "..." --download "./out/{index}.{ext}" # templated for batches
|
|
64
|
+
|
|
65
|
+
# Pipe-safe JSON
|
|
66
|
+
wavespeed run ... -p "..." --json | jq '.outputs[0]'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Project config (`wavespeed.json`)
|
|
70
|
+
|
|
71
|
+
`wavespeed init` writes one. Drop it into git so the whole team shares the same defaults.
|
|
72
|
+
|
|
73
|
+
```jsonc
|
|
74
|
+
{
|
|
75
|
+
"defaultModel": "google/nano-banana-2/text-to-image",
|
|
76
|
+
"outputDir": "wavespeed-output",
|
|
77
|
+
"aliases": {
|
|
78
|
+
"hero": { "model": "google/nano-banana-2/text-to-image",
|
|
79
|
+
"input": { "aspect_ratio": "16:9", "resolution": "2k" } },
|
|
80
|
+
"social": { "model": "google/nano-banana-2/text-to-image",
|
|
81
|
+
"input": { "aspect_ratio": "1:1", "resolution": "1k" } },
|
|
82
|
+
"vid": { "model": "bytedance/seedance-2.0/text-to-video",
|
|
83
|
+
"input": { "resolution": "720p", "duration": 5 } }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **`defaultModel`** — `wavespeed run -p "…"` (no model arg) uses this. Can itself be an alias name.
|
|
89
|
+
- **`aliases`** — your own shortcuts. `wavespeed run hero -p "…"` expands to model + input. CLI `-i k=v` flags override. `wavespeed run hero -h` shows the resolved schema. List them with `wavespeed aliases`.
|
|
90
|
+
|
|
91
|
+
Resolution: positional with `/` is a model ID; otherwise looked up as an alias. Merge order on inputs: `alias.input` → `--input-file` → `-i k=v` → `-p`. **The CLI never mutates the user's prompt or inputs** — what you pass is what hits the API.
|
|
92
|
+
|
|
93
|
+
## Recommended starting models
|
|
94
|
+
|
|
95
|
+
| Use case | Model |
|
|
96
|
+
|---|---|
|
|
97
|
+
| Text → image | `google/nano-banana-2/text-to-image` |
|
|
98
|
+
| Image edit | `google/nano-banana-2/edit` |
|
|
99
|
+
| Text → video | `bytedance/seedance-2.0/text-to-video` |
|
|
100
|
+
| Image → video | `bytedance/seedance-2.0/image-to-video` |
|
|
101
|
+
|
|
102
|
+
Browse alternatives with `wavespeed models <query>`.
|
|
103
|
+
|
|
104
|
+
## Use from coding agents
|
|
105
|
+
|
|
106
|
+
No MCP server, no daemon. The CLI **is** the interface — agents already know how to run shell commands.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
wavespeed skill install # writes .claude/skills/wavespeed/SKILL.md
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The skill teaches the agent the three-step pattern: `models` to find, `run <id> -h` to discover params, `run <id> -p "…" --json` to execute. Every command supports `--json` for clean piping.
|
|
113
|
+
|
|
114
|
+
## Commands
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
wavespeed login Browser + paste-key wizard (clipboard-aware)
|
|
118
|
+
wavespeed logout Clear stored API key
|
|
119
|
+
wavespeed status Show masked key, base URL, paths
|
|
120
|
+
wavespeed config [--default-model …] View / update CLI defaults
|
|
121
|
+
|
|
122
|
+
wavespeed run [model|alias] -p "…" Run any model or alias (uses defaultModel if omitted)
|
|
123
|
+
wavespeed run <model|alias> -h Dynamic schema-based help (alias-aware)
|
|
124
|
+
wavespeed schema <model> Pretty-print a model's input schema
|
|
125
|
+
wavespeed models [query] Browse the live catalog (cached 1h)
|
|
126
|
+
wavespeed aliases List aliases from wavespeed.json + user config
|
|
127
|
+
|
|
128
|
+
wavespeed upload <file...> Upload local file(s) → CDN URLs
|
|
129
|
+
wavespeed download <url...> Save URLs to disk
|
|
130
|
+
|
|
131
|
+
wavespeed init Create a wavespeed.json with defaults + alias stubs
|
|
132
|
+
wavespeed skill install Drop SKILL.md for coding agents
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Auth
|
|
136
|
+
|
|
137
|
+
| | Use when |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `wavespeed login` | Persistent, one machine, one user. Stored in `~/.config/wavespeed-nodejs/config.json`. |
|
|
140
|
+
| `WAVESPEED_API_KEY=… wavespeed run …` | CI, scripts, one-off shells. Env var wins over stored config. |
|
|
141
|
+
|
|
142
|
+
Get a key at [wavespeed.ai/accesskey](https://wavespeed.ai/accesskey).
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
git clone https://github.com/WaveSpeedAI/cli.git
|
|
148
|
+
cd cli
|
|
149
|
+
npm install
|
|
150
|
+
npm run dev -- run google/nano-banana-2/text-to-image -p "a serene mountain lake"
|
|
151
|
+
npm run build && npm link
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Stack: TypeScript, Commander, Inquirer, Chalk, Ora, the official `wavespeed` SDK.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT — see [LICENSE](LICENSE).
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { printBanner } from './lib/banner.js';
|
|
8
|
+
import { registerAuth } from './commands/auth.js';
|
|
9
|
+
import { registerRun } from './commands/run.js';
|
|
10
|
+
import { registerUpload } from './commands/upload.js';
|
|
11
|
+
import { registerDownload } from './commands/download.js';
|
|
12
|
+
import { registerModels } from './commands/models.js';
|
|
13
|
+
import { registerSchema } from './commands/schema.js';
|
|
14
|
+
import { registerAliases } from './commands/aliases.js';
|
|
15
|
+
import { registerInit } from './commands/init.js';
|
|
16
|
+
import { registerSkill } from './commands/skill.js';
|
|
17
|
+
import { detectRunHelp, printDynamicRunHelp } from './lib/dynamic-help.js';
|
|
18
|
+
// Read version from package.json at runtime so a bump in package.json is
|
|
19
|
+
// reflected automatically (no second place to keep in sync).
|
|
20
|
+
const VERSION = (() => {
|
|
21
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
// dist/cli.js → ../package.json (one level up because files: ["dist", ...])
|
|
23
|
+
const pkgPath = path.resolve(here, '..', 'package.json');
|
|
24
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
|
25
|
+
})();
|
|
26
|
+
const program = new Command();
|
|
27
|
+
program
|
|
28
|
+
.name('wavespeed')
|
|
29
|
+
.description('WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.')
|
|
30
|
+
.version(VERSION, '-v, --version', 'Print the CLI version')
|
|
31
|
+
.addHelpText('beforeAll', chalk.hex('#7c5cff').bold('\n wavespeed ') + chalk.gray(`v${VERSION} · every WaveSpeed model · open source\n`))
|
|
32
|
+
.addHelpText('after', `
|
|
33
|
+
${chalk.bold('Quick start:')}
|
|
34
|
+
${chalk.cyan('$ wavespeed login')}
|
|
35
|
+
${chalk.cyan('$ wavespeed models # browse the catalog')}
|
|
36
|
+
${chalk.cyan('$ wavespeed run google/nano-banana-2/text-to-image -h # see the model\'s flags')}
|
|
37
|
+
${chalk.cyan('$ wavespeed run google/nano-banana-2/text-to-image -p "a cyberpunk skyline"')}
|
|
38
|
+
${chalk.cyan('$ wavespeed run bytedance/seedance-2.0/text-to-video -p "drone shot over snowy peaks"')}
|
|
39
|
+
|
|
40
|
+
${chalk.bold('Use from any coding agent (Claude Code, Cursor, Codex):')}
|
|
41
|
+
${chalk.cyan('$ wavespeed skill install')} ${chalk.gray('# drops a SKILL.md so the agent knows how to call wavespeed')}
|
|
42
|
+
${chalk.gray('# agents just shell out: `wavespeed run <id> --json` — no MCP setup.')}
|
|
43
|
+
|
|
44
|
+
${chalk.gray('Docs:')} ${chalk.cyan('https://wavespeed.ai/docs')} ${chalk.gray('Models:')} ${chalk.cyan('https://wavespeed.ai/models')}
|
|
45
|
+
`);
|
|
46
|
+
registerAuth(program);
|
|
47
|
+
registerRun(program);
|
|
48
|
+
registerUpload(program);
|
|
49
|
+
registerDownload(program);
|
|
50
|
+
registerModels(program);
|
|
51
|
+
registerSchema(program);
|
|
52
|
+
registerAliases(program);
|
|
53
|
+
registerInit(program);
|
|
54
|
+
registerSkill(program);
|
|
55
|
+
program
|
|
56
|
+
.command('hello', { hidden: true })
|
|
57
|
+
.description('Print the WaveSpeed banner')
|
|
58
|
+
.action(() => printBanner());
|
|
59
|
+
// Show banner + help when run with no args
|
|
60
|
+
if (process.argv.length <= 2) {
|
|
61
|
+
printBanner();
|
|
62
|
+
program.outputHelp();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
async function main() {
|
|
66
|
+
// Intercept `wavespeed run <model> --help` so we can introspect the model's
|
|
67
|
+
// schema and print model-specific flags instead of Commander's static help.
|
|
68
|
+
const dynamicModel = detectRunHelp(process.argv);
|
|
69
|
+
if (dynamicModel) {
|
|
70
|
+
await printDynamicRunHelp(dynamicModel);
|
|
71
|
+
// If schema lookup failed, fall through to Commander's static help.
|
|
72
|
+
}
|
|
73
|
+
await program.parseAsync(process.argv);
|
|
74
|
+
}
|
|
75
|
+
main().catch((err) => {
|
|
76
|
+
console.error(chalk.red('Error: ') + (err.message ?? String(err)));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadAliases, loadProjectConfig, PROJECT_FILE, userConfig } from '../lib/config.js';
|
|
3
|
+
export function registerAliases(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('aliases')
|
|
6
|
+
.description('List model aliases defined in wavespeed.json + user config')
|
|
7
|
+
.option('--json', 'Emit the alias map as JSON')
|
|
8
|
+
.action((opts) => {
|
|
9
|
+
const aliases = loadAliases();
|
|
10
|
+
const project = loadProjectConfig();
|
|
11
|
+
const userMap = userConfig.get('aliases') ?? {};
|
|
12
|
+
const projectMap = project?.aliases ?? {};
|
|
13
|
+
if (opts.json) {
|
|
14
|
+
process.stdout.write(JSON.stringify(aliases, null, 2) + '\n');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const names = Object.keys(aliases);
|
|
18
|
+
if (names.length === 0) {
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.gray('No aliases defined yet.'));
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(chalk.bold('Add one to ' + chalk.cyan(PROJECT_FILE) + ':'));
|
|
23
|
+
console.log(chalk.gray(JSON.stringify({
|
|
24
|
+
aliases: {
|
|
25
|
+
hero: {
|
|
26
|
+
model: 'google/nano-banana-2/text-to-image',
|
|
27
|
+
input: { aspect_ratio: '16:9', resolution: '2k' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
}, null, 2)));
|
|
31
|
+
console.log();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(chalk.bold(`${names.length} alias${names.length === 1 ? '' : 'es'}`));
|
|
36
|
+
console.log();
|
|
37
|
+
const width = Math.max(...names.map((n) => n.length));
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
const a = aliases[name];
|
|
40
|
+
const origin = name in projectMap ? chalk.gray(' · project') : name in userMap ? chalk.gray(' · user') : '';
|
|
41
|
+
console.log(' ' + chalk.cyan(name.padEnd(width)) + ' ' + chalk.white(a.model) + origin);
|
|
42
|
+
if (a.description)
|
|
43
|
+
console.log(' ' + ' '.repeat(width) + ' ' + chalk.gray(a.description));
|
|
44
|
+
if (a.input && Object.keys(a.input).length > 0) {
|
|
45
|
+
console.log(' ' + ' '.repeat(width) + ' ' + chalk.gray(JSON.stringify(a.input)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.gray('Use: ') + chalk.cyan('wavespeed run <alias> -p "…"'));
|
|
50
|
+
console.log(chalk.gray('Inspect: ') + chalk.cyan('wavespeed run <alias> -h'));
|
|
51
|
+
console.log();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { password, confirm } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import clipboard from 'clipboardy';
|
|
6
|
+
import { userConfig, getBaseUrl, getApiKey, loadProjectConfig, PROJECT_FILE } from '../lib/config.js';
|
|
7
|
+
const KEY_URL = 'https://wavespeed.ai/accesskey';
|
|
8
|
+
function mask(key) {
|
|
9
|
+
if (key.length < 12)
|
|
10
|
+
return '••••';
|
|
11
|
+
return key.slice(0, 5) + '…' + key.slice(-4);
|
|
12
|
+
}
|
|
13
|
+
function looksLikeKey(s) {
|
|
14
|
+
if (!s)
|
|
15
|
+
return false;
|
|
16
|
+
const t = s.trim();
|
|
17
|
+
// WaveSpeed access keys are long hex strings (~64 chars). Be liberal:
|
|
18
|
+
// any 40+ char single-line token of [a-zA-Z0-9_-] counts.
|
|
19
|
+
return /^[A-Za-z0-9_-]{40,}$/.test(t);
|
|
20
|
+
}
|
|
21
|
+
async function readClipboardKey() {
|
|
22
|
+
try {
|
|
23
|
+
const value = await clipboard.read();
|
|
24
|
+
if (looksLikeKey(value))
|
|
25
|
+
return value.trim();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* no clipboard access — that's fine */
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
async function validateKey(apiKey, baseUrl) {
|
|
33
|
+
// The platform doesn't expose a dedicated /whoami yet, so we ping a known
|
|
34
|
+
// authenticated endpoint and treat anything that isn't 401/403 as "key works".
|
|
35
|
+
let res = null;
|
|
36
|
+
try {
|
|
37
|
+
res = await fetch(`${baseUrl}/api/v3/balance`, {
|
|
38
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return { ok: false, reason: `Could not reach ${baseUrl}: ${err.message}` };
|
|
43
|
+
}
|
|
44
|
+
if (res.status === 401 || res.status === 403) {
|
|
45
|
+
return { ok: false, status: res.status, reason: 'API rejected the key (401/403).' };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, status: res.status };
|
|
48
|
+
}
|
|
49
|
+
async function loginFlow(opts) {
|
|
50
|
+
console.log();
|
|
51
|
+
console.log(chalk.hex('#7c5cff').bold(' Welcome to WaveSpeed.'));
|
|
52
|
+
console.log(chalk.gray(' Sign in once and you\'re set on this machine.\n'));
|
|
53
|
+
let apiKey = opts.apiKey?.trim();
|
|
54
|
+
if (!apiKey && opts.browser) {
|
|
55
|
+
console.log(chalk.bold(' 1.') + ' Opening ' + chalk.cyan(KEY_URL) + chalk.gray(' (sign in if needed)'));
|
|
56
|
+
try {
|
|
57
|
+
await open(KEY_URL);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
console.log(chalk.gray(' (browser launch failed — open the link manually)'));
|
|
61
|
+
}
|
|
62
|
+
console.log(chalk.bold(' 2.') + ' Click ' + chalk.bold('Create key') + chalk.gray(' and copy it'));
|
|
63
|
+
console.log(chalk.bold(' 3.') + ' Paste below — we\'ll detect & validate it for you');
|
|
64
|
+
console.log();
|
|
65
|
+
}
|
|
66
|
+
// Try clipboard first — major UX win.
|
|
67
|
+
let detected = null;
|
|
68
|
+
if (!apiKey) {
|
|
69
|
+
detected = await readClipboardKey();
|
|
70
|
+
}
|
|
71
|
+
if (!apiKey && detected) {
|
|
72
|
+
const useIt = await confirm({
|
|
73
|
+
message: `Detected a key on your clipboard (${mask(detected)}). Use it?`,
|
|
74
|
+
default: true,
|
|
75
|
+
});
|
|
76
|
+
if (useIt)
|
|
77
|
+
apiKey = detected;
|
|
78
|
+
}
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
apiKey = await password({
|
|
81
|
+
message: 'Paste your API key:',
|
|
82
|
+
mask: '*',
|
|
83
|
+
validate: (v) => (v.trim().length > 8 ? true : 'API key looks too short.'),
|
|
84
|
+
});
|
|
85
|
+
apiKey = apiKey.trim();
|
|
86
|
+
}
|
|
87
|
+
const spinner = ora({ text: 'Validating with api.wavespeed.ai…', color: 'magenta' }).start();
|
|
88
|
+
const result = await validateKey(apiKey, getBaseUrl());
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
spinner.fail(result.reason ?? 'The key did not authenticate.');
|
|
91
|
+
console.log();
|
|
92
|
+
console.log(chalk.gray(' Double-check at ') + chalk.cyan(KEY_URL));
|
|
93
|
+
console.log(chalk.gray(' Or set ') + chalk.cyan('WAVESPEED_API_KEY') + chalk.gray(' in your shell and skip login altogether.'));
|
|
94
|
+
console.log();
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
spinner.succeed('Signed in.');
|
|
98
|
+
userConfig.set('apiKey', apiKey);
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(' ' + chalk.gray('Key: ') + chalk.green(mask(apiKey)));
|
|
101
|
+
console.log(' ' + chalk.gray('Stored: ') + chalk.cyan(userConfig.path));
|
|
102
|
+
console.log();
|
|
103
|
+
console.log(chalk.bold('Try it:'));
|
|
104
|
+
console.log(' ' + chalk.cyan('wavespeed models # browse the catalog'));
|
|
105
|
+
console.log(' ' + chalk.cyan('wavespeed run google/nano-banana-2/text-to-image -p "a cyberpunk skyline at golden hour"'));
|
|
106
|
+
console.log(' ' + chalk.cyan('wavespeed run bytedance/seedance-2.0/text-to-video -p "drone shot over snowy peaks"'));
|
|
107
|
+
console.log();
|
|
108
|
+
}
|
|
109
|
+
export function registerAuth(program) {
|
|
110
|
+
program
|
|
111
|
+
.command('login')
|
|
112
|
+
.description('Sign in to Wavespeed on this machine (browser + paste-key wizard)')
|
|
113
|
+
.option('--api-key <key>', 'Pre-fill the API key (skips the prompt)')
|
|
114
|
+
.option('--no-browser', 'Skip opening the access-key page in a browser')
|
|
115
|
+
.action(async (opts) => {
|
|
116
|
+
await loginFlow(opts);
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.command('logout')
|
|
120
|
+
.description('Clear the stored API key')
|
|
121
|
+
.action(() => {
|
|
122
|
+
userConfig.delete('apiKey');
|
|
123
|
+
console.log(chalk.green('Signed out.'));
|
|
124
|
+
});
|
|
125
|
+
program
|
|
126
|
+
.command('status')
|
|
127
|
+
.description('Show the active account, base URL, and where the key is stored')
|
|
128
|
+
.action(() => {
|
|
129
|
+
const key = getApiKey();
|
|
130
|
+
const baseUrl = getBaseUrl();
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(chalk.bold('WaveSpeed CLI status'));
|
|
133
|
+
console.log(' ' + chalk.gray('API key: ') + (key ? chalk.green(mask(key)) : chalk.red('not set — run `wavespeed login`')));
|
|
134
|
+
console.log(' ' + chalk.gray('Base URL: ') + chalk.cyan(baseUrl));
|
|
135
|
+
console.log(' ' + chalk.gray('Config: ') + chalk.cyan(userConfig.path));
|
|
136
|
+
console.log(' ' +
|
|
137
|
+
chalk.gray('Source: ') +
|
|
138
|
+
(process.env.WAVESPEED_API_KEY
|
|
139
|
+
? chalk.yellow('WAVESPEED_API_KEY env var (overrides stored config)')
|
|
140
|
+
: chalk.gray('stored config')));
|
|
141
|
+
const project = loadProjectConfig();
|
|
142
|
+
if (project) {
|
|
143
|
+
console.log(' ' + chalk.gray('Project: ') + chalk.cyan(PROJECT_FILE) + chalk.gray(' detected'));
|
|
144
|
+
}
|
|
145
|
+
console.log();
|
|
146
|
+
});
|
|
147
|
+
program
|
|
148
|
+
.command('config')
|
|
149
|
+
.description('View or change CLI defaults (base URL, default model, output dir)')
|
|
150
|
+
.option('--base-url <url>', 'Override API base URL')
|
|
151
|
+
.option('--default-model <id>', 'Default model used by `wavespeed run` when no model is given')
|
|
152
|
+
.option('--output-dir <dir>', 'Where to save outputs when --download has no path')
|
|
153
|
+
.option('--reset', 'Reset all CLI config to defaults')
|
|
154
|
+
.action((opts) => {
|
|
155
|
+
if (opts.reset) {
|
|
156
|
+
userConfig.clear();
|
|
157
|
+
console.log(chalk.green('Config reset.'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (opts.baseUrl)
|
|
161
|
+
userConfig.set('baseUrl', opts.baseUrl);
|
|
162
|
+
if (opts.defaultModel)
|
|
163
|
+
userConfig.set('defaultModel', opts.defaultModel);
|
|
164
|
+
if (opts.outputDir)
|
|
165
|
+
userConfig.set('outputDir', opts.outputDir);
|
|
166
|
+
const store = userConfig.store;
|
|
167
|
+
console.log();
|
|
168
|
+
console.log(chalk.bold('Stored config:'));
|
|
169
|
+
for (const [k, v] of Object.entries(store)) {
|
|
170
|
+
const printed = k === 'apiKey' && typeof v === 'string' ? mask(v) : JSON.stringify(v);
|
|
171
|
+
console.log(' ' + chalk.gray(k.padEnd(22)) + chalk.cyan(printed));
|
|
172
|
+
}
|
|
173
|
+
console.log();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
import { resolveOutputDir } from '../lib/config.js';
|
|
8
|
+
import { downloadOutputs } from '../lib/output.js';
|
|
9
|
+
async function downloadOne(url, dest) {
|
|
10
|
+
const res = await fetch(url);
|
|
11
|
+
if (!res.ok || !res.body) {
|
|
12
|
+
throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
|
|
13
|
+
}
|
|
14
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
15
|
+
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(dest));
|
|
16
|
+
}
|
|
17
|
+
export function registerDownload(program) {
|
|
18
|
+
program
|
|
19
|
+
.command('download')
|
|
20
|
+
.description('Download one or more remote URLs to your output directory')
|
|
21
|
+
.argument('<url...>', 'Remote URL(s) to fetch')
|
|
22
|
+
.option('-o, --output <path>', 'Write to this exact path (only valid with a single URL)')
|
|
23
|
+
.option('--output-dir <dir>', 'Directory to save into (default: wavespeed-output/)')
|
|
24
|
+
.option('--json', 'Emit a single JSON object on stdout')
|
|
25
|
+
.action(async (urls, opts) => {
|
|
26
|
+
if (opts.output && urls.length > 1) {
|
|
27
|
+
const msg = '`-o/--output` only works with a single URL. Use `--output-dir` for batches.';
|
|
28
|
+
if (opts.json)
|
|
29
|
+
process.stdout.write(JSON.stringify({ error: msg }, null, 2) + '\n');
|
|
30
|
+
else
|
|
31
|
+
console.error(chalk.red(msg));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
let saved = [];
|
|
35
|
+
if (opts.output) {
|
|
36
|
+
const spinner = !opts.json ? ora(`Downloading ${urls[0]}…`).start() : null;
|
|
37
|
+
try {
|
|
38
|
+
const dest = path.resolve(opts.output);
|
|
39
|
+
await downloadOne(urls[0], dest);
|
|
40
|
+
saved = [dest];
|
|
41
|
+
spinner?.succeed(`Saved ${path.relative(process.cwd(), dest)}`);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
spinner?.fail(err.message ?? String(err));
|
|
45
|
+
if (opts.json)
|
|
46
|
+
process.stdout.write(JSON.stringify({ error: err.message ?? String(err) }, null, 2) + '\n');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const dir = opts.outputDir || resolveOutputDir();
|
|
52
|
+
const spinner = !opts.json ? ora(`Downloading ${urls.length} file${urls.length > 1 ? 's' : ''}…`).start() : null;
|
|
53
|
+
try {
|
|
54
|
+
saved = await downloadOutputs(urls, { outputDir: dir });
|
|
55
|
+
spinner?.succeed(`Saved ${saved.length} file${saved.length > 1 ? 's' : ''}.`);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
spinner?.fail(err.message ?? String(err));
|
|
59
|
+
if (opts.json)
|
|
60
|
+
process.stdout.write(JSON.stringify({ error: err.message ?? String(err) }, null, 2) + '\n');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (opts.json) {
|
|
65
|
+
process.stdout.write(JSON.stringify({ outputs: urls, saved }, null, 2) + '\n');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.log();
|
|
69
|
+
for (const f of saved)
|
|
70
|
+
console.log(' ' + chalk.cyan(path.relative(process.cwd(), f)));
|
|
71
|
+
console.log();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { input, confirm } from '@inquirer/prompts';
|
|
5
|
+
import { PROJECT_FILE } from '../lib/config.js';
|
|
6
|
+
const STARTER_MODEL = 'google/nano-banana-2/text-to-image';
|
|
7
|
+
export function registerInit(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('init')
|
|
10
|
+
.description('Create a wavespeed.json with a defaultModel + alias stubs in this directory')
|
|
11
|
+
.option('-y, --yes', 'Skip prompts, write sensible defaults')
|
|
12
|
+
.option('-f, --force', 'Overwrite an existing wavespeed.json')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const target = path.resolve(PROJECT_FILE);
|
|
15
|
+
if (fs.existsSync(target) && !opts.force) {
|
|
16
|
+
console.log(chalk.yellow(`${PROJECT_FILE} already exists.`) + chalk.gray(' Pass --force to overwrite.'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
let project;
|
|
20
|
+
if (opts.yes) {
|
|
21
|
+
project = {
|
|
22
|
+
$schema: 'https://wavespeed.ai/schema/cli.json',
|
|
23
|
+
defaultModel: STARTER_MODEL,
|
|
24
|
+
outputDir: 'wavespeed-output',
|
|
25
|
+
aliases: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(chalk.bold('Project config'));
|
|
31
|
+
console.log(chalk.gray('Sets the per-project defaults the team can share via git. You can edit wavespeed.json anytime.'));
|
|
32
|
+
console.log();
|
|
33
|
+
const defaultModel = await input({
|
|
34
|
+
message: 'Default model (browse with `wavespeed models`):',
|
|
35
|
+
default: STARTER_MODEL,
|
|
36
|
+
});
|
|
37
|
+
const outputDir = await input({ message: 'Output directory (for --download):', default: 'wavespeed-output' });
|
|
38
|
+
project = {
|
|
39
|
+
$schema: 'https://wavespeed.ai/schema/cli.json',
|
|
40
|
+
defaultModel,
|
|
41
|
+
outputDir,
|
|
42
|
+
aliases: {},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(target, JSON.stringify(project, null, 2) + '\n');
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(chalk.green('Wrote ') + chalk.cyan(PROJECT_FILE));
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.gray('Now try:'));
|
|
50
|
+
console.log(' ' + chalk.cyan('wavespeed run -p "a serene mountain lake at sunrise"'));
|
|
51
|
+
console.log(chalk.gray('Add aliases by editing wavespeed.json — see ') + chalk.cyan('wavespeed aliases'));
|
|
52
|
+
console.log();
|
|
53
|
+
if (!opts.yes) {
|
|
54
|
+
const tellAgent = await confirm({
|
|
55
|
+
message: 'Add a Claude/Cursor skill so agents can call Wavespeed too?',
|
|
56
|
+
default: true,
|
|
57
|
+
});
|
|
58
|
+
if (tellAgent) {
|
|
59
|
+
await import('./skill.js').then(({ writeSkill }) => writeSkill(process.cwd()));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|