@chuhe2640/ahsopus-cli 0.1.0 → 0.1.3

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.
package/README.md CHANGED
@@ -75,7 +75,7 @@ go test -race ./...
75
75
  go build ./cmd/ahsops
76
76
  ```
77
77
 
78
- 依赖下载、Go 安装和正式签名需要在受控开发或 CI 环境中执行。本仓库 CI 负责格式、服务端CLI契约、测试、race 和三目标交叉构建;正式 Release 只能从`main`手工触发,要求名为`ahsops-cli-release`的受保护 GitHub Environment、人工审批以及其中配置的macOS与Windows签名Secret。同一SemVer的已发布签名产物不可覆盖。
78
+ 本仓库不再使用 GitHub Actions CLI 测试或签名发行。格式、契约和 `go test` 在本机执行。用户安装走 npm:`npx @chuhe2640/ahsopus-cli@latest install`。
79
79
 
80
80
  ## 验证边界
81
81
 
package/bin/cli.js CHANGED
@@ -4,8 +4,9 @@ import { existsSync } from 'node:fs'
4
4
  import { install } from '../npm/lib/install.js'
5
5
  import { nativeBinaryPath, placeBinary } from '../npm/lib/place-binary.js'
6
6
 
7
- const args = process.argv.slice(2)
8
- if (args[0] === 'install') {
7
+ const args = process.argv.slice(2).filter((value) => value !== '--')
8
+ const command = args.find((value) => !value.startsWith('-')) ?? 'install'
9
+ if (command === 'install') {
9
10
  try {
10
11
  await install()
11
12
  } catch (error) {
@@ -0,0 +1,74 @@
1
+ [
2
+ {"id":"aider-desk","name":"AiderDesk","detect":["~/.aider-desk"],"skills":["~/.aider-desk/skills"]},
3
+ {"id":"amp","name":"Amp","detect":["~config/amp"],"skills":["~config/agents/skills"]},
4
+ {"id":"antigravity","name":"Antigravity","detect":["~/.gemini/antigravity"],"skills":["~/.gemini/antigravity/skills"]},
5
+ {"id":"antigravity-cli","name":"Antigravity CLI","detect":["~/.gemini/antigravity-cli"],"skills":["~/.gemini/antigravity-cli/skills"]},
6
+ {"id":"astrbot","name":"AstrBot","detect":["~/.astrbot"],"skills":["~/.astrbot/data/skills"]},
7
+ {"id":"autohand-code","name":"Autohand Code CLI","detect":["$AUTOHAND_HOME","~/.autohand"],"skills":["$AUTOHAND_HOME/skills","~/.autohand/skills"]},
8
+ {"id":"augment","name":"Augment","detect":["~/.augment"],"skills":["~/.augment/skills"]},
9
+ {"id":"bob","name":"IBM Bob","detect":["~/.bob"],"skills":["~/.bob/skills"]},
10
+ {"id":"claude-code","name":"Claude Code","detect":["$CLAUDE_CONFIG_DIR","~/.claude"],"skills":["$CLAUDE_CONFIG_DIR/skills","~/.claude/skills"]},
11
+ {"id":"openclaw","name":"OpenClaw","detect":["~/.openclaw","~/.clawdbot","~/.moltbot"],"skills":["~/.openclaw/skills"]},
12
+ {"id":"cline","name":"Cline","detect":["~/.cline"],"skills":["~/.agents/skills"]},
13
+ {"id":"codearts-agent","name":"CodeArts Agent","detect":["~/.codeartsdoer"],"skills":["~/.codeartsdoer/skills"]},
14
+ {"id":"codebuddy","name":"CodeBuddy","detect":["~/.codebuddy"],"skills":["~/.codebuddy/skills"]},
15
+ {"id":"codemaker","name":"Codemaker","detect":["~/.codemaker"],"skills":["~/.codemaker/skills"]},
16
+ {"id":"codestudio","name":"Code Studio","detect":["~/.codestudio"],"skills":["~/.codestudio/skills"]},
17
+ {"id":"codex","name":"Codex","detect":["$CODEX_HOME","~/.codex"],"skills":["$CODEX_HOME/skills","~/.codex/skills"]},
18
+ {"id":"command-code","name":"Command Code","detect":["~/.commandcode"],"skills":["~/.commandcode/skills"]},
19
+ {"id":"continue","name":"Continue","detect":["~/.continue"],"skills":["~/.continue/skills"]},
20
+ {"id":"cortex","name":"Cortex Code","detect":["~/.snowflake/cortex"],"skills":["~/.snowflake/cortex/skills"]},
21
+ {"id":"crush","name":"Crush","detect":["~config/crush"],"skills":["~config/crush/skills"]},
22
+ {"id":"cursor","name":"Cursor","detect":["~/.cursor"],"skills":["~/.cursor/skills"]},
23
+ {"id":"deepagents","name":"Deep Agents","detect":["~/.deepagents"],"skills":["~/.deepagents/agent/skills"]},
24
+ {"id":"devin","name":"Devin for Terminal","detect":["~config/devin"],"skills":["~config/devin/skills"]},
25
+ {"id":"dexto","name":"Dexto","detect":["~/.dexto"],"skills":["~/.agents/skills"]},
26
+ {"id":"droid","name":"Droid","detect":["~/.factory"],"skills":["~/.factory/skills"]},
27
+ {"id":"firebender","name":"Firebender","detect":["~/.firebender"],"skills":["~/.firebender/skills"]},
28
+ {"id":"forgecode","name":"ForgeCode","detect":["~/.forge"],"skills":["~/.forge/skills"]},
29
+ {"id":"gemini-cli","name":"Gemini CLI","detect":["~/.gemini"],"skills":["~/.gemini/skills"]},
30
+ {"id":"github-copilot","name":"GitHub Copilot","detect":["~/.copilot"],"skills":["~/.copilot/skills"]},
31
+ {"id":"goose","name":"Goose","detect":["~config/goose"],"skills":["~config/goose/skills"]},
32
+ {"id":"grok","name":"Grok","detect":["$GROK_HOME","~/.grok"],"skills":["$GROK_HOME/skills","~/.grok/skills"]},
33
+ {"id":"hermes-agent","name":"Hermes Agent","detect":["$HERMES_HOME","~/.hermes"],"skills":["$HERMES_HOME/skills","~/.hermes/skills"]},
34
+ {"id":"inference-sh","name":"inference.sh","detect":["~/.inferencesh"],"skills":["~/.inferencesh/skills"]},
35
+ {"id":"jazz","name":"Jazz","detect":["~/.jazz"],"skills":["~/.jazz/skills"]},
36
+ {"id":"junie","name":"Junie","detect":["~/.junie"],"skills":["~/.junie/skills"]},
37
+ {"id":"iflow-cli","name":"iFlow CLI","detect":["~/.iflow"],"skills":["~/.iflow/skills"]},
38
+ {"id":"kilo","name":"Kilo Code","detect":["~/.kilocode"],"skills":["~/.kilocode/skills"]},
39
+ {"id":"kimchi","name":"Kimchi","detect":["~config/kimchi"],"skills":["~config/kimchi/harness/skills"]},
40
+ {"id":"kimi-code-cli","name":"Kimi Code CLI","detect":["~/.kimi-code","~/.kimi"],"skills":["~/.agents/skills"]},
41
+ {"id":"kiro-cli","name":"Kiro CLI","detect":["~/.kiro"],"skills":["~/.kiro/skills"]},
42
+ {"id":"kode","name":"Kode","detect":["~/.kode"],"skills":["~/.kode/skills"]},
43
+ {"id":"lingma","name":"Lingma","detect":["~/.lingma"],"skills":["~/.lingma/skills"]},
44
+ {"id":"loaf","name":"Loaf","detect":["~/.loaf"],"skills":["~/.agents/skills"]},
45
+ {"id":"mcpjam","name":"MCPJam","detect":["~/.mcpjam"],"skills":["~/.mcpjam/skills"]},
46
+ {"id":"minimax-code","name":"MiniMax Code","detect":["~/.minimax"],"skills":["~/.minimax/skills"]},
47
+ {"id":"mistral-vibe","name":"Mistral Vibe","detect":["$VIBE_HOME","~/.vibe"],"skills":["$VIBE_HOME/skills","~/.vibe/skills"]},
48
+ {"id":"moxby","name":"Moxby","detect":["~/.moxby"],"skills":["~/.moxby/skills"]},
49
+ {"id":"mux","name":"Mux","detect":["~/.mux"],"skills":["~/.mux/skills"]},
50
+ {"id":"opencode","name":"OpenCode","detect":["~config/opencode"],"skills":["~config/opencode/skills"]},
51
+ {"id":"openhands","name":"OpenHands","detect":["~/.openhands"],"skills":["~/.openhands/skills"]},
52
+ {"id":"ona","name":"Ona","detect":["~/.ona"],"skills":["~/.ona/skills"]},
53
+ {"id":"pi","name":"Pi","detect":["~/.pi/agent"],"skills":["~/.pi/agent/skills"]},
54
+ {"id":"posit-assistant","name":"Posit Assistant","detect":["~/.posit"],"skills":["~/.posit/assistant/skills"]},
55
+ {"id":"qoder","name":"Qoder","detect":["~/.qoder"],"skills":["~/.qoder/skills"]},
56
+ {"id":"qoder-cn","name":"Qoder CN","detect":["~/.qoder-cn"],"skills":["~/.qoder-cn/skills"]},
57
+ {"id":"qwen-code","name":"Qwen Code","detect":["~/.qwen"],"skills":["~/.qwen/skills"]},
58
+ {"id":"reasonix","name":"Reasonix","detect":["~/.reasonix"],"skills":["~/.reasonix/skills"]},
59
+ {"id":"rovodev","name":"Rovo Dev","detect":["~/.rovodev"],"skills":["~/.rovodev/skills"]},
60
+ {"id":"roo","name":"Roo Code","detect":["~/.roo"],"skills":["~/.roo/skills"]},
61
+ {"id":"tabnine-cli","name":"Tabnine CLI","detect":["~/.tabnine"],"skills":["~/.tabnine/agent/skills"]},
62
+ {"id":"terramind","name":"Terramind","detect":["~/.terramind"],"skills":["~/.terramind/skills"]},
63
+ {"id":"tinycloud","name":"Tinycloud","detect":["~/.tinycloud"],"skills":["~/.tinycloud/skills"]},
64
+ {"id":"trae","name":"Trae","detect":["~/.trae"],"skills":["~/.trae/skills"]},
65
+ {"id":"trae-cn","name":"Trae CN","detect":["~/.trae-cn"],"skills":["~/.trae-cn/skills"]},
66
+ {"id":"warp","name":"Warp","detect":["~/.warp"],"skills":["~/.agents/skills"]},
67
+ {"id":"windsurf","name":"Windsurf","detect":["~/.codeium/windsurf"],"skills":["~/.codeium/windsurf/skills"]},
68
+ {"id":"zed","name":"Zed","detect":["~config/zed"],"skills":["~/.agents/skills"]},
69
+ {"id":"zcode","name":"ZCode","detect":["~/.zcode"],"skills":["~/.zcode/skills"]},
70
+ {"id":"zencoder","name":"Zencoder","detect":["~/.zencoder"],"skills":["~/.zencoder/skills"]},
71
+ {"id":"neovate","name":"Neovate","detect":["~/.neovate"],"skills":["~/.neovate/skills"]},
72
+ {"id":"pochi","name":"Pochi","detect":["~/.pochi"],"skills":["~/.pochi/skills"]},
73
+ {"id":"adal","name":"AdaL","detect":["~/.adal"],"skills":["~/.adal/skills"]}
74
+ ]
@@ -0,0 +1,59 @@
1
+ // INPUT: agents.json(飞书 CLI npx skills add -g 的完整客户端目录)
2
+ // OUTPUT: 可检测的本机 Agent 与展示名
3
+ // POS: 安装向导与飞书 skills CLI 客户端目录对齐
4
+
5
+ import { existsSync, readFileSync } from 'node:fs'
6
+ import { homedir } from 'node:os'
7
+ import path from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ function loadCatalog() {
11
+ const here = path.dirname(fileURLToPath(import.meta.url))
12
+ const candidates = [
13
+ path.join(here, '../../internal/skills/agents.json'),
14
+ path.join(here, 'agents.json'),
15
+ ]
16
+ for (const file of candidates) {
17
+ if (existsSync(file)) {
18
+ return JSON.parse(readFileSync(file, 'utf8'))
19
+ }
20
+ }
21
+ throw new Error('未找到 agents.json')
22
+ }
23
+
24
+ export const AGENTS = loadCatalog()
25
+
26
+ function configHome(home) {
27
+ const value = process.env.XDG_CONFIG_HOME?.trim()
28
+ return value || path.join(home, '.config')
29
+ }
30
+
31
+ export function expandPath(pattern, home = homedir()) {
32
+ if (pattern.startsWith('$')) {
33
+ const slash = pattern.indexOf('/')
34
+ const envName = slash === -1 ? pattern.slice(1) : pattern.slice(1, slash)
35
+ const value = process.env[envName]?.trim()
36
+ if (!value) return null
37
+ return slash === -1 ? value : path.join(value, pattern.slice(slash + 1))
38
+ }
39
+ if (pattern.startsWith('~config/')) {
40
+ return path.join(configHome(home), pattern.slice('~config/'.length))
41
+ }
42
+ if (pattern.startsWith('~/')) {
43
+ return path.join(home, pattern.slice(2))
44
+ }
45
+ return pattern
46
+ }
47
+
48
+ export function detectedAgents(home = homedir()) {
49
+ return AGENTS.filter((agent) =>
50
+ agent.detect.some((pattern) => {
51
+ const resolved = expandPath(pattern, home)
52
+ return resolved && existsSync(resolved)
53
+ }),
54
+ )
55
+ }
56
+
57
+ export function agentName(id) {
58
+ return AGENTS.find((agent) => agent.id === id)?.name ?? id
59
+ }
@@ -1,21 +1,14 @@
1
1
  // INPUT: 本机 Node、已安装的 Agent、终端是否可交互
2
- // OUTPUT: 全局安装 ahsops,多选写入 Skills,并按当前站点配置 profile
3
- // POS: npx @chuhe2640/ahsopus-cli install 的跨平台入口,对齐飞书 CLI 安装向导,不要求 Go,不打印 Token
2
+ // OUTPUT: 安装或升级 ahsops,写入 Skills,并按当前站点配置 profile
3
+ // POS: npx @chuhe2640/ahsopus-cli install 的跨平台入口,对齐飞书 CLI 安装向导
4
4
 
5
5
  import * as p from '@clack/prompts'
6
6
  import { spawnSync } from 'node:child_process'
7
7
  import { existsSync, readFileSync } from 'node:fs'
8
- import { homedir, platform } from 'node:os'
9
- import path from 'node:path'
8
+ import { platform } from 'node:os'
10
9
  import { stdin } from 'node:process'
11
- import { packageRoot } from './place-binary.js'
12
-
13
- export const AGENTS = [
14
- { id: 'claude-code', name: 'Claude Code', hint: '~/.claude' },
15
- { id: 'codex', name: 'Codex', hint: '~/.codex' },
16
- { id: 'cursor', name: 'Cursor', hint: '~/.cursor' },
17
- { id: 'grok', name: 'Grok', hint: '~/.grok' },
18
- ]
10
+ import { AGENTS, detectedAgents } from './agents.js'
11
+ import { installUserBinary, resolveInstalledAhsops } from './place-binary.js'
19
12
 
20
13
  const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
21
14
 
@@ -45,6 +38,17 @@ export function parseInstallFlags(argv) {
45
38
  return { server, profile }
46
39
  }
47
40
 
41
+ export function semverLessThan(left, right) {
42
+ const parse = (value) => value.replace(/-.*$/, '').split('.').map((part) => Number(part) || 0)
43
+ const a = parse(left)
44
+ const b = parse(right)
45
+ for (let index = 0; index < 3; index += 1) {
46
+ if (a[index] < b[index]) return true
47
+ if (a[index] > b[index]) return false
48
+ }
49
+ return false
50
+ }
51
+
48
52
  function execFile(command, args, options) {
49
53
  if (platform() === 'win32') {
50
54
  return spawnSync('cmd.exe', ['/c', command, ...args], options)
@@ -71,60 +75,52 @@ function runQuiet(command, args, options = {}) {
71
75
  return run(command, args, { stdio: ['ignore', 'pipe', 'pipe'], ...options })
72
76
  }
73
77
 
74
- export function detectedAgents(home = homedir()) {
75
- return AGENTS.filter((agent) => existsSync(agent.hint.replace('~', home)))
76
- }
77
-
78
- function globalAhsops() {
79
- const prefix = runQuiet('npm', ['prefix', '-g']).stdout.trim()
80
- if (platform() === 'win32') {
81
- return path.join(prefix, 'ahsops.cmd')
82
- }
83
- return path.join(prefix, 'bin', 'ahsops')
84
- }
85
-
86
- function globallyInstalledVersion() {
78
+ function installedVersion(binary) {
79
+ if (!binary || !existsSync(binary)) return null
87
80
  try {
88
- const out = runQuiet('npm', ['list', '-g', pkg.name], { timeout: 15000 }).stdout
89
- const match = String(out).match(/@(\d+\.\d+\.\d+[^\s]*)/)
81
+ const out = runQuiet(binary, ['--version'])
82
+ const match = String(`${out.stdout}\n${out.stderr}`).match(/(\d+\.\d+\.\d+[^\s]*)/)
90
83
  return match ? match[1] : null
91
84
  } catch {
92
85
  return null
93
86
  }
94
87
  }
95
88
 
96
- async function installGlobally() {
97
- const installed = globallyInstalledVersion()
98
- if (installed === pkg.version) {
99
- p.log.info(`已安装 (v${installed}),跳过`)
100
- return
89
+ async function installCliBinary() {
90
+ const current = resolveInstalledAhsops()
91
+ const currentVersion = installedVersion(current)
92
+ if (currentVersion && currentVersion === pkg.version) {
93
+ p.log.info(`已安装 (v${currentVersion}),跳过`)
94
+ return current
101
95
  }
102
96
  const spinner = p.spinner()
103
- spinner.start(installed ? `正在升级 ${pkg.name} (v${installed} → v${pkg.version})...` : `正在安装 ${pkg.name}...`)
97
+ const upgrading = Boolean(currentVersion && semverLessThan(currentVersion, pkg.version))
98
+ spinner.start(
99
+ upgrading
100
+ ? `正在升级 ${pkg.name} (v${currentVersion} → v${pkg.version})...`
101
+ : `正在安装 ${pkg.name}...`,
102
+ )
104
103
  try {
105
- try {
106
- runQuiet('npm', ['install', '-g', `${pkg.name}@${pkg.version}`], { timeout: 120000 })
107
- } catch {
108
- runQuiet('npm', ['install', '-g', packageRoot()], { timeout: 120000 })
109
- }
110
- spinner.stop(installed ? `已升级到 v${pkg.version}` : '已全局安装')
111
- } catch {
112
- spinner.stop(`全局安装失败。运行以下命令重试: npm install -g ${pkg.name}`)
104
+ const file = installUserBinary()
105
+ spinner.stop(upgrading ? `已升级到 v${pkg.version}` : '已全局安装')
106
+ return file
107
+ } catch (error) {
108
+ spinner.stop(`全局安装失败。运行以下命令重试: npx ${pkg.name}@latest install`)
109
+ p.log.error(error instanceof Error ? error.message : String(error))
113
110
  process.exit(1)
114
111
  }
115
112
  }
116
113
 
117
114
  async function chooseAgents() {
118
115
  const found = detectedAgents()
116
+ const options = (found.length ? found : AGENTS).map((agent) => ({
117
+ value: agent.id,
118
+ label: agent.name,
119
+ hint: found.some((item) => item.id === agent.id) ? '已检测到' : undefined,
120
+ }))
119
121
  const selected = await p.multiselect({
120
- message: found.length
121
- ? `检测到本机已安装:${found.map((agent) => agent.name).join('、')}。空格多选,回车确认`
122
- : '未检测到已安装的 AI Agent。空格多选要写入 Skills 的工具,回车确认',
123
- options: AGENTS.map((agent) => ({
124
- value: agent.id,
125
- label: agent.name,
126
- hint: found.some((item) => item.id === agent.id) ? '已检测到' : undefined,
127
- })),
122
+ message: '安装 AI Skills',
123
+ options,
128
124
  initialValues: found.map((agent) => agent.id),
129
125
  required: false,
130
126
  })
@@ -135,35 +131,56 @@ async function chooseAgents() {
135
131
  return selected
136
132
  }
137
133
 
134
+ function skillsInSync(binary, target) {
135
+ try {
136
+ const out = runQuiet(binary, ['skills', 'status', '--target', target, '--scope', 'user'])
137
+ const parsed = JSON.parse(out.stdout)
138
+ const items = Array.isArray(parsed.data) ? parsed.data : parsed
139
+ return Array.isArray(items) && items.length > 0 && items.every((item) => item.in_sync)
140
+ } catch {
141
+ return false
142
+ }
143
+ }
144
+
138
145
  async function installSkills(binary, targets) {
139
146
  if (!targets.length) {
140
147
  p.log.info('已跳过 Skills 安装')
141
148
  return
142
149
  }
150
+ if (targets.every((target) => skillsInSync(binary, target))) {
151
+ p.log.info('已安装,跳过')
152
+ return
153
+ }
143
154
  const spinner = p.spinner()
144
155
  spinner.start('正在安装 Skills...')
145
- try {
146
- for (const target of targets) {
156
+ let failed = 0
157
+ for (const target of targets) {
158
+ try {
147
159
  runQuiet(binary, ['skills', 'install', '--target', target, '--scope', 'user', '--yes'])
160
+ } catch {
161
+ failed += 1
148
162
  }
149
- spinner.stop(`Skills 已安装(${targets.map((id) => AGENTS.find((agent) => agent.id === id)?.name ?? id).join('、')})`)
150
- } catch (error) {
151
- spinner.stop('Skills 安装失败')
152
- p.log.error(error instanceof Error ? error.message : String(error))
153
- p.log.info('可稍后执行:ahsops skills install --target claude-code --scope user --yes')
154
163
  }
164
+ if (failed === 0) {
165
+ spinner.stop('Skills 已安装')
166
+ return
167
+ }
168
+ if (failed < targets.length) {
169
+ spinner.stop('Skills 已安装')
170
+ return
171
+ }
172
+ spinner.stop('Skills 安装失败')
155
173
  }
156
174
 
157
175
  function writeProfile(binary, server, profile) {
158
176
  const profileName = profile || 'office'
159
177
  runQuiet(binary, ['profile', 'add', profileName, '--server', server])
160
178
  runQuiet(binary, ['profile', 'use', profileName])
161
- p.log.success(`已写入 profile ${profileName} → ${server}`)
162
179
  }
163
180
 
164
181
  async function maybeLogin(binary) {
165
182
  const yes = await p.confirm({
166
- message: '是否现在登录 AHSOpsHub?会打开浏览器,由你本人批准,不要打印或转发 Token',
183
+ message: '是否允许 AI 访问你的 AHSOpsHub 数据,并以你的名义执行操作?',
167
184
  initialValue: true,
168
185
  })
169
186
  if (p.isCancel(yes)) {
@@ -171,7 +188,7 @@ async function maybeLogin(binary) {
171
188
  process.exit(0)
172
189
  }
173
190
  if (!yes) {
174
- p.log.info('跳过授权。后续运行 ahsops auth login --recommend 完成登录')
191
+ p.log.info('跳过授权。后续运行 ahsops auth login 完成授权')
175
192
  return
176
193
  }
177
194
  p.log.step('授权')
@@ -179,7 +196,7 @@ async function maybeLogin(binary) {
179
196
  run(binary, ['auth', 'login', '--recommend'])
180
197
  p.log.success('授权完成')
181
198
  } catch {
182
- p.log.warn('授权失败。后续运行 ahsops auth login --recommend 重试')
199
+ p.log.warn('授权失败。运行以下命令重试: ahsops auth login')
183
200
  }
184
201
  }
185
202
 
@@ -189,24 +206,18 @@ export async function install() {
189
206
 
190
207
  if (!interactive) {
191
208
  console.error('正在设置 AHSOpsHub CLI...')
192
- runQuiet('npm', ['install', '-g', packageRoot()], { timeout: 120000 })
193
- const binary = globalAhsops()
194
- if (server) {
195
- runQuiet(binary, ['profile', 'add', profile || 'office', '--server', server])
196
- runQuiet(binary, ['profile', 'use', profile || 'office'])
197
- }
198
- console.error('当前不是交互终端。请在终端中运行:')
199
- console.error(' npx @chuhe2640/ahsopus-cli@latest install --server <当前站点> --profile <dev|office|prod>')
200
- console.error(' ahsops auth login --recommend')
209
+ const binary = installUserBinary()
210
+ if (server) writeProfile(binary, server, profile)
211
+ console.error('要完成配置,请在终端中运行:')
212
+ console.error(' ahsops auth login')
201
213
  return
202
214
  }
203
215
 
204
216
  p.intro('正在设置 AHSOpsHub CLI...')
205
- await installGlobally()
206
- const binary = globalAhsops()
217
+ const binary = await installCliBinary()
207
218
  if (server) writeProfile(binary, server, profile)
208
219
  const targets = await chooseAgents()
209
220
  await installSkills(binary, targets)
210
221
  await maybeLogin(binary)
211
- p.outro('安装完成!\n可以和你的 AI 工具(如 Claude Code、Codex、Cursor、Grok)说:“AHSOpsHub CLI 能帮我做什么?”')
222
+ p.outro('安装完成!\n可以和你的 AI 工具(如 Claude Code、Trae等)说:“AHSOpsHub CLI 能帮我做什么?结合我的情况推荐一下从哪里开始”')
212
223
  }
@@ -1,9 +1,10 @@
1
1
  // INPUT: 当前平台,以及 npm 包内的 prebuilt/ 或本机 Go
2
2
  // OUTPUT: native/ahsops 可执行文件路径
3
- // POS: postinstall 放置预编译二进制;用户安装不要求 Go
3
+ // POS: 把预编译 ahsops 写到用户 bin,覆盖已有同名命令;用户安装不要求 Go
4
4
 
5
- import { chmodSync, copyFileSync, existsSync, mkdirSync } from 'node:fs'
5
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, unlinkSync } from 'node:fs'
6
6
  import { spawnSync } from 'node:child_process'
7
+ import { homedir } from 'node:os'
7
8
  import path from 'node:path'
8
9
  import { fileURLToPath } from 'node:url'
9
10
  import { currentTarget } from './platforms.js'
@@ -17,6 +18,51 @@ export function nativeBinaryPath() {
17
18
  return path.join(packageRoot(), 'native', `ahsops${ext}`)
18
19
  }
19
20
 
21
+ export function userBinaryTarget() {
22
+ if (process.platform === 'win32') {
23
+ const dir = path.join(process.env.LOCALAPPDATA || homedir(), 'AHSOpsHub', 'bin')
24
+ return { dir, file: path.join(dir, 'ahsops.exe') }
25
+ }
26
+ const dir = path.join(homedir(), '.local', 'bin')
27
+ return { dir, file: path.join(dir, 'ahsops') }
28
+ }
29
+
30
+ export function isEphemeralBin(file) {
31
+ const normalized = file.replaceAll('\\', '/')
32
+ return (
33
+ normalized.includes('/.npm/_npx/') ||
34
+ normalized.includes('/.npm/_cacache/') ||
35
+ normalized.includes('/node_modules/')
36
+ )
37
+ }
38
+
39
+ function whichAhsops() {
40
+ const probe = process.platform === 'win32' ? 'where' : 'which'
41
+ const result = spawnSync(probe, ['ahsops'], { encoding: 'utf8', stdio: 'pipe' })
42
+ if (result.status !== 0) return ''
43
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || ''
44
+ }
45
+
46
+ export function resolveInstalledAhsops() {
47
+ const { file } = userBinaryTarget()
48
+ if (existsSync(file)) return file
49
+ const existing = whichAhsops()
50
+ if (existing && !isEphemeralBin(existing)) return existing
51
+ return ''
52
+ }
53
+
54
+ function copyBinary(source, destination) {
55
+ mkdirSync(path.dirname(destination), { recursive: true })
56
+ try {
57
+ const stat = lstatSync(destination)
58
+ if (stat.isFile() || stat.isSymbolicLink()) unlinkSync(destination)
59
+ } catch {
60
+ // 目标不存在时直接写入
61
+ }
62
+ copyFileSync(source, destination)
63
+ chmodSync(destination, 0o755)
64
+ }
65
+
20
66
  export function resolvePrebuiltPath(nodePlatform = process.platform, nodeArch = process.arch) {
21
67
  const target = currentTarget(nodePlatform, nodeArch)
22
68
  return path.join(packageRoot(), 'prebuilt', target.file)
@@ -27,16 +73,12 @@ function commandExists(name) {
27
73
  return spawnSync(probe, [name], { encoding: 'utf8', stdio: 'pipe' }).status === 0
28
74
  }
29
75
 
30
- export function placeBinary() {
31
- const destination = nativeBinaryPath()
32
- mkdirSync(path.dirname(destination), { recursive: true })
76
+ function resolveSourceBinary() {
33
77
  const prebuilt = resolvePrebuiltPath()
34
- if (existsSync(prebuilt)) {
35
- copyFileSync(prebuilt, destination)
36
- chmodSync(destination, 0o755)
37
- return destination
38
- }
78
+ if (existsSync(prebuilt)) return prebuilt
39
79
  if (commandExists('go')) {
80
+ const destination = nativeBinaryPath()
81
+ mkdirSync(path.dirname(destination), { recursive: true })
40
82
  const result = spawnSync('go', ['build', '-o', destination, './cmd/ahsops'], {
41
83
  cwd: packageRoot(),
42
84
  stdio: 'inherit',
@@ -51,6 +93,42 @@ export function placeBinary() {
51
93
  )
52
94
  }
53
95
 
96
+ export function placeBinary() {
97
+ const destination = nativeBinaryPath()
98
+ copyBinary(resolveSourceBinary(), destination)
99
+ return destination
100
+ }
101
+
102
+ export function installUserBinary() {
103
+ const source = resolveSourceBinary()
104
+ const { file: userFile } = userBinaryTarget()
105
+ const existing = process.platform === 'win32' ? '' : whichAhsops()
106
+ const destinations = [userFile]
107
+ if (
108
+ existing &&
109
+ !isEphemeralBin(existing) &&
110
+ path.resolve(existing) !== path.resolve(userFile)
111
+ ) {
112
+ destinations.push(existing)
113
+ }
114
+ const written = []
115
+ const errors = []
116
+ for (const destination of destinations) {
117
+ try {
118
+ copyBinary(source, destination)
119
+ written.push(destination)
120
+ } catch (error) {
121
+ errors.push(`${destination}: ${error instanceof Error ? error.message : String(error)}`)
122
+ }
123
+ }
124
+ if (!written.length) {
125
+ throw new Error(`无法写入 ahsops。${errors.join('; ')}`)
126
+ }
127
+ if (existing && written.includes(existing)) return existing
128
+ if (written.includes(userFile)) return userFile
129
+ return written[0]
130
+ }
131
+
54
132
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
55
133
  try {
56
134
  placeBinary()
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@chuhe2640/ahsopus-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Install AHSOpsHub CLI (ahsops) and Agent Skills",
5
5
  "type": "module",
6
6
  "bin": {
7
- "ahsops": "bin/cli.js"
7
+ "ahsopus-cli": "bin/cli.js"
8
8
  },
9
9
  "scripts": {
10
10
  "prepublishOnly": "node npm/lib/build-binaries.js",
@@ -15,9 +15,11 @@
15
15
  },
16
16
  "files": [
17
17
  "bin/cli.js",
18
+ "npm/lib/agents.js",
18
19
  "npm/lib/install.js",
19
20
  "npm/lib/place-binary.js",
20
21
  "npm/lib/platforms.js",
22
+ "internal/skills/agents.json",
21
23
  "prebuilt"
22
24
  ],
23
25
  "os": [
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file