inscope 0.2.0 ā 0.3.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 +5 -2
- package/dist/bin/index.mjs +50 -37
- package/dist/index.mjs +9 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,8 +120,9 @@ Launch `claude` from inside a mapped directory (or relaunch) to pick up the iden
|
|
|
120
120
|
|
|
121
121
|
```
|
|
122
122
|
inscope init Create the config, generate the hook, source it from ~/.zshrc
|
|
123
|
-
inscope add
|
|
124
|
-
inscope
|
|
123
|
+
inscope add [path] Map a directory to a GitHub account, git email, and MCP servers
|
|
124
|
+
inscope edit [path] Edit a workspace interactively, then re-apply
|
|
125
|
+
inscope rm [path] Remove a workspace mapping (alias: remove)
|
|
125
126
|
inscope list List configured workspaces (alias: ls)
|
|
126
127
|
inscope apply Regenerate the hook, git includes, and .mcp.json (alias: sync)
|
|
127
128
|
inscope doctor Verify tokens, identities, and the hook resolve correctly
|
|
@@ -184,6 +185,8 @@ inscope add ~/acme --gh acme --servers github,linear,notion,slack --seed-slack
|
|
|
184
185
|
|
|
185
186
|
`--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.
|
|
186
187
|
|
|
188
|
+
You need a Slack app with a user OAuth (`xoxp`) token first. If you don't have one, follow the [slack-mcp-server authentication guide](https://github.com/korotovsky/slack-mcp-server/blob/HEAD/docs/01-authentication-setup.md#option-2-using-slack_mcp_xoxp_token-user-oauth). inscope points you there during `add` when Slack is enabled.
|
|
189
|
+
|
|
187
190
|
---
|
|
188
191
|
|
|
189
192
|
## š Config File
|
package/dist/bin/index.mjs
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
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(),`
|
|
3
|
-
|
|
4
|
-
`)
|
|
5
|
-
`),
|
|
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}},T=(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??``}},te=()=>process.platform===`darwin`,E=()=>process.env.USER||``,ne=(e,t=T)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},re=(e=T)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},ie=(e=T)=>{let t=[];for(let n of re(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},D=(e,t=T)=>{let n=t(`git`,[`config`,`--global`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},O=(e,t=T)=>{let n=t(`security`,[`find-generic-password`,`-a`,E(),`-s`,e,`-w`]);return n.status===0&&n.stdout.trim().length>0},ae=(e,t,n=T)=>{let r=n(`security`,[`add-generic-password`,`-U`,`-a`,E(),`-s`,e,`-w`,t]);if(r.status!==0)throw Error(`security add-generic-password failed: ${r.stderr.trim()||`unknown error`}`)},oe=e=>`security add-generic-password -U -a "${E()||`$USER`}" -s ${e} -w 'xoxp-...'`,se=(e,t=T)=>{let n=t(`git`,[`config`,`--file`,e,`user.email`]);return n.status===0?n.stdout.trim():null},k=()=>!!(process.stdin.isTTY&&process.stdout.isTTY),A=e=>{let t=process.stdin;t.isTTY&&typeof t.setRawMode==`function`&&t.setRawMode(e)};let j=``;const M=e=>new Promise(t=>{process.stdout.write(e);let n=()=>{let e=j.indexOf(`
|
|
4
|
+
`);if(e<0)return!1;let n=j.slice(0,e).replace(/\r$/,``);return j=j.slice(e+1),t(n),!0};if(n())return;let r=e=>{j+=e.toString(`utf8`),j.includes(`
|
|
5
|
+
`)&&(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=j.replace(/\r$/,``);j=``,t(e)};process.stdin.on(`data`,r),process.stdin.on(`end`,i),process.stdin.resume()}),N=async(e,t=``)=>(await M(`${e}${t?` [${t}]`:``}: `)).trim()||t,P=async(e,t=!1)=>{let n=(await M(`${e} [${t?`Y/n`:`y/N`}]: `)).trim().toLowerCase();return n?n===`y`||n===`yes`:t},ce=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(`
|
|
6
|
+
`),n.close(),t(e.trim())})}),le=`\x1B[36m`,ue=`\x1B[0m`,F=(e,t,n=0)=>new Promise(r=>{if(!k()||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+`
|
|
7
|
+
`);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?le+r+ue:r}\n`)}};s(!0),a.emitKeypressEvents(process.stdin),A(!0),process.stdin.resume();let c=()=>{process.stdin.off(`keypress`,l),A(!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(`
|
|
8
|
+
`),process.exit(130))};process.stdin.on(`keypress`,l)}),de=(e,t)=>new Promise(n=>{let r=t.map(e=>!!e.checked),i=()=>t.filter((e,t)=>r[t]).map(e=>e.value);if(!k()||t.length===0){n(i());return}let o=0,s=process.stdout;s.write(e+`
|
|
9
|
+
`);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?le+i+ue:i}\n`)}};c(!0),a.emitKeypressEvents(process.stdin),A(!0),process.stdin.resume();let l=()=>{process.stdin.off(`keypress`,u),A(!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(`
|
|
10
|
+
`),process.exit(130))};process.stdin.on(`keypress`,u)}),I=e=>`# >>> inscope:${e} >>>`,L=e=>`# <<< inscope:${e} <<<`,R=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),fe=e=>RegExp(`${R(I(e))}\\n[\\s\\S]*?\\n${R(L(e))}\\n?`),pe=(e,t)=>{let n=t.replace(/\n+$/,``);return`${I(e)}\n${n}\n${L(e)}\n`},z=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},me=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=z(e),o=pe(r,i),s=fe(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)},he=(e,n)=>{let r=z(e);if(!r)return;let i=r.replace(fe(n),``).replace(/\n{3,}/g,`
|
|
11
|
+
|
|
12
|
+
`).replace(/^\n+/,``);t.writeFileSync(e,i)},ge=(e,t)=>{let n=z(e).match(RegExp(`${R(I(t))}\\n([\\s\\S]*?)\\n${R(L(t))}`));return n?n[1]:null},B=`gitconfig`,V=e=>!!(e.git&&(e.git.email||e.git.name)),H=e=>n.join(m(),`${e}.gitconfig`),_e=e=>l(e).replace(/\/+$/,``)+`/`,ve=e=>e.workspaces.filter(V).map(e=>`[includeIf "gitdir:${_e(e.path)}"]\n\tpath = ${l(H(e.name))}`).join(`
|
|
13
|
+
`),ye=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
14
|
`)+`
|
|
7
|
-
`},
|
|
15
|
+
`},be=e=>{t.mkdirSync(m(),{recursive:!0});for(let n of e.workspaces)V(n)&&t.writeFileSync(H(n.name),ye(n));let n=ve(e);n?me(h(),B,n):he(h(),B)},xe=e=>{let n=H(e);t.existsSync(n)&&t.rmSync(n)},Se=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},Ce=e=>e.servers.slack?e.servers.slack.keychain:``,U=e=>{let t=[...e.workspaces].sort((e,t)=>e.name.localeCompare(t.name));return`# Managed by inscope. Do not edit by hand.
|
|
8
16
|
# Source of truth: ~/.config/inscope/inscope.json
|
|
9
17
|
# Edit there, then run \`inscope apply\` to regenerate this file.
|
|
10
18
|
#
|
|
@@ -15,7 +23,7 @@ import{parseArgs as e}from"node:util";import t from"node:fs";import n from"node:
|
|
|
15
23
|
__inscope_resolve_identity() {
|
|
16
24
|
local ws
|
|
17
25
|
case "\${PWD}/" in
|
|
18
|
-
${t.map(e=>` ${
|
|
26
|
+
${t.map(e=>` ${Se(e.path)}) ws=${e.name} ;;`).join(`
|
|
19
27
|
`)||` # no workspaces configured`}
|
|
20
28
|
*) ws="" ;;
|
|
21
29
|
esac
|
|
@@ -25,7 +33,7 @@ ${t.map(e=>` ${ce(e.path)}) ws=${e.name} ;;`).join(`
|
|
|
25
33
|
|
|
26
34
|
local gh_user="" slack_svc=""
|
|
27
35
|
case "$ws" in
|
|
28
|
-
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=
|
|
36
|
+
${t.map(e=>{let t=[];e.gh&&t.push(`gh_user=${e.gh}`);let n=Ce(e);return n&&t.push(`slack_svc=${n}`),` ${e.name}) ${t.length?t.join(`; `):`:`} ;;`}).join(`
|
|
29
37
|
`)||` # no workspaces configured`}
|
|
30
38
|
*) return ;; # outside a mapped workspace: nothing set
|
|
31
39
|
esac
|
|
@@ -51,20 +59,14 @@ autoload -Uz add-zsh-hook
|
|
|
51
59
|
add-zsh-hook chpwd __inscope_resolve_identity
|
|
52
60
|
__inscope_ws="__init__" # force the first resolve, clearing any inherited token
|
|
53
61
|
__inscope_resolve_identity
|
|
54
|
-
`},
|
|
55
|
-
`)},
|
|
56
|
-
`)},
|
|
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.
|
|
62
|
+
`},we=[`github`,`linear`,`notion`,`slack`],W=e=>we.map(t=>`${t}-${e}`),G=e=>n.join(u(e.path),`.mcp.json`),K=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,Te=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:K(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:K(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},Ee=e=>{if(!t.existsSync(e))return{};try{return JSON.parse(t.readFileSync(e,`utf8`))}catch{return{}}},q=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)`)}},De=e=>{let n=G(e);return t.existsSync(n)?Ee(n):null},Oe=e=>{let r=G(e);t.mkdirSync(n.dirname(r),{recursive:!0});let i=q(r),a=i.mcpServers&&typeof i.mcpServers==`object`?{...i.mcpServers}:{};for(let t of W(e.name))delete a[t];Object.assign(a,Te(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
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.3.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.
|
|
63
65
|
Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
|
|
64
66
|
with the same path or label updates that workspace.
|
|
65
67
|
|
|
66
68
|
Usage:
|
|
67
|
-
$ ${
|
|
69
|
+
$ ${Z} add [path] [options]
|
|
68
70
|
|
|
69
71
|
Options:
|
|
70
72
|
--gh <account> gh account whose token this workspace uses
|
|
@@ -78,61 +80,72 @@ Options:
|
|
|
78
80
|
--slack-message allow the Slack MCP server to post messages
|
|
79
81
|
--seed-slack prompt for the Slack token and store it in the keychain
|
|
80
82
|
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
81
|
-
-h, --help Display help message`,
|
|
83
|
+
-h, --help Display help message`,Ve=[{label:`github`,value:`github`,checked:!0},{label:`linear`,value:`linear`,checked:!1},{label:`notion`,value:`notion`,checked:!1},{label:`slack`,value:`slack`,checked:!1}],He=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($),process.exit(0));let i=k()&&!r.yes,a=n[0];if(!a)if(i)a=await N(`Workspace directory`,process.cwd());else throw Error($);let o=r.label||S(a);i&&!r.label&&(o=await N(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await F(`GitHub account for this workspace`,[...ie().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=D(`user.email`);c=await N(`Git email${e?` [${e} Ā· global]`:``} (enter to inherit global)`)||void 0}if(u===void 0){let e=D(`user.name`);u=await N(`Git name${e?` [${e} Ā· global]`:``} (enter to inherit global)`)||void 0}}let d;d=r.servers===void 0?i?await de(`MCP servers (space toggles, enter confirms)`,Ve):[`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 ${X}`),r[`slack-keychain`]||(p=await N(`Slack keychain service`,p)),r[`slack-message`]||(m=await P(`Allow Slack to post messages?`,!0)),r[`seed-slack`]||(h=await P(`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)},Ue=`Regenerate the chpwd hook, git includes, and every .mcp.json
|
|
82
84
|
from your config. Idempotent: run it any time the config changes.
|
|
83
85
|
|
|
84
86
|
Usage:
|
|
85
|
-
$ ${
|
|
87
|
+
$ ${Z} apply
|
|
86
88
|
|
|
87
89
|
Options:
|
|
88
|
-
-h, --help Display help message`,
|
|
90
|
+
-h, --help Display help message`,We=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ue),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let r=y(),i=Y(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)},Ge=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},Ke=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},qe=(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)})},Je=(e=T)=>{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}},Ye=(e,n=T)=>{let r=[];te()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=p(),a=Ge(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===U(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(V)&&r.push(ge(h(),B)===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(ne(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(O(t,n)?{status:`ok`,label:`${e} slack`,detail:t}:{status:`fail`,label:`${e} slack`,detail:`${t} not in keychain; run \`${oe(t)}\``})}if(V(i)){let a=H(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=se(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=De(i);if(a===null)r.push({status:`warn`,label:`${e} mcp`,detail:"no .mcp.json; run `inscope apply`"});else{let t=W(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${e} mcp`,detail:`${t.length} server(s)`});let n=Ke(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},Xe=`Verify the setup: gh tokens resolve, keychain entries exist,
|
|
89
91
|
git emails match per path, the hook is current, and no MCP server is unpinned.
|
|
90
92
|
Exits non-zero if any check fails.
|
|
91
93
|
|
|
92
94
|
Usage:
|
|
93
|
-
$ ${
|
|
95
|
+
$ ${Z} doctor
|
|
96
|
+
|
|
97
|
+
Options:
|
|
98
|
+
-h, --help Display help message`,Ze={ok:`ā`,warn:`!`,fail:`ā`},Qe=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Xe),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let r=y(),i=Ye(r);for(let e of i)console.log(`${Ze[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=qe(r);if(a){let e=Je();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(`
|
|
99
|
+
All checks passed.`),process.exit(0)},$e=`Edit a configured workspace interactively, then re-apply.
|
|
100
|
+
Pick a workspace (or pass its path/label), step through the prompts pre-filled
|
|
101
|
+
with its current values, and inscope regenerates everything on save.
|
|
102
|
+
|
|
103
|
+
Usage:
|
|
104
|
+
$ ${Z} edit [path|label]
|
|
94
105
|
|
|
95
106
|
Options:
|
|
96
|
-
-h, --help Display help message`,
|
|
97
|
-
All checks passed.`),process.exit(0)},We=`Set up inscope: create the config, generate the chpwd hook, and
|
|
107
|
+
-h, --help Display help message`,et=async t=>{let{positionals:n,values:r}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});r.help&&(console.log($e),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let i=y();i.workspaces.length||(console.error(`No workspaces yet. Add one with \`${Z} 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(k())return F(`Edit which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e})));console.error(`Specify a workspace, e.g. \`${Z} edit <label>\`.`),process.exit(1)})();console.log(`\nEditing "${o.name}" (${o.path})\n`);let s=[...ie().map(e=>({label:e,value:e})),{label:`(none)`,value:``}],c=await F(`GitHub account`,s,Math.max(0,s.findIndex(e=>e.value===(o.gh??``))))||void 0,l=o.git?.email,u=await N(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 N(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 de(`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 ${X}`),b=await N(`Slack keychain service`,b),x=await P(`Allow Slack to post messages?`,x),O(b)||(S=await P(`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)},tt=`Set up inscope: create the config, generate the chpwd hook, and
|
|
98
108
|
source it from ~/.zshrc. Safe to run again; it never overwrites your config.
|
|
99
109
|
|
|
100
110
|
Usage:
|
|
101
|
-
$ ${
|
|
111
|
+
$ ${Z} init
|
|
102
112
|
|
|
103
113
|
Options:
|
|
104
|
-
-h, --help Display help message`,
|
|
114
|
+
-h, --help Display help message`,nt=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(tt),process.exit(0));let r;v()?(r=y(),console.log(`Using existing config at ${f()}`)):(r=_(),b(r),console.log(`Created ${f()}`)),Y(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
|
|
105
115
|
Next steps:
|
|
106
116
|
1. Reload your shell: source ~/.zshrc (or open a new terminal)
|
|
107
117
|
2. Sign each GitHub account in: gh auth login
|
|
108
|
-
3. Map a workspace: ${
|
|
109
|
-
`),process.exit(0)},
|
|
118
|
+
3. Map a workspace: ${Z} add ~/acme --gh acme --email you@acme.com
|
|
119
|
+
`),process.exit(0)},rt=`List the configured workspaces. Run \`${Z} doctor\` to verify
|
|
110
120
|
that their tokens and identities actually resolve.
|
|
111
121
|
|
|
112
122
|
Usage:
|
|
113
|
-
$ ${
|
|
123
|
+
$ ${Z} list
|
|
114
124
|
|
|
115
125
|
Options:
|
|
116
|
-
-h, --help Display help message`,
|
|
117
|
-
servers inscope manages; leaves your keychain and gh accounts untouched.
|
|
126
|
+
-h, --help Display help message`,it=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean).join(`, `)||`none`,at=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(rt),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} init\` first.`),process.exit(1));let r=y();r.workspaces.length||(console.log(`No workspaces yet. Add one with \`${Z} 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 ${it(e.servers)}`),e.servers.slack&&console.log(` slack keychain: ${e.servers.slack.keychain}`);process.exit(0)},ot=`Remove a workspace mapping. Drops its git include and the MCP
|
|
127
|
+
servers inscope manages; leaves your keychain and gh accounts untouched. Pick a
|
|
128
|
+
workspace, or pass its path/label.
|
|
118
129
|
|
|
119
130
|
Usage:
|
|
120
|
-
$ ${
|
|
131
|
+
$ ${Z} rm [path|label]
|
|
121
132
|
|
|
122
133
|
Options:
|
|
123
|
-
-
|
|
124
|
-
${
|
|
134
|
+
-y, --yes Skip the type-the-label confirmation
|
|
135
|
+
-h, --help Display help message`,st=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(ot),process.exit(0)),v()||(console.error(`No config found. Run \`${Z} 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 k()?o=await F(`Remove which workspace?`,i.workspaces.map(e=>({label:`${e.name} (${e.path})`,value:e}))):(console.error(`Specify a workspace, e.g. \`${Z} rm <label>\`.`),process.exit(1));if(!r.yes){console.log(`\nā Removing "${o.name}" (${o.path}) unmaps it from inscope.`);let e=await N(`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);ke(o),xe(o.name),b(s),Y(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)},ct=`Version:
|
|
136
|
+
${Z}@${Be}
|
|
125
137
|
|
|
126
138
|
Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
|
|
127
139
|
commit identity to the directory you are in, so concurrent sessions never clash.
|
|
128
140
|
|
|
129
141
|
Usage:
|
|
130
|
-
$ ${
|
|
142
|
+
$ ${Z} <command> [options]
|
|
131
143
|
|
|
132
144
|
Commands:
|
|
133
145
|
init Create the config, generate the hook, source it from ~/.zshrc
|
|
134
|
-
add
|
|
135
|
-
|
|
146
|
+
add [path] Map a directory to a GitHub account, git email, and MCP servers
|
|
147
|
+
edit [path] Edit a workspace interactively, then re-apply
|
|
148
|
+
rm [path] Remove a workspace mapping (alias: remove)
|
|
136
149
|
list List configured workspaces (alias: ls)
|
|
137
150
|
apply Regenerate the hook, git includes, and .mcp.json (alias: sync)
|
|
138
151
|
doctor Verify tokens, identities, and the hook resolve correctly
|
|
@@ -142,4 +155,4 @@ Options:
|
|
|
142
155
|
-h, --help Display help
|
|
143
156
|
|
|
144
157
|
Author:
|
|
145
|
-
${
|
|
158
|
+
${Q.name} <${Q.email}> (${Q.url})`;(async()=>{try{let e=process.argv.slice(2),t=e[0],n=e.slice(1);switch(t){case`init`:return nt(n);case`add`:return await He(n);case`edit`:return et(n);case`rm`:case`remove`:return await st(n);case`ls`:case`list`:return at(n);case`apply`:case`sync`:return We(n);case`doctor`:return Qe(n)}(t===`-v`||t===`--version`)&&(console.log(`${Z}@${Be}`),process.exit(0)),(!t||t===`-h`||t===`--help`)&&(console.log(ct),process.exit(0)),console.error(`unknown command: ${e.join(` `)}\n`),console.error(ct),process.exit(1)}catch(e){console.error(e.message),process.exit(1)}})();export{};
|
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(),`
|
|
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,te=()=>({version:1,workspaces:[]}),ne=()=>e.existsSync(u()),re=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n);return g(r),r},h=n=>{let r=u();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,JSON.stringify(n,null,2)+`
|
|
2
|
+
`)},g=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)),_=(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}},oe=(e,t)=>{let n=_(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?`),se=(e,t)=>{let n=t.replace(/\n+$/,``);return`${v(e)}\n${n}\n${y(e)}\n`},S=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},C=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=S(n),o=se(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)},ce=(t,n)=>{let r=S(t);if(!r)return;let i=r.replace(x(n),``).replace(/\n{3,}/g,`
|
|
3
3
|
|
|
4
|
-
`).replace(/^\n+/,``);e.writeFileSync(t,i)},
|
|
4
|
+
`).replace(/^\n+/,``);e.writeFileSync(t,i)},le=(e,t)=>{let n=S(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(f(),`${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
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
|
-
`},A=t=>{e.mkdirSync(
|
|
7
|
+
`},A=t=>{e.mkdirSync(f(),{recursive:!0});for(let n of t.workspaces)T(n)&&e.writeFileSync(E(n.name),k(n));let n=O(t);n?C(p(),w,n):ce(p(),w)},ue=e=>{let t=s(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},de=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
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=>` ${ue(e.path)}) ws=${e.name} ;;`).join(`
|
|
19
19
|
`)||` # no workspaces configured`}
|
|
20
20
|
*) ws="" ;;
|
|
21
21
|
esac
|
|
@@ -25,7 +25,7 @@ ${t.map(e=>` ${le(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=de(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
|
-
`},M=`1.3.0`,
|
|
55
|
-
`)},
|
|
56
|
-
`)},
|
|
54
|
+
`},M=`1.3.0`,fe=[`github`,`linear`,`notion`,`slack`],N=e=>fe.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=>{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)`)}},B=t=>{let n=P(t);return e.existsSync(n)?R(n):null},V=n=>{let r=P(n);e.mkdirSync(t.dirname(r),{recursive:!0});let i=z(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
|
+
`)},pe=t=>{let n=P(t);if(!e.existsSync(n))return;let r=z(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
|
+
`)},me=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},H=()=>{let e=me(d());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=m(),n=``;try{n=e.readFileSync(t,`utf8`)}catch{}let r=U(n);r!==n&&e.writeFileSync(t,r)},G=()=>{try{return e.readFileSync(m(),`utf8`).includes(H())}catch{return!1}},he=n=>{let r=d();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,j(n)),A(n),W();let i=[];for(let e of n.workspaces)V(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()},ge=(e=K)=>{let t=[];for(let n of X(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},_e=(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},ve=(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},ye=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},be=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},xe=(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)})},Se=(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}},Ce=(t,n=K)=>{let r=[];q()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=d(),a=ye(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(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(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=B(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=be(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,he as applyAll,A as applyGitconfig,V as applyMcp,ne as configExists,xe as currentWorkspace,te as defaultConfig,K as defaultRunner,W as ensureZshrcSource,_ as findWorkspace,ge as ghAccounts,X as ghStatus,Y as ghToken,$ as gitEmailForFile,_e as gitGlobal,q as isMacOS,Z as keychainHas,ve as keychainSet,Q as keychainSetCommand,ie as labelFromPath,Se as liveSnapshot,re as loadConfig,N as managedKeys,P as mcpFilePath,B as readMcp,pe as removeMcp,oe as removeWorkspace,O as renderGitInclude,j as renderHook,L as renderMcp,k as renderPerWorkspaceGitconfig,I as renderServers,U as renderZshrcSource,Ce as runDoctor,h as saveConfig,ae as upsertWorkspace,g as validateConfig,G as zshrcSourcesHook};
|
package/package.json
CHANGED