inscope 0.2.0 → 0.2.1
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/dist/bin/index.mjs +22 -22
- package/dist/index.mjs +8 -8
- package/package.json +1 -1
package/dist/bin/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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(),`
|
|
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`),_=e=>`# >>> inscope:${e} >>>`,v=e=>`# <<< inscope:${e} <<<`,y=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`),b=e=>RegExp(`${y(_(e))}\\n[\\s\\S]*?\\n${y(v(e))}\\n?`),ee=(e,t)=>{let n=t.replace(/\n+$/,``);return`${_(e)}\n${n}\n${v(e)}\n`},x=e=>{try{return t.readFileSync(e,`utf8`)}catch{return``}},te=(e,r,i)=>{t.mkdirSync(n.dirname(e),{recursive:!0});let a=x(e),o=ee(r,i),s=b(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)},ne=(e,n)=>{let r=x(e);if(!r)return;let i=r.replace(b(n),``).replace(/\n{3,}/g,`
|
|
3
3
|
|
|
4
|
-
`).replace(/^\n+/,``);t.writeFileSync(e,i)},
|
|
5
|
-
`),
|
|
4
|
+
`).replace(/^\n+/,``);t.writeFileSync(e,i)},re=(e,t)=>{let n=x(e).match(RegExp(`${y(_(t))}\\n([\\s\\S]*?)\\n${y(v(t))}`));return n?n[1]:null},S=`gitconfig`,C=e=>!!(e.git&&(e.git.email||e.git.name)),w=e=>n.join(m(),`${e}.gitconfig`),ie=e=>l(e).replace(/\/+$/,``)+`/`,ae=e=>e.workspaces.filter(C).map(e=>`[includeIf "gitdir:${ie(e.path)}"]\n\tpath = ${l(w(e.name))}`).join(`
|
|
5
|
+
`),oe=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
|
+
`},se=e=>{t.mkdirSync(m(),{recursive:!0});for(let n of e.workspaces)C(n)&&t.writeFileSync(w(n.name),oe(n));let n=ae(e);n?te(h(),S,n):ne(h(),S)},ce=e=>{let n=w(e);t.existsSync(n)&&t.rmSync(n)},le=e=>{let t=l(e);return t===`~`?`"$HOME/"*`:t.startsWith(`~/`)?`"$HOME/${t.slice(2)}/"*`:`"${t}/"*`},ue=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
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{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=>` ${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=>` ${ce(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,15 +51,15 @@ 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())})}),G=`\x1B[36m`,K=`\x1B[0m`,
|
|
54
|
+
`},de=[`github`,`linear`,`notion`,`slack`],E=e=>de.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,fe=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{}}},pe=e=>{let n=D(e);return t.existsSync(n)?k(n):null},me=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,fe(e)),i.mcpServers=a,t.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
55
|
+
`)},he=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
|
+
`)},ge=e=>{let t=o();return e===t?`$HOME`:e.startsWith(t+n.sep)?`$HOME/${e.slice(t.length+1)}`:e},A=()=>{let e=ge(p());return`[ -r "${e}" ] && source "${e}"`},_e=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`},ve=()=>{let e=g(),n=``;try{n=t.readFileSync(e,`utf8`)}catch{}let r=_e(n);r!==n&&t.writeFileSync(e,r)},ye=()=>{try{return t.readFileSync(g(),`utf8`).includes(A())}catch{return!1}},j=e=>{let r=p();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,T(e)),se(e),ve();let i=[];for(let t of e.workspaces)me(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(f()),P=()=>{let e=f(),n=t.readFileSync(e,`utf8`),r=JSON.parse(n);return be(r),r},F=e=>{let r=f();t.mkdirSync(n.dirname(r),{recursive:!0}),t.writeFileSync(r,JSON.stringify(e,null,2)+`
|
|
57
|
+
`)},be=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)}},xe=e=>n.basename(u(e)),Se=(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)},Ce=(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}},we=(e,t)=>{let n=Se(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??``}},Te=()=>process.platform===`darwin`,L=()=>process.env.USER||``,Ee=(e,t=I)=>{let n=t(`gh`,[`auth`,`token`,`-u`,e]),r=n.stdout.trim();return n.status===0&&r?r:null},De=(e=I)=>{let t=e(`gh`,[`auth`,`status`]);return(t.stdout+t.stderr).trim()},Oe=(e=I)=>{let t=[];for(let n of De(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},ke=(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-...'`,Ae=(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)})}),je=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`,Me=(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
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)}),
|
|
60
|
+
`),process.exit(130))};process.stdin.on(`keypress`,l)}),Ne=(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
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.
|
|
62
|
+
`),process.exit(130))};process.stdin.on(`keypress`,u)});var q=`inscope`,J=`0.2.1`,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
63
|
Runs interactively in a terminal; pass flags or -y to skip the prompts. Re-running
|
|
64
64
|
with the same path or label updates that workspace.
|
|
65
65
|
|
|
@@ -78,14 +78,14 @@ Options:
|
|
|
78
78
|
--slack-message allow the Slack MCP server to post messages
|
|
79
79
|
--seed-slack prompt for the Slack token and store it in the keychain
|
|
80
80
|
-y, --yes accept defaults, skip all prompts (non-interactive)
|
|
81
|
-
-h, --help Display help message`,
|
|
81
|
+
-h, --help Display help message`,Pe=[{label:`github`,value:`github`,checked:!0},{label:`linear`,value:`linear`,checked:!1},{label:`notion`,value:`notion`,checked:!1},{label:`slack`,value:`slack`,checked:!1}],Fe=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||xe(a);i&&!r.label&&(o=await U(`Label`,o));let s=r.gh;s===void 0&&i&&(s=await Me(`GitHub account for this workspace`,[...Oe().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 Ne(`MCP servers (space toggles, enter confirms)`,Pe):[`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=Ce(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 je(`Paste the Slack xoxp token for ${m}: `);e?(ke(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)},Ie=`Regenerate the chpwd hook, git includes, and every .mcp.json
|
|
82
82
|
from your config. Idempotent: run it any time the config changes.
|
|
83
83
|
|
|
84
84
|
Usage:
|
|
85
85
|
$ ${q} apply
|
|
86
86
|
|
|
87
87
|
Options:
|
|
88
|
-
-h, --help Display help message`,
|
|
88
|
+
-h, --help Display help message`,Le=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ie),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)},Re=e=>{try{return t.readFileSync(e,`utf8`)}catch{return null}},ze=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},Be=(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)})},Ve=(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}},He=(e,n=I)=>{let r=[];Te()||r.push({status:`warn`,label:`platform`,detail:`inscope's secret resolution targets macOS (gh keyring + Keychain)`});let i=p(),a=Re(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(ye()?{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(re(h(),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(Ee(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=Ae(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=pe(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=ze(a);n.length&&r.push({status:`warn`,label:`${e} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r},Ue=`Verify the setup: gh tokens resolve, keychain entries exist,
|
|
89
89
|
git emails match per path, the hook is current, and no MCP server is unpinned.
|
|
90
90
|
Exits non-zero if any check fails.
|
|
91
91
|
|
|
@@ -93,34 +93,34 @@ Usage:
|
|
|
93
93
|
$ ${q} doctor
|
|
94
94
|
|
|
95
95
|
Options:
|
|
96
|
-
-h, --help Display help message`,
|
|
97
|
-
All checks passed.`),process.exit(0)},
|
|
96
|
+
-h, --help Display help message`,Z={ok:`✓`,warn:`!`,fail:`✗`},We=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ue),process.exit(0)),N()||(console.error(`No config found. Run \`${q} init\` first.`),process.exit(1));let r=P(),i=He(r);for(let e of i)console.log(`${Z[e.status]} ${e.label}${e.detail?` ${e.detail}`:``}`);let a=Be(r);if(a){let e=Ve();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)},Ge=`Set up inscope: create the config, generate the chpwd hook, and
|
|
98
98
|
source it from ~/.zshrc. Safe to run again; it never overwrites your config.
|
|
99
99
|
|
|
100
100
|
Usage:
|
|
101
101
|
$ ${q} init
|
|
102
102
|
|
|
103
103
|
Options:
|
|
104
|
-
-h, --help Display help message`,
|
|
104
|
+
-h, --help Display help message`,Ke=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(Ge),process.exit(0));let r;N()?(r=P(),console.log(`Using existing config at ${f()}`)):(r=M(),F(r),console.log(`Created ${f()}`)),j(r),console.log(`Generated the chpwd hook and added a source line to ~/.zshrc.`),console.log(`
|
|
105
105
|
Next steps:
|
|
106
106
|
1. Reload your shell: source ~/.zshrc (or open a new terminal)
|
|
107
107
|
2. Sign each GitHub account in: gh auth login
|
|
108
108
|
3. Map a workspace: ${q} add ~/acme --gh acme --email you@acme.com
|
|
109
|
-
`),process.exit(0)},
|
|
109
|
+
`),process.exit(0)},qe=`List the configured workspaces. Run \`${q} doctor\` to verify
|
|
110
110
|
that their tokens and identities actually resolve.
|
|
111
111
|
|
|
112
112
|
Usage:
|
|
113
113
|
$ ${q} list
|
|
114
114
|
|
|
115
115
|
Options:
|
|
116
|
-
-h, --help Display help message`,
|
|
116
|
+
-h, --help Display help message`,Je=e=>[e.github&&`github`,e.linear&&`linear`,e.notion&&`notion`,e.slack&&`slack`].filter(Boolean).join(`, `)||`none`,Ye=t=>{let{values:n}=e({allowPositionals:!0,options:{help:{type:`boolean`,short:`h`}},args:t});n.help&&(console.log(qe),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 ${Je(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
|
|
117
117
|
servers inscope manages; leaves your keychain and gh accounts untouched.
|
|
118
118
|
|
|
119
119
|
Usage:
|
|
120
120
|
$ ${q} rm <path|label>
|
|
121
121
|
|
|
122
122
|
Options:
|
|
123
|
-
-h, --help Display help message`,
|
|
123
|
+
-h, --help Display help message`,Xe=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}=we(P(),i);o||(console.error(`No workspace matching "${i}".`),process.exit(1)),he(o),ce(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:
|
|
124
124
|
${q}@${J}
|
|
125
125
|
|
|
126
126
|
Per-workspace identity for Claude Code: scope MCP servers, GitHub auth, and git
|
|
@@ -142,4 +142,4 @@ Options:
|
|
|
142
142
|
-h, --help Display help
|
|
143
143
|
|
|
144
144
|
Author:
|
|
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
|
|
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 Ke(n);case`add`:return await Fe(n);case`rm`:case`remove`:return Xe(n);case`ls`:case`list`:return Ye(n);case`apply`:case`sync`:return Le(n);case`doctor`:return We(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.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,h=()=>({version:1,workspaces:[]}),te=()=>e.existsSync(u()),ne=()=>{let t=u(),n=e.readFileSync(t,`utf8`),r=JSON.parse(n);return _(r),r},g=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?`),C=(e,t)=>{let n=t.replace(/\n+$/,``);return`${y(e)}\n${n}\n${b(e)}\n`},w=t=>{try{return e.readFileSync(t,`utf8`)}catch{return``}},oe=(n,r,i)=>{e.mkdirSync(t.dirname(n),{recursive:!0});let a=w(n),o=C(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)},T=(t,n)=>{let r=w(t);if(!r)return;let i=r.replace(S(n),``).replace(/\n{3,}/g,`
|
|
3
3
|
|
|
4
|
-
`).replace(/^\n+/,``);e.writeFileSync(t,i)},
|
|
5
|
-
`),
|
|
4
|
+
`).replace(/^\n+/,``);e.writeFileSync(t,i)},se=(e,t)=>{let n=w(e).match(RegExp(`${x(y(t))}\\n([\\s\\S]*?)\\n${x(b(t))}`));return n?n[1]:null},E=`gitconfig`,D=e=>!!(e.git&&(e.git.email||e.git.name)),O=e=>t.join(f(),`${e}.gitconfig`),ce=e=>s(e).replace(/\/+$/,``)+`/`,k=e=>e.workspaces.filter(D).map(e=>`[includeIf "gitdir:${ce(e.path)}"]\n\tpath = ${s(O(e.name))}`).join(`
|
|
5
|
+
`),A=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
|
+
`},j=t=>{e.mkdirSync(f(),{recursive:!0});for(let n of t.workspaces)D(n)&&e.writeFileSync(O(n.name),A(n));let n=k(t);n?oe(p(),E,n):T(p(),E)},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:``,M=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
|
#
|
|
@@ -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
|
+
`},N=`1.3.0`,de=[`github`,`linear`,`notion`,`slack`],P=e=>de.map(t=>`${t}-${e}`),F=e=>t.join(c(e.path),`.mcp.json`),I=(e,t)=>e&&typeof e==`object`&&e.url?e.url:t,L=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:I(t.linear,`https://mcp.linear.app/mcp`)}),t.notion&&(n[`notion-${e.name}`]={type:`http`,url:I(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@${N}`,`--transport`,`stdio`],env:r}}return n},R=e=>({mcpServers:L(e)}),z=t=>{if(!e.existsSync(t))return{};try{return JSON.parse(e.readFileSync(t,`utf8`))}catch{return{}}},B=t=>{let n=F(t);return e.existsSync(n)?z(n):null},V=n=>{let r=F(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 P(n.name))delete a[e];Object.assign(a,L(n)),i.mcpServers=a,e.writeFileSync(r,JSON.stringify(i,null,2)+`
|
|
55
|
+
`)},fe=t=>{let n=F(t);if(!e.existsSync(n))return;let r=z(n);if(r.mcpServers&&typeof r.mcpServers==`object`)for(let e of P(t.name))delete r.mcpServers[e];e.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
56
|
+
`)},pe=e=>{let n=i();return e===n?`$HOME`:e.startsWith(n+t.sep)?`$HOME/${e.slice(n.length+1)}`:e},H=()=>{let e=pe(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}},me=n=>{let r=d();e.mkdirSync(t.dirname(r),{recursive:!0}),e.writeFileSync(r,M(n)),j(n),W();let i=[];for(let e of n.workspaces)V(e),i.push(F(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()},he=(e=K)=>{let t=[];for(let n of X(e).matchAll(/account (\S+) \(/g))t.includes(n[1])||t.push(n[1]);return t},ge=(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},_e=(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},ve=t=>{try{return e.readFileSync(t,`utf8`)}catch{return null}},ye=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},be=(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)})},xe=(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}},Se=(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=ve(i);a===null?r.push({status:`fail`,label:`hook`,detail:`missing ${i}; run \`inscope init\``}):a===M(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(D)&&r.push(se(p(),E)===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(D(i)){let a=O(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=P(i.name).filter(e=>a.mcpServers?.[e]);r.push({status:`ok`,label:`${t} mcp`,detail:`${e.length} server(s)`});let n=ye(a);n.length&&r.push({status:`warn`,label:`${t} mcp`,detail:`unpinned: ${n.join(`, `)}`})}}return r};export{ee as CONFIG_VERSION,N as SLACK_MCP_VERSION,me as applyAll,j as applyGitconfig,V as applyMcp,te as configExists,be as currentWorkspace,h as defaultConfig,K as defaultRunner,W as ensureZshrcSource,v as findWorkspace,he as ghAccounts,X as ghStatus,Y as ghToken,$ as gitEmailForFile,ge as gitGlobal,q as isMacOS,Z as keychainHas,_e as keychainSet,Q as keychainSetCommand,re as labelFromPath,xe as liveSnapshot,ne as loadConfig,P as managedKeys,F as mcpFilePath,B as readMcp,fe as removeMcp,ae as removeWorkspace,k as renderGitInclude,M as renderHook,R as renderMcp,A as renderPerWorkspaceGitconfig,L as renderServers,U as renderZshrcSource,Se as runDoctor,g as saveConfig,ie as upsertWorkspace,_ as validateConfig,G as zshrcSourcesHook};
|
package/package.json
CHANGED