Skip to content

Commit ee37d46

Browse files
YOMXXXxxiaoxiong
authored andcommitted
fix(landing): serve hosted install.sh wrapper (nexu-io#4866)
1 parent 8e142c4 commit ee37d46

5 files changed

Lines changed: 241 additions & 0 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,15 @@ You can use Open Design without ever opening the GUI — call it as a skill, plu
305305
od mcp install <agent>
306306
# <agent> = claude | codex | cursor | copilot | openclaw | antigravity | gemini
307307
# | pi | vibe | hermes | cline | kimi | trae | opencode
308+
309+
# Hosted equivalent for curl-based setup:
310+
curl -fsSL https://open-design.ai/install.sh | sh -s <agent>
308311
```
309312

313+
`install.sh` is a thin shell wrapper around `od mcp install`; it exists so the
314+
hosted URL returns shell instead of the landing-page HTML fallback and fails
315+
fast if your shell resolves a non-Open-Design `od` binary.
316+
310317
> **WSL2 users:** If your coding-agent CLIs run inside WSL2, follow the
311318
> [`WSL2 setup guide`](docs/wsl-setup.md) first. Linux's `/usr/bin/od` can
312319
> shadow Open Design's `od` command.

apps/daemon/src/cli.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ const MCP_INSTALL_STRING_FLAGS = new Set([
9191
'daemon-url',
9292
'name',
9393
]);
94+
const MCP_INSTALL_CLI_PROBE_FLAG = 'open-design-cli-probe';
95+
const MCP_INSTALL_CLI_PROBE_TOKEN = 'open-design-cli:mcp-install:v1';
9496
const MCP_INSTALL_BOOLEAN_FLAGS = new Set([
9597
'help',
9698
'h',
99+
MCP_INSTALL_CLI_PROBE_FLAG,
97100
'json',
98101
'print',
99102
'dry-run',
@@ -1277,6 +1280,10 @@ async function runMcpInstall(args) {
12771280
printMcpInstallHelp();
12781281
process.exit(2);
12791282
}
1283+
if (flags[MCP_INSTALL_CLI_PROBE_FLAG]) {
1284+
console.log(MCP_INSTALL_CLI_PROBE_TOKEN);
1285+
return;
1286+
}
12801287
if (flags.help || flags.h) {
12811288
printMcpInstallHelp();
12821289
return;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { execFile } from 'node:child_process';
2+
import { dirname, resolve as pathResolve } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import { promisify } from 'node:util';
5+
import { describe, expect, it } from 'vitest';
6+
7+
const execFileP = promisify(execFile);
8+
const __dirname = dirname(fileURLToPath(import.meta.url));
9+
const DAEMON_ROOT = pathResolve(__dirname, '..');
10+
const REPO_ROOT = pathResolve(__dirname, '../../..');
11+
const CLI_SRC = pathResolve(__dirname, '../src/cli.ts');
12+
const TSX_CLI = pathResolve(REPO_ROOT, 'node_modules/tsx/dist/cli.mjs');
13+
14+
async function runCli(args: string[]): Promise<{ stdout: string; stderr: string; code: number | null }> {
15+
const env: NodeJS.ProcessEnv = { ...process.env };
16+
delete env.NODE_OPTIONS;
17+
try {
18+
const { stdout, stderr } = await execFileP(process.execPath, [TSX_CLI, CLI_SRC, ...args], {
19+
cwd: DAEMON_ROOT,
20+
env,
21+
timeout: 15_000,
22+
maxBuffer: 4 * 1024 * 1024,
23+
});
24+
return { stdout, stderr, code: 0 };
25+
} catch (err) {
26+
const failed = err as { stdout?: string; stderr?: string; code?: number | null };
27+
return {
28+
stdout: failed.stdout ?? '',
29+
stderr: failed.stderr ?? '',
30+
code: failed.code ?? 1,
31+
};
32+
}
33+
}
34+
35+
describe('od mcp install CLI identity probe', () => {
36+
it('emits a stable identity token without requiring an agent slug', async () => {
37+
const result = await runCli(['mcp', 'install', '--open-design-cli-probe']);
38+
39+
expect(result.code).toBe(0);
40+
expect(result.stderr).toBe('');
41+
expect(result.stdout).toBe('open-design-cli:mcp-install:v1\n');
42+
});
43+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env sh
2+
# Open Design MCP installer wrapper.
3+
#
4+
# This file is served verbatim by the static landing page at:
5+
# https://open-design.ai/install.sh
6+
#
7+
# It intentionally delegates to the product-owned installer:
8+
# od mcp install <agent>
9+
#
10+
# Keeping the real installer in the daemon avoids duplicating the per-agent
11+
# config planner in a hosted shell script. This wrapper exists so curl|sh no
12+
# longer receives the landing-page HTML fallback, and so users get a clear
13+
# error when the shell resolves /usr/bin/od or another non-Open-Design binary.
14+
15+
set -eu
16+
17+
usage() {
18+
cat <<'EOF'
19+
Open Design MCP installer
20+
21+
Usage:
22+
curl -fsSL https://open-design.ai/install.sh | sh -s <agent> [options]
23+
24+
This is a thin hosted wrapper around:
25+
od mcp install <agent> [options]
26+
27+
Examples:
28+
curl -fsSL https://open-design.ai/install.sh | sh -s codex --print
29+
curl -fsSL https://open-design.ai/install.sh | sh -s cursor --write-config
30+
31+
Options are forwarded to `od mcp install`. For the complete option list:
32+
od mcp install --help
33+
EOF
34+
}
35+
36+
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
37+
usage
38+
exit 0
39+
fi
40+
41+
if [ "$#" -eq 0 ]; then
42+
printf '%s\n\n' "Open Design install.sh: missing required <agent> argument." >&2
43+
usage >&2
44+
exit 2
45+
fi
46+
47+
if ! command -v od >/dev/null 2>&1; then
48+
cat >&2 <<'EOF'
49+
Open Design install.sh: `od` was not found on PATH.
50+
51+
Install and open the Open Design desktop app, or run the daemon from a source
52+
checkout so the Open Design CLI is available, then re-run this command.
53+
EOF
54+
exit 1
55+
fi
56+
57+
od_probe="$(od mcp install --open-design-cli-probe 2>/dev/null || true)"
58+
if [ "${od_probe}" != "open-design-cli:mcp-install:v1" ]; then
59+
od_path="$(command -v od || true)"
60+
cat >&2 <<EOF
61+
Open Design install.sh: '${od_path}' does not look like the Open Design CLI.
62+
63+
On Linux and WSL2, /usr/bin/od is usually the coreutils octal-dump command and
64+
can shadow Open Design's CLI. Put the Open Design CLI earlier on PATH, then
65+
re-run this command.
66+
EOF
67+
exit 1
68+
fi
69+
70+
exec od mcp install "$@"
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import assert from 'node:assert/strict';
2+
import { mkdtempSync, rmSync, writeFileSync, chmodSync, readFileSync } from 'node:fs';
3+
import { tmpdir } from 'node:os';
4+
import { join, resolve } from 'node:path';
5+
import { spawnSync } from 'node:child_process';
6+
import { test } from 'node:test';
7+
8+
const repoRoot = resolve(import.meta.dirname, '../../..');
9+
const installScript = join(repoRoot, 'apps/landing-page/public/install.sh');
10+
11+
function runInstall(args: string[], pathDir: string) {
12+
return spawnSync('sh', [installScript, ...args], {
13+
encoding: 'utf8',
14+
env: {
15+
...process.env,
16+
PATH: `${pathDir}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`,
17+
},
18+
});
19+
}
20+
21+
test('landing page serves install.sh as a shell script, not the HTML app fallback', () => {
22+
const body = readFileSync(installScript, 'utf8');
23+
24+
assert.match(body, /^#!\/usr\/bin\/env sh\n/);
25+
assert.match(body, /od mcp install/);
26+
assert.doesNotMatch(body, /<!doctype html/i);
27+
assert.doesNotMatch(body, /<html/i);
28+
});
29+
30+
test('install.sh delegates to the Open Design CLI installer with the requested agent', () => {
31+
const tmp = mkdtempSync(join(tmpdir(), 'od-install-sh-'));
32+
const argvOut = join(tmp, 'argv.txt');
33+
const fakeOd = join(tmp, 'od');
34+
writeFileSync(
35+
fakeOd,
36+
`#!/bin/sh
37+
if [ "$1" = "mcp" ] && [ "$2" = "install" ] && [ "$3" = "--help" ]; then
38+
printf '%s\\n' 'Usage text intentionally does not define CLI identity.'
39+
exit 0
40+
fi
41+
if [ "$1" = "mcp" ] && [ "$2" = "install" ] && [ "$3" = "--open-design-cli-probe" ]; then
42+
printf '%s\\n' 'open-design-cli:mcp-install:v1'
43+
exit 0
44+
fi
45+
printf '%s\\n' "$@" > "${argvOut}"
46+
`,
47+
'utf8',
48+
);
49+
chmodSync(fakeOd, 0o755);
50+
51+
try {
52+
const result = runInstall(['codex', '--print'], tmp);
53+
54+
assert.equal(result.status, 0, result.stderr);
55+
assert.equal(readFileSync(argvOut, 'utf8'), 'mcp\ninstall\ncodex\n--print\n');
56+
} finally {
57+
rmSync(tmp, { recursive: true, force: true });
58+
}
59+
});
60+
61+
test('install.sh rejects a shadowed od binary even when its help exits successfully', () => {
62+
const tmp = mkdtempSync(join(tmpdir(), 'od-install-sh-shadow-success-'));
63+
const argvOut = join(tmp, 'argv.txt');
64+
const fakeOd = join(tmp, 'od');
65+
writeFileSync(
66+
fakeOd,
67+
`#!/bin/sh
68+
if [ "$3" = "--help" ]; then
69+
printf '%s\\n' 'Usage: od [OPTION]... [FILE]...'
70+
exit 0
71+
fi
72+
if [ "$3" = "--open-design-cli-probe" ]; then
73+
printf '%s\\n' 'Usage: od [OPTION]... [FILE]...'
74+
exit 0
75+
fi
76+
printf '%s\\n' "$@" > "${argvOut}"
77+
exit 0
78+
`,
79+
'utf8',
80+
);
81+
chmodSync(fakeOd, 0o755);
82+
83+
try {
84+
const result = runInstall(['cursor'], tmp);
85+
86+
assert.equal(result.status, 1);
87+
assert.match(result.stderr, /does not look like the Open Design CLI/);
88+
assert.throws(() => readFileSync(argvOut, 'utf8'), /ENOENT/);
89+
} finally {
90+
rmSync(tmp, { recursive: true, force: true });
91+
}
92+
});
93+
94+
test('install.sh rejects a non-Open-Design od binary instead of calling coreutils od', () => {
95+
const tmp = mkdtempSync(join(tmpdir(), 'od-install-sh-shadow-'));
96+
const fakeOd = join(tmp, 'od');
97+
writeFileSync(
98+
fakeOd,
99+
`#!/bin/sh
100+
exit 1
101+
`,
102+
'utf8',
103+
);
104+
chmodSync(fakeOd, 0o755);
105+
106+
try {
107+
const result = runInstall(['cursor'], tmp);
108+
109+
assert.equal(result.status, 1);
110+
assert.match(result.stderr, /does not look like the Open Design CLI/);
111+
} finally {
112+
rmSync(tmp, { recursive: true, force: true });
113+
}
114+
});

0 commit comments

Comments
 (0)