inscope 0.4.0-canary.0 โ 0.4.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.md +39 -19
- package/dist/bin/index.mjs +38 -38
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +10 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
> #### `cd` into a project and you are the right person: the right GitHub token, the right MCP servers, the right git commit email, all resolved live from `$PWD`. No toggles, no profile switching, and it holds up with several Claude Code sessions open at once.
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/
|
|
13
|
+
<img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo.gif" alt="inscope demo: interactive add, list, and doctor" width="900" />
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
Concurrent sessions in different projects should never bleed work and personal accounts into each other. You describe each workspace once; `inscope` owns the moving parts and keeps them in sync:
|
|
@@ -48,15 +48,18 @@ inscope init
|
|
|
48
48
|
# map a workspace interactively: pick the gh account, git identity, and servers
|
|
49
49
|
inscope add ~/acme
|
|
50
50
|
|
|
51
|
-
# or pass flags to skip the prompts (work gh account, work email, +
|
|
52
|
-
inscope add ~/acme --gh acme --email
|
|
51
|
+
# or pass flags to skip the prompts (work gh account, work email, + servers)
|
|
52
|
+
inscope add ~/acme --gh neeraj-acme-org --email neeraj@acme.org --servers github,linear
|
|
53
53
|
|
|
54
54
|
# map a personal directory: just your gh account and personal email
|
|
55
|
-
inscope add ~/
|
|
55
|
+
inscope add ~/personal --gh nrjdalal --email hello@nrjdalal.com
|
|
56
56
|
|
|
57
57
|
# list what is configured
|
|
58
58
|
inscope list
|
|
59
59
|
|
|
60
|
+
# edit a workspace interactively (gh account, git identity, servers)
|
|
61
|
+
inscope edit acme
|
|
62
|
+
|
|
60
63
|
# verify tokens, identities, and the hook all resolve
|
|
61
64
|
inscope doctor
|
|
62
65
|
|
|
@@ -67,7 +70,11 @@ inscope apply
|
|
|
67
70
|
inscope rm ~/acme
|
|
68
71
|
```
|
|
69
72
|
|
|
70
|
-
`cd ~/acme/api` and you are the work account, with work MCP servers and your work commit email. `cd ~/
|
|
73
|
+
`cd ~/acme/api` and you are the work account, with work MCP servers and your work commit email. `cd ~/personal/blog` and you are you.
|
|
74
|
+
|
|
75
|
+
<p align="center">
|
|
76
|
+
<img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo-switch.gif" alt="inscope switching git identity and tokens on cd" width="900" />
|
|
77
|
+
</p>
|
|
71
78
|
|
|
72
79
|
---
|
|
73
80
|
|
|
@@ -76,7 +83,7 @@ inscope rm ~/acme
|
|
|
76
83
|
- ๐ชช Per-directory identity: GitHub token, git commit email, and MCP servers scoped to `$PWD`
|
|
77
84
|
- ๐งต Race-free across concurrent shells and Claude Code sessions, with no global toggles
|
|
78
85
|
- ๐ No secrets on disk: GitHub tokens from the `gh` keyring, Slack tokens from the macOS Keychain
|
|
79
|
-
- ๐ค Generates a `.mcp.json` per workspace with uniquely named GitHub, Linear, Notion
|
|
86
|
+
- ๐ค Generates a `.mcp.json` per workspace with uniquely named GitHub, Atlassian, Linear, Notion, Plane, Sentry, Slack and Vercel servers
|
|
80
87
|
- โ๏ธ Git `includeIf` rules so every commit lands with the right author email per path
|
|
81
88
|
- ๐ช A single zsh `chpwd` hook does all the resolution; nothing else touches your shell
|
|
82
89
|
- ๐ฉบ `inscope doctor` verifies tokens, identities, and the hook before you trust them
|
|
@@ -108,8 +115,8 @@ inscope init
|
|
|
108
115
|
gh auth login
|
|
109
116
|
|
|
110
117
|
# 3. map your workspaces
|
|
111
|
-
inscope add ~/acme --gh acme
|
|
112
|
-
inscope add ~/
|
|
118
|
+
inscope add ~/acme --gh neeraj-acme-org --email neeraj@acme.org --servers github,linear
|
|
119
|
+
inscope add ~/personal --gh nrjdalal --email hello@nrjdalal.com
|
|
113
120
|
|
|
114
121
|
# 4. reload your shell, then verify
|
|
115
122
|
source ~/.zshrc
|
|
@@ -137,6 +144,10 @@ inscope doctor Verify tokens, identities, and the hook resolve correctly
|
|
|
137
144
|
|
|
138
145
|
Run any command with `-h` for its options.
|
|
139
146
|
|
|
147
|
+
<p align="center">
|
|
148
|
+
<img src="https://raw.githubusercontent.com/nrjdalal/inscope/main/.github/assets/demo-manage.gif" alt="inscope edit and rm with type-to-confirm" width="900" />
|
|
149
|
+
</p>
|
|
150
|
+
|
|
140
151
|
### `inscope add`
|
|
141
152
|
|
|
142
153
|
Run it bare and it walks you through everything: pick the GitHub account from your signed-in `gh` accounts, accept your global git identity or set a per-workspace one, and toggle which MCP servers to enable. Pass any flag to skip its prompt, or `-y` to take the defaults non-interactively (for scripts and CI).
|
|
@@ -146,7 +157,8 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
|
|
|
146
157
|
--email <email> git commit email (omit to inherit your global identity)
|
|
147
158
|
--git-name <name> git commit author name (omit to inherit global)
|
|
148
159
|
--label <name> workspace name; defaults to the directory basename
|
|
149
|
-
--servers <list> comma-separated: github,linear,
|
|
160
|
+
--servers <list> comma-separated, any of: github, atlassian, linear,
|
|
161
|
+
notion, plane, sentry, slack, vercel
|
|
150
162
|
(default: github)
|
|
151
163
|
--slack-keychain <s> keychain service for the Slack token
|
|
152
164
|
(default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
|
|
@@ -174,17 +186,21 @@ Run it bare and it walks you through everything: pick the GitHub account from yo
|
|
|
174
186
|
|
|
175
187
|
Each enabled server is written into the workspace `.mcp.json` with a name suffixed by the workspace label (for example `github-acme`), so servers from different workspaces never collide.
|
|
176
188
|
|
|
177
|
-
| Server
|
|
178
|
-
|
|
|
179
|
-
| `github`
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
189
|
+
| Server | Transport | Token source |
|
|
190
|
+
| ----------- | --------- | ---------------------------------------------- |
|
|
191
|
+
| `github` | http | `GITHUB_TOKEN` from the active `gh` account |
|
|
192
|
+
| `atlassian` | http | OAuth via the Atlassian (Jira/Confluence) MCP |
|
|
193
|
+
| `linear` | http | OAuth via the Linear MCP endpoint |
|
|
194
|
+
| `notion` | http | OAuth via the Notion MCP endpoint |
|
|
195
|
+
| `plane` | http | OAuth via the Plane MCP endpoint |
|
|
196
|
+
| `sentry` | http | OAuth via the Sentry MCP endpoint |
|
|
197
|
+
| `slack` | stdio | `SLACK_MCP_XOXP_TOKEN` from the macOS Keychain |
|
|
198
|
+
| `vercel` | http | OAuth via the Vercel MCP endpoint |
|
|
183
199
|
|
|
184
200
|
Slack is opt-in. Enable it with `--servers ...,slack`, then store the token once:
|
|
185
201
|
|
|
186
202
|
```sh
|
|
187
|
-
inscope add ~/acme --gh acme --servers github,
|
|
203
|
+
inscope add ~/acme --gh neeraj-acme-org --servers github,slack --seed-slack
|
|
188
204
|
```
|
|
189
205
|
|
|
190
206
|
`--seed-slack` prompts for the `xoxp` token and writes it to the Keychain. Pass `--slack-message` to allow the Slack MCP server to post messages.
|
|
@@ -204,16 +220,20 @@ The source of truth is `~/.config/inscope/inscope.json`:
|
|
|
204
220
|
{
|
|
205
221
|
"name": "acme",
|
|
206
222
|
"path": "~/acme",
|
|
207
|
-
"gh": "acme",
|
|
208
|
-
"git": { "email": "
|
|
223
|
+
"gh": "neeraj-acme-org",
|
|
224
|
+
"git": { "email": "neeraj@acme.org" },
|
|
209
225
|
"servers": {
|
|
210
226
|
"github": true,
|
|
227
|
+
"atlassian": false,
|
|
211
228
|
"linear": true,
|
|
212
|
-
"notion":
|
|
229
|
+
"notion": false,
|
|
230
|
+
"plane": false,
|
|
231
|
+
"sentry": false,
|
|
213
232
|
"slack": {
|
|
214
233
|
"keychain": "SLACK_MCP_XOXP_TOKEN_ACME",
|
|
215
234
|
"addMessageTool": false,
|
|
216
235
|
},
|
|
236
|
+
"vercel": false,
|
|
217
237
|
},
|
|
218
238
|
},
|
|
219
239
|
],
|
package/dist/bin/index.mjs
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{parseArgs as e}from"node:util";import t from"node:fs";import n from"node:path";import r from"node:os";import{spawnSync as i}from"node:child_process";import a from"node:readline";const o=()=>r.homedir(),s=()=>process.env.XDG_CONFIG_HOME?.trim()||n.join(o(),`.config`),c=e=>e===`~`?o():e.startsWith(`~/`)?n.join(o(),e.slice(2)):e,l=e=>{let t=c(e),r=o();return t===r?`~`:t.startsWith(r+n.sep)?`~/`+t.slice(r.length+1):t},u=e=>n.resolve(c(e)),d=()=>n.join(s(),`inscope`),f=()=>n.join(d(),`inscope.json`),p=()=>n.join(d(),`inscope.zsh`),m=()=>n.join(d(),`git`),h=()=>n.join(o(),`.gitconfig`),g=()=>n.join(o(),`.zshrc`),_=()=>({version:1,workspaces:[]}),v=()=>t.existsSync(f()),y=()=>{let e=f(),n=t.readFileSync(e,`utf8`),r=JSON.parse(n);return x(r),r},b=e=>{let r=f();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,JSON.stringify(e,null,2)+`
|
|
3
|
-
`)},x=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let t=new Set;for(let n of e.workspaces){if(!n.name)throw Error(`a workspace is missing a name`);if(!n.path)throw Error(`workspace "${n.name}" is missing a path`);if(t.has(n.name))throw Error(`duplicate workspace name "${n.name}"`);t.add(n.name)}},S=e=>n.basename(u(e)),C=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=u(t);return e.workspaces.find(e=>u(e.path)===r)},w=(e,t)=>{let n=e.workspaces.filter(e=>e.name!==t.name);return n.push({...t,path:l(t.path)}),n.sort((e,t)=>e.name.localeCompare(t.name)),{...e,workspaces:n}},ee=(e,t)=>{let n=C(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},
|
|
4
|
-
`)
|
|
5
|
-
`)
|
|
6
|
-
`)
|
|
7
|
-
`)
|
|
8
|
-
`),
|
|
9
|
-
`);let
|
|
10
|
-
`),process.exit(130))};process.stdin.on(`keypress`,
|
|
11
|
-
|
|
12
|
-
`).
|
|
13
|
-
|
|
3
|
+
`)},x=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let t=new Set;for(let n of e.workspaces){if(!n.name)throw Error(`a workspace is missing a name`);if(!n.path)throw Error(`workspace "${n.name}" is missing a path`);if(t.has(n.name))throw Error(`duplicate workspace name "${n.name}"`);t.add(n.name)}},S=e=>n.basename(u(e)),C=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=u(t);return e.workspaces.find(e=>u(e.path)===r)},w=(e,t)=>{let n=e.workspaces.filter(e=>e.name!==t.name);return n.push({...t,path:l(t.path)}),n.sort((e,t)=>e.name.localeCompare(t.name)),{...e,workspaces:n}},ee=(e,t)=>{let n=C(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},te={atlassian:`https://mcp.atlassian.com/v1/mcp`,linear:`https://mcp.linear.app/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,vercel:`https://mcp.vercel.com`},T=[`github`,`atlassian`,`linear`,`notion`,`plane`,`sentry`,`slack`,`vercel`],E=e=>T.map(t=>`${t}-${e}`),D=e=>n.join(u(e.path),`.mcp.json`),ne=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,re=e=>{let t=e.servers,n={};for(let r of T){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@1.3.0`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:ne(i,te[r])}}return n},ie=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{return{}}},O=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{throw Error(`${e} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},ae=e=>{let n=D(e);return t.existsSync(n)?ie(n):null},oe=e=>{let r=D(e);t.mkdirSync(n.dirname(r),{recursive:!0});let i=O(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let t of E(e.name))delete a[t];Object.assign(a,re(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
4
|
+
`)},se=e=>{let n=D(e);if(!t.existsSync(n))return;let r=O(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let t of E(e.name))delete r.mcpServers[t];t.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
5
|
+
`)},k=(e,t,n)=>{let r=i(e,t,{encoding:`utf8`,input:n?.input});return{status:r.status??(r.error?127:1),stdout:r.stdout??``,stderr:r.stderr??``}},ce=()=>process.platform===`darwin`,A=()=>process.env.USER||``,le=(e,t=k)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},ue=(e=k)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},de=(e=k)=>{let t=[];for(let n of ue(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},fe=(e,t=k)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},j=(e,t=k)=>{let n=t(`security`,[`find-generic-password`,`-a`,A(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},pe=(e,t,n=k)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,A(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},me=e=>`security add-generic-password -U -a "${A()||`$USER`}" -s ${e} -w 'xoxp-...'`,he=(e,t=k)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},M=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),N=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)};let P=``;const F=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=P.indexOf(`
|
|
6
|
+
`);if(e<0)return!1;let n=P.slice(0,e).replace(/\r$/,``);return P=P.slice(e+1),t(n),!0};if(n())return;let r=e=>{P+=e.toString(`utf8`),P.includes(`
|
|
7
|
+
`)&&(process.stdin.off(`data`,r),process.stdin.off(`end`,i),process.stdin.pause(),n())},i=()=>{process.stdin.off(`data`,r),process.stdin.off(`end`,i);let e=P.replace(/\r$/,``);P=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),I=async(e,t=``)=>(await F(`${e}${t?` [${t}]`:``}: `)).trim()||t,L=async(e,t=!1)=>{let n=(await F(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t},ge=e=>new Promise(t=>{let n=a.createInterface({input:process.stdin,output:process.stdout}),r=n.output,i=!1;n._writeToOutput=t=>{if(!i){r.write(t),t.includes(e)&&(i=!0);return}},n.question(e,e=>{r.write(`
|
|
8
|
+
`),n.close(),t(e.trim())})}),R=`\x1B[36m`,z=`\x1B[0m`,B=(e,t,n=0)=>new Promise(r=>{if(!M()||t.length===0){r(t[Math.min(n,t.length-1)]?.value);return}let i=Math.max(0,Math.min(n,t.length-1)),o=process.stdout;o.write(e+`
|
|
9
|
+
`);let s=e=>{e||o.write(`\x1b[${t.length}A`);for(let e=0;e<t.length;e++){let n=e===i,r=`${n?`โฏ`:` `} ${t[e].label}`;o.write(`\x1b[2K ${n?R+r+z:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),N(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),N(!1),process.stdin.pause()},l=(e,n)=>{n.name===`up`||n.name===`k`?(i=(i-1+t.length)%t.length,s(!1)):n.name===`down`||n.name===`j`?(i=(i+1)%t.length,s(!1)):n.name===`return`||n.name===`enter`?(c(),r(t[i].value)):n.ctrl&&n.name===`c`&&(c(),o.write(`
|
|
10
|
+
`),process.exit(130))};process.stdin.on(`keypress`,l)}),V=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!M()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
|
|
11
|
+
`);let c=e=>{e||s.write(`\x1b[${t.length}A`);for(let e=0;e<t.length;e++){let n=e===o,i=`${n?`โฏ`:` `} ${r[e]?`โ`:`โฏ`} ${t[e].label}`;s.write(`\x1b[2K ${n?R+i+z:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),N(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),N(!1),process.stdin.pause()},u=(e,a)=>{a.name===`up`||a.name===`k`?(o=(o-1+t.length)%t.length,c(!1)):a.name===`down`||a.name===`j`?(o=(o+1)%t.length,c(!1)):a.name===`space`||e===` `?(r[o]=!r[o],c(!1)):a.name===`return`||a.name===`enter`?(l(),n(i())):a.ctrl&&a.name===`c`&&(l(),s.write(`
|
|
12
|
+
`),process.exit(130))};process.stdin.on(`keypress`,u)}),H=e=>`# >>> inscope:${e} >>>`,U=e=>`# <<< inscope:${e} <<<`,W=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),G=e=>RegExp(`${W(H(e))}\\n[\\s\\S]*?\\n${W(U(e))}\\n?`),_e=(e,t)=>{let n=t.replace(/\n+$/,``);return`${H(e)}\n${n}\n${U(e)}\n`},K=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},ve=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=K(e),o=_e(r,i),s=G(r),c;if(s.test(a))c=a.replace(s,o);else{let e=a.replace(/\n*$/,``);c=e.length?`${e}\n\n${o}`:o}t.writeFileSync(e,c)},ye=(e,n)=>{let r=K(e);if(!r)return;let i=r.replace(G(n),``).replace(/\n{3,}/g,`
|
|
13
|
+
|
|
14
|
+
`).replace(/^\n+/,``);t.writeFileSync(e,i)},be=(e,t)=>{let n=K(e).match(RegExp(`${W(H(t))}\\n([\\s\\S]*?)\\n${W(U(t))}`));return n?n[1]:null},q=`gitconfig`,J=e=>!!(e.git&&(e.git.email||e.git.name)),Y=e=>n.join(m(),`${e}.gitconfig`),xe=e=>l(e).replace(/\/+$/,``)+`/`,Se=e=>e.workspaces.filter(J).map(e=>`[includeIf "gitdir:${xe(e.path)}"]\n\tpath = ${l(Y(e.name))}`).join(`
|
|
15
|
+
`),Ce=e=>{let t=[`# Managed by inscope. Do not edit by hand.`,`[user]`];return e.git?.email&&t.push(`\temail = ${e.git.email}`),e.git?.name&&t.push(`\tname = ${e.git.name}`),t.join(`
|
|
14
16
|
`)+`
|
|
15
|
-
`},
|
|
17
|
+
`},we=e=>{t.mkdirSync(m(),{recursive:!0});for(let n of e.workspaces)J(n)&&t.writeFileSync(Y(n.name),Ce(n));let n=Se(e);n?ve(h(),q,n):ye(h(),q)},Te=e=>{let n=Y(e);t.existsSync(n)&&t.rmSync(n)},Ee=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},De=e=>e.servers.slack?e.servers.slack.keychain:``,Oe=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
|
|
16
18
|
# Source of truth: ~/.config/inscope/inscope.json
|
|
17
19
|
# Edit there, then run \`inscope apply\` to regenerate this file.
|
|
18
20
|
#
|
|
@@ -23,7 +25,7 @@ import{parseArgs as e}from"node:util";import t from"node:fs";import n from"node:
|
|
|
23
25
|
__inscope_resolve_identity() {
|
|
24
26
|
local ws
|
|
25
27
|
case "\${PWD}/" in
|
|
26
|
-
${t.map(e=>` ${
|
|
28
|
+
${t.map(e=>` ${Ee(e.path)}) ws=${e.name} ;;`).join(`
|
|
27
29
|
`)||` # no workspaces configured`}
|
|
28
30
|
*) ws="" ;;
|
|
29
31
|
esac
|
|
@@ -33,7 +35,7 @@ ${t.map(e=>` ${Se(e.path)}) ws=${e.name} ;;`).join(`
|
|
|
33
35
|
|
|
34
36
|
local gh_user="" slack_svc=""
|
|
35
37
|
case "$ws" in
|
|
36
|
-
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=
|
|
38
|
+
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=De(e);return n&&t.push(`slack_svc=${n}`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
|
|
37
39
|
`)||` # no workspaces configured`}
|
|
38
40
|
*) return ;; # outside a mapped workspace: nothing set
|
|
39
41
|
esac
|
|
@@ -59,87 +61,85 @@ autoload -Uz add-zsh-hook
|
|
|
59
61
|
add-zsh-hook chpwd __inscope_resolve_identity
|
|
60
62
|
__inscope_ws="__init__" # force the first resolve, clearing any inherited token
|
|
61
63
|
__inscope_resolve_identity
|
|
62
|
-
`},
|
|
63
|
-
`)},ke=e=>{let n=G(e);if(!t.existsSync(n))return;let r=q(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let t of W(e.name))delete r.mcpServers[t];t.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
64
|
-
`)},Ae=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},J=()=>{let e=Ae(p());return`[ -r "${e}" ] && source "${e}"`},je=e=>{let t=J();if(e.includes(t))return e;let n=e.replace(/\n*$/,``),r=`# inscope: load each workspace's tokens (GitHub, Slack) from \$PWD on every cd\n${t}`;return n.length?`${n}\n\n${r}\n`:`${r}\n`},Me=()=>{let e=g(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=je(n);r!==n&&t.writeFileSync(e,r)},Ne=()=>{try{return t.readFileSync(g(),`utf8`).includes(J())}catch{return!1}},Y=e=>{let r=p();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,U(e)),be(e),Me();let i=[];for(let t of e.workspaces)Oe(t),i.push(G(t));return{hook:r,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},X=`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`,Pe=[`github`,`linear`,`notion`,`slack`],Fe=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,Ie=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean),Le=(e,t)=>({github:e.includes(`github`),linear:e.includes(`linear`),notion:e.includes(`notion`),slack:t?{keychain:t.keychain,addMessageTool:t.addMessageTool}:!1}),Re=e=>{let t=w(v()?y():_(),e);b(t),Y(t)},ze=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await ce(`Paste the Slack xoxp token for ${n}: `);e?(ae(n,e),console.log(`โ stored ${n} in the macOS keychain`)):console.error(`No token entered; skipped keychain write.`)}else O(n)||console.log(`\nSlack token not in the keychain yet. Create a Slack app (xoxp user OAuth):\n ${X}\nthen store the token once with:\n ${oe(n)}`)};var Z=`inscope`,Be=`0.4.0-canary.0`,Q={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const $=`Map a directory to a GitHub account, git email, and MCP servers.
|
|
64
|
+
`},ke=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},Ae=()=>{let e=ke(p());return`[ -r "${e}" ] && source "${e}"`},je=e=>{let t=Ae();if(e.includes(t))return e;let n=e.replace(/\n*$/,``),r=`# inscope: load each workspace's tokens (GitHub, Slack) from \$PWD on every cd\n${t}`;return n.length?`${n}\n\n${r}\n`:`${r}\n`},Me=()=>{let e=g(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=je(n);r!==n&&t.writeFileSync(e,r)},Ne=()=>{try{return t.readFileSync(g(),`utf8`).includes(Ae())}catch{return!1}},X=e=>{let r=p();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,Oe(e)),we(e),Me();let i=[];for(let t of e.workspaces)oe(t),i.push(D(t));return{hook:r,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},Z=`https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth`,Pe=T,Fe=e=>`SLACK_MCP_XOXP_TOKEN_${e.toUpperCase().replace(/[^A-Z0-9]+/g,`_`)}`,Ie=e=>T.filter(t=>!!e[t]),Le=(e,t)=>{let n={};for(let r of T)n[r]=r===`slack`?t?{keychain:t.keychain,addMessageTool:t.addMessageTool}:!1:e.includes(r);return n},Re=e=>{let t=w(v()?y():_(),e);b(t),X(t)},ze=async(e,t)=>{if(!e.servers.slack)return;let n=e.servers.slack.keychain;if(t){let e=await ge(`Paste the Slack xoxp token for ${n}: `);e?(pe(n,e),console.log(`โ stored ${n} in the macOS keychain`)):console.error(`No token entered; skipped keychain write.`)}else j(n)||console.log(`\nSlack token not in the keychain yet. Create a Slack app (xoxp user OAuth):\n ${Z}\nthen store the token once with:\n ${me(n)}`)};var Q=`inscope`,Be=`0.4.0`,$={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const Ve=`Map a directory to a GitHub account, git email, and MCP servers.
|
|
65
65
|
Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
|
|
66
66
|
with the same path or label updates that workspace.
|
|
67
67
|
|
|
68
68
|
Usage:
|
|
69
|
-
$ ${
|
|
69
|
+
$ ${Q} add [path] [options]
|
|
70
70
|
|
|
71
71
|
Options:
|
|
72
72
|
--gh <account> gh account whose token this workspace uses
|
|
73
73
|
--email <email> git commit email (omit to inherit your global identity)
|
|
74
74
|
--git-name <name> git commit author name (omit to inherit global)
|
|
75
75
|
--label <name> workspace name; defaults to the directory basename
|
|
76
|
-
--servers <list> comma-separated: github,linear,
|
|
76
|
+
--servers <list> comma-separated, any of: github, atlassian, linear,
|
|
77
|
+
notion, plane, sentry, slack, vercel
|
|
77
78
|
(default: github)
|
|
78
79
|
--slack-keychain <s> keychain service for the Slack token
|
|
79
80
|
(default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
|
|
80
81
|
--slack-message allow the Slack MCP server to post messages
|
|
81
82
|
--seed-slack prompt for the Slack token and store it in the keychain
|
|
82
83
|
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
83
|
-
-h, --help Display help message`,
|
|
84
|
+
-h, --help Display help message`,He=T.map(e=>({label:e,value:e,checked:e===`github`})),Ue=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`},gh:{type:`string`},email:{type:`string`},"git-name":{type:`string`},label:{type:`string`},servers:{type:`string`},"slack-keychain":{type:`string`},"slack-message":{type:`boolean`},"seed-slack":{type:`boolean`}},args:t});r.help&&(console.log(Ve),process.exit(0));let i=M()&&!r.yes,a=n[0];if(!a)if(i)a=await I(`Workspace directory`,process.cwd());else throw Error(Ve);let o=r.label||S(a);i&&!r.label&&(o=await I(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await B(`GitHub account for this workspace`,[...de().map(e=>({label:e,value:e})),{label:`(none)`,value:``}])||void 0);let c=r.email,u=r[`git-name`];if(i){if(c===void 0){let e=fe(`user.email`);c=await I(`Git email${e?` [${e} ยท global]`:``} (enter to inherit global)`)||void 0}if(u===void 0){let e=fe(`user.name`);u=await I(`Git name${e?` [${e} ยท global]`:``} (enter to inherit global)`)||void 0}}let d;d=r.servers===void 0?i?await V(`MCP servers (space toggles, enter confirms)`,He):[`github`]:r.servers.split(`,`).map(e=>e.trim()).filter(Boolean);let f=d.includes(`slack`)||!!r[`slack-keychain`]||!!r[`seed-slack`],p=r[`slack-keychain`]||Fe(o),m=!!r[`slack-message`],h=!!r[`seed-slack`];f&&i&&(console.log(`\nSlack uses a user OAuth (xoxp) token. If you haven't created the app yet,\nfollow the setup guide:\n ${Z}`),r[`slack-keychain`]||(p=await I(`Slack keychain service`,p)),r[`slack-message`]||(m=await L(`Allow Slack to post messages?`,!0)),r[`seed-slack`]||(h=await L(`Store the Slack token now?`,!0)));let g={name:o,path:l(a),gh:s,git:c||u?{email:c,name:u}:void 0,servers:Le(d,f?{keychain:p,addMessageTool:m}:null)};Re(g),console.log(`\nโ workspace "${o}" -> ${g.path}`),console.log(`โ regenerated the hook, git includes, and ${g.path}/.mcp.json`),await ze(g,h),console.log(`\nLaunch \`claude\` from ${g.path} (or relaunch) to pick up the new identity.`),process.exit(0)},We=`Regenerate the chpwd hook, git includes, and every .mcp.json
|
|
84
85
|
from your config. Idempotent: run it any time the config changes.
|
|
85
86
|
|
|
86
87
|
Usage:
|
|
87
|
-
$ ${
|
|
88
|
+
$ ${Q} apply
|
|
88
89
|
|
|
89
90
|
Options:
|
|
90
|
-
-h, --help Display help message`,
|
|
91
|
+
-h, --help Display help message`,Ge=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(We),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y(),i=X(r);console.log(`โ hook ${i.hook}`),i.gitconfig&&console.log(`โ gitconfig ~/.gitconfig (includeIf block)`);for(let e of i.mcp)console.log(`โ mcp ${e}`);console.log(`\nApplied ${r.workspaces.length} workspace(s).`),process.exit(0)},Ke=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},qe=e=>{let t=[],n=e?.mcpServers;if(!n||typeof n!=`object`)return t;for(let[e,r]of Object.entries(n)){let n=Array.isArray(r?.args)?r.args:[];if(n.some(e=>typeof e==`string`&&/@latest$/.test(e)))t.push(e);else if(r?.command===`npx`){let r=n.find(e=>typeof e==`string`&&!e.startsWith(`-`));r&&!r.includes(`@`)&&t.push(e)}}return t},Je=(e,t=process.cwd())=>{let r=n.resolve(t);return e.workspaces.find(e=>{let t=u(e.path);return r===t||r.startsWith(t+n.sep)})},Ye=(e=k)=>{let t=e(`gh`,[`api`,`user`,`--jq`,`.login`]),n=e(`git`,[`config`,`user.email`]);return{pwd:process.cwd(),gh:t.status===0&&t.stdout.trim()?t.stdout.trim():`none`,gitEmail:n.status===0?n.stdout.trim():`none`,tokenSet:!!process.env.GITHUB_TOKEN}},Xe=(e,n=k)=>{let r=[];ce()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=p(),a=Ke(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===Oe(e)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(Ne()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),e.workspaces.some(J)&&r.push(be(h(),q)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let i of e.workspaces){let e=`[${i.name}]`;if(i.gh&&r.push(le(i.gh,n)?{status:`ok`,label:`${e} gh`,detail:`token for ${i.gh}`}:{status:`fail`,label:`${e} gh`,detail:`no token for ${i.gh}; run \`gh auth login\``}),i.servers.slack){let t=i.servers.slack.keychain;r.push(j(t,n)?{status:`ok`,label:`${e} slack`,detail:t}:{status:`fail`,label:`${e} slack`,detail:`${t} not in keychain; run \`${me(t)}\``})}if(J(i)){let a=Y(i.name);if(!t.existsSync(a))r.push({status:`fail`,label:`${e} git`,detail:`missing ${a}; run \`inscope apply\``});else if(i.git?.email){let t=he(a,n);r.push(t===i.git.email?{status:`ok`,label:`${e} git`,detail:i.git.email}:{status:`fail`,label:`${e} git`,detail:`email is ${t??`unset`}, expected ${i.git.email}`})}}let a=ae(i);if(a===null)r.push({status:`warn`,label:`${e} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let t=E(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${e} mcp`,detail:`${t.length} server(s)`});let n=qe(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},Ze=`Verify the setup: gh tokens resolve, keychain entries exist,
|
|
91
92
|
git emails match per path, the hook is current, and no MCP server is unpinned.
|
|
92
93
|
Exits non-zero if any check fails.
|
|
93
94
|
|
|
94
95
|
Usage:
|
|
95
|
-
$ ${
|
|
96
|
+
$ ${Q} doctor
|
|
96
97
|
|
|
97
98
|
Options:
|
|
98
|
-
-h, --help Display help message`,
|
|
99
|
-
All checks passed.`),process.exit(0)}
|
|
99
|
+
-h, --help Display help message`,Qe={ok:`โ`,warn:`!`,fail:`โ`},$e=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ze),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y(),i=Xe(r);for(let e of i)console.log(`${Qe[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=Je(r);if(a){let e=Ye();console.log(`\nThis shell (${a.name}):`),console.log(` pwd ${e.pwd}`),console.log(` gh ${e.gh}`),console.log(` git ${e.gitEmail}`),console.log(` token ${e.tokenSet?`set`:`unset`}`)}let o=i.filter(e=>e.status===`fail`).length;o&&(console.log(`\n${o} check(s) failed.`),process.exit(1)),console.log(`
|
|
100
|
+
All checks passed.`),process.exit(0)},et=`Edit a configured workspace interactively, then re-apply.
|
|
100
101
|
Pick a workspace (or pass its path/label), step through the prompts pre-filled
|
|
101
102
|
with its current values, and inscope regenerates everything on save.
|
|
102
103
|
|
|
103
104
|
Usage:
|
|
104
|
-
$ ${
|
|
105
|
+
$ ${Q} edit [path|label]
|
|
105
106
|
|
|
106
107
|
Options:
|
|
107
|
-
-h, --help Display help message`,
|
|
108
|
+
-h, --help Display help message`,tt=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});r.help&&(console.log(et),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let i=y();i.workspaces.length||(console.error(`No workspaces yet. Add one with \`${Q} add <path>\`.`),process.exit(1));let a=n[0],o=await(async()=>{if(a){let e=C(i,a);return e||(console.error(`No workspace matching "${a}".`),process.exit(1)),e}if(i.workspaces.length===1)return i.workspaces[0];if(M())return B(`Edit which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e})));console.error(`Specify a workspace, e.g. \`${Q} edit <label>\`.`),process.exit(1)})();console.log(`\nEditing "${o.name}" (${o.path})\n`);let s=[...de().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await B(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await I(l?`Git email (enter keeps ${l}, "-" to inherit global)`:`Git email (enter to inherit global)`,l??``),d=u===`-`?void 0:u||void 0,f=o.git?.name,p=await I(f?`Git name (enter keeps ${f}, "-" to inherit global)`:`Git name (enter to inherit global)`,f??``),m=p===`-`?void 0:p||void 0,h=Ie(o.servers),g=await V(`MCP servers (space toggles, enter confirms)`,Pe.map(e=>({label:e,value:e,checked:h.includes(e)}))),_=g.includes(`slack`),b=o.servers.slack?o.servers.slack.keychain:Fe(o.name),x=o.servers.slack?!!o.servers.slack.addMessageTool:!1,S=!1;_&&(console.log(`\nSlack uses a user OAuth (xoxp) token. Setup guide:\n ${Z}`),b=await I(`Slack keychain service`,b),x=await L(`Allow Slack to post messages?`,x),j(b)||(S=await L(`Store the Slack token now?`,!0)));let w={name:o.name,path:o.path,gh:c,git:d||m?{email:d,name:m}:void 0,servers:Le(g,_?{keychain:b,addMessageTool:x}:null)};Re(w),console.log(`\nโ updated "${w.name}" -> ${w.path}`),await ze(w,S),console.log(`\nRelaunch \`claude\` from ${w.path} to pick up the changes.`),process.exit(0)},nt=`Set up inscope: create the config, generate the chpwd hook, and
|
|
108
109
|
source it from ~/.zshrc. Safe to run again; it never overwrites your config.
|
|
109
110
|
|
|
110
111
|
Usage:
|
|
111
|
-
$ ${
|
|
112
|
+
$ ${Q} init
|
|
112
113
|
|
|
113
114
|
Options:
|
|
114
|
-
-h, --help Display help message`,
|
|
115
|
+
-h, --help Display help message`,rt=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(nt),process.exit(0));let r;v()?(r=y(),console.log(`Using existing config at ${f()}`)):(r=_(),b(r),console.log(`Created ${f()}`)),X(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
|
|
115
116
|
Next steps:
|
|
116
117
|
1. Reload your shell: source ~/.zshrc (or open a new terminal)
|
|
117
118
|
2. Sign each GitHub account in: gh auth login
|
|
118
|
-
3. Map a workspace: ${
|
|
119
|
-
`),process.exit(0)},rt=`List the configured workspaces. Run \`${Z} doctor\` to verify
|
|
119
|
+
3. Map a workspace: ${Q} add ~/acme --gh acme --email you@acme.com`),process.exit(0)},it=`List the configured workspaces. Run \`${Q} doctor\` to verify
|
|
120
120
|
that their tokens and identities actually resolve.
|
|
121
121
|
|
|
122
122
|
Usage:
|
|
123
|
-
$ ${
|
|
123
|
+
$ ${Q} list
|
|
124
124
|
|
|
125
125
|
Options:
|
|
126
|
-
-h, --help Display help message`,
|
|
126
|
+
-h, --help Display help message`,at=e=>T.filter(t=>!!e[t]).join(`, `)||`none`,ot=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(it),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let r=y();r.workspaces.length||(console.log(`No workspaces yet. Add one with \`${Q} add <path> --gh <account>\`.`),process.exit(0));for(let e of r.workspaces)console.log(`${e.name}`),console.log(` path ${e.path}`),console.log(` gh ${e.gh??`(none)`}`),console.log(` git ${e.git?.email??`(default)`}`),console.log(` servers ${at(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},st=`Remove a workspace mapping. Drops its git include and the MCP
|
|
127
127
|
servers inscope manages; leaves your keychain and gh accounts untouched. Pick a
|
|
128
128
|
workspace, or pass its path/label.
|
|
129
129
|
|
|
130
130
|
Usage:
|
|
131
|
-
$ ${
|
|
131
|
+
$ ${Q} rm [path|label]
|
|
132
132
|
|
|
133
133
|
Options:
|
|
134
134
|
-y, --yes Skip the type-the-label confirmation
|
|
135
|
-
-h, --help Display help message`,
|
|
136
|
-
${
|
|
135
|
+
-h, --help Display help message`,ct=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`},yes:{type:`boolean`,short:`y`}},args:t});r.help&&(console.log(st),process.exit(0)),v()||(console.error(`No config found. Run \`${Q} init\` first.`),process.exit(1));let i=y();i.workspaces.length||(console.error(`No workspaces to remove.`),process.exit(1));let a=n[0],o;if(a){let e=C(i,a);e||(console.error(`No workspace matching "${a}".`),process.exit(1)),o=e}else M()?o=await B(`Remove which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e}))):(console.error(`Specify a workspace, e.g. \`${Q} rm <label>\`.`),process.exit(1));if(!r.yes){console.log(`\nโ Removing "${o.name}" (${o.path}) unmaps it from inscope.`);let e=await I(`Type "${o.name}" to confirm`);e!==o.name&&(console.error(`Aborted: "${e}" does not match "${o.name}".`),process.exit(1))}let{cfg:s}=ee(i,o.name);se(o),Te(o.name),b(s),X(s),console.log(`โ removed workspace "${o.name}"`),o.servers.slack&&console.log(`Note: the keychain entry ${o.servers.slack.keychain} was left in place.\nDelete it with: security delete-generic-password -s ${o.servers.slack.keychain}`),process.exit(0)},lt=`Version:
|
|
136
|
+
${Q}@${Be}
|
|
137
137
|
|
|
138
138
|
Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
|
|
139
139
|
commit identity to the directory you are in, so concurrent sessions never clash.
|
|
140
140
|
|
|
141
141
|
Usage:
|
|
142
|
-
$ ${
|
|
142
|
+
$ ${Q} <command> [options]
|
|
143
143
|
|
|
144
144
|
Commands:
|
|
145
145
|
init Create the config, generate the hook, source it from ~/.zshrc
|
|
@@ -155,4 +155,4 @@ Options:
|
|
|
155
155
|
-h, --help Display help
|
|
156
156
|
|
|
157
157
|
Author:
|
|
158
|
-
${
|
|
158
|
+
${$.name} <${$.email}> (${$.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return rt(n);case`add`:return await Ue(n);case`edit`:return tt(n);case`rm`:case`remove`:return await ct(n);case`ls`:case`list`:return ot(n);case`apply`:case`sync`:return Ge(n);case`doctor`:return $e(n)}(t===`-v`||t===`--version`)&&(console.log(`${Q}@${Be}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(lt),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(lt),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
|
package/dist/index.d.mts
CHANGED
|
@@ -8,9 +8,13 @@ type HttpServer = {
|
|
|
8
8
|
};
|
|
9
9
|
type Servers = {
|
|
10
10
|
github?: boolean;
|
|
11
|
+
atlassian?: boolean | HttpServer;
|
|
11
12
|
linear?: boolean | HttpServer;
|
|
12
13
|
notion?: boolean | HttpServer;
|
|
14
|
+
plane?: boolean | HttpServer;
|
|
15
|
+
sentry?: boolean | HttpServer;
|
|
13
16
|
slack?: SlackServer | false;
|
|
17
|
+
vercel?: boolean | HttpServer;
|
|
14
18
|
};
|
|
15
19
|
type Workspace = {
|
|
16
20
|
name: string;
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import n from"node:os";import{spawnSync as r}from"node:child_process";const i=()=>n.homedir(),a=()=>process.env.XDG_CONFIG_HOME?.trim()||t.join(i(),`.config`),o=e=>e===`~`?i():e.startsWith(`~/`)?t.join(i(),e.slice(2)):e,s=e=>{let n=o(e),r=i();return n===r?`~`:n.startsWith(r+t.sep)?`~/`+n.slice(r.length+1):n},c=e=>t.resolve(o(e)),l=()=>t.join(a(),`inscope`),u=()=>t.join(l(),`inscope.json`),d=()=>t.join(l(),`inscope.zsh`),f=()=>t.join(l(),`git`),p=()=>t.join(i(),`.gitconfig`),m=()=>t.join(i(),`.zshrc`),ee=1,
|
|
2
|
-
`)},
|
|
1
|
+
import e from"node:fs";import t from"node:path";import n from"node:os";import{spawnSync as r}from"node:child_process";const i=()=>n.homedir(),a=()=>process.env.XDG_CONFIG_HOME?.trim()||t.join(i(),`.config`),o=e=>e===`~`?i():e.startsWith(`~/`)?t.join(i(),e.slice(2)):e,s=e=>{let n=o(e),r=i();return n===r?`~`:n.startsWith(r+t.sep)?`~/`+n.slice(r.length+1):n},c=e=>t.resolve(o(e)),l=()=>t.join(a(),`inscope`),u=()=>t.join(l(),`inscope.json`),d=()=>t.join(l(),`inscope.zsh`),f=()=>t.join(l(),`git`),p=()=>t.join(i(),`.gitconfig`),m=()=>t.join(i(),`.zshrc`),ee=1,h=()=>({version:1,workspaces:[]}),g=()=>e.existsSync(u()),te=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n);return _(r),r},ne=n=>{let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(n,null,2)+`
|
|
2
|
+
`)},_=e=>{if(!e||typeof e!=`object`)throw Error(`config is not an object`);if(!Array.isArray(e.workspaces))throw Error(`config.workspaces must be an array`);let t=new Set;for(let n of e.workspaces){if(!n.name)throw Error(`a workspace is missing a name`);if(!n.path)throw Error(`workspace "${n.name}" is missing a path`);if(t.has(n.name))throw Error(`duplicate workspace name "${n.name}"`);t.add(n.name)}},re=e=>t.basename(c(e)),v=(e,t)=>{let n=e.workspaces.find(e=>e.name===t);if(n)return n;let r=c(t);return e.workspaces.find(e=>c(e.path)===r)},ie=(e,t)=>{let n=e.workspaces.filter(e=>e.name!==t.name);return n.push({...t,path:s(t.path)}),n.sort((e,t)=>e.name.localeCompare(t.name)),{...e,workspaces:n}},ae=(e,t)=>{let n=v(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},y=e=>`# >>> inscope:${e} >>>`,b=e=>`# <<< inscope:${e} <<<`,x=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),S=e=>RegExp(`${x(y(e))}\\n[\\s\\S]*?\\n${x(b(e))}\\n?`),oe=(e,t)=>{let n=t.replace(/\n+$/,``);return`${y(e)}\n${n}\n${b(e)}\n`},C=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},se=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=C(n),o=oe(r,i),s=S(r),c;if(s.test(a))c=a.replace(s,o);else{let e=a.replace(/\n*$/,``);c=e.length?`${e}\n\n${o}`:o}e.writeFileSync(n,c)},ce=(t,n)=>{let r=C(t);if(!r)return;let i=r.replace(S(n),``).replace(/\n{3,}/g,`
|
|
3
3
|
|
|
4
|
-
`).replace(/^\n+/,``);e.writeFileSync(t,i)},le=(e,t)=>{let n=
|
|
5
|
-
`),
|
|
4
|
+
`).replace(/^\n+/,``);e.writeFileSync(t,i)},le=(e,t)=>{let n=C(e).match(RegExp(`${x(y(t))}\\n([\\s\\S]*?)\\n${x(b(t))}`));return n?n[1]:null},w=`gitconfig`,T=e=>!!(e.git&&(e.git.email||e.git.name)),E=e=>t.join(f(),`${e}.gitconfig`),ue=e=>s(e).replace(/\/+$/,``)+`/`,D=e=>e.workspaces.filter(T).map(e=>`[includeIf "gitdir:${ue(e.path)}"]\n\tpath = ${s(E(e.name))}`).join(`
|
|
5
|
+
`),O=e=>{let t=[`# Managed by inscope. Do not edit by hand.`,`[user]`];return e.git?.email&&t.push(`\temail = ${e.git.email}`),e.git?.name&&t.push(`\tname = ${e.git.name}`),t.join(`
|
|
6
6
|
`)+`
|
|
7
|
-
`},
|
|
7
|
+
`},k=t=>{e.mkdirSync(f(),{recursive:!0});for(let n of t.workspaces)T(n)&&e.writeFileSync(E(n.name),O(n));let n=D(t);n?se(p(),w,n):ce(p(),w)},de=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},fe=e=>e.servers.slack?e.servers.slack.keychain:``,A=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
|
|
8
8
|
# Source of truth: ~/.config/inscope/inscope.json
|
|
9
9
|
# Edit there, then run \`inscope apply\` to regenerate this file.
|
|
10
10
|
#
|
|
@@ -15,7 +15,7 @@ import e from"node:fs";import t from"node:path";import n from"node:os";import{sp
|
|
|
15
15
|
__inscope_resolve_identity() {
|
|
16
16
|
local ws
|
|
17
17
|
case "\${PWD}/" in
|
|
18
|
-
${t.map(e=>` ${
|
|
18
|
+
${t.map(e=>` ${de(e.path)}) ws=${e.name} ;;`).join(`
|
|
19
19
|
`)||` # no workspaces configured`}
|
|
20
20
|
*) ws="" ;;
|
|
21
21
|
esac
|
|
@@ -25,7 +25,7 @@ ${t.map(e=>` ${ue(e.path)}) ws=${e.name} ;;`).join(`
|
|
|
25
25
|
|
|
26
26
|
local gh_user="" slack_svc=""
|
|
27
27
|
case "$ws" in
|
|
28
|
-
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=
|
|
28
|
+
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=fe(e);return n&&t.push(`slack_svc=${n}`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
|
|
29
29
|
`)||` # no workspaces configured`}
|
|
30
30
|
*) return ;; # outside a mapped workspace: nothing set
|
|
31
31
|
esac
|
|
@@ -51,6 +51,6 @@ autoload -Uz add-zsh-hook
|
|
|
51
51
|
add-zsh-hook chpwd __inscope_resolve_identity
|
|
52
52
|
__inscope_ws="__init__" # force the first resolve, clearing any inherited token
|
|
53
53
|
__inscope_resolve_identity
|
|
54
|
-
`},
|
|
55
|
-
`)},
|
|
56
|
-
`)},
|
|
54
|
+
`},j=`1.3.0`,pe={atlassian:`https://mcp.atlassian.com/v1/mcp`,linear:`https://mcp.linear.app/mcp`,notion:`https://mcp.notion.com/mcp`,plane:`https://mcp.plane.so/http/mcp`,sentry:`https://mcp.sentry.dev/mcp`,vercel:`https://mcp.vercel.com`},M=[`github`,`atlassian`,`linear`,`notion`,`plane`,`sentry`,`slack`,`vercel`],N=e=>M.map(t=>`${t}-${e}`),P=e=>t.join(c(e.path),`.mcp.json`),me=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,F=e=>{let t=e.servers,n={};for(let r of M){let i=t[r];if(!i)continue;let a=`${r}-${e.name}`;if(r===`github`)n[a]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}};else if(r===`slack`){let e=i,t={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};e.addMessageTool&&(t.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[a]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@${j}`,`--transport`,`stdio`],env:t}}else n[a]={type:`http`,url:me(i,pe[r])}}return n},I=e=>({mcpServers:F(e)}),L=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},R=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{throw Error(`${t} is not valid JSON; fix or remove it, then re-run inscope (left it untouched)`)}},z=t=>{let n=P(t);return e.existsSync(n)?L(n):null},B=n=>{let r=P(n);e.mkdirSync(t.dirname(r),{recursive:!0});let i=R(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let e of N(n.name))delete a[e];Object.assign(a,F(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
55
|
+
`)},he=t=>{let n=P(t);if(!e.existsSync(n))return;let r=R(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of N(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
56
|
+
`)},ge=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},V=()=>{let e=ge(d());return`[ -r "${e}" ] && source "${e}"`},H=e=>{let t=V();if(e.includes(t))return e;let n=e.replace(/\n*$/,``),r=`# inscope: load each workspace's tokens (GitHub, Slack) from \$PWD on every cd\n${t}`;return n.length?`${n}\n\n${r}\n`:`${r}\n`},U=()=>{let t=m(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=H(n);r!==n&&e.writeFileSync(t,r)},W=()=>{try{return e.readFileSync(m(),`utf8`).includes(V())}catch{return!1}},_e=n=>{let r=d();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,A(n)),k(n),U();let i=[];for(let e of n.workspaces)B(e),i.push(P(e));return{hook:r,gitconfig:n.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},G=(e,t,n)=>{let i=r(e,t,{encoding:`utf8`,input:n?.input});return{status:i.status??(i.error?127:1),stdout:i.stdout??``,stderr:i.stderr??``}},K=()=>process.platform===`darwin`,q=()=>process.env.USER||``,J=(e,t=G)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Y=(e=G)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},ve=(e=G)=>{let t=[];for(let n of Y(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},ye=(e,t=G)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},X=(e,t=G)=>{let n=t(`security`,[`find-generic-password`,`-a`,q(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},be=(e,t,n=G)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,q(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Z=e=>`security add-generic-password -U -a "${q()||`$USER`}" -s ${e} -w 'xoxp-...'`,Q=(e,t=G)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},xe=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},$=e=>{let t=[],n=e?.mcpServers;if(!n||typeof n!=`object`)return t;for(let[e,r]of Object.entries(n)){let n=Array.isArray(r?.args)?r.args:[];if(n.some(e=>typeof e==`string`&&/@latest$/.test(e)))t.push(e);else if(r?.command===`npx`){let r=n.find(e=>typeof e==`string`&&!e.startsWith(`-`));r&&!r.includes(`@`)&&t.push(e)}}return t},Se=(e,n=process.cwd())=>{let r=t.resolve(n);return e.workspaces.find(e=>{let n=c(e.path);return r===n||r.startsWith(n+t.sep)})},Ce=(e=G)=>{let t=e(`gh`,[`api`,`user`,`--jq`,`.login`]),n=e(`git`,[`config`,`user.email`]);return{pwd:process.cwd(),gh:t.status===0&&t.stdout.trim()?t.stdout.trim():`none`,gitEmail:n.status===0?n.stdout.trim():`none`,tokenSet:!!process.env.GITHUB_TOKEN}},we=(t,n=G)=>{let r=[];K()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=d(),a=xe(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===A(t)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(W()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),t.workspaces.some(T)&&r.push(le(p(),w)===null?{status:`fail`,label:`gitconfig`,detail:"missing includeIf block; run `inscope apply`"}:{status:`ok`,label:`gitconfig`,detail:`includeIf block present`});for(let i of t.workspaces){let t=`[${i.name}]`;if(i.gh&&r.push(J(i.gh,n)?{status:`ok`,label:`${t} gh`,detail:`token for ${i.gh}`}:{status:`fail`,label:`${t} gh`,detail:`no token for ${i.gh}; run \`gh auth login\``}),i.servers.slack){let e=i.servers.slack.keychain;r.push(X(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Z(e)}\``})}if(T(i)){let a=E(i.name);if(!e.existsSync(a))r.push({status:`fail`,label:`${t} git`,detail:`missing ${a}; run \`inscope apply\``});else if(i.git?.email){let e=Q(a,n);r.push(e===i.git.email?{status:`ok`,label:`${t} git`,detail:i.git.email}:{status:`fail`,label:`${t} git`,detail:`email is ${e??`unset`}, expected ${i.git.email}`})}}let a=z(i);if(a===null)r.push({status:`warn`,label:`${t} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let e=N(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${t} mcp`,detail:`${e.length} server(s)`});let n=$(a);n.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r};export{ee as CONFIG_VERSION,j as SLACK_MCP_VERSION,_e as applyAll,k as applyGitconfig,B as applyMcp,g as configExists,Se as currentWorkspace,h as defaultConfig,G as defaultRunner,U as ensureZshrcSource,v as findWorkspace,ve as ghAccounts,Y as ghStatus,J as ghToken,Q as gitEmailForFile,ye as gitGlobal,K as isMacOS,X as keychainHas,be as keychainSet,Z as keychainSetCommand,re as labelFromPath,Ce as liveSnapshot,te as loadConfig,N as managedKeys,P as mcpFilePath,z as readMcp,he as removeMcp,ae as removeWorkspace,D as renderGitInclude,A as renderHook,I as renderMcp,O as renderPerWorkspaceGitconfig,F as renderServers,H as renderZshrcSource,we as runDoctor,ne as saveConfig,ie as upsertWorkspace,_ as validateConfig,W as zshrcSourcesHook};
|
package/package.json
CHANGED