inscope 0.2.0-canary.0 → 0.2.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 +27 -18
- package/dist/bin/index.mjs +33 -27
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +11 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,10 @@ Nothing sensitive is written to disk. GitHub tokens come from the `gh` keyring a
|
|
|
41
41
|
# set up the config + hook, and source it from ~/.zshrc
|
|
42
42
|
inscope init
|
|
43
43
|
|
|
44
|
-
# map a
|
|
44
|
+
# map a workspace interactively: pick the gh account, git identity, and servers
|
|
45
|
+
inscope add ~/acme
|
|
46
|
+
|
|
47
|
+
# or pass flags to skip the prompts (work gh account, work email, + slack)
|
|
45
48
|
inscope add ~/acme --gh acme --email you@acme.com --servers github,linear,notion,slack
|
|
46
49
|
|
|
47
50
|
# map a personal directory: just your gh account and personal email
|
|
@@ -129,33 +132,36 @@ inscope doctor Verify tokens, identities, and the hook resolve correctly
|
|
|
129
132
|
|
|
130
133
|
Run any command with `-h` for its options.
|
|
131
134
|
|
|
132
|
-
### `inscope add`
|
|
135
|
+
### `inscope add`
|
|
136
|
+
|
|
137
|
+
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).
|
|
133
138
|
|
|
134
139
|
```
|
|
135
140
|
--gh <account> gh account whose token this workspace uses
|
|
136
|
-
--email <email> git commit email
|
|
137
|
-
--git-name <name> git commit author name (
|
|
141
|
+
--email <email> git commit email (omit to inherit your global identity)
|
|
142
|
+
--git-name <name> git commit author name (omit to inherit global)
|
|
138
143
|
--label <name> workspace name; defaults to the directory basename
|
|
139
144
|
--servers <list> comma-separated: github,linear,notion,slack
|
|
140
|
-
(default: github
|
|
145
|
+
(default: github)
|
|
141
146
|
--slack-keychain <s> keychain service for the Slack token
|
|
142
|
-
(default:
|
|
147
|
+
(default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
|
|
143
148
|
--slack-message allow the Slack MCP server to post messages
|
|
144
149
|
--seed-slack prompt for the Slack token and store it in the keychain
|
|
150
|
+
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
145
151
|
```
|
|
146
152
|
|
|
147
153
|
---
|
|
148
154
|
|
|
149
155
|
## 🧩 What It Manages
|
|
150
156
|
|
|
151
|
-
| Surface | Location
|
|
152
|
-
| ------------ |
|
|
153
|
-
| Config | `~/.config/
|
|
154
|
-
| chpwd hook | `~/.config/
|
|
155
|
-
| MCP servers | `<workspace>/.mcp.json`
|
|
156
|
-
| Git identity | `~/.gitconfig` includeIf + `~/.config/git/<name>.gitconfig` |
|
|
157
|
+
| Surface | Location |
|
|
158
|
+
| ------------ | ------------------------------------------------------------------- |
|
|
159
|
+
| Config | `~/.config/inscope/inscope.json` |
|
|
160
|
+
| chpwd hook | `~/.config/inscope/inscope.zsh` |
|
|
161
|
+
| MCP servers | `<workspace>/.mcp.json` |
|
|
162
|
+
| Git identity | `~/.gitconfig` includeIf + `~/.config/inscope/git/<name>.gitconfig` |
|
|
157
163
|
|
|
158
|
-
`inscope` only touches the blocks it owns; your other `.zshrc`, `.gitconfig` and `.mcp.json` content is left alone. Edit `
|
|
164
|
+
`inscope` only touches the blocks it owns; your other `.zshrc`, `.gitconfig` and `.mcp.json` content is left alone. Edit `inscope.json` by hand if you like, then run `inscope apply`.
|
|
159
165
|
|
|
160
166
|
---
|
|
161
167
|
|
|
@@ -182,7 +188,7 @@ inscope add ~/acme --gh acme --servers github,linear,notion,slack --seed-slack
|
|
|
182
188
|
|
|
183
189
|
## 📋 Config File
|
|
184
190
|
|
|
185
|
-
The source of truth is `~/.config/
|
|
191
|
+
The source of truth is `~/.config/inscope/inscope.json`:
|
|
186
192
|
|
|
187
193
|
```jsonc
|
|
188
194
|
{
|
|
@@ -197,10 +203,13 @@ The source of truth is `~/.config/claude/workspaces.json`:
|
|
|
197
203
|
"github": true,
|
|
198
204
|
"linear": true,
|
|
199
205
|
"notion": true,
|
|
200
|
-
"slack": {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
"slack": {
|
|
207
|
+
"keychain": "SLACK_MCP_XOXP_TOKEN_ACME",
|
|
208
|
+
"addMessageTool": false,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
204
213
|
}
|
|
205
214
|
```
|
|
206
215
|
|
package/dist/bin/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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(),`claude`,`workspaces.json`),f=()=>n.join(s(),`claude`,`mcp-tokens.zsh`),p=()=>n.join(s(),`git`),m=()=>n.join(o(),`.gitconfig`),h=()=>n.join(o(),`.zshrc`),g=e=>`# >>> inscope:${e} >>>`,_=e=>`# <<< inscope:${e} <<<`,v=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),y=e=>RegExp(`${v(g(e))}\\n[\\s\\S]*?\\n${v(_(e))}\\n?`),b=(e,t)=>{let n=t.replace(/\n+$/,``);return`${g(e)}\n${n}\n${_(e)}\n`},x=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},
|
|
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(),`claude`,`workspaces.json`),f=()=>n.join(s(),`claude`,`mcp-tokens.zsh`),p=()=>n.join(s(),`git`),m=()=>n.join(o(),`.gitconfig`),h=()=>n.join(o(),`.zshrc`),g=e=>`# >>> inscope:${e} >>>`,_=e=>`# <<< inscope:${e} <<<`,v=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),y=e=>RegExp(`${v(g(e))}\\n[\\s\\S]*?\\n${v(_(e))}\\n?`),b=(e,t)=>{let n=t.replace(/\n+$/,``);return`${g(e)}\n${n}\n${_(e)}\n`},x=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},ee=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=x(e),o=b(r,i),s=y(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)},te=(e,n)=>{let r=x(e);if(!r)return;let i=r.replace(y(n),``).replace(/\n{3,}/g,`
|
|
3
3
|
|
|
4
|
-
`).replace(/^\n+/,``);t.writeFileSync(e,i)},
|
|
5
|
-
`),
|
|
4
|
+
`).replace(/^\n+/,``);t.writeFileSync(e,i)},ne=(e,t)=>{let n=x(e).match(RegExp(`${v(g(t))}\\n([\\s\\S]*?)\\n${v(_(t))}`));return n?n[1]:null},S=`gitconfig`,C=e=>!!(e.git&&(e.git.email||e.git.name)),w=e=>n.join(p(),`${e}.gitconfig`),re=e=>l(e).replace(/\/+$/,``)+`/`,ie=e=>e.workspaces.filter(C).map(e=>`[includeIf "gitdir:${re(e.path)}"]\n\tpath = ${l(w(e.name))}`).join(`
|
|
5
|
+
`),ae=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
|
-
`},
|
|
8
|
-
# Source of truth: ~/.config/
|
|
7
|
+
`},oe=e=>{t.mkdirSync(p(),{recursive:!0});for(let n of e.workspaces)C(n)&&t.writeFileSync(w(n.name),ae(n));let n=ie(e);n?ee(m(),S,n):te(m(),S)},se=e=>{let n=w(e);t.existsSync(n)&&t.rmSync(n)},ce=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},le=e=>e.servers.slack?e.servers.slack.keychain:``,T=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
|
|
8
|
+
# Source of truth: ~/.config/inscope/inscope.json
|
|
9
9
|
# Edit there, then run \`inscope apply\` to regenerate this file.
|
|
10
10
|
#
|
|
11
11
|
# One chpwd hook resolves per-workspace secrets from \$PWD on every cd: it maps
|
|
@@ -15,7 +15,7 @@ import{parseArgs as e}from"node:util";import t from"node:fs";import n from"node:
|
|
|
15
15
|
__inscope_resolve_identity() {
|
|
16
16
|
local ws
|
|
17
17
|
case "\${PWD}/" in
|
|
18
|
-
${t.map(e=>` ${
|
|
18
|
+
${t.map(e=>` ${ce(e.path)}) ws=${e.name} ;;`).join(`
|
|
19
19
|
`)||` # no workspaces configured`}
|
|
20
20
|
*) ws="" ;;
|
|
21
21
|
esac
|
|
@@ -25,7 +25,7 @@ ${t.map(e=>` ${D(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=le(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,35 +51,41 @@ 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
|
-
`)},
|
|
57
|
-
`)},
|
|
58
|
-
`),n.close(),t(e.trim())})})
|
|
59
|
-
|
|
54
|
+
`},ue=[`github`,`linear`,`notion`,`slack`],E=e=>ue.map(t=>`${t}-${e}`),D=e=>n.join(u(e.path),`.mcp.json`),O=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,de=e=>{let t=e.servers,n={};if(t.github&&(n[`github-${e.name}`]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}}),t.linear&&(n[`linear-${e.name}`]={type:`http`,url:O(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:O(t.notion,`https://mcp.notion.com/mcp`)}),t.slack){let r={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};t.slack.addMessageTool&&(r.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[`slack-${e.name}`]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@1.3.0`,`--transport`,`stdio`],env:r}}return n},k=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{return{}}},fe=e=>{let n=D(e);return t.existsSync(n)?k(n):null},pe=e=>{let r=D(e);t.mkdirSync(n.dirname(r),{recursive:!0});let i=k(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let t of E(e.name))delete a[t];Object.assign(a,de(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
55
|
+
`)},me=e=>{let n=D(e);if(!t.existsSync(n))return;let r=k(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)+`
|
|
56
|
+
`)},he=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},A=()=>{let e=he(f());return`[ -r "${e}" ] && source "${e}"`},ge=e=>{let t=A();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`},_e=()=>{let e=h(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=ge(n);r!==n&&t.writeFileSync(e,r)},ve=()=>{try{return t.readFileSync(h(),`utf8`).includes(A())}catch{return!1}},j=e=>{let r=f();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,T(e)),oe(e),_e();let i=[];for(let t of e.workspaces)pe(t),i.push(D(t));return{hook:r,gitconfig:e.workspaces.some(e=>e.git?.email||e.git?.name),mcp:i}},M=()=>({version:1,workspaces:[]}),N=()=>t.existsSync(d()),P=()=>{let e=d(),n=t.readFileSync(e,`utf8`),r=JSON.parse(n);return ye(r),r},F=e=>{let r=d();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,JSON.stringify(e,null,2)+`
|
|
57
|
+
`)},ye=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)}},be=e=>n.basename(u(e)),xe=(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)},Se=(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}},Ce=(e,t)=>{let n=xe(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},I=(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??``}},we=()=>process.platform===`darwin`,L=()=>process.env.USER||``,Te=(e,t=I)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Ee=(e=I)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},De=(e=I)=>{let t=[];for(let n of Ee(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},R=(e,t=I)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},z=(e,t=I)=>{let n=t(`security`,[`find-generic-password`,`-a`,L(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},Oe=(e,t,n=I)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,L(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},B=e=>`security add-generic-password -U -a "${L()||`$USER`}" -s ${e} -w 'xoxp-...'`,ke=(e,t=I)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},V=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),H=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)},U=(e,t=``)=>new Promise(n=>{let r=a.createInterface({input:process.stdin,output:process.stdout}),i=t?` [${t}]`:``;r.question(`${e}${i}: `,e=>{r.close(),n(e.trim()||t)})}),W=(e,t=!1)=>new Promise(n=>{let r=a.createInterface({input:process.stdin,output:process.stdout});r.question(`${e} [${t?`Y/n`:`y/N`}]: `,e=>{r.close();let i=e.trim().toLowerCase();n(i?i===`y`||i===`yes`:t)})}),Ae=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(`
|
|
58
|
+
`),n.close(),t(e.trim())})}),G=`\x1B[36m`,K=`\x1B[0m`,je=(e,t,n=0)=>new Promise(r=>{if(!V()||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+`
|
|
59
|
+
`);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?G+r+K:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),H(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),H(!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(`
|
|
60
|
+
`),process.exit(130))};process.stdin.on(`keypress`,l)}),Me=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!V()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
|
|
61
|
+
`);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?G+i+K:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),H(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),H(!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(`
|
|
62
|
+
`),process.exit(130))};process.stdin.on(`keypress`,u)});var q=`inscope`,J=`0.2.0`,Y={name:`Neeraj Dalal`,email:`admin@nrjdalal.com`,url:`https://nrjdalal.com`};const X=`Map a directory to a GitHub account, git email, and MCP servers.
|
|
63
|
+
Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
|
|
64
|
+
with the same path or label updates that workspace.
|
|
60
65
|
|
|
61
66
|
Usage:
|
|
62
|
-
$ ${q} add
|
|
67
|
+
$ ${q} add [path] [options]
|
|
63
68
|
|
|
64
69
|
Options:
|
|
65
70
|
--gh <account> gh account whose token this workspace uses
|
|
66
|
-
--email <email> git commit email
|
|
67
|
-
--git-name <name> git commit author name (
|
|
71
|
+
--email <email> git commit email (omit to inherit your global identity)
|
|
72
|
+
--git-name <name> git commit author name (omit to inherit global)
|
|
68
73
|
--label <name> workspace name; defaults to the directory basename
|
|
69
74
|
--servers <list> comma-separated: github,linear,notion,slack
|
|
70
|
-
(default: github
|
|
75
|
+
(default: github)
|
|
71
76
|
--slack-keychain <s> keychain service for the Slack token
|
|
72
|
-
(default:
|
|
77
|
+
(default: SLACK_MCP_XOXP_TOKEN_<LABEL> when slack is on)
|
|
73
78
|
--slack-message allow the Slack MCP server to post messages
|
|
74
79
|
--seed-slack prompt for the Slack token and store it in the keychain
|
|
75
|
-
-
|
|
80
|
+
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
81
|
+
-h, --help Display help message`,Ne=[{label:`github`,value:`github`,checked:!0},{label:`linear`,value:`linear`,checked:!1},{label:`notion`,value:`notion`,checked:!1},{label:`slack`,value:`slack`,checked:!1}],Pe=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(X),process.exit(0));let i=V()&&!r.yes,a=n[0];if(!a)if(i)a=await U(`Workspace directory`,process.cwd());else throw Error(X);let o=r.label||be(a);i&&!r.label&&(o=await U(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await je(`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=R(`user.email`);c=await U(`Git email${e?` [${e} · global]`:``} (enter to inherit global)`)||void 0}if(u===void 0){let e=R(`user.name`);u=await U(`Git name${e?` [${e} · global]`:``} (enter to inherit global)`)||void 0}}let d;d=r.servers===void 0?i?await Me(`MCP servers (space toggles, enter confirms)`,Ne):[`github`]:r.servers.split(`,`).map(e=>e.trim()).filter(Boolean);let f=d.includes(`slack`)||!!r[`slack-keychain`]||!!r[`seed-slack`],p=o.toUpperCase().replace(/[^A-Z0-9]+/g,`_`),m=r[`slack-keychain`]||`SLACK_MCP_XOXP_TOKEN_${p}`,h=!!r[`slack-message`],g=!!r[`seed-slack`];f&&i&&(r[`slack-keychain`]||(m=await U(`Slack keychain service`,m)),r[`slack-message`]||(h=await W(`Allow Slack to post messages?`,!1)),r[`seed-slack`]||(g=await W(`Store the Slack token now?`,!1)));let _={github:d.includes(`github`),linear:d.includes(`linear`),notion:d.includes(`notion`),slack:f?{keychain:m,addMessageTool:h}:!1},v=c||u?{email:c,name:u}:void 0,y={name:o,path:l(a),gh:s,git:v,servers:_},b=Se(N()?P():M(),y);if(F(b),j(b),console.log(`\n✓ workspace "${o}" -> ${y.path}`),console.log(`✓ regenerated the hook, git includes, and ${y.path}/.mcp.json`),_.slack)if(g){let e=await Ae(`Paste the Slack xoxp token for ${m}: `);e?(Oe(m,e),console.log(`✓ stored ${m} in the macOS keychain`)):console.error(`No token entered; skipped keychain write.`)}else z(m)||console.log(`\nSlack token not in the keychain yet. Store it once with:\n ${B(m)}`);console.log(`\nLaunch \`claude\` from ${y.path} (or relaunch) to pick up the new identity.`),process.exit(0)},Fe=`Regenerate the chpwd hook, git includes, and every .mcp.json
|
|
76
82
|
from your config. Idempotent: run it any time the config changes.
|
|
77
83
|
|
|
78
84
|
Usage:
|
|
79
85
|
$ ${q} apply
|
|
80
86
|
|
|
81
87
|
Options:
|
|
82
|
-
-h, --help Display help message`,
|
|
88
|
+
-h, --help Display help message`,Ie=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Fe),process.exit(0)),N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=P(),i=j(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)},Le=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},Re=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},ze=(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)})},Be=(e=I)=>{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}},Ve=(e,n=I)=>{let r=[];we()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=f(),a=Le(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===T(e)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(ve()?{status:`ok`,label:`zshrc`,detail:`sources the hook`}:{status:`warn`,label:`zshrc`,detail:"does not source the hook; run `inscope init`"}),e.workspaces.some(C)&&r.push(ne(m(),S)===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(Te(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(z(t,n)?{status:`ok`,label:`${e} slack`,detail:t}:{status:`fail`,label:`${e} slack`,detail:`${t} not in keychain; run \`${B(t)}\``})}if(C(i)){let a=w(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=ke(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=fe(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=Re(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},He=`Verify the setup: gh tokens resolve, keychain entries exist,
|
|
83
89
|
git emails match per path, the hook is current, and no MCP server is unpinned.
|
|
84
90
|
Exits non-zero if any check fails.
|
|
85
91
|
|
|
@@ -87,34 +93,34 @@ Usage:
|
|
|
87
93
|
$ ${q} doctor
|
|
88
94
|
|
|
89
95
|
Options:
|
|
90
|
-
-h, --help Display help message`,
|
|
91
|
-
All checks passed.`),process.exit(0)},
|
|
96
|
+
-h, --help Display help message`,Ue={ok:`✓`,warn:`!`,fail:`✗`},Z=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(He),process.exit(0)),N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=P(),i=Ve(r);for(let e of i)console.log(`${Ue[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=ze(r);if(a){let e=Be();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(`
|
|
97
|
+
All checks passed.`),process.exit(0)},We=`Set up inscope: create the config, generate the chpwd hook, and
|
|
92
98
|
source it from ~/.zshrc. Safe to run again; it never overwrites your config.
|
|
93
99
|
|
|
94
100
|
Usage:
|
|
95
101
|
$ ${q} init
|
|
96
102
|
|
|
97
103
|
Options:
|
|
98
|
-
-h, --help Display help message`,
|
|
104
|
+
-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));let r;N()?(r=P(),console.log(`Using existing config at ${d()}`)):(r=M(),F(r),console.log(`Created ${d()}`)),j(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
|
|
99
105
|
Next steps:
|
|
100
106
|
1. Reload your shell: source ~/.zshrc (or open a new terminal)
|
|
101
107
|
2. Sign each GitHub account in: gh auth login
|
|
102
108
|
3. Map a workspace: ${q} add ~/acme --gh acme --email you@acme.com
|
|
103
|
-
`),process.exit(0)},
|
|
109
|
+
`),process.exit(0)},Ke=`List the configured workspaces. Run \`${q} doctor\` to verify
|
|
104
110
|
that their tokens and identities actually resolve.
|
|
105
111
|
|
|
106
112
|
Usage:
|
|
107
113
|
$ ${q} list
|
|
108
114
|
|
|
109
115
|
Options:
|
|
110
|
-
-h, --help Display help message`,
|
|
116
|
+
-h, --help Display help message`,qe=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean).join(`, `)||`none`,Je=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ke),process.exit(0)),N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=P();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 ${qe(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},Q=`Remove a workspace mapping. Drops its git include and the MCP
|
|
111
117
|
servers inscope manages; leaves your keychain and gh accounts untouched.
|
|
112
118
|
|
|
113
119
|
Usage:
|
|
114
120
|
$ ${q} rm <path|label>
|
|
115
121
|
|
|
116
122
|
Options:
|
|
117
|
-
-h, --help Display help message`,
|
|
123
|
+
-h, --help Display help message`,Ye=t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});r.help&&(console.log(Q),process.exit(0));let i=n[0];if(!i)throw Error(Q);N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let{cfg:a,removed:o}=Ce(P(),i);o||(console.error(`No workspace matching "${i}".`),process.exit(1)),me(o),se(o.name),F(a),j(a),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)},$=`Version:
|
|
118
124
|
${q}@${J}
|
|
119
125
|
|
|
120
126
|
Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
|
|
@@ -136,4 +142,4 @@ Options:
|
|
|
136
142
|
-h, --help Display help
|
|
137
143
|
|
|
138
144
|
Author:
|
|
139
|
-
${Y.name} <${Y.email}> (${Y.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return
|
|
145
|
+
${Y.name} <${Y.email}> (${Y.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return Ge(n);case`add`:return await Pe(n);case`rm`:case`remove`:return Ye(n);case`ls`:case`list`:return Je(n);case`apply`:case`sync`:return Ie(n);case`doctor`:return Z(n)}(t===`-v`||t===`--version`)&&(console.log(`${q}@${J}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log($),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error($),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
|
package/dist/index.d.mts
CHANGED
|
@@ -41,8 +41,9 @@ declare const removeWorkspace: (cfg: Config, key: string) => {
|
|
|
41
41
|
};
|
|
42
42
|
//#endregion
|
|
43
43
|
//#region src/apply.d.ts
|
|
44
|
-
declare const
|
|
44
|
+
declare const renderZshrcSource: (current: string) => string;
|
|
45
45
|
declare const ensureZshrcSource: () => void;
|
|
46
|
+
declare const zshrcSourcesHook: () => boolean;
|
|
46
47
|
type ApplyResult = {
|
|
47
48
|
hook: string;
|
|
48
49
|
gitconfig: boolean;
|
|
@@ -63,6 +64,8 @@ declare const defaultRunner: Runner;
|
|
|
63
64
|
declare const isMacOS: () => boolean;
|
|
64
65
|
declare const ghToken: (account: string, run?: Runner) => string | null;
|
|
65
66
|
declare const ghStatus: (run?: Runner) => string;
|
|
67
|
+
declare const ghAccounts: (run?: Runner) => string[];
|
|
68
|
+
declare const gitGlobal: (key: string, run?: Runner) => string | null;
|
|
66
69
|
declare const keychainHas: (service: string, run?: Runner) => boolean;
|
|
67
70
|
declare const keychainSet: (service: string, token: string, run?: Runner) => void;
|
|
68
71
|
declare const keychainSetCommand: (service: string) => string;
|
|
@@ -104,4 +107,4 @@ declare const readMcp: (ws: Workspace) => Record<string, any> | null;
|
|
|
104
107
|
declare const applyMcp: (ws: Workspace) => void;
|
|
105
108
|
declare const removeMcp: (ws: Workspace) => void;
|
|
106
109
|
//#endregion
|
|
107
|
-
export { ApplyResult, CONFIG_VERSION, Check, CheckStatus, Config, HttpServer, RunResult, Runner, SLACK_MCP_VERSION, Servers, SlackServer, Workspace,
|
|
110
|
+
export { ApplyResult, CONFIG_VERSION, Check, CheckStatus, Config, HttpServer, RunResult, Runner, SLACK_MCP_VERSION, Servers, SlackServer, Workspace, applyAll, applyGitconfig, applyMcp, configExists, currentWorkspace, defaultConfig, defaultRunner, ensureZshrcSource, findWorkspace, ghAccounts, ghStatus, ghToken, gitEmailForFile, gitGlobal, isMacOS, keychainHas, keychainSet, keychainSetCommand, labelFromPath, liveSnapshot, loadConfig, managedKeys, mcpFilePath, readMcp, removeMcp, removeWorkspace, renderGitInclude, renderHook, renderMcp, renderPerWorkspaceGitconfig, renderServers, renderZshrcSource, runDoctor, saveConfig, upsertWorkspace, validateConfig, zshrcSourcesHook };
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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(),`claude`,`workspaces.json`),u=()=>t.join(a(),`claude`,`mcp-tokens.zsh`),d=()=>t.join(a(),`git`),f=()=>t.join(i(),`.gitconfig`),p=()=>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(),`claude`,`workspaces.json`),u=()=>t.join(a(),`claude`,`mcp-tokens.zsh`),d=()=>t.join(a(),`git`),f=()=>t.join(i(),`.gitconfig`),p=()=>t.join(i(),`.zshrc`),ee=1,m=()=>({version:1,workspaces:[]}),te=()=>e.existsSync(l()),ne=()=>{let t=l(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n);return h(r),r},re=n=>{let r=l();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(n,null,2)+`
|
|
2
|
+
`)},h=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)}},ie=e=>t.basename(c(e)),g=(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)},ae=(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}},_=(e,t)=>{let n=g(e,t);return n?{cfg:{...e,workspaces:e.workspaces.filter(e=>e.name!==n.name)},removed:n}:{cfg:e}},v=e=>`# >>> inscope:${e} >>>`,y=e=>`# <<< inscope:${e} <<<`,b=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),x=e=>RegExp(`${b(v(e))}\\n[\\s\\S]*?\\n${b(y(e))}\\n?`),S=(e,t)=>{let n=t.replace(/\n+$/,``);return`${v(e)}\n${n}\n${y(e)}\n`},C=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},oe=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=C(n),o=S(r,i),s=x(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)},se=(t,n)=>{let r=C(t);if(!r)return;let i=r.replace(x(n),``).replace(/\n{3,}/g,`
|
|
3
3
|
|
|
4
|
-
`).replace(/^\n+/,``);e.writeFileSync(t,i)},
|
|
5
|
-
`),
|
|
4
|
+
`).replace(/^\n+/,``);e.writeFileSync(t,i)},ce=(e,t)=>{let n=C(e).match(RegExp(`${b(v(t))}\\n([\\s\\S]*?)\\n${b(y(t))}`));return n?n[1]:null},w=`gitconfig`,T=e=>!!(e.git&&(e.git.email||e.git.name)),E=e=>t.join(d(),`${e}.gitconfig`),D=e=>s(e).replace(/\/+$/,``)+`/`,O=e=>e.workspaces.filter(T).map(e=>`[includeIf "gitdir:${D(e.path)}"]\n\tpath = ${s(E(e.name))}`).join(`
|
|
5
|
+
`),k=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
|
-
`},
|
|
8
|
-
# Source of truth: ~/.config/
|
|
7
|
+
`},A=t=>{e.mkdirSync(d(),{recursive:!0});for(let n of t.workspaces)T(n)&&e.writeFileSync(E(n.name),k(n));let n=O(t);n?oe(f(),w,n):se(f(),w)},le=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},ue=e=>e.servers.slack?e.servers.slack.keychain:``,j=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
|
|
8
|
+
# Source of truth: ~/.config/inscope/inscope.json
|
|
9
9
|
# Edit there, then run \`inscope apply\` to regenerate this file.
|
|
10
10
|
#
|
|
11
11
|
# One chpwd hook resolves per-workspace secrets from \$PWD on every cd: it maps
|
|
@@ -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=>` ${le(e.path)}) ws=${e.name} ;;`).join(`
|
|
19
19
|
`)||` # no workspaces configured`}
|
|
20
20
|
*) ws="" ;;
|
|
21
21
|
esac
|
|
@@ -25,7 +25,7 @@ ${t.map(e=>` ${se(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=ue(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
|
+
`},M=`1.3.0`,de=[`github`,`linear`,`notion`,`slack`],N=e=>de.map(t=>`${t}-${e}`),P=e=>t.join(c(e.path),`.mcp.json`),F=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,I=e=>{let t=e.servers,n={};if(t.github&&(n[`github-${e.name}`]={type:`http`,url:`https://api.githubcopilot.com/mcp/`,headers:{Authorization:"Bearer ${GITHUB_TOKEN}"}}),t.linear&&(n[`linear-${e.name}`]={type:`http`,url:F(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:F(t.notion,`https://mcp.notion.com/mcp`)}),t.slack){let r={SLACK_MCP_XOXP_TOKEN:"${SLACK_MCP_XOXP_TOKEN}"};t.slack.addMessageTool&&(r.SLACK_MCP_ADD_MESSAGE_TOOL=`true`),n[`slack-${e.name}`]={type:`stdio`,command:`npx`,args:[`-y`,`slack-mcp-server@${M}`,`--transport`,`stdio`],env:r}}return n},L=e=>({mcpServers:I(e)}),R=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},z=t=>{let n=P(t);return e.existsSync(n)?R(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,I(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
55
|
+
`)},V=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
|
+
`)},fe=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},H=()=>{let e=fe(u());return`[ -r "${e}" ] && source "${e}"`},U=e=>{let t=H();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`},W=()=>{let t=p(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=U(n);r!==n&&e.writeFileSync(t,r)},G=()=>{try{return e.readFileSync(p(),`utf8`).includes(H())}catch{return!1}},pe=n=>{let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,j(n)),A(n),W();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}},K=(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??``}},q=()=>process.platform===`darwin`,J=()=>process.env.USER||``,Y=(e,t=K)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},X=(e=K)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},me=(e=K)=>{let t=[];for(let n of X(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},he=(e,t=K)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},Z=(e,t=K)=>{let n=t(`security`,[`find-generic-password`,`-a`,J(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},ge=(e,t,n=K)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,J(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},Q=e=>`security add-generic-password -U -a "${J()||`$USER`}" -s ${e} -w 'xoxp-...'`,$=(e,t=K)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},_e=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},ve=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},ye=(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)})},be=(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=(t,n=K)=>{let r=[];q()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=u(),a=_e(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===j(t)?r.push({status:`ok`,label:`hook`,detail:i}):r.push({status:`warn`,label:`hook`,detail:"out of date; run `inscope apply`"}),r.push(G()?{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(ce(f(),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(Y(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(Z(e,n)?{status:`ok`,label:`${t} slack`,detail:e}:{status:`fail`,label:`${t} slack`,detail:`${e} not in keychain; run \`${Q(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=$(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=ve(a);n.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r};export{ee as CONFIG_VERSION,M as SLACK_MCP_VERSION,pe as applyAll,A as applyGitconfig,B as applyMcp,te as configExists,ye as currentWorkspace,m as defaultConfig,K as defaultRunner,W as ensureZshrcSource,g as findWorkspace,me as ghAccounts,X as ghStatus,Y as ghToken,$ as gitEmailForFile,he as gitGlobal,q as isMacOS,Z as keychainHas,ge as keychainSet,Q as keychainSetCommand,ie as labelFromPath,be as liveSnapshot,ne as loadConfig,N as managedKeys,P as mcpFilePath,z as readMcp,V as removeMcp,_ as removeWorkspace,O as renderGitInclude,j as renderHook,L as renderMcp,k as renderPerWorkspaceGitconfig,I as renderServers,U as renderZshrcSource,xe as runDoctor,re as saveConfig,ae as upsertWorkspace,h as validateConfig,G as zshrcSourcesHook};
|
package/package.json
CHANGED