@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/selftest.mjs CHANGED
@@ -1,25 +1,11 @@
1
1
  // Minimal runnable check for the spec->request mapping. Run: node selftest.mjs
2
2
  import assert from 'node:assert/strict'
3
- import { mkdtempSync, statSync } from 'node:fs'
3
+ import { mkdtempSync, symlinkSync } from 'node:fs'
4
4
  import { readFile } from 'node:fs/promises'
5
- import { createServer } from 'node:http'
6
5
  import { tmpdir } from 'node:os'
7
6
  import { join } from 'node:path'
8
- import { spawn } from 'node:child_process'
9
- import { fileURLToPath } from 'node:url'
10
- import {
11
- kebab,
12
- listOperations,
13
- buildRequest,
14
- configPath,
15
- resolveApiKey,
16
- compareVersions,
17
- updateCheckPath,
18
- upgradeMessage,
19
- opToJson,
20
- isSafeVersion,
21
- updateCheckEnv,
22
- } from './src/index.mjs'
7
+ import { execFileSync } from 'node:child_process'
8
+ import { kebab, listOperations, buildRequest, configPath, resolveApiKey } from './src/index.mjs'
23
9
 
24
10
  const spec = {
25
11
  openapi: '3.0.3',
@@ -169,10 +155,9 @@ assert.deepEqual(JSON.parse(message.body), { id: 'msg_1', content: 'ciao' })
169
155
  assert.throws(() => buildRequest(ops.get('upload-file'), {}, 'x'), /request body media type is not supported/)
170
156
 
171
157
  // config path honours XDG_CONFIG_HOME, falls back to ~/.config
172
- assert.equal(configPath({ XDG_CONFIG_HOME: '/x' }, 'linux'), '/x/wevion/config.json')
173
- assert.equal(configPath({ HOME: '/home/u' }, 'linux'), '/home/u/.config/wevion/config.json')
174
- const appData = 'C:\\Users\\u\\AppData\\Roaming'
175
- assert.equal(configPath({ APPDATA: appData }, 'win32'), join(appData, 'Wevion', 'config.json'))
158
+ assert.equal(configPath({ XDG_CONFIG_HOME: '/x' }), '/x/wevion/config.json')
159
+ assert.equal(configPath({ HOME: '/home/u' }), '/home/u/.config/wevion/config.json')
160
+ assert.equal(configPath({ APPDATA: 'C:\\Users\\u\\AppData\\Roaming' }, 'win32'), 'C:\\Users\\u\\AppData\\Roaming/Wevion/config.json')
176
161
 
177
162
  // api key precedence: env > config
178
163
  assert.equal(resolveApiKey({ WEVION_API_KEY: 'env' }, { apiKey: 'cfg' }), 'env')
@@ -185,245 +170,14 @@ assert.match(bundled.openapi, /^3\./)
185
170
  assert.ok(Object.keys(bundled.paths || {}).length > 0)
186
171
  const knownCommand = listOperations(bundled).keys().next().value
187
172
  assert.ok(knownCommand)
188
- const knownSimpleCommand = 'get-api-v1-ad-accounts'
189
- assert.ok(listOperations(bundled).has(knownSimpleCommand))
190
173
 
191
174
  const tmp = mkdtempSync(join(tmpdir(), 'wevion-cli-'))
192
- const cli = fileURLToPath(new URL('./src/index.mjs', import.meta.url))
193
- const childBaseEnv = {
194
- PATH: process.env.PATH || '',
195
- Path: process.env.Path || '',
196
- SystemRoot: process.env.SystemRoot || '',
197
- WINDIR: process.env.WINDIR || '',
198
- TEMP: process.env.TEMP || tmp,
199
- TMP: process.env.TMP || tmp,
200
- CI: '1',
201
- NO_UPDATE_NOTIFIER: '1',
202
- WEVION_API_KEY: '',
203
- WEVION_TIMEOUT_MS: '1000',
204
- WEVION_SPEC_TIMEOUT_MS: '500',
205
- XDG_CONFIG_HOME: tmp,
206
- HOME: tmp,
207
- APPDATA: tmp,
208
- }
209
-
210
- function runCli(args, env = {}) {
211
- return new Promise((resolve) => {
212
- const child = spawn(process.execPath, [cli, ...args], {
213
- env: { ...childBaseEnv, ...env },
214
- stdio: ['ignore', 'pipe', 'pipe'],
215
- })
216
- let stdout = ''
217
- let stderr = ''
218
- child.stdout.setEncoding('utf8')
219
- child.stderr.setEncoding('utf8')
220
- child.stdout.on('data', (chunk) => {
221
- stdout += chunk
222
- })
223
- child.stderr.on('data', (chunk) => {
224
- stderr += chunk
225
- })
226
- child.on('error', (err) => {
227
- resolve({ status: 1, stdout, stderr: `${stderr}${err.message}` })
228
- })
229
- child.on('close', (status) => {
230
- resolve({ status, stdout, stderr })
231
- })
232
- })
233
- }
234
-
235
- const offlineHelp = await runCli(['help', knownCommand, '--base-url=http://127.0.0.1:9'])
236
- assert.equal(offlineHelp.status, 0)
237
- assert.match(offlineHelp.stdout, /Flags:/)
238
- const leadingBaseUrlList = await runCli(['--base-url=http://127.0.0.1:9', 'list'])
239
- assert.equal(leadingBaseUrlList.status, 0)
240
- assert.match(leadingBaseUrlList.stdout, /Wevion CLI/)
241
- const leadingBaseUrlHelpJson = await runCli(['--base-url', 'http://127.0.0.1:9', 'help', knownCommand, '--json'])
242
- assert.equal(leadingBaseUrlHelpJson.status, 0)
243
- assert.equal(JSON.parse(leadingBaseUrlHelpJson.stdout).command, knownCommand)
244
-
245
- const liveSpec = {
246
- openapi: '3.0.3',
247
- security: [{ apiKeyAuth: [] }],
248
- paths: {
249
- '/api/v1/sentinel': {
250
- get: {
251
- operationId: 'sentinelLiveSpec',
252
- tags: ['sentinel'],
253
- summary: 'sentinel-live-spec',
254
- responses: { 200: { description: 'OK' } },
255
- },
256
- },
257
- '/api/v1/no-content': {
258
- get: {
259
- operationId: 'noContentSuccess',
260
- tags: ['sentinel'],
261
- summary: 'no-content-success',
262
- responses: { 204: { description: 'No content' } },
263
- },
264
- },
265
- '/api/v1/error': {
266
- get: {
267
- operationId: 'errorWithSecret',
268
- tags: ['sentinel'],
269
- summary: 'error-with-secret',
270
- parameters: [{ name: 'api_key', in: 'query', schema: { type: 'string' } }],
271
- responses: { 500: { description: 'Error' } },
272
- },
273
- },
274
- },
275
- }
276
-
277
- let mode = 'live'
278
- const hits = []
279
- const server = createServer((req, res) => {
280
- hits.push({ method: req.method, url: req.url, headers: req.headers })
281
- if (req.url === '/docs/json') {
282
- if (mode === 'upgrade') {
283
- res.writeHead(426, {
284
- 'content-type': 'application/json',
285
- 'x-wevion-min-cli-version': '9.0.0',
286
- })
287
- res.end(JSON.stringify({ message: 'please upgrade test' }))
288
- return
289
- }
290
- if (mode === 'invalid') {
291
- res.writeHead(200, { 'content-type': 'text/html' })
292
- res.end('<html>not openapi</html>')
293
- return
294
- }
295
- res.writeHead(200, { 'content-type': 'application/json' })
296
- if (req.method === 'HEAD') {
297
- res.end()
298
- return
299
- }
300
- res.end(JSON.stringify(liveSpec))
301
- return
302
- }
303
- if (req.url === '/api/v1/no-content') {
304
- res.writeHead(204)
305
- res.end()
306
- return
307
- }
308
- if (req.url?.startsWith('/api/v1/error')) {
309
- res.writeHead(500, { 'content-type': 'application/json' })
310
- res.end(JSON.stringify({ error: 'boom' }))
311
- return
312
- }
313
- res.writeHead(404, { 'content-type': 'application/json' })
314
- res.end(JSON.stringify({ error: 'not found' }))
315
- })
316
-
317
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
318
- try {
319
- const baseUrl = `http://127.0.0.1:${server.address().port}`
320
- const liveList = await runCli(['list', '--json'], { WEVION_BASE_URL: baseUrl })
321
- assert.equal(liveList.status, 0)
322
- assert.equal(liveList.stderr, '')
323
- assert.ok(JSON.parse(liveList.stdout).find((op) => op.command === 'sentinel-live-spec'))
324
- assert.equal(hits[0].url, '/docs/json')
325
- assert.equal(hits[0].method, 'GET')
326
- assert.equal(hits[0].headers['x-wevion-cli-version'], '1.0.0')
327
- if (process.platform !== 'win32') {
328
- assert.equal(statSync(join(tmp, 'wevion', 'spec-cache.json')).mode & 0o777, 0o600)
329
- }
330
-
331
- const cacheHitList = await runCli(['list', '--json'], { WEVION_BASE_URL: baseUrl })
332
- assert.equal(cacheHitList.status, 0)
333
- assert.ok(JSON.parse(cacheHitList.stdout).find((op) => op.command === 'sentinel-live-spec'))
334
- assert.ok(hits.find((hit) => hit.method === 'HEAD' && hit.url === '/docs/json'))
335
-
336
- mode = 'invalid'
337
- const invalidSpecFallback = await runCli(['list', '--json'], {
338
- WEVION_BASE_URL: baseUrl,
339
- WEVION_SPEC_TTL_MS: '0',
340
- })
341
- assert.equal(invalidSpecFallback.status, 0)
342
- assert.ok(JSON.parse(invalidSpecFallback.stdout).find((op) => op.command === 'sentinel-live-spec'))
343
- assert.match(invalidSpecFallback.stderr, /invalid OpenAPI spec/)
344
- mode = 'live'
345
-
346
- const helpJson = await runCli(['help', 'sentinel-live-spec', '--json'], { WEVION_BASE_URL: baseUrl })
347
- assert.equal(helpJson.status, 0)
348
- assert.equal(JSON.parse(helpJson.stdout).command, 'sentinel-live-spec')
349
-
350
- const agentGuide = await runCli(['agent'], { WEVION_BASE_URL: baseUrl })
351
- assert.equal(agentGuide.status, 0)
352
- assert.match(agentGuide.stdout, /agent guide/)
353
- assert.equal(hits.filter((hit) => hit.url === '/docs/json').length, 4)
354
-
355
- const badListFlag = await runCli(['list', '--json', '--bad'], { WEVION_BASE_URL: baseUrl })
356
- assert.equal(badListFlag.status, 2)
357
- const badHelpFlag = await runCli(['help', 'sentinel-live-spec', '--json', '--bad'], { WEVION_BASE_URL: baseUrl })
358
- assert.equal(badHelpFlag.status, 2)
359
-
360
- const usageError = await runCli(['no-content-success', '--definitely-unknown'], {
361
- WEVION_BASE_URL: baseUrl,
362
- WEVION_API_KEY: 'test-key',
363
- })
364
- assert.equal(usageError.status, 2)
365
-
366
- const emptySuccess = await runCli(['no-content-success'], {
367
- WEVION_BASE_URL: baseUrl,
368
- WEVION_API_KEY: 'test-key',
369
- })
370
- assert.equal(emptySuccess.status, 0)
371
- assert.deepEqual(JSON.parse(emptySuccess.stdout), { ok: true, status: 204 })
372
-
373
- const unsafeBaseUrl = await runCli([knownSimpleCommand, '--base-url', `http://user:pass@127.0.0.1:${server.address().port}`], {
374
- WEVION_API_KEY: 'test-key',
375
- })
376
- assert.equal(unsafeBaseUrl.status, 2)
377
- assert.match(unsafeBaseUrl.stderr, /must not include credentials/)
378
-
379
- const redactedError = await runCli(['error-with-secret', '--api_key', 'secret-value'], {
380
- WEVION_BASE_URL: baseUrl,
381
- WEVION_API_KEY: 'test-key',
382
- })
383
- assert.equal(redactedError.status, 1)
384
- assert.match(redactedError.stderr, /api_key=%5BREDACTED%5D/)
385
- assert.doesNotMatch(redactedError.stderr, /secret-value/)
386
-
387
- mode = 'upgrade'
388
- const upgrade = await runCli(['--base-url', baseUrl, 'list'], { WEVION_SPEC_TTL_MS: '0' })
389
- assert.equal(upgrade.status, 3)
390
- assert.equal(upgrade.stdout, '')
391
- assert.match(upgrade.stderr, /please upgrade test/)
392
- } finally {
393
- await new Promise((resolve) => server.close(resolve))
394
- }
395
-
396
- // update notifier: version compare + cache path location
397
- assert.ok(compareVersions('1.0.10', '1.0.2') > 0) // numeric, not lexical
398
- assert.ok(compareVersions('1.0.2', '1.0.10') < 0)
399
- assert.equal(compareVersions('1.0.3', '1.0.3'), 0)
400
- assert.ok(compareVersions('1.1.0', '1.0.999') > 0)
401
- assert.ok(compareVersions('1.0.0-beta.1', '1.0.0') < 0)
402
- assert.ok(compareVersions('1.0.0', '1.0.0-beta.1') > 0)
403
- assert.ok(isSafeVersion('1.0.0-beta.1'))
404
- assert.equal(isSafeVersion('1.0.0\nbad'), false)
405
- assert.equal(updateCheckPath({ XDG_CONFIG_HOME: '/x' }), '/x/wevion/update-check.json')
406
- assert.deepEqual(updateCheckEnv({ HOME: '/h', WEVION_API_KEY: 'secret', HTTPS_PROXY: 'http://p' }), {
407
- HOME: '/h',
408
- HTTPS_PROXY: 'http://p',
175
+ const symlink = join(tmp, 'wevion')
176
+ symlinkSync(new URL('./src/index.mjs', import.meta.url), symlink)
177
+ const help = execFileSync(process.execPath, [symlink, 'help', knownCommand, '--base-url=http://127.0.0.1:9'], {
178
+ encoding: 'utf8',
179
+ env: { ...process.env, WEVION_API_KEY: '' },
409
180
  })
410
-
411
- // 426 upgrade message: prefer the server-provided message, else a default
412
- assert.equal(upgradeMessage('{"message":"upgrade now: npm i -g @wevion/cli@latest"}'), 'upgrade now: npm i -g @wevion/cli@latest')
413
- assert.match(upgradeMessage('not json'), /no longer supported/)
414
- assert.match(upgradeMessage('', '9.0.0'), /Minimum: 9\.0\.0/)
415
-
416
- // opToJson: machine-readable command view for agents (list --json / help --json)
417
- const getJson = opToJson(ops.get('get-ad-account-by-id'))
418
- assert.equal(getJson.command, 'get-ad-account-by-id')
419
- assert.equal(getJson.method, 'GET')
420
- assert.deepEqual(
421
- getJson.params.find((p) => p.flag === '--id'),
422
- { flag: '--id', in: 'path', required: true, type: 'string', array: false },
423
- )
424
- assert.equal(getJson.body, null)
425
- const postJson = opToJson(ops.get('create-campaign'))
426
- assert.ok(postJson.body.flags.find((f) => f.field === 'name' && f.required === true))
427
- assert.equal(postJson.body.raw, "--json '<json>'")
181
+ assert.match(help, /Flags:/)
428
182
 
429
183
  console.log('ok')