@wevion/cli 1.0.2

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/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@wevion/cli",
3
+ "version": "1.0.2",
4
+ "description": "Wevion API command-line interface — generated from the public OpenAPI spec",
5
+ "type": "module",
6
+ "bin": {
7
+ "wevion": "src/index.mjs"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "selftest.mjs",
12
+ "openapi.json"
13
+ ],
14
+ "engines": {
15
+ "node": ">=22"
16
+ },
17
+ "scripts": {
18
+ "prepack": "node -e \"require('fs').accessSync('openapi.json')\"",
19
+ "test": "node selftest.mjs"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
24
+ }
package/selftest.mjs ADDED
@@ -0,0 +1,183 @@
1
+ // Minimal runnable check for the spec->request mapping. Run: node selftest.mjs
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, symlinkSync } from 'node:fs'
4
+ import { readFile } from 'node:fs/promises'
5
+ import { tmpdir } from 'node:os'
6
+ import { join } from 'node:path'
7
+ import { execFileSync } from 'node:child_process'
8
+ import { kebab, listOperations, buildRequest, configPath, resolveApiKey } from './src/index.mjs'
9
+
10
+ const spec = {
11
+ openapi: '3.0.3',
12
+ security: [{ apiKeyAuth: [] }],
13
+ paths: {
14
+ '/api/v1/ad-accounts/{id}': {
15
+ get: {
16
+ operationId: 'getAdAccountById',
17
+ tags: ['ad-accounts'],
18
+ parameters: [
19
+ { name: 'id', in: 'path', required: true, schema: { type: 'string' } },
20
+ { name: 'fields', in: 'query', schema: { type: 'string' } },
21
+ ],
22
+ },
23
+ },
24
+ '/api/v1/search': {
25
+ get: {
26
+ operationId: 'searchThings',
27
+ tags: ['search'],
28
+ parameters: [
29
+ { name: 'q', in: 'query', required: true, schema: { type: 'string' } },
30
+ { name: 'ids', in: 'query', schema: { type: 'array', items: { type: 'string' } } },
31
+ { name: 'x-test-header', in: 'header', required: true, schema: { type: 'string' } },
32
+ ],
33
+ },
34
+ },
35
+ '/api/v1/campaigns': {
36
+ post: {
37
+ operationId: 'createCampaign',
38
+ tags: ['campaigns'],
39
+ requestBody: {
40
+ content: {
41
+ 'application/json': {
42
+ schema: {
43
+ type: 'object',
44
+ properties: {
45
+ name: { type: 'string' },
46
+ budget: { type: 'number' },
47
+ enabled: { type: 'boolean' },
48
+ labels: { type: 'array', items: { type: 'string' } },
49
+ settings: { type: 'object' },
50
+ },
51
+ required: ['name'],
52
+ },
53
+ },
54
+ },
55
+ },
56
+ },
57
+ },
58
+ '/api/v1/sessions/{id}/messages': {
59
+ post: {
60
+ operationId: 'createSessionMessage',
61
+ tags: ['chat'],
62
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
63
+ requestBody: {
64
+ content: {
65
+ 'application/json': {
66
+ schema: {
67
+ type: 'object',
68
+ properties: { id: { type: 'string' }, content: { type: 'string' } },
69
+ required: ['id', 'content'],
70
+ },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ },
76
+ '/api/v1/uploads': {
77
+ post: {
78
+ operationId: 'uploadFile',
79
+ tags: ['uploads'],
80
+ requestBody: {
81
+ content: {
82
+ 'multipart/form-data': {
83
+ schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ }
91
+
92
+ assert.equal(kebab('getAdAccountById'), 'get-ad-account-by-id')
93
+
94
+ const ops = listOperations(spec)
95
+ assert.equal(ops.size, 5)
96
+
97
+ // path + query mapping
98
+ const get = buildRequest(ops.get('get-ad-account-by-id'), { id: 'act_9', fields: 'name,status' }, 'https://api.wevion.ai')
99
+ assert.equal(get.method, 'GET')
100
+ assert.equal(get.url, 'https://api.wevion.ai/api/v1/ad-accounts/act_9?fields=name%2Cstatus')
101
+ assert.equal(get.body, undefined)
102
+ assert.deepEqual(get.headers, {})
103
+
104
+ // missing path param throws
105
+ assert.throws(() => buildRequest(ops.get('get-ad-account-by-id'), {}, 'x'), /missing required path param --id/)
106
+
107
+ // required query/header and query array mapping
108
+ assert.throws(() => buildRequest(ops.get('search-things'), { q: 'abc' }, 'https://api.wevion.ai'), /missing required header param --x-test-header/)
109
+ const search = buildRequest(
110
+ ops.get('search-things'),
111
+ { q: 'abc', ids: ['act_1', 'act_2,act_3'], 'x-test-header': 'secret' },
112
+ 'https://api.wevion.ai',
113
+ )
114
+ assert.equal(search.url, 'https://api.wevion.ai/api/v1/search?q=abc&ids=act_1&ids=act_2&ids=act_3')
115
+ assert.deepEqual(search.headers, { 'x-test-header': 'secret' })
116
+
117
+ // body assembly from flags with schema-aware coercion
118
+ const post = buildRequest(
119
+ ops.get('create-campaign'),
120
+ {
121
+ name: 'Q3',
122
+ budget: '100.5',
123
+ enabled: 'false',
124
+ labels: '["launch","q3"]',
125
+ settings: '{"daily":true}',
126
+ },
127
+ 'https://api.wevion.ai/',
128
+ )
129
+ assert.equal(post.method, 'POST')
130
+ assert.equal(post.url, 'https://api.wevion.ai/api/v1/campaigns')
131
+ assert.deepEqual(JSON.parse(post.body), {
132
+ name: 'Q3',
133
+ budget: 100.5,
134
+ enabled: false,
135
+ labels: ['launch', 'q3'],
136
+ settings: { daily: true },
137
+ })
138
+ assert.throws(() => buildRequest(ops.get('create-campaign'), { budget: '100' }, 'x'), /missing required body field --name/)
139
+
140
+ // raw --json overrides body but is still parsed for top-level required fields
141
+ const raw = buildRequest(ops.get('create-campaign'), { json: '{"name":"raw"}' }, 'x')
142
+ assert.equal(raw.body, '{"name":"raw"}')
143
+ assert.throws(() => buildRequest(ops.get('create-campaign'), { json: '{"budget":10}' }, 'x'), /missing required body field --name/)
144
+
145
+ // body fields that collide with path/query/header flags are exposed as --body-*
146
+ const message = buildRequest(
147
+ ops.get('create-session-message'),
148
+ { id: 'sess_1', 'body-id': 'msg_1', content: 'ciao' },
149
+ 'https://api.wevion.ai',
150
+ )
151
+ assert.equal(message.url, 'https://api.wevion.ai/api/v1/sessions/sess_1/messages')
152
+ assert.deepEqual(JSON.parse(message.body), { id: 'msg_1', content: 'ciao' })
153
+
154
+ // unsupported request body media types fail before fetch
155
+ assert.throws(() => buildRequest(ops.get('upload-file'), {}, 'x'), /request body media type is not supported/)
156
+
157
+ // 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
+ assert.equal(configPath({ APPDATA: 'C:\\Users\\u\\AppData\\Roaming' }, 'win32'), 'C:\\Users\\u\\AppData\\Roaming/Wevion/config.json')
161
+
162
+ // api key precedence: env > config
163
+ assert.equal(resolveApiKey({ WEVION_API_KEY: 'env' }, { apiKey: 'cfg' }), 'env')
164
+ assert.equal(resolveApiKey({}, { apiKey: 'cfg' }), 'cfg')
165
+ assert.equal(resolveApiKey({}, {}), null)
166
+
167
+ // bundled snapshot must exist and keep list/help offline.
168
+ const bundled = JSON.parse(await readFile(new URL('./openapi.json', import.meta.url), 'utf8'))
169
+ assert.match(bundled.openapi, /^3\./)
170
+ assert.ok(Object.keys(bundled.paths || {}).length > 0)
171
+ const knownCommand = listOperations(bundled).keys().next().value
172
+ assert.ok(knownCommand)
173
+
174
+ const tmp = mkdtempSync(join(tmpdir(), 'wevion-cli-'))
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: '' },
180
+ })
181
+ assert.match(help, /Flags:/)
182
+
183
+ console.log('ok')