@wevion/cli 1.0.2 → 1.0.3
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 +100 -27
- package/openapi.json +472 -74
- package/package.json +1 -1
- package/selftest.mjs +258 -12
- package/src/index.mjs +540 -27
package/selftest.mjs
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
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,
|
|
3
|
+
import { mkdtempSync, statSync } from 'node:fs'
|
|
4
4
|
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { createServer } from 'node:http'
|
|
5
6
|
import { tmpdir } from 'node:os'
|
|
6
7
|
import { join } from 'node:path'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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'
|
|
9
23
|
|
|
10
24
|
const spec = {
|
|
11
25
|
openapi: '3.0.3',
|
|
@@ -155,9 +169,10 @@ assert.deepEqual(JSON.parse(message.body), { id: 'msg_1', content: 'ciao' })
|
|
|
155
169
|
assert.throws(() => buildRequest(ops.get('upload-file'), {}, 'x'), /request body media type is not supported/)
|
|
156
170
|
|
|
157
171
|
// config path honours XDG_CONFIG_HOME, falls back to ~/.config
|
|
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
|
-
|
|
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'))
|
|
161
176
|
|
|
162
177
|
// api key precedence: env > config
|
|
163
178
|
assert.equal(resolveApiKey({ WEVION_API_KEY: 'env' }, { apiKey: 'cfg' }), 'env')
|
|
@@ -170,14 +185,245 @@ assert.match(bundled.openapi, /^3\./)
|
|
|
170
185
|
assert.ok(Object.keys(bundled.paths || {}).length > 0)
|
|
171
186
|
const knownCommand = listOperations(bundled).keys().next().value
|
|
172
187
|
assert.ok(knownCommand)
|
|
188
|
+
const knownSimpleCommand = 'get-api-v1-ad-accounts'
|
|
189
|
+
assert.ok(listOperations(bundled).has(knownSimpleCommand))
|
|
173
190
|
|
|
174
191
|
const tmp = mkdtempSync(join(tmpdir(), 'wevion-cli-'))
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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',
|
|
180
409
|
})
|
|
181
|
-
|
|
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>'")
|
|
182
428
|
|
|
183
429
|
console.log('ok')
|