@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/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
- // The API changes often (with breaking changes), so the live spec is the source
555
- // of truth: commands/params always match the API. The bundled snapshot is only
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
- const spec = validateSpec(JSON.parse(readFileSync(bundled, 'utf8')), bundled)
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
- return fallbackSpec(new Error(`invalid OpenAPI spec from ${url}: ${err.message}`), cacheSpec)
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 (typeof values['base-url'] === 'string' && values['base-url']) || env.WEVION_BASE_URL || config.baseUrl || DEFAULT_BASE_URL
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 commandArgv = stripLeadingGlobalOptions(argv)
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 { spec, source } = await resolveSpec(baseUrl)
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
- let asJson = false
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 agent # guide for LLMs/agents\n`)
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
- let values
948
- try {
949
- ;({ values } = parseArgs({
950
- args: rest,
951
- options: buildParseOptions(op),
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
- let req
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 bytes = Buffer.from(await res.arrayBuffer())
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} ${redactUrl(req.url)} -> ${res.status}\n`)
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
- const cliArgs = process.argv.slice(2)
1024
- main(cliArgs)
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
  })