@zhama/tego-plugin-sdk 1.37.1 → 1.40.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.
Files changed (71) hide show
  1. package/README.md +48 -9
  2. package/agent/RULES.md +34 -11
  3. package/agent/guides/agent-templates.md +51 -0
  4. package/agent/guides/backend.md +8 -0
  5. package/agent/guides/host-api-versions.md +3 -0
  6. package/agent/reference/plugin-sdk-api.md +1 -0
  7. package/agent/rules.json +500 -0
  8. package/agent/skills/add-agent-template/SKILL.md +19 -0
  9. package/agent/skills/add-claw-tool/SKILL.md +21 -0
  10. package/agent/skills/add-i18n-strings/SKILL.md +19 -0
  11. package/agent/skills/add-migration/SKILL.md +22 -0
  12. package/agent/skills/add-route-with-access/SKILL.md +30 -0
  13. package/agent/skills/add-screen/SKILL.md +27 -0
  14. package/bin/tego-plugin.mjs +7 -3
  15. package/dist/access.d.cts +1 -0
  16. package/dist/access.d.ts +1 -0
  17. package/dist/{chunk-EIVA5BR6.js → chunk-2KXEYXM2.js} +1 -1
  18. package/dist/{chunk-I5Z3ECC4.js → chunk-44LPR67H.js} +1 -1
  19. package/dist/chunk-66LDWN4T.cjs +1 -0
  20. package/dist/{chunk-HZ4YWRYX.cjs → chunk-6EKQMOYT.cjs} +2 -2
  21. package/dist/{chunk-SKNPNPJP.cjs → chunk-H6GXDFLP.cjs} +1 -1
  22. package/dist/{chunk-6HNG5OZI.cjs → chunk-IOZFHJ4F.cjs} +1 -1
  23. package/dist/chunk-JV2G2Z33.js +1 -0
  24. package/dist/{chunk-4LKRXDYY.cjs → chunk-QQOUNXLC.cjs} +8 -8
  25. package/dist/{chunk-FCYAAOHC.js → chunk-UZBWS5AR.js} +2 -2
  26. package/dist/{chunk-56QOZQTY.js → chunk-XOXUS4VI.js} +1 -1
  27. package/dist/decision-card-IF2c4wvM.d.cts +161 -0
  28. package/dist/decision-card-IF2c4wvM.d.ts +161 -0
  29. package/dist/dev.cjs +8 -3
  30. package/dist/dev.d.cts +33 -2
  31. package/dist/dev.d.ts +33 -2
  32. package/dist/dev.js +13 -8
  33. package/dist/doctor.cjs +15 -8
  34. package/dist/doctor.d.cts +99 -2
  35. package/dist/doctor.d.ts +99 -2
  36. package/dist/doctor.js +15 -8
  37. package/dist/eslint.cjs +1 -0
  38. package/dist/eslint.d.cts +29 -0
  39. package/dist/eslint.d.ts +29 -0
  40. package/dist/eslint.js +1 -0
  41. package/dist/hono.d.cts +1 -0
  42. package/dist/hono.d.ts +1 -0
  43. package/dist/index.cjs +1 -1
  44. package/dist/index.d.cts +14 -1
  45. package/dist/index.d.ts +14 -1
  46. package/dist/index.js +1 -1
  47. package/dist/manifest.cjs +1 -1
  48. package/dist/manifest.js +1 -1
  49. package/dist/pack.cjs +1 -1
  50. package/dist/pack.d.cts +5 -0
  51. package/dist/pack.d.ts +5 -0
  52. package/dist/pack.js +1 -1
  53. package/dist/publish.cjs +1 -1
  54. package/dist/publish.js +1 -1
  55. package/dist/scaffold.cjs +53 -8
  56. package/dist/scaffold.js +59 -14
  57. package/dist/server-runtime.cjs +1 -1
  58. package/dist/server-runtime.d.cts +1 -0
  59. package/dist/server-runtime.d.ts +1 -0
  60. package/dist/server-runtime.js +1 -1
  61. package/dist/server.cjs +1 -1
  62. package/dist/server.d.cts +25 -0
  63. package/dist/server.d.ts +25 -0
  64. package/dist/server.js +1 -1
  65. package/dist/testing.d.cts +1 -0
  66. package/dist/testing.d.ts +1 -0
  67. package/dist/vite.cjs +1 -1
  68. package/dist/vite.js +1 -1
  69. package/package.json +8 -1
  70. /package/dist/{chunk-QKSPPMVN.js → chunk-CDJQ7OPA.js} +0 -0
  71. /package/dist/{chunk-BN6TGHSB.cjs → chunk-SR5V7ZC2.cjs} +0 -0
package/dist/dev.js CHANGED
@@ -1,9 +1,13 @@
1
- var Me=Object.defineProperty;var s=(e,t)=>Me(e,"name",{value:t,configurable:!0});import{signPluginContext as Fe}from"./chunk-WLTASJF6.js";import{readManifest as Ke,readPluginLocales as Je,buildPluginBackend as We}from"./chunk-FCYAAOHC.js";import"./chunk-I5Z3ECC4.js";import{applyConfigDefaults as ge}from"./chunk-DEGQDJF6.js";import"./chunk-EIVA5BR6.js";import"./chunk-2NTAWYCG.js";import{resolvePluginParts as ze}from"./chunk-IVJECPGG.js";import"./chunk-INCA3GFW.js";import"./chunk-56QOZQTY.js";import{PLUGIN_CONTEXT_HEADERS as L}from"./chunk-QKSPPMVN.js";import"./chunk-VMCGDDGP.js";import"./chunk-SNUCO2MK.js";import{randomUUID as qe,randomBytes as fe,createHash as Xe}from"crypto";import{existsSync as K,realpathSync as Ye,watch as Qe}from"fs";import{readdir as Ze,readFile as J,mkdir as ee,copyFile as pe,rm as te,writeFile as ne,stat as et}from"fs/promises";import{createServer as tt,request as nt}from"http";import{createRequire as rt}from"module";import{join as w,resolve as I,isAbsolute as at,sep as he,dirname as C,relative as me}from"path";import{spawn as ot}from"child_process";import{createServer as st}from"net";var re="x-tego-dev-user-id",ae="x-tego-dev-roles",O="dev-tenant",ve="00000000-0000-4000-8000-000000000001";function we(e){const t=s(r=>(Array.isArray(r)?r[0]:r)?.trim()??"","single"),n=t(e[re])||ve;let a=[];try{const r=JSON.parse(t(e[ae])||"[]");Array.isArray(r)&&(a=r.filter(i=>typeof i=="string"))}catch{}return{userId:n,roles:a}}s(we,"devCallerOf");function ye(e,t,n,a=Date.now()){const r={};for(const[o,l]of Object.entries(e)){const u=o.toLowerCase();l===void 0||u.startsWith("x-tego-")||u==="cookie"||u==="authorization"||u==="host"||u==="connection"||(r[u]=Array.isArray(l)?l.join(", "):l)}const i=JSON.stringify(t.roles),d=String(a);return r[L.userId]=t.userId,r[L.tenantId]=O,r[L.roles]=i,r[L.issuedAt]=d,r[L.signature]=Fe({userId:t.userId,tenantId:O,rolesHeader:i,avatarId:"",issuedAt:d},n),r}s(ye,"gatewayHeaders");var it="tego_dev_migrations";function be(e){return`tego_dev_${e.toLowerCase().replace(/[^a-z0-9_]/g,"_")}`.slice(0,63)}s(be,"devSchemaName");function Se(e,t){const n=new URL(e);n.searchParams.delete("options");const a=n.search.replace(/^\?/,"");return n.search=`?${a?`${a}&`:""}options=${encodeURIComponent(`-c search_path=${t}`)}`,n.toString()}s(Se,"devDatabaseUrl");async function ct(e){let t;try{t=await import("pg")}catch(r){throw new Error(`The local backend needs the "pg" driver; add it to the plugin's dependencies.`,{cause:r})}const n=t.default?.Client??t.Client;if(!n)throw new Error('The "pg" module has no Client constructor.');const a=new n({connectionString:e});try{await a.connect()}catch(r){throw new Error(`Cannot connect to DATABASE_URL (${oe(e)}): ${r instanceof Error?r.message:String(r)}`,{cause:r})}return a}s(ct,"connect");function oe(e){try{const t=new URL(e);return t.password&&(t.password="***"),t.toString()}catch{return"<unparseable DATABASE_URL>"}}s(oe,"redactUrl");var Ee=s(e=>`"${e.replace(/"/g,'""')}"`,"quote");async function Te(e){const t=await Ze(e.migrationsDir).then(i=>i.filter(d=>d.endsWith(".sql")).sort(),i=>{if(i.code==="ENOENT")return[];throw i}),n=await ct(e.databaseUrl),a=Ee(e.schema),r=`${a}.${Ee(it)}`;try{await n.query(`CREATE SCHEMA IF NOT EXISTS ${a}`),await n.query(`CREATE TABLE IF NOT EXISTS ${r} (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`);const i=new Set((await n.query(`SELECT name FROM ${r}`)).rows.map(o=>String(o.name))),d=[];for(const o of t){if(i.has(o))continue;const l=await J(w(e.migrationsDir,o),"utf8");await n.query("BEGIN");try{await n.query(`SET LOCAL search_path TO ${a}`),await n.query("SELECT set_config('app.tenant_id', $1, true)",[e.tenantId]),await n.query(l),await n.query(`INSERT INTO ${r} (name) VALUES ($1)`,[o]),await n.query("COMMIT")}catch(u){throw await n.query("ROLLBACK").catch(()=>{}),new Error(`Migration ${o} failed: ${u instanceof Error?u.message:String(u)}`,{cause:u})}d.push(o)}return d}finally{await n.end().catch(()=>{})}}s(Te,"applyDevMigrations");var lt=250,dt=5e3;function ut(){return new Promise((e,t)=>{const n=st();n.once("error",t),n.listen(0,"127.0.0.1",()=>{const a=n.address(),r=typeof a=="object"&&a?a.port:0;n.close(()=>e(r))})})}s(ut,"freePort");function ke(e){return!e||e.exitCode!==null||e.signalCode!==null?Promise.resolve():new Promise(t=>{const n=setTimeout(()=>{e.kill("SIGKILL"),t()},dt);e.once("exit",()=>{clearTimeout(n),t()}),e.kill("SIGTERM")})}s(ke,"stopChild");async function gt(e){const{part:t,log:n}=e;if(t.runtime==="python")throw new Error("The local preview runs TypeScript backends only; a python backend runs through the platform.");const a=await ut(),r=be(e.pluginId),i=I(e.root,t.migrationsDirPath),d=w(e.outDir,t.entryPath),o={...process.env,NODE_ENV:"development",TEGO_PLUGIN_ID:e.pluginId,TEGO_PLUGIN_VERSION:e.version,TEGO_PLUGIN_INSTALL_ID:"dev-install",TEGO_TENANT_ID:O,TEGO_PLUGIN_PORT:String(a),TEGO_PLUGIN_HEALTH_PATH:t.healthPath,TEGO_PLUGIN_HOST_API_URL:e.hostApiUrl,TEGO_PLUGIN_HOST_API_TOKEN:e.hostApiToken,TEGO_PLUGIN_GATEWAY_KEY:e.gatewayKey,DATABASE_URL:Se(e.databaseUrl,r)};let l=null,u=!1;async function f(){await te(e.outDir,{recursive:!0,force:!0}),await We(t,{root:e.root,outDir:e.outDir});const h=await Te({databaseUrl:e.databaseUrl,schema:r,migrationsDir:i,tenantId:O});for(const E of h)n(`[backend] applied migration ${E}`)}s(f,"build");function y(){const h=ot(process.execPath,[d],{env:o,stdio:["ignore","pipe","pipe"]});l=h,h.stdout?.on("data",E=>process.stdout.write(E)),h.stderr?.on("data",E=>process.stderr.write(E)),h.on("exit",E=>{l===h&&!u&&(l=null,n(`[backend] exited (code ${E??"null"}); it starts again on the next save`))})}s(y,"start"),await f(),y(),n(`[backend] ${e.pluginId} on 127.0.0.1:${a}, schema ${r} in ${oe(e.databaseUrl)}`);let S=null,v=null,R=!1;const H=s(async()=>{if(v){R=!0;return}v=(async()=>{try{await f();const h=l;l=null,await ke(h),u||(y(),n("[backend] rebuilt and restarted"))}catch(h){n(`[backend] rebuild failed, the previous build keeps running: ${h instanceof Error?h.message:String(h)}`)}})(),await v,v=null,R&&!u&&(R=!1,await H())},"rebuild"),V=[];for(const h of[t.dir,"shared"]){const E=I(e.root,h);K(E)&&V.push(Qe(E,{recursive:!0},(_,x)=>{x&&/(^|[\\/])(node_modules|dist)([\\/]|$)/.test(String(x))||(S&&clearTimeout(S),S=setTimeout(()=>void H(),lt))}))}return{port:a,schema:r,async close(){u=!0,S&&clearTimeout(S);for(const h of V)h.close();await v,await ke(l)}}}s(gt,"startDevBackend");var se="/__tego/host",ie="/__tego/files",ft=1024*1024;async function ce(e,t=1/0){const n=[];let a=0;for await(const r of e){if(a+=r.length,a>t)throw Object.assign(new Error("body too large"),{status:413});n.push(r)}return Buffer.concat(n)}s(ce,"readBody");function p(e,t,n){if(n===void 0){e.writeHead(t),e.end();return}e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(n))}s(p,"send");function le(e,t){const n=I(e),a=I(n,t);if(!t||at(t)||!a.startsWith(n+he))throw Object.assign(new Error(`invalid storage key ${JSON.stringify(t)}`),{status:400});return a}s(le,"storagePath");var U=s(e=>`${e}.tego-meta.json`,"metaOf");async function B(e){const t=await et(e).catch(()=>null);if(!t?.isFile())return null;const n=await J(U(e),"utf8").then(a=>JSON.parse(a),()=>({}));return{size:t.size,contentType:n.contentType??null,versionTag:Xe("sha1").update(`${t.size}:${t.mtimeMs}`).digest("hex")}}s(B,"head");async function xe(e,t,n){await ee(C(e),{recursive:!0}),await ne(e,t),await ne(U(e),JSON.stringify({contentType:n??null}))}s(xe,"put");async function Ie(e,t,n){const a=new URL(e.url??"/","http://preview.local");try{if(a.pathname.startsWith(ie)){const o=le(n.storageDir,a.searchParams.get("key")??""),l=await B(o);if(!l)return p(t,404);t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(await J(o));return}if(e.headers.authorization!==`Bearer ${n.token}`)return p(t,401,{error:"bad host API token"});const r=a.pathname.slice(se.length)||"/",i=s(async()=>JSON.parse((await ce(e,ft)).toString("utf8")||"{}"),"json"),d=s(o=>le(n.storageDir,String(o??"")),"keyFile");switch(r){case"/config":return p(t,200,{data:{values:n.config()}});case"/log":{const{level:o,message:l,fields:u}=await i();return n.log(`[backend ${String(o??"info")}] ${String(l??"")}${u?` ${JSON.stringify(u)}`:""}`),p(t,200,{data:{}})}case"/events":{const{name:o,payload:l}=await i();return n.log(`[event] ${String(o)} ${JSON.stringify(l??null)}`),p(t,200,{data:{}})}case"/notifications":return n.log(`[notification] ${JSON.stringify(await i())}`),p(t,200,{data:{id:qe()}});case"/directory/identity":{const{userId:o}=await i(),l=n.users().get(String(o));return p(t,200,{data:{active:!!l,roleNames:l??[],memberships:[],checkedAt:new Date().toISOString()}})}case"/directory/list":{const{kind:o,search:l,ids:u}=await i(),f=typeof l=="string"?l.toLowerCase():"",y=Array.isArray(u)?new Set(u.map(String)):void 0,S=o==="users"?[...n.users().keys()].filter(v=>(!y||y.has(v))&&v.toLowerCase().includes(f)).map(v=>({id:v,displayName:v,active:!0})):[];return p(t,200,{data:{items:S,hasMore:!1}})}case"/directory/match":{const{userId:o,principals:l}=await i(),u=Array.isArray(l)?l:[];return p(t,200,{data:{active:n.users().has(String(o)),matched:u.flatMap((f,y)=>f.kind==="user"&&f.id===o?[y]:[]),checkedAt:new Date().toISOString()}})}case"/directory/validate":{const{principals:o}=await i(),l=Array.isArray(o)?o:[],u=n.users();return p(t,200,{data:{invalid:l.flatMap((f,y)=>f.kind==="user"&&u.has(String(f.id))?[]:[y])}})}case"/storage/get":case"/storage/stream":{const o=d(a.searchParams.get("key"));if(e.method==="POST"&&r==="/storage/stream")return await xe(o,await ce(e),e.headers["content-type"]),p(t,200,{data:{}});const l=await B(o);if(!l)return p(t,404);let u=await J(o);const f=Number(a.searchParams.get("bytes"));Number.isFinite(f)&&f>0&&(u=u.subarray(0,f)),t.writeHead(200,{"content-type":l.contentType??"application/octet-stream"}),t.end(u);return}case"/storage/put":return await xe(d(a.searchParams.get("key")),await ce(e),e.headers["content-type"]),p(t,200,{data:{}});case"/storage/head":{const o=await B(d((await i()).key));return o?p(t,200,{data:o}):p(t,404)}case"/storage/delete":{const o=d((await i()).key);return await te(o,{force:!0}),await te(U(o),{force:!0}),p(t,200,{data:{}})}case"/storage/copy":{const{sourceKey:o,destinationKey:l}=await i(),u=d(o),f=d(l);return await B(u)?(await ee(C(f),{recursive:!0}),await pe(u,f),await pe(U(u),U(f)).catch(()=>{}),p(t,200,{data:await B(f)})):p(t,404)}case"/storage/signed-url":{const{key:o}=await i();d(o);const l=`${ie}?key=${encodeURIComponent(String(o))}`;return p(t,200,{data:{url:`${n.origin()}${l}`,path:l}})}default:return n.log(`[host] ${r} is not available in local preview`),p(t,501,{error:{code:"not_available_in_preview",message:`${r} is not available in local preview`}})}}catch(r){const i=r.status??500;p(t,i,{error:{message:r instanceof Error?r.message:String(r)}})}}s(Ie,"handleFakeHost");var Pe="@tego/plugin-sdk",pt=JSON.stringify(Pe),ht=JSON.stringify(`${Pe}/frontend`);function _e(e){return[{path:"index.html",contents:vt(e)},{path:"main.tsx",contents:bt(e)},{path:"shell.css",contents:wt},{path:"plugin-surface.tsx",contents:yt}]}s(_e,"shellFiles");var mt=s(e=>e.replace(/[&<>"]/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[t]),"escapeHtml");function vt({displayName:e}){return`<!doctype html>
1
+ var at=Object.defineProperty;var s=(e,t)=>at(e,"name",{value:t,configurable:!0});import{signPluginContext as ot}from"./chunk-WLTASJF6.js";import{readManifest as st,readPluginLocales as it,buildPluginBackend as ct}from"./chunk-UZBWS5AR.js";import"./chunk-44LPR67H.js";import{applyConfigDefaults as ve}from"./chunk-DEGQDJF6.js";import"./chunk-2KXEYXM2.js";import"./chunk-2NTAWYCG.js";import{resolvePluginParts as lt}from"./chunk-IVJECPGG.js";import"./chunk-INCA3GFW.js";import"./chunk-XOXUS4VI.js";import{PLUGIN_CONTEXT_HEADERS as M}from"./chunk-CDJQ7OPA.js";import"./chunk-JV2G2Z33.js";import"./chunk-VMCGDDGP.js";import"./chunk-SNUCO2MK.js";import{randomUUID as ut,randomBytes as we,createHash as dt}from"crypto";import{existsSync as q,watch as ye,realpathSync as ft}from"fs";import{readdir as be,readFile as X,mkdir as oe,copyFile as Ee,rm as Y,writeFile as Q,stat as Se,mkdtemp as gt}from"fs/promises";import{createServer as pt,request as ht}from"http";import{createRequire as mt}from"module";import{join as m,resolve as P,isAbsolute as vt,sep as Te,dirname as j,relative as ke}from"path";import{spawn as wt}from"child_process";import{createServer as yt}from"net";import{tmpdir as bt}from"os";var se="x-tego-dev-user-id",ie="x-tego-dev-roles",V="dev-tenant",Pe="00000000-0000-4000-8000-000000000001";function xe(e){const t=s(a=>(Array.isArray(a)?a[0]:a)?.trim()??"","single"),n=t(e[se])||Pe;let r=[];try{const a=JSON.parse(t(e[ie])||"[]");Array.isArray(a)&&(r=a.filter(c=>typeof c=="string"))}catch{}return{userId:n,roles:r}}s(xe,"devCallerOf");function Ie(e,t,n,r=Date.now()){const a={};for(const[o,i]of Object.entries(e)){const d=o.toLowerCase();i===void 0||d.startsWith("x-tego-")||d==="cookie"||d==="authorization"||d==="host"||d==="connection"||(a[d]=Array.isArray(i)?i.join(", "):i)}const c=JSON.stringify(t.roles),u=String(r);return a[M.userId]=t.userId,a[M.tenantId]=V,a[M.roles]=c,a[M.issuedAt]=u,a[M.signature]=ot({userId:t.userId,tenantId:V,rolesHeader:c,avatarId:"",issuedAt:u},n),a}s(Ie,"gatewayHeaders");var Et="tego_dev_migrations";function _e(e){return`tego_dev_${e.toLowerCase().replace(/[^a-z0-9_]/g,"_")}`.slice(0,63)}s(_e,"devSchemaName");function $e(e,t){const n=new URL(e);n.searchParams.delete("options");const r=n.search.replace(/^\?/,"");return n.search=`?${r?`${r}&`:""}options=${encodeURIComponent(`-c search_path=${t}`)}`,n.toString()}s($e,"devDatabaseUrl");async function Ae(e){let t;try{t=await import("pg")}catch(a){throw new Error(`The local backend needs the "pg" driver; add it to the plugin's dependencies.`,{cause:a})}const n=t.default?.Client??t.Client;if(!n)throw new Error('The "pg" module has no Client constructor.');const r=new n({connectionString:e});try{await r.connect()}catch(a){throw new Error(`Cannot connect to DATABASE_URL (${ce(e)}): ${De(a)}`,{cause:a})}return r}s(Ae,"connect");function De(e){const t=e?.code,n=e instanceof Error?e.message:String(e);switch(t){case"ECONNREFUSED":return`${n} \u2014 is PostgreSQL running at that host and port?`;case"ENOTFOUND":return`${n} \u2014 the host name does not resolve.`;case"28P01":case"28000":return`${n} \u2014 check the user name and password.`;case"3D000":return`${n} \u2014 create the database first (createdb <name>).`;default:return n}}s(De,"explainDatabaseError");async function Ne(e){const t=await Ae(e);try{await t.query("select 1")}finally{await t.end().catch(()=>{})}}s(Ne,"checkDevDatabase");function ce(e){try{const t=new URL(e);return t.password&&(t.password="***"),t.toString()}catch{return"<unparseable DATABASE_URL>"}}s(ce,"redactUrl");var Le=s(e=>`"${e.replace(/"/g,'""')}"`,"quote");async function Re(e){const t=await be(e.migrationsDir).then(c=>c.filter(u=>u.endsWith(".sql")).sort(),c=>{if(c.code==="ENOENT")return[];throw c}),n=await Ae(e.databaseUrl),r=Le(e.schema),a=`${r}.${Le(Et)}`;try{await n.query(`CREATE SCHEMA IF NOT EXISTS ${r}`),await n.query(`CREATE TABLE IF NOT EXISTS ${a} (name text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`);const c=new Set((await n.query(`SELECT name FROM ${a}`)).rows.map(o=>String(o.name))),u=[];for(const o of t){if(c.has(o))continue;const i=await X(m(e.migrationsDir,o),"utf8");await n.query("BEGIN");try{await n.query(`SET LOCAL search_path TO ${r}`),await n.query("SELECT set_config('app.tenant_id', $1, true)",[e.tenantId]),await n.query(i),await n.query(`INSERT INTO ${a} (name) VALUES ($1)`,[o]),await n.query("COMMIT")}catch(d){throw await n.query("ROLLBACK").catch(()=>{}),new Error(`Migration ${o} failed: ${d instanceof Error?d.message:String(d)}`,{cause:d})}u.push(o)}return u}finally{await n.end().catch(()=>{})}}s(Re,"applyDevMigrations");var le=1e3,St=800,Oe=new Set(["node_modules","dist",".git",".tego-dev","build"]);async function Ce(e){const t=new Map,n=s(async r=>{let a;try{a=await be(r,{withFileTypes:!0})}catch{return}for(const c of a){const u=m(r,c.name);if(c.isDirectory())Oe.has(c.name)||await n(u);else if(c.isFile())try{const o=await Se(u);t.set(u,`${o.mtimeMs}:${o.size}`)}catch{}}},"walk");return await n(e),t}s(Ce,"treeSnapshot");function Tt(e,t){if(e.size!==t.size)return!1;for(const[n,r]of e)if(t.get(n)!==r)return!1;return!0}s(Tt,"sameSnapshot");function kt(e,t,n){let r=!1,a,c;const u=s(async()=>{let i=await Ce(e);const d=s(async()=>{if(r)return;const p=await Ce(e);!r&&!Tt(i,p)&&(i=p,t()),r||(c=setTimeout(()=>void d(),le))},"tick");r||(c=setTimeout(()=>void d(),le))},"poll"),o=s(i=>{a?.close(),a=void 0,!r&&(n.log(`[watch] ${n.label}: file events unavailable (${i}); checking for changes every ${le/1e3}s`),u())},"fallBack");if(n.poll)u();else try{a=ye(e,{recursive:!0},(i,d)=>{String(d??"").split(/[\\/]/).some(y=>Oe.has(y))||t()}),a.on("error",i=>o(i.message))}catch(i){o(i instanceof Error?i.message:String(i))}return{close(){r=!0,a?.close(),c&&clearTimeout(c)}}}s(kt,"watchTree");async function Pt(){const e=await gt(m(bt(),"tego-dev-watch-"));try{return await new Promise(t=>{let n,r=!1;const a=s(u=>{clearTimeout(c),n?.close(),t(u)},"finish"),c=setTimeout(()=>a(r),St);try{n=ye(e,{recursive:!0},()=>r=!0),n.on("error",()=>a(!1))}catch{a(!1);return}Q(m(e,"probe"),"x").catch(()=>a(!1))})}finally{await Y(e,{recursive:!0,force:!0})}}s(Pt,"fileEventsAvailable");var xt=250,It=5e3,Ue=3e4,_t=200,$t=40,At=9229;function Dt(){return new Promise((e,t)=>{const n=yt();n.once("error",t),n.listen(0,"127.0.0.1",()=>{const r=n.address(),a=typeof r=="object"&&r?r.port:0;n.close(()=>e(a))})})}s(Dt,"freePort");function Be(e,t){return["--enable-source-maps",...t?[`--inspect=127.0.0.1:${t}`]:[],e]}s(Be,"backendNodeArgs");function He(e,t){let n="";return{push(r){const a=(n+r).split(/\r?\n/);n=a.pop()??"";for(const c of a)t(`${e}${c}`)},flush(){n&&t(`${e}${n}`),n=""}}}s(He,"linePrefixer");function ue(e){return!e||e.exitCode!==null||e.signalCode!==null?Promise.resolve():new Promise(t=>{const n=setTimeout(()=>{e.kill("SIGKILL"),t()},It);e.once("exit",()=>{clearTimeout(n),t()}),e.kill("SIGTERM")})}s(ue,"stopChild");async function Nt(e){const{part:t,log:n}=e;if(t.runtime==="python")throw new Error("The local preview runs TypeScript backends only; a python backend runs through the platform.");const r=await Dt(),a=_e(e.pluginId),c=P(e.root,t.migrationsDirPath),u=m(e.outDir,t.entryPath),o={...process.env,NODE_ENV:"development",TEGO_PLUGIN_ID:e.pluginId,TEGO_PLUGIN_VERSION:e.version,TEGO_PLUGIN_INSTALL_ID:"dev-install",TEGO_TENANT_ID:V,TEGO_PLUGIN_PORT:String(r),TEGO_PLUGIN_HEALTH_PATH:t.healthPath,TEGO_PLUGIN_HOST_API_URL:e.hostApiUrl,TEGO_PLUGIN_HOST_API_TOKEN:e.hostApiToken,TEGO_PLUGIN_GATEWAY_KEY:e.gatewayKey,DATABASE_URL:$e(e.databaseUrl,a)};let i=null,d=!1;const p=[],y=s(f=>{p.push(f),p.length>$t&&p.shift(),n(f)},"remember");async function I(){await Y(e.outDir,{recursive:!0,force:!0}),await ct(t,{root:e.root,outDir:e.outDir,sourcemap:!0});const f=await Re({databaseUrl:e.databaseUrl,schema:a,migrationsDir:c,tenantId:V});for(const w of f)n(`[backend] applied migration ${w}`)}s(I,"build");function E(){const f=wt(process.execPath,Be(u,e.inspect),{env:o,stdio:["ignore","pipe","pipe"]});i=f;for(const w of[f.stdout,f.stderr]){const L=He("[backend] ",y);w?.setEncoding("utf8"),w?.on("data",S=>L.push(S)),w?.on("end",()=>L.flush())}return f.on("exit",w=>{i===f&&!d&&(i=null,n(`[backend] exited (code ${w??"null"}); fix the code and save to restart`))}),f}s(E,"start");async function D(f){const w=`http://127.0.0.1:${r}${t.healthPath}`,L=Date.now()+Ue;for(;Date.now()<L;){if(f.exitCode!==null||f.signalCode!==null)throw new Error(`The backend exited while starting (code ${f.exitCode??f.signalCode}):
2
+ ${p.join(`
3
+ `)}`);try{if((await fetch(w)).ok)return}catch{}await new Promise(S=>setTimeout(S,_t))}throw new Error(`The backend did not answer ${t.healthPath} within ${Ue/1e3}s:
4
+ ${p.join(`
5
+ `)}`)}s(D,"healthy"),await I();const N=E();try{await D(N)}catch(f){throw d=!0,await ue(i),f}n(`[backend] ${e.pluginId} on 127.0.0.1:${r}, schema ${a} in ${ce(e.databaseUrl)}`),e.inspect&&n(`[backend] debugger: attach to 127.0.0.1:${e.inspect} (chrome://inspect, or your editor's Node attach)`);let _=null,$=null,U=!1;const B=s(async()=>{if($){U=!0;return}$=(async()=>{try{await I();const f=i;i=null,await ue(f),d||(E(),n("[backend] rebuilt and restarted"))}catch(f){n(`[backend] rebuild failed, the previous build keeps running: ${f instanceof Error?f.message:String(f)}`)}})(),await $,$=null,U&&!d&&(U=!1,await B())},"rebuild"),K=[];for(const f of[t.dir,"shared"]){const w=P(e.root,f);q(w)&&K.push(kt(w,()=>{_&&clearTimeout(_),_=setTimeout(()=>void B(),xt)},{poll:e.poll===!0,log:n,label:f}))}return{port:r,schema:a,async close(){d=!0,_&&clearTimeout(_);for(const f of K)f.close();await $,await ue(i)}}}s(Nt,"startDevBackend");var de="/__tego/host",fe="/__tego/files",Lt=1024*1024;async function ge(e,t=1/0){const n=[];let r=0;for await(const a of e){if(r+=a.length,r>t)throw Object.assign(new Error("body too large"),{status:413});n.push(a)}return Buffer.concat(n)}s(ge,"readBody");function h(e,t,n){if(n===void 0){e.writeHead(t),e.end();return}e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(n))}s(h,"send");function pe(e,t){const n=P(e),r=P(n,t);if(!t||vt(t)||!r.startsWith(n+Te))throw Object.assign(new Error(`invalid storage key ${JSON.stringify(t)}`),{status:400});return r}s(pe,"storagePath");var F=s(e=>`${e}.tego-meta.json`,"metaOf");async function G(e){const t=await Se(e).catch(()=>null);if(!t?.isFile())return null;const n=await X(F(e),"utf8").then(r=>JSON.parse(r),()=>({}));return{size:t.size,contentType:n.contentType??null,versionTag:dt("sha1").update(`${t.size}:${t.mtimeMs}`).digest("hex")}}s(G,"head");async function Me(e,t,n){await oe(j(e),{recursive:!0}),await Q(e,t),await Q(F(e),JSON.stringify({contentType:n??null}))}s(Me,"put");async function je(e,t,n){const r=new URL(e.url??"/","http://preview.local");try{if(r.pathname.startsWith(fe)){const o=pe(n.storageDir,r.searchParams.get("key")??""),i=await G(o);if(!i)return h(t,404);t.writeHead(200,{"content-type":i.contentType??"application/octet-stream"}),t.end(await X(o));return}if(e.headers.authorization!==`Bearer ${n.token}`)return h(t,401,{error:"bad host API token"});const a=r.pathname.slice(de.length)||"/",c=s(async()=>JSON.parse((await ge(e,Lt)).toString("utf8")||"{}"),"json"),u=s(o=>pe(n.storageDir,String(o??"")),"keyFile");switch(a){case"/config":return h(t,200,{data:{values:n.config()}});case"/log":{const{level:o,message:i,fields:d}=await c();return n.log(`[backend ${String(o??"info")}] ${String(i??"")}${d?` ${JSON.stringify(d)}`:""}`),h(t,200,{data:{}})}case"/events":{const{name:o,payload:i}=await c();return n.log(`[event] ${String(o)} ${JSON.stringify(i??null)}`),h(t,200,{data:{}})}case"/notifications":return n.log(`[notification] ${JSON.stringify(await c())}`),h(t,200,{data:{id:ut()}});case"/directory/identity":{const{userId:o}=await c(),i=n.users().get(String(o));return h(t,200,{data:{active:!!i,roleNames:i??[],memberships:[],checkedAt:new Date().toISOString()}})}case"/directory/list":{const{kind:o,search:i,ids:d}=await c(),p=typeof i=="string"?i.toLowerCase():"",y=Array.isArray(d)?new Set(d.map(String)):void 0,I=o==="users"?[...n.users().keys()].filter(E=>(!y||y.has(E))&&E.toLowerCase().includes(p)).map(E=>({id:E,displayName:E,active:!0})):[];return h(t,200,{data:{items:I,hasMore:!1}})}case"/directory/match":{const{userId:o,principals:i}=await c(),d=Array.isArray(i)?i:[];return h(t,200,{data:{active:n.users().has(String(o)),matched:d.flatMap((p,y)=>p.kind==="user"&&p.id===o?[y]:[]),checkedAt:new Date().toISOString()}})}case"/directory/validate":{const{principals:o}=await c(),i=Array.isArray(o)?o:[],d=n.users();return h(t,200,{data:{invalid:i.flatMap((p,y)=>p.kind==="user"&&d.has(String(p.id))?[]:[y])}})}case"/storage/get":case"/storage/stream":{const o=u(r.searchParams.get("key"));if(e.method==="POST"&&a==="/storage/stream")return await Me(o,await ge(e),e.headers["content-type"]),h(t,200,{data:{}});const i=await G(o);if(!i)return h(t,404);let d=await X(o);const p=Number(r.searchParams.get("bytes"));Number.isFinite(p)&&p>0&&(d=d.subarray(0,p)),t.writeHead(200,{"content-type":i.contentType??"application/octet-stream"}),t.end(d);return}case"/storage/put":return await Me(u(r.searchParams.get("key")),await ge(e),e.headers["content-type"]),h(t,200,{data:{}});case"/storage/head":{const o=await G(u((await c()).key));return o?h(t,200,{data:o}):h(t,404)}case"/storage/delete":{const o=u((await c()).key);return await Y(o,{force:!0}),await Y(F(o),{force:!0}),h(t,200,{data:{}})}case"/storage/copy":{const{sourceKey:o,destinationKey:i}=await c(),d=u(o),p=u(i);return await G(d)?(await oe(j(p),{recursive:!0}),await Ee(d,p),await Ee(F(d),F(p)).catch(()=>{}),h(t,200,{data:await G(p)})):h(t,404)}case"/storage/signed-url":{const{key:o}=await c();u(o);const i=`${fe}?key=${encodeURIComponent(String(o))}`;return h(t,200,{data:{url:`${n.origin()}${i}`,path:i}})}default:return n.log(`[host] ${a} is not available in local preview`),h(t,501,{error:{code:"not_available_in_preview",message:`${a} is not available in local preview`}})}}catch(a){const c=a.status??500;h(t,c,{error:{message:a instanceof Error?a.message:String(a)}})}}s(je,"handleFakeHost");var Ve="@tego/plugin-sdk",Rt=JSON.stringify(Ve),Ot=JSON.stringify(`${Ve}/frontend`);function Fe(e){return[{path:"index.html",contents:Ut(e)},{path:"main.tsx",contents:Mt(e)},{path:"shell.css",contents:Bt},{path:"plugin-surface.tsx",contents:Ht}]}s(Fe,"shellFiles");var Ct=s(e=>e.replace(/[&<>"]/g,t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"})[t]),"escapeHtml");function Ut({displayName:e}){return`<!doctype html>
2
6
  <html lang="en" class="h-full">
3
7
  <head>
4
8
  <meta charset="UTF-8" />
5
9
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>${mt(e)} \xB7 local preview</title>
10
+ <title>${Ct(e)} \xB7 local preview</title>
7
11
  </head>
8
12
  <body class="h-full">
9
13
  <div id="tego-dev-bar"></div>
@@ -11,7 +15,7 @@ var Me=Object.defineProperty;var s=(e,t)=>Me(e,"name",{value:t,configurable:!0})
11
15
  <script type="module" src="/main.tsx"><\/script>
12
16
  </body>
13
17
  </html>
14
- `}s(vt,"indexHtml");var wt=`@tailwind base;
18
+ `}s(Ut,"indexHtml");var Bt=`@tailwind base;
15
19
  @tailwind components;
16
20
  @tailwind utilities;
17
21
 
@@ -31,7 +35,7 @@ body {
31
35
  min-height: 0;
32
36
  overflow: auto;
33
37
  }
34
- `,yt=`import type { ReactNode } from 'react';
38
+ `,Ht=`import type { ReactNode } from 'react';
35
39
 
36
40
  function Unavailable(): ReactNode {
37
41
  return (
@@ -46,12 +50,12 @@ export const ChatPanel = Unavailable;
46
50
  export function useTopic() {
47
51
  return { topic: undefined, topics: [], loading: false };
48
52
  }
49
- `;function bt(e){const t=JSON.stringify({pluginId:e.pluginId,displayName:e.displayName,nav:e.nav.map(a=>({key:a.key,labelKey:a.labelKey,path:a.path??"/"})),config:e.config,locales:e.locales,defaultTarget:e.defaultTarget??"web",userHeader:re,rolesHeader:ae,defaultUser:ve},null,2),n=e.entry?`import plugin from ${JSON.stringify(e.entry)};`:"const plugin = undefined;";return`// Generated by \`tego-plugin dev\` on every start; edits here are overwritten.
53
+ `;function Mt(e){const t=JSON.stringify({pluginId:e.pluginId,displayName:e.displayName,nav:e.nav.map(r=>({key:r.key,labelKey:r.labelKey,path:r.path??"/"})),config:e.config,locales:e.locales,defaultTarget:e.defaultTarget??"web",userHeader:se,rolesHeader:ie,defaultUser:Pe},null,2),n=e.entry?`import plugin from ${JSON.stringify(e.entry)};`:"const plugin = undefined;";return`// Generated by \`tego-plugin dev\` on every start; edits here are overwritten.
50
54
  import '@tego/ui/styles/tokens.css';
51
55
  import './shell.css';
52
56
 
53
- import { pluginI18nNamespace } from ${pt};
54
- import type { PluginFrontendModule, PluginHostContext, PluginRoute } from ${ht};
57
+ import { pluginI18nNamespace } from ${Rt};
58
+ import type { PluginFrontendModule, PluginHostContext, PluginRoute } from ${Ot};
55
59
  import {
56
60
  Button,
57
61
  Input,
@@ -378,4 +382,5 @@ if (frontend) {
378
382
  DEV.pluginId +
379
383
  '/</p>';
380
384
  }
381
- `}s(bt,"mainTsx");var Ae="/__tego/csp-report",$e="TEGO_DEV_CSP ",St="tego-dev-target",Et=100*1024*1024,Ne=["default-src 'self'","object-src 'none'","base-uri 'self'"],De={web:[...Ne,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline'","img-src 'self' data: blob: https:","font-src 'self' data:","connect-src 'self' wss: ws:","frame-src 'self'","worker-src 'self' blob:"].join("; "),desktop:[...Ne,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline' https:","img-src 'self' data: blob: http: https:","font-src 'self' data: blob: https:","media-src 'self' data: blob:","worker-src 'self' blob:","connect-src 'self' http: https: ws: wss:","frame-src 'self'"].join("; ")};function de(e){return e==="web"||e==="desktop"}s(de,"isPreviewTarget");function Re(e,t){const n=new URL(e.url??"/","http://preview.local").searchParams.get("target");if(de(n))return n;const a=/(?:^|;\s*)tego-dev-target=(web|desktop)(?:;|$)/.exec(String(e.headers.cookie??""))?.[1];return de(a)?a:t}s(Re,"targetOf");function Le(e){try{const t=JSON.parse(e)["csp-report"];if(!t)return;const n=String(t["effective-directive"]??t["violated-directive"]??""),a=String(t["blocked-uri"]??"");if(!n)return;const r=typeof t["source-file"]=="string"?t["source-file"]:void 0,i=typeof t["line-number"]=="number"?t["line-number"]:void 0;return{directive:n,blocked:a,...r?{source:r}:{},...i?{line:i}:{}}}catch{return}}s(Le,"parseCspReport");var Ce=5280,Tt=20,Oe="TEGO_DEV_READY ",Ue=".tego-dev",kt="/api/v1/tego-os/plugin-api/";function xt(e){const{displayName:t}=e;return typeof t=="string"?t:t.en??t["zh-CN"]??Object.values(t)[0]??e.id}s(xt,"displayNameOf");function ue(e,t){for(let n=I(e);;n=C(n)){const a=w(n,"node_modules",...t.split("/"));if(K(w(a,"package.json")))return Ye(a);if(C(n)===n)return}}s(ue,"packageDir");function D(e,t,n){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(n))}s(D,"sendJson");function It(e,t,n){return new Promise((a,r)=>{const i=s(o=>{e.off("listening",d),r(o)},"onError"),d=s(()=>{e.off("error",i);const o=e.address();a(typeof o=="object"&&o?o.port:t)},"onListening");e.once("error",i),e.once("listening",d),e.listen(t,n)})}s(It,"listen");async function Pt(e,t){const n=rt(w(e,"vite.config.ts"));let a,r;try{a=n("@tego/ui/tailwind-preset"),r=C(n.resolve("@tego/ui"))}catch(d){throw new Error(`The preview cannot load @tego/ui's Tailwind preset from ${e}: ${d instanceof Error?d.message:String(d)}. Install the plugin's dependencies.`,{cause:d})}const i=(await import("tailwindcss")).default;return i({presets:[a],content:[w(r,"**/*.{js,cjs}"),w(t,"*.tsx")]})}s(Pt,"shellTailwind");function _t(e,t,n){return{postcssPlugin:"tego-dev-stylesheets",async Once(a,r){const i=a.source?.input.file,d=i&&I(i)===e?[t]:n;if(d.length===0)return;const o=await r.postcss(d).process(a,{from:i});r.result.messages.push(...o.messages)}}}s(_t,"perStylesheet");function At(e){return Array.isArray(e)?e.flat(1/0).map(t=>{if(!t||typeof t!="object")return t;const{buildStart:n}=t;return typeof n!="function"?t:{...t,buildStart(...a){const r=new Proxy(this,{get(i,d){if(d==="emitFile")return()=>"";const o=Reflect.get(i,d);return typeof o=="function"?o.bind(i):o}});return n.apply(r,a)}}}):e}s(At,"servePlugins");async function $t(e){const t=I(e.root),n=e.log??(c=>console.log(c)),a=await Ke(t),r=ze(a),i=w(t,Ue),d=w(i,"shell"),o=fe(32).toString("hex"),l=fe(24).toString("hex");let u="";const f=new Map;let y,S={};const v=r.frontend?I(t,r.frontend.dir):t,{loadConfigFromFile:R,createServer:H,mergeConfig:V}=await import("vite");if(r.frontend){const c=w(v,"vite.config.ts");K(c)&&(S=(await R({command:"serve",mode:"development"},c,t))?.config??{});const g=S.build?.rollupOptions?.input,b=I(t,typeof g=="string"?g:`${r.frontend.dir}/src/index.tsx`);if(!K(b))throw new Error(`The frontend entry ${me(t,b)} does not exist.`);y=me(d,b).split(he).join("/"),y.startsWith(".")||(y=`./${y}`)}const h=s(()=>r.frontend?Je(t,a,r.frontend.i18nDirPath):Promise.resolve(new Map),"localeFiles"),E=[...(await h()).keys()].map(c=>c.replace(/\.json$/,""));await ee(d,{recursive:!0});for(const c of _e({pluginId:a.id,displayName:xt(a),nav:r.frontend?.nav??[],config:r.frontend?.config?ge(r.frontend.config.schema,{}):{},entry:y,locales:E,defaultTarget:e.target??"web"}))await ne(w(d,c.path),c.contents);let _,x;const Be=s(()=>r.backend?.config?ge(r.backend.config.schema,{}):{},"backendConfig"),j=tt((c,g)=>void He(c,g));async function He(c,g){const b=(c.url??"/").split("?")[0],P=Re(c,e.target??"web");try{if(b==="/favicon.ico"){g.writeHead(204),g.end();return}if(b===Ae){const m=await new Promise(F=>{let N="";c.setEncoding("utf8"),c.on("data",A=>N+=A),c.on("end",()=>F(N))}),k=Le(m);k&&(n(`[csp:${P}] blocked ${k.blocked||"(inline)"} by ${k.directive}${k.source?` in ${k.source}${k.line?`:${k.line}`:""}`:""}`),n(`${$e}${JSON.stringify({target:P,...k})}`)),g.writeHead(204),g.end();return}if(b.startsWith(se)||b.startsWith(ie)){await Ie(c,g,{token:l,storageDir:w(i,"storage"),config:Be,origin:s(()=>u,"origin"),users:s(()=>f,"users"),log:n});return}const T=/^\/i18n\/([A-Za-z-]+)\.json$/.exec(b);if(T){const m=(await h()).get(`${T[1]}.json`);if(m===void 0)return D(g,404,{error:`no ${T[1]}.json`});g.writeHead(200,{"content-type":"application/json"}),g.end(m);return}const $=`${kt}${a.id}`;if(b===$||b.startsWith(`${$}/`)){Ve(c,g,(c.url??"/").slice($.length)||"/",P);return}if(String(c.headers.accept??"").includes("text/html")){g.setHeader("Content-Security-Policy",`${De[P]}; report-uri ${Ae}`);const m=new URL(c.url??"/","http://preview.local").searchParams.get("target");de(m)&&g.setHeader("Set-Cookie",`${St}=${m}; Path=/; SameSite=Lax`)}z.middlewares(c,g,()=>D(g,404,{error:`${b} not found`}))}catch(T){D(g,500,{error:T instanceof Error?T.message:String(T)})}}s(He,"route");function Ve(c,g,b,P){if(!_){D(g,503,{error:x??"The plugin has no backend part."});return}const T=we(c.headers);f.set(T.userId,T.roles);const $=nt({host:"127.0.0.1",port:_.port,method:c.method,path:b,headers:{...ye(c.headers,T,o),...P==="desktop"?{"accept-encoding":"identity"}:{}}},m=>{if(P!=="desktop"){g.writeHead(m.statusCode??502,m.headers),m.pipe(g);return}const k=[];let F=0,N=!1;m.on("data",A=>{if(!N){if(F+=A.length,F>Et){N=!0,m.destroy(),n(`[desktop] ${c.method} ${b}: response over 100 MB refused (413)`),D(g,413,{error:"Response over 100 MB: the desktop app refuses it. Serve large files through storage paths."});return}k.push(A)}}),m.on("end",()=>{if(N)return;const A=Buffer.concat(k),Z={...m.headers};delete Z["transfer-encoding"],Z["content-length"]=String(A.length),g.writeHead(m.statusCode??502,Z),g.end(A)})});$.on("error",m=>{g.headersSent?g.destroy():D(g,502,{error:`The backend is not answering (${m.message}); see the terminal.`})}),c.pipe($)}s(Ve,"proxy");const je=w(d,"shell.css"),Ge=(()=>{const c=S.css?.postcss;return c&&typeof c=="object"?c.plugins??[]:[]})(),G=(()=>{const c=ue(v,"react-i18next");return(c&&ue(c,"i18next"))??ue(v,"i18next")})(),{css:Nt,mode:Dt,build:Rt,...W}=S;W.plugins=At(W.plugins);const z=await H(V(W,{configFile:!1,root:d,mode:"development",appType:"spa",clearScreen:!1,logLevel:"info",esbuild:{jsx:"automatic",jsxDev:!1},css:{postcss:{plugins:[_t(je,await Pt(v,d),Ge)]}},resolve:{alias:[{find:"@tego/chat/plugin-surface",replacement:w(d,"plugin-surface.tsx")},...G?[{find:/^i18next$/,replacement:G}]:[]]},server:{middlewareMode:!0,hmr:{server:j},fs:{allow:[t,d,...G?[G]:[]]}},optimizeDeps:{entries:[w(d,"index.html")]}})),M=e.host??"127.0.0.1";let q;const X=e.port??Ce;for(let c=0;q===void 0;c++)try{q=await It(j,X===0?0:X+c,M)}catch(g){if(g.code!=="EADDRINUSE"||X===0||c>=Tt)throw await z.close(),g}const Y=M==="0.0.0.0"||M==="::"?"127.0.0.1":M;if(u=`http://${Y.includes(":")?`[${Y}]`:Y}:${q}`,!r.backend)x="The plugin has no backend part.";else if(e.backend===!1)x="The backend is off (--no-backend).";else if(!e.databaseUrl)x="The backend needs a PostgreSQL: start the preview with DATABASE_URL=postgres://\u2026 (each plugin gets its own schema).";else try{_=await gt({root:t,pluginId:a.id,version:a.version,part:r.backend,databaseUrl:e.databaseUrl,outDir:w(i,"backend"),hostApiUrl:`${u}${se}`,hostApiToken:l,gatewayKey:o,log:n})}catch(c){x=`The backend did not start: ${c instanceof Error?c.message:String(c)}`}x&&r.backend&&n(`[backend] ${x}`);const Q=`${u}/p/${a.id}/`;return n(`${a.id} ${a.version}: local preview at ${Q}`),e.databaseUrl&&_&&n(` database ${oe(e.databaseUrl)}, schema ${_.schema}`),n(`${Oe}${JSON.stringify({url:Q})}`),{url:Q,async close(){await _?.close(),await z.close(),j.closeAllConnections(),await new Promise(c=>j.close(()=>c()))}}}s($t,"startDevServer");export{$e as CSP_REPORT_PREFIX,Ce as DEFAULT_DEV_PORT,Ue as DEV_DIR,Oe as DEV_READY_PREFIX,ae as DEV_ROLES_HEADER,O as DEV_TENANT_ID,re as DEV_USER_HEADER,De as PREVIEW_CSP,Te as applyDevMigrations,we as devCallerOf,Se as devDatabaseUrl,be as devSchemaName,ye as gatewayHeaders,Ie as handleFakeHost,Le as parseCspReport,_e as shellFiles,$t as startDevServer,le as storagePath,Re as targetOf};
385
+ `}s(Mt,"mainTsx");var Ge="/__tego/csp-report",Ke="TEGO_DEV_CSP ",jt="tego-dev-target",Vt=100*1024*1024,Je=["default-src 'self'","object-src 'none'","base-uri 'self'"],ze={web:[...Je,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline'","img-src 'self' data: blob: https:","font-src 'self' data:","connect-src 'self' wss: ws:","frame-src 'self'","worker-src 'self' blob:"].join("; "),desktop:[...Je,"script-src 'self' 'wasm-unsafe-eval'","style-src 'self' 'unsafe-inline' https:","img-src 'self' data: blob: http: https:","font-src 'self' data: blob: https:","media-src 'self' data: blob:","worker-src 'self' blob:","connect-src 'self' http: https: ws: wss:","frame-src 'self'"].join("; ")};function he(e){return e==="web"||e==="desktop"}s(he,"isPreviewTarget");function We(e,t){const n=new URL(e.url??"/","http://preview.local").searchParams.get("target");if(he(n))return n;const r=/(?:^|;\s*)tego-dev-target=(web|desktop)(?:;|$)/.exec(String(e.headers.cookie??""))?.[1];return he(r)?r:t}s(We,"targetOf");function qe(e){try{const t=JSON.parse(e)["csp-report"];if(!t)return;const n=String(t["effective-directive"]??t["violated-directive"]??""),r=String(t["blocked-uri"]??"");if(!n)return;const a=typeof t["source-file"]=="string"?t["source-file"]:void 0,c=typeof t["line-number"]=="number"?t["line-number"]:void 0;return{directive:n,blocked:r,...a?{source:a}:{},...c?{line:c}:{}}}catch{return}}s(qe,"parseCspReport");var Xe=5280,Ft=20,Ye="TEGO_DEV_READY ",Qe=".tego-dev",Gt="/api/v1/tego-os/plugin-api/";function Ze(e){return[`${e} has a backend, and the preview runs it for real against a PostgreSQL: set DATABASE_URL`,"(DATABASE_URL=postgres://user:password@localhost:5432/tego_dev pnpm dev; each plugin gets its own schema).","In Tego Studio: Settings \u2192 Plugins \u2192 Tego plugin development \u2192 Dev database URL.","To look at the frontend alone, run with --no-backend."].join(`
386
+ `)}s(Ze,"missingDatabaseMessage");function Kt(e){const{displayName:t}=e;return typeof t=="string"?t:t.en??t["zh-CN"]??Object.values(t)[0]??e.id}s(Kt,"displayNameOf");function me(e,t){for(let n=P(e);;n=j(n)){const r=m(n,"node_modules",...t.split("/"));if(q(m(r,"package.json")))return ft(r);if(j(n)===n)return}}s(me,"packageDir");function C(e,t,n){e.writeHead(t,{"content-type":"application/json"}),e.end(JSON.stringify(n))}s(C,"sendJson");function Jt(e,t,n){return new Promise((r,a)=>{const c=s(o=>{e.off("listening",u),a(o)},"onError"),u=s(()=>{e.off("error",c);const o=e.address();r(typeof o=="object"&&o?o.port:t)},"onListening");e.once("error",c),e.once("listening",u),e.listen(t,n)})}s(Jt,"listen");async function zt(e,t){const n=mt(m(e,"vite.config.ts"));let r,a;try{r=n("@tego/ui/tailwind-preset"),a=j(n.resolve("@tego/ui"))}catch(u){throw new Error(`The preview cannot load @tego/ui's Tailwind preset from ${e}: ${u instanceof Error?u.message:String(u)}. Install the plugin's dependencies.`,{cause:u})}const c=(await import("tailwindcss")).default;return c({presets:[r],content:[m(a,"**/*.{js,cjs}"),m(t,"*.tsx")]})}s(zt,"shellTailwind");function Wt(e,t,n){return{postcssPlugin:"tego-dev-stylesheets",async Once(r,a){const c=r.source?.input.file,u=c&&P(c)===e?[t]:n;if(u.length===0)return;const o=await a.postcss(u).process(r,{from:c});a.result.messages.push(...o.messages)}}}s(Wt,"perStylesheet");function qt(e){return Array.isArray(e)?e.flat(1/0).map(t=>{if(!t||typeof t!="object")return t;const{buildStart:n}=t;return typeof n!="function"?t:{...t,buildStart(...r){const a=new Proxy(this,{get(c,u){if(u==="emitFile")return()=>"";const o=Reflect.get(c,u);return typeof o=="function"?o.bind(c):o}});return n.apply(a,r)}}}):e}s(qt,"servePlugins");async function Xt(e){const t=P(e.root),n=e.log??(l=>console.log(l)),r=await st(t),a=lt(r),c=process.env.TEGO_DEV_POLL==="1"||!await Pt();c&&n("[watch] file events are unavailable on this machine (too many watchers open); the preview polls for changes");const u=!!a.backend&&e.backend!==!1;if(u){if(!e.databaseUrl)throw new Error(Ze(r.id));await Ne(e.databaseUrl)}const o=m(t,Qe),i=m(o,"shell"),d=we(32).toString("hex"),p=we(24).toString("hex");let y="";const I=new Map;let E,D={};const N=a.frontend?P(t,a.frontend.dir):t,{loadConfigFromFile:_,createServer:$,mergeConfig:U}=await import("vite");if(a.frontend){const l=m(N,"vite.config.ts");q(l)&&(D=(await _({command:"serve",mode:"development"},l,t))?.config??{});const g=D.build?.rollupOptions?.input,b=P(t,typeof g=="string"?g:`${a.frontend.dir}/src/index.tsx`);if(!q(b))throw new Error(`The frontend entry ${ke(t,b)} does not exist.`);E=ke(i,b).split(Te).join("/"),E.startsWith(".")||(E=`./${E}`)}const B=s(()=>a.frontend?it(t,r,a.frontend.i18nDirPath):Promise.resolve(new Map),"localeFiles"),K=[...(await B()).keys()].map(l=>l.replace(/\.json$/,""));await oe(i,{recursive:!0});for(const l of Fe({pluginId:r.id,displayName:Kt(r),nav:a.frontend?.nav??[],config:a.frontend?.config?ve(a.frontend.config.schema,{}):{},entry:E,locales:K,defaultTarget:e.target??"web"}))await Q(m(i,l.path),l.contents);let f,w;const L=s(()=>a.backend?.config?ve(a.backend.config.schema,{}):{},"backendConfig"),S=pt((l,g)=>void et(l,g));async function et(l,g){const b=(l.url??"/").split("?")[0],x=We(l,e.target??"web");try{if(b==="/favicon.ico"){g.writeHead(204),g.end();return}if(b===Ge){const v=await new Promise(W=>{let O="";l.setEncoding("utf8"),l.on("data",A=>O+=A),l.on("end",()=>W(O))}),k=qe(v);k&&(n(`[csp:${x}] blocked ${k.blocked||"(inline)"} by ${k.directive}${k.source?` in ${k.source}${k.line?`:${k.line}`:""}`:""}`),n(`${Ke}${JSON.stringify({target:x,...k})}`)),g.writeHead(204),g.end();return}if(b.startsWith(de)||b.startsWith(fe)){await je(l,g,{token:p,storageDir:m(o,"storage"),config:L,origin:s(()=>y,"origin"),users:s(()=>I,"users"),log:n});return}const T=/^\/i18n\/([A-Za-z-]+)\.json$/.exec(b);if(T){const v=(await B()).get(`${T[1]}.json`);if(v===void 0)return C(g,404,{error:`no ${T[1]}.json`});g.writeHead(200,{"content-type":"application/json"}),g.end(v);return}const R=`${Gt}${r.id}`;if(b===R||b.startsWith(`${R}/`)){tt(l,g,(l.url??"/").slice(R.length)||"/",x);return}if(String(l.headers.accept??"").includes("text/html")){g.setHeader("Content-Security-Policy",`${ze[x]}; report-uri ${Ge}`);const v=new URL(l.url??"/","http://preview.local").searchParams.get("target");he(v)&&g.setHeader("Set-Cookie",`${jt}=${v}; Path=/; SameSite=Lax`)}H.middlewares(l,g,()=>C(g,404,{error:`${b} not found`}))}catch(T){C(g,500,{error:T instanceof Error?T.message:String(T)})}}s(et,"route");function tt(l,g,b,x){if(!f){C(g,503,{error:w??"The plugin has no backend part."});return}const T=xe(l.headers);I.set(T.userId,T.roles);const R=ht({host:"127.0.0.1",port:f.port,method:l.method,path:b,headers:{...Ie(l.headers,T,d),...x==="desktop"?{"accept-encoding":"identity"}:{}}},v=>{if(x!=="desktop"){g.writeHead(v.statusCode??502,v.headers),v.pipe(g);return}const k=[];let W=0,O=!1;v.on("data",A=>{if(!O){if(W+=A.length,W>Vt){O=!0,v.destroy(),n(`[desktop] ${l.method} ${b}: response over 100 MB refused (413)`),C(g,413,{error:"Response over 100 MB: the desktop app refuses it. Serve large files through storage paths."});return}k.push(A)}}),v.on("end",()=>{if(O)return;const A=Buffer.concat(k),ae={...v.headers};delete ae["transfer-encoding"],ae["content-length"]=String(A.length),g.writeHead(v.statusCode??502,ae),g.end(A)})});R.on("error",v=>{g.headersSent?g.destroy():C(g,502,{error:`The backend is not answering (${v.message}); see the terminal.`})}),l.pipe(R)}s(tt,"proxy");const nt=m(i,"shell.css"),rt=(()=>{const l=D.css?.postcss;return l&&typeof l=="object"?l.plugins??[]:[]})(),J=(()=>{const l=me(N,"react-i18next");return(l&&me(l,"i18next"))??me(N,"i18next")})(),{css:Yt,mode:Qt,build:Zt,...Z}=D;Z.plugins=qt(Z.plugins);const H=await $(U(Z,{configFile:!1,root:i,mode:"development",appType:"spa",clearScreen:!1,logLevel:"info",esbuild:{jsx:"automatic",jsxDev:!1},css:{postcss:{plugins:[Wt(nt,await zt(N,i),rt)]}},resolve:{alias:[{find:"@tego/chat/plugin-surface",replacement:m(i,"plugin-surface.tsx")},...J?[{find:/^i18next$/,replacement:J}]:[]]},server:{middlewareMode:!0,hmr:{server:S},fs:{allow:[t,i,...J?[J]:[]]},watch:{ignored:["**/.tego-dev/backend/**","**/build/**","**/dist/**"],...c?{usePolling:!0,interval:500}:{}}},optimizeDeps:{entries:[m(i,"index.html")]}}));H.watcher.on("error",l=>n(`[watch] frontend: ${l instanceof Error?l.message:String(l)}; restart the preview if saves stop reloading`));const z=e.host??"127.0.0.1";let ee;const te=e.port??Xe;for(let l=0;ee===void 0;l++)try{ee=await Jt(S,te===0?0:te+l,z)}catch(g){if(g.code!=="EADDRINUSE"||te===0||l>=Ft)throw await H.close(),g}const ne=z==="0.0.0.0"||z==="::"?"127.0.0.1":z;if(y=`http://${ne.includes(":")?`[${ne}]`:ne}:${ee}`,!a.backend)w="The plugin has no backend part.";else if(!u)w="The backend is off (--no-backend).",n(`[backend] ${w}`);else try{f=await Nt({root:t,pluginId:r.id,version:r.version,part:a.backend,databaseUrl:e.databaseUrl,outDir:m(o,"backend"),hostApiUrl:`${y}${de}`,hostApiToken:p,gatewayKey:d,...e.inspect?{inspect:e.inspect}:{},poll:c,log:n})}catch(l){throw await H.close(),S.closeAllConnections(),await new Promise(g=>S.close(()=>g())),new Error(`The backend did not start: ${l instanceof Error?l.message:String(l)}`,{cause:l})}const re=`${y}/p/${r.id}/`;return n(`${r.id} ${r.version}: local preview at ${re}`),e.databaseUrl&&f&&n(` database ${ce(e.databaseUrl)}, schema ${f.schema}`),n(`${Ye}${JSON.stringify({url:re})}`),{url:re,async close(){await f?.close(),await H.close(),S.closeAllConnections(),await new Promise(l=>S.close(()=>l()))}}}s(Xt,"startDevServer");export{Ke as CSP_REPORT_PREFIX,Xe as DEFAULT_DEV_PORT,At as DEFAULT_INSPECT_PORT,Qe as DEV_DIR,Ye as DEV_READY_PREFIX,ie as DEV_ROLES_HEADER,V as DEV_TENANT_ID,se as DEV_USER_HEADER,ze as PREVIEW_CSP,Re as applyDevMigrations,Be as backendNodeArgs,Ne as checkDevDatabase,xe as devCallerOf,$e as devDatabaseUrl,_e as devSchemaName,De as explainDatabaseError,Ie as gatewayHeaders,je as handleFakeHost,He as linePrefixer,Ze as missingDatabaseMessage,qe as parseCspReport,Fe as shellFiles,Xt as startDevServer,pe as storagePath,We as targetOf};
package/dist/doctor.cjs CHANGED
@@ -1,8 +1,15 @@
1
- "use strict";var A=Object.defineProperty;var d=(e,t)=>A(e,"name",{value:t,configurable:!0});var chunkSKNPNPJP_cjs=require("./chunk-SKNPNPJP.cjs"),chunk45Y4CR4R_cjs=require("./chunk-45Y4CR4R.cjs"),chunk2P6UEAPV_cjs=require("./chunk-2P6UEAPV.cjs"),chunkN4SXZZPY_cjs=require("./chunk-N4SXZZPY.cjs"),chunk6HNG5OZI_cjs=require("./chunk-6HNG5OZI.cjs");require("./chunk-AJY7ZHXK.cjs"),require("./chunk-OP64Y4IN.cjs");var child_process=require("child_process"),promises=require("fs/promises"),path=require("path"),COLOR_ALLOW_MARKER="ui-colors-allow",PALETTE="slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose",UTILITY="bg|text|border(?:-[trblxyse])?|ring(?:-offset)?|from|via|to|fill|stroke|outline|divide|placeholder|caret|accent|decoration|shadow",COLOR_PATTERNS=[new RegExp(`(?<![\\w-])(?:${UTILITY})-(?:(?:${PALETTE})-(?:50|[1-9]00|950)|white|black)(?:\\/\\d{1,3})?(?![\\w-])`,"g"),/-\[(?:#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\(\s*\d+)/g,/(?<![a-zA-Z0-9[-])(?:rgba?|hsla?)\(\s*\d+/g],QUOTED_HEX=/['"`]#[0-9a-fA-F]{3,8}['"`]/g,CSS_HEX=/(?<![\w&-])#[0-9a-fA-F]{3,8}(?![\w-])/g,DARK_VARIANT=/(?<![\w-])dark:(?=\S)/g,DARK_SELECTOR=/\.dark(?![\w-])/g;function stripComments(e){return e.replace(/(^|[\s{(,;])\/\*[\s\S]*?\*\//g,(t,r)=>r+t.slice(r.length).replace(/[^\n]/g," ")).replace(/(^|\s)\/\/.*$/gm,(t,r)=>r)}d(stripComments,"stripComments");function allowedLines(e){const t=new Set;return e.forEach((r,s)=>{r.includes(COLOR_ALLOW_MARKER)&&(t.add(s),/^\s*(?:\/\/|\/\*|\*|\{\/\*)/.test(r)&&t.add(s+1))}),t}d(allowedLines,"allowedLines");function findColorIssues(e,t){const r=t===".css",s=allowedLines(e.split(`
2
- `)),c=stripComments(e).split(`
3
- `),i=[...COLOR_PATTERNS,r?CSS_HEX:QUOTED_HEX],o=r?[DARK_VARIANT,DARK_SELECTOR]:[DARK_VARIANT],h=[];return c.forEach((f,n)=>{if(s.has(n))return;const u=[];for(const[a,l]of[["color",i],["dark",o]])for(const w of l)for(const k of f.matchAll(w))u.push({kind:a,match:k[0],at:k.index??0});u.sort((a,l)=>a.kind.localeCompare(l.kind)||a.at-l.at).forEach(({kind:a,match:l})=>h.push({line:n+1,kind:a,match:l}))}),h}d(findColorIssues,"findColorIssues");var rule=d((e,t,r,s)=>({id:e,severity:t,guide:r,title:s}),"rule"),DOCTOR_RULES=[rule("TP-MAN-001","error","manifest.md","tego.plugin.json is valid"),rule("TP-MAN-002","error","host-api-versions.md","hostApiRange admits every host capability the code uses"),rule("TP-PKG-001","error","manifest.md","SDK packages come from npm aliases or workspace links, never a plain @tego/* name"),rule("TP-PKG-002","warning","testing.md","package.json has the lint, type-check, test and build gates"),rule("TP-CLAW-002","warning","claw.md","Each claw skill's frontmatter name matches its folder"),rule("TP-DESK-001","error","desktop-web.md","No eval or new Function (the desktop CSP refuses them)"),rule("TP-DESK-002","error","desktop-web.md","No addresses built from location.origin (tauri://localhost on desktop)"),rule("TP-DESK-003","error","desktop-web.md","External links open with ctx.openExternal, not window.open"),rule("TP-DESK-004","error","desktop-web.md","Files are saved with ctx.download, not <a download> or a.download"),rule("TP-DESK-005","warning","desktop-web.md","No streamed responses or EventSource from the frontend (the desktop proxy buffers; use ctx.events)"),rule("TP-DESK-006","warning","desktop-web.md","Clipboard writes go through ctx.copyText"),rule("TP-WEB-001","error","desktop-web.md","The frontend calls no third-party URL (the web CSP blocks it); call the backend, which declares egress"),rule("TP-WEB-002","error","desktop-web.md","No remote or injected scripts; everything ships in the bundle"),rule("TP-UI-001","error","ui.md","Colors come from token classes: no hex, rgb(), palette classes or dark: overrides"),rule("TP-UI-002","error","ui.md","No direct @radix-ui/* imports; use @tego/ui"),rule("TP-UI-003","error","ui.md","The plugin's root element carries its scope class (.<pluginId>) when it compiles its own Tailwind"),rule("TP-UI-004","error","frontend.md","The frontend has its own error boundary"),rule("TP-I18N-001","error","i18n.md","All ten locales, with the same keys as zh-CN, and every nav label defined"),rule("TP-I18N-002","error","i18n.md","Every locale value keeps the {{placeholders}} of zh-CN and is not empty"),rule("TP-I18N-003","error","i18n.md","Every literal t('\u2026') key exists in zh-CN"),rule("TP-I18N-004","warning","i18n.md","Current terminology: \u6570\u5B57\u5458\u5DE5 / Employee, never \u6570\u5B57\u5206\u8EAB / digital avatar"),rule("TP-BE-001","error","backend.md","Identity comes from the verified ctx, never from the raw x-tego-plugin-* headers"),rule("TP-BE-002","error","access.md","No host role names in business code; ask the access engine"),rule("TP-BE-003","error","migrations.md","Migrations are <ordinal>_<name>.sql with unique ordinals"),rule("TP-BE-004","error","migrations.md","Committed migrations are never edited or deleted"),rule("TP-BE-005","warning","backend.md","Backend errors are logged through host.log, not console"),rule("TP-ACC-001","error","access.md","With an access catalog, a test asserts every route has a policy (assertRouteCoverage)"),rule("TP-ACC-002","error","access.md","With an access catalog, start() reconciles it (reconcile(host))"),rule("TP-TEST-001","warning","testing.md","The plugin has tests")],BY_ID=new Map(DOCTOR_RULES.map(e=>[e.id,e]));function doctorRule(e){return BY_ID.get(e)}d(doctorRule,"doctorRule");var VERSIONED_CAPABILITIES=[{pattern:/\.openExternal\s*\(/,since:"1.37.0",name:"ctx.openExternal"},{pattern:/\.copyText\s*\(/,since:"1.37.0",name:"ctx.copyText"},{pattern:/\b(?:ctx|host|context)\.download\s*\(/,since:"1.37.0",name:"ctx.download"},{pattern:/\b(?:ctx|host|context)\.platform\b/,since:"1.37.0",name:"ctx.platform"}],SOURCE_EXTENSIONS=new Set([".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".css"]),SKIP_DIRS=new Set(["node_modules","dist","build",".tego-dev",".turbo",".drizzle","__fixtures__","coverage"]),TEST_FILE=/\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|[\\/])test[\\/]|(?:^|[\\/])test-[\w-]+\.[cm]?[jt]s$|-double\.[cm]?[jt]s$|\.d\.ts$/,IGNORE_MARKER=/tego-doctor-ignore\s+([A-Z0-9-]+(?:\s*,\s*[A-Z0-9-]+)*)/,PLACEHOLDER=/\{\{\s*([\w.-]+)\s*\}\}/g,REQUIRED_SCRIPTS=["lint","type-check","test","build"];function toPosix(e){return e.split(path.sep).join("/")}d(toPosix,"toPosix");async function walk(e,t,r){let s;try{s=await promises.readdir(path.join(e,t),{withFileTypes:!0})}catch{return}for(const c of s){const i=t?`${t}/${c.name}`:c.name;c.isDirectory()?!SKIP_DIRS.has(c.name)&&!c.name.startsWith(".")&&await walk(e,i,r):SOURCE_EXTENSIONS.has(path.extname(c.name))&&r.push(i)}}d(walk,"walk");function folderReader(e){return{async list(t){try{return(await promises.readdir(path.join(e,t),{withFileTypes:!0})).filter(s=>s.isFile()).map(s=>s.name)}catch{return[]}},readText:d(t=>promises.readFile(path.join(e,t),"utf8").catch(()=>null),"readText")}}d(folderReader,"folderReader");function runGit(e,t){return new Promise(r=>{child_process.execFile("git",["-C",e,...t],{maxBuffer:8*1024*1024},(s,c)=>r(s?null:c))})}d(runGit,"runGit");function ignoredLines(e){const t=new Map;return e.split(`
4
- `).forEach((r,s)=>{const c=IGNORE_MARKER.exec(r);if(!c?.[1])return;const i=new Set(c[1].split(",").map(h=>h.trim())),o=d(h=>t.set(h,new Set([...t.get(h)??[],...i])),"add");o(s),/^\s*(?:\/\/|\/\*|\*|\{\/\*)/.test(r)&&o(s+1)}),t}d(ignoredLines,"ignoredLines");var Findings=class{static{d(this,"Findings")}list=[];add(e,t,r={}){const{severity:s,guide:c}=doctorRule(e);this.list.push({rule:e,severity:s,message:t,guide:c,...r})}};function eachMatch(e,t,r,s){const c=ignoredLines(e.text);e.code.split(`
5
- `).forEach((i,o)=>{if(c.get(o)?.has(t))return;const h=r.exec(i);h&&s(o+1,h[0])})}d(eachMatch,"eachMatch");function partOf(e,t){const r=d(s=>!!s&&(e===s||e.startsWith(`${s}/`)),"inside");return r(t.frontend?.dir)||r(t.publicFrontend?.dir)?"frontend":r(t.backend?.dir)?"backend":r(t.claw?.dir)?"claw":r("shared")?"shared":"other"}d(partOf,"partOf");async function readJson(e){try{return JSON.parse(await promises.readFile(e,"utf8"))}catch{return}}d(readJson,"readJson");function checkPackageJson(e,t){if(!t)return;for(const c of["dependencies","devDependencies","peerDependencies"]){const i=t[c];if(!(!i||typeof i!="object"))for(const[o,h]of Object.entries(i))!o.startsWith("@tego/")||typeof h!="string"||/^(?:npm:@zhama\/tego-|workspace:|file:|link:|portal:)/.test(h)||e.add("TP-PKG-001",`${c}.${o} is "${h}": on npm @tego/* belongs to another project. Install it as "npm:@zhama/tego-${o.slice(6)}@^<sdk version>".`,{file:"package.json"})}const r=t.scripts??{},s=REQUIRED_SCRIPTS.filter(c=>typeof r[c]!="string");s.length>0&&e.add("TP-PKG-002",`package.json has no ${s.map(c=>`"${c}"`).join(", ")} script; the platform's gates are lint, type-check, test and build.`,{file:"package.json"})}d(checkPackageJson,"checkPackageJson");async function checkLocales(e,t,r,s,c){const i=s.frontend;if(!i)return;const{bundles:o,errors:h}=await chunk45Y4CR4R_cjs.verifyFrontendLocales(folderReader(t),i);for(const p of h)e.add("TP-I18N-001",p,{file:i.i18nDirPath});const f=o.get("zh-CN");if(!f)return;const n=d((p,m="",P=new Map)=>{if(typeof p!="object"||p===null||Array.isArray(p))return P.set(m,p),P;for(const[b,T]of Object.entries(p))n(T,m?`${m}.${b}`:b,P);return P},"leaves"),u=d(p=>new Set(typeof p=="string"?[...p.matchAll(PLACEHOLDER)].map(m=>m[1]):[]),"placeholders"),a=n(f);for(const[p,m]of o){const P=`${i.i18nDirPath}/${p}.json`;let b=0;for(const[T,g]of n(m)){if(b>=10)break;if(typeof g=="string"&&g.trim()===""){e.add("TP-I18N-002",`"${T}" is empty in ${p}.json.`,{file:P}),b++;continue}if(p==="zh-CN"||!a.has(T))continue;const v=u(a.get(T)),y=u(g),x=[...v].filter(E=>!y.has(E)),S=[...y].filter(E=>!v.has(E));(x.length||S.length)&&(e.add("TP-I18N-002",`"${T}" in ${p}.json has placeholders {${[...y].join(", ")}} but zh-CN has {${[...v].join(", ")}}.`,{file:P}),b++)}for(const[T,g]of n(m))if(typeof g=="string"&&/数字分身|digital avatar/i.test(g)){e.add("TP-I18N-004",`"${T}" in ${p}.json says "${/数字分身/.test(g)?"\u6570\u5B57\u5206\u8EAB":"digital avatar"}"; the platform calls them \u6570\u5B57\u5458\u5DE5 / Employee ({{avatarTerm}}).`,{file:P});break}}const l=chunk45Y4CR4R_cjs.flattenLocaleKeys(f),w=new Set(Object.keys(f)),k=d(p=>l.has(p)||["_one","_other","_zero","_few","_many","_two"].some(m=>l.has(p+m))||[...l].some(m=>m.startsWith(`${p}.`)),"exists");for(const p of c){if(p.part!=="frontend"||p.test)continue;const m=ignoredLines(p.text);p.code.split(`
6
- `).forEach((P,b)=>{if(!m.get(b)?.has("TP-I18N-003"))for(const T of P.matchAll(/\bt\(\s*['"]([A-Za-z0-9_.-]+)['"]/g)){const g=T[1],v=g.split(".")[0];!g.includes(".")||!w.has(v)||k(g)||e.add("TP-I18N-003",`t('${g}') has no entry in zh-CN.json (namespace ${chunkN4SXZZPY_cjs.pluginI18nNamespace(r.id)}).`,{file:p.path,line:b+1})}})}}d(checkLocales,"checkLocales");async function checkMigrations(e,t,r){const s=r.backend;if(!s)return;const{errors:c}=await chunk45Y4CR4R_cjs.verifyMigrationNames(folderReader(t),s.migrationsDirPath);for(const h of c)e.add("TP-BE-003",h,{file:s.migrationsDirPath});const i=await runGit(t,["diff","--name-status","HEAD","--",s.migrationsDirPath]);if(!i)return;const o=(await runGit(t,["rev-parse","--show-prefix"]))?.trim()??"";for(const h of i.split(`
7
- `)){const[f,n]=h.split(" ");if(!n||f!=="M"&&f!=="D")continue;const u=n.startsWith(o)?n.slice(o.length):n;e.add("TP-BE-004",`${u} is a committed migration and was ${f==="M"?"edited":"deleted"}; an install may have applied it already. Revert it and add a new migration instead.`,{file:u})}}d(checkMigrations,"checkMigrations");async function checkClaw(e,t,r){const s=r.claw;if(!s)return;const c=path.join(t,s.dir,"skills");let i=[];try{i=(await promises.readdir(c,{withFileTypes:!0})).filter(o=>o.isDirectory()).map(o=>o.name)}catch{return}for(const o of i){const h=await promises.readFile(path.join(c,o,"SKILL.md"),"utf8").catch(()=>null),f=`${s.dir}/skills/${o}/SKILL.md`,n=h?/^---[\s\S]*?^name:\s*["']?([^"'\n]+)["']?\s*$/m.exec(h)?.[1]?.trim():void 0;n!==o&&e.add("TP-CLAW-002",h?`${f} declares name "${n??"(none)"}"; it must equal the folder name "${o}".`:`${s.dir}/skills/${o} has no SKILL.md.`,{file:f})}}d(checkClaw,"checkClaw");function checkSources(e,t,r,s,c,i){const o=t.hostApiRange&&chunk6HNG5OZI_cjs.isValidHostApiRange(t.hostApiRange)?chunk6HNG5OZI_cjs.hostApiRangeMinimum(t.hostApiRange):void 0,h=d(n=>{if(!o)return!1;const[u,a,l]=o.split(".").map(Number),[w,k,p]=n.split(".").map(Number);return u!==w?u<w:a!==k?a<k:l<p},"below");for(const n of s){if(!c(n)||n.test)continue;const u=d(a=>({file:n.path,line:a}),"at");if(n.part==="frontend"){if(n.path.endsWith(".css")||n.path.endsWith(".tsx")||n.path.endsWith(".ts")){const a=ignoredLines(n.text);for(const l of findColorIssues(n.text,path.extname(n.path)))a.get(l.line-1)?.has("TP-UI-001")||e.add("TP-UI-001",l.kind==="dark"?`"${l.match}" is a hand-written dark override; tokens already carry both themes.`:`"${l.match}" is a literal color; use a token class (bg-card, text-muted-foreground, bg-primary\u2026).`,u(l.line))}eachMatch(n,"TP-DESK-001",/\beval\s*\(|\bnew\s+Function\s*\(/,a=>e.add("TP-DESK-001","eval / new Function: the desktop app's CSP (script-src 'self') refuses code compiled from strings.",u(a))),eachMatch(n,"TP-DESK-002",/\b(?:window\.|document\.)?location\.origin\b/,a=>e.add("TP-DESK-002","location.origin is tauri://localhost in the desktop app; build host addresses with ctx.hostUrl(path) and share links from ctx.publicPagesUrl.",u(a))),eachMatch(n,"TP-DESK-003",/\bwindow\.open\s*\(/,a=>e.add("TP-DESK-003","window.open does not reliably open links in the desktop app; use ctx.openExternal(url) (hostApiRange ^1.37.0), or ctx.download for a file.",u(a))),eachMatch(n,"TP-DESK-004",/\.download\s*=(?!=)|<a\b[^>]*\sdownload(?:[\s=>]|$)/,a=>e.add("TP-DESK-004","An <a download> save is not handled by the desktop app; use ctx.download({ path | blob, fileName }) (hostApiRange ^1.37.0).",u(a))),eachMatch(n,"TP-DESK-005",/\bnew\s+EventSource\s*\(|\.body\s*(?:\?\.|!\.|\.)\s*getReader\s*\(/,a=>e.add("TP-DESK-005","The desktop app buffers ctx.api responses whole (and caps them at 100 MB), so streams arrive at the end; subscribe to ctx.events for pushed updates.",u(a))),eachMatch(n,"TP-DESK-006",/\bnavigator\.clipboard\./,a=>e.add("TP-DESK-006","navigator.clipboard is not available in every shell; use ctx.copyText(text) (hostApiRange ^1.37.0).",u(a))),eachMatch(n,"TP-WEB-001",/\bfetch\s*\(\s*['"`]https?:\/\//,a=>e.add("TP-WEB-001","The web app's CSP allows connections to the platform only; call your backend through ctx.api and let it reach the service (declare it in parts.backend.egress).",u(a))),eachMatch(n,"TP-WEB-002",/\bimport\s*\(\s*['"`]https?:|createElement\(\s*['"]script['"]\s*\)|<script\b[^>]*\bsrc=/,a=>e.add("TP-WEB-002","Remote or injected scripts are refused by both shells' CSP; bundle the code.",u(a))),eachMatch(n,"TP-UI-002",/from\s+['"]@radix-ui\//,a=>e.add("TP-UI-002","Import the @tego/ui wrapper instead of @radix-ui/* (the shell shares @tego/ui; a second Radix copy breaks focus and portals).",u(a)));for(const a of VERSIONED_CAPABILITIES){if(!h(a.since))continue;const l=a.name.split(".").pop();new RegExp(`\\.${l}\\s*(?:\\)|\\?(?!\\.)|&&)`).test(n.code)||eachMatch(n,"TP-MAN-002",a.pattern,w=>e.add("TP-MAN-002",`${a.name} needs host API ${a.since}, but hostApiRange is "${t.hostApiRange}"; raise it to ^${a.since}.`,u(w)))}}n.part==="backend"&&(eachMatch(n,"TP-BE-001",/\bheaders?\s*(?:\.get)?\(\s*['"]x-tego-plugin-(?:user-id|tenant-id|roles|avatar-id|delegation)['"]|\bheaders\s*\[\s*['"]x-tego-plugin-(?:user-id|tenant-id|roles|avatar-id|delegation)['"]/i,a=>e.add("TP-BE-001","Take the caller from the verified ctx (ctx.userId, ctx.tenantId, ctx.roles) that serveBackend hands you; the raw x-tego-plugin-* headers are unverified here.",u(a))),eachMatch(n,"TP-BE-005",/\bconsole\.(?:error|warn)\s*\(/,a=>e.add("TP-BE-005","Log through host.log so the message reaches the platform's logs; console output from a plugin container is not collected.",u(a)))),n.part!=="other"&&eachMatch(n,"TP-BE-002",/\b(?:roles|roleNames)\s*\??\.\s*(?:includes|indexOf)\(\s*['"`](?:admin|owner|super_?admin|tenant_?admin|member|manager)['"`]/i,a=>e.add("TP-BE-002","Do not decide by host role names; declare permissions in the access catalog (hostRoleBindings) and ask the engine (authorize / useAccess).",u(a)))}const f=s.filter(n=>n.part==="frontend"&&!n.test);if(r.frontend&&f.length>0&&(f.some(n=>/getDerivedStateFromError|componentDidCatch|PluginErrorBoundary/.test(n.code))||e.add("TP-UI-004","No error boundary in the frontend: the shell's boundary cannot see into your React root, so a render error leaves a blank page. Wrap the root in PluginErrorBoundary (@tego/plugin-sdk/react) or your own.",{file:r.frontend.dir}),i?.includes("tegoPluginTailwind"))){const n=new RegExp(`(?:className|class)=\\{?\\s*(?:cn\\()?\\s*['"\`][^'"\`]*(?<![\\w-])${t.id.replace(/[.]/g,"\\.")}(?![\\w-])`);f.some(u=>n.test(u.code))||e.add("TP-UI-003",`tegoPluginTailwind scopes every utility under .${t.id}, but no element carries className="${t.id}": none of the plugin's own classes will apply. Put it on the root element (and on portalled content, or use tegoPluginScopeRoot).`,{file:r.frontend.dir})}}d(checkSources,"checkSources");function checkAccessAndTests(e,t,r,s){const c=s.filter(o=>o.test);if(c.length===0&&e.add("TP-TEST-001","The plugin has no *.test.ts files; backend routes and pure frontend logic need tests (createTestPluginHost for SQL)."),!t.access||!r.backend)return;c.some(o=>o.text.includes("assertRouteCoverage"))||e.add("TP-ACC-001","The manifest declares an access catalog but no test calls assertRouteCoverage(app): a route without a policy would be open to everyone who can open the plugin.",{file:r.backend.dir}),s.filter(o=>(o.part==="backend"||o.part==="shared")&&!o.test).some(o=>/\breconcile\s*\(/.test(o.code))||e.add("TP-ACC-002","The manifest declares an access catalog but start() never calls reconcile(host): built-in roles and catalog changes never reach the database.",{file:r.backend.dir})}d(checkAccessAndTests,"checkAccessAndTests");async function runDoctor(e){const t=e.root;await promises.stat(path.join(t,"tego.plugin.json")).catch(()=>{throw new Error(`No tego.plugin.json in ${t}; run doctor in the plugin folder.`)});const r=new Findings;let s;try{s=JSON.parse(await promises.readFile(path.join(t,"tego.plugin.json"),"utf8"))}catch(l){return r.add("TP-MAN-001",`tego.plugin.json is not valid JSON: ${l instanceof Error?l.message:String(l)}`,{file:"tego.plugin.json"}),summarize(r,void 0)}const c=chunkSKNPNPJP_cjs.parseTegoPluginManifest(s);if(!c.ok||!c.manifest){for(const l of c.errors)r.add("TP-MAN-001",l,{file:"tego.plugin.json"});return summarize(r,s.id)}const i=c.manifest,o=chunk2P6UEAPV_cjs.resolvePluginParts(i),h=[];await walk(t,"",h);const f=await Promise.all(h.map(async l=>{const w=await promises.readFile(path.join(t,l),"utf8");return{path:l,part:partOf(l,o),text:w,code:stripComments(w),test:TEST_FILE.test(l)}})),n=e.files?.map(l=>toPosix(path.relative(t,path.join(t,l)))),u=d(l=>!n||n.includes(l.path),"scanned"),a=o.frontend?await promises.readFile(path.join(t,o.frontend.dir,"vite.config.ts"),"utf8").catch(()=>null):null;return checkPackageJson(r,await readJson(path.join(t,"package.json"))),await checkLocales(r,t,i,o,f.filter(u)),await checkMigrations(r,t,o),await checkClaw(r,t,o),checkSources(r,i,o,f,u,a),checkAccessAndTests(r,i,o,f),summarize(r,i.id)}d(runDoctor,"runDoctor");function summarize(e,t){const r={errors:e.list.filter(i=>i.severity==="error").length,warnings:e.list.filter(i=>i.severity==="warning").length},s=new Map(DOCTOR_RULES.map((i,o)=>[i.id,o])),c=[...e.list].sort((i,o)=>(i.severity===o.severity?0:i.severity==="error"?-1:1)||(s.get(i.rule)??0)-(s.get(o.rule)??0)||(i.file??"").localeCompare(o.file??"")||(i.line??0)-(o.line??0));return{...t?{pluginId:t}:{},findings:c,...r}}d(summarize,"summarize");var UPLOAD_BLOCKING_RULES=["TP-MAN-001","TP-I18N-001","TP-BE-003"];function formatDoctorReport(e){const t=e.findings.map(r=>{const s=r.file?`${r.file}${r.line?`:${r.line}`:""} `:"";return`${r.severity==="error"?"\u2716":"\u26A0"} ${r.rule} ${s}${r.message}`});return t.push(e.findings.length===0?"\u2705 doctor: no problems":`${e.errors} error(s), ${e.warnings} warning(s). Each rule is explained in node_modules/@tego/plugin-sdk/agent/RULES.md.`),t.join(`
8
- `)}d(formatDoctorReport,"formatDoctorReport"),exports.DOCTOR_RULES=DOCTOR_RULES,exports.UPLOAD_BLOCKING_RULES=UPLOAD_BLOCKING_RULES,exports.doctorRule=doctorRule,exports.findColorIssues=findColorIssues,exports.formatDoctorReport=formatDoctorReport,exports.runDoctor=runDoctor;
1
+ "use strict";var C=Object.defineProperty;var f=(e,t)=>C(e,"name",{value:t,configurable:!0});var chunkH6GXDFLP_cjs=require("./chunk-H6GXDFLP.cjs"),chunk45Y4CR4R_cjs=require("./chunk-45Y4CR4R.cjs"),chunk2P6UEAPV_cjs=require("./chunk-2P6UEAPV.cjs"),chunkN4SXZZPY_cjs=require("./chunk-N4SXZZPY.cjs"),chunkIOZFHJ4F_cjs=require("./chunk-IOZFHJ4F.cjs");require("./chunk-AJY7ZHXK.cjs"),require("./chunk-OP64Y4IN.cjs");var child_process=require("child_process"),promises=require("fs/promises"),path=require("path"),crypto=require("crypto"),COLOR_ALLOW_MARKER="ui-colors-allow",PALETTE="slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose",UTILITY="bg|text|border(?:-[trblxyse])?|ring(?:-offset)?|from|via|to|fill|stroke|outline|divide|placeholder|caret|accent|decoration|shadow",COLOR_PATTERNS=[new RegExp(`(?<![\\w-])(?:${UTILITY})-(?:(?:${PALETTE})-(?:50|[1-9]00|950)|white|black)(?:\\/\\d{1,3})?(?![\\w-])`,"g"),/-\[(?:#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\(\s*\d+)/g,/(?<![a-zA-Z0-9[-])(?:rgba?|hsla?)\(\s*\d+/g],QUOTED_HEX=/['"`]#[0-9a-fA-F]{3,8}['"`]/g,CSS_HEX=/(?<![\w&-])#[0-9a-fA-F]{3,8}(?![\w-])/g,DARK_VARIANT=/(?<![\w-])dark:(?=\S)/g,DARK_SELECTOR=/\.dark(?![\w-])/g;function stripComments(e){return e.replace(/(^|[\s{(,;])\/\*[\s\S]*?\*\//g,(t,s)=>s+t.slice(s.length).replace(/[^\n]/g," ")).replace(/(^|\s)\/\/.*$/gm,(t,s)=>s)}f(stripComments,"stripComments");function allowedLines(e){const t=new Set;return e.forEach((s,n)=>{s.includes(COLOR_ALLOW_MARKER)&&(t.add(n),/^\s*(?:\/\/|\/\*|\*|\{\/\*)/.test(s)&&t.add(n+1))}),t}f(allowedLines,"allowedLines");function findColorIssues(e,t){const s=t===".css",n=allowedLines(e.split(`
2
+ `)),l=stripComments(e).split(`
3
+ `),r=[...COLOR_PATTERNS,s?CSS_HEX:QUOTED_HEX],a=s?[DARK_VARIANT,DARK_SELECTOR]:[DARK_VARIANT],o=[];return l.forEach((p,i)=>{if(n.has(i))return;const h=[];for(const[d,c]of[["color",r],["dark",a]])for(const u of c)for(const g of p.matchAll(u))h.push({kind:d,match:g[0],at:g.index??0});h.sort((d,c)=>d.kind.localeCompare(c.kind)||d.at-c.at).forEach(({kind:d,match:c})=>o.push({line:i+1,kind:d,match:c}))}),o}f(findColorIssues,"findColorIssues");function findingFingerprint(e,t,s){return crypto.createHash("sha1").update(`${e}\0${t??""}\0${s.replace(/\s+/g," ").trim()}`).digest("hex").slice(0,16)}f(findingFingerprint,"findingFingerprint");var rule=f((e,t,s,n)=>({id:e,severity:t,guide:s,title:n}),"rule"),DOCTOR_RULES=[rule("TP-MAN-001","error","manifest.md","tego.plugin.json is valid"),rule("TP-MAN-002","error","host-api-versions.md","hostApiRange admits every host capability the code uses"),rule("TP-PKG-001","error","manifest.md","SDK packages come from npm aliases or workspace links, never a plain @tego/* name"),rule("TP-PKG-002","warning","testing.md","package.json has the lint, type-check, test and build gates"),rule("TP-PKG-003","error","manifest.md","No imports of @tego/shared or host app code; only published packages"),rule("TP-CLAW-002","warning","claw.md","Each claw skill's frontmatter name matches its folder"),rule("TP-DESK-001","error","desktop-web.md","No eval or new Function (the desktop CSP refuses them)"),rule("TP-DESK-002","error","desktop-web.md","No addresses built from location.origin (tauri://localhost on desktop)"),rule("TP-DESK-003","error","desktop-web.md","External links open with ctx.openExternal, not window.open"),rule("TP-DESK-004","error","desktop-web.md","Files are saved with ctx.download, not <a download> or a.download"),rule("TP-DESK-005","warning","desktop-web.md","No streamed responses or EventSource from the frontend (the desktop proxy buffers; use ctx.events)"),rule("TP-DESK-006","warning","desktop-web.md","Clipboard writes go through ctx.copyText"),rule("TP-WEB-001","error","desktop-web.md","The frontend calls no third-party URL (the web CSP blocks it); call the backend, which declares egress"),rule("TP-WEB-002","error","desktop-web.md","No remote or injected scripts; everything ships in the bundle"),rule("TP-WEB-003","error","frontend.md","The frontend calls its backend through ctx.api (createApiClient), never a raw fetch of a path"),rule("TP-WEB-004","error","desktop-web.md","No cookie-based auth (document.cookie, hono/cookie); ctx.api carries the credentials"),rule("TP-UI-001","error","ui.md","Colors come from token classes: no hex, rgb(), palette classes or dark: overrides"),rule("TP-UI-002","error","ui.md","No direct @radix-ui/* imports; use @tego/ui"),rule("TP-UI-003","error","ui.md","The plugin's root element carries its scope class (.<pluginId>) when it compiles its own Tailwind"),rule("TP-UI-004","error","frontend.md","The frontend has its own error boundary"),rule("TP-UI-005","warning","ui.md","Interface copy stays within budget: explanations \u2264 24 characters, any string \u2264 40 (zh-CN)"),rule("TP-I18N-001","error","i18n.md","All ten locales, with the same keys as zh-CN, and every nav label defined"),rule("TP-I18N-002","error","i18n.md","Every locale value keeps the {{placeholders}} of zh-CN and is not empty"),rule("TP-I18N-003","error","i18n.md","Every literal t('\u2026') key exists in zh-CN"),rule("TP-I18N-004","warning","i18n.md","Current terminology: \u6570\u5B57\u5458\u5DE5 / Employee, never \u6570\u5B57\u5206\u8EAB / digital avatar"),rule("TP-I18N-005","error","i18n.md","tegoAccess.* strings are merged by pack; the plugin locales do not copy them"),rule("TP-BE-001","error","backend.md","Identity comes from the verified ctx, never from the raw x-tego-plugin-* headers"),rule("TP-BE-002","error","access.md","No host role names in business code; ask the access engine"),rule("TP-BE-003","error","migrations.md","Migrations are <ordinal>_<name>.sql with unique ordinals"),rule("TP-BE-004","error","migrations.md","Committed migrations are never edited or deleted"),rule("TP-BE-005","warning","backend.md","Backend errors are logged through host.log, not console"),rule("TP-BE-006","error","backend.md","Configuration comes from host.config, never process.env"),rule("TP-BE-007","warning","backend.md","The backend app turns every error into JSON with pluginErrorHandler"),rule("TP-ACC-001","error","access.md","With an access catalog, a test asserts every route has a policy (assertRouteCoverage)"),rule("TP-ACC-002","error","access.md","With an access catalog, start() reconciles it (reconcile(host))"),rule("TP-TEST-001","warning","testing.md","The plugin has tests"),rule("TP-TEST-002","warning","testing.md","Each test file starts with a Business Cases list"),rule("TP-TPL-001","error","agent-templates.md","A template whose content changed raises its version (existing employees only see updates by version)"),rule("TP-TPL-002","warning","agent-templates.md","usageGuide.summary is at most 30 characters and quickPrompts lists 3\u20134 prompts")],BY_ID=new Map(DOCTOR_RULES.map(e=>[e.id,e]));function doctorRule(e){return BY_ID.get(e)}f(doctorRule,"doctorRule");var GUIDE_SCOPES={"manifest.md":["tego.plugin.json","package.json"],"host-api-versions.md":["tego.plugin.json"],"desktop-web.md":["<frontend>/src/**"],"frontend.md":["<frontend>/src/**"],"ui.md":["<frontend>/src/**/*.tsx","<frontend>/src/**/*.css"],"i18n.md":["<frontend>/i18n/**"],"backend.md":["<backend>/src/**"],"storage.md":["<backend>/src/**/storage*","<backend>/src/**/upload*","<backend>/src/**/files*"],"migrations.md":["<backend>/migrations/**","<backend>/src/**/schema*.ts","<backend>/drizzle.config.ts"],"access.md":["shared/access.json","<backend>/src/access*.ts","<backend>/src/**/access/**"],"claw.md":["<claw>/**"],"testing.md":["**/*.test.ts"],"agent-templates.md":["templates/**"]};function ruleCatalog(e={}){const t=f(n=>n.replace("<frontend>",e.frontend??"frontend").replace("<backend>",e.backend??"backend").replace("<claw>",e.claw??"claw"),"resolve2"),s=GUIDE_SCOPES;return{catalogVersion:1,rules:DOCTOR_RULES.map(n=>({...n,appliesTo:(s[n.guide]??[]).map(t),enforcedBy:"doctor"})),guides:Object.entries(s).map(([n,l])=>({file:n,appliesTo:l.map(t)}))}}f(ruleCatalog,"ruleCatalog");var DEFAULT_CHECK_STEPS=["doctor","lint","type-check","test"],LOCKFILES=[["pnpm-lock.yaml","pnpm"],["package-lock.json","npm"],["yarn.lock","yarn"],["bun.lockb","bun"],["bun.lock","bun"]],MAX_DETAIL=4e3,MAX_STEP_FINDINGS=50;async function detectPackageManager(e){let t=path.resolve(e);for(;;){for(const[n,l]of LOCKFILES)if(await promises.access(path.join(t,n)).then(()=>!0,()=>!1))return l;const s=path.dirname(t);if(s===t)return"npm";t=s}}f(detectPackageManager,"detectPackageManager");function run(e,t,s,n){return new Promise(l=>{const r=child_process.spawn(e,t,{cwd:s,signal:n,shell:process.platform==="win32",env:{...process.env,FORCE_COLOR:"0",NO_COLOR:"1",CI:"1"}});let a="";r.stdout.on("data",o=>a+=String(o)),r.stderr.on("data",o=>a+=String(o)),r.on("error",o=>l({code:null,output:`${a}
4
+ ${o.message}`})),r.on("close",o=>l({code:o,output:a}))})}f(run,"run");function stripAnsi(e){return e.replace(/\u001b\[[0-9;]*m/g,"")}f(stripAnsi,"stripAnsi");function toRelative(e,t){const s=path.isAbsolute(t)?t:path.resolve(e,t),n=path.relative(e,s);return(n.startsWith("..")?t:n).split(path.sep).join("/")}f(toRelative,"toRelative");var TS_PAREN=/^(.+?)\((\d+),\d+\):\s+error\s+(TS\d+):\s+(.*)$/,TS_COLON=/^(.+?):(\d+):\d+\s+-\s+error\s+(TS\d+):\s+(.*)$/;function parseTypeErrors(e,t){const s=[];for(const n of stripAnsi(e).split(`
5
+ `)){const l=n.trim();if(l.startsWith(">"))continue;const r=TS_PAREN.exec(l)??TS_COLON.exec(l);if(!r)continue;const a=toRelative(t,r[1]);if(s.push({source:"type-check",severity:"error",rule:r[3],file:a,line:Number(r[2]),message:r[4],fingerprint:findingFingerprint(r[3],a,r[4])}),s.length>=MAX_STEP_FINDINGS)break}return s}f(parseTypeErrors,"parseTypeErrors");function parseEslintJson(e,t){const s=e.indexOf("[{"),n=e.lastIndexOf("}]");if(s<0||n<s)return e.trim().startsWith("[]")?[]:void 0;let l;try{l=JSON.parse(e.slice(s,n+2))}catch{return}const r=[];for(const a of l){const o=toRelative(t,a.filePath);for(const p of a.messages){const i=p.ruleId??"eslint";if(r.push({source:"lint",severity:p.severity>=2?"error":"warning",rule:i,file:o,...p.line?{line:p.line}:{},message:p.message,fingerprint:findingFingerprint(i,o,p.message)}),r.length>=MAX_STEP_FINDINGS)return r}}return r}f(parseEslintJson,"parseEslintJson");function parseTestFailures(e,t){const s=[],n=new Set;for(const l of stripAnsi(e).split(`
6
+ `)){const r=/^\s*(?:FAIL|×|✗)\s+(\S+\.(?:test|spec)\.[cm]?[jt]sx?)\s*(?:>\s*(.*))?$/.exec(l);if(!r)continue;const a=toRelative(t,r[1]),o=r[2]?.trim().replace(/^$/,"a test failed")??"a test failed",p=`${a}\0${o}`;if(!n.has(p)&&(n.add(p),s.push({source:"test",severity:"error",rule:"test",file:a,message:o,fingerprint:findingFingerprint("test",a,o)}),s.length>=MAX_STEP_FINDINGS))break}return s}f(parseTestFailures,"parseTestFailures");function tail(e){const t=stripAnsi(e).trim();return t.length>MAX_DETAIL?`\u2026${t.slice(-MAX_DETAIL)}`:t}f(tail,"tail");async function runChecks(e){const t=path.resolve(e.root),s=e.steps??DEFAULT_CHECK_STEPS,l=JSON.parse(await promises.readFile(path.join(t,"package.json"),"utf8").catch(()=>"{}")).scripts??{},r=await detectPackageManager(t),a=[],o=[];for(const p of s){const i=Date.now(),h=f((g,w)=>{a.push({step:p,status:g,durationMs:Date.now()-i,...w?{detail:w}:{}})},"done");if(e.signal?.aborted)break;if(p==="doctor"){const g=await runDoctor(e.files?{root:t,files:e.files}:{root:t});for(const w of g.findings)o.push({source:"doctor",...w});h(g.errors>0?"failed":"passed");continue}const d=l[p];if(!d){h("skipped",`package.json has no "${p}" script`);continue}if(p==="lint"&&/^\s*eslint\b/.test(d)){const g=e.files?.length?e.files.filter(b=>/\.[cm]?[jt]sx?$/.test(b)):["."];if(g.length===0){h("skipped","no lintable files among the given ones");continue}const k=await run(r,[...r==="npm"?["exec","--","eslint"]:["exec","eslint"],"--format","json","--no-warn-ignored",...g],t,e.signal),m=parseEslintJson(k.output,t);if(m){o.push(...m),h(m.some(b=>b.severity==="error")?"failed":"passed");continue}if(k.code===0){h("passed");continue}o.push({source:"lint",severity:"error",rule:"lint",message:tail(k.output),fingerprint:findingFingerprint("lint",void 0,"lint failed")}),h("failed",tail(k.output));continue}const c=await run(r,["run",p],t,e.signal);if(c.code===0){h("passed");continue}const u=p==="type-check"?parseTypeErrors(c.output,t):p==="test"?parseTestFailures(c.output,t):[];o.push(...u.length>0?u:[{source:p,severity:"error",rule:p,message:tail(c.output),fingerprint:findingFingerprint(p,void 0,`${p} failed`)}]),h("failed",tail(c.output))}return{passed:!a.some(p=>p.status==="failed"),steps:a,findings:o}}f(runChecks,"runChecks");function formatCheckReport(e){const t=e.steps.map(s=>`${s.status==="passed"?"\u2713":s.status==="failed"?"\u2716":"\u2013"} ${s.step}${s.status==="skipped"&&s.detail?` (${s.detail})`:""}`);for(const s of e.findings){const n=s.file?`${s.file}${s.line?`:${s.line}`:""} `:"";t.push(` ${s.severity==="error"?"\u2716":"\u26A0"} [${s.source}] ${s.rule??""} ${n}${s.message.split(`
7
+ `)[0]}`)}return t.push(e.passed?"\u2705 check: passed":"\u2716 check: failed"),t.join(`
8
+ `)}f(formatCheckReport,"formatCheckReport");var VERSIONED_CAPABILITIES=[{pattern:/\.openExternal\s*\(/,since:"1.37.0",name:"ctx.openExternal"},{pattern:/\.copyText\s*\(/,since:"1.37.0",name:"ctx.copyText"},{pattern:/\b(?:ctx|host|context)\.download\s*\(/,since:"1.37.0",name:"ctx.download"},{pattern:/\b(?:ctx|host|context)\.platform\b/,since:"1.37.0",name:"ctx.platform"},{pattern:/\b(?:ctx|context)\.events\b/,since:"1.3.0",name:"ctx.events"},{pattern:/\bhost\.directory\b/,since:"1.5.0",name:"host.directory"},{pattern:/\b(?:ctx|context)\.ai\b/,since:"1.9.0",name:"ctx.ai"},{pattern:/\.hostUrl\s*\(/,since:"1.12.0",name:"ctx.hostUrl"},{pattern:/\.uploadPath\s*\(/,since:"1.12.0",name:"storage.uploadPath"},{pattern:/\.collapseSidebar\s*\(/,since:"1.14.0",name:"ctx.layout.collapseSidebar"},{pattern:/\.managedPath\s*\(/,since:"1.15.0",name:"storage.managedPath"},{pattern:/\bstorage\.copy\s*\(/,since:"1.19.0",name:"storage.copy"},{pattern:/\b(?:ctx|context)\.office\b/,since:"1.21.0",name:"ctx.office"},{pattern:/\bhost\.tenantId\b/,since:"1.22.0",name:"host.tenantId"},{pattern:/\bproactive\.emit\s*\(/,since:"1.26.0",name:"proactive.emit"},{pattern:/\bmail\.send\s*\(/,since:"1.29.0",name:"mail.send"},{pattern:/\battention\.request\s*\(/,since:"1.34.0",name:"attention.request"},{pattern:/\bDecisionCardSurface\b/,since:"1.38.0",name:"DecisionCardSurface"},{pattern:/\bsources\.latestUserMessage\s*\(/,since:"1.39.0",name:"sources.latestUserMessage"}],SOURCE_EXTENSIONS=new Set([".ts",".tsx",".mts",".cts",".js",".jsx",".mjs",".css"]),SKIP_DIRS=new Set(["node_modules","dist","build",".tego-dev",".turbo",".drizzle","__fixtures__","coverage"]),TEST_FILE=/\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|[\\/])test[\\/]|(?:^|[\\/])test-[\w-]+\.[cm]?[jt]s$|-double\.[cm]?[jt]s$|\.d\.ts$/,IGNORE_MARKER=/tego-doctor-ignore\s+([A-Z0-9-]+(?:\s*,\s*[A-Z0-9-]+)*)/,PLACEHOLDER=/\{\{\s*([\w.-]+)\s*\}\}/g,REQUIRED_SCRIPTS=["lint","type-check","test","build"];function toPosix(e){return e.split(path.sep).join("/")}f(toPosix,"toPosix");async function walk(e,t,s){let n;try{n=await promises.readdir(path.join(e,t),{withFileTypes:!0})}catch{return}for(const l of n){const r=t?`${t}/${l.name}`:l.name;l.isDirectory()?!SKIP_DIRS.has(l.name)&&!l.name.startsWith(".")&&await walk(e,r,s):SOURCE_EXTENSIONS.has(path.extname(l.name))&&s.push(r)}}f(walk,"walk");function folderReader(e){return{async list(t){try{return(await promises.readdir(path.join(e,t),{withFileTypes:!0})).filter(n=>n.isFile()).map(n=>n.name)}catch{return[]}},readText:f(t=>promises.readFile(path.join(e,t),"utf8").catch(()=>null),"readText")}}f(folderReader,"folderReader");function runGit(e,t){return new Promise(s=>{child_process.execFile("git",["-C",e,...t],{maxBuffer:8*1024*1024},(n,l)=>s(n?null:l))})}f(runGit,"runGit");function ignoredLines(e){const t=new Map;return e.split(`
9
+ `).forEach((s,n)=>{const l=IGNORE_MARKER.exec(s);if(!l?.[1])return;const r=new Set(l[1].split(",").map(o=>o.trim())),a=f(o=>t.set(o,new Set([...t.get(o)??[],...r])),"add");a(n),/^\s*(?:\/\/|\/\*|\*|\{\/\*)/.test(s)&&a(n+1)}),t}f(ignoredLines,"ignoredLines");var Findings=class{static{f(this,"Findings")}list=[];add(e,t,s={}){const{severity:n,guide:l}=doctorRule(e),{evidence:r,...a}=s;this.list.push({rule:e,severity:n,message:t,guide:l,...a,fingerprint:findingFingerprint(e,a.file,r??t)})}};function eachMatch(e,t,s,n){const l=ignoredLines(e.text);e.code.split(`
10
+ `).forEach((r,a)=>{if(l.get(a)?.has(t))return;const o=s.exec(r);o&&n(a+1,o[0],r.trim())})}f(eachMatch,"eachMatch");function partOf(e,t){const s=f(n=>!!n&&(e===n||e.startsWith(`${n}/`)),"inside");return s(t.frontend?.dir)||s(t.publicFrontend?.dir)?"frontend":s(t.backend?.dir)?"backend":s(t.claw?.dir)?"claw":s("shared")?"shared":"other"}f(partOf,"partOf");async function readJson(e){try{return JSON.parse(await promises.readFile(e,"utf8"))}catch{return}}f(readJson,"readJson");function checkPackageJson(e,t){if(!t)return;for(const l of["dependencies","devDependencies","peerDependencies"]){const r=t[l];if(!(!r||typeof r!="object"))for(const[a,o]of Object.entries(r))!a.startsWith("@tego/")||typeof o!="string"||/^(?:npm:@zhama\/tego-|workspace:|file:|link:|portal:)/.test(o)||e.add("TP-PKG-001",`${l}.${a} is "${o}": on npm @tego/* belongs to another project. Install it as "npm:@zhama/tego-${a.slice(6)}@^<sdk version>".`,{file:"package.json"})}const s=t.scripts??{},n=REQUIRED_SCRIPTS.filter(l=>typeof s[l]!="string");n.length>0&&e.add("TP-PKG-002",`package.json has no ${n.map(l=>`"${l}"`).join(", ")} script; the platform's gates are lint, type-check, test and build.`,{file:"package.json"})}f(checkPackageJson,"checkPackageJson");async function checkLocales(e,t,s,n,l){const r=n.frontend;if(!r)return;const{bundles:a,errors:o}=await chunk45Y4CR4R_cjs.verifyFrontendLocales(folderReader(t),r);for(const m of o)e.add("TP-I18N-001",m,{file:r.i18nDirPath});const p=a.get("zh-CN");if(!p)return;const i=f((m,b="",T=new Map)=>{if(typeof m!="object"||m===null||Array.isArray(m))return T.set(b,m),T;for(const[v,P]of Object.entries(m))i(P,b?`${b}.${v}`:v,T);return T},"leaves"),h=f(m=>new Set(typeof m=="string"?[...m.matchAll(PLACEHOLDER)].map(b=>b[1]):[]),"placeholders"),d=i(p),c=`${r.i18nDirPath}/zh-CN.json`;[...d.keys()].some(m=>m==="tegoAccess"||m.startsWith("tegoAccess."))&&e.add("TP-I18N-005","zh-CN.json contains tegoAccess.* strings; pack merges the access UI strings, remove the copy from every locale.",{file:c,evidence:"tegoAccess"});let u=0;for(const[m,b]of d){if(typeof b!="string"||u>=10)continue;const T=m.slice(m.lastIndexOf(".")+1);if(MODEL_TEXT_KEY.test(T))continue;const v=EXPLANATION_KEY.test(T)?EXPLANATION_LIMIT:ANY_STRING_LIMIT,P=copyLength(b);P>v&&(u++,e.add("TP-UI-005",`"${m}" is ${P} characters (budget ${v}); shorten it, or move the explanation into an InfoTip.`,{file:c,evidence:m}))}for(const[m,b]of a){const T=`${r.i18nDirPath}/${m}.json`;let v=0;for(const[P,y]of i(b)){if(v>=10)break;if(typeof y=="string"&&y.trim()===""){e.add("TP-I18N-002",`"${P}" is empty in ${m}.json.`,{file:T}),v++;continue}if(m==="zh-CN"||!d.has(P))continue;const E=h(d.get(P)),x=h(y),S=[...E].filter($=>!x.has($)),A=[...x].filter($=>!E.has($));(S.length||A.length)&&(e.add("TP-I18N-002",`"${P}" in ${m}.json has placeholders {${[...x].join(", ")}} but zh-CN has {${[...E].join(", ")}}.`,{file:T}),v++)}for(const[P,y]of i(b))if(typeof y=="string"&&/数字分身|digital avatar/i.test(y)){e.add("TP-I18N-004",`"${P}" in ${m}.json says "${/数字分身/.test(y)?"\u6570\u5B57\u5206\u8EAB":"digital avatar"}"; the platform calls them \u6570\u5B57\u5458\u5DE5 / Employee ({{avatarTerm}}).`,{file:T});break}}const g=chunk45Y4CR4R_cjs.flattenLocaleKeys(p),w=new Set(Object.keys(p)),k=f(m=>g.has(m)||["_one","_other","_zero","_few","_many","_two"].some(b=>g.has(m+b))||[...g].some(b=>b.startsWith(`${m}.`)),"exists");for(const m of l){if(m.part!=="frontend"||m.test)continue;const b=ignoredLines(m.text);m.code.split(`
11
+ `).forEach((T,v)=>{if(!b.get(v)?.has("TP-I18N-003"))for(const P of T.matchAll(/\bt\(\s*['"]([A-Za-z0-9_.-]+)['"]/g)){const y=P[1],E=y.split(".")[0];!y.includes(".")||!w.has(E)||k(y)||e.add("TP-I18N-003",`t('${y}') has no entry in zh-CN.json (namespace ${chunkN4SXZZPY_cjs.pluginI18nNamespace(s.id)}).`,{file:m.path,line:v+1})}})}}f(checkLocales,"checkLocales");function copyLength(e){return[...e.replace(/\{\{[^}]*\}\}/g,"X")].length}f(copyLength,"copyLength");var EXPLANATION_KEY=/(help|hint|description|desc|subtitle|intro)$/i,MODEL_TEXT_KEY=/prompt/i,EXPLANATION_LIMIT=24,ANY_STRING_LIMIT=40;async function checkTemplates(e,t,s){const n=s.agentTemplates??[];if(n.length===0)return;const l=(await runGit(t,["rev-parse","--show-prefix"]))?.trim()??"";for(const r of n){const a=r.entry.split("/").slice(0,-1).join("/"),o=`${a}/template.config.json`,p=await readJson(path.join(t,o));if(!p)continue;const i=p.content??{},h=i.usageGuide;typeof h?.summary=="string"&&copyLength(h.summary)>30&&e.add("TP-TPL-002",`${r.key}: usageGuide.summary is ${copyLength(h.summary)} characters; the landing page shows one line, keep it within 30.`,{file:o,evidence:"summary"});const d=i.quickPrompts;Array.isArray(d)&&(d.length<3||d.length>4)&&e.add("TP-TPL-002",`${r.key}: quickPrompts has ${d.length} prompt(s); offer 3\u20134 ready-to-click prompts.`,{file:o,evidence:"quickPrompts"});const u=(await runGit(t,["status","--porcelain","--untracked-files=all","--",a])??"").split(`
12
+ `).map(k=>k.slice(3).trim()).filter(k=>k&&!k.endsWith("/template.json"));if(u.length===0)continue;const g=await runGit(t,["show",`HEAD:${l}${o}`]);if(!g)continue;let w;try{w=JSON.parse(g).version}catch{continue}w!==void 0&&w===p.version&&e.add("TP-TPL-001",`${r.key} changed (${u.length} file(s)) but its version is still ${String(p.version)}; raise version in ${o} so existing employees see the update.`,{file:o,evidence:"version"})}}f(checkTemplates,"checkTemplates");async function checkMigrations(e,t,s){const n=s.backend;if(!n)return;const{errors:l}=await chunk45Y4CR4R_cjs.verifyMigrationNames(folderReader(t),n.migrationsDirPath);for(const o of l)e.add("TP-BE-003",o,{file:n.migrationsDirPath});const r=await runGit(t,["diff","--name-status","HEAD","--",n.migrationsDirPath]);if(!r)return;const a=(await runGit(t,["rev-parse","--show-prefix"]))?.trim()??"";for(const o of r.split(`
13
+ `)){const[p,i]=o.split(" ");if(!i||p!=="M"&&p!=="D")continue;const h=i.startsWith(a)?i.slice(a.length):i;e.add("TP-BE-004",`${h} is a committed migration and was ${p==="M"?"edited":"deleted"}; an install may have applied it already. Revert it and add a new migration instead.`,{file:h})}}f(checkMigrations,"checkMigrations");async function checkClaw(e,t,s){const n=s.claw;if(!n)return;const l=path.join(t,n.dir,"skills");let r=[];try{r=(await promises.readdir(l,{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>a.name)}catch{return}for(const a of r){const o=await promises.readFile(path.join(l,a,"SKILL.md"),"utf8").catch(()=>null),p=`${n.dir}/skills/${a}/SKILL.md`,i=o?/^---[\s\S]*?^name:\s*["']?([^"'\n]+)["']?\s*$/m.exec(o)?.[1]?.trim():void 0;i!==a&&e.add("TP-CLAW-002",o?`${p} declares name "${i??"(none)"}"; it must equal the folder name "${a}".`:`${n.dir}/skills/${a} has no SKILL.md.`,{file:p})}}f(checkClaw,"checkClaw");function checkSources(e,t,s,n,l,r){const a=t.hostApiRange&&chunkIOZFHJ4F_cjs.isValidHostApiRange(t.hostApiRange)?chunkIOZFHJ4F_cjs.hostApiRangeMinimum(t.hostApiRange):void 0,o=f(i=>{if(!a)return!1;const[h,d,c]=a.split(".").map(Number),[u,g,w]=i.split(".").map(Number);return h!==u?h<u:d!==g?d<g:c<w},"below");for(const i of n){if(!l(i)||i.test)continue;const h=f((d,c)=>({file:i.path,line:d,evidence:c}),"at");if(i.part==="frontend"){if(i.path.endsWith(".css")||i.path.endsWith(".tsx")||i.path.endsWith(".ts")){const d=ignoredLines(i.text);for(const c of findColorIssues(i.text,path.extname(i.path)))d.get(c.line-1)?.has("TP-UI-001")||e.add("TP-UI-001",c.kind==="dark"?`"${c.match}" is a hand-written dark override; tokens already carry both themes.`:`"${c.match}" is a literal color; use a token class (bg-card, text-muted-foreground, bg-primary\u2026).`,h(c.line,c.match))}eachMatch(i,"TP-DESK-001",/\beval\s*\(|\bnew\s+Function\s*\(/,(d,c,u)=>e.add("TP-DESK-001","eval / new Function: the desktop app's CSP (script-src 'self') refuses code compiled from strings.",h(d,u))),eachMatch(i,"TP-DESK-002",/\b(?:window\.|document\.)?location\.origin\b/,(d,c,u)=>e.add("TP-DESK-002","location.origin is tauri://localhost in the desktop app; build host addresses with ctx.hostUrl(path) and share links from ctx.publicPagesUrl.",h(d,u))),eachMatch(i,"TP-DESK-003",/\bwindow\.open\s*\(/,(d,c,u)=>e.add("TP-DESK-003","window.open does not reliably open links in the desktop app; use ctx.openExternal(url) (hostApiRange ^1.37.0), or ctx.download for a file.",h(d,u))),eachMatch(i,"TP-DESK-004",/\.download\s*=(?!=)|<a\b[^>]*\sdownload(?:[\s=>]|$)/,(d,c,u)=>e.add("TP-DESK-004","An <a download> save is not handled by the desktop app; use ctx.download({ path | blob, fileName }) (hostApiRange ^1.37.0).",h(d,u))),eachMatch(i,"TP-DESK-005",/\bnew\s+EventSource\s*\(|\.body\s*(?:\?\.|!\.|\.)\s*getReader\s*\(/,(d,c,u)=>e.add("TP-DESK-005","The desktop app buffers ctx.api responses whole (and caps them at 100 MB), so streams arrive at the end; subscribe to ctx.events for pushed updates.",h(d,u))),eachMatch(i,"TP-DESK-006",/\bnavigator\.clipboard\./,(d,c,u)=>e.add("TP-DESK-006","navigator.clipboard is not available in every shell; use ctx.copyText(text) (hostApiRange ^1.37.0).",h(d,u))),eachMatch(i,"TP-WEB-001",/\bfetch\s*\(\s*['"`]https?:\/\//,(d,c,u)=>e.add("TP-WEB-001","The web app's CSP allows connections to the platform only; call your backend through ctx.api and let it reach the service (declare it in parts.backend.egress).",h(d,u))),eachMatch(i,"TP-WEB-002",/\bimport\s*\(\s*['"`]https?:|createElement\(\s*['"]script['"]\s*\)|<script\b[^>]*\bsrc=/,(d,c,u)=>e.add("TP-WEB-002","Remote or injected scripts are refused by both shells' CSP; bundle the code.",h(d,u))),eachMatch(i,"TP-UI-002",/from\s+['"]@radix-ui\//,(d,c,u)=>e.add("TP-UI-002","Import the @tego/ui wrapper instead of @radix-ui/* (the shell shares @tego/ui; a second Radix copy breaks focus and portals).",h(d,u))),eachMatch(i,"TP-WEB-003",/\bfetch\s*\(\s*['"`](?!https?:|data:|blob:)/,(d,c,u)=>e.add("TP-WEB-003","Call your backend through ctx.api (createApiClient from @tego/plugin-sdk/react): a raw fetch misses the base path and the credentials in the desktop app, and loses the error body.",h(d,u))),eachMatch(i,"TP-WEB-004",/\bdocument\.cookie\b/,(d,c,u)=>e.add("TP-WEB-004","Cookies do not carry the session in the desktop app; ctx.api sends the credentials in both shells.",h(d,u)))}if(i.part==="backend"&&(eachMatch(i,"TP-BE-001",/\bheaders?\s*(?:\.get)?\(\s*['"]x-tego-plugin-(?:user-id|tenant-id|roles|avatar-id|delegation)['"]|\bheaders\s*\[\s*['"]x-tego-plugin-(?:user-id|tenant-id|roles|avatar-id|delegation)['"]/i,(d,c,u)=>e.add("TP-BE-001","Take the caller from the verified ctx (ctx.userId, ctx.tenantId, ctx.roles) that serveBackend hands you; the raw x-tego-plugin-* headers are unverified here.",h(d,u))),eachMatch(i,"TP-BE-005",/\bconsole\.(?:error|warn)\s*\(/,(d,c,u)=>e.add("TP-BE-005","Log through host.log so the message reaches the platform's logs; console output from a plugin container is not collected.",h(d,u))),eachMatch(i,"TP-BE-006",/\bprocess\.env\b(?!\.NODE_ENV\b|\[\s*['"]NODE_ENV['"]\s*\])/,(d,c,u)=>e.add("TP-BE-006","Read configuration from host.config.current() (declared in the manifest's config schema); the platform does not pass a plugin its own environment variables.",h(d,u))),eachMatch(i,"TP-WEB-004",/from\s+['"]hono\/cookie['"]/,(d,c,u)=>e.add("TP-WEB-004","The caller is the verified ctx that serveBackend hands you; cookies are not how plugins authenticate.",h(d,u)))),i.part!=="other"){eachMatch(i,"TP-PKG-003",/(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"](?:@tego\/shared(?:\/|['"])|@tego\/chat(?!\/plugin-surface)(?:\/|['"]))/,(c,u,g)=>e.add("TP-PKG-003","Plugins only import published packages (@tego/plugin-sdk, @tego/ui, @tego/chat/plugin-surface, \u2026); @tego/shared and host app code do not exist outside the platform.",h(c,g)));const d=ignoredLines(i.text);i.code.split(`
14
+ `).forEach((c,u)=>{if(!d.get(u)?.has("TP-PKG-003"))for(const g of c.matchAll(/(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"](\.\.?\/[^'"]*)['"]/g)){const w=path.posix.normalize(path.posix.join(path.posix.dirname(i.path),g[1]));w.startsWith("../")&&e.add("TP-PKG-003",`"${g[1]}" resolves outside the plugin folder (${w}); a packed plugin only has its own files and published packages.`,h(u+1,c.trim()))}});for(const c of VERSIONED_CAPABILITIES){if(!o(c.since))continue;const u=c.name.split(".").pop();new RegExp(`\\.${u}\\s*(?:\\)|\\?(?!\\.)|&&)`).test(i.code)||eachMatch(i,"TP-MAN-002",c.pattern,(g,w,k)=>e.add("TP-MAN-002",`${c.name} needs host API ${c.since}, but hostApiRange is "${t.hostApiRange}"; raise it to ^${c.since}.`,h(g,k)))}}i.part!=="other"&&eachMatch(i,"TP-BE-002",/\b(?:roles|roleNames)\s*\??\.\s*(?:includes|indexOf)\(\s*['"`](?:admin|owner|super_?admin|tenant_?admin|member|manager)['"`]/i,(d,c,u)=>e.add("TP-BE-002","Do not decide by host role names; declare permissions in the access catalog (hostRoleBindings) and ask the engine (authorize / useAccess).",h(d,u)))}const p=n.filter(i=>i.part==="frontend"&&!i.test);if(s.frontend&&p.length>0&&(p.some(i=>/getDerivedStateFromError|componentDidCatch|PluginErrorBoundary/.test(i.code))||e.add("TP-UI-004","No error boundary in the frontend: the shell's boundary cannot see into your React root, so a render error leaves a blank page. Wrap the root in PluginErrorBoundary (@tego/plugin-sdk/react) or your own.",{file:s.frontend.dir}),r?.includes("tegoPluginTailwind"))){const i=new RegExp(`(?:className|class)=\\{?\\s*(?:cn\\()?\\s*['"\`][^'"\`]*(?<![\\w-])${t.id.replace(/[.]/g,"\\.")}(?![\\w-])`);p.some(h=>i.test(h.code))||e.add("TP-UI-003",`tegoPluginTailwind scopes every utility under .${t.id}, but no element carries className="${t.id}": none of the plugin's own classes will apply. Put it on the root element (and on portalled content, or use tegoPluginScopeRoot).`,{file:s.frontend.dir})}}f(checkSources,"checkSources");function checkAccessAndTests(e,t,s,n,l=()=>!0){const r=n.filter(o=>o.test);for(const o of r)!l(o)||!/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(o.path)||/Business Cases/.test(o.text)||e.add("TP-TEST-002",`${o.path} has no Business Cases list: start it with the cases it covers (one line each, an id per case) and begin each it() title with its id.`,{file:o.path,line:1,evidence:"no business cases"});if(s.backend){const o=n.filter(p=>p.part==="backend"&&!p.test);o.some(p=>/\bnew\s+Hono\b/.test(p.code))&&!o.some(p=>/\.onError\s*\(/.test(p.code))&&e.add("TP-BE-007","The backend app has no error handler (app.onError(pluginErrorHandler())): an uncaught error becomes Hono's plain-text 500 instead of JSON the frontend can show.",{file:s.backend.dir})}if(r.length===0&&e.add("TP-TEST-001","The plugin has no *.test.ts files; backend routes and pure frontend logic need tests (createTestPluginHost for SQL)."),!t.access||!s.backend)return;r.some(o=>o.text.includes("assertRouteCoverage"))||e.add("TP-ACC-001","The manifest declares an access catalog but no test calls assertRouteCoverage(app): a route without a policy would be open to everyone who can open the plugin.",{file:s.backend.dir}),n.filter(o=>(o.part==="backend"||o.part==="shared")&&!o.test).some(o=>/\breconcile\s*\(/.test(o.code))||e.add("TP-ACC-002","The manifest declares an access catalog but start() never calls reconcile(host): built-in roles and catalog changes never reach the database.",{file:s.backend.dir})}f(checkAccessAndTests,"checkAccessAndTests");async function runDoctor(e){const t=e.root;await promises.stat(path.join(t,"tego.plugin.json")).catch(()=>{throw new Error(`No tego.plugin.json in ${t}; run doctor in the plugin folder.`)});const s=new Findings;let n;try{n=JSON.parse(await promises.readFile(path.join(t,"tego.plugin.json"),"utf8"))}catch(c){return s.add("TP-MAN-001",`tego.plugin.json is not valid JSON: ${c instanceof Error?c.message:String(c)}`,{file:"tego.plugin.json"}),summarize(s,void 0)}const l=chunkH6GXDFLP_cjs.parseTegoPluginManifest(n);if(!l.ok||!l.manifest){for(const c of l.errors)s.add("TP-MAN-001",c,{file:"tego.plugin.json"});return summarize(s,n.id)}const r=l.manifest,a=chunk2P6UEAPV_cjs.resolvePluginParts(r),o=[];await walk(t,"",o);const p=await Promise.all(o.map(async c=>{const u=await promises.readFile(path.join(t,c),"utf8");return{path:c,part:partOf(c,a),text:u,code:stripComments(u),test:TEST_FILE.test(c)}})),i=e.files?.map(c=>toPosix(path.relative(t,path.join(t,c)))),h=f(c=>!i||i.includes(c.path),"scanned"),d=a.frontend?await promises.readFile(path.join(t,a.frontend.dir,"vite.config.ts"),"utf8").catch(()=>null):null;return checkPackageJson(s,await readJson(path.join(t,"package.json"))),await checkLocales(s,t,r,a,p.filter(h)),await checkMigrations(s,t,a),await checkClaw(s,t,a),checkSources(s,r,a,p,h,d),checkAccessAndTests(s,r,a,p,h),await checkTemplates(s,t,r),summarize(s,r.id)}f(runDoctor,"runDoctor");function summarize(e,t){const s={errors:e.list.filter(r=>r.severity==="error").length,warnings:e.list.filter(r=>r.severity==="warning").length},n=new Map(DOCTOR_RULES.map((r,a)=>[r.id,a])),l=[...e.list].sort((r,a)=>(r.severity===a.severity?0:r.severity==="error"?-1:1)||(n.get(r.rule)??0)-(n.get(a.rule)??0)||(r.file??"").localeCompare(a.file??"")||(r.line??0)-(a.line??0));return{...t?{pluginId:t}:{},findings:l,...s}}f(summarize,"summarize");var UPLOAD_BLOCKING_RULES=["TP-MAN-001","TP-I18N-001","TP-BE-003"];function formatDoctorReport(e){const t=e.findings.map(s=>{const n=s.file?`${s.file}${s.line?`:${s.line}`:""} `:"";return`${s.severity==="error"?"\u2716":"\u26A0"} ${s.rule} ${n}${s.message}`});return t.push(e.findings.length===0?"\u2705 doctor: no problems":`${e.errors} error(s), ${e.warnings} warning(s). Each rule is explained in node_modules/@tego/plugin-sdk/agent/RULES.md.`),t.join(`
15
+ `)}f(formatDoctorReport,"formatDoctorReport"),exports.DEFAULT_CHECK_STEPS=DEFAULT_CHECK_STEPS,exports.DOCTOR_RULES=DOCTOR_RULES,exports.GUIDE_SCOPES=GUIDE_SCOPES,exports.UPLOAD_BLOCKING_RULES=UPLOAD_BLOCKING_RULES,exports.detectPackageManager=detectPackageManager,exports.doctorRule=doctorRule,exports.findColorIssues=findColorIssues,exports.findingFingerprint=findingFingerprint,exports.formatCheckReport=formatCheckReport,exports.formatDoctorReport=formatDoctorReport,exports.parseEslintJson=parseEslintJson,exports.parseTestFailures=parseTestFailures,exports.parseTypeErrors=parseTypeErrors,exports.ruleCatalog=ruleCatalog,exports.runChecks=runChecks,exports.runDoctor=runDoctor;
package/dist/doctor.d.cts CHANGED
@@ -14,9 +14,52 @@ interface DoctorRule {
14
14
  /** Guide file under `agent/guides/` that explains it. */
15
15
  guide: string;
16
16
  }
17
- declare const DOCTOR_RULES: readonly [DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule];
17
+ declare const DOCTOR_RULES: readonly [DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule, DoctorRule];
18
18
  type DoctorRuleId = (typeof DOCTOR_RULES)[number]['id'];
19
19
  declare function doctorRule(id: DoctorRuleId): DoctorRule;
20
+ /**
21
+ * Where each guide applies, as globs relative to the plugin root. `<frontend>`,
22
+ * `<backend>` and `<claw>` stand for the part directories the manifest declares
23
+ * (`frontend`, `backend`, `claw` by default). Tools use this to hand an agent
24
+ * the guide when it first touches matching files.
25
+ */
26
+ declare const GUIDE_SCOPES: {
27
+ readonly 'manifest.md': readonly ["tego.plugin.json", "package.json"];
28
+ readonly 'host-api-versions.md': readonly ["tego.plugin.json"];
29
+ readonly 'desktop-web.md': readonly ["<frontend>/src/**"];
30
+ readonly 'frontend.md': readonly ["<frontend>/src/**"];
31
+ readonly 'ui.md': readonly ["<frontend>/src/**/*.tsx", "<frontend>/src/**/*.css"];
32
+ readonly 'i18n.md': readonly ["<frontend>/i18n/**"];
33
+ readonly 'backend.md': readonly ["<backend>/src/**"];
34
+ readonly 'storage.md': readonly ["<backend>/src/**/storage*", "<backend>/src/**/upload*", "<backend>/src/**/files*"];
35
+ readonly 'migrations.md': readonly ["<backend>/migrations/**", "<backend>/src/**/schema*.ts", "<backend>/drizzle.config.ts"];
36
+ readonly 'access.md': readonly ["shared/access.json", "<backend>/src/access*.ts", "<backend>/src/**/access/**"];
37
+ readonly 'claw.md': readonly ["<claw>/**"];
38
+ readonly 'testing.md': readonly ["**/*.test.ts"];
39
+ readonly 'agent-templates.md': readonly ["templates/**"];
40
+ };
41
+ interface RuleCatalog {
42
+ /** Version of this file's shape. */
43
+ catalogVersion: 1;
44
+ rules: (DoctorRule & {
45
+ appliesTo: readonly string[];
46
+ enforcedBy: 'doctor';
47
+ })[];
48
+ guides: {
49
+ file: string;
50
+ appliesTo: readonly string[];
51
+ }[];
52
+ }
53
+ /**
54
+ * The rules as data, for tools: every doctor rule with where it applies, and
55
+ * every guide with its scope. Written to `agent/rules.json` at build time and
56
+ * printed by `tego-plugin rules --json` (with the part directories resolved).
57
+ */
58
+ declare function ruleCatalog(parts?: {
59
+ frontend?: string;
60
+ backend?: string;
61
+ claw?: string;
62
+ }): RuleCatalog;
20
63
 
21
64
  interface ColorIssue {
22
65
  line: number;
@@ -26,6 +69,54 @@ interface ColorIssue {
26
69
  /** Literal colors and hand-written dark overrides in one file; `ext` like `.tsx` or `.css`. */
27
70
  declare function findColorIssues(source: string, ext: string): ColorIssue[];
28
71
 
72
+ /** A stable id for a finding: rule, file and the offending text, never the line number. */
73
+ declare function findingFingerprint(rule: string, file: string | undefined, evidence: string): string;
74
+
75
+ type CheckStep = 'doctor' | 'lint' | 'type-check' | 'test' | 'build';
76
+ declare const DEFAULT_CHECK_STEPS: readonly CheckStep[];
77
+ interface CheckFinding {
78
+ source: CheckStep;
79
+ severity: 'error' | 'warning';
80
+ message: string;
81
+ file?: string;
82
+ line?: number;
83
+ rule?: string;
84
+ guide?: string;
85
+ fingerprint: string;
86
+ }
87
+ interface CheckStepResult {
88
+ step: CheckStep;
89
+ status: 'passed' | 'failed' | 'skipped';
90
+ durationMs: number;
91
+ /** Why it was skipped, or the end of the output when it failed. */
92
+ detail?: string;
93
+ }
94
+ interface CheckReport {
95
+ passed: boolean;
96
+ steps: CheckStepResult[];
97
+ findings: CheckFinding[];
98
+ }
99
+ interface CheckOptions {
100
+ root: string;
101
+ steps?: readonly CheckStep[];
102
+ /** Limit doctor's source checks and lint to these files (relative to root). */
103
+ files?: string[];
104
+ signal?: AbortSignal;
105
+ }
106
+ type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
107
+ /** The package manager of the nearest lockfile at or above `root`; npm when there is none. */
108
+ declare function detectPackageManager(root: string): Promise<PackageManager>;
109
+ /** Type errors in tsc / tsgo output (either format). */
110
+ declare function parseTypeErrors(output: string, root: string): CheckFinding[];
111
+ /** Problems in ESLint's JSON output (`--format json`). */
112
+ declare function parseEslintJson(output: string, root: string): CheckFinding[] | undefined;
113
+ /** Failed test files in vitest output (`FAIL path > suite > name`). */
114
+ declare function parseTestFailures(output: string, root: string): CheckFinding[];
115
+ /** Runs the gates in order; never throws for a failing gate. */
116
+ declare function runChecks(options: CheckOptions): Promise<CheckReport>;
117
+ /** One line per step and per finding, for the terminal. */
118
+ declare function formatCheckReport(report: CheckReport): string;
119
+
29
120
  interface DoctorFinding {
30
121
  rule: DoctorRuleId;
31
122
  severity: DoctorSeverity;
@@ -35,6 +126,12 @@ interface DoctorFinding {
35
126
  line?: number;
36
127
  /** `agent/guides/<file>` in the installed SDK. */
37
128
  guide: string;
129
+ /**
130
+ * Identifies the problem independently of its line number (rule, file and
131
+ * the offending text), so tools can tell a problem a change introduced from
132
+ * one that was already there.
133
+ */
134
+ fingerprint: string;
38
135
  }
39
136
  interface DoctorReport {
40
137
  pluginId?: string;
@@ -59,4 +156,4 @@ declare const UPLOAD_BLOCKING_RULES: readonly DoctorRuleId[];
59
156
  /** Human-readable report, one finding per line, for the terminal. */
60
157
  declare function formatDoctorReport(report: DoctorReport): string;
61
158
 
62
- export { DOCTOR_RULES, type DoctorFinding, type DoctorOptions, type DoctorReport, type DoctorRule, type DoctorRuleId, UPLOAD_BLOCKING_RULES, doctorRule, findColorIssues, formatDoctorReport, runDoctor };
159
+ export { type CheckFinding, type CheckOptions, type CheckReport, type CheckStep, type CheckStepResult, DEFAULT_CHECK_STEPS, DOCTOR_RULES, type DoctorFinding, type DoctorOptions, type DoctorReport, type DoctorRule, type DoctorRuleId, GUIDE_SCOPES, type RuleCatalog, UPLOAD_BLOCKING_RULES, detectPackageManager, doctorRule, findColorIssues, findingFingerprint, formatCheckReport, formatDoctorReport, parseEslintJson, parseTestFailures, parseTypeErrors, ruleCatalog, runChecks, runDoctor };