@yosai/cli 0.2.1-test.11 → 0.2.1-test.12

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.
@@ -8,3 +8,4 @@ import './kb.js';
8
8
  import './skill.js';
9
9
  import './version.js';
10
10
  import './update.js';
11
+ import './install.js';
@@ -0,0 +1,126 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { register } from '../cli.js';
4
+ import { loadConfig, resolveChannel } from '../config.js';
5
+ import { BusinessError } from '../errors.js';
6
+ import { ensureVersionExists, INSTALLABLE_VERSION, installGlobal } from '../npm-install.js';
7
+ import { globalPackageDir, readPackageMeta, runNpm, runtimeFromNode } from '../npm-meta.js';
8
+ import { print, printJson } from '../output.js';
9
+ register({
10
+ name: 'install',
11
+ description: '把 CLI 安装到指定的全局位置(传入用户终端的 node/npm 路径,落点由该 node 决定)',
12
+ usage: 'yos install --node <path> [--npm <path>] [--channel latest|test] [--version <x.y.z>] [--json]',
13
+ group: '基础',
14
+ risk: 'write',
15
+ options: {
16
+ node: { type: 'string' },
17
+ npm: { type: 'string' },
18
+ channel: { type: 'string' },
19
+ version: { type: 'string' },
20
+ json: { type: 'boolean' },
21
+ },
22
+ paramNotes: [
23
+ ['--node <path>', '用户终端 node 的绝对路径(npm 全局落点由它决定)'],
24
+ ['--npm <path>', '用户终端 npm 的绝对路径;缺省从 node 同目录推断'],
25
+ ['--channel <latest|test>', '安装通道;缺省跟随当前服务环境(生产 latest / 测试 test)'],
26
+ ['--version <x.y.z>', '安装指定版本(与 --channel 互斥)'],
27
+ ['--json', '结构化输出'],
28
+ ],
29
+ async run({ flags }) {
30
+ const meta = readPackageMeta();
31
+ if (!meta) {
32
+ throw new BusinessError('无法确定包名(未找到 package.json)', '请通过 npm 全局安装后重试');
33
+ }
34
+ const version = typeof flags.version === 'string' ? flags.version : null;
35
+ const channelFlag = typeof flags.channel === 'string' ? flags.channel : null;
36
+ if (version !== null && channelFlag !== null) {
37
+ throw new BusinessError('--version 与 --channel 不能同时使用', `安装指定版本用 --version;安装通道最新版用 --channel(缺省跟随服务环境)`);
38
+ }
39
+ if (version !== null && !INSTALLABLE_VERSION.test(version)) {
40
+ throw new BusinessError(`版本格式非法:${version}`, '示例:0.2.0 或 0.2.1-test.9');
41
+ }
42
+ if (channelFlag !== null && channelFlag !== 'latest' && channelFlag !== 'test') {
43
+ throw new BusinessError(`通道非法:${channelFlag}`, '仅支持 latest(正式)或 test(测试)');
44
+ }
45
+ if (typeof flags.node !== 'string') {
46
+ throw new BusinessError('缺少 --node 参数(用户终端 node 的绝对路径)', '先用干净终端探测用户终端 node/npm 路径(如 env -i HOME="$HOME" TERM=xterm "$SHELL" -l -c "command -v node; command -v npm"),再以 --node <路径> 传入;探测不到时退回 npm install -g @yosai/cli 裸装');
47
+ }
48
+ const runtime = resolveRuntime(flags.node, typeof flags.npm === 'string' ? flags.npm : null);
49
+ if (version !== null) {
50
+ await ensureVersionExists(meta.name, version);
51
+ }
52
+ const channel = version === null ? (channelFlag ?? resolveChannel(loadConfig()) ?? 'latest') : null;
53
+ const spec = `${meta.name}@${version ?? channel}`;
54
+ await installGlobal(spec, { runtime });
55
+ const prefix = await queryGlobalPrefix(runtime);
56
+ const installedVersion = readInstalledVersion(prefix, meta.name);
57
+ const binEntry = cliBinEntry(prefix);
58
+ if (installedVersion === null || !existsSync(binEntry)) {
59
+ throw new BusinessError(`安装后未在 ${prefix} 找到可用的 ${meta.name}(包目录或 yos 入口缺失)`, `手动检查:ls ${prefix};也可在用户终端执行 npm ls -g ${meta.name} 查看`);
60
+ }
61
+ if (flags.json) {
62
+ printJson({
63
+ name: meta.name,
64
+ version: installedVersion,
65
+ prefix,
66
+ nodePath: runtime.nodePath,
67
+ npmPath: runtime.npmPath,
68
+ channel,
69
+ });
70
+ }
71
+ else {
72
+ print(`已安装 ${meta.name}@${installedVersion} 到 ${prefix}`);
73
+ print(`用户终端 node:${runtime.nodePath}`);
74
+ print('下一步:在用户终端执行 yos --version 验证命令可达,再执行 yos login 完成认证');
75
+ }
76
+ },
77
+ });
78
+ function resolveRuntime(nodeArg, npmArg) {
79
+ const nodePath = resolve(nodeArg);
80
+ if (!existsSync(nodePath) || !statSync(nodePath).isFile()) {
81
+ throw new BusinessError(`node 路径不存在或不是文件:${nodePath}`, '确认路径来自用户终端探测(command -v node),且指向 node 可执行文件');
82
+ }
83
+ if (npmArg !== null) {
84
+ const npmPath = resolveNpmJs(npmArg);
85
+ if (!existsSync(npmPath) || !statSync(npmPath).isFile()) {
86
+ throw new BusinessError(`npm 路径不存在或不是文件:${npmPath}`, '传入用户终端 npm 的绝对路径(command -v npm)');
87
+ }
88
+ return { nodePath, npmPath };
89
+ }
90
+ const runtime = runtimeFromNode(nodePath);
91
+ if (runtime === null) {
92
+ throw new BusinessError(`无法从 ${nodePath} 推断配套 npm`, '请显式传入 --npm <npm 绝对路径>(npm 是 Node 脚本,需与 node 位于同一环境)');
93
+ }
94
+ return runtime;
95
+ }
96
+ function resolveNpmJs(npmArg) {
97
+ const path = resolve(npmArg);
98
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(path)) {
99
+ return join(dirname(path), 'node_modules', 'npm', 'bin', 'npm-cli.js');
100
+ }
101
+ return path;
102
+ }
103
+ async function queryGlobalPrefix(runtime) {
104
+ const result = await runNpm(['prefix', '-g'], runtime);
105
+ const queried = result.ok ? (result.stdout.split(/\r?\n/)[0]?.trim() ?? '') : '';
106
+ if (queried !== '') {
107
+ return queried;
108
+ }
109
+ return process.platform === 'win32' ? dirname(runtime.nodePath) : dirname(dirname(runtime.nodePath));
110
+ }
111
+ function readInstalledVersion(prefix, name) {
112
+ const pkgJson = join(globalPackageDir(prefix, name), 'package.json');
113
+ if (!existsSync(pkgJson)) {
114
+ return null;
115
+ }
116
+ try {
117
+ const parsed = JSON.parse(readFileSync(pkgJson, 'utf8'));
118
+ return typeof parsed.version === 'string' ? parsed.version : null;
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ function cliBinEntry(prefix) {
125
+ return process.platform === 'win32' ? join(prefix, 'yos.cmd') : join(prefix, 'bin', 'yos');
126
+ }
@@ -1,7 +1,8 @@
1
1
  import { register } from '../cli.js';
2
2
  import { loadConfig, resolveChannel } from '../config.js';
3
3
  import { BusinessError } from '../errors.js';
4
- import { compareVersions, detectGlobalInstall, fetchLatestVersion, PUBLIC_REGISTRY, readPackageMeta, runNpm } from '../npm-meta.js';
4
+ import { ensureVersionExists, INSTALLABLE_VERSION, installGlobal } from '../npm-install.js';
5
+ import { compareVersions, detectGlobalInstall, fetchLatestVersion, PUBLIC_REGISTRY, readPackageMeta, resolveInstallPrefix } from '../npm-meta.js';
5
6
  import { print, printJson } from '../output.js';
6
7
  import { installSkillsTo } from '../skill-sync.js';
7
8
  import { saveVersionState } from '../version-state.js';
@@ -56,7 +57,6 @@ register({
56
57
  print('提示:重启 Agent 工具以加载刷新后的 Skill');
57
58
  },
58
59
  });
59
- const INSTALLABLE_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
60
60
  async function installVersion(meta, version) {
61
61
  if (!INSTALLABLE_VERSION.test(version)) {
62
62
  throw new BusinessError(`版本格式非法:${version}`, '示例:0.2.0 或 0.2.1-test.9;安装通道最新版请直接执行 yos update');
@@ -67,10 +67,7 @@ async function installVersion(meta, version) {
67
67
  print(`已是指定版本(${version})`);
68
68
  return;
69
69
  }
70
- const probe = await runNpm(['view', `${meta.name}@${version}`, 'version', `--registry=${PUBLIC_REGISTRY}`]);
71
- if (!probe.ok || probe.stdout.trim() === '') {
72
- throw new BusinessError(`npm registry 上不存在版本 ${version}`, `查看可用版本:npm view ${meta.name} versions --registry=${PUBLIC_REGISTRY}`);
73
- }
70
+ await ensureVersionExists(meta.name, version);
74
71
  await installGlobally(meta, `${meta.name}@${version}`);
75
72
  syncSkills();
76
73
  saveVersionState(version);
@@ -78,19 +75,17 @@ async function installVersion(meta, version) {
78
75
  print('提示:重启 Agent 工具以加载刷新后的 Skill');
79
76
  }
80
77
  async function installGlobally(meta, spec) {
78
+ const prefix = resolveInstallPrefix(meta);
79
+ if (prefix !== null) {
80
+ await installGlobal(spec, { prefix });
81
+ return;
82
+ }
81
83
  const shape = await detectGlobalInstall(meta);
82
84
  if (!shape.global) {
83
85
  print(`当前安装不在 npm 全局目录(${meta.dir}),不代跑安装`);
84
- throw new BusinessError('非全局安装,已跳过自动安装', `手动安装:npm install -g ${spec} --registry=${PUBLIC_REGISTRY};或先 npm uninstall -g ${meta.name} 再全局安装`);
85
- }
86
- const install = await runNpm(['install', '-g', spec, `--registry=${PUBLIC_REGISTRY}`]);
87
- if (!install.ok) {
88
- const output = `${install.stderr}\n${install.stdout}`;
89
- if (/EACCES|EPERM|permission denied/i.test(output)) {
90
- throw new BusinessError('npm 全局目录没有写权限', '把 npm 前缀改到用户目录后重试(npm config set prefix ~/.npm-global 并把其 bin 加入 PATH),不要 sudo npm install -g');
91
- }
92
- throw new BusinessError(`npm install -g ${spec} 失败:${firstLine(install.stderr) || firstLine(install.stdout) || '无输出'}`);
86
+ throw new BusinessError('非全局安装,已跳过自动安装', `手动安装:npm install -g ${spec} --registry=${PUBLIC_REGISTRY};或先 npm uninstall -g ${meta.name} 再全局安装;也可用 yos install --node <用户终端 node 路径> 装到指定位置`);
93
87
  }
88
+ await installGlobal(spec);
94
89
  }
95
90
  function syncSkills() {
96
91
  const dirs = loadConfig()?.installedSkillDirs ?? [];
@@ -107,6 +102,3 @@ function syncSkills() {
107
102
  }
108
103
  print(`已重同步 Skill:${dirs.join(', ')}`);
109
104
  }
110
- function firstLine(text) {
111
- return text.split(/\r?\n/)[0]?.trim() ?? '';
112
- }
@@ -1,7 +1,7 @@
1
1
  import { register } from '../cli.js';
2
2
  import { loadConfig, resolveChannel, resolveEnv } from '../config.js';
3
3
  import { BusinessError } from '../errors.js';
4
- import { compareVersions, detectGlobalInstall, fetchLatestVersion, readPackageMeta } from '../npm-meta.js';
4
+ import { compareVersions, fetchLatestVersion, isGlobalInstall, readPackageMeta } from '../npm-meta.js';
5
5
  import { print, printJson } from '../output.js';
6
6
  register({
7
7
  name: 'version',
@@ -21,7 +21,7 @@ register({
21
21
  const env = resolveEnv(config);
22
22
  if (!flags.check) {
23
23
  if (flags.json) {
24
- const installedGlobally = meta ? (await detectGlobalInstall(meta)).global : false;
24
+ const installedGlobally = meta ? await isGlobalInstall(meta) : false;
25
25
  printJson({ version, installedGlobally, env });
26
26
  }
27
27
  else {
@@ -36,7 +36,7 @@ register({
36
36
  const latest = await fetchLatestVersion(meta.name, channel);
37
37
  const outdated = compareVersions(meta.version, latest) < 0;
38
38
  if (flags.json) {
39
- printJson({ version, latest, outdated, installedGlobally: (await detectGlobalInstall(meta)).global, env, channel });
39
+ printJson({ version, latest, outdated, installedGlobally: await isGlobalInstall(meta), env, channel });
40
40
  return;
41
41
  }
42
42
  print(outdated
@@ -0,0 +1,31 @@
1
+ import { BusinessError } from './errors.js';
2
+ import { prefixRuntime, PUBLIC_REGISTRY, runNpm } from './npm-meta.js';
3
+ export const INSTALLABLE_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
4
+ export async function ensureVersionExists(name, version) {
5
+ const probe = await runNpm(['view', `${name}@${version}`, 'version', `--registry=${PUBLIC_REGISTRY}`]);
6
+ if (!probe.ok || probe.stdout.trim() === '') {
7
+ throw new BusinessError(`npm registry 上不存在版本 ${version}`, `查看可用版本:npm view ${name} versions --registry=${PUBLIC_REGISTRY}`);
8
+ }
9
+ }
10
+ export async function installGlobal(spec, target = {}) {
11
+ let runtime = target.runtime;
12
+ if (runtime === undefined && target.prefix !== undefined) {
13
+ runtime = prefixRuntime(target.prefix) ?? undefined;
14
+ }
15
+ const args = ['install', '-g', spec];
16
+ if (target.prefix !== undefined && runtime === undefined) {
17
+ args.push(`--prefix=${target.prefix}`);
18
+ }
19
+ args.push(`--registry=${PUBLIC_REGISTRY}`);
20
+ const install = runtime === undefined ? await runNpm(args) : await runNpm(args, runtime);
21
+ if (!install.ok) {
22
+ const output = `${install.stderr}\n${install.stdout}`;
23
+ if (/EACCES|EPERM|permission denied/i.test(output)) {
24
+ throw new BusinessError('npm 全局目录没有写权限', '把 npm 前缀改到用户目录后重试(npm config set prefix ~/.npm-global 并把其 bin 加入 PATH),不要 sudo npm install -g');
25
+ }
26
+ throw new BusinessError(`npm install -g ${spec} 失败:${firstLine(install.stderr) || firstLine(install.stdout) || '无输出'}`);
27
+ }
28
+ }
29
+ function firstLine(text) {
30
+ return text.split(/\r?\n/)[0]?.trim() ?? '';
31
+ }
package/dist/npm-meta.js CHANGED
@@ -28,16 +28,19 @@ export function getCliVersion() {
28
28
  cliVersionCache ??= readPackageMeta()?.version ?? 'unknown';
29
29
  return cliVersionCache;
30
30
  }
31
- export async function runNpm(args) {
32
- const useCmdWrapper = process.platform === 'win32';
33
- const command = useCmdWrapper ? (process.env.ComSpec ?? 'cmd.exe') : 'npm';
34
- const commandArgs = useCmdWrapper ? ['/d', '/s', '/c', 'npm', ...args] : args;
31
+ export async function runNpm(args, runtime) {
32
+ const useCmdWrapper = process.platform === 'win32' && runtime === undefined;
33
+ const command = runtime?.nodePath ?? (useCmdWrapper ? (process.env.ComSpec ?? 'cmd.exe') : 'npm');
34
+ const commandArgs = runtime !== undefined
35
+ ? [runtime.npmPath, ...args]
36
+ : useCmdWrapper ? ['/d', '/s', '/c', 'npm', ...args] : args;
35
37
  try {
36
38
  const { stdout, stderr } = await execFileAsync(command, commandArgs, {
37
39
  encoding: 'utf8',
38
40
  maxBuffer: 8 * 1024 * 1024,
39
41
  timeout: 30_000,
40
42
  windowsHide: true,
43
+ env: cleanNpmEnv(),
41
44
  });
42
45
  return { ok: true, stdout: `${stdout}`.trim(), stderr: `${stderr}`.trim() };
43
46
  }
@@ -50,6 +53,15 @@ export async function runNpm(args) {
50
53
  };
51
54
  }
52
55
  }
56
+ function cleanNpmEnv() {
57
+ const env = {};
58
+ for (const [key, value] of Object.entries(process.env)) {
59
+ if (!key.toLowerCase().startsWith('npm_config_')) {
60
+ env[key] = value;
61
+ }
62
+ }
63
+ return env;
64
+ }
53
65
  export async function fetchLatestVersion(name, channel = 'latest') {
54
66
  const result = await runNpm(['view', `${name}@${channel}`, 'version', `--registry=${PUBLIC_REGISTRY}`]);
55
67
  if (!result.ok) {
@@ -152,6 +164,66 @@ export function isPackageUnderPrefix(packageDir, prefix) {
152
164
  const base = stripTrailingSep(realpathOrSelf(prefix));
153
165
  return dir === base || dir.startsWith(`${base}${sep}`);
154
166
  }
167
+ export async function isGlobalInstall(meta) {
168
+ if (resolveInstallPrefix(meta) !== null) {
169
+ return true;
170
+ }
171
+ return (await detectGlobalInstall(meta)).global;
172
+ }
173
+ export function resolveInstallPrefix(meta) {
174
+ const dir = stripTrailingSep(realpathOrSelf(meta.dir));
175
+ const suffix = process.platform === 'win32'
176
+ ? join('node_modules', meta.name)
177
+ : join('lib', 'node_modules', meta.name);
178
+ if (!dir.endsWith(`${sep}${suffix}`)) {
179
+ return null;
180
+ }
181
+ const prefix = dir.slice(0, dir.length - suffix.length - 1);
182
+ if (prefix === '' || prefix.split(sep).includes('_npx')) {
183
+ return null;
184
+ }
185
+ return binEntryPointsTo(prefix, dir) ? prefix : null;
186
+ }
187
+ export function prefixRuntime(prefix) {
188
+ const nodePath = process.platform === 'win32' ? join(prefix, 'node.exe') : join(prefix, 'bin', 'node');
189
+ const npmPath = process.platform === 'win32'
190
+ ? join(prefix, 'node_modules', 'npm', 'bin', 'npm-cli.js')
191
+ : join(prefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js');
192
+ if (!existsSync(nodePath) || !existsSync(npmPath)) {
193
+ return null;
194
+ }
195
+ return { nodePath, npmPath };
196
+ }
197
+ export function runtimeFromNode(nodePath) {
198
+ const dir = dirname(nodePath);
199
+ if (process.platform !== 'win32') {
200
+ const sibling = join(dir, 'npm');
201
+ if (existsSync(sibling)) {
202
+ return { nodePath, npmPath: sibling };
203
+ }
204
+ }
205
+ const prefix = process.platform === 'win32' ? dir : dirname(dir);
206
+ const runtime = prefixRuntime(prefix);
207
+ const sameNode = runtime !== null
208
+ && stripTrailingSep(realpathOrSelf(runtime.nodePath)) === stripTrailingSep(realpathOrSelf(nodePath));
209
+ return sameNode ? runtime : null;
210
+ }
211
+ export function globalPackageDir(prefix, name) {
212
+ return process.platform === 'win32'
213
+ ? join(prefix, 'node_modules', name)
214
+ : join(prefix, 'lib', 'node_modules', name);
215
+ }
216
+ function binEntryPointsTo(prefix, packageDir) {
217
+ if (process.platform === 'win32') {
218
+ return existsSync(join(prefix, 'yos.cmd'));
219
+ }
220
+ const entry = join(prefix, 'bin', 'yos');
221
+ if (!existsSync(entry)) {
222
+ return false;
223
+ }
224
+ const target = stripTrailingSep(realpathOrSelf(entry));
225
+ return target.startsWith(`${packageDir}${sep}`);
226
+ }
155
227
  function realpathOrSelf(path) {
156
228
  try {
157
229
  return realpathSync(path);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yos-install
3
- description: 引导用户在本机安装并配置 YOS CLI(环境检查 → npm 全局安装 → 登录访问令牌 → 安装内置 Skill → doctor 验证)。用户提到 yos/YOS 且需要安装或修复 CLI 环境(yos 命令不存在、yos doctor 未通过、要装 CLI / 配置令牌 / 装不上)时使用。本文件自包含,可单独放到发布仓库供用户下载后完成引导
3
+ description: 引导用户在本机安装并配置 YOS CLI(环境检查 → 全局安装 → 登录访问令牌 → 安装内置 Skill → doctor 验证)。用户提到 yos/YOS 且需要安装或修复 CLI 环境(yos 命令不存在、yos doctor 未通过、要装 CLI / 配置令牌 / 装不上)时使用。本文件自包含,可单独放到发布仓库供用户下载后完成引导
4
4
  ---
5
5
 
6
6
  # YOS CLI 安装与配置引导
@@ -15,52 +15,77 @@ description: 引导用户在本机安装并配置 YOS CLI(环境检查 → npm
15
15
  - 需要访问令牌时:优先让用户自己在终端输入(`yos login` 的输入不回显);确需 Agent 代跑时用 `--key`/环境变量
16
16
  - **凭据零外泄**:访问令牌不写进代码仓库、日志、聊天正文或文档;引用时一律掩码(形如 `xxxxxxxx****xxxx`,即原令牌前 8 位 + `****` + 后 4 位)
17
17
  - 不擅自改用户的全局配置:写 `~/.npmrc`、改 `npm prefix`、删 skill 目录这类动作先取得用户同意
18
- - 全程不需要 `sudo`。权限报错按第 7 节处置,不要 `sudo npm install -g`(会把 root 拥有的文件混进全局目录,后续更难维护)
18
+ - 全程不需要 `sudo`。权限报错按第 7 步处置,不要 `sudo npm install -g`(会把 root 拥有的文件混进全局目录,后续更难维护)
19
19
 
20
- ## 1. 环境检查
20
+ ## 1. 安装前检查
21
+
22
+ ### 检查 Node 与 npm
21
23
 
22
24
  ```bash
23
25
  node -v # 需要 >= 18(package.json engines: ">=18";低版本会因 ESM/parseArgs 报错)
24
26
  npm -v
25
- command -v yos # 已有 yos → 跳到第 3 步(登录),不要重复安装
27
+ yos --version # 已有输出不代表装好了——仍要做第 2 步的落点确认与验收;报未知命令 → 更早的旧版本
26
28
  ```
27
29
 
28
30
  - Node 缺失或 < 18:让用户先装 Node 18+(LTS 均可),装完重开终端确认 `node -v`
29
- - 已有 `yos` 但版本旧:走第 6 节
31
+ - 已有 `yos` 但版本旧:走第 2 步覆盖安装(或第 6 步升级)
30
32
 
31
33
  ## 2. 安装 CLI
32
34
 
33
- 先做**落点判定**——`npm install -g` 的落点由**执行它的那个 node** 决定。Agent 工具常自带托管 Node,直接装会把 `yos` 送进 Agent 私有目录(路径含 `.workbuddy`/`.cursor`/`.qoder`/`.claude`/`.trae` 等隐藏目录,或含 `versions/<版本号>`):你这边能跑、用户终端却 `command not found`,工具升级或清缓存后 CLI 还会无声消失。
35
+ ### 目标与坑(先读)
36
+
37
+ 目标是把 CLI 装到**用户自己终端可达的全局位置**——用户新开终端执行 `yos --version` 能出版本号。`npm install -g` 的落点由**执行它的那个 node** 决定:你(Agent)自带的托管 Node 会把 `yos` 装进 Agent 私有目录(路径含 `.workbuddy`/`.cursor`/`.qoder`/`.claude`/`.trae` 等隐藏目录,或含 `versions/<版本号>`),你这边能跑、用户终端却 `command not found`,工具升级或清缓存后 CLI 还会无声消失。落点如何保证由你判断,常用做法见下。
34
38
 
35
- macOS / Linux:
39
+ ### 安装
36
40
 
37
41
  ```bash
38
- npm prefix -g # ① 你这边(Agent)的落点
39
- env -i HOME="$HOME" TERM=xterm "$SHELL" -l -c 'command -v node; npm prefix -g' # ② 用户终端视角
42
+ npm install -g @yosai/cli
40
43
  ```
41
44
 
42
- - `env -i` 不可省略:普通登录 shell 会继承你进程的 PATH,两边恒等是假阳性;`$SHELL` 为空时改试 `/bin/zsh`、`/bin/bash`;② 查不到 node(nvm 场景)改用 `-i -l`
43
- - ① ② 一致 → 直接装;不一致,或 ① 落在 Agent 工具目录 → 用 ② 查到的**用户终端 node 绝对路径**装(npm 取同目录),如 `/usr/local/bin/node /usr/local/bin/npm install -g @yosai/cli`
44
- - ② 查不到 node → 用户机器没有全局 Node:引导用户装 Node 18+ 并重开终端后复检;**不要**退而装进 Agent 私有目录,那样用户终端永远用不了
45
+ - 你与用户终端是**同一套 Node**(下方干净探测出的 node 路径与你自己的 `command -v node` 一致)→ 直接执行上面的命令
46
+ - 不是同一套或拿不准 → 探测用户终端的 node 与 npm 绝对路径,**用该 node 直接跑该 npm 安装**(落点由该 node 决定):
45
47
 
46
- Windows(PowerShell,读注册表 PATH 才不受你进程 PATH 污染):
48
+ ```bash
49
+ env -i HOME="$HOME" TERM=xterm "$SHELL" -l -c 'command -v node; command -v npm; command -v yos' # 探测($SHELL 空改试 /bin/zsh、/bin/bash;nvm 场景加 -i -l)
50
+ /usr/local/bin/node /usr/local/bin/npm install -g @yosai/cli # 换成探测到的实际路径
51
+ ```
52
+
53
+ - `env -i` 不可省略:普通登录 shell 会继承你进程的 PATH,探测结果是你自己的,是假阳性
54
+ - 想省事 → `yos install --node <NODE_PATH> --npm <NPM_PATH>`(CLI 自带安装命令,自动安装并校验落点;用户终端还没有 `yos` 时用 `npx -y @yosai/cli install --node <NODE_PATH> --npm <NPM_PATH>`,npx 拉到的版本报「未知命令: install」就退回上面的做法)
55
+
56
+ Windows(PowerShell,读注册表 PATH 才不受你进程 PATH 污染;每新开会话重跑赋值块):
47
57
 
48
58
  ```powershell
49
59
  $clean = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
50
60
  $nodeDir = ($clean -split ';' | Where-Object { $_ -and (Test-Path (Join-Path $_ 'node.exe')) } | Select-Object -First 1)
51
- & (Join-Path $nodeDir 'npm.cmd') install -g '@yosai/cli'
61
+ # 用用户终端的 node 直接跑 npm 的 JS 入口(npm.cmd 依赖 PATH 里的 node,锁不住落点)
62
+ & (Join-Path $nodeDir 'node.exe') (Join-Path $nodeDir 'node_modules\npm\bin\npm-cli.js') install -g '@yosai/cli'
63
+ # 或用 CLI 自带安装命令:
64
+ npx -y @yosai/cli install --node (Join-Path $nodeDir 'node.exe') --npm (Join-Path $nodeDir 'npm.cmd')
65
+ ```
66
+
67
+ ### 装完验收(硬性)
68
+
69
+ 你这边通过不算数,必须再做**用户终端视角**校验——这条也输出版本号才算装好:
70
+
71
+ ```bash
72
+ command -v yos && yos --version # 你这边
73
+ env -i HOME="$HOME" TERM=xterm "$SHELL" -l -c 'command -v yos && yos --version' # 用户终端视角(干净环境)
52
74
  ```
53
75
 
54
- 装完立刻验证(最后一条是用户终端视角的复查,通过才算真的装好):
76
+ - 干净环境查不到 → 装进了 Agent 私有目录:按上方常用做法重装到用户终端落点,并清掉私产那份(第 7 步)
77
+ - `command -v yos` 指向 Agent 私产目录(你这边 PATH 里私产优先)→ 同样先清私产再校验,避免旧版本干扰判断
78
+
79
+ ### 装不进全局时的兜底
80
+
81
+ 用户终端探测不到 node 且不便引导安装、权限问题(EACCES)无法解决时,可退而直接安装(落点为 Agent 私有目录):
55
82
 
56
83
  ```bash
57
- command -v yos # 全局 bin 路径
58
- yos --version # 输出「yos CLI 0.2.0」即安装成功(等价 yos version)
59
- yos --help # 输出「yos - YOS 能力的 Agent 执行入口」
60
- env -i HOME="$HOME" TERM=xterm "$SHELL" -l -c 'command -v yos && yos --version' # 干净终端同样输出版本号才算装好
84
+ npm install -g @yosai/cli # 用你自己的 npm
61
85
  ```
62
86
 
63
- - 干净终端查不到即装进了 Agent 私有目录 → 用 ② 查到的 node 重装;重装后在 Agent 侧清掉私产那份(破坏性操作,先取得用户同意)
87
+ - 此时仅你(Agent)这侧可用,用户终端 `command not found`——**必须如实告知用户这一影响**,不要略过
88
+ - 用户终端后续具备条件(装好 Node 等)后,按上方常用做法装到全局,再卸掉私产这份(`npm uninstall -g @yosai/cli`,破坏性操作先取得用户同意)
64
89
 
65
90
  ## 3. 登录(唯一需要用户配合的动作)
66
91
 
@@ -149,12 +174,13 @@ CLI 平时无需手动检查升级:每次命令执行前会检查对应通道
149
174
  yos update # 看到落后提示或需要立即升级时执行(到当前通道最新版并自动重同步 Skill)
150
175
  yos update --check # 只查是否有新版本,不安装
151
176
  yos update --version <x.y.z> # 安装指定版本(可升可降,用于回退或锁定版本)
152
- npm install -g @yosai/cli@latest # 等价手动写法(正式版);走这条路后需再跑 yos skill install 刷新 Skill
177
+ yos install --node <NODE_PATH> --npm <NPM_PATH> # 手动重装到用户终端落点(见第 2 步)
153
178
  ```
154
179
 
155
- (0.2.0 之前的版本没有 `yos update`,直接用 `npm install -g @yosai/cli@latest` 升级)
156
-
157
- 以上所有 `npm install -g`(含 `yos update` 内部调用)同样受第 2 节落点约束:落点不一致时会把新版装进 Agent 私有目录——改用用户终端 node 对应的 npm 重装,装完重跑可达性校验。
180
+ - `yos update` 会按当前安装位置反推全局前缀,用该前缀配套的 node / npm 直装,不依赖 PATH 里 npm 的指向
181
+ - 报「非全局安装」跳过时(安装形态特殊,如 npx 缓存目录、源码链接等):按第 2 步常用做法重装到用户终端落点,装完重跑可达性校验
182
+ - 0.2.0 之前的版本没有 `yos update`:按第 2 步覆盖安装最新版
183
+ - 手工 `npm install -g @yosai/cli@latest` 会把新版装进**你(Agent)这边** Node 的前缀(可能正是私产落点)——除非确认两边同一套 Node,否则以 `yos update` / `yos install` 为准
158
184
 
159
185
  卸载(破坏性,逐项取得用户确认后再动手):
160
186
 
@@ -169,14 +195,15 @@ ls ~/.claude/skills | grep -E '^yos' # 先看要删哪些,
169
195
  | 看到的原文 | 原因 | 处置 |
170
196
  |---|---|---|
171
197
  | `command not found: yos` | 未安装,或全局 bin 不在 PATH | 先 `npm ls -g @yosai/cli` 判断是否装上;装上了就查 `npm prefix -g` 的 `/bin` 是否在 PATH,改 `~/.zshrc` 后重开终端 |
172
- | 你这边 `yos --version` 正常,用户终端却 `command not found: yos` | 装进了 Agent 托管 Node 的私有 prefix(路径常带版本号,只在 Agent 进程里进 PATH) | 按第 2 节落点判定,用用户终端 node 对应的 npm 重装;重装后清掉私产那份(破坏性操作,先取得用户同意) |
198
+ | 你这边 `yos --version` 正常,用户终端却 `command not found: yos` | 装进了 Agent 托管 Node 的私有 prefix(路径常带版本号,只在 Agent 进程里进 PATH) | 按第 2 步常用做法重装到用户终端落点(探测 node/npm 绝对路径后用该 node 直接跑该 npm,或 `yos install --node <路径> --npm <路径>`);重装后清掉私产那份(破坏性操作,先取得用户同意)。不便重装时可保持私产形态继续用,但须告知用户「仅 Agent 侧可用」 |
199
+ | `未知命令: install`(退出码 2) | 当前装的 CLI 版本还没有 install 命令 | 按第 2 步常用做法(绝对路径 node+npm 或裸装)安装 / 升级新版后再用 |
173
200
  | `npm error code E404` / `'@yosai/cli' is not in this registry` | 包名拼错、该版本尚未发布到 npmjs,或 npm 镜像未同步 | 核对包名 `@yosai/cli`;已发布仍 404 多为镜像未同步,加 `--registry=https://registry.npmjs.org` 重试;仍有问题联系维护者 |
174
201
  | `npm error code EACCES`(写全局目录失败) | npm 全局前缀指向了 root 目录 | 让用户同意后再改前缀:`npm config set prefix ~/.npm-global`,把 `~/.npm-global/bin` 加进 PATH,重开终端重装。**不要 sudo** |
175
202
  | `错误: 令牌格式不正确(应为 40 位小写 hex)` + `提示: 请在客户端「个人设置-访问令牌」页创建并复制完整令牌` | 令牌长度不对 / 大写 / 粘了空格或前缀 | 让用户重新整段复制(40 位、仅 `0-9a-f`);不要让用户逐字补 |
176
203
  | `错误: 无法连接 YOS 服务,原因:fetch failed` | 服务不可达(网络受限、服务异常;测试/开发装机也可能环境未切换) | 先 `yos ping` 复测并检查网络;测试/开发装机环境选错用 `yos login --env test` 重配;网络问题找运维确认 |
177
204
  | `未配置访问令牌` + `请执行 yos login 或设置环境变量 YOS_API_KEY` | 没配置也没 env | 回第 3 步 |
178
205
  | `错误: 未发现已安装的 Agent 技能目录` | 本机没有已知的 AI 工具 Skill 目录 | `yos skill install --target <该工具 skills 目录>`,见第 4 步 |
179
- | `yos --version` 无输出或报未知命令 | 装的是 0.2.0 之前的旧版本(旧版没有 version/update 命令) | 先 `npm install -g @yosai/cli@latest` 升到最新版(≥0.2.0),之后即可用 `yos version` / `yos update` |
206
+ | `yos --version` 无输出或报未知命令 | 装的是 0.2.0 之前的旧版本(旧版没有 version/update/install 命令) | 按第 2 步覆盖安装最新版(≥0.2.0),之后即可用 `yos version` / `yos update` / `yos install` |
180
207
  | 装完 Skill 后 AI 工具不认 YOS 能力 | 没重启工具,或 Skill 装到了别的工具目录 | 重启对应工具;`ls` 确认该工具目录下有 `yos/SKILL.md`,必要时 `--target` 重装 |
181
208
 
182
209
  ## 8. 装好之后
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yosai/cli",
3
- "version": "0.2.1-test.11",
3
+ "version": "0.2.1-test.12",
4
4
  "description": "YOS 能力的 Agent 执行入口:工具调用、知识库管理与文件上传",
5
5
  "type": "module",
6
6
  "bin": {