notemend 0.1.6 → 0.1.8
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/.claude-plugin/plugin.json +1 -1
- package/README.md +191 -176
- package/bin/init.mjs +6 -6
- package/bin/notemend.mjs +19 -15
- package/commands/turbo.md +22 -0
- package/package.json +3 -2
- package/src/client/config.js +1 -1
- package/src/client/markup.js +18 -12
- package/src/client/notemend.css +1 -1
- package/src/client/run.js +73 -64
- package/src/server/middleware.mjs +1 -1
- package/src/store/cloud.mjs +1 -1
- package/src/store/comments.mjs +3 -3
- package/store.d.ts +2 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as R,existsSync as T,statSync as W,readdirSync as z}from"node:fs";import{mask as b}from"../store/mask.mjs";import{join as $,isAbsolute as K}from"node:path";import{isIP as D}from"node:net";import{pathToFileURL as V}from"node:url";import{openStore as Z,retry as Q}from"../store/cloud.mjs";import{mentionsIn as X}from"../store/thread.mjs";import{CATEGORIES as Y}from"../store/categories.mjs";import{BREAKPOINTS as q,DEVICES as tt,ICONS as H,SHELLS as M,BROWSERS as G}from"../presets.mjs";const J={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",gif:"image/gif",svg:"image/svg+xml",webm:"video/webm"},U=t=>t.split(".").slice(0,2).map(Number).reduce((n,u)=>n*100+(Number.isNaN(u)?9999:u),0),et=(t,n)=>{try{const m=R(K(n)?n:$(t,n),"utf8").match(/title="([^"]+)"|\btitle:\s*["'`]([^"'`]+)["'`]/);return(m?.[1]??m?.[2])?.split("\u2014").pop()?.trim()||null}catch{return null}};function vt(t){const n=["app","src/app"].map(c=>$(t,c)).find(T),u=[],m=(c,f)=>{for(const l of z(c,{withFileTypes:!0})){const i=l.name;if(l.isDirectory()){if(/^[_@]|^\(\./.test(i))continue;m($(c,i),/^\(.+\)$/.test(i)?f:[...f,i])}else/^page\.(jsx?|tsx?|mdx?)$/.test(i)&&u.push({origin:"project",type:"page",pattern:"/"+f.join("/"),entrypoint:$(c,i)})}};return n&&m(n,[]),u}const nt=t=>t[0].toUpperCase()+t.slice(1).replace(/[-_]+/g," ");function rt({root:t,options:n,routes:u}){const m=n.pages??{},c=(e,d,O)=>({route:e,name:d,label:d.replace(/^[\d.]+[A-Z]?\.\s*/,""),group:O}),f=u.filter(e=>e.origin==="project"&&e.type==="page"&&!e.pattern.includes("[")).map(e=>({route:e.pattern.replace(/\/+$/,"")||"/",entrypoint:e.entrypoint})),l=new Set(f.map(e=>e.route)),i=[...f,...Object.keys(m).filter(e=>!l.has(e)).map(e=>({route:e}))],N={};for(const{route:e}of i){const d=e.split("/")[1];d&&(N[d]=(N[d]||0)+1)}const k=e=>{const d=e.split("/")[1];return d&&N[d]>1?nt(d):"Pages"},L=n.groups?()=>n.groups[n.groups.length-1]:k,I=i.map(({route:e,entrypoint:d})=>{const[O,C]=m[e]??[];return c(e,O??(d&&et(t,d))??e,C??L(e))}),r=n.groups??[...new Set([...Object.values(m).map(([,e])=>e).filter(Boolean),"Pages",...I.map(e=>e.group).sort()])],g=I.sort((e,d)=>r.indexOf(e.group)-r.indexOf(d.group)||U(e.name)-U(d.name)||e.route.localeCompare(d.route)),s=(n.breakpoints??q).map(e=>({id:e.id,label:e.label,min:e.min,ideal:e.ideal,max:Number.isFinite(e.max)?e.max:null,icon:e.icon??H[e.id]??H.desktop})),o=(n.devices??tt).filter(e=>e.w>0&&e.h>0&&M.includes(e.shell)&&G.includes(e.browser)?!0:(console.warn(`[notemend] device "${e.id}" skipped: it needs w and h, shell (${M.join(", ")}) and browser (${G.join(", ")})${e.frame?"; `frame` pictures were replaced by shell + browser in 0.3":""}`),!1));return{pages:g,groups:r.filter(e=>g.some(d=>d.group===e)),breakpoints:s,devices:o,categories:Y,sanity:!!n.sanity,shortcuts:n.shortcuts!==!1,ignore:n.ignore??[],grid:{container:"container",grid:"grid",columns:12,...n.grid},zIndex:n.zIndex??2000000020}}const st=t=>{const n=$(t,"notemend.config.mjs");let u=0,m={};return async()=>{if(!T(n))return{};const c=W(n).mtimeMs;if(c===u)return m;u=c;try{const{default:f,...l}=await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ `${V(n).href}?t=${c}`);m={...l,...f}}catch(f){console.warn(`[notemend] notemend.config.mjs: ${f.message}`)}return m}},ot=t=>{const n=String(t.headers.host||"").toLowerCase(),u=n.startsWith("[")?n.slice(1,n.indexOf("]")):n.replace(/:\d+$/,"");return u.includes(":")?D(u)===6:!u||!u.includes(".")||D(u)===4||/\.(localhost|local|test|internal|lan|home\.arpa)$/.test(u)},h=(t,n)=>String(t??"").replace(/[\u0000-\u001f\u007f]+/g," ").trim().slice(0,n),at=t=>({route:h(t.route,500),text:String(t.text??"").slice(0,1e4),selector:h(t.selector,2e3),rx:Number.isFinite(t.rx)?t.rx:0,ry:Number.isFinite(t.ry)?t.ry:0,label:h(t.label,200),ctx:h(t.ctx,120),breakpoint:h(t.breakpoint,40),browser:h(t.browser,80),device:h(t.device,80),tag:h(t.tag,40),classes:Array.isArray(t.classes)?t.classes.slice(0,20).map(n=>h(n,80)):[],nth:Number.isInteger(t.nth)?t.nth:null,category:h(t.category,40),viewport:t.viewport&&Number.isFinite(t.viewport.w)&&Number.isFinite(t.viewport.h)?{w:t.viewport.w,h:t.viewport.h}:null,...t.assignee==="claude"||typeof t.assignee=="string"&&/^[^\s@]+@[^\s@]+$/.test(t.assignee)&&t.assignee.length<=200?{assignee:t.assignee.toLowerCase()}:{},...Array.isArray(t.logs)&&t.logs.length?{logs:t.logs.slice(-20).map(n=>({at:h(n?.at,30),kind:["console","error","rejection","network"].includes(n?.kind)?n.kind:"console",text:b(h(n?.text,500))})).filter(n=>n.text)}:{},...t.suggest&&typeof t.suggest.from=="string"&&typeof t.suggest.to=="string"&&t.suggest.to.trim()?{suggest:{from:t.suggest.from.slice(0,2e3),to:t.suggest.to.slice(0,2e3)}}:{}}),it=t=>{const n={};return(t.status==="open"||t.status==="done")&&(n.status=t.status),"doneBy"in t&&(n.doneBy=t.doneBy==="you"?"you":null),typeof t.category=="string"&&(n.category=h(t.category,40)),"assignee"in t&&(n.assignee=t.assignee==="claude"||typeof t.assignee=="string"&&/^[^\s@]+@[^\s@]+$/.test(t.assignee)&&t.assignee.length<=200?t.assignee.toLowerCase():null),n},ct=t=>{const n=t.headers.origin;if(!n)return!1;try{return new URL(n).host!==t.headers.host}catch{return!0}};function lt({root:t,options:n,routes:u,open:m=()=>Z(t),crossOrigin:c=!1}){const f=st(t),l=(r,g=200)=>({status:g,type:"application/json",body:JSON.stringify(r)}),i=(r,g)=>({status:r,type:"text/plain",body:g}),N=async r=>{const g=await r.body();if(g.length>45e5)throw Object.assign(new Error("too large"),{status:413});try{return g.length?JSON.parse(g.toString("utf8")):{}}catch{throw Object.assign(new Error("not JSON"),{status:400})}},k=async(r,g)=>{const s=async o=>{const e=await g(o);if(e.status>=400)throw Object.assign(new Error("not written"),{out:e});return e};try{if(r.cloud)return await Q(r,s);const o=r.load(),e=await s(o);return r.save(o),e}catch(o){if(o.out)return o.out;throw o}},L=async(r,g)=>{if(r.method!=="GET"&&!c&&ct(r))return i(403,"Cross-origin write refused");const s=await m();if(r.method==="GET"){const a=g.searchParams.get("route"),p=s.version?String(await s.version()):null;if(p&&g.searchParams.get("since")===p)return{status:204,type:"text/plain",body:""};const x=await s.load();return l({version:p,comments:a?x.comments.filter(S=>S.route===a):x.comments})}let o=null,e=null;const d=s.cloud&&r.method!=="DELETE"?s.people().then(a=>{o=a,e=a.me},a=>{throw a}).catch(a=>a):Promise.resolve(),O=async()=>{const a=await d;if(a instanceof Error)throw a},C=a=>{if(e&&a){a.by=e.email,a.byName=e.name;const p=X(a.text,o.team.filter(x=>x.email!==e.email));p.length&&(a.mentions=p)}return a};if(r.method==="POST"){const{image:a,images:p,...x}=await N(r),S=at(x);return S.text.trim()?S.route.startsWith("/")?k(s,async A=>{await O();const y=C(s.add(A,S));for(const P of[].concat(p??a??[]))await s.attach(A,y.id,P);return l(y,201)}):l({error:"route must be a path"},400):l({error:"empty comment"},400)}if(r.method==="PATCH"){const{id:a,reply:p,image:x,images:S,removeImage:A,edit:y,...P}=await N(r),w=Number(a);if(!Number.isInteger(w))return l({error:"bad id"},400);const E=it(P);return k(s,async j=>{if(await O(),y){const v=j.comments.find(B=>B.id===Number(w)),_=y.at?v?.replies?.find(B=>B.at===y.at):v;if(e&&_?.by&&_.by!==e.email||!(y.text?.trim?.()&&s.edit(j,w,y.at??null,y.text.trim().slice(0,1e4))))return l({error:`no message of yours to edit in #${w}`},404)}p?.trim?.()&&C(s.reply(j,w,"you",p.trim().slice(0,1e4))?.replies.at(-1));for(const v of[].concat(S??x??[]))await s.attach(j,w,v);for(const v of[].concat(A||[]))await s.detach(j,w,v);e&&E.status==="done"&&Object.assign(E,{confirmedBy:e.email,confirmedAt:new Date().toISOString()}),e&&E.status==="open"&&Object.assign(E,{confirmedBy:null,confirmedAt:null});const F=Object.keys(E).length||!p?s.patch(j,w,E):j.comments.find(v=>v.id===Number(w));return F?l(F):l({error:`no comment #${w}`},404)})}if(r.method==="DELETE"){const a=Number(g.searchParams.get("id"));return k(s,async p=>await s.remove(p,a)?l({ok:!0,id:a}):l({error:`no comment #${a}`},404))}return i(405,"Method not allowed")},I=(r,g)=>{const s=g.replace(/^.*[/\\]/,""),o=s.split(".").pop()?.toLowerCase()??"";if(!/^[\w.-]+$/.test(s)||!J[o])return i(400,"Bad name");const e=$(r,s);return T(e)?{status:200,type:J[o],body:R(e)}:i(404,"Not found")};return async r=>{const g=new URL(r.url,"http://devbar"),s=g.pathname.slice(10);if(!c&&!ot(r))return i(403,"Notemend answers local addresses only");try{if(s==="config"){const o=rt({root:t,options:{...await f(),...n},routes:u()}),e=await m();return e.cloud&&Object.assign(o,await e.people().catch(()=>({}))),l(o)}if(s==="comments")return await L(r,g);if(s==="image"){const o=await m(),e=(g.searchParams.get("name")||"").replace(/^.*[/\\]/,"");return o.cloud&&/^[\w.-]+$/.test(e)&&await o.fetchImage(e).catch(()=>null),I(o.imageDir,e)}return i(404,"Not found")}catch(o){return o.status===400||o.status===413?i(o.status,o.message):(console.error("[notemend]",o),i(500,"Something went wrong \u2014 the dev server log says what."))}}}function St(t){const n=lt(t),u=m=>new Promise((c,f)=>{const l=[];m.on("data",i=>l.push(i)),m.on("end",()=>c(Buffer.concat(l))),m.on("error",f)});return async function(c,f,l){if(!c.url||!c.url.startsWith("/__devbar/"))return l();const i=await n({method:c.method,url:c.url,headers:c.headers,body:()=>u(c)});f.statusCode=i.status,f.setHeader("content-type",i.type),f.setHeader("cache-control","no-store"),f.end(i.body)}}export{rt as buildConfig,lt as notemendHandler,St as notemendMiddleware,vt as scanApp,et as titleOf,nt as titled};
|
|
1
|
+
import{readFileSync as b,existsSync as D,statSync as W,readdirSync as z}from"node:fs";import{mask as K}from"../store/mask.mjs";import{join as k,isAbsolute as V}from"node:path";import{isIP as R}from"node:net";import{pathToFileURL as Z}from"node:url";import{openStore as Q,retry as X}from"../store/cloud.mjs";import{mentionsIn as Y}from"../store/thread.mjs";import{CATEGORIES as q}from"../store/categories.mjs";import{BREAKPOINTS as tt,DEVICES as et,ICONS as H,SHELLS as M,BROWSERS as G}from"../presets.mjs";const J={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",gif:"image/gif",svg:"image/svg+xml",webm:"video/webm"},U=t=>t.split(".").slice(0,2).map(Number).reduce((n,l)=>n*100+(Number.isNaN(l)?9999:l),0),nt=(t,n)=>{try{const m=b(V(n)?n:k(t,n),"utf8").match(/title="([^"]+)"|\btitle:\s*["'`]([^"'`]+)["'`]/);return(m?.[1]??m?.[2])?.split("\u2014").pop()?.trim()||null}catch{return null}};function vt(t){const n=["app","src/app"].map(u=>k(t,u)).find(D),l=[],m=(u,f)=>{for(const i of z(u,{withFileTypes:!0})){const c=i.name;if(i.isDirectory()){if(/^[_@]|^\(\./.test(c))continue;m(k(u,c),/^\(.+\)$/.test(c)?f:[...f,c])}else/^page\.(jsx?|tsx?|mdx?)$/.test(c)&&l.push({origin:"project",type:"page",pattern:"/"+f.join("/"),entrypoint:k(u,c)})}};return n&&m(n,[]),l}const rt=t=>t[0].toUpperCase()+t.slice(1).replace(/[-_]+/g," ");function st({root:t,options:n,routes:l}){const m=n.pages??{},u=(e,d,$)=>({route:e,name:d,label:d.replace(/^[\d.]+[A-Z]?\.\s*/,""),group:$}),f=l.filter(e=>e.origin==="project"&&e.type==="page"&&!e.pattern.includes("[")).map(e=>({route:e.pattern.replace(/\/+$/,"")||"/",entrypoint:e.entrypoint})),i=new Set(f.map(e=>e.route)),c=[...f,...Object.keys(m).filter(e=>!i.has(e)).map(e=>({route:e}))],N={};for(const{route:e}of c){const d=e.split("/")[1];d&&(N[d]=(N[d]||0)+1)}const E=e=>{const d=e.split("/")[1];return d&&N[d]>1?rt(d):"Pages"},P=n.groups?()=>n.groups[n.groups.length-1]:E,I=c.map(({route:e,entrypoint:d})=>{const[$,B]=m[e]??[];return u(e,$??(d&&nt(t,d))??e,B??P(e))}),r=n.groups??[...new Set([...Object.values(m).map(([,e])=>e).filter(Boolean),"Pages",...I.map(e=>e.group).sort()])],g=I.sort((e,d)=>r.indexOf(e.group)-r.indexOf(d.group)||U(e.name)-U(d.name)||e.route.localeCompare(d.route)),s=(n.breakpoints??tt).map(e=>({id:e.id,label:e.label,min:e.min,ideal:e.ideal,max:Number.isFinite(e.max)?e.max:null,icon:e.icon??H[e.id]??H.desktop})),a=(n.devices??et).filter(e=>e.w>0&&e.h>0&&M.includes(e.shell)&&G.includes(e.browser)?!0:(console.warn(`[notemend] device "${e.id}" skipped: it needs w and h, shell (${M.join(", ")}) and browser (${G.join(", ")})${e.frame?"; `frame` pictures were replaced by shell + browser in 0.3":""}`),!1));return{pages:g,groups:r.filter(e=>g.some(d=>d.group===e)),breakpoints:s,devices:a,categories:q,sanity:!!n.sanity,shortcuts:n.shortcuts!==!1,ignore:n.ignore??[],grid:{container:"container",grid:"grid",columns:12,...n.grid},zIndex:n.zIndex??2000000020}}const ot=t=>{const n=k(t,"notemend.config.mjs");let l=0,m={};return async()=>{if(!D(n))return{};const u=W(n).mtimeMs;if(u===l)return m;l=u;try{const{default:f,...i}=await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ `${Z(n).href}?t=${u}`);m={...i,...f}}catch(f){console.warn(`[notemend] notemend.config.mjs: ${f.message}`)}return m}},at=t=>{const n=String(t.headers.host||"").toLowerCase(),l=n.startsWith("[")?n.slice(1,n.indexOf("]")):n.replace(/:\d+$/,"");return l.includes(":")?R(l)===6:!l||!l.includes(".")||R(l)===4||/\.(localhost|local|test|internal|lan|home\.arpa)$/.test(l)},p=(t,n)=>String(t??"").replace(/[\u0000-\u001f\u007f]+/g," ").trim().slice(0,n),it=t=>({route:p(t.route,500),text:String(t.text??"").slice(0,1e4),selector:p(t.selector,2e3),rx:Number.isFinite(t.rx)?t.rx:0,ry:Number.isFinite(t.ry)?t.ry:0,label:p(t.label,200),ctx:p(t.ctx,120),breakpoint:p(t.breakpoint,40),browser:p(t.browser,80),device:p(t.device,80),tag:p(t.tag,40),classes:Array.isArray(t.classes)?t.classes.slice(0,20).map(n=>p(n,80)):[],nth:Number.isInteger(t.nth)?t.nth:null,category:p(t.category,40),viewport:t.viewport&&Number.isFinite(t.viewport.w)&&Number.isFinite(t.viewport.h)?{w:t.viewport.w,h:t.viewport.h}:null,...t.assignee==="claude"||typeof t.assignee=="string"&&/^[^\s@]+@[^\s@]+$/.test(t.assignee)&&t.assignee.length<=200?{assignee:t.assignee.toLowerCase()}:{},...typeof t.host=="string"&&/^[a-z0-9.-]{1,120}(:\d{1,5})?$/i.test(t.host)?{host:t.host.toLowerCase()}:{},...Array.isArray(t.logs)&&t.logs.length?{logs:t.logs.slice(-20).map(n=>({at:p(n?.at,30),kind:["console","error","rejection","network"].includes(n?.kind)?n.kind:"console",text:K(p(n?.text,500))})).filter(n=>n.text)}:{},...t.suggest&&typeof t.suggest.from=="string"&&typeof t.suggest.to=="string"&&t.suggest.to.trim()?{suggest:{from:t.suggest.from.slice(0,2e3),to:t.suggest.to.slice(0,2e3)}}:{}}),ct=t=>{const n={};return(t.status==="open"||t.status==="done")&&(n.status=t.status),"doneBy"in t&&(n.doneBy=t.doneBy==="you"?"you":null),typeof t.category=="string"&&(n.category=p(t.category,40)),"assignee"in t&&(n.assignee=t.assignee==="claude"||typeof t.assignee=="string"&&/^[^\s@]+@[^\s@]+$/.test(t.assignee)&&t.assignee.length<=200?t.assignee.toLowerCase():null),n},ut=t=>{const n=t.headers.origin;if(!n)return!1;try{return new URL(n).host!==t.headers.host}catch{return!0}};function lt({root:t,options:n,routes:l,open:m=()=>Q(t),crossOrigin:u=!1}){const f=ot(t),i=(r,g=200)=>({status:g,type:"application/json",body:JSON.stringify(r)}),c=(r,g)=>({status:r,type:"text/plain",body:g}),N=async r=>{const g=await r.body();if(g.length>45e5)throw Object.assign(new Error("too large"),{status:413});try{return g.length?JSON.parse(g.toString("utf8")):{}}catch{throw Object.assign(new Error("not JSON"),{status:400})}},E=async(r,g)=>{const s=async a=>{const e=await g(a);if(e.status>=400)throw Object.assign(new Error("not written"),{out:e});return e};try{if(r.cloud)return await X(r,s);const a=r.load(),e=await s(a);return r.save(a),e}catch(a){if(a.out)return a.out;throw a}},P=async(r,g)=>{if(r.method!=="GET"&&!u&&ut(r))return c(403,"Cross-origin write refused");const s=await m();if(r.method==="GET"){const o=g.searchParams.get("route"),h=s.version?String(await s.version()):null;if(h&&g.searchParams.get("since")===h)return{status:204,type:"text/plain",body:""};const w=await s.load();return i({version:h,comments:o?w.comments.filter(j=>j.route===o):w.comments,listening:w.listening??null,turbo:w.turbo??null})}let a=null,e=null;const d=s.cloud&&r.method!=="DELETE"?s.people().then(o=>{a=o,e=o.me},o=>{throw o}).catch(o=>o):Promise.resolve(),$=async()=>{const o=await d;if(o instanceof Error)throw o},B=o=>{if(e&&o){o.by=e.email,o.byName=e.name;const h=Y(o.text,a.team.filter(w=>w.email!==e.email));h.length&&(o.mentions=h)}return o};if(r.method==="POST"){const{image:o,images:h,...w}=await N(r),j=it(w);return j.text.trim()?j.route.startsWith("/")?E(s,async C=>{await $();const A=B(s.add(C,j));for(const O of[].concat(h??o??[]))await s.attach(C,A.id,O);return i(A,201)}):i({error:"route must be a path"},400):i({error:"empty comment"},400)}if(r.method==="PATCH"){const o=await N(r);if(o.turbo==="on"||o.turbo==="off")return E(s,async S=>(S.turbo={paused:o.turbo==="off",at:new Date().toISOString()},i({turbo:S.turbo})));const{id:h,reply:w,image:j,images:C,removeImage:A,edit:O,...F}=o,x=Number(h);if(!Number.isInteger(x))return i({error:"bad id"},400);const v=ct(F);return E(s,async S=>{if(await $(),O){const y=S.comments.find(T=>T.id===Number(x)),L=O.at?y?.replies?.find(T=>T.at===O.at):y;if(e&&L?.by&&L.by!==e.email||!(O.text?.trim?.()&&s.edit(S,x,O.at??null,O.text.trim().slice(0,1e4))))return i({error:`no message of yours to edit in #${x}`},404)}w?.trim?.()&&B(s.reply(S,x,"you",w.trim().slice(0,1e4))?.replies.at(-1));for(const y of[].concat(C??j??[]))await s.attach(S,x,y);for(const y of[].concat(A||[]))await s.detach(S,x,y);if(e&&v.status==="done"&&Object.assign(v,{confirmedBy:e.email,confirmedAt:new Date().toISOString()}),e&&v.status==="open"&&Object.assign(v,{confirmedBy:null,confirmedAt:null}),F.archive===!0){const y=S.comments.find(L=>L.id===x);y&&y.status!=="done"&&Object.assign(v,{status:"done",doneBy:"you",working:null}),y?.doneBy==="claude"&&e&&Object.assign(v,{doneBy:"you",confirmedBy:e.email,confirmedAt:new Date().toISOString()}),Object.assign(v,{archivedAt:new Date().toISOString(),...e?{archivedBy:e.email}:{}})}const _=Object.keys(v).length||!w?s.patch(S,x,v):S.comments.find(y=>y.id===Number(x));return _?i(_):i({error:`no comment #${x}`},404)})}if(r.method==="DELETE"){const o=Number(g.searchParams.get("id"));return E(s,async h=>await s.remove(h,o)?i({ok:!0,id:o}):i({error:`no comment #${o}`},404))}return c(405,"Method not allowed")},I=(r,g)=>{const s=g.replace(/^.*[/\\]/,""),a=s.split(".").pop()?.toLowerCase()??"";if(!/^[\w.-]+$/.test(s)||!J[a])return c(400,"Bad name");const e=k(r,s);return D(e)?{status:200,type:J[a],body:b(e)}:c(404,"Not found")};return async r=>{const g=new URL(r.url,"http://devbar"),s=g.pathname.slice(10);if(!u&&!at(r))return c(403,"Notemend answers local addresses only");try{if(s==="config"){const a=st({root:t,options:{...await f(),...n},routes:l()}),e=await m();return e.cloud&&Object.assign(a,await e.people().catch(()=>({}))),i(a)}if(s==="comments")return await P(r,g);if(s==="image"){const a=await m(),e=(g.searchParams.get("name")||"").replace(/^.*[/\\]/,"");return a.cloud&&/^[\w.-]+$/.test(e)&&await a.fetchImage(e).catch(()=>null),I(a.imageDir,e)}return c(404,"Not found")}catch(a){return a.status===400||a.status===413?c(a.status,a.message):(console.error("[notemend]",a),c(500,"Something went wrong \u2014 the dev server log says what."))}}}function Ot(t){const n=lt(t),l=m=>new Promise((u,f)=>{const i=[];m.on("data",c=>i.push(c)),m.on("end",()=>u(Buffer.concat(i))),m.on("error",f)});return async function(u,f,i){if(!u.url||!u.url.startsWith("/__devbar/"))return i();const c=await n({method:u.method,url:u.url,headers:u.headers,body:()=>l(u)});f.statusCode=c.status,f.setHeader("content-type",c.type),f.setHeader("cache-control","no-store"),f.end(c.body)}}export{st as buildConfig,lt as notemendHandler,Ot as notemendMiddleware,vt as scanApp,nt as titleOf,rt as titled};
|
package/src/store/cloud.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import{readFileSync as D,writeFileSync as S,existsSync as A,mkdirSync as j,rmSync as $}from"node:fs";import{homedir as L}from"node:os";import{dirname as F,join as h}from"node:path";import{createStore as H,add as K,reply as z,edit as Y,patch as Z,shotName as v}from"./comments.mjs";const M=process.env.NOTEMEND_APP||"https://app.notemend.com",g={url:process.env.NOTEMEND_URL||"https://xzhnjldvkirrydbyrdnm.supabase.co",key:process.env.NOTEMEND_KEY||"sb_publishable_b8h8jTHZ9KtbPvMuroLhqA_-yBqZwd6"},b=h(process.env.NOTEMEND_HOME||h(L(),".config","notemend"),"session.json"),N=e=>h(e,".notemend.json"),G=e=>{try{return JSON.parse(D(N(e),"utf8")).project||null}catch{return null}},se=(e,n)=>S(N(e),JSON.stringify({project:n},null,2)+`
|
|
2
2
|
`),ae=e=>$(N(e),{force:!0}),R=()=>{try{const e=JSON.parse(D(b,"utf8"));return e.url===g.url?e:null}catch{return null}},W=e=>{j(F(b),{recursive:!0}),S(b,JSON.stringify(e,null,2)+`
|
|
3
|
-
`,{mode:384})},ie=()=>$(b,{force:!0});async function w(e,{method:n="GET",token:
|
|
3
|
+
`,{mode:384})},ie=()=>$(b,{force:!0});async function w(e,{method:n="GET",token:i,body:s,headers:f={},raw:c=!1}={}){const d=await fetch(`${g.url}${e}`,{method:n,headers:{apikey:g.key,authorization:`Bearer ${i||g.key}`,...s&&!c?{"content-type":"application/json"}:{},...f},body:c?s:s&&JSON.stringify(s)});if(!d.ok){const m=await d.text();let y=m;try{const k=JSON.parse(m);y=k.msg||k.message||k.error_description||k.error||m}catch{}const _=new Error(`${y} (${d.status})`);throw _.status=d.status,_}return d}const E=e=>{const n={url:g.url,email:e.user.email,user:e.user.id,access_token:e.access_token,refresh_token:e.refresh_token,expires_at:e.expires_at??Math.floor(Date.now()/1e3)+e.expires_in};return W(n),n},ce=e=>w("/auth/v1/otp",{method:"POST",body:{email:e,create_user:!0}}),ue=async(e,n)=>E(await(await w("/auth/v1/verify",{method:"POST",body:{email:e,token:n,type:"email"}})).json());async function le(e){const n=await fetch(`${M}/api/cli/device`,{method:"POST"}),i=await n.json().catch(()=>({}));if(!n.ok)throw new Error(i.error||`The cabinet said ${n.status}`);console.log(`Your code: ${i.code}`),e(i.url);const s=Date.now()+i.expires_in*1e3;for(;Date.now()<s;){await new Promise(m=>setTimeout(m,i.interval*1e3));const f=await fetch(`${M}/api/cli/device?device=${i.device}`).catch(()=>null);if(!f||f.status===202)continue;const c=await f.json().catch(()=>({}));if(!f.ok)throw new Error(c.error||`The cabinet said ${f.status}`);const d=await(await w("/auth/v1/user",{token:c.access_token})).json();return E({user:d,access_token:c.access_token,refresh_token:c.refresh_token,expires_at:Number(c.expires_at)})}throw new Error("No answer from the browser in 10 minutes.")}async function fe(e,n){const i=await O();if(!i)throw new Error("Sign in first: npx notemend login");return(await(await w("/rest/v1/projects",{method:"POST",token:i.access_token,headers:{prefer:"return=representation"},body:{name:e,...n?{site_url:n}:{}}})).json())[0]}async function O(){const e=R();if(!e)return null;if(e.expires_at-60>Date.now()/1e3)return e;try{const n=await w("/auth/v1/token?grant_type=refresh_token",{method:"POST",body:{refresh_token:e.refresh_token}});return E(await n.json())}catch(n){if(n.status===400||n.status===401)return null;throw n}}class B extends Error{}function Q(e,n,i=null){const s=h(e,"feedback","images"),f=t=>`feedback/images/${t}`,c=new WeakMap,d=i?async()=>i:async()=>{const t=await O();if(!t)throw new Error("Not signed in to Notemend \u2014 run `npx notemend login`.");return t},m=async(t,r={})=>w(`/rest/v1/${t}`,{...r,token:(await d()).access_token}),y=async(t,r={})=>w(`/storage/v1/object/shots/${n}/${t}`,{...r,token:(await d()).access_token}),_=async()=>{const t=await(await m(`stores?project_id=eq.${n}&select=doc,version`)).json();if(!t.length)throw new Error(`Project ${n} is not there, or you are not in it. Check \`npx notemend link\`.`);const r=t[0].doc;return c.set(r,t[0].version),r},k=async()=>(await(await m(`stores?project_id=eq.${n}&select=version`)).json())[0]?.version??null,q=async t=>{const r=c.get(t),u=await(await m(`stores?project_id=eq.${n}&version=eq.${r}`,{method:"PATCH",body:{doc:t},headers:{prefer:"return=representation"}})).json();if(!u.length)throw new B("Someone else wrote first");c.set(t,u[0].version)},J=async(t,r,u)=>{const a=t.comments.find(I=>I.id===Number(r)),l=/^data:(?:image|video)\/(png|jpeg|webp|gif|webm);base64,(.+)$/s.exec(u||"");if(!a||!l)return null;const p=l[1]==="jpeg"?"jpg":l[1];let o=Date.now();for(;A(h(s,`${a.id}-${o}.${p}`));)o++;const x=`${a.id}-${o}.${p}`,P=Buffer.from(l[2],"base64");return await y(x,{method:"POST",body:P,raw:!0,headers:{"content-type":l[1]==="webm"?"video/webm":`image/${l[1]}`,"x-upsert":"true"}}),j(s,{recursive:!0}),S(h(s,x),P),(a.images||=[]).push(f(x)),a.updatedAt=new Date().toISOString(),f(x)},T=async t=>{v(t)&&($(h(s,t),{force:!0}),await y(t,{method:"DELETE"}).catch(()=>{}))};return{cloud:!0,project:n,root:e,imageDir:s,load:_,version:k,save:q,attach:J,detach:async(t,r,u)=>{const a=t.comments.find(o=>o.id===Number(r)),l=v(u),p=l&&a?.images?.find(o=>v(o)===l);return p?(await T(l),a.images=a.images.filter(o=>o!==p),a.updatedAt=new Date().toISOString(),!0):!1},remove:async(t,r)=>{const u=t.comments.find(a=>a.id===Number(r));if(!u)return!1;for(const a of u.images||[])await T(v(a));return t.comments=t.comments.filter(a=>a.id!==Number(r)),!0},fetchImage:async t=>{if(!v(t)||v(t)!==t)return null;const r=h(s,t);if(A(r))return r;const u=await y(t).catch(()=>null);return u?(j(s,{recursive:!0}),S(r,Buffer.from(await u.arrayBuffer())),r):null},people:async()=>{const t=await d(),[r,u,a]=await Promise.all([m("profiles?select=email,name,nickname,avatar_url").then(o=>o.json()),m("rpc/project_people",{method:"POST",body:{p:n}}).then(o=>o.json()).catch(()=>null),m("rpc/is_staff",{method:"POST",body:{p:n}}).then(o=>o.json()).catch(()=>!0)]),l=Array.isArray(u)?new Set(u):null,p=l?r.filter(o=>l.has(o.email)):r;return{me:r.find(o=>o.email===t.email)??{email:t.email,name:"",avatar_url:null},team:p,client:a===!1}},add:K,reply:z,edit:Y,patch:Z}}let C=!1;async function me(e){const n=G(e);return n&&await O()?Q(e,n):(n&&!C&&(C=!0,console.warn("[notemend] this site is linked to a Notemend project, but this computer is not signed in \u2014 comments stay in feedback/ until you run `npx notemend login`.")),H(e))}async function de(e,n){for(let i=0;;i++){const s=await e.load(),f=await n(s);try{return await e.save(s),f}catch(c){if(!(c instanceof B)||i>=4)throw c}}}export{g as BACKEND,M as CABINET,B as Conflict,Q as cloudStore,fe as createProject,le as deviceLogin,se as link,G as linkedProject,ie as logout,me as openStore,de as retry,ce as sendCode,O as session,ae as unlink,ue as verifyCode};
|
package/src/store/comments.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{readFileSync as E,writeFileSync as O,existsSync as
|
|
2
|
-
`),J(`${t}.tmp`,t)},attach:(r,d,l)=>{const e=r.comments.find(
|
|
3
|
-
`),r.nextId=I(r),r.comments=i,{moving:e,keeping:i,archiveFile:m,images:c}}}}export{Q as CATEGORIES,X as STATE_LABEL,L as add,U as categoryOf,H as createStore,G as edit,I as nextId,R as patch,C as reply,
|
|
1
|
+
import{readFileSync as E,writeFileSync as O,existsSync as v,mkdirSync as b,renameSync as J,rmSync as B}from"node:fs";import{dirname as M,join as p}from"node:path";import{categoryOf as T}from"./categories.mjs";import{CATEGORIES as Q,categoryOf as U}from"./categories.mjs";import{stateOf as W,STATE_LABEL as X}from"./status.mjs";const x=n=>{const t=String(n??"").split(/[/\\]/).pop();return/^[\w-][\w.-]*\.(png|jpe?g|webp|gif|webm)$/i.test(t)?t:""},I=n=>Math.max(n.nextId||0,n.comments.reduce((t,s)=>Math.max(t,s.id),0)+1);function L(n,{route:t,selector:s,rx:a,ry:o,text:u,label:g,viewport:y,breakpoint:N,browser:$,device:D,tag:A,classes:r,nth:d,category:l,suggest:e,assignee:i,ctx:m,logs:c,host:S}){const h={id:I(n),route:t,selector:s,rx:a,ry:o,text:u,tag:A||"",classes:r||[],nth:Number.isInteger(d)?d:null,label:g||"",ctx:m||"",viewport:y||null,breakpoint:N||"desktop",browser:$||"",device:D||"",category:T(l).id,status:"open",note:"",replies:[],doneBy:null,images:[],...e?{suggest:e}:{},...i?{assignee:i}:{},...S?{host:S}:{},...c?.length?{logs:c}:{},createdAt:new Date().toISOString(),updatedAt:null};return n.comments.push(h),n.nextId=h.id+1,h}function C(n,t,s,a,o){const u=n.comments.find(g=>g.id===Number(t));return u?((u.replies||=[]).push({author:s,text:a,at:new Date().toISOString(),...o&&o!=="claude"?{agent:o}:{}}),u.updatedAt=new Date().toISOString(),u):null}function G(n,t,s,a){const o=n.comments.find(g=>g.id===Number(t)),u=s?o?.replies?.find(g=>g.at===s&&g.author==="you"):o;return u?(u.text=a,o.updatedAt=new Date().toISOString(),o):null}function R(n,t,s){const a=n.comments.find(o=>o.id===Number(t));return a?(Object.assign(a,s,{updatedAt:new Date().toISOString()}),a):null}function H(n){const t=p(n,"feedback","comments.json"),s=p(n,"feedback","images"),a=p(n,"feedback","archive"),o=p(a,"images"),u=r=>`feedback/images/${r}`;return{root:n,file:t,imageDir:s,archiveDir:a,archiveImageDir:o,load:()=>v(t)?JSON.parse(E(t,"utf8")):{comments:[]},save:r=>{b(M(t),{recursive:!0}),O(`${t}.tmp`,JSON.stringify(r,null,2)+`
|
|
2
|
+
`),J(`${t}.tmp`,t)},attach:(r,d,l)=>{const e=r.comments.find(h=>h.id===Number(d)),i=/^data:(?:image|video)\/(png|jpeg|webp|gif|webm);base64,(.+)$/s.exec(l||"");if(!e||!i)return null;b(s,{recursive:!0});const m=i[1]==="jpeg"?"jpg":i[1];let c=Date.now();for(;v(p(s,`${e.id}-${c}.${m}`));)c++;const S=`${e.id}-${c}.${m}`;return O(p(s,S),Buffer.from(i[2],"base64")),(e.images||=[]).push(u(S)),e.updatedAt=new Date().toISOString(),u(S)},detach:(r,d,l)=>{const e=r.comments.find(c=>c.id===Number(d)),i=x(l),m=i&&e?.images?.find(c=>x(c)===i);return m?(B(p(s,i),{force:!0}),e.images=e.images.filter(c=>c!==m),e.updatedAt=new Date().toISOString(),!0):!1},remove:(r,d)=>{const l=r.comments.find(e=>e.id===Number(d));if(!l)return!1;for(const e of l.images||[])x(e)&&B(p(s,x(e)),{force:!0});return r.comments=r.comments.filter(e=>e.id!==Number(d)),!0},add:L,patch:R,reply:C,edit:G,nextId:I,prune:(r,{dry:d=!1,date:l=new Date().toISOString().slice(0,10)}={})=>{const e=r.comments.filter(f=>f.status==="done"),i=r.comments.filter(f=>f.status!=="done"),m=p(a,`comments-${l}.json`);if(d||!e.length)return{moving:e,keeping:i,archiveFile:m,images:e.flatMap(f=>f.images||[])};b(o,{recursive:!0});const c=[];for(const f of e)f.images=(f.images||[]).map(k=>{const w=x(k);if(!w)return k;const j=p(s,w),F=p(o,w);return v(j)&&J(j,F),c.push(w),`feedback/archive/images/${w}`});const S=v(m)?JSON.parse(E(m,"utf8")).comments||[]:[],h=new Set(S.map(f=>f.id));return O(m,JSON.stringify({comments:[...S,...e.filter(f=>!h.has(f.id))]},null,2)+`
|
|
3
|
+
`),r.nextId=I(r),r.comments=i,{moving:e,keeping:i,archiveFile:m,images:c}}}}export{Q as CATEGORIES,X as STATE_LABEL,L as add,U as categoryOf,H as createStore,G as edit,I as nextId,R as patch,C as reply,x as shotName,W as stateOf};
|
package/store.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface Comment {
|
|
|
15
15
|
doneBy: string | null; doneAgent?: string | null;
|
|
16
16
|
images: string[]; suggest?: { from: string; to: string }; assignee?: string;
|
|
17
17
|
working?: Working | null; logs?: LogLine[];
|
|
18
|
+
/** the copy of the site it was written on — live, staging, localhost — as host[:port] */
|
|
19
|
+
host?: string;
|
|
18
20
|
by?: string; byName?: string; createdAt: string; updatedAt: string | null;
|
|
19
21
|
}
|
|
20
22
|
export interface Db { comments: Comment[]; nextId?: number }
|