@quolu/aishell 0.5.0 → 0.6.0
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.ja.md +30 -19
- package/README.md +33 -23
- package/dist/AIShell.app/Contents/Helpers/aishell-mcp +0 -0
- package/dist/AIShell.app/Contents/Helpers/aishell-run-supervisor +0 -0
- package/dist/AIShell.app/Contents/Info.plist +2 -2
- package/dist/AIShell.app/Contents/MacOS/AIShell +0 -0
- package/dist/AIShell.app/Contents/_CodeSignature/CodeResources +4 -4
- package/package.json +15 -3
- package/scripts/aishell-setup.mjs +24 -0
- package/scripts/setup/host-cli.mjs +63 -0
- package/scripts/setup/hosts.mjs +115 -0
- package/scripts/setup/mcp-client.mjs +64 -0
- package/scripts/setup/setup.mjs +104 -0
- package/scripts/setup/toml-registration.mjs +53 -0
package/README.ja.md
CHANGED
|
@@ -28,9 +28,7 @@ AIShellはfile identity、filesystem照合state、直接起動したprocess、
|
|
|
28
28
|
Apple Silicon Mac、macOS 15以降が必要。
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
|
-
npm install -g @quolu/aishell
|
|
32
|
-
aishell-open
|
|
33
|
-
codex mcp add aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
|
|
31
|
+
npm install -g @quolu/aishell && aishell-setup
|
|
34
32
|
```
|
|
35
33
|
|
|
36
34
|
フォルダの事前登録は不要。新しいCodex taskで対象フォルダを指定して実行する。
|
|
@@ -60,7 +58,7 @@ semantic search、project profile、Git branch/worktree modeが加わる。
|
|
|
60
58
|
Codexでは次のように登録する。
|
|
61
59
|
|
|
62
60
|
```sh
|
|
63
|
-
|
|
61
|
+
aishell-setup --ai codex
|
|
64
62
|
```
|
|
65
63
|
|
|
66
64
|
未知値または空の`AISHELL_CAPABILITY_SET`と`AISHELL_TOOL_PROFILE`はtyped errorでstartup停止し、
|
|
@@ -101,16 +99,16 @@ flowchart LR
|
|
|
101
99
|
|
|
102
100
|
## npmからinstall
|
|
103
101
|
|
|
104
|
-
global packageは`aishell-mcp
|
|
102
|
+
global packageは`aishell-mcp`、`aishell-open`、`aishell-setup`を`PATH`へ追加する。npm install自体ではスクリプトも管理アプリも起動しない。
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
対象AIのCLI(`claude`、`codex`、`grok`、Cursorの`agent`)を先に導入する。setupは各CLIからの読戻しも確認する。
|
|
105
|
+
|
|
106
|
+
`aishell-setup`は導入済みのClaude Code・Codex・Grok Build・Cursorを検出し、管理アプリ準備、MCP登録、設定の読戻し、実際のMCP操作まで確認する。登録はbare `aishell-mcp`+`AISHELL_CAPABILITY_SET=expanded-v1`。利用者のenv、PATH、他の設定を保持する。`--ai`で対象を指定でき、`--check`は設定やアプリ起動を変更せず診断する。Windows/LinuxとIntel Macは対象外。詳細は[製品単体の導入契約](https://github.com/kitepon/aishell/blob/main/docs/setup.md)を参照。
|
|
107
|
+
|
|
108
|
+
更新後も同じ`aishell-setup`を実行する。旧管理アプリを正常終了して導入済みのアプリを開き、登録保持・読戻し・MCP実操作まで確認する。接続済みのMCPは、hostで再接続すると新版へ切り替わる。
|
|
110
109
|
|
|
111
110
|
```sh
|
|
112
|
-
npm install -g @quolu/aishell
|
|
113
|
-
aishell-open
|
|
111
|
+
npm install -g @quolu/aishell && aishell-setup
|
|
114
112
|
```
|
|
115
113
|
|
|
116
114
|
現在の実験版はDeveloper ID署名・notarization前である。
|
|
@@ -134,9 +132,8 @@ MCP実行ファイルは`build/AIShell.app/Contents/Helpers/aishell-mcp`へ同
|
|
|
134
132
|
global npm install後は`PATH`上のcommand名とexpanded development surfaceを登録する。
|
|
135
133
|
|
|
136
134
|
```sh
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
codex mcp get aishell
|
|
135
|
+
aishell-setup --ai claude,codex,grok,cursor
|
|
136
|
+
aishell-setup --check
|
|
140
137
|
```
|
|
141
138
|
|
|
142
139
|
解除:
|
|
@@ -166,8 +163,8 @@ full profileにはfile一覧・read、SHA-256競合検出付きatomic update、c
|
|
|
166
163
|
|
|
167
164
|
## 現在の制限
|
|
168
165
|
|
|
169
|
-
- stdio
|
|
170
|
-
- MCP
|
|
166
|
+
- stdio requestは復旧操作・読み取り・実行の3系統で処理する。読み取りと復旧操作は長時間の実行中も応答するが、実行系requestは直列化する。
|
|
167
|
+
- MCPの`notifications/cancelled`を受け付ける。管理対象processの明示的な停止は`run_observe`の`cancel`で行う。
|
|
171
168
|
- timeout時は直接所有するprocess treeを終了するが、終了までに許可workerがopen-worldな副作用を起こし得る。
|
|
172
169
|
- 初回workspace entryはbounded previewで、後続deltaはcursor pageになる。
|
|
173
170
|
- Developer ID署名とnotarizationは未設定。
|
|
@@ -177,8 +174,7 @@ full profileにはfile一覧・read、SHA-256競合検出付きatomic update、c
|
|
|
177
174
|
単独installの更新は初回と同じ公式npm経路を使い、新版の管理アプリを開く。
|
|
178
175
|
|
|
179
176
|
```sh
|
|
180
|
-
npm install -g @quolu/aishell@latest
|
|
181
|
-
aishell-open
|
|
177
|
+
npm install -g @quolu/aishell@latest && aishell-setup
|
|
182
178
|
```
|
|
183
179
|
|
|
184
180
|
停止中の復旧入口は`runtime_status`と`runtime_open_manager`である。
|
|
@@ -194,13 +190,28 @@ npm test
|
|
|
194
190
|
npm run test:package
|
|
195
191
|
git fetch origin
|
|
196
192
|
npm run verify:release-commit
|
|
197
|
-
npm
|
|
193
|
+
npm whoami
|
|
194
|
+
npm publish --access public --browser=false
|
|
198
195
|
```
|
|
199
196
|
|
|
200
197
|
release gateはdirty treeと既定branchへ未着地のcommitを拒否する。publish後は対応する
|
|
201
198
|
GitHub Releaseを作り、`@quolu/aishell@latest`を再installしてMCP initializeと
|
|
202
199
|
`factory_diagnostics`をsmokeする。公開済みversionの正本はGitHub Releasesである。
|
|
203
200
|
|
|
201
|
+
### 公開認証と導入確認
|
|
202
|
+
|
|
203
|
+
`npm whoami`が認証エラーを返した場合は、対話端末で`npm login --registry=https://registry.npmjs.org/ --browser=false`を実行し、表示された新しいURLを利用するブラウザで開いて認証する。公開コマンドも対話端末で実行し、出力をファイルへリダイレクトしない。公開用の認証URLが表示された場合は、ログインとは別に認証する。URLが失効した場合はコマンドを再実行して新しいURLを使う。
|
|
204
|
+
|
|
205
|
+
npmのログインsessionは2時間で失効し、公開時には二要素認証が適用される([npm公式説明](https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/))。公開の成功後に、registryとグローバルインストールを確認する。
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
npm view @quolu/aishell dist-tags.latest
|
|
209
|
+
npm install -g @quolu/aishell@latest
|
|
210
|
+
npm ls -g @quolu/aishell --depth=0
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
MCPを再接続し、`initialize`のversion、`runtime_status`、事前登録のない対象フォルダの検索と実行を確認する。工場診断は別processを`AISHELL_TOOL_PROFILE=factory`で起動し、`AISHELL_CAPABILITY_SET`を設定せずに確認する。
|
|
214
|
+
|
|
204
215
|
## 開発検証
|
|
205
216
|
|
|
206
217
|
```sh
|
package/README.md
CHANGED
|
@@ -29,9 +29,7 @@ AIShell owns the OS-facing state below the model: file identity, filesystem reco
|
|
|
29
29
|
Requires an Apple Silicon Mac running macOS 15 or later.
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
|
-
npm install -g @quolu/aishell
|
|
33
|
-
aishell-open
|
|
34
|
-
codex mcp add aishell --env AISHELL_CAPABILITY_SET=expanded-v1 -- aishell-mcp
|
|
32
|
+
npm install -g @quolu/aishell && aishell-setup
|
|
35
33
|
```
|
|
36
34
|
|
|
37
35
|
フォルダの事前登録は不要です。新しいCodex taskで対象フォルダを指定して実行します。
|
|
@@ -58,7 +56,7 @@ Set `AISHELL_CAPABILITY_SET=expanded-v1` on the MCP server process to opt in to
|
|
|
58
56
|
For Codex, register the expanded surface explicitly:
|
|
59
57
|
|
|
60
58
|
```sh
|
|
61
|
-
|
|
59
|
+
aishell-setup --ai codex
|
|
62
60
|
```
|
|
63
61
|
|
|
64
62
|
Unknown or empty `AISHELL_CAPABILITY_SET` and `AISHELL_TOOL_PROFILE` values fail startup with typed errors; they never fall back to another profile.
|
|
@@ -99,17 +97,16 @@ flowchart LR
|
|
|
99
97
|
|
|
100
98
|
## Install from npm
|
|
101
99
|
|
|
102
|
-
|
|
100
|
+
global packageは`aishell-mcp`、`aishell-open`、`aishell-setup`を`PATH`へ追加する。npm install自体ではスクリプトも管理アプリも起動しない。
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
対象AIのCLI(`claude`、`codex`、`grok`、Cursorの`agent`)を先に導入する。setupは各CLIからの読戻しも確認する。
|
|
103
|
+
|
|
104
|
+
`aishell-setup`は導入済みのClaude Code・Codex・Grok Build・Cursorを検出し、管理アプリ準備、MCP登録、設定の読戻し、実際のMCP操作まで確認する。登録はbare `aishell-mcp`+`AISHELL_CAPABILITY_SET=expanded-v1`。利用者のenv、PATH、他の設定を保持する。`--ai`で対象を指定でき、`--check`は設定やアプリ起動を変更せず診断する。Windows/LinuxとIntel Macは対象外。詳細は[製品単体の導入契約](https://github.com/kitepon/aishell/blob/main/docs/setup.md)を参照。
|
|
105
|
+
|
|
106
|
+
更新後も同じ`aishell-setup`を実行する。旧管理アプリを正常終了して導入済みのアプリを開き、登録保持・読戻し・MCP実操作まで確認する。接続済みのMCPは、hostで再接続すると新版へ切り替わる。
|
|
109
107
|
|
|
110
108
|
```sh
|
|
111
|
-
npm install -g @quolu/aishell
|
|
112
|
-
aishell-open
|
|
109
|
+
npm install -g @quolu/aishell && aishell-setup
|
|
113
110
|
```
|
|
114
111
|
|
|
115
112
|
The current experimental build is not yet Developer ID signed or notarized.
|
|
@@ -133,9 +130,8 @@ The MCP executable is bundled at `build/AIShell.app/Contents/Helpers/aishell-mcp
|
|
|
133
130
|
For a global npm installation, register the executable name from `PATH` and the expanded development surface:
|
|
134
131
|
|
|
135
132
|
```sh
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
codex mcp get aishell
|
|
133
|
+
aishell-setup --ai claude,codex,grok,cursor
|
|
134
|
+
aishell-setup --check
|
|
139
135
|
```
|
|
140
136
|
|
|
141
137
|
Remove the registration with:
|
|
@@ -167,8 +163,8 @@ The full profile includes file listing and reads, atomic SHA-256-guarded updates
|
|
|
167
163
|
|
|
168
164
|
## Current limitations
|
|
169
165
|
|
|
170
|
-
-
|
|
171
|
-
- MCP
|
|
166
|
+
- stdio requestは復旧操作・読み取り・実行の3系統で処理する。読み取りと復旧操作は長時間の実行中も応答するが、実行系requestは直列化する。
|
|
167
|
+
- MCPの`notifications/cancelled`を受け付ける。管理対象processの明示的な停止は`run_observe`の`cancel`で行う。
|
|
172
168
|
- A timeout terminates the directly owned process tree, but an allowed worker remains capable of open-world side effects before termination.
|
|
173
169
|
- Initial workspace entries are a bounded preview; later deltas are cursor-paged.
|
|
174
170
|
- Developer ID signing and notarization are not yet configured.
|
|
@@ -176,14 +172,13 @@ The full profile includes file listing and reads, atomic SHA-256-guarded updates
|
|
|
176
172
|
## Operations, updates, and releases
|
|
177
173
|
|
|
178
174
|
Upgrade a standalone installation through the same official npm path used for
|
|
179
|
-
initial installation, then
|
|
175
|
+
initial installation, then run the explicit setup:
|
|
180
176
|
|
|
181
177
|
```sh
|
|
182
|
-
npm install -g @quolu/aishell@latest
|
|
183
|
-
aishell-open
|
|
178
|
+
npm install -g @quolu/aishell@latest && aishell-setup
|
|
184
179
|
```
|
|
185
180
|
|
|
186
|
-
`runtime_status` and `runtime_open_manager` are the recovery entrypoints for
|
|
181
|
+
`runtime_status` and `runtime_open_manager` are the recovery entrypoints for a
|
|
187
182
|
paused runtime. Factory consumers call `factory_diagnostics`
|
|
188
183
|
through the dedicated `AISHELL_TOOL_PROFILE=factory` MCP surface; its schema and
|
|
189
184
|
privacy boundary are owned by [the product contract](https://github.com/kitepon/aishell/blob/main/docs/factory-diagnostics.md).
|
|
@@ -196,14 +191,29 @@ npm test
|
|
|
196
191
|
npm run test:package
|
|
197
192
|
git fetch origin
|
|
198
193
|
npm run verify:release-commit
|
|
199
|
-
npm
|
|
194
|
+
npm whoami
|
|
195
|
+
npm publish --access public --browser=false
|
|
200
196
|
```
|
|
201
197
|
|
|
202
198
|
The release gate rejects a dirty tree or a commit that has not landed on the
|
|
203
199
|
default branch. After publishing, create the matching GitHub Release, reinstall
|
|
204
|
-
`@quolu/aishell@latest`, and
|
|
200
|
+
`@quolu/aishell@latest`, run `aishell-setup` and `aishell-setup --check`, and separately smoke `factory_diagnostics`.
|
|
205
201
|
GitHub Releases are the public record of shipped versions.
|
|
206
202
|
|
|
203
|
+
### 公開認証と導入確認
|
|
204
|
+
|
|
205
|
+
`npm whoami`が認証エラーを返した場合は、対話端末で`npm login --registry=https://registry.npmjs.org/ --browser=false`を実行し、表示された新しいURLを利用するブラウザで開いて認証する。公開コマンドも対話端末で実行し、出力をファイルへリダイレクトしない。公開用の認証URLが表示された場合は、ログインとは別に認証する。URLが失効した場合はコマンドを再実行して新しいURLを使う。
|
|
206
|
+
|
|
207
|
+
npmのログインsessionは2時間で失効し、公開時には二要素認証が適用される([npm公式説明](https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/))。公開の成功後に、registryとグローバルインストールを確認する。
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
npm view @quolu/aishell dist-tags.latest
|
|
211
|
+
npm install -g @quolu/aishell@latest
|
|
212
|
+
npm ls -g @quolu/aishell --depth=0
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
MCPを再接続し、`initialize`のversion、`runtime_status`、事前登録のない対象フォルダの検索と実行を確認する。工場診断は別processを`AISHELL_TOOL_PROFILE=factory`で起動し、`AISHELL_CAPABILITY_SET`を設定せずに確認する。
|
|
216
|
+
|
|
207
217
|
## Development
|
|
208
218
|
|
|
209
219
|
```sh
|
|
Binary file
|
|
Binary file
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
<key>CFBundlePackageType</key>
|
|
18
18
|
<string>APPL</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.
|
|
20
|
+
<string>0.6.0</string>
|
|
21
21
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>
|
|
22
|
+
<string>21</string>
|
|
23
23
|
<key>LSApplicationCategoryType</key>
|
|
24
24
|
<string>public.app-category.utilities</string>
|
|
25
25
|
<key>LSMinimumSystemVersion</key>
|
|
Binary file
|
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
<dict>
|
|
11
11
|
<key>cdhash</key>
|
|
12
12
|
<data>
|
|
13
|
-
|
|
13
|
+
UTk6ryOelBpibaQkQByLjOitg6Q=
|
|
14
14
|
</data>
|
|
15
15
|
<key>requirement</key>
|
|
16
|
-
<string>cdhash H"
|
|
16
|
+
<string>cdhash H"51393aaf239e941a626da424401c8b8ce8ad83a4"</string>
|
|
17
17
|
</dict>
|
|
18
18
|
<key>Helpers/aishell-run-supervisor</key>
|
|
19
19
|
<dict>
|
|
20
20
|
<key>cdhash</key>
|
|
21
21
|
<data>
|
|
22
|
-
|
|
22
|
+
JDIaGpdJLctZbPoDtu5xZihSiRM=
|
|
23
23
|
</data>
|
|
24
24
|
<key>requirement</key>
|
|
25
|
-
<string>cdhash H"
|
|
25
|
+
<string>cdhash H"24321a1a97492dcb596cfa03b6ee716628528913"</string>
|
|
26
26
|
</dict>
|
|
27
27
|
</dict>
|
|
28
28
|
<key>rules</key>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quolu/aishell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A native macOS state runtime with high-density MCP tools for AI development",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": {
|
|
@@ -30,17 +30,25 @@
|
|
|
30
30
|
],
|
|
31
31
|
"bin": {
|
|
32
32
|
"aishell-mcp": "dist/AIShell.app/Contents/Helpers/aishell-mcp",
|
|
33
|
-
"aishell-open": "scripts/aishell-open.mjs"
|
|
33
|
+
"aishell-open": "scripts/aishell-open.mjs",
|
|
34
|
+
"aishell-setup": "scripts/aishell-setup.mjs"
|
|
34
35
|
},
|
|
35
36
|
"files": [
|
|
36
37
|
"dist/AIShell.app",
|
|
37
38
|
"scripts/aishell-open.mjs",
|
|
39
|
+
"scripts/aishell-setup.mjs",
|
|
40
|
+
"scripts/setup/hosts.mjs",
|
|
41
|
+
"scripts/setup/host-cli.mjs",
|
|
42
|
+
"scripts/setup/setup.mjs",
|
|
43
|
+
"scripts/setup/mcp-client.mjs",
|
|
44
|
+
"scripts/setup/toml-registration.mjs",
|
|
38
45
|
"README.md"
|
|
39
46
|
],
|
|
40
47
|
"scripts": {
|
|
41
48
|
"build:npm": "node scripts/prepare-npm-release.mjs",
|
|
42
49
|
"prepack": "npm run build:npm",
|
|
43
|
-
"test": "swift test && npm run test:release-gate && npm run test:repository-contract",
|
|
50
|
+
"test": "swift test && npm run test:setup && npm run test:release-gate && npm run test:repository-contract",
|
|
51
|
+
"test:setup": "node --test scripts/setup/*.test.mjs",
|
|
44
52
|
"test:release-gate": "node --test scripts/verify-release-commit.test.mjs",
|
|
45
53
|
"test:repository-contract": "node --test scripts/repository-contract.test.mjs",
|
|
46
54
|
"test:package": "npm run build:npm && node scripts/verify-npm-package.mjs",
|
|
@@ -59,5 +67,9 @@
|
|
|
59
67
|
},
|
|
60
68
|
"publishConfig": {
|
|
61
69
|
"access": "public"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"smol-toml": "1.8.0",
|
|
73
|
+
"toml-eslint-parser": "0.10.0"
|
|
62
74
|
}
|
|
63
75
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { setup } from './setup/setup.mjs';
|
|
3
|
+
import { SetupError } from './setup/mcp-client.mjs';
|
|
4
|
+
import { aiNames } from './setup/hosts.mjs';
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
9
|
+
console.log('aishell-setup [--ai claude,codex,grok,cursor] [--check]\n初回・再実行・更新後に管理アプリ準備、MCP登録、読戻し、実操作を確認します。\n省略時は導入済みAIを検出。--checkは設定変更と管理アプリ起動を行いません。');
|
|
10
|
+
} else {
|
|
11
|
+
const options = {};
|
|
12
|
+
for (let index = 0; index < args.length; index++) {
|
|
13
|
+
if (args[index] === '--check' && options.check === undefined) options.check = true;
|
|
14
|
+
else if (args[index] === '--ai' && options.ais === undefined) {
|
|
15
|
+
options.ais = (args[++index] ?? '').split(',');
|
|
16
|
+
if (options.ais.some(ai => !aiNames.includes(ai)) || new Set(options.ais).size !== options.ais.length) throw new SetupError('AI_UNSUPPORTED', '--aiにはclaude,codex,grok,cursorを重複なしで指定してください。');
|
|
17
|
+
} else throw new SetupError('ARGUMENT_INVALID', '引数を確認してください。aishell-setup --helpで使い方を表示できます。');
|
|
18
|
+
}
|
|
19
|
+
console.log(JSON.stringify(await setup(options)));
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(JSON.stringify({ ...(error.report ?? { schemaVersion: 'aishell.setup.v1', status: 'failed' }), error: { code: error.code ?? 'SETUP_FAILED', message: error instanceof SetupError ? error.message : 'setup中の入出力処理に失敗しました。設定fileの権限と導入状態を確認してください。' } }));
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { lstat, readdir } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { backupFiles } from './hosts.mjs';
|
|
5
|
+
import { SetupError } from './mcp-client.mjs';
|
|
6
|
+
|
|
7
|
+
export function runHostCLI(command, args, options) {
|
|
8
|
+
try { return execFileSync(command, args, { ...options, encoding: 'utf8', timeout: 45000, stdio: ['ignore', 'pipe', 'pipe'] }); }
|
|
9
|
+
catch (error) {
|
|
10
|
+
throw new SetupError(error.code === 'ENOENT' ? 'AI_CLI_NOT_FOUND' : 'AI_VERIFICATION_FAILED', `${command}によるMCP確認に失敗しました。AI本体の導入・接続・承認状態を確認してください。`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function present(file) {
|
|
15
|
+
try {
|
|
16
|
+
const info = await lstat(file);
|
|
17
|
+
if (!info.isFile()) throw new SetupError('CONFIG_PATH_UNSUPPORTED', 'Cursorの設定は通常fileである必要があります。');
|
|
18
|
+
return true;
|
|
19
|
+
} catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function backupCursor(spec, backupDirectory) {
|
|
23
|
+
const files = [];
|
|
24
|
+
const config = path.join(spec.base, 'cli-config.json');
|
|
25
|
+
if (await present(config)) files.push(config);
|
|
26
|
+
const directory = path.join(spec.base, 'projects');
|
|
27
|
+
let entries;
|
|
28
|
+
try { entries = await readdir(directory, { withFileTypes: true }); }
|
|
29
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; entries = []; }
|
|
30
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
31
|
+
const file = path.join(directory, entry.name, 'mcp-approvals.json');
|
|
32
|
+
if (await present(file)) files.push(file);
|
|
33
|
+
}
|
|
34
|
+
await backupFiles(files, backupDirectory, 'cursor-approval');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function verifyHostCLI(spec, registration, { check, env, cwd, backupDirectory, run = runHostCLI }) {
|
|
38
|
+
const options = { env: { ...env, NO_COLOR: '1', TERM: 'dumb' }, cwd };
|
|
39
|
+
let actual;
|
|
40
|
+
if (spec.ai === 'codex') {
|
|
41
|
+
const value = JSON.parse(run('codex', ['mcp', 'get', 'aishell', '--json'], options));
|
|
42
|
+
if (value.enabled !== true || value.transport?.type !== 'stdio') throw new SetupError('AI_REGISTRATION_MISMATCH', 'Codexの実効登録が有効なstdioではありません。');
|
|
43
|
+
actual = value.transport;
|
|
44
|
+
} else if (spec.ai === 'grok') {
|
|
45
|
+
const values = JSON.parse(run('grok', ['mcp', 'list', '--json'], options)).filter(item => item.name === 'aishell');
|
|
46
|
+
if (values.length !== 1 || values[0].enabled !== true) throw new SetupError('AI_REGISTRATION_MISMATCH', 'Grokの実効登録を一意に確認できません。');
|
|
47
|
+
actual = values[0];
|
|
48
|
+
} else if (spec.ai === 'claude') {
|
|
49
|
+
const output = run('claude', ['mcp', 'get', 'aishell'], options);
|
|
50
|
+
if (!/^\s*Command: aishell-mcp\s*$/m.test(output) || !output.includes('expanded-v1') || !/Status:.*Connected/.test(output)) throw new SetupError('AI_REGISTRATION_MISMATCH', 'Claudeの実効登録またはMCP接続が一致しません。');
|
|
51
|
+
} else {
|
|
52
|
+
if (!check) {
|
|
53
|
+
await backupCursor(spec, backupDirectory);
|
|
54
|
+
run('agent', ['mcp', 'enable', 'aishell'], options);
|
|
55
|
+
} else if (!await present(path.join(spec.base, 'cli-config.json'))) {
|
|
56
|
+
throw new SetupError('AI_VERIFICATION_FAILED', 'Cursor CLIが未準備です。aishell-setupを実行してください。');
|
|
57
|
+
}
|
|
58
|
+
const output = run('agent', ['mcp', 'list-tools', 'aishell'], options);
|
|
59
|
+
if (!['runtime_status', 'workspace_snapshot', 'apply_change_set'].every(name => output.includes(name))) throw new SetupError('AI_REGISTRATION_MISMATCH', 'Cursorからexpanded-v1のtoolを確認できません。');
|
|
60
|
+
}
|
|
61
|
+
if (actual && (actual.command !== registration.command || (actual.args ?? []).length !== 0 || actual.env?.AISHELL_CAPABILITY_SET !== 'expanded-v1')) throw new SetupError('AI_REGISTRATION_MISMATCH', `${spec.ai}の実効登録が製品登録と一致しません。project設定の上書きも確認してください。`);
|
|
62
|
+
return { hostVerified: true };
|
|
63
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFile, lstat, mkdir, writeFile, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
|
+
import { parse } from 'smol-toml';
|
|
7
|
+
import { replaceTOMLRegistration, replaceTopLevelValue } from './toml-registration.mjs';
|
|
8
|
+
import { SetupError } from './mcp-client.mjs';
|
|
9
|
+
|
|
10
|
+
export const aiNames = ['claude', 'codex', 'grok', 'cursor'];
|
|
11
|
+
export function hostSpec(ai, home, env) {
|
|
12
|
+
if (ai === 'cursor' && env.CURSOR_HOME && path.resolve(env.CURSOR_HOME) !== path.join(home, '.cursor')) {
|
|
13
|
+
throw new SetupError('AI_CONFIG_LOCATION_UNSUPPORTED', 'CursorはCURSOR_HOMEを読みません。通常の~/.cursorを使う環境でsetupを実行してください。');
|
|
14
|
+
}
|
|
15
|
+
const bases = {
|
|
16
|
+
claude: env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'),
|
|
17
|
+
codex: env.CODEX_HOME || path.join(home, '.codex'),
|
|
18
|
+
grok: env.GROK_HOME || path.join(home, '.grok'),
|
|
19
|
+
cursor: path.join(home, '.cursor'),
|
|
20
|
+
};
|
|
21
|
+
if (!aiNames.includes(ai)) throw new SetupError('AI_UNSUPPORTED', `未対応のAIです: ${ai}`);
|
|
22
|
+
const base = path.resolve(bases[ai]);
|
|
23
|
+
return {
|
|
24
|
+
ai, base,
|
|
25
|
+
file: ai === 'claude' ? (env.CLAUDE_CONFIG_DIR ? path.join(base, '.claude.json') : path.join(home, '.claude.json'))
|
|
26
|
+
: path.join(base, ai === 'cursor' ? 'mcp.json' : 'config.toml'),
|
|
27
|
+
format: ['codex', 'grok'].includes(ai) ? 'toml' : 'json',
|
|
28
|
+
key: ['codex', 'grok'].includes(ai) ? 'mcp_servers' : 'mcpServers',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function object(value, label) {
|
|
33
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new SetupError('CONFIG_INVALID', `${label}はobjectである必要があります。`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function canonicalRegistration(current = {}, ai) {
|
|
38
|
+
object(current, 'AIShell設定');
|
|
39
|
+
const next = { ...current, command: 'aishell-mcp', args: [], env: { ...object(current.env ?? {}, 'AIShell env'), AISHELL_CAPABILITY_SET: 'expanded-v1' } };
|
|
40
|
+
if (Object.values(next.env).some(value => typeof value !== 'string')) throw new SetupError('CONFIG_INVALID', 'AIShell envの値は文字列である必要があります。');
|
|
41
|
+
// transportの旧値はAIShellが所有する。利用者のtimeout・許可tool等は保持する。
|
|
42
|
+
for (const key of ['url', 'http_headers', 'env_http_headers', 'bearer_token_env_var', 'headers']) delete next[key];
|
|
43
|
+
if (ai === 'claude' || ai === 'cursor') next.type = 'stdio';
|
|
44
|
+
else { delete next.type; next.enabled = true; }
|
|
45
|
+
if ('enabled' in next) next.enabled = true;
|
|
46
|
+
if ('disabled' in next) next.disabled = false;
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function decode(text, spec) {
|
|
51
|
+
try { return object(text.trim() ? (spec.format === 'toml' ? parse(text) : JSON.parse(text)) : {}, 'AI設定'); }
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof SetupError) throw error;
|
|
54
|
+
throw new SetupError('CONFIG_INVALID', `${spec.ai}の設定を解析できません。`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function readHost(spec) {
|
|
59
|
+
let info;
|
|
60
|
+
try { info = await lstat(spec.file); }
|
|
61
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
62
|
+
if (info && !info.isFile()) throw new SetupError('CONFIG_PATH_UNSUPPORTED', `${spec.ai}の設定は通常fileである必要があります。`);
|
|
63
|
+
const text = info ? await readFile(spec.file, 'utf8') : '';
|
|
64
|
+
const data = decode(text, spec);
|
|
65
|
+
const servers = object(data[spec.key] ?? {}, `${spec.ai} MCP設定`);
|
|
66
|
+
return { text, data, info, registration: servers.aishell };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function planHost(spec) {
|
|
70
|
+
const before = await readHost(spec);
|
|
71
|
+
const registration = canonicalRegistration(before.registration, spec.ai);
|
|
72
|
+
const data = { ...before.data, [spec.key]: { ...before.data[spec.key], aishell: registration } };
|
|
73
|
+
if (spec.ai === 'grok' && data.disabled_mcp_servers !== undefined) {
|
|
74
|
+
if (!Array.isArray(data.disabled_mcp_servers) || data.disabled_mcp_servers.some(name => typeof name !== 'string')) throw new SetupError('CONFIG_INVALID', 'Grokのdisabled_mcp_serversは文字列配列である必要があります。');
|
|
75
|
+
data.disabled_mcp_servers = data.disabled_mcp_servers.filter(name => name !== 'aishell');
|
|
76
|
+
}
|
|
77
|
+
const changed = !isDeepStrictEqual(before.data, data);
|
|
78
|
+
let text;
|
|
79
|
+
try { text = changed ? (spec.format === 'toml' ? replaceTOMLRegistration(before.text, registration) : JSON.stringify(data, null, 2) + '\n') : before.text; }
|
|
80
|
+
catch { throw new SetupError('CONFIG_INVALID', `${spec.ai}の設定形式を保持して更新できません。`); }
|
|
81
|
+
if (!isDeepStrictEqual(before.data.disabled_mcp_servers, data.disabled_mcp_servers)) text = replaceTopLevelValue(text, 'disabled_mcp_servers', data.disabled_mcp_servers);
|
|
82
|
+
// serializeで他の設定値が変わる形式は、書込み前に明示拒否する。
|
|
83
|
+
if (!isDeepStrictEqual(decode(text, spec), data)) throw new SetupError('CONFIG_SERIALIZATION_LOSS', `${spec.ai}の設定値を保持できません。`);
|
|
84
|
+
return { spec, before, text, data, registration, changed };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function backupFiles(files, backupDirectory, label) {
|
|
88
|
+
if (!files.length) return;
|
|
89
|
+
await mkdir(backupDirectory, { recursive: true, mode: 0o700 });
|
|
90
|
+
// 設定の原本をtarへ保存してから更新する。秘密を含み得るため作成時から0600に限定する。
|
|
91
|
+
const archive = path.join(backupDirectory, `${label}-${randomUUID()}.tar`);
|
|
92
|
+
await writeFile(archive, '', { flag: 'wx', mode: 0o600 });
|
|
93
|
+
execFileSync('/usr/bin/tar', ['-cf', archive, '-C', '/', ...files.map(file => file.slice(1))], { stdio: 'pipe' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function writeHost(plan, backupDirectory) {
|
|
97
|
+
if (!plan.changed) return;
|
|
98
|
+
const { spec, before, text } = plan;
|
|
99
|
+
if (before.info) await backupFiles([spec.file], backupDirectory, spec.ai);
|
|
100
|
+
await mkdir(path.dirname(spec.file), { recursive: true, mode: 0o700 });
|
|
101
|
+
const temporary = path.join(path.dirname(spec.file), `.aishell-${randomUUID()}.tmp`);
|
|
102
|
+
try {
|
|
103
|
+
await writeFile(temporary, text, { flag: 'wx', mode: before.info ? before.info.mode & 0o777 : 0o600 });
|
|
104
|
+
const current = await readHost(spec);
|
|
105
|
+
if (current.text !== before.text || Boolean(current.info) !== Boolean(before.info)) throw new SetupError('CONFIG_CHANGED', `${spec.ai}の設定が同時に更新されました。再実行してください。`);
|
|
106
|
+
await rename(temporary, spec.file);
|
|
107
|
+
} finally { await rm(temporary, { force: true }); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function verifyHost(plan) {
|
|
111
|
+
const actual = await readHost(plan.spec);
|
|
112
|
+
if (!isDeepStrictEqual(actual.registration, plan.registration)) throw new SetupError('REGISTRATION_MISMATCH', `${plan.spec.ai}の登録読戻しが一致しません。`);
|
|
113
|
+
if (!isDeepStrictEqual(actual.data, plan.data)) throw new SetupError('CONFIG_CHANGED', `${plan.spec.ai}の他設定が変更されました。`);
|
|
114
|
+
return actual.registration;
|
|
115
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
|
|
4
|
+
export class SetupError extends Error {
|
|
5
|
+
constructor(code, message) { super(message); this.code = code; }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 読戻した登録をそのまま起動する。env値と子process出力は診断へ転載しない。
|
|
9
|
+
export async function withMCP(registration, operation, { cwd, env = process.env, timeout = 30000 } = {}) {
|
|
10
|
+
const child = spawn(registration.command, registration.args ?? [], {
|
|
11
|
+
cwd, env: { ...env, ...registration.env }, stdio: ['pipe', 'pipe', 'pipe'],
|
|
12
|
+
});
|
|
13
|
+
const pending = new Map();
|
|
14
|
+
let sequence = 0;
|
|
15
|
+
let failure;
|
|
16
|
+
const fail = (error) => {
|
|
17
|
+
failure = error;
|
|
18
|
+
for (const item of pending.values()) item.reject(error);
|
|
19
|
+
pending.clear();
|
|
20
|
+
};
|
|
21
|
+
child.on('error', () => fail(new SetupError('MCP_START_FAILED', '登録されたMCPを起動できません。PATHと導入状態を確認してください。')));
|
|
22
|
+
child.stdin.on('error', () => fail(new SetupError('MCP_CLOSED', 'MCPの入力が閉じました。')));
|
|
23
|
+
child.stderr.resume();
|
|
24
|
+
const closed = new Promise(resolve => child.once('close', resolve));
|
|
25
|
+
child.once('close', () => fail(new SetupError('MCP_CLOSED', 'MCPが応答完了前に終了しました。')));
|
|
26
|
+
const lines = createInterface({ input: child.stdout });
|
|
27
|
+
lines.on('line', line => {
|
|
28
|
+
let response;
|
|
29
|
+
try { response = JSON.parse(line); }
|
|
30
|
+
catch { fail(new SetupError('MCP_PROTOCOL_INVALID', 'MCPがJSON以外を返しました。')); return; }
|
|
31
|
+
const item = pending.get(response.id);
|
|
32
|
+
if (!item) return;
|
|
33
|
+
pending.delete(response.id);
|
|
34
|
+
if (response.error || response.result?.isError) item.reject(new SetupError('MCP_OPERATION_FAILED', `${item.method}が失敗しました。`));
|
|
35
|
+
else item.resolve(response.result);
|
|
36
|
+
});
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
fail(new SetupError('MCP_TIMEOUT', 'MCP応答が制限時間を超えました。'));
|
|
39
|
+
child.kill('SIGTERM');
|
|
40
|
+
}, timeout);
|
|
41
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
42
|
+
if (failure) { reject(failure); return; }
|
|
43
|
+
const id = ++sequence;
|
|
44
|
+
pending.set(id, { resolve, reject, method });
|
|
45
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
const initialized = await request('initialize', {
|
|
49
|
+
protocolVersion: '2025-11-25', capabilities: {},
|
|
50
|
+
clientInfo: { name: 'aishell-setup', version: '1' },
|
|
51
|
+
});
|
|
52
|
+
if (initialized?.protocolVersion !== '2025-11-25') throw new SetupError('MCP_PROTOCOL_INVALID', 'MCP protocol versionが一致しません。');
|
|
53
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
|
|
54
|
+
return await operation({ initialized, request, call: (name, args = {}) => request('tools/call', { name, arguments: args }) });
|
|
55
|
+
} finally {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
child.stdin.end();
|
|
58
|
+
const killTimer = setTimeout(() => child.kill('SIGKILL'), 1000);
|
|
59
|
+
child.kill('SIGTERM');
|
|
60
|
+
await closed;
|
|
61
|
+
clearTimeout(killTimer);
|
|
62
|
+
lines.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { access, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { aiNames, hostSpec, planHost, verifyHost, writeHost } from './hosts.mjs';
|
|
8
|
+
import { SetupError, withMCP } from './mcp-client.mjs';
|
|
9
|
+
import { verifyHostCLI } from './host-cli.mjs';
|
|
10
|
+
|
|
11
|
+
export const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
12
|
+
|
|
13
|
+
export function supportedPlatform(platform, arch, macVersion) {
|
|
14
|
+
if (platform !== 'darwin' || arch !== 'arm64' || !/^\d+(\.\d+)*$/.test(macVersion ?? '') || Number(macVersion.split('.')[0]) < 15) {
|
|
15
|
+
throw new SetupError('PLATFORM_UNSUPPORTED', '対応環境はmacOS 15以降のApple Silicon(arm64)だけです。設定・processは作成しません。');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function exists(file, mode) {
|
|
20
|
+
try { await access(file, mode); return true; }
|
|
21
|
+
catch (error) { if (['ENOENT', 'EACCES'].includes(error.code)) return false; throw error; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function installed(spec, env) {
|
|
25
|
+
if (await exists(spec.base) || await exists(spec.file)) return true;
|
|
26
|
+
const commands = spec.ai === 'cursor' ? ['cursor', 'agent'] : [spec.ai];
|
|
27
|
+
for (const directory of (env.PATH ?? '').split(path.delimiter)) {
|
|
28
|
+
if (!directory) continue;
|
|
29
|
+
for (const command of commands) if (await exists(path.join(directory, command), constants.X_OK)) return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function prepareManager() {
|
|
35
|
+
const binary = path.join(packageDirectory, 'dist/AIShell.app/Contents/Helpers/aishell-mcp');
|
|
36
|
+
let value;
|
|
37
|
+
try { value = JSON.parse(execFileSync(binary, ['--prepare-manager'], { encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] })); }
|
|
38
|
+
catch { throw new SetupError('MANAGER_PREPARATION_FAILED', '管理アプリの準備に失敗しました。aishell-openで状態を確認してください。'); }
|
|
39
|
+
if (!Number.isInteger(value.processIdentifier) || value.processIdentifier <= 0) throw new SetupError('MANAGER_PREPARATION_FAILED', '管理アプリの実行確認が返りませんでした。');
|
|
40
|
+
return { ready: true, processIdentifier: value.processIdentifier };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function smoke(registration, expectedVersion, { env = process.env } = {}) {
|
|
44
|
+
const directory = await mkdtemp(path.join(tmpdir(), 'aishell-setup-smoke-'));
|
|
45
|
+
try {
|
|
46
|
+
await writeFile(path.join(directory, 'probe.txt'), 'AIShell setup smoke\n');
|
|
47
|
+
return await withMCP(registration, async ({ initialized, request, call }) => {
|
|
48
|
+
if (initialized?.serverInfo?.version !== expectedVersion) throw new SetupError('MCP_VERSION_MISMATCH', '登録先のMCP版が導入済みpackageと一致しません。PATHを確認してください。');
|
|
49
|
+
const list = await request('tools/list', {});
|
|
50
|
+
if (!list?.tools?.some(tool => tool.name === 'workspace_snapshot') || !list.tools.some(tool => tool.name === 'apply_change_set')) throw new SetupError('MCP_CAPABILITY_MISMATCH', 'expanded-v1の開発toolを利用できません。AISHELL_TOOL_PROFILEなどのenvを確認してください。');
|
|
51
|
+
const status = (await call('runtime_status')).structuredContent;
|
|
52
|
+
if (status?.isPaused !== false) throw new SetupError('RUNTIME_NOT_READY', 'AIShellが停止中か、状態を確認できません。管理アプリで再開してから再実行してください。');
|
|
53
|
+
const snapshot = (await call('workspace_snapshot', { path: directory, context_budget: 0, entry_limit: 10, project_profile: { mode: 'none' } })).structuredContent;
|
|
54
|
+
if (snapshot?.freshness !== 'fresh' || !snapshot.entries?.some(entry => entry.path === 'probe.txt' || entry.relativePath === 'probe.txt' || entry.path === path.join(directory, 'probe.txt'))) throw new SetupError('MCP_SMOKE_FAILED', '未登録フォルダの実fileをMCPで確認できません。');
|
|
55
|
+
return { version: initialized.serverInfo.version, toolCount: list.tools.length, operation: 'workspace_snapshot', ready: true };
|
|
56
|
+
}, { cwd: directory, env });
|
|
57
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function setup(options = {}, dependencies = {}) {
|
|
61
|
+
const env = dependencies.env ?? process.env;
|
|
62
|
+
const platform = dependencies.platform ?? process.platform;
|
|
63
|
+
const arch = dependencies.arch ?? process.arch;
|
|
64
|
+
// 対応外OSではmacOS commandを含めて一切の準備処理を実行しない。
|
|
65
|
+
const macVersion = platform === 'darwin' ? (dependencies.macVersion ?? execFileSync('/usr/bin/sw_vers', ['-productVersion'], { encoding: 'utf8' }).trim()) : null;
|
|
66
|
+
supportedPlatform(platform, arch, macVersion);
|
|
67
|
+
const home = dependencies.home ?? homedir();
|
|
68
|
+
const chosen = options.ais ? options.ais.map(ai => hostSpec(ai, home, env)) : [];
|
|
69
|
+
if (!options.ais) for (const ai of aiNames) {
|
|
70
|
+
const spec = hostSpec(ai, home, { ...env, CURSOR_HOME: undefined });
|
|
71
|
+
if (await installed(spec, env)) chosen.push(hostSpec(ai, home, env));
|
|
72
|
+
}
|
|
73
|
+
if (!chosen.length) throw new SetupError('AI_NOT_FOUND', '対応AIを検出できません。対象を--ai claude,codex,grok,cursorで指定してください。');
|
|
74
|
+
const report = { schemaVersion: 'aishell.setup.v1', mode: options.check ? 'diagnose' : 'setup', platform: { os: platform, arch, version: macVersion }, manager: null, hosts: [], skipped: aiNames.filter(ai => !chosen.some(spec => spec.ai === ai)) };
|
|
75
|
+
const version = dependencies.version ?? JSON.parse(await (await import('node:fs/promises')).readFile(path.join(packageDirectory, 'package.json'), 'utf8')).version;
|
|
76
|
+
const backupDirectory = path.join(home, 'Library/Application Support/AIShell/setup-backups');
|
|
77
|
+
let stage = 'preflight';
|
|
78
|
+
try {
|
|
79
|
+
// 全対象を先に解析する。壊れた設定があるままappや別AIを更新しない。
|
|
80
|
+
const plans = [];
|
|
81
|
+
for (const spec of chosen) plans.push(await planHost(spec));
|
|
82
|
+
if (!options.check) {
|
|
83
|
+
stage = 'prepare';
|
|
84
|
+
report.manager = await (dependencies.prepare ?? prepareManager)();
|
|
85
|
+
}
|
|
86
|
+
for (const plan of plans) {
|
|
87
|
+
stage = `register:${plan.spec.ai}`;
|
|
88
|
+
if (!options.check) await (dependencies.write ?? writeHost)(plan, backupDirectory);
|
|
89
|
+
else if (plan.changed) throw new SetupError('REGISTRATION_MISMATCH', `${plan.spec.ai}のAIShell登録が正規契約と一致しません。setupを実行してください。`);
|
|
90
|
+
const registration = await verifyHost(plan);
|
|
91
|
+
const host = { ai: plan.spec.ai, registration: options.check ? 'verified' : plan.changed ? 'updated' : 'unchanged', ready: false };
|
|
92
|
+
report.hosts.push(host);
|
|
93
|
+
stage = `host:${plan.spec.ai}`;
|
|
94
|
+
Object.assign(host, await (dependencies.verifyCLI ?? verifyHostCLI)(plan.spec, registration, { check: options.check, env, cwd: dependencies.cwd ?? process.cwd(), backupDirectory }));
|
|
95
|
+
stage = `smoke:${plan.spec.ai}`;
|
|
96
|
+
const result = await (dependencies.smoke ?? smoke)(registration, version, { env });
|
|
97
|
+
Object.assign(host, result);
|
|
98
|
+
}
|
|
99
|
+
return { ...report, status: 'ready' };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
error.report = { ...report, status: 'failed', stage };
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { parseForESLint } from 'toml-eslint-parser';
|
|
2
|
+
import { stringify } from 'smol-toml';
|
|
3
|
+
|
|
4
|
+
const target = ['mcp_servers', 'aishell'];
|
|
5
|
+
const prefix = (left, right) => left.every((key, index) => right[index] === key);
|
|
6
|
+
const keyPath = node => node.keys.map(key => key.type === 'TOMLBare' ? key.name : key.value);
|
|
7
|
+
|
|
8
|
+
export function replaceTopLevelValue(text, key, value) {
|
|
9
|
+
const { ast } = parseForESLint(text);
|
|
10
|
+
const node = ast.body[0].body.find(node => node.type === 'TOMLKeyValue' && keyPath(node.key).length === 1 && keyPath(node.key)[0] === key);
|
|
11
|
+
if (!node) throw new Error('トップレベルの設定値を特定できません。');
|
|
12
|
+
return text.slice(0, node.value.range[0]) + inline(value) + text.slice(node.value.range[1]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function inline(value) {
|
|
16
|
+
if (Array.isArray(value)) return `[${value.map(inline).join(', ')}]`;
|
|
17
|
+
if (value && typeof value === 'object' && !(value instanceof Date)) return `{ ${Object.entries(value).map(([key, item]) => `${JSON.stringify(key)} = ${inline(item)}`).join(', ')} }`;
|
|
18
|
+
return stringify({ value }).slice('value = '.length).trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 構文木の範囲だけを変更し、別server、コメント、float/date表記をそのまま残す。
|
|
22
|
+
// 複数行文字列にある偽のtable headerを正規表現で誤認しない。
|
|
23
|
+
export function replaceTOMLRegistration(text, registration) {
|
|
24
|
+
const { ast } = parseForESLint(text);
|
|
25
|
+
const edits = [];
|
|
26
|
+
let replaced = false;
|
|
27
|
+
function visitKeyValue(node, base) {
|
|
28
|
+
const keys = [...base, ...keyPath(node.key)];
|
|
29
|
+
if (keys.length === target.length && prefix(target, keys)) {
|
|
30
|
+
edits.push({ range: node.value.range, text: inline(registration) });
|
|
31
|
+
replaced = true;
|
|
32
|
+
} else if (prefix(target, keys)) {
|
|
33
|
+
edits.push({ range: node.range, text: '' });
|
|
34
|
+
} else if (prefix(keys, target) && node.value.type === 'TOMLInlineTable') {
|
|
35
|
+
for (const child of node.value.body) visitKeyValue(child, keys);
|
|
36
|
+
if (!replaced) {
|
|
37
|
+
const at = node.value.range[1] - 1;
|
|
38
|
+
edits.push({ range: [at, at], text: `${node.value.body.length ? ', ' : ''}${JSON.stringify(target.at(-1))} = ${inline(registration)}` });
|
|
39
|
+
replaced = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const node of ast.body[0].body) {
|
|
44
|
+
if (node.type === 'TOMLTable') {
|
|
45
|
+
if (prefix(target, node.resolvedKey)) edits.push({ range: node.range, text: '' });
|
|
46
|
+
else for (const child of node.body) visitKeyValue(child, node.resolvedKey);
|
|
47
|
+
} else visitKeyValue(node, []);
|
|
48
|
+
}
|
|
49
|
+
let result = text;
|
|
50
|
+
for (const edit of edits.sort((a, b) => b.range[0] - a.range[0])) result = result.slice(0, edit.range[0]) + edit.text + result.slice(edit.range[1]);
|
|
51
|
+
if (!replaced) result += `\n${stringify({ mcp_servers: { aishell: registration } })}`;
|
|
52
|
+
return result;
|
|
53
|
+
}
|