@wevion/cli 1.0.3 → 1.0.2368
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 +27 -100
- package/openapi.json +33 -454
- package/package.json +1 -1
- package/selftest.mjs +12 -258
- package/src/index.mjs +27 -540
package/src/index.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util'
|
|
13
13
|
import {
|
|
14
14
|
chmodSync,
|
|
15
|
+
existsSync,
|
|
15
16
|
mkdirSync,
|
|
16
17
|
readFileSync,
|
|
17
18
|
realpathSync,
|
|
@@ -23,7 +24,6 @@ import { homedir } from 'node:os'
|
|
|
23
24
|
import { createInterface } from 'node:readline/promises'
|
|
24
25
|
import { fileURLToPath } from 'node:url'
|
|
25
26
|
import { dirname, join } from 'node:path'
|
|
26
|
-
import { spawn } from 'node:child_process'
|
|
27
27
|
|
|
28
28
|
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete']
|
|
29
29
|
const JSON_CONTENT_TYPE = 'application/json'
|
|
@@ -31,7 +31,6 @@ const SUPPORTED_AUTH_SCHEMES = new Set(['apiKeyAuth'])
|
|
|
31
31
|
// ponytail: hardcoded prod default; stage via --base-url / WEVION_BASE_URL / config.
|
|
32
32
|
const DEFAULT_BASE_URL = 'https://api.wevion.ai'
|
|
33
33
|
const DEFAULT_TIMEOUT_MS = 30_000
|
|
34
|
-
const DEFAULT_SPEC_TIMEOUT_MS = 3000
|
|
35
34
|
|
|
36
35
|
// ~/.config/wevion/config.json (honours XDG_CONFIG_HOME). Stores { apiKey, baseUrl }.
|
|
37
36
|
export function configPath(env = process.env, platform = process.platform) {
|
|
@@ -163,188 +162,6 @@ function logout() {
|
|
|
163
162
|
return 0
|
|
164
163
|
}
|
|
165
164
|
|
|
166
|
-
// ── Update notifier ──────────────────────────────────────────────
|
|
167
|
-
// Zero-dep: a detached background process refreshes a cached "latest" at most
|
|
168
|
-
// once/day; the notice (stderr only, never stdout) shows on the next run.
|
|
169
|
-
|
|
170
|
-
function pkgInfo() {
|
|
171
|
-
try {
|
|
172
|
-
const p = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
|
|
173
|
-
const { name, version } = JSON.parse(readFileSync(p, 'utf8'))
|
|
174
|
-
return { name, version }
|
|
175
|
-
} catch {
|
|
176
|
-
return {}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function updateCheckPath(env = process.env, platform = process.platform) {
|
|
181
|
-
return join(dirname(configPath(env, platform)), 'update-check.json')
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function parseSemver(v) {
|
|
185
|
-
const match = String(v).match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/)
|
|
186
|
-
if (!match) return null
|
|
187
|
-
return {
|
|
188
|
-
major: Number(match[1]),
|
|
189
|
-
minor: Number(match[2]),
|
|
190
|
-
patch: Number(match[3]),
|
|
191
|
-
prerelease: match[4] ? match[4].split('.') : [],
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function comparePrerelease(a, b) {
|
|
196
|
-
if (!a.length && !b.length) return 0
|
|
197
|
-
if (!a.length) return 1
|
|
198
|
-
if (!b.length) return -1
|
|
199
|
-
const len = Math.max(a.length, b.length)
|
|
200
|
-
for (let i = 0; i < len; i++) {
|
|
201
|
-
const ai = a[i]
|
|
202
|
-
const bi = b[i]
|
|
203
|
-
if (ai == null) return -1
|
|
204
|
-
if (bi == null) return 1
|
|
205
|
-
const an = /^\d+$/.test(ai) ? Number(ai) : null
|
|
206
|
-
const bn = /^\d+$/.test(bi) ? Number(bi) : null
|
|
207
|
-
if (an !== null && bn !== null && an !== bn) return an - bn
|
|
208
|
-
if (an !== null && bn === null) return -1
|
|
209
|
-
if (an === null && bn !== null) return 1
|
|
210
|
-
if (an === null && bn === null && ai !== bi) return ai < bi ? -1 : 1
|
|
211
|
-
}
|
|
212
|
-
return 0
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Minimal semver compare: numeric core, prerelease lower than release.
|
|
216
|
-
export function compareVersions(a, b) {
|
|
217
|
-
const pa = parseSemver(a)
|
|
218
|
-
const pb = parseSemver(b)
|
|
219
|
-
if (!pa || !pb) return 0
|
|
220
|
-
return pa.major - pb.major || pa.minor - pb.minor || pa.patch - pb.patch || comparePrerelease(pa.prerelease, pb.prerelease)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function isSafeVersion(v) {
|
|
224
|
-
return typeof v === 'string' && /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(v)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function updateCheckEnv(env = process.env) {
|
|
228
|
-
const allowlist = [
|
|
229
|
-
'HOME',
|
|
230
|
-
'USERPROFILE',
|
|
231
|
-
'APPDATA',
|
|
232
|
-
'XDG_CONFIG_HOME',
|
|
233
|
-
'HTTP_PROXY',
|
|
234
|
-
'HTTPS_PROXY',
|
|
235
|
-
'NO_PROXY',
|
|
236
|
-
'http_proxy',
|
|
237
|
-
'https_proxy',
|
|
238
|
-
'no_proxy',
|
|
239
|
-
'NODE_EXTRA_CA_CERTS',
|
|
240
|
-
'SSL_CERT_FILE',
|
|
241
|
-
'SSL_CERT_DIR',
|
|
242
|
-
'SystemRoot',
|
|
243
|
-
'WINDIR',
|
|
244
|
-
'TEMP',
|
|
245
|
-
'TMP',
|
|
246
|
-
]
|
|
247
|
-
return Object.fromEntries(allowlist.filter((key) => env[key]).map((key) => [key, env[key]]))
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function readUpdateCache() {
|
|
251
|
-
try {
|
|
252
|
-
const cache = JSON.parse(readFileSync(updateCheckPath(), 'utf8'))
|
|
253
|
-
return cache && typeof cache === 'object' ? cache : {}
|
|
254
|
-
} catch {
|
|
255
|
-
return {}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function writePrivateJsonFile(p, payload) {
|
|
260
|
-
const dir = dirname(p)
|
|
261
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
262
|
-
try {
|
|
263
|
-
chmodSync(dir, 0o700)
|
|
264
|
-
} catch {
|
|
265
|
-
/* best effort on platforms without POSIX modes */
|
|
266
|
-
}
|
|
267
|
-
const tmp = `${p}.${process.pid}.tmp`
|
|
268
|
-
try {
|
|
269
|
-
writeFileSync(tmp, JSON.stringify(payload) + '\n', { mode: 0o600 })
|
|
270
|
-
renameSync(tmp, p)
|
|
271
|
-
try {
|
|
272
|
-
chmodSync(p, 0o600)
|
|
273
|
-
} catch {
|
|
274
|
-
/* best effort on platforms without POSIX modes */
|
|
275
|
-
}
|
|
276
|
-
} catch (err) {
|
|
277
|
-
try {
|
|
278
|
-
rmSync(tmp, { force: true })
|
|
279
|
-
} catch {
|
|
280
|
-
/* ignore cleanup failure */
|
|
281
|
-
}
|
|
282
|
-
throw err
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function writeUpdateCache(cache) {
|
|
287
|
-
writePrivateJsonFile(updateCheckPath(), cache)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Hidden `__update-check` subcommand: fetch latest from npm, cache it. Best-effort.
|
|
291
|
-
async function refreshUpdateCache() {
|
|
292
|
-
const cache = readUpdateCache()
|
|
293
|
-
try {
|
|
294
|
-
const { name } = pkgInfo()
|
|
295
|
-
if (!name) return 0
|
|
296
|
-
const ctrl = new AbortController()
|
|
297
|
-
const t = setTimeout(() => ctrl.abort(), 5000)
|
|
298
|
-
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`, {
|
|
299
|
-
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
300
|
-
signal: ctrl.signal,
|
|
301
|
-
}).finally(() => clearTimeout(t))
|
|
302
|
-
if (!res.ok) return 0
|
|
303
|
-
const data = await res.json()
|
|
304
|
-
const latest = data?.['dist-tags']?.latest
|
|
305
|
-
if (isSafeVersion(latest)) cache.latest = latest
|
|
306
|
-
} catch {
|
|
307
|
-
/* offline / registry down — never surface */
|
|
308
|
-
} finally {
|
|
309
|
-
try {
|
|
310
|
-
writeUpdateCache({ ...cache, lastCheck: Date.now() })
|
|
311
|
-
} catch {
|
|
312
|
-
/* cache is best-effort */
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return 0
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Print an update notice from cache (if any) and kick off a background refresh
|
|
319
|
-
// when the cache is stale. Must never block or throw, and never write to stdout.
|
|
320
|
-
function notifyUpdate() {
|
|
321
|
-
try {
|
|
322
|
-
if (process.env.NO_UPDATE_NOTIFIER || process.env.CI || !process.stdout.isTTY || !process.stderr.isTTY) return
|
|
323
|
-
const { name, version } = pkgInfo()
|
|
324
|
-
if (!name || !version) return
|
|
325
|
-
const cache = readUpdateCache()
|
|
326
|
-
if (isSafeVersion(cache.latest) && compareVersions(cache.latest, version) > 0) {
|
|
327
|
-
process.stderr.write(
|
|
328
|
-
`\nUpdate available for ${name}: ${version} → ${cache.latest}\n` +
|
|
329
|
-
`Run: npm i -g ${name}@latest\n\n`,
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
const DAY_MS = 86_400_000
|
|
333
|
-
const lastCheck = Number(cache.lastCheck)
|
|
334
|
-
if (!Number.isFinite(lastCheck) || Date.now() - lastCheck > DAY_MS) {
|
|
335
|
-
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), '__update-check'], {
|
|
336
|
-
detached: true,
|
|
337
|
-
stdio: 'ignore',
|
|
338
|
-
env: updateCheckEnv(),
|
|
339
|
-
windowsHide: true,
|
|
340
|
-
})
|
|
341
|
-
child.unref()
|
|
342
|
-
}
|
|
343
|
-
} catch {
|
|
344
|
-
/* never break the CLI over an update check */
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
165
|
export function kebab(s) {
|
|
349
166
|
return s
|
|
350
167
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
@@ -551,139 +368,15 @@ function validateSpec(spec, source) {
|
|
|
551
368
|
return spec
|
|
552
369
|
}
|
|
553
370
|
|
|
554
|
-
|
|
555
|
-
//
|
|
556
|
-
// an offline fallback. A short on-disk cache avoids re-downloading the (large)
|
|
557
|
-
// spec on every command.
|
|
558
|
-
const CLI_VERSION_HEADER = 'x-wevion-cli-version'
|
|
559
|
-
const MIN_CLI_VERSION_HEADER = 'x-wevion-min-cli-version'
|
|
560
|
-
|
|
561
|
-
class UpgradeRequiredError extends Error {}
|
|
562
|
-
|
|
563
|
-
function cliVersion() {
|
|
564
|
-
return pkgInfo().version || '0.0.0'
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function specCachePath(env = process.env, platform = process.platform) {
|
|
568
|
-
return join(dirname(configPath(env, platform)), 'spec-cache.json')
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function specTtlMs(env = process.env) {
|
|
572
|
-
const parsed = Number(env.WEVION_SPEC_TTL_MS)
|
|
573
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 300_000 // 5 min
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function specTimeoutMs(env = process.env) {
|
|
577
|
-
const parsed = Number(env.WEVION_SPEC_TIMEOUT_MS)
|
|
578
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_SPEC_TIMEOUT_MS
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function specSignalFromEnv(env = process.env) {
|
|
582
|
-
return AbortSignal.timeout(specTimeoutMs(env))
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Extract the server's upgrade message (426 body), or a sensible default.
|
|
586
|
-
export function upgradeMessage(text, minVersion) {
|
|
587
|
-
try {
|
|
588
|
-
const j = JSON.parse(text)
|
|
589
|
-
if (j && typeof j.message === 'string') return j.message
|
|
590
|
-
} catch {
|
|
591
|
-
/* not JSON */
|
|
592
|
-
}
|
|
593
|
-
return (
|
|
594
|
-
`This @wevion/cli version (${cliVersion()}) is no longer supported.` +
|
|
595
|
-
`${minVersion ? ` Minimum: ${minVersion}.` : ''} Run: npm i -g @wevion/cli@latest`
|
|
596
|
-
)
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function readSpecCache(cachePath) {
|
|
600
|
-
try {
|
|
601
|
-
const cache = JSON.parse(readFileSync(cachePath, 'utf8'))
|
|
602
|
-
return cache && typeof cache === 'object' ? cache : null
|
|
603
|
-
} catch {
|
|
604
|
-
return null
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function validCachedSpec(cache, baseUrl, cachePath) {
|
|
609
|
-
if (!cache || cache.baseUrl !== baseUrl || !Number.isFinite(Number(cache.fetchedAt))) return null
|
|
610
|
-
try {
|
|
611
|
-
return validateSpec(cache.spec, cachePath)
|
|
612
|
-
} catch {
|
|
613
|
-
return null
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async function assertCliVersionSupported(baseUrl) {
|
|
618
|
-
const url = `${baseUrl.replace(/\/$/, '')}/docs/json`
|
|
619
|
-
try {
|
|
620
|
-
const res = await fetch(url, {
|
|
621
|
-
method: 'HEAD',
|
|
622
|
-
headers: { [CLI_VERSION_HEADER]: cliVersion() },
|
|
623
|
-
signal: specSignalFromEnv(),
|
|
624
|
-
})
|
|
625
|
-
if (res.status === 426) {
|
|
626
|
-
throw new UpgradeRequiredError(upgradeMessage('', res.headers.get(MIN_CLI_VERSION_HEADER)))
|
|
627
|
-
}
|
|
628
|
-
} catch (err) {
|
|
629
|
-
if (err instanceof UpgradeRequiredError) throw err
|
|
630
|
-
/* A failed probe is not fatal; the cached spec still preserves offline list/help. */
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function fallbackSpec(err, cacheSpec) {
|
|
635
|
-
if (cacheSpec) {
|
|
636
|
-
process.stderr.write(`warning: API spec unreachable, using cached copy (${err.message})\n`)
|
|
637
|
-
return { spec: cacheSpec, source: 'stale-cache' }
|
|
638
|
-
}
|
|
371
|
+
function loadSpec(baseUrl) {
|
|
372
|
+
// Prefer the bundled snapshot (offline help, reproducible per release).
|
|
639
373
|
const bundled = join(dirname(fileURLToPath(import.meta.url)), '..', 'openapi.json')
|
|
640
374
|
try {
|
|
641
|
-
|
|
642
|
-
process.stderr.write(`warning: API spec unreachable, using bundled snapshot (${err.message})\n`)
|
|
643
|
-
return { spec, source: 'bundled' }
|
|
644
|
-
} catch {
|
|
645
|
-
throw err
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
async function resolveSpec(baseUrl) {
|
|
650
|
-
const cachePath = specCachePath()
|
|
651
|
-
const cache = readSpecCache(cachePath)
|
|
652
|
-
const cacheSpec = validCachedSpec(cache, baseUrl, cachePath)
|
|
653
|
-
if (cacheSpec && Date.now() - Number(cache.fetchedAt) < specTtlMs()) {
|
|
654
|
-
await assertCliVersionSupported(baseUrl)
|
|
655
|
-
return { spec: cacheSpec, source: 'cache' }
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const url = `${baseUrl.replace(/\/$/, '')}/docs/json`
|
|
659
|
-
let res
|
|
660
|
-
let text
|
|
661
|
-
try {
|
|
662
|
-
res = await fetch(url, {
|
|
663
|
-
headers: { [CLI_VERSION_HEADER]: cliVersion() },
|
|
664
|
-
signal: specSignalFromEnv(),
|
|
665
|
-
})
|
|
666
|
-
text = await res.text()
|
|
667
|
-
} catch (err) {
|
|
668
|
-
return fallbackSpec(err, cacheSpec)
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (res.status === 426) throw new UpgradeRequiredError(upgradeMessage(text, res.headers.get(MIN_CLI_VERSION_HEADER)))
|
|
672
|
-
if (!res.ok) return fallbackSpec(new Error(`cannot fetch spec from ${url}: ${res.status}`), cacheSpec)
|
|
673
|
-
|
|
674
|
-
let spec
|
|
675
|
-
try {
|
|
676
|
-
spec = validateSpec(JSON.parse(text), url)
|
|
375
|
+
return { spec: validateSpec(JSON.parse(readFileSync(bundled, 'utf8')), bundled), source: 'bundled' }
|
|
677
376
|
} catch (err) {
|
|
678
|
-
|
|
377
|
+
if (existsSync(bundled)) throw err
|
|
378
|
+
return { source: 'remote', url: `${baseUrl.replace(/\/$/, '')}/docs/json` }
|
|
679
379
|
}
|
|
680
|
-
|
|
681
|
-
try {
|
|
682
|
-
writePrivateJsonFile(cachePath, { fetchedAt: Date.now(), baseUrl, spec })
|
|
683
|
-
} catch {
|
|
684
|
-
/* cache is best-effort */
|
|
685
|
-
}
|
|
686
|
-
return { spec, source: 'live' }
|
|
687
380
|
}
|
|
688
381
|
|
|
689
382
|
function timeoutMs(env = process.env) {
|
|
@@ -695,37 +388,6 @@ function signalFromEnv(env = process.env) {
|
|
|
695
388
|
return AbortSignal.timeout(timeoutMs(env))
|
|
696
389
|
}
|
|
697
390
|
|
|
698
|
-
function isLoopbackHostname(hostname) {
|
|
699
|
-
const h = hostname.toLowerCase()
|
|
700
|
-
return h === 'localhost' || h === '::1' || h === '[::1]' || /^127\./.test(h)
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function assertSafeAuthenticatedUrl(rawUrl) {
|
|
704
|
-
const url = new URL(rawUrl)
|
|
705
|
-
if (url.username || url.password) {
|
|
706
|
-
throw new Error('base URL must not include credentials')
|
|
707
|
-
}
|
|
708
|
-
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopbackHostname(url.hostname))) {
|
|
709
|
-
throw new Error('refusing to send API key to a non-HTTPS base URL')
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function redactUrl(rawUrl) {
|
|
714
|
-
try {
|
|
715
|
-
const url = new URL(rawUrl)
|
|
716
|
-
if (url.username) url.username = '[REDACTED]'
|
|
717
|
-
if (url.password) url.password = '[REDACTED]'
|
|
718
|
-
for (const key of [...url.searchParams.keys()]) {
|
|
719
|
-
if (/token|secret|password|api[-_]?key|authorization|code/i.test(key)) {
|
|
720
|
-
url.searchParams.set(key, '[REDACTED]')
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
return url.toString()
|
|
724
|
-
} catch {
|
|
725
|
-
return rawUrl
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
391
|
function resolveBaseUrl(argv, config, env = process.env) {
|
|
730
392
|
const globalOptions = { 'base-url': { type: 'string' } }
|
|
731
393
|
try {
|
|
@@ -735,127 +397,12 @@ function resolveBaseUrl(argv, config, env = process.env) {
|
|
|
735
397
|
allowPositionals: true,
|
|
736
398
|
strict: false,
|
|
737
399
|
})
|
|
738
|
-
return
|
|
400
|
+
return values['base-url'] || env.WEVION_BASE_URL || config.baseUrl || DEFAULT_BASE_URL
|
|
739
401
|
} catch {
|
|
740
402
|
return env.WEVION_BASE_URL || config.baseUrl || DEFAULT_BASE_URL
|
|
741
403
|
}
|
|
742
404
|
}
|
|
743
405
|
|
|
744
|
-
function stripLeadingGlobalOptions(argv) {
|
|
745
|
-
for (let i = 0; i < argv.length; i++) {
|
|
746
|
-
const arg = argv[i]
|
|
747
|
-
if (arg === '--base-url') {
|
|
748
|
-
i += 1
|
|
749
|
-
continue
|
|
750
|
-
}
|
|
751
|
-
if (arg.startsWith('--base-url=')) continue
|
|
752
|
-
return argv.slice(i)
|
|
753
|
-
}
|
|
754
|
-
return []
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function parseJsonOutputFlag(args) {
|
|
758
|
-
const { values } = parseArgs({
|
|
759
|
-
args,
|
|
760
|
-
options: { json: { type: 'boolean' }, 'base-url': { type: 'string' } },
|
|
761
|
-
allowPositionals: false,
|
|
762
|
-
})
|
|
763
|
-
return Boolean(values.json)
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function schemaMeta(schema = {}) {
|
|
767
|
-
return {
|
|
768
|
-
type: schema.type || 'string',
|
|
769
|
-
...(schema.format ? { format: schema.format } : {}),
|
|
770
|
-
...(schema.default !== undefined ? { default: schema.default } : {}),
|
|
771
|
-
...(schema.enum ? { enum: schema.enum } : {}),
|
|
772
|
-
...(schema.description ? { description: schema.description } : {}),
|
|
773
|
-
...(schema.items ? { items: schemaMeta(schema.items) } : {}),
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Machine-readable view of a command, for agents (list --json / help --json).
|
|
778
|
-
export function opToJson(op) {
|
|
779
|
-
return {
|
|
780
|
-
command: op.id,
|
|
781
|
-
method: op.method,
|
|
782
|
-
path: op.path,
|
|
783
|
-
summary: op.summary,
|
|
784
|
-
tags: op.tags,
|
|
785
|
-
params: op.params
|
|
786
|
-
.filter((p) => p.in === 'path' || p.in === 'query' || p.in === 'header')
|
|
787
|
-
.map((p) => ({
|
|
788
|
-
flag: `--${p.name}`,
|
|
789
|
-
in: p.in,
|
|
790
|
-
required: Boolean(p.required),
|
|
791
|
-
...(p.description && !p.schema?.description ? { description: p.description } : {}),
|
|
792
|
-
...schemaMeta(p.schema),
|
|
793
|
-
array: p.schema?.type === 'array',
|
|
794
|
-
})),
|
|
795
|
-
body: op.bodyFields.length
|
|
796
|
-
? {
|
|
797
|
-
flags: op.bodyFields.map((f) => ({
|
|
798
|
-
flag: `--${f.flag}`,
|
|
799
|
-
field: f.name,
|
|
800
|
-
required: op.bodyRequired.includes(f.name),
|
|
801
|
-
...schemaMeta(f.schema),
|
|
802
|
-
})),
|
|
803
|
-
raw: "--json '<json>'",
|
|
804
|
-
}
|
|
805
|
-
: null,
|
|
806
|
-
...(op.unsupported ? { unsupported: op.unsupported } : {}),
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// One-shot primer so an LLM/agent with no Wevion knowledge can use the CLI.
|
|
811
|
-
function printAgent() {
|
|
812
|
-
const { name = '@wevion/cli', version = '?' } = pkgInfo()
|
|
813
|
-
process.stdout.write(`# ${name} — agent guide (v${version})
|
|
814
|
-
|
|
815
|
-
Command-line access to the Wevion advertising API. Everything you need is here;
|
|
816
|
-
no prior Wevion knowledge required.
|
|
817
|
-
|
|
818
|
-
## Model
|
|
819
|
-
- Every command maps to ONE Wevion REST endpoint. The command id is the
|
|
820
|
-
kebab-case operationId, e.g. \`get-api-v1-ad-accounts\` -> GET /api/v1/ad-accounts.
|
|
821
|
-
- Commands are derived LIVE from the API's OpenAPI spec, so they always match
|
|
822
|
-
the current API.
|
|
823
|
-
|
|
824
|
-
## Auth
|
|
825
|
-
- Set the API key: \`WEVION_API_KEY=<key>\` (env) or run \`wevion login\`.
|
|
826
|
-
- Create a key in the Wevion app under Settings → API keys (permission-scoped).
|
|
827
|
-
|
|
828
|
-
## Discover (machine-readable)
|
|
829
|
-
- \`wevion list --json\` → all commands: [{command, method, path, summary, tags, params, body}]
|
|
830
|
-
- \`wevion help <command> --json\` → one command's exact params/body schema
|
|
831
|
-
|
|
832
|
-
## Invoke
|
|
833
|
-
- Path, query and supported header params are flags using the exact OpenAPI
|
|
834
|
-
name. Names can be camelCase or snake_case (\`--accountId\`, \`--ad_id\`,
|
|
835
|
-
\`--folderId\`, \`--limit\`). Get exact names from \`help <command> --json\`;
|
|
836
|
-
do not guess.
|
|
837
|
-
- Array params: repeat the flag or pass a comma-separated list.
|
|
838
|
-
- Request body: pass fields as flags, or the whole JSON via \`--json '<json>'\`.
|
|
839
|
-
|
|
840
|
-
## Output & exit codes
|
|
841
|
-
- Success: JSON on stdout, exit 0.
|
|
842
|
-
- API error: request line + JSON body on stderr, exit 1.
|
|
843
|
-
- Usage error (unknown command / missing required flag / no API key): exit 2.
|
|
844
|
-
- Upgrade required (CLI too old for the API): message on stderr, exit 3
|
|
845
|
-
→ run \`npm i -g ${name}@latest\`.
|
|
846
|
-
|
|
847
|
-
## Config
|
|
848
|
-
- Base URL: \`--base-url\` | \`WEVION_BASE_URL\` | stored login config |
|
|
849
|
-
default ${DEFAULT_BASE_URL} (staging: https://api-stage.wevion.ai).
|
|
850
|
-
|
|
851
|
-
## Typical flow
|
|
852
|
-
1. wevion agent # this guide
|
|
853
|
-
2. wevion list --json # find the command
|
|
854
|
-
3. wevion help <command> --json # get its params
|
|
855
|
-
4. wevion <command> --flag value ... # run it; parse JSON from stdout
|
|
856
|
-
`)
|
|
857
|
-
}
|
|
858
|
-
|
|
859
406
|
function printList(ops) {
|
|
860
407
|
const byTag = new Map()
|
|
861
408
|
for (const op of ops.values()) {
|
|
@@ -888,55 +435,33 @@ function printHelp(op) {
|
|
|
888
435
|
}
|
|
889
436
|
|
|
890
437
|
async function main(argv) {
|
|
891
|
-
const
|
|
892
|
-
const [cmd, ...rest] = commandArgv
|
|
438
|
+
const [cmd, ...rest] = argv
|
|
893
439
|
|
|
894
|
-
if (cmd === '__update-check') return refreshUpdateCache()
|
|
895
440
|
if (cmd === 'login') return login(rest)
|
|
896
441
|
if (cmd === 'logout') return logout()
|
|
897
|
-
if (cmd === 'agent') {
|
|
898
|
-
printAgent()
|
|
899
|
-
return 0
|
|
900
|
-
}
|
|
901
442
|
|
|
902
443
|
const config = readConfig()
|
|
903
444
|
const baseUrl = resolveBaseUrl(argv, config)
|
|
904
445
|
|
|
905
|
-
const
|
|
446
|
+
const loaded = loadSpec(baseUrl)
|
|
447
|
+
const spec =
|
|
448
|
+
loaded.spec ||
|
|
449
|
+
(await fetch(loaded.url, { signal: signalFromEnv() }).then((r) => {
|
|
450
|
+
if (!r.ok) throw new Error(`cannot fetch spec from ${loaded.url}: ${r.status}`)
|
|
451
|
+
return r.json().then((json) => validateSpec(json, loaded.url))
|
|
452
|
+
}))
|
|
906
453
|
const ops = listOperations(spec)
|
|
907
454
|
|
|
908
455
|
if (!cmd || cmd === 'list' || cmd === '--help' || cmd === '-h') {
|
|
909
|
-
|
|
910
|
-
try {
|
|
911
|
-
asJson = parseJsonOutputFlag(rest)
|
|
912
|
-
} catch (err) {
|
|
913
|
-
return fail(err.message)
|
|
914
|
-
}
|
|
915
|
-
if (asJson) {
|
|
916
|
-
process.stdout.write(`${JSON.stringify([...ops.values()].map(opToJson), null, 2)}\n`)
|
|
917
|
-
return 0
|
|
918
|
-
}
|
|
919
|
-
process.stdout.write(`Wevion CLI — ${ops.size} commands (${source} spec)\n`)
|
|
456
|
+
process.stdout.write(`Wevion CLI — ${ops.size} commands (${loaded.source} spec)\n`)
|
|
920
457
|
printList(ops)
|
|
921
458
|
process.stdout.write(`\nUsage: wevion <command> [--param value ...]\n`)
|
|
922
|
-
process.stdout.write(` wevion
|
|
923
|
-
process.stdout.write(` wevion list --json | help <command> [--json]\n`)
|
|
924
|
-
process.stdout.write(` wevion login | logout\n`)
|
|
459
|
+
process.stdout.write(` wevion login | logout | help <command>\n`)
|
|
925
460
|
return 0
|
|
926
461
|
}
|
|
927
462
|
if (cmd === 'help') {
|
|
928
463
|
const op = ops.get(rest[0])
|
|
929
464
|
if (!op) return fail(`unknown command: ${rest[0] || '(none)'}`)
|
|
930
|
-
let asJson = false
|
|
931
|
-
try {
|
|
932
|
-
asJson = parseJsonOutputFlag(rest.slice(1))
|
|
933
|
-
} catch (err) {
|
|
934
|
-
return fail(err.message)
|
|
935
|
-
}
|
|
936
|
-
if (asJson) {
|
|
937
|
-
process.stdout.write(`${JSON.stringify(opToJson(op), null, 2)}\n`)
|
|
938
|
-
return 0
|
|
939
|
-
}
|
|
940
465
|
printHelp(op)
|
|
941
466
|
return 0
|
|
942
467
|
}
|
|
@@ -944,62 +469,32 @@ async function main(argv) {
|
|
|
944
469
|
const op = ops.get(cmd)
|
|
945
470
|
if (!op) return fail(`unknown command: ${cmd} (try "wevion list")`)
|
|
946
471
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
allowPositionals: false,
|
|
953
|
-
}))
|
|
954
|
-
} catch (err) {
|
|
955
|
-
return fail(err.message)
|
|
956
|
-
}
|
|
472
|
+
const { values } = parseArgs({
|
|
473
|
+
args: rest,
|
|
474
|
+
options: buildParseOptions(op),
|
|
475
|
+
allowPositionals: false,
|
|
476
|
+
})
|
|
957
477
|
|
|
958
478
|
const apiKey = resolveApiKey(process.env, config)
|
|
959
479
|
if (!apiKey) return fail('no API key — run "wevion login" or set WEVION_API_KEY')
|
|
960
480
|
|
|
961
|
-
|
|
962
|
-
try {
|
|
963
|
-
req = buildRequest(op, values, values['base-url'] || baseUrl)
|
|
964
|
-
assertSafeAuthenticatedUrl(req.url)
|
|
965
|
-
} catch (err) {
|
|
966
|
-
return fail(err.message)
|
|
967
|
-
}
|
|
481
|
+
const req = buildRequest(op, values, values['base-url'] || baseUrl)
|
|
968
482
|
const res = await fetch(req.url, {
|
|
969
483
|
method: req.method,
|
|
970
484
|
headers: {
|
|
971
485
|
...req.headers,
|
|
972
486
|
'x-api-key': apiKey,
|
|
973
|
-
[CLI_VERSION_HEADER]: cliVersion(),
|
|
974
487
|
...(req.body ? { 'content-type': 'application/json' } : {}),
|
|
975
488
|
},
|
|
976
489
|
body: req.body,
|
|
977
490
|
signal: signalFromEnv(),
|
|
978
491
|
})
|
|
979
|
-
const
|
|
980
|
-
const text = bytes.toString('utf8')
|
|
981
|
-
if (res.status === 426) throw new UpgradeRequiredError(upgradeMessage(text, res.headers.get(MIN_CLI_VERSION_HEADER)))
|
|
492
|
+
const text = await res.text()
|
|
982
493
|
if (!res.ok) {
|
|
983
|
-
process.stderr.write(`${req.method} ${
|
|
494
|
+
process.stderr.write(`${req.method} ${req.url} -> ${res.status}\n`)
|
|
984
495
|
if (text) process.stderr.write(text.endsWith('\n') ? text : text + '\n')
|
|
985
496
|
return 1
|
|
986
497
|
}
|
|
987
|
-
const contentType = res.headers.get('content-type') || ''
|
|
988
|
-
if (res.status === 204 || bytes.length === 0) {
|
|
989
|
-
process.stdout.write(`${JSON.stringify({ ok: true, status: res.status })}\n`)
|
|
990
|
-
return 0
|
|
991
|
-
}
|
|
992
|
-
if (!contentType.includes('application/json')) {
|
|
993
|
-
process.stdout.write(
|
|
994
|
-
`${JSON.stringify({ ok: true, status: res.status, contentType, bodyBase64: bytes.toString('base64') })}\n`,
|
|
995
|
-
)
|
|
996
|
-
return 0
|
|
997
|
-
}
|
|
998
|
-
try {
|
|
999
|
-
JSON.parse(text)
|
|
1000
|
-
} catch {
|
|
1001
|
-
throw new Error(`API returned invalid JSON for ${req.method} ${req.url}`)
|
|
1002
|
-
}
|
|
1003
498
|
process.stdout.write(text.endsWith('\n') ? text : text + '\n')
|
|
1004
499
|
return 0
|
|
1005
500
|
}
|
|
@@ -1020,17 +515,9 @@ function isExecutedAsBin() {
|
|
|
1020
515
|
|
|
1021
516
|
// Run only when executed as a bin, not when imported by selftest.
|
|
1022
517
|
if (isExecutedAsBin()) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
.then((code) => {
|
|
1026
|
-
if (cliArgs[0] !== '__update-check') notifyUpdate()
|
|
1027
|
-
process.exit(code)
|
|
1028
|
-
})
|
|
518
|
+
main(process.argv.slice(2))
|
|
519
|
+
.then((code) => process.exit(code))
|
|
1029
520
|
.catch((err) => {
|
|
1030
|
-
if (err instanceof UpgradeRequiredError) {
|
|
1031
|
-
process.stderr.write(`\n${err.message}\n\n`)
|
|
1032
|
-
process.exit(3)
|
|
1033
|
-
}
|
|
1034
521
|
process.stderr.write(`error: ${err.message}\n`)
|
|
1035
522
|
process.exit(1)
|
|
1036
523
|
})
|