Skip to content

Commit 543508f

Browse files
committed
fix(cli): installers show the skill hint only for a binary that has install skills
The released v1 binary has no install skills command, so the stable installer would point agents at a missing command until the next Dify release ships v2. The hint now asks the installed binary first and stays quiet when it fails.
1 parent f071dea commit 543508f

8 files changed

Lines changed: 132 additions & 6 deletions

File tree

‎cli/scripts/install-cli.sh‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ err() { printf '%s\n' "install-cli: $*" >&2; }
2828
die() { err "$*"; rm -f "$FETCH_ERR_FILE"; exit 1; }
2929
need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; }
3030
re_escape() { printf '%s' "$1" | sed 's/[][\\.^$*+?(){}|/]/\\&/g'; }
31+
# print_skill_hint BIN: the hint names a v2 command, so a binary without it (any v1 build) gets none.
3132
print_skill_hint() {
33+
"$1" install skills --help >/dev/null 2>&1 || return 0
3234
printf '%s\n' 'AI agents: run `difyctl install skills <skills root>` after each install or upgrade; the skill ships inside the binary.'
3335
}
3436

@@ -248,7 +250,7 @@ main() {
248250
case ":${PATH}:" in
249251
*":${bin_dir}:"*)
250252
printf 'verify: run "difyctl version"\n'
251-
print_skill_hint
253+
print_skill_hint "$target_bin"
252254
;;
253255
*)
254256
printf '\n%s is not on your PATH. Add this to your shell profile:\n' "$bin_dir"

‎cli/scripts/install-cli.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,31 @@ describe('install-cli.sh install_binary', () => {
471471
expect(r.stderr).toMatch(/cannot write \/nonexistent-dir\/difyctl/)
472472
})
473473
})
474+
475+
// A v2 binary answers `install skills --help`; a v1 one exits 1 with "unknown command".
476+
describe.skipIf(process.platform === 'win32')('install-cli.sh print_skill_hint', () => {
477+
const fakeBins = [
478+
'd="$(mktemp -d)"',
479+
'printf \'#!/bin/sh\\n[ "$*" = "install skills --help" ]\\n\' > "$d/v2"',
480+
'printf \'#!/bin/sh\\necho "unknown command: $*" >&2\\nexit 1\\n\' > "$d/v1"',
481+
'chmod 0755 "$d/v2" "$d/v1"',
482+
]
483+
484+
it('prints the hint for a binary that has install skills', () => {
485+
const r = runLib([...fakeBins, 'print_skill_hint "$d/v2"', 'rm -rf "$d"'].join('\n'))
486+
expect(r.code).toBe(0)
487+
expect(r.stdout).toMatch(/difyctl install skills <skills root>/)
488+
})
489+
490+
it('prints nothing for a binary without install skills', () => {
491+
const r = runLib([...fakeBins, 'print_skill_hint "$d/v1"', 'rm -rf "$d"'].join('\n'))
492+
expect(r.code).toBe(0)
493+
expect(r.stdout).toBe('')
494+
})
495+
496+
it('prints nothing when the binary cannot run', () => {
497+
const r = runLib('print_skill_hint /nonexistent/difyctl')
498+
expect(r.code).toBe(0)
499+
expect(r.stdout).toBe('')
500+
})
501+
})

‎cli/scripts/install-r2.ps1‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ function Get-PathHint([string]$Dir) {
5151
return " [Environment]::SetEnvironmentVariable('PATH', `"$Dir;`" + [Environment]::GetEnvironmentVariable('PATH', 'User'), 'User')"
5252
}
5353

54-
function Write-SkillHint {
54+
# The hint names a v2 command, so a binary without it (any v1 build) gets none. Under
55+
# Windows PowerShell 5.1 a redirected stderr line throws when errors stop, hence the catch.
56+
function Write-SkillHint([string]$Bin) {
57+
try { & $Bin install skills --help *> $null } catch { return }
58+
if ($LASTEXITCODE -ne 0) { return }
5559
Write-Host 'AI agents: run `difyctl install skills <skills root>` after each install or upgrade; the skill ships inside the binary.'
5660
}
5761

@@ -79,7 +83,7 @@ function Install-DifyctlBinary([string]$dlUrl, [string]$sha, [string]$version, [
7983
Write-Host (Get-PathHint $installDir)
8084
}
8185
else {
82-
Write-SkillHint
86+
Write-SkillHint $dest
8387
}
8488
}
8589

‎cli/scripts/install-r2.ps1.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { spawnSync } from 'node:child_process'
2+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3+
import { tmpdir } from 'node:os'
24
import { join } from 'node:path'
35
import { fileURLToPath } from 'node:url'
46
import { describe, expect, it } from 'vite-plus/test'
@@ -106,3 +108,30 @@ d('install-r2.ps1 install location, PATH hint and download', () => {
106108
expect(r.stdout).toMatch(/checksum mismatch/)
107109
})
108110
})
111+
112+
// A v2 binary answers `install skills --help`; a v1 one exits 1 with "unknown command".
113+
// The fakes are shell scripts, so this runs where pwsh runs outside Windows.
114+
describe.skipIf(!hasPwsh || process.platform === 'win32')('install-r2.ps1 Write-SkillHint', () => {
115+
const hint = (bin: string) => pwsh(`Write-SkillHint '${bin}'`)
116+
117+
it('prints the hint for a binary that has install skills, and stays quiet otherwise', () => {
118+
const dir = mkdtempSync(join(tmpdir(), 'difyctl-hint-'))
119+
try {
120+
const v2 = join(dir, 'v2')
121+
const v1 = join(dir, 'v1')
122+
writeFileSync(v2, '#!/bin/sh\n[ "$*" = "install skills --help" ]\n', { mode: 0o755 })
123+
writeFileSync(v1, '#!/bin/sh\necho "unknown command: $*" >&2\nexit 1\n', { mode: 0o755 })
124+
expect(hint(v2)).toMatchObject({
125+
code: 0,
126+
stdout: expect.stringContaining('difyctl install skills <skills root>'),
127+
})
128+
expect(hint(v1)).toMatchObject({ code: 0, stdout: '' })
129+
} finally {
130+
rmSync(dir, { recursive: true, force: true })
131+
}
132+
})
133+
134+
it('prints nothing when the binary cannot run', () => {
135+
expect(hint('/nonexistent/difyctl')).toMatchObject({ code: 0, stdout: '' })
136+
})
137+
})

‎cli/scripts/install-r2.sh‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ trap 'rm -f "$tmp_m" "${tmp_c:-}" "${tmp_b:-}"' EXIT INT TERM
2222
err() { printf '%s\n' "install-r2: $*" >&2; }
2323
die() { err "$*"; exit 1; }
2424
need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; }
25+
# print_skill_hint BIN: the hint names a v2 command, so a binary without it (any v1 build) gets none.
2526
print_skill_hint() {
27+
"$1" install skills --help >/dev/null 2>&1 || return 0
2628
printf '%s\n' 'AI agents: run `difyctl install skills <skills root>` after each install or upgrade; the skill ships inside the binary.'
2729
}
2830

@@ -114,7 +116,7 @@ fetch_verify_install() {
114116

115117
printf 'difyctl %s (channel %s) installed: %s\n' "$3" "$4" "${install_dir}/difyctl"
116118
case ":${PATH}:" in
117-
*":${install_dir}:"*) print_skill_hint ;;
119+
*":${install_dir}:"*) print_skill_hint "${install_dir}/difyctl" ;;
118120
*) printf 'note: add %s to your PATH\n' "$install_dir" ;;
119121
esac
120122
}

‎cli/scripts/install-r2.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,31 @@ describe.skipIf(process.platform === 'win32')('install-r2.sh install_binary', ()
192192
expect(r.stdout.split('\n')).toEqual(['-rwxr-xr-x', 'new', 'difyctl'])
193193
})
194194
})
195+
196+
// A v2 binary answers `install skills --help`; a v1 one exits 1 with "unknown command".
197+
describe.skipIf(process.platform === 'win32')('install-r2.sh print_skill_hint', () => {
198+
const fakeBins = [
199+
'd="$(mktemp -d)"',
200+
'printf \'#!/bin/sh\\n[ "$*" = "install skills --help" ]\\n\' > "$d/v2"',
201+
'printf \'#!/bin/sh\\necho "unknown command: $*" >&2\\nexit 1\\n\' > "$d/v1"',
202+
'chmod 0755 "$d/v2" "$d/v1"',
203+
]
204+
205+
it('prints the hint for a binary that has install skills', () => {
206+
const r = lib([...fakeBins, 'print_skill_hint "$d/v2"', 'rm -rf "$d"'].join('\n'))
207+
expect(r.code).toBe(0)
208+
expect(r.stdout).toMatch(/difyctl install skills <skills root>/)
209+
})
210+
211+
it('prints nothing for a binary without install skills', () => {
212+
const r = lib([...fakeBins, 'print_skill_hint "$d/v1"', 'rm -rf "$d"'].join('\n'))
213+
expect(r.code).toBe(0)
214+
expect(r.stdout).toBe('')
215+
})
216+
217+
it('prints nothing when the binary cannot run', () => {
218+
const r = lib('print_skill_hint /nonexistent/difyctl')
219+
expect(r.code).toBe(0)
220+
expect(r.stdout).toBe('')
221+
})
222+
})

‎cli/scripts/install.ps1‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ function Get-PathHint([string]$Dir) {
127127
return " [Environment]::SetEnvironmentVariable('PATH', `"$Dir;`" + [Environment]::GetEnvironmentVariable('PATH', 'User'), 'User')"
128128
}
129129

130-
function Write-SkillHint {
130+
# The hint names a v2 command, so a binary without it (any v1 build) gets none. Under
131+
# Windows PowerShell 5.1 a redirected stderr line throws when errors stop, hence the catch.
132+
function Write-SkillHint([string]$Bin) {
133+
try { & $Bin install skills --help *> $null } catch { return }
134+
if ($LASTEXITCODE -ne 0) { return }
131135
Write-Host 'AI agents: run `difyctl install skills <skills root>` after each install or upgrade; the skill ships inside the binary.'
132136
}
133137

@@ -182,7 +186,7 @@ function Invoke-Main {
182186
}
183187
else {
184188
Write-Host 'verify: run "difyctl version"'
185-
Write-SkillHint
189+
Write-SkillHint $targetBin
186190
}
187191
}
188192
finally {

‎cli/scripts/install.ps1.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { spawnSync } from 'node:child_process'
2+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3+
import { tmpdir } from 'node:os'
24
import { join } from 'node:path'
35
import { fileURLToPath } from 'node:url'
46
import { describe, expect, it } from 'vite-plus/test'
@@ -328,3 +330,30 @@ describe.skipIf(!PWSH)('install.ps1 install location and PATH hint', () => {
328330
expect(hint).not.toContain('$env:PATH')
329331
})
330332
})
333+
334+
// A v2 binary answers `install skills --help`; a v1 one exits 1 with "unknown command".
335+
// The fakes are shell scripts, so this runs where pwsh runs outside Windows.
336+
describe.skipIf(!PWSH || process.platform === 'win32')('install.ps1 Write-SkillHint', () => {
337+
const hint = (bin: string) => runPwsh(`Write-SkillHint '${bin}'`)
338+
339+
it('prints the hint for a binary that has install skills, and stays quiet otherwise', () => {
340+
const dir = mkdtempSync(join(tmpdir(), 'difyctl-hint-'))
341+
try {
342+
const v2 = join(dir, 'v2')
343+
const v1 = join(dir, 'v1')
344+
writeFileSync(v2, '#!/bin/sh\n[ "$*" = "install skills --help" ]\n', { mode: 0o755 })
345+
writeFileSync(v1, '#!/bin/sh\necho "unknown command: $*" >&2\nexit 1\n', { mode: 0o755 })
346+
expect(hint(v2)).toMatchObject({
347+
code: 0,
348+
stdout: expect.stringContaining('difyctl install skills <skills root>'),
349+
})
350+
expect(hint(v1)).toMatchObject({ code: 0, stdout: '' })
351+
} finally {
352+
rmSync(dir, { recursive: true, force: true })
353+
}
354+
})
355+
356+
it('prints nothing when the binary cannot run', () => {
357+
expect(hint('/nonexistent/difyctl')).toMatchObject({ code: 0, stdout: '' })
358+
})
359+
})

0 commit comments

Comments
 (0)