@visa/cli 5.0.0-rc.409 → 5.0.0-rc.412

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.
@@ -1,328 +0,0 @@
1
- #!/usr/bin/env node
2
- // Portable provisioner for the pair-visa-agent skill.
3
- //
4
- // Ensures the `visa` CLI is installed and serves the MCP tools the pairing skill
5
- // calls (`get_status` and `agent_enroll`, both in the CLI's six served tools) so
6
- // ANY Agent Skills runtime can pair — not just OpenClaw (whose
7
- // `metadata.openclaw.install` auto-runs). Hermes, Claude Code, and any other agentskills.io-compatible
8
- // runtime run this bundled script per the standard's `scripts/` execution stage.
9
- //
10
- // Safe to run repeatedly:
11
- // - Idempotent no-op when a compatible version with required tools is installed.
12
- // - Upgrades when a stale or incomplete binary (one without the six served tools) is found.
13
- // - Installs `@visa/cli@rc` when no binary exists.
14
-
15
- import { execFileSync } from 'node:child_process'
16
- import { existsSync, readFileSync } from 'node:fs'
17
- import { dirname, join } from 'node:path'
18
- import { createRequire } from 'node:module'
19
- import { pathToFileURL } from 'node:url'
20
-
21
- export const DEFAULT_RELEASE_CHANNEL = 'rc'
22
- // 5.0.0-rc.335 is the first release serving the six tools (`SIX_TOOL_NAMES` in
23
- // packages/cli/src/mcp-server/six-tools.ts). The tools this list named before,
24
- // `agent_handoff_claim`, `agent_connect` and `agent_connect_poll`, were retired
25
- // with it, so a current CLI could never pass this check.
26
- export const MIN_COMPATIBLE_VERSION = '5.0.0-rc.335'
27
- export const REQUIRED_MCP_TOOLS = Object.freeze(['get_status', 'agent_enroll'])
28
-
29
- const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
30
-
31
- export function parseSemver(v) {
32
- if (typeof v !== 'string') return null
33
- const cleaned = v.trim().replace(/^v/, '')
34
- const m = cleaned.match(SEMVER_RE)
35
- if (!m) return null
36
- return {
37
- major: Number(m[1]),
38
- minor: Number(m[2]),
39
- patch: Number(m[3]),
40
- pre: m[4] ?? null,
41
- }
42
- }
43
-
44
- /**
45
- * Returns true if `version` satisfies >= `minVersion`.
46
- * Rules (semver 2.0.0):
47
- * - Release (no prerelease) outranks any prerelease of the same major.minor.patch.
48
- * - For identical major.minor.patch with prerelease `rc.N`, compares N numerically.
49
- */
50
- export function isSemverCompatible(version, minVersion = MIN_COMPATIBLE_VERSION) {
51
- const cur = parseSemver(version)
52
- const min = parseSemver(minVersion)
53
- if (!cur || !min) return false
54
-
55
- if (cur.major !== min.major) return cur.major > min.major
56
- if (cur.minor !== min.minor) return cur.minor > min.minor
57
- if (cur.patch !== min.patch) return cur.patch > min.patch
58
-
59
- // Both main versions are equal.
60
- // A release version (no prerelease) outranks any prerelease of the same main version.
61
- if (!cur.pre && min.pre) return true
62
- if (cur.pre && !min.pre) return false
63
- if (!cur.pre && !min.pre) return true
64
-
65
- // Both have prereleases. Compare prerelease identifiers.
66
- const curParts = cur.pre.split('.')
67
- const minParts = min.pre.split('.')
68
- const len = Math.max(curParts.length, minParts.length)
69
-
70
- for (let i = 0; i < len; i++) {
71
- if (i >= curParts.length) return false
72
- if (i >= minParts.length) return true
73
- const a = curParts[i]
74
- const b = minParts[i]
75
- const aNum = /^\d+$/.test(a)
76
- const bNum = /^\d+$/.test(b)
77
- if (aNum && bNum) {
78
- const numA = Number(a)
79
- const numB = Number(b)
80
- if (numA !== numB) return numA > numB
81
- } else if (aNum && !bNum) {
82
- return false
83
- } else if (!aNum && bNum) {
84
- return true
85
- } else if (a !== b) {
86
- return a > b
87
- }
88
- }
89
- return true
90
- }
91
-
92
- export function findCliBinary(execFn = execFileSync) {
93
- for (const bin of ['visa', 'visa-cli']) {
94
- try {
95
- execFn(bin, ['--version'], { stdio: 'ignore', timeout: 5000 })
96
- return bin
97
- } catch {
98
- // not resolvable or failed
99
- }
100
- }
101
- return null
102
- }
103
-
104
- export function detectCliVersion(bin, execFn = execFileSync) {
105
- if (!bin) return null
106
-
107
- try {
108
- const rawCap = execFn(bin, ['capabilities', '--format', 'json'], {
109
- encoding: 'utf-8',
110
- timeout: 5000,
111
- })
112
- const cap = JSON.parse(rawCap)
113
- const version = cap?.data?.build?.version ?? cap?.build?.version
114
- if (typeof version === 'string' && version.trim()) {
115
- return version.trim()
116
- }
117
- } catch {
118
- // fallback to --version
119
- }
120
-
121
- try {
122
- const raw = execFn(bin, ['--version'], {
123
- encoding: 'utf-8',
124
- timeout: 5000,
125
- })
126
- const cleaned = raw.trim().replace(/^v/, '')
127
- if (cleaned) return cleaned
128
- } catch {
129
- // unable to detect version
130
- }
131
-
132
- return null
133
- }
134
-
135
- function defaultLocalMcpBundlePath() {
136
- try {
137
- const req = createRequire(import.meta.url)
138
- const pkgJson = req.resolve('@visa/cli/package.json')
139
- return join(dirname(pkgJson), 'dist', 'mcp-server', 'index.js')
140
- } catch {
141
- return null
142
- }
143
- }
144
-
145
- export function resolveMcpBundlePath(
146
- npmRootFn = () => execFileSync('npm', ['root', '-g'], { encoding: 'utf-8' }),
147
- localBundleFn = defaultLocalMcpBundlePath
148
- ) {
149
- const candidates = []
150
-
151
- try {
152
- const globalRoot = npmRootFn().trim()
153
- if (globalRoot) {
154
- candidates.push(join(globalRoot, '@visa', 'cli', 'dist', 'mcp-server', 'index.js'))
155
- }
156
- } catch {
157
- // npm root -g failed
158
- }
159
-
160
- const local = localBundleFn()
161
- if (local) candidates.push(local)
162
-
163
- for (const candidate of candidates) {
164
- if (existsSync(candidate)) return candidate
165
- }
166
- return null
167
- }
168
-
169
- export function probeMcpTools(mcpPath, requiredTools = REQUIRED_MCP_TOOLS) {
170
- if (!mcpPath || !existsSync(mcpPath)) {
171
- return { ok: false, missingTools: [...requiredTools] }
172
- }
173
-
174
- try {
175
- const content = readFileSync(mcpPath, 'utf-8')
176
- const missing = requiredTools.filter((tool) => {
177
- const exact = new RegExp(`(?:^|[^A-Za-z0-9_])${tool}(?:[^A-Za-z0-9_]|$)`)
178
- return !exact.test(content)
179
- })
180
- return {
181
- ok: missing.length === 0,
182
- missingTools: missing,
183
- }
184
- } catch {
185
- return { ok: false, missingTools: [...requiredTools] }
186
- }
187
- }
188
-
189
- export function verifyCliReadiness(
190
- execFn = execFileSync,
191
- npmRootFn = () => execFileSync('npm', ['root', '-g'], { encoding: 'utf-8' }),
192
- localBundleFn
193
- ) {
194
- const bin = findCliBinary(execFn)
195
- if (!bin) {
196
- return {
197
- ready: false,
198
- reason: 'not_installed',
199
- bin: null,
200
- version: null,
201
- missingTools: [...REQUIRED_MCP_TOOLS],
202
- }
203
- }
204
-
205
- const version = detectCliVersion(bin, execFn)
206
- const isVersionOk = Boolean(version && isSemverCompatible(version, MIN_COMPATIBLE_VERSION))
207
-
208
- const mcpPath =
209
- localBundleFn === undefined
210
- ? resolveMcpBundlePath(npmRootFn)
211
- : resolveMcpBundlePath(npmRootFn, localBundleFn)
212
- const toolProbe = mcpPath
213
- ? probeMcpTools(mcpPath, REQUIRED_MCP_TOOLS)
214
- : { ok: false, missingTools: [...REQUIRED_MCP_TOOLS] }
215
-
216
- if (isVersionOk && toolProbe.ok) {
217
- return {
218
- ready: true,
219
- bin,
220
- version: version || 'unknown',
221
- missingTools: [],
222
- }
223
- }
224
-
225
- return {
226
- ready: false,
227
- reason: 'stale_or_incomplete',
228
- bin,
229
- version,
230
- missingTools:
231
- toolProbe.missingTools.length > 0
232
- ? toolProbe.missingTools
233
- : isVersionOk
234
- ? []
235
- : [...REQUIRED_MCP_TOOLS],
236
- }
237
- }
238
-
239
- export function performInstallOrUpgrade(pkgName, execFn = execFileSync) {
240
- execFn('npm', ['install', '-g', pkgName], {
241
- stdio: 'inherit',
242
- timeout: 120_000,
243
- })
244
- }
245
-
246
- export async function runSetup(env = process.env, execs = {}) {
247
- const execFn = execs.execFileSync ?? execFileSync
248
- const npmRootFn = execs.npmRootFn ?? (() => execFn('npm', ['root', '-g'], { encoding: 'utf-8' }))
249
-
250
- const channel = env.VISA_RELEASE_CHANNEL || DEFAULT_RELEASE_CHANNEL
251
- const pkgName = `@visa/cli@${channel}`
252
-
253
- const initial = verifyCliReadiness(execFn, npmRootFn)
254
-
255
- if (initial.ready) {
256
- console.log(
257
- `✓ visa CLI v${initial.version} already installed and verified (${REQUIRED_MCP_TOOLS.join(', ')} present) — nothing to do. Run the pairing flow in SKILL.md.`
258
- )
259
- return {
260
- status: 'already_ready',
261
- version: initial.version,
262
- bin: initial.bin,
263
- }
264
- }
265
-
266
- if (initial.reason === 'stale_or_incomplete') {
267
- const missingStr =
268
- initial.missingTools.length > 0 ? initial.missingTools.join(', ') : 'v4 tool generation'
269
- console.log(
270
- `Detected stale or incomplete CLI (v${initial.version || 'unknown'} lacks required tools: ${missingStr}). Upgrading to ${pkgName}…`
271
- )
272
- } else {
273
- console.log(`Installing ${pkgName} (the v4 agent surface is prerelease)…`)
274
- }
275
-
276
- try {
277
- performInstallOrUpgrade(pkgName, execFn)
278
- } catch (err) {
279
- console.error(
280
- `Global install failed. Try \`npm install -g ${pkgName}\` manually (may need sudo, or set a\n` +
281
- 'user-writable npm prefix: `npm config set prefix ~/.npm-global` and add its `bin` to PATH).'
282
- )
283
- process.exit(1)
284
- }
285
-
286
- const post = verifyCliReadiness(execFn, npmRootFn)
287
-
288
- if (!post.bin) {
289
- console.error(
290
- 'Installed, but `visa` is not on PATH. Ensure your npm global bin dir is on PATH\n' +
291
- '(`npm bin -g` shows it), then re-run this script.'
292
- )
293
- process.exit(1)
294
- }
295
-
296
- if (!post.ready) {
297
- const missingStr =
298
- post.missingTools.length > 0 ? post.missingTools.join(', ') : 'v4 tool generation'
299
- console.error(
300
- `Readiness check failed: installed CLI v${post.version || 'unknown'} is missing required tools (${missingStr}).\n` +
301
- `Explicit upgrade action:\n` +
302
- ` npm install -g ${pkgName}`
303
- )
304
- process.exit(1)
305
- }
306
-
307
- const status = initial.reason === 'not_installed' ? 'installed' : 'upgraded'
308
- console.log(
309
- `✓ visa CLI ready (v${post.version}, ${REQUIRED_MCP_TOOLS.join(', ')} present). Now run the pairing flow in SKILL.md.`
310
- )
311
- return {
312
- status,
313
- version: post.version,
314
- bin: post.bin,
315
- }
316
- }
317
-
318
- const isMain =
319
- typeof process !== 'undefined' &&
320
- process.argv[1] &&
321
- (import.meta.url === pathToFileURL(process.argv[1]).href || process.argv[1].endsWith('setup.mjs'))
322
-
323
- if (isMain) {
324
- runSetup().catch((err) => {
325
- console.error(`Provisioning error: ${err && err.message ? err.message : String(err)}`)
326
- process.exit(1)
327
- })
328
- }