mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
Merge pull request #616 from opticon454/fix/deepseek-install-path
fix(deepseek): find pnpm when installing the terminal profile, and install it with dsh Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
5eccaca3bc
7 files changed
+99
-8
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'aicodeman': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Installing DeepSeek from CLI management no longer leaves it "not installed". The `dsh` terminal profile install ran `dsh plugin`, which needs `pnpm` on its PATH, but the server's own PATH usually lacks the user prefix (`~/.local/bin`) where CLI management puts `dsh`. The profile install now puts `dsh`'s directory first on PATH, and the DeepSeek install command installs `pnpm` alongside `dsh` when none is on PATH (an existing one, such as a corepack shim, is left alone: `npm install -g pnpm` refuses to overwrite it).
|
||||||
@@ -238,8 +238,8 @@
|
|||||||
},
|
},
|
||||||
"install": {
|
"install": {
|
||||||
"command": {
|
"command": {
|
||||||
"linux": "npm install -g @deepseek-ai/dsh",
|
"linux": "npm install -g @deepseek-ai/dsh && (command -v pnpm >/dev/null || npm install -g pnpm)",
|
||||||
"darwin": "npm install -g @deepseek-ai/dsh"
|
"darwin": "npm install -g @deepseek-ai/dsh && (command -v pnpm >/dev/null || npm install -g pnpm)"
|
||||||
},
|
},
|
||||||
"npmPackage": "@deepseek-ai/dsh",
|
"npmPackage": "@deepseek-ai/dsh",
|
||||||
"docsUrl": "https://github.com/deepseek-ai/deepseek-harness",
|
"docsUrl": "https://github.com/deepseek-ai/deepseek-harness",
|
||||||
|
|||||||
@@ -1300,8 +1300,14 @@ const DEEPSEEK: CliEntry = {
|
|||||||
launcherTargetParam: 'profile',
|
launcherTargetParam: 'profile',
|
||||||
install: {
|
install: {
|
||||||
command: {
|
command: {
|
||||||
linux: 'npm install -g @deepseek-ai/dsh',
|
// pnpm rides along: `dsh plugin` spawns a literal `pnpm` with no npm fallback, so the
|
||||||
darwin: 'npm install -g @deepseek-ai/dsh',
|
// terminal profile this CLI needs before it can run cannot be installed without it (#352).
|
||||||
|
// ⚠️ Only when none is on PATH. `corepack enable` puts a `pnpm` shim in node's own bin
|
||||||
|
// directory, which is npm's global bin for nvm and Homebrew, and `npm install -g pnpm`
|
||||||
|
// refuses to overwrite it (EEXIST). Asked for in the same command as dsh, that failure
|
||||||
|
// took dsh down with it, on exactly the machines that already had a working pnpm.
|
||||||
|
linux: 'npm install -g @deepseek-ai/dsh && (command -v pnpm >/dev/null || npm install -g pnpm)',
|
||||||
|
darwin: 'npm install -g @deepseek-ai/dsh && (command -v pnpm >/dev/null || npm install -g pnpm)',
|
||||||
},
|
},
|
||||||
npmPackage: '@deepseek-ai/dsh',
|
npmPackage: '@deepseek-ai/dsh',
|
||||||
docsUrl: 'https://github.com/deepseek-ai/deepseek-harness',
|
docsUrl: 'https://github.com/deepseek-ai/deepseek-harness',
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ export const DEEPSEEK_VERSION_REGEX = /(?:^|\s)v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z][0
|
|||||||
*/
|
*/
|
||||||
export const DEEPSEEK_IDENTITY_REGEX = /DeepSeek\s+Harness/i;
|
export const DEEPSEEK_IDENTITY_REGEX = /DeepSeek\s+Harness/i;
|
||||||
|
|
||||||
const DEEPSEEK_NOT_FOUND = 'DeepSeek Harness CLI (dsh) not found. Install with: npm install -g @deepseek-ai/dsh';
|
const DEEPSEEK_NOT_FOUND =
|
||||||
|
'DeepSeek Harness CLI (dsh) not found. Install with: npm install -g @deepseek-ai/dsh && (command -v pnpm >/dev/null || npm install -g pnpm)';
|
||||||
|
|
||||||
/** Where profiles live: `$DSH_HOME/profiles`, defaulting to `~/.dsh/profiles`. */
|
/** Where profiles live: `$DSH_HOME/profiles`, defaulting to `~/.dsh/profiles`. */
|
||||||
export function resolveDshHome(): string {
|
export function resolveDshHome(): string {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const RUN_MODE_LAUNCH = {
|
|||||||
},
|
},
|
||||||
deepseek: {
|
deepseek: {
|
||||||
label: 'DeepSeek',
|
label: 'DeepSeek',
|
||||||
installHint: 'DeepSeek Harness CLI (dsh) not found. Install with: npm install -g @deepseek-ai/dsh',
|
installHint: 'DeepSeek Harness CLI (dsh) not found. Install with: npm install -g @deepseek-ai/dsh && (command -v pnpm >/dev/null || npm install -g pnpm)',
|
||||||
// The two-part availability check is deliberate. `dsh` being installed is
|
// The two-part availability check is deliberate. `dsh` being installed is
|
||||||
// not enough — DeepSeek ships no terminal front door, so a box can have a
|
// not enough — DeepSeek ships no terminal front door, so a box can have a
|
||||||
// perfect binary and nothing a pane can run.
|
// perfect binary and nothing a pane can run.
|
||||||
|
|||||||
@@ -617,13 +617,16 @@ export function registerSystemRoutes(
|
|||||||
// synchronous-spawn precedent this endpoint is modelled on.
|
// synchronous-spawn precedent this endpoint is modelled on.
|
||||||
detached: true,
|
detached: true,
|
||||||
// Inherit the environment: this needs a HOME to resolve $DSH_HOME
|
// Inherit the environment: this needs a HOME to resolve $DSH_HOME
|
||||||
// against, and a PATH carrying `pnpm`. ⚠️ `dsh plugin` does NOT bundle a
|
// against, and a PATH carrying `pnpm` (see the PATH below). ⚠️ `dsh plugin` does NOT bundle a
|
||||||
// package manager — it `spawnSync`s a literal `pnpm` with no npm
|
// package manager — it `spawnSync`s a literal `pnpm` with no npm
|
||||||
// fallback, so on a host without one this exits 127 and dsh's own
|
// fallback, so on a host without one this exits 127 and dsh's own
|
||||||
// stderr ("pnpm not found on PATH") is what reaches the caller through
|
// stderr ("pnpm not found on PATH") is what reaches the caller through
|
||||||
// the OPERATION_FAILED detail below. That is the same missing
|
// the OPERATION_FAILED detail below. That is the same missing
|
||||||
// dependency that broke the docker agent image in issue #352.
|
// dependency that broke the docker agent image in issue #352.
|
||||||
env: process.env,
|
// PATH leads with the directory `dsh` itself resolved to: CLI management installs
|
||||||
|
// `dsh` and `pnpm` into the user prefix (`~/.local/bin`), which a service's own PATH
|
||||||
|
// usually lacks, so `dsh plugin` could not find the `pnpm` sitting next to it.
|
||||||
|
env: { ...process.env, PATH: [dir, process.env.PATH].filter(Boolean).join(':') },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resolve({ code: null, output: `spawn failed: ${getErrorMessage(err)}`, timedOut: false });
|
resolve({ code: null, output: `spawn failed: ${getErrorMessage(err)}`, timedOut: false });
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join, resolve } from 'node:path';
|
||||||
|
import { STOCK_CLIS } from '../src/config/cli-registry/stock.js';
|
||||||
|
|
||||||
|
const root = resolve(import.meta.dirname, '..');
|
||||||
|
const dirs: string[] = [];
|
||||||
|
afterEach(() => {
|
||||||
|
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
function deepseekInstallCommand(platform: 'linux' | 'darwin'): string {
|
||||||
|
const ds = STOCK_CLIS.find((c) => (c.id as string) === 'deepseek')!;
|
||||||
|
return ds.discovery.install.command![platform]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the stock install command through `/bin/sh -c` (what CLI management does, `shell: true`)
|
||||||
|
* with a fake `npm` that records each call, on a PATH that holds only the fake bins. So this
|
||||||
|
* exercises the real string, not a copy of it.
|
||||||
|
*/
|
||||||
|
function runInstall(opts: { pnpmOnPath: boolean; npmFails?: boolean }): { status: number | null; npmCalls: string[] } {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'dsh-install-'));
|
||||||
|
dirs.push(dir);
|
||||||
|
const npmBin = join(dir, 'npm-bin');
|
||||||
|
const pnpmBin = join(dir, 'pnpm-bin');
|
||||||
|
mkdirSync(npmBin);
|
||||||
|
mkdirSync(pnpmBin);
|
||||||
|
const log = join(dir, 'npm.log');
|
||||||
|
writeFileSync(log, '');
|
||||||
|
writeFileSync(join(npmBin, 'npm'), '#!/bin/sh\necho "$*" >> "$NPM_LOG"\nexit "${NPM_EXIT:-0}"\n');
|
||||||
|
chmodSync(join(npmBin, 'npm'), 0o755);
|
||||||
|
writeFileSync(join(pnpmBin, 'pnpm'), '#!/bin/sh\nexit 0\n');
|
||||||
|
chmodSync(join(pnpmBin, 'pnpm'), 0o755);
|
||||||
|
const result = spawnSync('/bin/sh', ['-c', deepseekInstallCommand('linux')], {
|
||||||
|
env: {
|
||||||
|
PATH: opts.pnpmOnPath ? `${npmBin}:${pnpmBin}` : npmBin,
|
||||||
|
NPM_LOG: log,
|
||||||
|
NPM_EXIT: opts.npmFails ? '1' : '0',
|
||||||
|
},
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
const npmCalls = readFileSync(log, 'utf-8').split('\n').filter(Boolean);
|
||||||
|
return { status: result.status, npmCalls };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DeepSeek install', () => {
|
||||||
|
it('is the same command on linux and darwin', () => {
|
||||||
|
expect(deepseekInstallCommand('darwin')).toBe(deepseekInstallCommand('linux'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('installs pnpm with dsh when none is on PATH, since `dsh plugin` spawns a literal pnpm', () => {
|
||||||
|
const { status, npmCalls } = runInstall({ pnpmOnPath: false });
|
||||||
|
expect(status).toBe(0);
|
||||||
|
expect(npmCalls).toEqual(['install -g @deepseek-ai/dsh', 'install -g pnpm']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves an existing pnpm alone (a corepack shim makes `npm install -g pnpm` fail with EEXIST)', () => {
|
||||||
|
const { status, npmCalls } = runInstall({ pnpmOnPath: true });
|
||||||
|
expect(status).toBe(0);
|
||||||
|
expect(npmCalls).toEqual(['install -g @deepseek-ai/dsh']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports a failed dsh install and does not go on to pnpm', () => {
|
||||||
|
const { status, npmCalls } = runInstall({ pnpmOnPath: false, npmFails: true });
|
||||||
|
expect(status).not.toBe(0);
|
||||||
|
expect(npmCalls).toEqual(['install -g @deepseek-ai/dsh']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("puts dsh's own directory first on the profile install's PATH", () => {
|
||||||
|
const src = readFileSync(resolve(root, 'src/web/routes/system-routes.ts'), 'utf8');
|
||||||
|
expect(src).toMatch(/env: \{ \.\.\.process\.env, PATH: \[dir, process\.env\.PATH\]/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user