@wavespeed/cli 0.4.14 → 0.4.15

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/README.md CHANGED
@@ -42,6 +42,10 @@ npm install -g @wavespeed/cli
42
42
  # friendlier error path)
43
43
  curl -fsSL https://raw.githubusercontent.com/WaveSpeedAI/wavespeed-cli/main/install.sh | bash
44
44
 
45
+ # no Node.js/npm? install.sh falls back to a standalone executable on macOS/Linux;
46
+ # on Windows (PowerShell):
47
+ irm https://raw.githubusercontent.com/WaveSpeedAI/wavespeed-cli/main/install.ps1 | iex
48
+
45
49
  # sign in (opens the access-key page, paste to confirm)
46
50
  wavespeed login
47
51
  ```
@@ -136,6 +140,7 @@ wavespeed login Browser + paste-key wizard (clipboard-a
136
140
  wavespeed logout Clear stored API key
137
141
  wavespeed status Show masked key, base URL, useful links
138
142
  wavespeed config [--default-model …] View / update CLI defaults
143
+ wavespeed upgrade [--check] [--json] Upgrade to the latest version (--check only reports)
139
144
 
140
145
  # generation
141
146
  wavespeed run [model|alias] -p "…" Run any model or alias (uses defaultModel if omitted)
package/dist/cli.js CHANGED
@@ -1,9 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
4
  import { printBanner } from './lib/banner.js';
8
5
  import { registerAuth } from './commands/auth.js';
9
6
  import { registerRun } from './commands/run.js';
@@ -23,15 +20,9 @@ import { registerDelete } from './commands/delete.js';
23
20
  import { registerOpen } from './commands/open.js';
24
21
  import { registerBillings } from './commands/billings.js';
25
22
  import { registerUsage } from './commands/usage.js';
23
+ import { registerUpgrade } from './commands/upgrade.js';
24
+ import { VERSION } from './lib/version.js';
26
25
  import { detectRunHelp, printDynamicRunHelp } from './lib/dynamic-help.js';
27
- // Read version from package.json at runtime so a bump in package.json is
28
- // reflected automatically (no second place to keep in sync).
29
- const VERSION = (() => {
30
- const here = path.dirname(fileURLToPath(import.meta.url));
31
- // dist/cli.js → ../package.json (one level up because files: ["dist", ...])
32
- const pkgPath = path.resolve(here, '..', 'package.json');
33
- return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
34
- })();
35
26
  const program = new Command();
36
27
  program
37
28
  .name('wavespeed')
@@ -70,6 +61,7 @@ registerBillings(program);
70
61
  registerUsage(program);
71
62
  registerInit(program);
72
63
  registerSkill(program);
64
+ registerUpgrade(program);
73
65
  program
74
66
  .command('hello', { hidden: true })
75
67
  .description('Print the WaveSpeed banner')
@@ -2,6 +2,11 @@ import chalk from 'chalk';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ function skillBody() {
6
+ if (typeof __WS_SKILL__ === 'string')
7
+ return __WS_SKILL__;
8
+ return fs.readFileSync(skillSourcePath(), 'utf8');
9
+ }
5
10
  function skillSourcePath() {
6
11
  const here = path.dirname(fileURLToPath(import.meta.url));
7
12
  // dist/commands/skill.js -> ../../skills/SKILL.md
@@ -16,7 +21,7 @@ export function writeSkills(cwd = process.cwd()) {
16
21
  return SKILL_ROOTS.map((root) => {
17
22
  const dest = path.join(cwd, root, 'skills', 'wavespeed', 'SKILL.md');
18
23
  fs.mkdirSync(path.dirname(dest), { recursive: true });
19
- fs.copyFileSync(skillSourcePath(), dest);
24
+ fs.writeFileSync(dest, skillBody());
20
25
  return dest;
21
26
  });
22
27
  }
@@ -34,7 +39,7 @@ export function registerSkill(program) {
34
39
  .option('--print', 'Print the skill body to stdout instead of writing a file')
35
40
  .action((opts) => {
36
41
  if (opts.print) {
37
- process.stdout.write(fs.readFileSync(skillSourcePath(), 'utf8'));
42
+ process.stdout.write(skillBody());
38
43
  return;
39
44
  }
40
45
  const dests = writeSkills();
@@ -0,0 +1,200 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import os from 'node:os';
6
+ import { spawn } from 'node:child_process';
7
+ import { IS_STANDALONE, PACKAGE_NAME, VERSION, assetName, isNewer } from '../lib/version.js';
8
+ import { emitJson, emitJsonError, info, setJsonMode } from '../lib/log.js';
9
+ const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
10
+ const RELEASES_URL = 'https://github.com/WaveSpeedAI/wavespeed-cli/releases';
11
+ // The standalone executable is distributed through GitHub Releases (the npm
12
+ // publish workflow creates the release right after publishing).
13
+ async function fetchLatestReleaseVersion() {
14
+ const res = await fetch('https://api.github.com/repos/WaveSpeedAI/wavespeed-cli/releases/latest', {
15
+ headers: { accept: 'application/vnd.github+json', 'user-agent': 'wavespeed-cli' },
16
+ signal: AbortSignal.timeout(15_000),
17
+ });
18
+ if (!res.ok)
19
+ throw new Error(`GitHub releases returned HTTP ${res.status}`);
20
+ const tag = (await res.json()).tag_name;
21
+ if (!tag)
22
+ throw new Error('GitHub release response had no tag');
23
+ return tag.replace(/^v/, '');
24
+ }
25
+ async function download(url) {
26
+ const res = await fetch(url, { signal: AbortSignal.timeout(300_000), redirect: 'follow' });
27
+ if (!res.ok)
28
+ throw new Error(`download of ${url} failed: HTTP ${res.status}`);
29
+ return Buffer.from(await res.arrayBuffer());
30
+ }
31
+ /** Replace the running executable with the release asset, after checking its SHA-256. */
32
+ async function replaceExecutable(version) {
33
+ const asset = assetName();
34
+ const base = `${RELEASES_URL}/download/v${version}`;
35
+ const [bin, sums] = await Promise.all([download(`${base}/${asset}`), download(`${base}/SHA256SUMS`)]);
36
+ const expected = sums
37
+ .toString('utf8')
38
+ .split('\n')
39
+ .map((l) => l.trim().split(/\s+/))
40
+ .find((p) => p[1]?.replace(/^\*/, '') === asset)?.[0];
41
+ const actual = crypto.createHash('sha256').update(bin).digest('hex');
42
+ if (!expected || expected.toLowerCase() !== actual)
43
+ throw new Error(`checksum mismatch for ${asset}; not installing`);
44
+ const exe = process.execPath;
45
+ const next = `${exe}.new`;
46
+ fs.writeFileSync(next, bin, { mode: 0o755 });
47
+ if (process.platform === 'win32') {
48
+ // A running .exe cannot be overwritten but can be renamed out of the way.
49
+ const old = `${exe}.old`;
50
+ fs.rmSync(old, { force: true });
51
+ fs.renameSync(exe, old);
52
+ }
53
+ fs.renameSync(next, exe);
54
+ }
55
+ export async function fetchLatestVersion() {
56
+ if (IS_STANDALONE)
57
+ return fetchLatestReleaseVersion();
58
+ const res = await fetch(REGISTRY_URL, {
59
+ headers: { accept: 'application/json' },
60
+ signal: AbortSignal.timeout(15_000),
61
+ });
62
+ if (!res.ok)
63
+ throw new Error(`npm registry returned HTTP ${res.status}`);
64
+ const body = (await res.json());
65
+ if (!body.version)
66
+ throw new Error('npm registry response had no version');
67
+ return body.version;
68
+ }
69
+ function runNpmInstall() {
70
+ return new Promise((resolve, reject) => {
71
+ // Child output goes to stderr so `--json` keeps stdout clean.
72
+ const child = spawn('npm', ['install', '-g', `${PACKAGE_NAME}@latest`], {
73
+ stdio: ['ignore', 2, 2],
74
+ shell: process.platform === 'win32', // npm is npm.cmd on Windows
75
+ });
76
+ child.on('error', reject);
77
+ child.on('close', (code) => resolve(code ?? 1));
78
+ });
79
+ }
80
+ /** Skill files written by `wavespeed skill install` are a snapshot; flag the ones in this directory. */
81
+ function installedSkillRoots(cwd = process.cwd()) {
82
+ return ['.claude', '.agents']
83
+ .map((root) => path.join(root, 'skills', 'wavespeed', 'SKILL.md'))
84
+ .filter((rel) => fs.existsSync(path.join(cwd, rel)));
85
+ }
86
+ /**
87
+ * Other `wavespeed` executables on PATH besides the one running (e.g. an npm
88
+ * install shadowing a standalone one). Upgrading one of them while the other
89
+ * wins on PATH makes the upgrade look like it did nothing.
90
+ */
91
+ export function otherInstalls(pathEnv = process.env.PATH ?? '', self = process.argv[1] ?? '') {
92
+ const names = process.platform === 'win32' ? ['wavespeed.exe', 'wavespeed.cmd', 'wavespeed.ps1'] : ['wavespeed'];
93
+ const real = (p) => { try {
94
+ return fs.realpathSync(p);
95
+ }
96
+ catch {
97
+ return null;
98
+ } };
99
+ const mine = new Set([real(process.execPath), real(self)].filter(Boolean));
100
+ const seen = new Set();
101
+ const found = [];
102
+ for (const dir of pathEnv.split(path.delimiter).filter(Boolean)) {
103
+ for (const name of names) {
104
+ const full = path.join(dir, name);
105
+ const r = real(full);
106
+ if (!r || seen.has(r))
107
+ continue;
108
+ seen.add(r);
109
+ // The npm shim and the package entry both count as "this" install.
110
+ if (!mine.has(r) && !(!IS_STANDALONE && r.includes(`${path.sep}@wavespeed${path.sep}cli${path.sep}`)))
111
+ found.push(full);
112
+ }
113
+ }
114
+ return found;
115
+ }
116
+ function warnOtherInstalls(json) {
117
+ const others = otherInstalls();
118
+ if (others.length && !json) {
119
+ info(chalk.yellow('Note: another wavespeed is also on your PATH, so the version you run may not be the one just upgraded:'));
120
+ for (const o of others)
121
+ info(chalk.gray(' ' + o.replace(os.homedir(), '~')));
122
+ }
123
+ return others;
124
+ }
125
+ export function registerUpgrade(program) {
126
+ program
127
+ .command('upgrade')
128
+ .alias('update')
129
+ .description('Upgrade the CLI to the latest published version')
130
+ .option('--check', 'Only report whether a newer version exists; do not install')
131
+ .option('--json', 'Emit JSON: {current, latest, update_available, upgraded}')
132
+ .action(async (opts) => {
133
+ setJsonMode(!!opts.json);
134
+ const fail = (message) => {
135
+ if (opts.json)
136
+ emitJsonError(message, { current: VERSION });
137
+ else
138
+ console.error(chalk.red('Error: ') + message);
139
+ process.exit(1);
140
+ };
141
+ let latest;
142
+ try {
143
+ latest = await fetchLatestVersion();
144
+ }
145
+ catch (err) {
146
+ return fail(`Could not check for updates: ${err.message ?? String(err)}`);
147
+ }
148
+ const updateAvailable = isNewer(latest, VERSION);
149
+ let others = [];
150
+ if (!updateAvailable || opts.check) {
151
+ if (opts.json) {
152
+ emitJson({ current: VERSION, latest, update_available: updateAvailable, upgraded: false, other_installs: otherInstalls() });
153
+ }
154
+ else if (!updateAvailable) {
155
+ info(chalk.green(`Already up to date (v${VERSION}).`));
156
+ warnOtherInstalls(opts.json);
157
+ }
158
+ else {
159
+ info(`Update available: v${VERSION} → ${chalk.bold('v' + latest)}. Run ${chalk.cyan('wavespeed upgrade')}.`);
160
+ }
161
+ return;
162
+ }
163
+ info(chalk.gray(`Upgrading v${VERSION} → v${latest}…`));
164
+ if (IS_STANDALONE) {
165
+ // Installed by a package manager: let it own the upgrade.
166
+ const managed = /[\\/](Cellar|homebrew|Homebrew|WinGet|scoop)[\\/]/.exec(process.execPath);
167
+ if (managed) {
168
+ return fail(`This copy is managed by ${managed[1]}; upgrade it with your package manager (e.g. brew upgrade wavespeed).`);
169
+ }
170
+ try {
171
+ await replaceExecutable(latest);
172
+ }
173
+ catch (err) {
174
+ return fail(`Could not replace ${process.execPath}: ${err.message ?? String(err)}`);
175
+ }
176
+ }
177
+ else {
178
+ let code;
179
+ try {
180
+ code = await runNpmInstall();
181
+ }
182
+ catch (err) {
183
+ return fail(`Could not run npm: ${err.message ?? String(err)}`);
184
+ }
185
+ if (code !== 0) {
186
+ return fail(`npm install -g exited with code ${code}. If it is a permissions error, fix npm's prefix or re-run with sudo: npm install -g ${PACKAGE_NAME}@latest`);
187
+ }
188
+ }
189
+ others = warnOtherInstalls(opts.json);
190
+ const skills = installedSkillRoots();
191
+ if (opts.json) {
192
+ emitJson({ current: VERSION, latest, update_available: true, upgraded: true, stale_skill_files: skills, other_installs: others });
193
+ return;
194
+ }
195
+ info(chalk.green(`Upgraded to v${latest}.`));
196
+ if (skills.length) {
197
+ info(chalk.gray(' Skill files here are from the old version: ') + chalk.cyan('wavespeed skill install') + chalk.gray(' to refresh.'));
198
+ }
199
+ });
200
+ }
@@ -2,24 +2,9 @@
2
2
  // for the CLI's direct REST calls, matching the convention wavespeed-desktop
3
3
  // uses. Calls that go through the `wavespeed` SDK get the same headers from
4
4
  // the SDK itself (via the clientName option in client.ts).
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
5
  import os from 'node:os';
8
- import { fileURLToPath } from 'node:url';
6
+ import { VERSION } from './version.js';
9
7
  export const CLI_CLIENT_NAME = 'wavespeed-cli';
10
- // Read version from package.json at runtime so a bump in package.json is
11
- // reflected automatically (same approach as cli.ts).
12
- function cliVersion() {
13
- try {
14
- const here = path.dirname(fileURLToPath(import.meta.url));
15
- // dist/lib/client-headers.js → ../../package.json
16
- const pkgPath = path.resolve(here, '..', '..', 'package.json');
17
- return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
18
- }
19
- catch {
20
- return '0.0.0';
21
- }
22
- }
23
8
  // Lowercase OS name: darwin / linux / windows (win32 is reported as windows).
24
9
  function operatingSystem() {
25
10
  const platform = os.platform();
@@ -34,7 +19,7 @@ export function resolveClientName() {
34
19
  export function clientAttributionHeaders() {
35
20
  return {
36
21
  'X-Client-Name': resolveClientName(),
37
- 'X-Client-Version': cliVersion(),
22
+ 'X-Client-Version': VERSION,
38
23
  'X-Client-OS': operatingSystem(),
39
24
  };
40
25
  }
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export const PACKAGE_NAME = '@wavespeed/cli';
5
+ /** True when running as the standalone executable rather than under Node + npm. */
6
+ export const IS_STANDALONE = typeof __WS_VERSION__ === 'string';
7
+ // Otherwise read package.json at runtime so a bump is reflected automatically
8
+ // (no second place to keep in sync).
9
+ export const VERSION = IS_STANDALONE ? __WS_VERSION__ : (() => {
10
+ const here = path.dirname(fileURLToPath(import.meta.url));
11
+ // dist/lib/version.js (or src/lib/version.ts) → ../../package.json
12
+ const pkgPath = path.resolve(here, '..', '..', 'package.json');
13
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
14
+ })();
15
+ /** Parse "1.2.3" (optionally "v"-prefixed / with a -prerelease or +build tail) into numbers. */
16
+ function parse(v) {
17
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
18
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
19
+ }
20
+ /** True when `latest` is strictly newer than `current`. Unparseable input → false. */
21
+ export function isNewer(latest, current) {
22
+ const a = parse(latest);
23
+ const b = parse(current);
24
+ if (!a || !b)
25
+ return false;
26
+ for (let i = 0; i < 3; i++) {
27
+ if (a[i] !== b[i])
28
+ return a[i] > b[i];
29
+ }
30
+ return false;
31
+ }
32
+ /** Release asset name for this platform, e.g. wavespeed-linux-x64, wavespeed-windows-x64.exe. */
33
+ export function assetName(platform = process.platform, arch = process.arch) {
34
+ const os = platform === 'win32' ? 'windows' : platform;
35
+ return `wavespeed-${os}-${arch}${platform === 'win32' ? '.exe' : ''}`;
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/cli",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
4
4
  "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -60,6 +60,8 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^20.14.0",
63
+ "esbuild": "^0.28.2",
64
+ "postject": "^1.0.0-alpha.6",
63
65
  "prettier": "^3.3.0",
64
66
  "tsx": "^4.19.0",
65
67
  "typescript": "^5.5.0",