@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 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
+ }