@wevion/cli 1.0.3 → 1.0.2366
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 -671
- package/package.json +1 -1
- package/selftest.mjs +12 -258
- package/src/index.mjs +27 -540
package/package.json
CHANGED
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,
|
|
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 {
|
|
9
|
-
import {
|
|
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' }
|
|
173
|
-
assert.equal(configPath({ HOME: '/home/u' }
|
|
174
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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')
|