squad-kit 1.3.13 → 1.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +13 -13
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -208,7 +208,7 @@ ${r.error??""}`,by2=SPEND.test(said2)?"spend":TURNS.test(said2)?"turns":r.capped
|
|
|
208
208
|
`):"";for(let b of claudeBlocked(out22))opts.onBlocked(b)}}else if(event.type==="result"){let isError=!!event.is_error||event.subtype!=="success",usage=claudeUsage(event,costBefore),errors=Array.isArray(event.errors)?event.errors.filter(e=>typeof e=="string"):[],said2=typeof event.result=="string"?event.result:[String(event.subtype),...errors].join(": "),capped=event.subtype==="error_max_turns"?"turns":event.subtype==="error_max_budget_usd"?"spend":null;result={ok:!isError,sessionId:event.session_id??sessionId,text:typeof event.result=="string"?event.result:lastText,structured:event.structured_output??void 0,...isError?{error:plainError(said2),detail:said2.trim().slice(-2e3)}:{},...usage?{usage}:{},...capped?{capped:{by:capped}}:{}}}},saveError,saving=line3=>{if(!saveError)try{handle(line3)}catch(err2){saveError=asError(err2),result={ok:!1,sessionId,text:lastText,error:COULD_NOT_SAVE,saveError},child.kill("SIGTERM")}};child.stdout.on("data",chunk=>{buffer+=chunk.toString("utf8");let nl;for(;(nl=buffer.indexOf(`
|
|
209
209
|
`))>=0;){let line3=buffer.slice(0,nl);buffer=buffer.slice(nl+1),saving(line3)}}),child.stderr.on("data",chunk=>{stderr=(stderr+chunk.toString("utf8")).slice(-4e3)}),child.on("error",err2=>{clearTimeout(timer),resolve2({ok:!1,sessionId,text:"",error:`Could not start Claude Code: ${err2.message}`})}),whenExited(child).then(code=>{if(clearTimeout(timer),buffer&&saving(buffer),result)return resolve2(result);let said2=stderr.trim()||`Claude Code stopped (exit ${code}).`;resolve2({ok:!1,sessionId,text:lastText,error:plainError(said2),detail:said2.slice(-2e3)})})})}var OWN_WORDS=[/^The squad was interrupted before it finished, for example because the board was closed\. Press Try again to carry on\.$/,/^Claude's usage limit is reached\. (It resets .{1,80}; press Try again after that|Press Try again once the limit resets)\.$/,/^Claude Code is signed out on this computer\. Open the Terminal app, type claude and press Enter, then sign in when it asks\. Then press Try again\.$/,/^The Claude account has no credit left\. Add credit or sign in with another account, then press Try again\.$/,/^Claude is busy right now\. Wait a few minutes, then press Try again\.$/,/^Claude Code could not reach the internet\. Check the connection, then press Try again\.$/,/^Claude's service had a problem on its side\. Wait a few minutes, then press Try again\.$/,/^Claude Code is not installed\. Install it and log in, then try again\.$/],PASSING=[/^Claude is busy right now\. /,/^Claude Code could not reach the internet\. /,/^Claude's service had a problem on its side\. /];function passing(said2){return PASSING.some(re=>re.test(said2.trim()))}function ownWords(said2){return OWN_WORDS.some(re=>re.test(said2.trim()))}var SIGNED_OUT=[/\bnot logged in\b/i,/\b(?:please )?run \/login\b/i,/\binvalid api key\b/i,/\bfailed to authenticate\. api error\b/i,/\bAPI Error: 401\b/,/"type":\s*"authentication_error"/,/\boauth token (?:has )?(?:expired|been revoked|is expired|is invalid)\b/i,/^authentication required\b/im];function plainError(raw,now4=Date.now()){let s=raw.toLowerCase();if(s.includes("exit 143")||s.includes("sigterm"))return"The squad was interrupted before it finished, for example because the board was closed. Press Try again to carry on.";let limit=claudeLimit(raw,now4);return limit?limit.resetsAt?`Claude's usage limit is reached. It resets ${resetWords(limit.resetsAt,now4)}; press Try again after that.`:"Claude's usage limit is reached. Press Try again once the limit resets.":SIGNED_OUT.some(re=>re.test(raw))?"Claude Code is signed out on this computer. Open the Terminal app, type claude and press Enter, then sign in when it asks. Then press Try again.":s.includes("credit balance")&&s.includes("low")?"The Claude account has no credit left. Add credit or sign in with another account, then press Try again.":s.includes("overloaded")||/\b529\b/.test(s)?"Claude is busy right now. Wait a few minutes, then press Try again.":/\b(enotfound|econnreset|econnrefused|etimedout|getaddrinfo|fetch failed|network error|connection error|socket hang up)\b/.test(s)?"Claude Code could not reach the internet. Check the connection, then press Try again.":/api error:? 5\d\d\b/.test(s)?"Claude's service had a problem on its side. Wait a few minutes, then press Try again.":raw.split(`
|
|
210
210
|
`).filter(Boolean).slice(-3).join(" ").slice(0,400)}function describeTool(e,cwd){let rel=p=>{if(typeof p!="string")return"";let r=path41.relative(cwd,p);return r?r.startsWith("..")?path41.basename(p):r:p},i=e.input;switch(e.name){case"Read":return`Reading ${rel(i.file_path)}`;case"Edit":case"MultiEdit":return`Editing ${rel(i.file_path)}`;case"Write":return`Writing ${rel(i.file_path)}`;case"Glob":return`Looking for ${String(i.pattern??"files")}`;case"Grep":return`Searching for "${String(i.pattern??"").slice(0,40)}"`;case"LS":return`Listing ${rel(i.path)||"the folder"}`;case"Bash":{let cmd17=String(i.command??"").split(`
|
|
211
|
-
`)[0].replace(/^cd\s+("[^"]*"|'[^']*'|\S+)\s*&&\s*/,"").slice(0,70);return cmd17?`Running ${cmd17}`:null}case"WebFetch":case"WebSearch":return`Looking up ${String(i.url??i.query??"").slice(0,60)}`;default:return null}}import{execFile as execFile3}from"child_process";var CLOUD={bedrock:"AWS Bedrock",vertex:"Google Vertex AI",foundry:"Azure AI Foundry",anthropicAws:"Anthropic on AWS",anthropicGoogleCloud:"Anthropic on Google Cloud",gateway:"a model gateway"};function planLabel(plan){if(typeof plan!="string"||!plan.trim())return"Claude plan";let titled=plan.trim().replace(/\S+/g,w=>w.charAt(0).toUpperCase()+w.slice(1));return/^claude(\s|$)/i.test(titled)?titled:`Claude ${titled}`}function billingFrom(stdout){let s;try{s=JSON.parse(stdout)}catch{return null}if(!s||typeof s!="object"||Array.isArray(s))return null;let o=s,provider=typeof o.apiProvider=="string"?o.apiProvider:"firstParty";return provider!=="firstParty"?{kind:"api-key",label:CLOUD[provider]??provider}:typeof o.apiKeySource=="string"&&o.apiKeySource?{kind:"api-key",label:"Anthropic API key"}:o.authMethod==="api_key"||o.authMethod==="api_key_helper"?{kind:"api-key",label:"Anthropic API key"}:o.authMethod==="third_party"?{kind:"api-key",label:"a cloud account"}:o.authMethod==="claude.ai"||o.authMethod==="oauth_token"||typeof o.subscriptionType=="string"?{kind:"subscription",label:planLabel(o.subscriptionType)}:null}function claudeBilling(timeoutMs=8e3){let bin=claudeBinary();return bin?new Promise(resolve2=>{execFile3(bin,["auth","status","--json"],{env:claudeEnv(),timeout:timeoutMs,encoding:"utf8",windowsHide:!0},(_err,stdout)=>{resolve2(billingFrom(String(stdout??"")))})}):Promise.resolve(null)}function quoted(s){return/^[\w@%+=:,./-]+$/.test(s)?s:`'${s.replace(/'/g,"'\\''")}'`}function claudeResume(sessionId,cwd){return`${cwd?`cd ${quoted(cwd)} && `:""}claude --resume ${sessionId}`}var READ_SHELL=["cat","ls","grep","rg","head","tail","wc","cd","pwd","git log","git show","git diff","git status"].map(c=>`Bash(${c}:*)`),READ_ONLY=["Read","Glob","Grep","LS",...READ_SHELL],GIT_READ=["git diff","git status","git log","git show"].map(c=>`Bash(${c}:*)`),TODO_TOOLS=["TodoWrite","TaskCreate","TaskUpdate","TaskList","TaskGet"],BUILD_TOOLS=["Read","Edit","MultiEdit","Write","Glob","Grep","LS","Bash",...TODO_TOOLS],RUN_TOOLS=["Read","Glob","Grep","LS","Bash","TodoWrite"],NO_GIT_WRITES=["Bash(git commit:*)","Bash(git push:*)","Bash(git checkout:*)","Bash(git switch:*)","Bash(git reset:*)","Bash(git rebase:*)","Bash(git merge:*)","Bash(git branch:*)","Bash(git tag:*)","Bash(git stash:*)","Bash(git clean:*)","Bash(git pull:*)","Bash(git cherry-pick:*)","Bash(git revert:*)","Bash(git am:*)","Bash(git update-ref:*)","Bash(git worktree:*)","Bash(git -C:*)","Bash(gh pr:*)","Bash(gh repo:*)"],FILE_TOOLS=["Edit","Write","MultiEdit"];function protectedRules(names2){return names2.flatMap(n=>{let name=n.replace(/^\//,""),pattern=name.endsWith("/")?`**/${name}**`:`**/${name}`;return[pattern,`//${pattern}`].flatMap(p=>FILE_TOOLS.map(t=>`${t}(${p})`))})}function windowTools(w){return w?w.tools.map(t=>`mcp__${w.name}__${t}`):[]}var LEVEL_MODEL={strongest:"opus",balanced:"sonnet",fast:"haiku"};function claudeRunOptions(r){let edits=r.permissions.includes("edit"),access={allowedTools:[...edits?BUILD_TOOLS:r.permissions.includes("run")?RUN_TOOLS:[...new Set([...READ_ONLY,...r.permissions.includes("git-read")?GIT_READ:[]])],...windowTools(r.window)],disallowedTools:[...NO_GIT_WRITES,...protectedRules(r.protectedFiles??[])],permissionMode:edits?"acceptEdits":"default"};return{cwd:r.cwd,prompt:r.prompt,...r.sessionId?{sessionId:r.sessionId}:{},...r.resume?{resume:r.resume}:{},...access,...r.secretFiles?{secretFiles:r.secretFiles}:{},...r.privatePlaces?{privatePlaces:r.privatePlaces}:{},...r.schema?{schema:r.schema}:{},...r.instructions?{appendSystemPrompt:r.instructions}:{},...r.model||r.level?{model:r.model||LEVEL_MODEL[r.level]}:{},...r.effort?{effort:r.effort}:{},...r.addDirs?{addDirs:r.addDirs}:{},...r.sandbox?{sandbox:claudeSandbox(r.sandbox)}:{},...r.env?{env:r.env}:{},...r.caps?.turns?{maxTurns:r.caps.turns}:{},...r.caps?.spendUsd?{maxBudgetUsd:r.caps.spendUsd}:{},...r.window?{window:r.window}:{},...r.timeoutMs?{timeoutMs:r.timeoutMs}:{},...r.signal?{signal:r.signal}:{},...r.onTool?{onTool:r.onTool}:{},...r.onBlocked?{onBlocked:r.onBlocked}:{},...r.onText?{onText:r.onText}:{},...r.onPlan?{onPlan:r.onPlan}:{},...r.onActivity?{onActivity:r.onActivity}:{},...r.onSpawn?{onSpawn:r.onSpawn}:{}}}function acpAdapterPath(){try{let pkg=createRequire2(import.meta.url).resolve("@agentclientprotocol/claude-agent-acp/package.json"),entry=path42.join(path42.dirname(pkg),"dist","index.js");return fs51.existsSync(entry)?entry:null}catch{return null}}var WINDOW_ENV={MCP_TOOL_TIMEOUT:"240000"};function claudeLaunch(opts,adapter,bin){let denied=deniedTools(opts),options={...opts.allowedTools?.length?{allowedTools:opts.allowedTools}:{},...denied.length?{disallowedTools:denied}:{},allowDangerouslySkipPermissions:!1,...opts.permissionMode?{permissionMode:opts.permissionMode}:{},...opts.schema?{outputFormat:{type:"json_schema",schema:opts.schema}}:{},...opts.appendSystemPrompt?{systemPrompt:{type:"preset",preset:"claude_code",append:opts.appendSystemPrompt}}:{},...opts.model?{model:opts.model}:{},...opts.effort?{effort:opts.effort}:{},...opts.addDirs?.length?{additionalDirectories:opts.addDirs}:{},...opts.sandbox?{sandbox:opts.sandbox}:{},...opts.maxTurns?{maxTurns:opts.maxTurns}:{},...opts.maxBudgetUsd?{maxBudgetUsd:opts.maxBudgetUsd}:{}};return{label:"Claude Code",command:process.execPath,args:[adapter],env:{...claudeEnv(),...opts.env,CLAUDE_CODE_EXECUTABLE:bin,...opts.model?{ANTHROPIC_MODEL:opts.model}:{},...opts.window?WINDOW_ENV:{}},meta:{claudeCode:{options}},mode:opts.permissionMode??"default",canResume:sessionExists,costSoFar,explain:plainError,toolName:update=>update._meta?.claudeCode?.toolName,structuredTool:"StructuredOutput",blocked:claudeBlocked}}async function runClaudeAcp(opts){let adapter=acpAdapterPath();if(!adapter)throw new AcpStartError("The Claude ACP adapter is not installed.");let bin=claudeBinary();return bin?runAcp(claudeLaunch(opts,adapter,bin),{...opts,...opts.logDir?{logDir:opts.logDir}:{}}):{ok:!1,sessionId:opts.resume??opts.sessionId??"",text:"",error:"Claude Code is not installed. Install it and log in, then try again."}}var SAID_LIMIT=/^(you've (hit|reached) your [\w' -]{0,40}limit\b|you're out of (extra )?usage|claude (ai )?usage limit reached|(5-hour|weekly|session) limit reached)/i;function withLimit(r){if(r.limit)return r;if(r.ok){let said2=(r.text??"").trim();return r.structured!==void 0||said2.length>400||!SAID_LIMIT.test(said2)?r:withLimit({...r,ok:!1,error:plainError(said2),detail:said2})}let found2=claudeLimit(r.detail??r.error??"");return found2?{...r,limit:{name:"Claude",...found2}}:r}function adapterVersion(){try{return createRequire2(import.meta.url)("@agentclientprotocol/claude-agent-acp/package.json").version??null}catch{return null}}var claudeTool={name:"claude",label:"Claude Code",plan:"Claude",canRun:!0,supportsEffort:!0,efforts:["xhigh","high","medium","low"],instructionFiles:["CLAUDE.md","CLAUDE.local.md",".claude/"],privateFiles:home2=>claudePrivateFiles(home2),secretEnv:CLAUDE_ENV,modelFor:level=>({strongest:"Opus",balanced:"Sonnet",fast:"Haiku"})[level],ready(){let bin=claudeBinary();return!!(bin&&toolVersion(bin))},version(){let bin=claudeBinary(),v=bin?toolVersion(bin):null,acp=adapterVersion();return v?`${v}${acp?`, ACP adapter ${acp}`:""}`:null},sandbox:claudeSandboxSupport,caps:{turns:!0,spend:!0},blocked:claudeBlocked,async run(r){let opts=claudeRunOptions(r),done2=res=>withCap(withLimit(res),r.caps);if(r.direct||!acpAdapterPath())return done2(await runClaude(opts));try{return done2(await runClaudeAcp({...opts,...r.logDir?{logDir:r.logDir}:{}}))}catch(err2){if(!(err2 instanceof AcpStartError))throw err2;return r.signal?.aborted?{ok:!1,sessionId:opts.resume??opts.sessionId??"",text:"",error:"You stopped the squad."}:(r.onFallback?.(err2.message),done2(await runClaude(opts)))}},ownWords,passing,describe:describeTool,billing:()=>claudeBilling(),resumeCommand:claudeResume};import{execSync}from"child_process";import path43 from"path";var codexTool={name:"codex",label:"Codex",plan:"ChatGPT",canRun:!1,supportsEffort:!1,instructionFiles:["AGENTS.md"],privateFiles:home2=>["auth.json","sessions","history.jsonl"].map(f=>path43.join(home2,".codex",f)),secretEnv:["OPENAI_*","CODEX_*"],modelFor:()=>"",ready(){try{return execSync(process.platform==="win32"?"where codex":"which codex",{stdio:"ignore"}),!0}catch{return!1}},run:()=>Promise.reject(new Error("squad-kit cannot run Codex yet.")),describe:()=>null};import{execFile as execFile4}from"child_process";import fs52 from"fs";import path44 from"path";import{pathToFileURL}from"url";import os7 from"os";import net from"net";var PRIVATE_V4=[["0.0.0.0",8],["10.0.0.0",8],["100.64.0.0",10],["127.0.0.0",8],["169.254.0.0",16],["172.16.0.0",12],["192.168.0.0",16],["198.18.0.0",15]];function v4Number(ip){return ip.split(".").reduce((n,part)=>n*256+Number(part),0)}function privateV4(ip){let n=v4Number(ip);return PRIVATE_V4.some(([net42,bits])=>Math.floor(n/2**(32-bits))===Math.floor(v4Number(net42)/2**(32-bits)))}function v6Words(ip){let s=ip.toLowerCase().replace(/%.*$/,""),tail4=/(\d+\.\d+\.\d+\.\d+)$/.exec(s);if(tail4){let n=v4Number(tail4[1]);s=`${s.slice(0,-tail4[1].length)}${(n>>>16).toString(16)}:${(n&65535).toString(16)}`}let[head,rest]=s.split("::"),a=head?head.split(":"):[],b=rest!==void 0&&rest!==""?rest.split(":"):[],fill2=rest===void 0?0:8-a.length-b.length;if(fill2<0)return null;let words2=[...a,...Array(fill2).fill("0"),...b].map(w=>parseInt(w||"0",16));return words2.length===8&&words2.every(w=>Number.isInteger(w)&&w>=0&&w<=65535)?words2:null}function privateV6(ip){let w=v6Words(ip);if(!w)return!0;let v4=(hi,lo)=>`${hi>>>8}.${hi&255}.${lo>>>8}.${lo&255}`;return w.slice(0,5).every(x=>x===0)&&(w[5]===0||w[5]===65535)?w[5]===0&&w[6]===0?w[7]<=1:privateV4(v4(w[6],w[7])):w[0]===100&&w[1]===65435&&w.slice(2,6).every(x=>x===0)?privateV4(v4(w[6],w[7])):w[0]===8194?privateV4(v4(w[1],w[2])):(w[0]&65024)===64512||(w[0]&65472)===65152}function isLocalHost(host3){let h=host3.toLowerCase().replace(/^\[|\]$/g,"").replace(/\.$/,"");if(h==="localhost"||h.endsWith(".localhost")||h.endsWith(".local"))return!0;let kind=net.isIP(h);return kind===4?privateV4(h):kind===6?privateV6(h):!1}function isAppAddress(url,origin){let u,o;try{u=new URL(url),o=new URL(origin)}catch{return!1}if(u.origin===o.origin)return!0;let loop=x=>["localhost","127.0.0.1","[::1]"].includes(x.hostname);return loop(u)&&loop(o)&&u.protocol===o.protocol&&u.port===o.port}function blockedFrom(url,origin,also=[]){let u;try{u=new URL(url)}catch{return!1}return/^(https?|wss?):$/.test(u.protocol)?!isAppAddress(url,origin)&&!also.some(a=>isAppAddress(url,a))&&isLocalHost(u.hostname):!1}var StepFailure=class extends Error{constructor(kind,message,address){super(message);this.kind=kind;this.address=address}kind;address},BrowserSetupError=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail};var SETTING_UP="Setting up squad-kit's screenshot browser. This happens only once.",PLAYWRIGHT_VERSION="1.64.1";function playwrightDir(){return path44.join(squadHome(),"tools","playwright")}var run2=(file3,args,o)=>new Promise(resolve2=>{let env2=Object.fromEntries(Object.entries(process.env).filter(([k])=>!/^npm_/i.test(k)));execFile4(file3,args,{cwd:o.cwd,timeout:o.timeoutMs,maxBuffer:8*1024*1024,shell:process.platform==="win32",env:{...env2,npm_config_update_notifier:"false",ELECTRON_RUN_AS_NODE:"1",PLAYWRIGHT_SKIP_BROWSER_GC:"1"}},(err2,stdout,stderr)=>resolve2({ok:!err2,output:`${stdout}
|
|
211
|
+
`)[0].replace(/^cd\s+("[^"]*"|'[^']*'|\S+)\s*&&\s*/,"").slice(0,70);return cmd17?`Running ${cmd17}`:null}case"WebFetch":case"WebSearch":return`Looking up ${String(i.url??i.query??"").slice(0,60)}`;default:return null}}import{execFile as execFile3}from"child_process";var CLOUD={bedrock:"AWS Bedrock",vertex:"Google Vertex AI",foundry:"Azure AI Foundry",anthropicAws:"Anthropic on AWS",anthropicGoogleCloud:"Anthropic on Google Cloud",gateway:"a model gateway"};function planLabel(plan){if(typeof plan!="string"||!plan.trim())return"Claude plan";let titled=plan.trim().replace(/\S+/g,w=>w.charAt(0).toUpperCase()+w.slice(1));return/^claude(\s|$)/i.test(titled)?titled:`Claude ${titled}`}function billingFrom(stdout){let s;try{s=JSON.parse(stdout)}catch{return null}if(!s||typeof s!="object"||Array.isArray(s))return null;let o=s,provider=typeof o.apiProvider=="string"?o.apiProvider:"firstParty";return provider!=="firstParty"?{kind:"api-key",label:CLOUD[provider]??provider}:typeof o.apiKeySource=="string"&&o.apiKeySource?{kind:"api-key",label:"Anthropic API key"}:o.authMethod==="api_key"||o.authMethod==="api_key_helper"?{kind:"api-key",label:"Anthropic API key"}:o.authMethod==="third_party"?{kind:"api-key",label:"a cloud account"}:o.authMethod==="claude.ai"||o.authMethod==="oauth_token"||typeof o.subscriptionType=="string"?{kind:"subscription",label:planLabel(o.subscriptionType)}:null}function claudeBilling(timeoutMs=8e3){let bin=claudeBinary();return bin?new Promise(resolve2=>{execFile3(bin,["auth","status","--json"],{env:claudeEnv(),timeout:timeoutMs,encoding:"utf8",windowsHide:!0},(_err,stdout)=>{resolve2(billingFrom(String(stdout??"")))})}):Promise.resolve(null)}function quoted(s){return/^[\w@%+=:,./-]+$/.test(s)?s:`'${s.replace(/'/g,"'\\''")}'`}function claudeResume(sessionId,cwd){return`${cwd?`cd ${quoted(cwd)} && `:""}claude --resume ${sessionId}`}var READ_SHELL=["cat","ls","grep","rg","head","tail","wc","cd","pwd","git log","git show","git diff","git status"].map(c=>`Bash(${c}:*)`),READ_ONLY=["Read","Glob","Grep","LS",...READ_SHELL],GIT_READ=["git diff","git status","git log","git show"].map(c=>`Bash(${c}:*)`),TODO_TOOLS=["TodoWrite","TaskCreate","TaskUpdate","TaskList","TaskGet"],BUILD_TOOLS=["Read","Edit","MultiEdit","Write","Glob","Grep","LS","Bash",...TODO_TOOLS],RUN_TOOLS=["Read","Glob","Grep","LS","Bash","TodoWrite"],NO_GIT_WRITES=["Bash(git commit:*)","Bash(git push:*)","Bash(git checkout:*)","Bash(git switch:*)","Bash(git reset:*)","Bash(git rebase:*)","Bash(git merge:*)","Bash(git branch:*)","Bash(git tag:*)","Bash(git stash:*)","Bash(git clean:*)","Bash(git pull:*)","Bash(git cherry-pick:*)","Bash(git revert:*)","Bash(git am:*)","Bash(git update-ref:*)","Bash(git worktree:*)","Bash(git -C:*)","Bash(gh pr:*)","Bash(gh repo:*)"],FILE_TOOLS=["Edit","Write","MultiEdit"];function protectedRules(names2){return names2.flatMap(n=>{let name=n.replace(/^\//,""),pattern=name.endsWith("/")?`**/${name}**`:`**/${name}`;return[pattern,`//${pattern}`].flatMap(p=>FILE_TOOLS.map(t=>`${t}(${p})`))})}function windowTools(w){return w?w.tools.map(t=>`mcp__${w.name}__${t}`):[]}var LEVEL_MODEL={strongest:"opus",balanced:"sonnet",fast:"haiku"};function claudeRunOptions(r){let edits=r.permissions.includes("edit"),access={allowedTools:[...edits?BUILD_TOOLS:r.permissions.includes("run")?RUN_TOOLS:[...new Set([...READ_ONLY,...r.permissions.includes("git-read")?GIT_READ:[]])],...windowTools(r.window)],disallowedTools:[...NO_GIT_WRITES,...protectedRules(r.protectedFiles??[])],permissionMode:edits?"acceptEdits":"default"};return{cwd:r.cwd,prompt:r.prompt,...r.sessionId?{sessionId:r.sessionId}:{},...r.resume?{resume:r.resume}:{},...access,...r.secretFiles?{secretFiles:r.secretFiles}:{},...r.privatePlaces?{privatePlaces:r.privatePlaces}:{},...r.schema?{schema:r.schema}:{},...r.instructions?{appendSystemPrompt:r.instructions}:{},...r.model||r.level?{model:r.model||LEVEL_MODEL[r.level]}:{},...r.effort?{effort:r.effort}:{},...r.addDirs?{addDirs:r.addDirs}:{},...r.sandbox?{sandbox:claudeSandbox(r.sandbox)}:{},...r.env?{env:r.env}:{},...r.caps?.turns?{maxTurns:r.caps.turns}:{},...r.caps?.spendUsd?{maxBudgetUsd:r.caps.spendUsd}:{},...r.window?{window:r.window}:{},...r.timeoutMs?{timeoutMs:r.timeoutMs}:{},...r.signal?{signal:r.signal}:{},...r.onTool?{onTool:r.onTool}:{},...r.onBlocked?{onBlocked:r.onBlocked}:{},...r.onText?{onText:r.onText}:{},...r.onPlan?{onPlan:r.onPlan}:{},...r.onActivity?{onActivity:r.onActivity}:{},...r.onSpawn?{onSpawn:r.onSpawn}:{}}}function acpAdapterPath(){try{let pkg=createRequire2(import.meta.url).resolve("@agentclientprotocol/claude-agent-acp/package.json"),entry=path42.join(path42.dirname(pkg),"dist","index.js");return fs51.existsSync(entry)?entry:null}catch{return null}}var WINDOW_ENV={MCP_TOOL_TIMEOUT:"240000"};function claudeLaunch(opts,adapter,bin){let denied=deniedTools(opts),options={...opts.allowedTools?.length?{allowedTools:opts.allowedTools}:{},...denied.length?{disallowedTools:denied}:{},allowDangerouslySkipPermissions:!1,...opts.permissionMode?{permissionMode:opts.permissionMode}:{},...opts.schema?{outputFormat:{type:"json_schema",schema:opts.schema}}:{},...opts.appendSystemPrompt?{systemPrompt:{type:"preset",preset:"claude_code",append:opts.appendSystemPrompt}}:{},...opts.model?{model:opts.model}:{},...opts.effort?{effort:opts.effort}:{},...opts.addDirs?.length?{additionalDirectories:opts.addDirs}:{},...opts.sandbox?{sandbox:opts.sandbox}:{},...opts.maxTurns?{maxTurns:opts.maxTurns}:{},...opts.maxBudgetUsd?{maxBudgetUsd:opts.maxBudgetUsd}:{}};return{label:"Claude Code",command:process.execPath,args:[adapter],env:{...claudeEnv(),...opts.env,CLAUDE_CODE_EXECUTABLE:bin,...opts.model?{ANTHROPIC_MODEL:opts.model}:{},...opts.window?WINDOW_ENV:{}},meta:{claudeCode:{options}},mode:opts.permissionMode??"default",canResume:sessionExists,costSoFar,explain:plainError,toolName:update=>update._meta?.claudeCode?.toolName,structuredTool:"StructuredOutput",blocked:claudeBlocked}}async function runClaudeAcp(opts){let adapter=acpAdapterPath();if(!adapter)throw new AcpStartError("The Claude ACP adapter is not installed.");let bin=claudeBinary();return bin?runAcp(claudeLaunch(opts,adapter,bin),{...opts,...opts.logDir?{logDir:opts.logDir}:{}}):{ok:!1,sessionId:opts.resume??opts.sessionId??"",text:"",error:"Claude Code is not installed. Install it and log in, then try again."}}var SAID_LIMIT=/^(you've (hit|reached) your [\w' -]{0,40}limit\b|you're out of (extra )?usage|claude (ai )?usage limit reached|(5-hour|weekly|session) limit reached)/i;function withLimit(r){if(r.limit)return r;if(r.ok){let said2=(r.text??"").trim();return r.structured!==void 0||said2.length>400||!SAID_LIMIT.test(said2)?r:withLimit({...r,ok:!1,error:plainError(said2),detail:said2})}let found2=claudeLimit(r.detail??r.error??"");return found2?{...r,limit:{name:"Claude",...found2}}:r}function adapterVersion(){try{return createRequire2(import.meta.url)("@agentclientprotocol/claude-agent-acp/package.json").version??null}catch{return null}}var claudeTool={name:"claude",label:"Claude Code",plan:"Claude",canRun:!0,supportsEffort:!0,efforts:["xhigh","high","medium","low"],instructionFiles:["CLAUDE.md","CLAUDE.local.md",".claude/"],privateFiles:home2=>claudePrivateFiles(home2),secretEnv:CLAUDE_ENV,modelFor:level=>({strongest:"Opus",balanced:"Sonnet",fast:"Haiku"})[level],ready(){let bin=claudeBinary();return!!(bin&&toolVersion(bin))},version(){let bin=claudeBinary(),v=bin?toolVersion(bin):null,acp=adapterVersion();return v?`${v}${acp?`, ACP adapter ${acp}`:""}`:null},sandbox:claudeSandboxSupport,caps:{turns:!0,spend:!0},blocked:claudeBlocked,async run(r){let opts=claudeRunOptions(r),done2=res=>withCap(withLimit(res),r.caps);if(r.direct||!acpAdapterPath())return done2(await runClaude(opts));try{return done2(await runClaudeAcp({...opts,...r.logDir?{logDir:r.logDir}:{}}))}catch(err2){if(!(err2 instanceof AcpStartError))throw err2;return r.signal?.aborted?{ok:!1,sessionId:opts.resume??opts.sessionId??"",text:"",error:"You stopped the squad."}:(r.onFallback?.(err2.message),done2(await runClaude(opts)))}},ownWords,passing,describe:describeTool,billing:()=>claudeBilling(),resumeCommand:claudeResume};import{execSync}from"child_process";import path43 from"path";var codexTool={name:"codex",label:"Codex",plan:"ChatGPT",canRun:!1,supportsEffort:!1,instructionFiles:["AGENTS.md"],privateFiles:home2=>["auth.json","sessions","history.jsonl"].map(f=>path43.join(home2,".codex",f)),secretEnv:["OPENAI_*","CODEX_*"],modelFor:()=>"",ready(){try{return execSync(process.platform==="win32"?"where codex":"which codex",{stdio:"ignore"}),!0}catch{return!1}},run:()=>Promise.reject(new Error("squad-kit cannot run Codex yet.")),describe:()=>null};import{execFile as execFile4}from"child_process";import fs52 from"fs";import path44 from"path";import{pathToFileURL}from"url";import os7 from"os";import net from"net";var PRIVATE_V4=[["0.0.0.0",8],["10.0.0.0",8],["100.64.0.0",10],["127.0.0.0",8],["169.254.0.0",16],["172.16.0.0",12],["192.168.0.0",16],["198.18.0.0",15]];function v4Number(ip){return ip.split(".").reduce((n,part)=>n*256+Number(part),0)}function privateV4(ip){let n=v4Number(ip);return PRIVATE_V4.some(([net42,bits])=>Math.floor(n/2**(32-bits))===Math.floor(v4Number(net42)/2**(32-bits)))}function v6Words(ip){let s=ip.toLowerCase().replace(/%.*$/,""),tail4=/(\d+\.\d+\.\d+\.\d+)$/.exec(s);if(tail4){let n=v4Number(tail4[1]);s=`${s.slice(0,-tail4[1].length)}${(n>>>16).toString(16)}:${(n&65535).toString(16)}`}let[head,rest]=s.split("::"),a=head?head.split(":"):[],b=rest!==void 0&&rest!==""?rest.split(":"):[],fill3=rest===void 0?0:8-a.length-b.length;if(fill3<0)return null;let words2=[...a,...Array(fill3).fill("0"),...b].map(w=>parseInt(w||"0",16));return words2.length===8&&words2.every(w=>Number.isInteger(w)&&w>=0&&w<=65535)?words2:null}function privateV6(ip){let w=v6Words(ip);if(!w)return!0;let v4=(hi,lo)=>`${hi>>>8}.${hi&255}.${lo>>>8}.${lo&255}`;return w.slice(0,5).every(x=>x===0)&&(w[5]===0||w[5]===65535)?w[5]===0&&w[6]===0?w[7]<=1:privateV4(v4(w[6],w[7])):w[0]===100&&w[1]===65435&&w.slice(2,6).every(x=>x===0)?privateV4(v4(w[6],w[7])):w[0]===8194?privateV4(v4(w[1],w[2])):(w[0]&65024)===64512||(w[0]&65472)===65152}function isLocalHost(host3){let h=host3.toLowerCase().replace(/^\[|\]$/g,"").replace(/\.$/,"");if(h==="localhost"||h.endsWith(".localhost")||h.endsWith(".local"))return!0;let kind=net.isIP(h);return kind===4?privateV4(h):kind===6?privateV6(h):!1}function isAppAddress(url,origin){let u,o;try{u=new URL(url),o=new URL(origin)}catch{return!1}if(u.origin===o.origin)return!0;let loop=x=>["localhost","127.0.0.1","[::1]"].includes(x.hostname);return loop(u)&&loop(o)&&u.protocol===o.protocol&&u.port===o.port}function blockedFrom(url,origin,also=[]){let u;try{u=new URL(url)}catch{return!1}return/^(https?|wss?):$/.test(u.protocol)?!isAppAddress(url,origin)&&!also.some(a=>isAppAddress(url,a))&&isLocalHost(u.hostname):!1}var StepFailure=class extends Error{constructor(kind,message,address){super(message);this.kind=kind;this.address=address}kind;address},BrowserSetupError=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail};var SETTING_UP="Setting up squad-kit's screenshot browser. This happens only once.",PLAYWRIGHT_VERSION="1.64.1";function playwrightDir(){return path44.join(squadHome(),"tools","playwright")}var run2=(file3,args,o)=>new Promise(resolve2=>{let env2=Object.fromEntries(Object.entries(process.env).filter(([k])=>!/^npm_/i.test(k)));execFile4(file3,args,{cwd:o.cwd,timeout:o.timeoutMs,maxBuffer:8*1024*1024,shell:process.platform==="win32",env:{...env2,npm_config_update_notifier:"false",ELECTRON_RUN_AS_NODE:"1",PLAYWRIGHT_SKIP_BROWSER_GC:"1"}},(err2,stdout,stderr)=>resolve2({ok:!err2,output:`${stdout}
|
|
212
212
|
${stderr}
|
|
213
213
|
${err2?err2.message:""}`.trim().slice(-4e3)}))}),flights=new Map;function singleFlight(key,job){let going=flights.get(key);if(going)return going;let p=job().finally(()=>flights.delete(key));return flights.set(key,p),p}function installedVersion(dir){try{return JSON.parse(fs52.readFileSync(path44.join(dir,"node_modules","playwright-core","package.json"),"utf8")).version??null}catch{return null}}async function readyPlaywright(o={}){let dir=o.dir??playwrightDir(),entry=path44.join(dir,"node_modules","playwright-core","index.mjs");return installedVersion(dir)===PLAYWRIGHT_VERSION&&fs52.existsSync(entry)||await singleFlight(`install:${dir}`,async()=>{if(installedVersion(dir)===PLAYWRIGHT_VERSION)return;o.onLine?.(SETTING_UP);let part=`${dir}.part-${process.pid}-${Date.now()}`;fs52.mkdirSync(part,{recursive:!0}),fs52.writeFileSync(path44.join(part,"package.json"),`${JSON.stringify({name:"squad-kit-screenshots",private:!0},null,2)}
|
|
214
214
|
`);let args=["install",`playwright-core@${PLAYWRIGHT_VERSION}`,"--save-exact","--no-audit","--no-fund","--ignore-scripts","--prefer-offline","--loglevel=error"],r=await(o.run??run2)(npmCommand(),args,{cwd:part,timeoutMs:5*6e4});if(!r.ok||installedVersion(part)!==PLAYWRIGHT_VERSION)throw fs52.rmSync(part,{recursive:!0,force:!0}),new BrowserSetupError(`Installing Playwright ${PLAYWRIGHT_VERSION} did not work`,`npm ${args.join(" ")} in ${part}: ${r.output}`);fs52.rmSync(dir,{recursive:!0,force:!0}),fs52.mkdirSync(path44.dirname(dir),{recursive:!0}),fs52.renameSync(part,dir)}),entry}function npmCommand(){let beside=path44.join(path44.dirname(process.execPath),process.platform==="win32"?"npm.cmd":"npm");return fs52.existsSync(beside)?beside:process.platform==="win32"?"npm.cmd":"npm"}function installBrowser(dir,runner4){let cli=path44.join(dir,"node_modules","playwright-core","cli.js");return runner4(process.execPath,[cli,"install","--only-shell","--no-remove","chromium"],{cwd:dir,timeoutMs:10*6e4})}function browsersDir(dir){let set2=process.env.PLAYWRIGHT_BROWSERS_PATH;if(set2==="0")return path44.join(dir,"node_modules","playwright-core",".local-browsers");if(set2)return path44.resolve(set2);let home2=os7.homedir(),cache4=process.platform==="darwin"?path44.join(home2,"Library","Caches"):process.platform==="win32"?process.env.LOCALAPPDATA??path44.join(home2,"AppData","Local"):process.env.XDG_CACHE_HOME??path44.join(home2,".cache");return path44.join(cache4,"ms-playwright")}function browserThere(dir){try{let shell2=JSON.parse(fs52.readFileSync(path44.join(dir,"node_modules","playwright-core","browsers.json"),"utf8")).browsers?.find(b=>b.name==="chromium-headless-shell");return!shell2||shell2.revisionOverrides?null:fs52.existsSync(path44.join(browsersDir(dir),`chromium_headless_shell-${shell2.revision}`,"INSTALLATION_COMPLETE"))}catch{return null}}function ownBrowserFiles(dir=playwrightDir()){try{let shell2=JSON.parse(fs52.readFileSync(path44.join(dir,"node_modules","playwright-core","browsers.json"),"utf8")).browsers?.find(b=>b.name==="chromium-headless-shell");return shell2?[path44.join(browsersDir(dir),`chromium_headless_shell-${shell2.revision}`)]:[]}catch{return[]}}var prepared=new Set;async function preparePlaywright(o={}){let dir=o.dir??playwrightDir();if(prepared.has(dir))return;let said2=!1,once2=text4=>{said2||(said2=!0,o.onLine?.(text4))};await readyPlaywright({...o,onLine:once2});let there=browserThere(dir);if(!there){there===!1&&once2(SETTING_UP);let got=await singleFlight(`browser:${dir}`,()=>installBrowser(dir,o.run??run2));if(!got.ok)throw new BrowserSetupError("Getting Chromium did not work",got.output)}prepared.add(dir)}var sleep=ms2=>new Promise(r=>setTimeout(r,ms2));function originOf(url){try{return new URL(url).origin}catch{return null}}var FIELD_ROLES=["textbox","combobox","listbox","searchbox","spinbutton","checkbox","radio","switch","slider"];function locate(page,t,exact2){return t.role?page.getByRole(t.role,t.name?{name:t.name,exact:exact2}:{}):t.label&&exact2?FIELD_ROLES.reduce((loc,role)=>loc.or(page.getByRole(role,{name:t.label,exact:!0})),page.getByLabel(t.label,{exact:!0})):t.label?page.getByLabel(t.label,{exact:exact2}):t.placeholder?page.getByPlaceholder(t.placeholder,{exact:exact2}):t.text?page.getByText(t.text,{exact:exact2}):t.testId?page.getByTestId(t.testId):page.locator(t.css??"")}async function find(page,t,ms2){let until=Date.now()+ms2,ways=[locate(page,t,!0),locate(page,t,!1)];for(;;){for(let loc of ways){let n=await loc.count().catch(()=>0);if(n===1)return loc;if(n>1){let shown2=loc.filter({visible:!0});return await shown2.count().catch(()=>0)?shown2.first():loc.first()}}if(Date.now()>=until)throw new StepFailure("missing",`Nothing on the page matches ${JSON.stringify(t)}`);await sleep(200)}}function failure2(err2){if(err2 instanceof StepFailure)return err2;let msg=err2?.message??String(err2);return/has been closed|Target closed/i.test(msg)?new StepFailure("closed",msg):/did not find some options|not find option/i.test(msg)?new StepFailure("option",msg):/element is not enabled|element is disabled/i.test(msg)?new StepFailure("disabled",msg):/element is not visible|element is outside of the viewport/i.test(msg)?new StepFailure("hidden",msg):/ERR_BLOCKED_BY_CLIENT/i.test(msg)?new StepFailure("blocked",msg):err2?.name==="TimeoutError"||/Timeout \d+ms exceeded/i.test(msg)?new StepFailure("timeout",msg):new StepFailure("other",msg)}function wrapPage(page,context,origin,stopped){let blocked=null;page.on("dialog",d=>{d.accept().catch(()=>{})}),page.on("filechooser",()=>{}),page.on("framenavigated",f=>{f.parentFrame()||f.url()==="about:blank"||originOf(f.url())===origin||(blocked=f.url().startsWith("chrome-error://")&&stopped.at?stopped.at:f.url(),stopped.at=null,page.goto("about:blank",{timeout:5e3,waitUntil:"load"}).catch(()=>{}))});let outside=()=>{if(!blocked)return;let to=blocked;throw blocked=null,new StepFailure("blocked",`The page tried to open ${to}`,to)},afterAction=async ms2=>{await sleep(150),await page.waitForLoadState("load",{timeout:Math.min(ms2,5e3)}).catch(()=>{}),outside()};return{async goto(url,ms2){blocked=null;try{let res=await page.goto(url,{timeout:ms2,waitUntil:"load"});return outside(),res?.status()??null}catch(err2){throw outside(),failure2(err2)}},async act(step2,ms2){blocked=null;let until=Date.now()+ms2,left2=()=>Math.max(1e3,until-Date.now());try{let loc=step2.target?await find(page,step2.target,ms2):null;switch(step2.action){case"click":await loc.click({timeout:left2()});break;case"fill":await loc.fill(step2.value??"",{timeout:left2()});break;case"select":await loc.selectOption(step2.value??"",{timeout:left2()});break;case"check":await loc.check({timeout:left2()});break;case"uncheck":await loc.uncheck({timeout:left2()});break;case"hover":await loc.hover({timeout:left2()});break;case"press":loc?await loc.press(step2.key??"Enter",{timeout:left2()}):await page.keyboard.press(step2.key??"Enter");break;case"wait":loc?await loc.waitFor({state:"visible",timeout:left2()}):await page.waitForTimeout(Math.min(step2.ms??0,5e3));break;default:throw new StepFailure("other",`${step2.action} is not a step the browser knows`)}await afterAction(left2())}catch(err2){throw outside(),failure2(err2)}},async settle(ms2){await page.waitForLoadState("load",{timeout:ms2}).catch(()=>{}),await page.waitForLoadState("networkidle",{timeout:Math.min(ms2,3e3)}).catch(()=>{}),await page.evaluate("document.fonts ? document.fonts.ready.then(() => true) : true").catch(()=>{}),await sleep(150)},async snapshot(){return page.locator("body").ariaSnapshot({timeout:5e3})},url(){return page.url()},async screenshot(){return page.screenshot({animations:"disabled",caret:"hide",timeout:15e3})},async close(){await context.close().catch(()=>{})}}}function wrapBrowser(browser){return{async page(o){let context=await browser.newContext({viewport:{width:o.width,height:o.height},deviceScaleFactor:1,...o.phone?{isMobile:!0,hasTouch:!0}:{},acceptDownloads:!1,serviceWorkers:"block",ignoreHTTPSErrors:!0}),stopped={at:null};await context.route("**/*",async route=>{let req=route.request(),url=req.url(),top=!1;try{top=req.isNavigationRequest()&&!req.frame().parentFrame()}catch{top=!1}return top&&originOf(url)!==o.origin?(stopped.at=url,route.abort("blockedbyclient").catch(()=>{})):blockedFrom(url,o.origin,o.also)?route.abort("blockedbyclient").catch(()=>{}):route.continue().catch(()=>{})});let page=await context.newPage();return context.on("page",p=>{p!==page&&p.close().catch(()=>{})}),wrapPage(page,context,o.origin,stopped)},async close(){await browser.close().catch(()=>{})}}}var MISSING_BROWSER=/Executable doesn't exist|playwright install|browserType\.launch: .*not found/i;async function launchPlaywright(o={}){let dir=o.dir??playwrightDir();await preparePlaywright(o);let entry=await readyPlaywright(o),pw;try{pw=await import(pathToFileURL(entry).href)}catch(err2){throw new BrowserSetupError("Playwright did not load",`${entry}: ${err2.message}`)}let start=()=>pw.chromium.launch({headless:!0,timeout:6e4});try{return wrapBrowser(await start())}catch(err2){let msg=err2.message??String(err2);if(!MISSING_BROWSER.test(msg))throw new BrowserSetupError("Chromium did not start",msg)}o.onLine?.(SETTING_UP);let got=await singleFlight(`browser:${dir}`,()=>installBrowser(dir,o.run??run2));if(!got.ok)throw new BrowserSetupError("Getting Chromium did not work",got.output);try{return wrapBrowser(await start())}catch(err2){throw new BrowserSetupError("Chromium did not start",err2.message??String(err2))}}var playwrightBrowser={name:"playwright",label:"Playwright",launch:(o={})=>launchPlaywright(o.onLine?{onLine:o.onLine}:{}),prepare:(o={})=>preparePlaywright(o.onLine?{onLine:o.onLine}:{}),ownFiles:()=>ownBrowserFiles()};import{execFile as execFile5}from"child_process";import fs53 from"fs";import path45 from"path";import{randomBytes as randomBytes2}from"crypto";var PhoneError=class extends Error{constructor(message,detail){super(message);this.detail=detail}detail};var realRun=(file3,args,o={})=>new Promise(resolve2=>{execFile5(file3,args,{timeout:o.timeoutMs??6e4,maxBuffer:32*1024*1024,encoding:"utf8",...o.signal?{signal:o.signal}:{}},(err2,out22,errText)=>resolve2({ok:!err2,out:String(out22??""),err:`${String(errText??"")}${err2&&!errText?err2.message:""}`})).stdin?.end()}),NEEDS_MAC="An iPhone simulator needs a Mac.",LOCALES={ar:"ar_SA",en:"en_US",fr:"fr_FR",de:"de_DE",es:"es_ES",ur:"ur_PK",tr:"tr_TR"};function localeOf(language){let l=language.trim().toLowerCase().split(/[-_]/)[0]||"en";return LOCALES[l]??`${l}_${l.toUpperCase()}`}function languageArgs(language){if(!language?.trim())return[];let l=language.trim().toLowerCase().split(/[-_]/)[0];return["-AppleLanguages",`(${l})`,"-AppleLocale",localeOf(l)]}var STATES={Booted:"booted",Booting:"booting",Shutdown:"shutdown","Shutting Down":"shutting-down"};function runtimesOf(text4){try{let j=JSON.parse(text4),out22=[];for(let r of j.runtimes??[]){if(!r.identifier||!r.version||r.isAvailable===!1||(r.platform?r.platform!=="iOS":!/^iOS\b/.test(r.name??"")))continue;let models=(r.supportedDeviceTypes??[]).flatMap(d=>{if(!d.identifier||!d.name)return[];let size2=d.productFamily==="iPad"||/^iPad\b/.test(d.name)?"tablet":d.productFamily==="iPhone"||/^iPhone\b/.test(d.name)?"phone":null;return size2?[{id:d.identifier,name:d.name,size:size2}]:[]});models.length&&out22.push({id:r.identifier,version:r.version,models})}let num4=v=>v.split(".").map(Number);return out22.sort((a,b)=>{let x=num4(a.version),y=num4(b.version);for(let i=0;i<Math.max(x.length,y.length);i+=1)if((y[i]??0)!==(x[i]??0))return(y[i]??0)-(x[i]??0);return 0})}catch{return[]}}function devicesOf(text4){try{let j=JSON.parse(text4);return Object.entries(j.devices??{}).flatMap(([runtime,list8])=>list8.flatMap(d=>d.udid?[{id:d.udid,name:d.name??"",state:STATES[d.state??""]??"other",runtime}]:[]))}catch{return[]}}var DEVICE_ID=/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;function exact(id2){if(!DEVICE_ID.test(id2))throw new PhoneError("squad-kit only uses its own iPhone, by its exact id.",`"${id2}" is not a device id`);return id2}var firstLine=s=>(s.split(`
|
|
@@ -239,7 +239,7 @@ ${yaml23}`},ms2+SLACK_MS);if(a.isError)throw failureOf(textOf(a));let said2=text
|
|
|
239
239
|
element:
|
|
240
240
|
${el}
|
|
241
241
|
direction: DOWN
|
|
242
|
-
timeout: ${Math.round(left2())}`,left2())}case"press":{step2.target&&await driver.act(appId,{action:"click",target:step2.target},left2());let key=step2.key??"Enter";if(key==="Escape")return await keyboardUp(left2())?run7(appId,"- hideKeyboard",left2()):void 0;if(KEYS[key])return run7(appId,`- pressKey: ${KEYS[key]}`,left2());if(/^(Shift\+)?[A-Za-z0-9]$/.test(key)||key==="Space")return run7(appId,`- inputText: ${q(key==="Space"?" ":key.slice(-1))}`,left2());throw new StepFailure("other",`a phone has no ${key} key: use Enter, Backspace, Escape (it hides the keyboard), a letter or a digit`)}case"wait":{step2.target?await find2(appId,step2.target,ms2):await sleep2(Math.min(step2.ms??0,5e3));return}default:throw new StepFailure("other",`${step2.action} is not a step the phone knows`)}},async screenText(ms2){return screenWords(await screen(ms2))},get alive(){return mcp.alive},async close(){o.signal?.removeEventListener("abort",onAbort),await stop()}};return driver}var maestroDriver={name:"maestro",label:"Maestro",open:o=>openMaestro(o)};var TRACKERS=[azureTracker],CODE_HOSTS=[azureCodeHost],SPEECH=[munsitSpeech],AI_TOOLS=[claudeTool,codexTool],BROWSERS=[playwrightBrowser],PHONES=[iosSimulator,androidEmulator],PHONE_DRIVERS=[maestroDriver];function rootOf(bp){return bp.root}function trackerModule(name){return TRACKERS.find(m=>m.name===name)}function trackerFor(bp){let root=rootOf(bp),configFile=buildPaths(root).configFile;return fs56.existsSync(configFile)?trackerModule(loadConfig(configFile).tracker.type)?.open(root)??null:null}function repoHostFor(bp,remoteUrl){let root=rootOf(bp);for(let m of CODE_HOSTS){let host3=m.repoFor(root,remoteUrl);if(host3)return host3}return null}function guessTracker(remoteUrls){for(let url of remoteUrls)for(let m of TRACKERS){let values=m.guess(url);if(values)return{module:m,values}}return null}function speechFor(bp,opts={}){let root=rootOf(bp);for(let m of SPEECH){let s=m.open(root,opts);if(s)return s}return null}function speechModule(){return SPEECH[0]}function shotBrowser(){return BROWSERS[0]}function shotPhone(system="ios"){return system==="android"?PHONES[1]:PHONES[0]}function phoneDriver(){return PHONE_DRIVERS[0]}function aiTool(name){return AI_TOOLS.find(t=>t.name===name)}function describeToolEvent(e,cwd){for(let t of AI_TOOLS){let line3=t.describe(e,cwd);if(line3)return line3}return null}import fs57 from"fs";import path48 from"path";var wired=new Map,redisWired=new Map,inside=(dir,p)=>p===dir||p.startsWith(`${dir}${path48.sep}`);function wireCopy(dir,w){wired.set(path48.resolve(dir),w),writeCopyFile(path48.resolve(dir),w)}function wireCopyRedis(dir,w){redisWired.set(path48.resolve(dir),w);for(let f of w.files)writeCopyFile(path48.resolve(dir),{env:{},value:f.value,file:f})}function rewireCopy(dir){let w=wired.get(path48.resolve(dir));w&&writeCopyFile(path48.resolve(dir),w);for(let f of redisWired.get(path48.resolve(dir))?.files??[])writeCopyFile(path48.resolve(dir),{env:{},value:f.value,file:f})}function unwireCopy(dir){wired.delete(path48.resolve(dir))}function unwireCopyRedis(dir){redisWired.delete(path48.resolve(dir))}function wiringFor(map,cwd){let at3=path48.resolve(cwd);for(let[dir,w]of map)if(inside(dir,at3))return[dir,w];let within3=[...map].filter(([dir])=>inside(at3,dir));return within3.length===1?within3[0]:void 0}function copyDbEnv(cwd){return cwd?{...wiringFor(redisWired,cwd)?.[1].env??{},...wiringFor(wired,cwd)?.[1].env??{}}:{}}function ownDbWired(cwd){return!!wiringFor(wired,cwd)}function copyDbFiles(cwd){if(!cwd)return[];let hit=wiringFor(wired,cwd),redis=wiringFor(redisWired,cwd);return[...new Set([...hit?.[1].file?[path48.join(hit[0],hit[1].file.path)]:[],...(redis?.[1].files??[]).map(f=>path48.join(redis[0],f.path))])]}function placeIn(dir,rel){let at3=dir;for(let part of rel.split("/").filter(x=>x&&x!=="."))if(at3=path48.join(at3,part),fs57.lstatSync(at3,{throwIfNoEntry:!1})?.isSymbolicLink())return null;return inside(dir,at3)?at3:null}function writeCopyFile(dir,w){if(!w.file)return;let to=placeIn(dir,w.file.path);if(to)try{writeSetting(to,w.file.form,w.file.key,w.value)}catch{}}function envValue(v){return/[\s#"'\\$`]/.test(v)?`"${v.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`:v}function writeSetting(file3,form,key,value){let before=fs57.existsSync(file3)?fs57.readFileSync(file3,"utf8"):"",after;if(form==="json"){let doc=before.trim()?JSON.parse(before):{},at3=doc,parts2=key.split(":");for(let p of parts2.slice(0,-1))(!at3[p]||typeof at3[p]!="object")&&(at3[p]={}),at3=at3[p];at3[parts2.at(-1)]=value,after=`${JSON.stringify(doc,null,2)}
|
|
242
|
+
timeout: ${Math.round(left2())}`,left2())}case"press":{step2.target&&await driver.act(appId,{action:"click",target:step2.target},left2());let key=step2.key??"Enter";if(key==="Escape")return await keyboardUp(left2())?run7(appId,"- hideKeyboard",left2()):void 0;if(KEYS[key])return run7(appId,`- pressKey: ${KEYS[key]}`,left2());if(/^(Shift\+)?[A-Za-z0-9]$/.test(key)||key==="Space")return run7(appId,`- inputText: ${q(key==="Space"?" ":key.slice(-1))}`,left2());throw new StepFailure("other",`a phone has no ${key} key: use Enter, Backspace, Escape (it hides the keyboard), a letter or a digit`)}case"wait":{step2.target?await find2(appId,step2.target,ms2):await sleep2(Math.min(step2.ms??0,5e3));return}default:throw new StepFailure("other",`${step2.action} is not a step the phone knows`)}},async screenText(ms2){return screenWords(await screen(ms2))},get alive(){return mcp.alive},async close(){o.signal?.removeEventListener("abort",onAbort),await stop()}};return driver}var maestroDriver={name:"maestro",label:"Maestro",open:o=>openMaestro(o)};var TRACKERS=[azureTracker],CODE_HOSTS=[azureCodeHost],SPEECH=[munsitSpeech],AI_TOOLS=[claudeTool,codexTool],BROWSERS=[playwrightBrowser],PHONES=[iosSimulator,androidEmulator],PHONE_DRIVERS=[maestroDriver];function rootOf(bp){return bp.root}function trackerModule(name){return TRACKERS.find(m=>m.name===name)}function trackerFor(bp){let root=rootOf(bp),configFile=buildPaths(root).configFile;return fs56.existsSync(configFile)?trackerModule(loadConfig(configFile).tracker.type)?.open(root)??null:null}function repoHostFor(bp,remoteUrl){let root=rootOf(bp);for(let m of CODE_HOSTS){let host3=m.repoFor(root,remoteUrl);if(host3)return host3}return null}function guessTracker(remoteUrls){for(let url of remoteUrls)for(let m of TRACKERS){let values=m.guess(url);if(values)return{module:m,values}}return null}function speechFor(bp,opts={}){let root=rootOf(bp);for(let m of SPEECH){let s=m.open(root,opts);if(s)return s}return null}function speechModule(){return SPEECH[0]}function shotBrowser(){return BROWSERS[0]}function shotPhone(system="ios"){return system==="android"?PHONES[1]:PHONES[0]}function phoneDriver(){return PHONE_DRIVERS[0]}function aiTool(name){return AI_TOOLS.find(t=>t.name===name)}function describeToolEvent(e,cwd){for(let t of AI_TOOLS){let line3=t.describe(e,cwd);if(line3)return line3}return null}import fs57 from"fs";import path48 from"path";var wired=new Map,redisWired=new Map,inside=(dir,p)=>p===dir||p.startsWith(`${dir}${path48.sep}`);function wireCopy(dir,w){wired.set(path48.resolve(dir),w),writeCopyFile(path48.resolve(dir),w)}function wireCopyRedis(dir,w){redisWired.set(path48.resolve(dir),w);for(let f of w.files)writeCopyFile(path48.resolve(dir),{env:{},value:f.value,file:f})}function rewireCopy(dir){let w=wired.get(path48.resolve(dir));w&&writeCopyFile(path48.resolve(dir),w);for(let f of redisWired.get(path48.resolve(dir))?.files??[])writeCopyFile(path48.resolve(dir),{env:{},value:f.value,file:f})}function unwireCopy(dir){wired.delete(path48.resolve(dir))}function unwireCopyRedis(dir){redisWired.delete(path48.resolve(dir))}function wiringFor(map,cwd){let at3=path48.resolve(cwd);for(let[dir,w]of map)if(inside(dir,at3))return[dir,w];let within3=[...map].filter(([dir])=>inside(at3,dir));return within3.length===1?within3[0]:void 0}function copyDbEnv(cwd){return cwd?{...wiringFor(redisWired,cwd)?.[1].env??{},...wiringFor(wired,cwd)?.[1].env??{}}:{}}function ownDbWired(cwd){return!!wiringFor(wired,cwd)}function copyDbFiles(cwd){if(!cwd)return[];let hit=wiringFor(wired,cwd),redis=wiringFor(redisWired,cwd);return[...new Set([...hit?.[1].file?[path48.join(hit[0],hit[1].file.path)]:[],...(redis?.[1].files??[]).map(f=>path48.join(redis[0],f.path))])]}function placeIn(dir,rel){let at3=dir;for(let part of rel.split("/").filter(x=>x&&x!=="."))if(at3=path48.join(at3,part),fs57.lstatSync(at3,{throwIfNoEntry:!1})?.isSymbolicLink())return null;return inside(dir,at3)?at3:null}function writeCopyFile(dir,w){if(!w.file)return;let to=placeIn(dir,w.file.path);if(to)try{writeSetting(to,w.file.form,w.file.key,w.value)}catch{}}function envValue(v){return/[\s#"'\\$`]/.test(v)?v.includes("$")&&!/['\r\n]/.test(v)&&!v.endsWith("\\")?`'${v}'`:`"${v.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$")}"`:v}function writeSetting(file3,form,key,value){let before=fs57.existsSync(file3)?fs57.readFileSync(file3,"utf8"):"",after;if(form==="json"){let doc=before.trim()?JSON.parse(before):{},at3=doc,parts2=key.split(":");for(let p of parts2.slice(0,-1))(!at3[p]||typeof at3[p]!="object")&&(at3[p]={}),at3=at3[p];at3[parts2.at(-1)]=value,after=`${JSON.stringify(doc,null,2)}
|
|
243
243
|
`}else{let esc2=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),line3=form==="env"?new RegExp(`^(\\s*(?:export\\s+)?)${esc2}\\s*=.*$`):new RegExp(`^(\\s*)${esc2}\\s*[=:].*$`),text4=form==="env"?`${key}=${envValue(value)}`:`${key}=${value.replace(/\\/g,"\\\\")}`,lines3=before?before.replace(/\r?\n$/,"").split(/\r?\n/):[],set2=!1,out22=[];for(let l of lines3){let m=line3.exec(l);m?set2||(out22.push(`${m[1]}${text4}`),set2=!0):out22.push(l)}set2||out22.push(text4),after=`${out22.join(`
|
|
244
244
|
`)}
|
|
245
245
|
`}after!==before&&(fs57.mkdirSync(path48.dirname(file3),{recursive:!0}),fs57.writeFileSync(file3,after,{mode:384}))}import path57 from"path";import{createHash as createHash3}from"crypto";import fs58 from"fs";import os8 from"os";import path49 from"path";var DOCKER_WAIT_MS=20*6e4,DOCKER_STOP_MS=2*6e4,OWN_COMPOSE_FILES=["compose.yaml","compose.yml","docker-compose.yaml","docker-compose.yml"],OWN_OVERRIDES=["compose.override.yaml","compose.override.yml","docker-compose.override.yaml","docker-compose.override.yml"],VALUE_FLAGS=new Set(["-f","--file","--env-file","--profile","--parallel","--ansi","--progress","--project-directory"]),isCompose=command=>/^\s*(docker\s+compose|docker-compose)(\s|$)/.test(command);function dockerSocket(env2=process.env,home2=os8.homedir(),system="/var/run/docker.sock"){let named=/^unix:\/\/(\/.+)$/.exec(env2.DOCKER_HOST?.trim()??"")?.[1];for(let place of[named,path49.join(home2,".docker","run","docker.sock"),system])if(place)try{return fs58.realpathSync(place)}catch{}return null}function composeRun(meta,repo,command,dir,root){if(!isCompose(command))return null;let cannot=why=>({why:`squad-kit cannot run this part's Docker command: ${why}`});if(/[;&|`$<>\n()\\'"]/.test(command))return cannot("it must be one plain docker compose command, with nothing else on the line.");let words2=command.trim().split(/\s+/),start=words2[0]==="docker"?2:1,up=words2.indexOf("up",start);if(up<0)return cannot("it must be docker compose up.");let named=[];for(let i=start;i<up;i+=1){let w=words2[i];if(w==="-p"||w.startsWith("--project-name"))return cannot("squad-kit gives it its own project name, so it must not name one.");if(w==="--project-directory"||w.startsWith("--project-directory="))return cannot("it must run from its own folder.");w==="-f"||w==="--file"?named.push(words2[i+=1]??""):w.startsWith("--file=")?named.push(w.slice(7)):VALUE_FLAGS.has(w)&&(i+=1)}let inFolder=f=>fs58.existsSync(path49.join(dir,f)),files=named.length?named:[...OWN_COMPOSE_FILES.filter(inFolder).slice(0,1),...OWN_OVERRIDES.filter(inFolder)];if(!files.length)return cannot("it has no compose file.");let safe2=(meta.lockedFiles??[]).concat(meta.localFiles??[]).filter(f=>f.repo===repo).map(f=>f.path),open3=files.filter(f=>!safe2.includes(path49.normalize(f)));if(open3.length)return cannot(`${open3.join(", ")} ${open3.length===1?"is":"are"} not locked, so a story could change what it mounts. An engineer can lock the Docker files in Settings.`);let socket=dockerSocket();if(!socket)return cannot("Docker is not running on this computer, or squad-kit cannot find it.");let name=`squad-${createHash3("sha1").update(`${root}
|
|
@@ -362,19 +362,19 @@ ${patterns.join(`
|
|
|
362
362
|
`)]).filter(([,v])=>v!==void 0)),t=joinTicket(bp,id2.toUpperCase(),docs,readYaml(stateFile(bp,id2))??{}),wrong=shapeProblem(t,id2);if(wrong)throw new Error(wrong);let doc=docProblem(docs,t);if(doc)throw new TicketFileProblem(doc.file,doc.detail);return t})}catch(err2){throw new UnreadableTicket(id2.toUpperCase(),err2.message,err2 instanceof TicketFileProblem?err2.file:void 0)}}function idNumber(id2){return Number(id2.replace(/^\D+/,"").replace(/\D.*$/,""))||0}function listTickets(bp){return fs60.existsSync(bp.ticketsDir)?fs60.readdirSync(bp.ticketsDir,{withFileTypes:!0}).filter(d=>d.isDirectory()).map(d=>readableTicket(bp,d.name)).filter(t=>!!t).sort((a,b)=>idNumber(a.id)-idNumber(b.id)||a.id.localeCompare(b.id)):[]}function readTicket(bp,id2){return readTicketFiles(bp,id2)}function readableTicket(bp,id2){try{return readTicketFiles(bp,id2)}catch{return null}}function ticketProblem(bp,id2){try{return readTicketFiles(bp,id2),null}catch(err2){return err2.message}}function brokenTickets(bp,listed3=listTickets(bp)){if(!fs60.existsSync(bp.ticketsDir))return[];let read9=new Set(listed3.map(t=>t.id)),out22=[];for(let d of fs60.readdirSync(bp.ticketsDir,{withFileTypes:!0}))if(!(!d.isDirectory()||read9.has(d.name.toUpperCase())))try{readTicketFiles(bp,d.name)}catch(err2){let e=err2;out22.push({id:e.id,problem:e.message,file:path51.relative(bp.root,path51.join(ticketDir(bp,e.id),e.file)),detail:e.detail})}return out22.sort((a,b)=>idNumber(a.id)-idNumber(b.id)||a.id.localeCompare(b.id))}function requireTicket(bp,id2){let ticket=readTicket(bp,id2);if(!ticket)throw new Error(`No ticket ${id2.toUpperCase()} on this board.`);return ticket}function writeTicket(bp,ticket){let folder=ticketDir(bp,ticket.id),{docs,state}=splitTicket(bp,ticket);for(let f of DOC_FILES){let file3=path51.join(folder,f);docs[f]!==void 0?writeFile(file3,docs[f]):fs60.existsSync(file3)&&fs60.rmSync(file3)}writeFile(stateFile(bp,ticket.id),dumpYaml(state))}function updateTicket(bp,id2,change){let ticket=requireTicket(bp,id2),stage=ticket.stage;return change(ticket),trackStage(ticket,stage),waitsNow(ticket)&&pauseClock(ticket),writeTicket(bp,ticket),ticket}function removeTicket(bp,id2){fs60.rmSync(ticketDir(bp,id2),{recursive:!0,force:!0}),fs60.rmSync(stateFile(bp,id2),{force:!0}),fs60.rmSync(ticketMediaDir(bp,id2),{recursive:!0,force:!0})}function branchNumbers(bp){if(!fs60.existsSync(path51.join(bp.root,".git")))return new Set;let refs=git(bp.root,["for-each-ref","--format=%(refname)","refs/heads/squad","refs/remotes/*/squad/*"],{allowFail:!0});return new Set(refs.split(`
|
|
363
363
|
`).map(r=>r.split("/squad/")[1]??"").filter(Boolean))}function nextTicketId(bp){let meta=readMeta(bp),id2=`${meta.key}-${meta.nextNumber}`,taken=branchNumbers(bp);for(;fs60.existsSync(ticketDir(bp,id2))||taken.has(id2.toLowerCase());)meta.nextNumber+=1,id2=`${meta.key}-${meta.nextNumber}`;return meta.nextNumber+=1,writeMeta(bp,meta),id2}function addActivity(ticket,who4,text4){ticket.activity.unshift({at:now2(),who:who4,text:text4})}var RECORD_WORD={decision:"Decision",release:"Release",breakdown:"Breakdown"};function recordProblemWords(kind,id2){return`${RECORD_WORD[kind]} ${id2} could not be read: its file is damaged, so it is not shown here. Nothing is lost. An engineer can look at it.`}function readDoc(file3,kind){return cached2(`doc:${file3}`,[file3],()=>{let text4=fs60.readFileSync(file3,"utf8"),name=path51.basename(file3,".md"),detail,x=null;try{detail=hasMergeMarks(text4)?"It still has the marks of a merge in it.":null,x=kind.parse(text4),detail??=kind.problem(x,name)}catch(err2){detail=err2.message}return detail?{value:null,detail:kind.named.test(name)||kind.claims.test(text4)?detail:null}:{value:x,detail:null}})}function docFiles(dir){return fs60.existsSync(dir)?fs60.readdirSync(dir).filter(f=>f.endsWith(".md")).map(f=>path51.join(dir,f)):[]}function listDocs(dir,kind){return docFiles(dir).map(f=>readDoc(f,kind).value).filter(x=>x!==null)}function brokenDocs(bp,dir,kind){return docFiles(dir).flatMap(f=>{let{detail}=readDoc(f,kind),id2=path51.basename(f,".md");return detail?[{kind:kind.kind,id:id2,problem:recordProblemWords(kind.kind,id2),file:path51.relative(bp.root,f),detail}]:[]})}function dayText(d){return d instanceof Date?d.toISOString().slice(0,10):d}var DECISION_FIELDS={from:"text",approvedBy:"text",date:"text",area:"text",replaces:"text",replacedBy:"text",tickets:"texts",buildsOn:"texts",proposed:"yes-no"},DECISION_DOC={kind:"decision",parse(text4){let d=parseDecisionDoc(text4);return d.date=dayText(d.date),typeof d.id=="string"&&typeof d.title=="string"&&Object.assign(d,{tickets:d.tickets??[],from:d.from??"",approvedBy:d.approvedBy??"",date:d.date??"",checkedBy:d.checkedBy??null}),d},problem(d,name){return typeof d.id!="string"||!/^[\w.-]+$/.test(d.id)?"It has no id.":typeof d.title!="string"||!d.title?"It has no title.":DECISION_DOC.named.test(name)&&d.id!==name?`Its id says ${d.id}, in the file of ${name}.`:d.checkedBy!==null&&(typeof d.checkedBy!="object"||Array.isArray(d.checkedBy))?'"checkedBy" is not in the right form.':wrongField(d,DECISION_FIELDS)},named:/^([A-Za-z][\w-]*\.\d+|ADR-\d+)$/,claims:/^(id: |## Chosen$)/m};function listDecisions(bp){return listDocs(bp.decisionsDir,DECISION_DOC).sort((a,b)=>String(b.date).localeCompare(String(a.date))||idNumber(b.id)-idNumber(a.id)||b.id.localeCompare(a.id))}function brokenDecisions(bp){return brokenDocs(bp,bp.decisionsDir,DECISION_DOC)}function writeDecision(bp,decision){writeFile(path51.join(bp.decisionsDir,`${decision.id}.md`),decisionDoc(decision))}function nextDecisionId(bp,ticketId){let prefix=ticketId?ticketId.toUpperCase():"STACK",taken=new Set([...listDecisions(bp).map(d=>d.id),...brokenDecisions(bp).map(d=>d.id)]),n=1;for(;taken.has(`${prefix}.${n}`);)n+=1;return`${prefix}.${n}`}function versionKey(v){return v.replace(/^v/,"").split(".").map(Number)}function releaseStateFile(bp,version){return path51.join(bp.stateDir,"releases",`${version}.yaml`)}function withReleaseState(bp,r){let state;try{state=readYaml(releaseStateFile(bp,r.version))}catch{state=void 0}return{...r,...Array.isArray(state?.log)?{log:state.log}:{},...Array.isArray(state?.unpushed)&&state.unpushed.length?{unpushed:state.unpushed}:{}}}var RELEASE_FIELDS={from:"text",date:"text",whatsappDraft:"text",tickets:"texts",bugs:"texts",out:"texts",addons:"object",titles:"object",gateStop:"objects"},RELEASE_DOC={kind:"release",parse(text4){let r=parseReleaseDoc(text4);return r.date=dayText(r.date),r.date===void 0&&delete r.date,typeof r.version=="string"&&Object.assign(r,{tickets:r.tickets??[],from:r.from??"",addons:r.addons??{changelog:!0,tag:!0,pipeline:!0,whatsapp:!0}}),r},problem(r,name){return typeof r.version!="string"||!/^v\d+\.\d+\.\d+[\w.-]*$/.test(r.version)?"It has no version.":r.status!=="draft"&&r.status!=="released"?`"${String(r.status)}" is not a release's state.`:RELEASE_DOC.named.test(name)&&r.version!==name?`Its version says ${r.version}, in the file of ${name}.`:wrongField(r,RELEASE_FIELDS)},named:/^v\d+\.\d+\.\d+[\w.-]*$/,claims:/^(version: |## Notes$)/m};function listReleases(bp){return listDocs(bp.releasesDir,RELEASE_DOC).map(r=>withReleaseState(bp,r)).sort((a,b)=>{let[x,y]=[versionKey(a.version),versionKey(b.version)];for(let i=0;i<3;i+=1)if(x[i]!==y[i])return(y[i]??0)-(x[i]??0);return 0})}function brokenReleases(bp){return brokenDocs(bp,bp.releasesDir,RELEASE_DOC)}function readRelease(bp,version){let v=version.startsWith("v")?version:`v${version}`,file3=path51.join(bp.releasesDir,`${v}.md`);if(!fs60.existsSync(file3))return null;let{value,detail}=readDoc(file3,RELEASE_DOC);if(detail)throw new Error(recordProblemWords("release",v));return value?withReleaseState(bp,value):null}function writeRelease(bp,release){writeFile(path51.join(bp.releasesDir,`${release.version}.md`),releaseDoc(release)),release.log||release.unpushed?.length?writeFile(releaseStateFile(bp,release.version),dumpYaml({...release.log?{log:release.log}:{},...release.unpushed?.length?{unpushed:release.unpushed}:{}})):fs60.rmSync(releaseStateFile(bp,release.version),{force:!0})}function removeRelease(bp,version){fs60.rmSync(path51.join(bp.releasesDir,`${version}.md`),{force:!0}),fs60.rmSync(releaseStateFile(bp,version),{force:!0})}function draftRelease(bp){return listReleases(bp).find(r=>r.status==="draft")??null}function latestRelease(bp){return listReleases(bp).find(r=>r.status==="released")??null}function codeChanged(bp){let meta=readMeta(bp);if(meta.engine==="live")return meta.blueprint?.status==="pending"||meta.blueprint?.status==="running"||(meta.blueprint={stack:[],patterns:[],...meta.blueprint,status:"pending"},writeMeta(bp,meta)),meta.blueprint}function queueImpactCheck(bp,adr){let meta=readMeta(bp);meta.engine!=="live"||meta.impactChecks?.some(c=>c.adr===adr)||writeMeta(bp,{...meta,impactChecks:[...meta.impactChecks??[],{adr,status:"pending"}]})}function clearBoard(bp){for(let dir of[bp.ticketsDir,bp.decisionsDir,bp.releasesDir,bp.rulesDir,bp.knowledgeDir,bp.localDir])fs60.rmSync(dir,{recursive:!0,force:!0})}function newProjectId(name){return`${name.replace(/[^\w.-]+/g,"-").toLowerCase()||"project"}-${randomUUID3().slice(0,8)}`}import fs61 from"fs";import path52 from"path";var LANG={".py":"Python",".ts":"TypeScript",".tsx":"TypeScript",".js":"JavaScript",".jsx":"JavaScript",".mjs":"JavaScript",".vue":"JavaScript",".cs":"C#",".java":"Java",".kt":"Kotlin",".go":"Go",".rb":"Ruby",".php":"PHP",".rs":"Rust",".swift":"Swift",".dart":"Dart",".html":"HTML"};function reposOf(meta){return meta.repos?.length?meta.repos:[{path:".",base:meta.git?.base??"main",remote:meta.git?.remote??"",commands:meta.commands??{}}]}function repoFor(meta,ticket){let all=reposOf(meta);return all.find(r=>r.path===ticket.repo)??all[0]}function baseCommits(root,meta){let out22={};for(let r of reposOf(meta)){let sha3=git(path52.join(root,r.path),["rev-parse","--verify","--quiet",`${r.base}^{commit}`],{allowFail:!0});sha3&&(out22[r.path]=sha3)}return out22}function filesMoved(root,meta,read9,skip){return reposOf(meta).some(r=>{let dir=path52.join(root,r.path),from=read9.commits?.[r.path]??(read9.at?git(dir,["rev-list","-1","--first-parent",`--before=${read9.at}`,r.base],{allowFail:!0}):"");if(!from)return!0;let own2=skip?path52.relative(dir,skip):"",keepOut=own2&&!own2.startsWith("..")&&!path52.isAbsolute(own2)?[`:(exclude)${own2}`]:[],changes;try{changes=git(dir,["diff","--name-status","-M",from,r.base,"--",".",...keepOut])}catch{return!0}return changes.split(`
|
|
364
364
|
`).some(l=>/^[ADRC]/.test(l))})}var SKIP=new Set(["node_modules",".git",".squad","dist","build",".next","vendor"]);function findNestedRepos(root){return fs61.existsSync(root)?fs61.readdirSync(root,{withFileTypes:!0}).filter(d=>d.isDirectory()&&!SKIP.has(d.name)&&!d.name.startsWith(".")).filter(d=>fs61.existsSync(path52.join(root,d.name,".git"))).map(d=>{let dir=path52.join(root,d.name);return{path:d.name,base:currentBranch(dir),remote:remotes(dir)[0]??"",commands:{}}}):[]}var HELPERS=new Set(["scripts","tools","docs"]);function isOwnCode(file3){if(!LANG[path52.extname(file3).toLowerCase()])return!1;let parts2=file3.split("/");return!HELPERS.has(parts2[0])&&!parts2.some(p=>p.startsWith(".")||SKIP.has(p))}function workspaceRepos(root){let nested=findNestedRepos(root);if(!nested.length||!isRepo(root))return nested;let files=(...args)=>git(root,["ls-files","-z",...args],{allowFail:!0}).split("\0").filter(Boolean),inside5=(file3,r)=>file3===r.path||file3.startsWith(`${r.path}/`),tracked=files("--cached"),alone=nested.filter(r=>fs61.statSync(path52.join(root,r.path,".git")).isDirectory()&&!tracked.some(f=>inside5(f,r)));return[...tracked,...files("--others","--exclude-standard")].filter(f=>!alone.some(r=>inside5(f,r))).some(isOwnCode)?[]:alone}function rulesFiles(root){let found2=[];for(let f of["CLAUDE.md","AGENTS.md"]){let p=path52.join(root,f);fs61.existsSync(p)&&found2.push(p)}let rulesDir=path52.join(root,".claude","rules");if(fs61.existsSync(rulesDir))for(let f of fs61.readdirSync(rulesDir))f.endsWith(".md")&&found2.push(path52.join(rulesDir,f));let wiki=path52.join(root,".squad","board","context","wiki");if(fs61.existsSync(wiki))for(let f of fs61.readdirSync(wiki).sort())f.endsWith(".md")&&found2.push(path52.join(wiki,f));return found2}var ENV_FILE_BYTES=256*1024;function cleanPath(file3){return file3.trim().replace(/\\/g,"/").split("/").filter(x=>x&&x!==".").join("/")}function envFilesOf(commands){return[...new Set((commands?.envFile??"").split(",").map(cleanPath).filter(Boolean))]}function repoEnvFiles(meta,repo){return envFilesOf(reposOf(meta).find(r=>r.path===repo)?.commands)}function envFileProblem(file3){let text4=file3.trim().replace(/\\/g,"/"),parts2=text4.split("/").filter(x=>x&&x!==".");return parts2.length?text4.startsWith("/")||/^[a-z]:/i.test(text4)||parts2.includes("..")?"The env file must be inside the repo, like .env.dev.":parts2.includes(".git")||parts2.includes(SQUAD_DIR)?`${parts2.join("/")} is not an env file of the app.`:text4.endsWith("/")?`${parts2.join("/")} is a folder. Name the env file in it, like .env.dev.`:null:"Write the env file's path inside the repo, like .env.dev."}function envFileSetting(given){let list8=(Array.isArray(given)?given:(given??"").split(",")).map(f=>f.trim()).filter(Boolean);for(let f of list8){let problem2=envFileProblem(f);if(problem2)throw new Error(problem2)}return[...new Set(list8.map(cleanPath))].join(", ")}function checkEnvFiles(meta){for(let c of[meta.commands,...(meta.repos??[]).map(r=>r.commands)]){if(!c||c.envFile===void 0)continue;let value=envFileSetting(c.envFile);value?c.envFile=value:delete c.envFile}}function setEnvFile(bp,repo,files){let meta=readMeta(bp),repos=reposOf(meta).map(r2=>r2.path),r=repo?.trim()||(repos.length===1?repos[0]:"");if(!repos.includes(r))throw new Error(r?`There is no code repo at ${r}. The repos: ${repos.join(", ")}.`:`Say which repo it is for: ${repos.join(", ")}.`);let value=envFileSetting(files),put=c=>{let{envFile:_was,...rest}=c;return value?{...rest,envFile:value}:rest};return meta.repos?.length?writeMeta(bp,{...meta,repos:meta.repos.map(x=>x.path===r?{...x,commands:put(x.commands)}:x)}):writeMeta(bp,{...meta,commands:put(meta.commands??{})}),{repo:r,files:envFilesOf({envFile:value})}}function unescape(c){return c==="n"?`
|
|
365
|
-
`:c==="r"?"\r":c}function parseEnvFile(text4){let s=text4.replace(/^\uFEFF/,"").replace(/\r\n?/g,`
|
|
366
|
-
`),out22=new Map,i=0;for(;i<s.length;){let nl=s.indexOf(`
|
|
367
|
-
`,i),lineEnd=nl<0?s.length:nl,m=/^[ \t]*(?:export[ \t]+)?([A-Za-z_][\w.-]*)[ \t]*=[ \t]*/.exec(s.slice(i,lineEnd));if(!m){i=lineEnd+1;continue}let key=m[1],at3=i+m[0].length,quote=s[at3];if(quote==='"'||quote==="'"){let v="",j=at3+1;for(;j<s.length&&s[j]!==quote;){if(quote==='"'&&s[j]==="\\"&&j+1<s.length){v+=unescape(s[j+1]),j+=2;continue}v+=s[j],j+=1}if(j<s.length){out22.set(key,v);let after=s.indexOf(`
|
|
368
|
-
`,j);i=after<0?s.length:after+1;continue}}let raw=s.slice(at3,lineEnd),spaced=m[0].length>m[0].trimEnd().length;out22.set(key,(spaced&&raw.startsWith("#")?"":raw.split(/[ \t]#/)[0]).trim()),i=lineEnd+1}return[...out22]}function readEnvFiles(top,files){let out22={env:{},read:[],missing:[],refused:[]},real2;try{real2=fs62.realpathSync(top)}catch{return{...out22,missing:[...files]}}for(let f of files){let file3;try{file3=fs62.realpathSync(path53.join(real2,f))}catch{out22.missing.push(f);continue}let stat=fs62.statSync(file3,{throwIfNoEntry:!1});if(!file3.startsWith(`${real2}${path53.sep}`)||!stat?.isFile()||stat.size>ENV_FILE_BYTES){out22.refused.push(f);continue}for(let[k,v]of parseEnvFile(fs62.readFileSync(file3,"utf8")))out22.env[k]=v;out22.read.push(f)}return out22}function readText2(file3){try{return fs62.readFileSync(file3,"utf8")}catch{return""}}function gitTop(dir){let at3;try{at3=fs62.realpathSync(dir)}catch{return null}for(;;){let dotGit=path53.join(at3,".git"),stat=fs62.lstatSync(dotGit,{throwIfNoEntry:!1});if(stat?.isDirectory())return{top:at3,source:at3,copy:!1};if(stat?.isFile()){let own2=/^gitdir:\s*(.+)$/m.exec(readText2(dotGit))?.[1]?.trim();if(!own2)return null;let gitDir=path53.resolve(at3,own2),common=path53.resolve(gitDir,readText2(path53.join(gitDir,"commondir")).trim()||".");if(path53.basename(common)!==".git")return null;try{return{top:at3,source:fs62.realpathSync(path53.dirname(common)),copy:!0}}catch{return null}}let up=path53.dirname(at3);if(up===at3)return null;at3=up}}function envFilesFor(dir){try{let g=gitTop(dir),root=g?findSquadRoot(g.source):null;if(!g||!root)return null;let meta=readMeta(boardPaths(path53.join(root,SQUAD_DIR)));for(let r of reposOf(meta)){let source2;try{source2=fs62.realpathSync(path53.join(root,r.path))}catch{continue}if(source2!==g.source)continue;let files=envFilesOf(r.commands);return files.length?{top:g.top,repo:r.path,files,copy:g.copy}:null}}catch{}return null}function envFilesIn(dir,copies=!1){let part=envFilesFor(dir);if(!part||copies&&!part.copy)return[];let from;try{from=fs62.realpathSync(dir)}catch{return[]}return part.files.map(f=>path53.relative(from,path53.join(part.top,f)).split(path53.sep).join("/")).filter(f=>f&&!f.startsWith(".."))}import fs65 from"fs";import path56 from"path";import yaml8 from"js-yaml";import{createHash as createHash5}from"crypto";import fs63 from"fs";import path54 from"path";var DbError=class extends Error{constructor(message,reason){super(message);this.reason=reason;this.name="DbError"}reason};var LOCAL=new Set(["","localhost","127.0.0.1","::1","[::1]","0.0.0.0","host.docker.internal"]);function normalHost(host3){let h=(host3??"").trim().toLowerCase();return LOCAL.has(h)?"localhost":h}var DEFAULT_PORT={postgres:5432,mysql:3306,mongo:27017},SCHEMES={postgres:"postgres",postgresql:"postgres",mysql:"mysql",mysql2:"mysql",mariadb:"mysql",mongodb:"mongo","mongodb+srv":"mongo",sqlite:"sqlite",sqlite3:"sqlite",file:"sqlite"};function kindOfAddress(address){let a=address.trim(),scheme=/^(?:jdbc:)?([a-z][a-z0-9+.-]*):/i.exec(a)?.[1]?.toLowerCase();if(scheme&&SCHEMES[scheme])return SCHEMES[scheme];let pairs=readPairs(a);if(pairs){let keys=new Set(pairs.map(p=>p.key));return keys.has("data source")&&/\.(db|sqlite3?|db3)$/i.test(pairs.find(p=>p.key==="data source").value)||keys.has("filename")?"sqlite":keys.has("dbname")||keys.has("sslmode")?"postgres":keys.has("uid")||keys.has("pwd")?"mysql":null}return/\.(db|sqlite3?|db3)$/i.test(a)?"sqlite":null}function safePart(text4){return text4.normalize("NFKD").replace(/[\u0300-\u036f]/g,"").toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")}var MAX_NAME=63,shortHash=text4=>createHash5("sha256").update(text4).digest("hex").slice(0,6);function fitName(base2,tail4){let full=`${base2}_${tail4}`;if(full.length<=MAX_NAME)return full;let t=tail4.length>30?`${tail4.slice(0,23)}_${shortHash(tail4)}`:tail4,room=MAX_NAME-t.length-1-7;return`${base2.slice(0,Math.max(1,room)).replace(/_+$/,"")}_${shortHash(base2)}_${t}`}function baseName(sourceName){return safePart(sourceName)||"db"}function storyName(sourceName,storyId){let story2=safePart(storyId);if(!story2)throw new DbError(`The story id "${storyId}" has no letters or digits to name a database with.`,"bad-address");return fitName(baseName(sourceName),story2)}function templateName(sourceName){return fitName(baseName(sourceName),"sk_template")}var SYSTEM_NAMES={postgres:new Set(["postgres","template0","template1"]),mysql:new Set(["mysql","information_schema","performance_schema","sys"]),mongo:new Set(["admin","local","config"]),sqlite:new Set};function readPairs(text4){if(!/^\s*[A-Za-z][\w ]*=/.test(text4)||/^[a-z][a-z0-9+.-]*:\/\//i.test(text4))return null;let semi=text4.includes(";")||!/^\s*\w+=\S*(\s+\w+=\S*)*\s*$/.test(text4.replace(/'(?:[^'\\]|\\.)*'/g,"x")),pairs=[],i=0;for(;i<text4.length;){for(;i<text4.length&&(text4[i]===";"||/\s/.test(text4[i]));)i++;if(i>=text4.length)break;let eq=text4.indexOf("=",i);if(eq<0)return null;let key=text4.slice(i,eq).trim().toLowerCase();if(i=eq+1,!semi)for(;i<text4.length&&text4[i]===" ";)i++;let start=i,value="",q5=text4[i];if(q5==='"'||q5==="'"){for(i++,start=i;i<text4.length;){if(text4[i]==="\\"&&!semi&&i+1<text4.length){value+=text4[i+1],i+=2;continue}if(text4[i]===q5){if(semi&&text4[i+1]===q5){value+=q5,i+=2;continue}break}value+=text4[i],i++}pairs.push({key,value,start,end:i}),i++}else{for(;i<text4.length&&(semi?text4[i]!==";":!/\s/.test(text4[i]));)i++;value=text4.slice(start,i).trim(),pairs.push({key,value,start,end:start+text4.slice(start,i).trimEnd().length})}}return pairs.length?pairs:null}var pick=(pairs,...keys)=>pairs.find(p=>keys.includes(p.key));function putPair(text4,p,value){let v=text4[p.start-1]==='"'||text4[p.start-1]==="'"?value:/[;\s'"]/.test(value)?`"${value.replace(/"/g,'""')}"`:value;return text4.slice(0,p.start)+v+text4.slice(p.end)}function readLink(text4){let m=/^((?:jdbc:)?)([a-z][a-z0-9+.-]*):\/\//i.exec(text4);if(!m)return null;let rest=text4.slice(m[0].length),endAuthority=rest.search(/[/?#]/),authEnd=endAuthority<0?rest.length:endAuthority,at3=rest.lastIndexOf("@",authEnd),auth2=at3>=0?rest.slice(0,at3):"",hosts=rest.slice(at3+1,authEnd),tail4=rest.slice(authEnd),pm=/^(\/[^?#]*)?(\?[^#]*)?(#.*)?$/.exec(tail4),p=pm?.[1]??"",query=pm?.[2]??"",colon=auth2.indexOf(":"),dec2=s=>{try{return decodeURIComponent(s)}catch{return s}},head=text4.slice(0,m[0].length+authEnd);return{scheme:m[2].toLowerCase(),...auth2?{user:dec2(colon>=0?auth2.slice(0,colon):auth2)}:{},...colon>=0?{password:dec2(auth2.slice(colon+1))}:{},hosts,path:dec2(p.replace(/^\//,"")),query,before:head+"/",after:query+(pm?.[3]??"")}}function queryParams(query){let out22={};for(let[k,v]of new URLSearchParams(query.replace(/^\?/,"")))out22[k.toLowerCase()]=v;return out22}function linkWithoutPassword(text4,l){if(l.password===void 0)return text4.replace(/([?&](?:password|pwd)=)[^&#]*/gi,"$1***");let m=/^((?:jdbc:)?[a-z][a-z0-9+.-]*:\/\/)/i.exec(text4),rest=text4.slice(m[0].length),authEnd=(()=>{let e=rest.search(/[/?#]/);return e<0?rest.length:e})(),at3=rest.lastIndexOf("@",authEnd),auth2=rest.slice(0,at3),user=auth2.slice(0,auth2.indexOf(":"));return(m[1]+`${user}:***@`+rest.slice(at3+1)).replace(/([?&](?:password|pwd)=)[^&#]*/gi,"$1***")}function serverOf(kind,host3,port,socket){return socket?`${kind}:socket:${path54.resolve(socket)}`:`${kind}:${normalHost(host3)}:${port??DEFAULT_PORT[kind]??""}`}function hostPort(text4){let t=text4.trim(),v6=/^\[([^\]]+)\](?::(\d+))?$/.exec(t);if(v6)return{host:v6[1],...v6[2]?{port:Number(v6[2])}:{}};let m=/^([^:]*)(?::(\d+))?$/.exec(t);if(!m)return{host:t};let host3=m[1];try{host3=decodeURIComponent(host3)}catch{}return{host:host3,...m[2]?{port:Number(m[2])}:{}}}function bad(kind,why){throw new DbError(`squad-kit could not read the ${label(kind)} address: ${why}.`,"bad-address")}function label(kind){return{sqlite:"SQLite",postgres:"Postgres",mysql:"MySQL",mongo:"MongoDB"}[kind]}function readAddress(kind,address){let text4=address.trim();if(text4||bad(kind,"it is empty"),kind==="sqlite")return readSqlite(text4);let link=readLink(text4);if(link){if(kind==="mongo")return readMongoLink(text4,link);let params2=queryParams(link.query),first2=link.hosts.split(",")[0]??"",hp2=hostPort(first2),socket2=params2.host?.startsWith("/")?params2.host:params2.socket,name=link.path||params2.dbname||params2.database||"";name||bad(kind,"it names no database");let user2=link.user??params2.user??params2.username,password5=link.password??params2.password??params2.pwd;return{kind,name,host:hp2.host||(socket2?"":"localhost"),...hp2.port?{port:hp2.port}:params2.port?{port:Number(params2.port)}:{},...socket2?{socket:socket2}:{},...user2?{user:user2}:{},...password5!==void 0?{password:password5}:{},params:params2,withName:n=>link.path||!params2.dbname&&!params2.database?link.before+encodeURIComponent(n)+link.after:text4.replace(/([?&](?:dbname|database)=)[^&#]*/i,`$1${encodeURIComponent(n)}`),safe:linkWithoutPassword(text4,link),server:serverOf(kind,hp2.host,hp2.port??(params2.port?Number(params2.port):void 0),socket2)}}let pairs=readPairs(text4);pairs||bad(kind,"it is not a link or a list of key=value settings");let nameP=pick(pairs,"database","dbname","initial catalog","db");(!nameP||!nameP.value)&&bad(kind,"it names no database");let hostP=pick(pairs,"host","server","data source","hostaddr","address","addr","network address"),portP=pick(pairs,"port"),hp=hostPort(hostP?.value??"localhost"),socket=pick(pairs,"socket","unix_socket")?.value||(hp.host.startsWith("/")?hp.host:void 0),user=pick(pairs,"user","username","user id","userid","uid","user name")?.value,passP=pick(pairs,"password","pwd"),params={};for(let p of pairs)params[p.key]=p.value;let port=portP?Number(portP.value):hp.port,safe2=passP?putPair(text4,passP,"***"):text4;return{kind,name:nameP.value,host:socket?"":hp.host,...port?{port}:{},...socket?{socket}:{},...user?{user}:{},...passP?{password:passP.value}:{},params,withName:n=>putPair(text4,nameP,n),safe:safe2,server:serverOf(kind,hp.host,port,socket)}}function readMongoLink(text4,link){link.scheme!=="mongodb"&&link.scheme!=="mongodb+srv"&&bad("mongo","it is not a mongodb:// link");let params=queryParams(link.query);link.path||bad("mongo","it names no database (add it after the hosts: mongodb://host:27017/name)");let hosts=link.hosts.split(",").map(h=>{let hp=hostPort(h);return`${normalHost(hp.host)}:${hp.port??(link.scheme==="mongodb+srv"?"":27017)}`}).sort().join(","),first2=hostPort(link.hosts.split(",")[0]??"");return{kind:"mongo",name:link.path,host:first2.host,...first2.port?{port:first2.port}:{},...link.user?{user:link.user}:{},...link.password!==void 0?{password:link.password}:{},params,hosts:link.hosts,srv:link.scheme==="mongodb+srv",withName:n=>{let keep=link.user&&!params.authsource?`${link.query?"&":"?"}authSource=${encodeURIComponent(link.path)}`:"",q5=link.query+keep;return link.before+encodeURIComponent(n)+q5+link.after.slice(link.query.length)},safe:linkWithoutPassword(text4,link),server:`mongo:${link.scheme==="mongodb+srv"?"srv:":""}${hosts}`}}function readSqlite(text4){let file3=text4,rewrite=p=>p,pairs=/^\s*(data source|datasource|filename)\s*=/i.test(text4)?readPairs(text4):null;if(pairs){let p=pick(pairs,"data source","datasource","filename");file3=p.value,rewrite=n=>putPair(text4,p,n)}else{let m=/^(jdbc:sqlite:|sqlite3?:(?:\/\/)?|file:(?:\/\/(?=\/))?)(.*)$/i.exec(text4);if(m){let prefix=m[1],[p,q5=""]=m[2].split("?");file3=decodeURIComponent(p),rewrite=n=>prefix+n+(q5?`?${q5}`:"")}}return(!file3||file3===":memory:")&&bad("sqlite","it names no file"),path54.isAbsolute(file3)||bad("sqlite",`the file must be given by its full path, not "${file3}"`),{kind:"sqlite",name:path54.resolve(file3),host:"",params:{},withName:rewrite,safe:text4,server:"sqlite"}}function realFile(file3){try{return fs63.realpathSync.native(file3)}catch{try{return path54.join(fs63.realpathSync.native(path54.dirname(file3)),path54.basename(file3))}catch{return path54.resolve(file3)}}}function sameFile(a,b){let ra=realFile(a),rb=realFile(b);if(ra===rb||process.platform!=="linux"&&ra.toLowerCase()===rb.toLowerCase())return!0;try{let sa=fs63.statSync(a),sb2=fs63.statSync(b);return sa.ino===sb2.ino&&sa.dev===sb2.dev}catch{return!1}}function isPersonsOwn(kind,address,personsAddress2){let a,p;try{a=readAddress(kind,address),p=readAddress(kind,personsAddress2)}catch{return!0}return kind==="sqlite"?sameFile(a.name,p.name):a.name.toLowerCase()===p.name.toLowerCase()}function refuseOwn(kind,address,personsAddress2,doing){if(isPersonsOwn(kind,address,personsAddress2))throw new DbError(`squad-kit never ${doing} your own database. It only reads it, to make the template.`,"persons-own");let name=readAddress(kind,address).name;if(SYSTEM_NAMES[kind].has(name.toLowerCase()))throw new DbError(`squad-kit never ${doing} "${name}": the ${label(kind)} server needs it for itself.`,"persons-own")}import fs64 from"fs";import path55 from"path";import yaml7 from"js-yaml";var SKIP_DIRS=new Set(["node_modules",".git",".hg",".svn",".squad","dist","build","out",".next",".nuxt",".svelte-kit",".output",".turbo",".cache","coverage",".venv","venv","__pycache__","target","vendor","Pods",".gradle",".idea",".vscode","bin","obj","wwwroot"]),LIMITS={depth:6,entries:8e3,fileBytes:256*1024,code:600},COMPOSE=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$/i,ENV_FILE=/^\.env(?:\.[\w.-]+)?$/,EXAMPLE=/(?:^|\.)(?:example|sample|template|dist|defaults|tpl)$/i,OTHER_PLACE=/\.env\.(?:[\w-]+\.)*(?:prod|production|beta|staging|stage|uat|qa|preprod|live|release)(?:\.[\w.-]+)?$/i,APPSETTINGS=/^appsettings(?:\.[\w-]+)?\.json$/i,SPRING=/^application(?:-[\w-]+)?\.(?:properties|ya?ml)$/,CODE=/\.(?:m?[jt]sx?|c[jt]s|py|cs)$/i,DB_ENV=/^(?:[A-Z0-9]+_)*(?:DATABASE|DB|MONGO|MONGODB|POSTGRES|POSTGRESQL|PG|MYSQL|MARIADB|SQLITE)(?:_[A-Z0-9]+)*_(?:URL|URI|DSN|CONNECTION|CONNECTION_STRING|CONN|CONN_STRING|PATH|FILE)$/,NOT_THE_APPS=/(?:^|_)(?:SHADOW|TEST|TESTS|TESTING|CI|E2E|READ|REPLICA|DIRECT)(?:_|$)/,SCHEMES2={postgres:"postgres",postgresql:"postgres",postgis:"postgres",mysql:"mysql",mysql2:"mysql",mysqlx:"mysql",mariadb:"mysql",mongodb:"mongo","mongodb+srv":"mongo",sqlite:"sqlite",sqlite3:"sqlite",file:"sqlite",sqlserver:"sqlserver",mssql:"sqlserver"},unquote=s=>s.trim().replace(/^(['"`])(.*)\1$/s,"$2").trim();function pairsOf(address){if(!/^[\w\s]+=/.test(address)||/^[a-z][\w+.-]*:\/\//i.test(address))return null;let out22=new Map;for(let part of address.split(";")){let at3=part.indexOf("=");at3>0&&out22.set(part.slice(0,at3).trim().toLowerCase(),part.slice(at3+1).trim())}return out22.size?out22:null}var SQLITE_FILE=/\.(?:db|sqlite3?|db3)$/i;function kindOf(address){let a=unquote(address),scheme=/^(?:jdbc:)?([a-z][a-z0-9+.-]*):/i.exec(a)?.[1]?.toLowerCase();if(scheme&&SCHEMES2[scheme])return SCHEMES2[scheme];let pairs=pairsOf(a);if(pairs){let has4=(...keys)=>keys.some(k=>pairs.has(k));return SQLITE_FILE.test(pairs.get("data source")??pairs.get("filename")??"")?"sqlite":has4("trusted_connection","integrated security","initial catalog","encrypt","trustservercertificate","multipleactiveresultsets")?"sqlserver":has4("host")&&has4("database","username")?"postgres":has4("uid","pwd")||pairs.get("port")==="3306"?"mysql":null}return SQLITE_FILE.test(a)&&!/\s/.test(a)?"sqlite":null}function readAddress2(address){let a=unquote(address).replace(/^jdbc:/i,""),file3=/^(?:file|sqlite3?):(?:\/\/)?(.+)$/i.exec(a);if(file3)return{name:file3[1].split("?")[0],password:!1};let link=/^[a-z][\w+.-]*:\/\/(?:([^:@/]*)(?::([^@/]*))?@)?([^/?#]*)(?:\/([^?#]*))?/i.exec(a);if(link){let first2=(link[3]??"").split(",")[0]??"",[host3,port]=first2.startsWith("[")?[first2.slice(0,first2.indexOf("]")+1),first2.slice(first2.indexOf("]")+2)]:first2.split(":"),name=decodeURIComponent((link[4]??"").split("/")[0]??"");return{...name?{name}:{},...host3?{host:host3}:{},...port?{port}:{},password:!!link[2]}}let pairs=pairsOf(a);if(pairs){let name=pairs.get("database")??pairs.get("initial catalog")??pairs.get("dbname")??(SQLITE_FILE.test(pairs.get("data source")??"")?pairs.get("data source"):void 0)??pairs.get("filename"),server=pairs.get("host")??pairs.get("server")??pairs.get("address")??pairs.get("addr"),[host3,port]=(server??"").replace(/^tcp:/i,"").split(/[:,]/);return{...name?{name}:{},...host3?{host:host3}:{},...pairs.get("port")??port?{port:pairs.get("port")??port}:{},password:!!(pairs.get("password")??pairs.get("pwd"))}}return SQLITE_FILE.test(a)?{name:a,password:!1}:{password:!1}}function withoutPassword(address){return address.replace(/^((?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:)([^@/]*)@/i,(_m,head,pass)=>pass?`${head}***@`:`${head}@`).replace(/((?:^|;)\s*(?:password|pwd)\s*=)([^;]*)/gi,"$1***")}function hasPassword(address){return readAddress2(address).password}function filesOf(dir){let out22={prisma:[],compose:[],env:[],appsettings:[],spring:[],csproj:[],packages:[],code:[]},entries=0,visit=(rel,depth)=>{if(depth>LIMITS.depth||entries>=LIMITS.entries)return;let list8;try{list8=fs64.readdirSync(path55.join(dir,rel),{withFileTypes:!0})}catch{return}if(!list8.some(e=>e.name==="pyvenv.cfg"))for(let e of list8){if(++entries>=LIMITS.entries)return;let p=rel?`${rel}/${e.name}`:e.name;if(e.isDirectory()){!SKIP_DIRS.has(e.name)&&!e.name.startsWith(".")&&visit(p,depth+1);continue}e.isFile()&&(e.name.endsWith(".prisma")?out22.prisma.push(p):COMPOSE.test(e.name)?out22.compose.push(p):ENV_FILE.test(e.name)&&depth<=3?out22.env.push(p):APPSETTINGS.test(e.name)?out22.appsettings.push(p):SPRING.test(e.name)&&(rel.endsWith("resources")||rel==="config"||rel==="")?out22.spring.push(p):e.name.endsWith(".csproj")?out22.csproj.push(p):e.name==="package.json"&&depth<=3?out22.packages.push(p):CODE.test(e.name)&&out22.code.length<LIMITS.code&&out22.code.push(p))}};return visit("",0),out22}function read(file3){try{return fs64.statSync(file3).size>LIMITS.fileBytes?"":fs64.readFileSync(file3,"utf8")}catch{return""}}var PLACEHOLDER=/\$\{[^}]*\}|\$[A-Z_][A-Z0-9_]*/;function envPairs(text4){let out22=new Map;for(let line3 of text4.split(/\r?\n/)){let m=/^\s*(?:export\s+)?([A-Za-z_][\w.]*)\s*=\s*(.*?)\s*$/.exec(line3);if(!m)continue;let value=m[2];/^['"]/.test(value)||(value=value.replace(/\s+#.*$/,"")),out22.set(m[1],unquote(value))}return out22}function composeValue(v){if(v==null)return;let s=String(v),m=/^\$\{[A-Za-z_]\w*:?-([^}]*)\}$/.exec(s);return m?m[1]:PLACEHOLDER.test(s)?void 0:s}function composeEnv(env2){let out22=new Map;if(Array.isArray(env2))for(let item of env2){let s=String(item),at3=s.indexOf("=");at3>0&&out22.set(s.slice(0,at3),composeValue(s.slice(at3+1)))}else if(env2&&typeof env2=="object")for(let[k,v]of Object.entries(env2))out22.set(k,composeValue(v));return out22}var IMAGE_KIND=[[/(?:^|\/)(?:postgres|postgresql|postgis|timescaledb[\w-]*)(?::|$)/i,"postgres"],[/(?:^|\/)(?:mysql|mariadb|percona)(?::|$)/i,"mysql"],[/(?:^|\/)(?:mongo|mongodb(?:-community-server)?)(?::|$)/i,"mongo"],[/mssql|sql-?server|azure-sql-edge/i,"sqlserver"]],KIND_PORT={postgres:5432,mysql:3306,mongo:27017,sqlite:0,sqlserver:1433};function published(ports,target){for(let p of Array.isArray(ports)?ports:[]){if(p&&typeof p=="object"){let o=p;if(Number(o.target)===target&&o.published!==void 0)return String(o.published);continue}let parts2=String(p).split("/")[0].split(":"),inside5=Number(parts2.at(-1));if(inside5===target)return parts2.length>=2?parts2.at(-2):String(inside5)}}function composeDbs(dir,files){let dbs=[],settings=[];for(let file3 of files){let doc;try{doc=yaml7.load(read(path55.join(dir,file3)))}catch{continue}let services=doc?.services;if(!(!services||typeof services!="object"))for(let[service,raw]of Object.entries(services)){let s=raw??{},env2=composeEnv(s.environment),kind=IMAGE_KIND.find(([re])=>re.test(String(s.image??"")))?.[1];if(!kind){for(let[k,v]of env2)DB_ENV.test(k)&&!NOT_THE_APPS.test(k)&&settings.push({file:file3,env:k,...v?{value:v}:{}});continue}let port=published(s.ports,KIND_PORT[kind]),get=(...keys)=>keys.map(k=>env2.get(k)).find(v=>v),at3=`localhost:${port??KIND_PORT[kind]}`,userPass=(user,pass)=>user?`${encodeURIComponent(user)}${pass?`:${encodeURIComponent(pass)}`:""}@`:"",name,address;if(kind==="postgres"){let user=get("POSTGRES_USER")??"postgres";name=get("POSTGRES_DB")??user,address=`postgres://${userPass(user,get("POSTGRES_PASSWORD"))}${at3}/${name}`}else if(kind==="mysql"){let user=get("MYSQL_USER","MARIADB_USER")??"root",pass=user==="root"?get("MYSQL_ROOT_PASSWORD","MARIADB_ROOT_PASSWORD"):get("MYSQL_PASSWORD","MARIADB_PASSWORD");name=get("MYSQL_DATABASE","MARIADB_DATABASE"),name&&(address=`mysql://${userPass(user,pass)}${at3}/${name}`)}else if(kind==="mongo"){name=get("MONGO_INITDB_DATABASE");let user=get("MONGO_INITDB_ROOT_USERNAME");name&&(address=`mongodb://${userPass(user,get("MONGO_INITDB_ROOT_PASSWORD"))}${at3}/${name}${user?"?authSource=admin":""}`)}dbs.push({file:file3,service,kind,...name?{name}:{},...port?{port}:{},...address?{address}:{}})}}return{dbs,settings}}var PROVIDER={postgresql:"postgres",postgres:"postgres",mysql:"mysql",sqlite:"sqlite",mongodb:"mongo",sqlserver:"sqlserver",cockroachdb:"postgres"};function prismaSources(dir,files){let out22=[];for(let file3 of files){let text4=read(path55.join(dir,file3)),block3=/datasource\s+\w+\s*\{([^}]*)\}/.exec(text4)?.[1];if(!block3)continue;let provider=/provider\s*=\s*"(\w+)"/.exec(block3)?.[1],env2=/url\s*=\s*env\(\s*"([^"]+)"\s*\)/.exec(block3)?.[1],url=/url\s*=\s*"([^"]+)"/.exec(block3)?.[1];out22.push({file:file3,...provider&&PROVIDER[provider]?{kind:PROVIDER[provider]}:{},...env2?{env:env2}:{},...url?{url}:{}})}return out22}function looseJson(text4){let bare=text4.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g,(m,str5)=>str5??"").replace(/,(\s*[}\]])/g,"$1");return JSON.parse(bare)}function appsettingsKeys(dir,file3){let doc;try{doc=looseJson(read(path55.join(dir,file3)))}catch{return[]}let out22=[],strings2=doc?.ConnectionStrings;if(strings2&&typeof strings2=="object")for(let[k,v]of Object.entries(strings2))typeof v=="string"&&v.trim()&&out22.push({file:file3,key:`ConnectionStrings:${k}`,value:v});let walk3=(o,at3,depth)=>{if(!(!o||typeof o!="object"||Array.isArray(o)||depth>2)){for(let[k,v]of Object.entries(o))if(!(!at3.length&&k==="ConnectionStrings"))if(typeof v=="string"&&kindOf(v)&&/:\/\/|=/.test(v)){let named=Object.entries(o).find(([n,x])=>/^(?:database(?:name)?|dbname)$/i.test(n)&&typeof x=="string")?.[1];out22.push({file:file3,key:[...at3,k].join(":"),value:v,...named?{name:named}:{}})}else walk3(v,[...at3,k],depth+1)}};return walk3(doc,[],0),out22}var SPRING_KEYS=["spring.datasource.url","spring.data.mongodb.uri","spring.r2dbc.url"];function springKeys(dir,file3){let text4=read(path55.join(dir,file3)),flat=new Map;if(file3.endsWith(".properties"))for(let line3 of text4.split(/\r?\n/)){let m=/^\s*([\w.-]+)\s*[=:]\s*(.*?)\s*$/.exec(line3);m&&!line3.trim().startsWith("#")&&flat.set(m[1],m[2])}else{let doc;try{doc=yaml7.load(text4)}catch{return[]}let walk3=(o,at3)=>{if(!(!o||typeof o!="object"||Array.isArray(o)))for(let[k,v]of Object.entries(o)){let key=at3?`${at3}.${k}`:k;v&&typeof v=="object"?walk3(v,key):v!=null&&flat.set(key,String(v))}};walk3(doc,"")}let out22=[];for(let key of SPRING_KEYS){let raw=flat.get(key);if(!raw)continue;let holder=/^\$\{([A-Za-z_][\w.]*)(?::([^}]*))?\}$/.exec(raw),value=holder?holder[2]??"":raw,user=flat.get("spring.datasource.username"),pass=flat.get("spring.datasource.password"),db=flat.get("spring.data.mongodb.database");out22.push({file:file3,key,value,...holder?{env:holder[1]}:{},...user&&!PLACEHOLDER.test(user)?{user}:{},...pass&&!PLACEHOLDER.test(pass)?{pass}:{},...db?{db}:{}})}return out22}var PACKAGE_KIND=[[/^(?:mongoose|mongodb|@nestjs\/mongoose|mongodb\.driver|mongodb\.entityframeworkcore|mongoengine|pymongo|motor)$/i,"mongo"],[/^(?:pg|postgres|pg-promise|@neondatabase\/serverless|npgsql(?:\.entityframeworkcore\.postgresql)?|psycopg2?(?:-binary)?|asyncpg)$/i,"postgres"],[/^(?:mysql|mysql2|pomelo\.entityframeworkcore\.mysql|mysql\.entityframeworkcore|mysqlconnector|pymysql|mysqlclient)$/i,"mysql"],[/^(?:better-sqlite3|sqlite3|sqlite|microsoft\.entityframeworkcore\.sqlite|microsoft\.data\.sqlite)$/i,"sqlite"],[/^(?:microsoft\.entityframeworkcore\.sqlserver|microsoft\.data\.sqlclient|system\.data\.sqlclient|mssql|tedious)$/i,"sqlserver"]];function packageKinds(dir,files){let names2=[];for(let f of files.packages)try{let p=JSON.parse(read(path55.join(dir,f)));names2.push(...Object.keys({...p.dependencies,...p.devDependencies}))}catch{}for(let f of files.csproj)for(let m of read(path55.join(dir,f)).matchAll(/<PackageReference\s+Include="([^"]+)"/g))names2.push(m[1]);let kinds=[];for(let n of names2){let k=PACKAGE_KIND.find(([re])=>re.test(n))?.[1];k&&!kinds.includes(k)&&kinds.push(k)}return kinds}var ENV_READS=[/process\.env\.([A-Z][A-Z0-9_]*)/g,/process\.env\[\s*['"`]([A-Z][A-Z0-9_]*)['"`]\s*\]/g,/(?:os\.environ\.get|os\.getenv|environ\.get|Deno\.env\.get|Environment\.GetEnvironmentVariable)\(\s*['"]([A-Z][A-Z0-9_]*)['"]/g,/os\.environ\[\s*['"]([A-Z][A-Z0-9_]*)['"]\s*\]/g];function codeReads(dir,files){let env2=new Set,connection=new Set;for(let f of files){let text4=read(path55.join(dir,f));if(text4){for(let re of ENV_READS)for(let m of text4.matchAll(re))(DB_ENV.test(m[1])||/MONGO/.test(m[1]))&&env2.add(m[1]);for(let m of text4.matchAll(/GetConnectionString\(\s*"([^"]+)"\s*\)/g))connection.add(m[1])}}return{env:env2,connection}}function realEnvFile(file3){let dir=path55.posix.dirname(file3),base2=path55.posix.basename(file3).replace(/\.(?:example|sample|template|dist|defaults|tpl)$/i,"");return dir==="."?base2:`${dir}/${base2}`}function findIn2(root,repo){let dir=path55.join(root,repo),files=filesOf(dir),prisma=prismaSources(dir,files.prisma),compose=composeDbs(dir,files.compose),kinds=packageKinds(dir,files),reads=codeReads(dir,files.code),env2=files.env.map(f=>({file:f,example:EXAMPLE.test(f),pairs:envPairs(read(path55.join(dir,f)))})),envValue2=name2=>{let hits=env2.filter(e=>e.pairs.has(name2)&&e.pairs.get(name2)&&!PLACEHOLDER.test(e.pairs.get(name2))),best2=hits.find(e=>!e.example&&!OTHER_PLACE.test(e.file))??hits.find(e=>!e.example)??hits[0];return best2?{file:best2.file,value:best2.pairs.get(name2),example:best2.example}:void 0},envFileOf=name2=>{let own3=env2.find(e=>!e.example&&!OTHER_PLACE.test(e.file)&&e.pairs.has(name2))??env2.find(e=>!e.example&&e.pairs.has(name2))??env2.find(e=>e.pairs.has(name2));return own3?realEnvFile(own3.file):".env"},candidates=[];for(let p of prisma)if(p.env){let v=envValue2(p.env);candidates.push({setting:{env:p.env,file:envFileOf(p.env)},...v?{address:v.value}:{},...p.kind?{kind:p.kind}:{},from:[p.file,...v?[v.file]:[]],score:50})}else p.url&&candidates.push({setting:{file:p.file,key:"datasource url"},address:p.url,...p.kind?{kind:p.kind}:{},from:[p.file],score:35});let dotnet=[...files.appsettings].sort((a,b)=>Number(/development/i.test(b))-Number(/development/i.test(a))||a.length-b.length);for(let f of dotnet)for(let k of appsettingsKeys(dir,f)){let asked=[...reads.connection].some(n=>k.key===`ConnectionStrings:${n}`),kind2=kindOf(k.value)??void 0,name2=k.name??readAddress2(k.value).name;candidates.push({setting:{file:f,key:k.key,env:k.key.split(":").join("__")},address:k.value,...kind2?{kind:kind2}:{},...name2?{name:name2}:{},from:[f],score:40+(asked?5:0)+(/development/i.test(f)?2:0)})}for(let f of files.spring)for(let k of springKeys(dir,f)){let envName=k.env??k.key.toUpperCase().replace(/[.-]/g,"_"),address2=k.value||void 0;address2&&k.user&&!readAddress2(address2).password&&(address2=address2.replace(/^((?:jdbc:)?[a-z][\w+.-]*:\/\/)/i,`$1${encodeURIComponent(k.user)}${k.pass?`:${encodeURIComponent(k.pass)}`:""}@`));let kind2=address2?kindOf(address2)??void 0:void 0;candidates.push({setting:{file:f,key:k.key,env:envName},...address2?{address:address2}:{},...kind2?{kind:kind2}:{},...k.db?{name:k.db}:{},from:[f],score:40+(/(?:^|\/)application\.(?:properties|ya?ml)$/.test(f)?2:0)})}let named=new Set;for(let e of env2)for(let[k,v]of e.pairs)(DB_ENV.test(k)&&!NOT_THE_APPS.test(k)||kindOf(v)&&/:\/\//.test(v)&&!NOT_THE_APPS.test(k))&&named.add(k);for(let c of candidates){let v=c.setting.key&&c.setting.env?envValue2(c.setting.env):void 0;if(!v||v.example||OTHER_PLACE.test(v.file)||!kindOf(v.value))continue;c.address=v.value;let name2=readAddress2(v.value).name;name2&&(c.name=name2),c.from.push(v.file)}for(let k of named){if(candidates.some(c=>c.setting.env===k))continue;let v=envValue2(k),kind2=v?kindOf(v.value)??void 0:void 0;candidates.push({setting:{env:k,file:envFileOf(k)},...v?{address:v.value}:{},...kind2?{kind:kind2}:{},from:v?[v.file]:env2.filter(e=>e.pairs.has(k)).map(e=>e.file),score:25+(reads.env.has(k)?15:0)+(v?5:0)})}for(let k of reads.env)candidates.some(c=>c.setting.env===k)||NOT_THE_APPS.test(k)||candidates.push({setting:{env:k,file:".env"},from:[],score:22});for(let s of compose.settings){if(candidates.some(c=>c.setting.env===s.env)){candidates.find(c=>c.setting.env===s.env).from.push(s.file);continue}candidates.push({setting:{env:s.env,file:".env"},...s.value&&kindOf(s.value)?{kind:kindOf(s.value)}:{},from:[s.file],score:20})}let db=compose.dbs[0];if(!candidates.length&&!db)return null;let best=candidates.sort((a,b)=>b.score-a.score)[0]??{setting:{},from:[],score:10},kind=(best.address?kindOf(best.address):null)??best.kind??prisma.find(p=>p.kind)?.kind??db?.kind??kinds[0];if(!kind)return null;let own2=db&&db.kind===kind?db:void 0,address=best.address&&!(own2&&readAddress2(best.address).host===own2.service)?best.address:own2?.address??best.address,said2=address?readAddress2(address):{password:!1,name:void 0,host:void 0,port:void 0},name=best.name??said2.name??own2?.name,where2=own2?`the ${own2.service} service in ${own2.file}${own2.port?`, on port ${own2.port}`:""}`:kind==="sqlite"?"a file in the project":said2.host?`${said2.host}${said2.port?`:${said2.port}`:""}`:void 0,from=[...new Set([...best.from,...own2?[own2.file]:[]])];return{...best,kind,...address?{address}:{},...name?{name}:{},...where2?{where:where2}:{},from,repo}}function findDatabase(root,repos){let best=repos.map(r=>findIn2(root,r)).filter(x=>!!x).sort((a,b)=>b.score-a.score)[0];if(!best)return{found:null};if(best.kind==="sqlserver")return{found:null,later:"This project uses SQL Server. A database of its own for each story comes for SQL Server in a later version; until then stories use it as before."};let{score:_score,kind,...rest}=best;return{found:{...rest,kind}}}var CODE_GLOBS=["*.cs","*.fs","*.vb","*.ts","*.tsx","*.mts","*.cts","*.js","*.jsx","*.mjs","*.cjs","*.py","*.java","*.kt","*.scala","*.go","*.rb","*.php","*.rs","*.ex","*.exs","*.dart","*.swift"],OPENS=/(?:(?:\b(?:GetDatabase|getDatabase|get_database|getSiblingDB|useDb)|\bclient\.db|\.Database)\(\s*|\bdbName\s*[:=]\s*)(["'`])([^"'`]+)\1/,OPENS_GREP="((GetDatabase|getDatabase|get_database|getSiblingDB|useDb|client\\.db|\\.Database)\\([[:space:]]*|dbName[[:space:]]*[:=][[:space:]]*)[\"'`]",DB_WORDS=/data ?base|db|mongo|catalog|schema|connect|client/i,COMMON_NAMES=new Set(["postgres","postgresql","mysql","mariadb","mongo","mongodb","test","admin","app","db","database","default","main","local","dev"]),TEST_PATH=/(?:^|\/)(?:tests?|__tests__|spec|specs|e2e)\/|[.-](?:test|spec)\.[a-z]+$|Tests?\.(?:cs|fs|java|kt)$|(?:^|\/)test_[^/]*\.py$|_test\.(?:py|go)$/i,SERVER_KINDS=["postgres","mysql","mongo"],GREP_MS=2e4;function grepLines(dir,args){return git(dir,["grep","-n","-I",...args,"--",...CODE_GLOBS],{allowFail:!0,timeoutMs:GREP_MS}).split(`
|
|
369
|
-
`).map(l=>/^(.+?):(\d+):(.*)$/.exec(l)).filter(m=>!!m).map(m=>({file:m[1],line:m[2],text:m[3]}))}var escapeRe=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function composeSets(dir,files,name){let passes=new RegExp(`^\\$(?:\\{${escapeRe(name)}(?::?-[^}]*)?\\}|${escapeRe(name)})$`);for(let file3 of files){let doc;try{doc=yaml7.load(read(path55.join(dir,file3)))}catch{continue}let services=doc?.services;if(!(!services||typeof services!="object"))for(let[service,raw]of Object.entries(services)){let env2=raw?.environment,value;if(Array.isArray(env2))for(let item of env2){let s=String(item),at3=s.indexOf("=");at3>0&&s.slice(0,at3)===name&&(value=s.slice(at3+1))}else env2&&typeof env2=="object"&&(value=env2[name]);if(!(value==null||!String(value).trim())&&!passes.test(String(value).trim()))return{file:file3,service}}}}function storyCopyIgnored(dir,db,address){if(!SERVER_KINDS.includes(db.kind))return;let setting=db.setting.env??db.setting.key??"its setting",takes=`squad-kit gives each story its own copy when the app takes the database's name from ${setting}.`,name=db.name??(address?readAddress2(address).name:void 0);if(name&&!COMMON_NAMES.has(name.toLowerCase())){let quoted4=[`"${name}"`,`'${name}'`,`\`${name}\``],reads=text4=>text4.replace(/\bGetConnectionString\s*\(\s*(["'`])[^"'`]*\1\s*\)|\bConnectionStrings[:_]+\w+/g,""),hits=grepLines(dir,["-F",...quoted4.flatMap(q5=>["-e",q5])]).map(h=>({...h,text:reads(h.text)})).filter(h=>quoted4.some(q5=>h.text.includes(q5))&&DB_WORDS.test(quoted4.reduce((t,q5)=>t.split(q5).join(""),h.text))),at3=h=>`${h.file}:${h.line}`;if(hits.length)return`The code names the database ${name} itself, in ${hits.length===1?at3(hits[0]):`${hits.length} places like ${at3(hits[0])}`}, so it would not use a story's own copy under another name. ${takes}`}for(let h of grepLines(dir,["-E","-e",OPENS_GREP])){let other=TEST_PATH.test(h.file)?void 0:OPENS.exec(h.text)?.[2];if(other)return`The code opens the database ${other} by its name, in ${h.file}:${h.line}, so it would not use a story's own copy under another name. ${takes}`}let files=filesOf(dir),values=[];if(db.setting.env)for(let f of files.env){let v=envPairs(read(path55.join(dir,f))).get(db.setting.env);v&&values.push({value:v,file:f})}address&&values.push({value:address,file:""});for(let{value,file:file3}of values)if(!(!(kindOf(value)||/:\/\//.test(value))||readAddress2(value).name))return`The address in ${file3||setting} names no database, so the app picks its database some other way and would not use a story's own copy. ${takes}`;if(db.setting.env){let set2=composeSets(dir,files.compose,db.setting.env);if(set2)return`${set2.file} sets ${db.setting.env} itself for the ${set2.service} service, from other settings, so the app there would not see a story's own database.`}}var COMPOSE2=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$/i,ENV_FILE2=/^\.env(?:\.[\w.-]+)?$/,EXAMPLE2=/(?:^|\.)(?:example|sample|template|dist|defaults|tpl)$/i,APPSETTINGS2=/^appsettings(?:\.[\w-]+)?\.json$/i,PROPERTIES=/^application(?:-[\w-]+)?\.properties$/,OTHER_PLACE2=/(?:^|[.\-_])(?:prod|production|beta|staging|stage|uat|qa|preprod|live|release|test|tests|testing|ci|e2e)(?:[.\-_]|$)/i,SKIP_DIRS2=new Set(["node_modules",".git",".squad","dist","build","out",".next",".nuxt","coverage",".venv","venv","__pycache__","target","vendor","Pods","bin","obj","wwwroot"]),LIMITS2={depth:4,entries:4e3,fileBytes:256*1024},DEFAULT_PORT2={postgres:5432,mysql:3306,mongo:27017,redis:6379},SCHEMES3={postgres:"postgres",postgresql:"postgres",mysql:"mysql",mysql2:"mysql",mariadb:"mysql",mongodb:"mongo","mongodb+srv":"mongo",redis:"redis",rediss:"redis"},REDIS_OPTIONS=/^(?:abortconnect|defaultdatabase|allowadmin|connectretry|connecttimeout|synctimeout|asynctimeout|ssl|sslhost|sslprotocols|servicename|keepalive|tiebreaker|resolvedns|channelprefix|configchannel|configcheckseconds)$/i;function read2(file3){try{return fs65.statSync(file3).size>LIMITS2.fileBytes?"":fs65.readFileSync(file3,"utf8")}catch{return""}}function filesOf2(dir){let out22={compose:[],env:[],appsettings:[],properties:[]},entries=0,visit=(rel,depth)=>{if(depth>LIMITS2.depth||entries>=LIMITS2.entries)return;let list8;try{list8=fs65.readdirSync(path56.join(dir,rel),{withFileTypes:!0})}catch{return}for(let e of list8.sort((a,b)=>a.name.localeCompare(b.name))){if(++entries>=LIMITS2.entries)return;let p=rel?`${rel}/${e.name}`:e.name;if(e.isDirectory()){!SKIP_DIRS2.has(e.name)&&!e.name.startsWith(".")&&visit(p,depth+1);continue}!e.isFile()||OTHER_PLACE2.test(e.name.replace(/\.(?:ya?ml|json|properties)$/i,""))||(COMPOSE2.test(e.name)?out22.compose.push(p):ENV_FILE2.test(e.name)&&!EXAMPLE2.test(e.name)&&!rel?out22.env.push(p):APPSETTINGS2.test(e.name)?out22.appsettings.push(p):PROPERTIES.test(e.name)&&out22.properties.push(p))}};visit("",0);let plainFirst=(a,b)=>path56.posix.basename(a).split(".").length-path56.posix.basename(b).split(".").length||a.localeCompare(b);return out22.compose.sort(plainFirst),out22.appsettings.sort((a,b)=>-plainFirst(a,b)),out22.properties.sort((a,b)=>-plainFirst(a,b)),out22}function plainValue(v){if(v==null||typeof v=="object")return;let s=String(v).replace(/\$\{[A-Za-z_]\w*:?-([^}]*)\}/g,"$1");return/\$\{|\$[A-Za-z_]/.test(s)?void 0:s}function composeDocs(dir,files){let out22=[];for(let file3 of files){let doc;try{doc=yaml8.load(read2(path56.join(dir,file3)))}catch{continue}let services=doc?.services;services&&typeof services=="object"&&out22.push({file:file3,services})}return out22}function publishes(ports,into){for(let p of Array.isArray(ports)?ports:[]){if(p&&typeof p=="object"){let o=p,target=Number(o.target),published2=Number(plainValue(o.published));(!o.protocol||o.protocol==="tcp")&&target&&published2&&(into[target]=published2);continue}let[spec="",protocol]=(plainValue(p)??"").split("/");if(protocol&&protocol!=="tcp")continue;let m=/^(?:(?:\[[^\]]*\]|[\d.]+):)?(\d+):(\d+)$/.exec(spec.trim());m&&(into[Number(m[2])]=Number(m[1]))}}function composeServices(dir){let byName=new Map;for(let doc of composeDocs(dir,filesOf2(dir).compose))for(let[name,raw]of Object.entries(doc.services)){let s=raw??{},one2=byName.get(name)??{names:[],ports:{}};byName.set(name,one2);let aliases=s.networks&&typeof s.networks=="object"&&!Array.isArray(s.networks)?Object.values(s.networks).flatMap(n=>Array.isArray(n?.aliases)?n.aliases:[]):[];for(let n of[name,s.container_name,s.hostname,...aliases]){let v=plainValue(n)?.trim().toLowerCase();v&&!one2.names.includes(v)&&one2.names.push(v)}publishes(s.ports,one2.ports),s.network_mode==="host"&&(one2.hostNetwork=!0)}return[...byName.values()]}function endpoint(text4,start){let t=text4.trim();if(!t)return null;let at3=start+text4.indexOf(t),v6=/^\[([^\]]+)\](?::(\d+))?$/.exec(t);if(v6)return{host:v6[1],...v6[2]?{port:Number(v6[2])}:{},start:at3,end:at3+t.length};if(/^[\da-f]*:[\da-f]*:[\da-f:.]*$/i.test(t))return{host:t,start:at3,end:at3+t.length};let m=/^([^:/\s]+)(?::(\d+))?$/.exec(t);return m?{host:m[1],...m[2]?{port:Number(m[2])}:{},start:at3,end:at3+t.length}:null}function split(text4,by2,from=0){let out22=[],start=0;for(let part of text4.split(by2))out22.push({text:part,start:from+start}),start+=part.length+by2.length;return out22}function hostsOf(address,redis=!1){let text4=address,link=/^((?:jdbc:)?)([a-z][a-z0-9+.-]*):\/\//i.exec(text4);if(link){let scheme=link[2].toLowerCase(),kind=SCHEMES3[scheme];if(!kind)return null;let from=link[0].length,rest=text4.slice(from),stop=rest.search(/[/?#]/),authEnd=stop<0?rest.length:stop,at3=rest.lastIndexOf("@",authEnd),hostsStart=from+at3+1,raw=text4.slice(hostsStart,from+authEnd),endpoints2=raw?split(raw,",",hostsStart).map(p=>endpoint(p.text,p.start)):[];return endpoints2.some(e=>!e)?null:{form:"link",kind,endpoints:endpoints2,...scheme==="mongodb+srv"?{srv:!0}:{}}}if(/^\s*[A-Za-z][\w ]*=/.test(text4)){let kind=kindOf(text4)??kindOfAddress(text4);if(kind!=="postgres"&&kind!=="mysql")return null;let semi=text4.includes(";"),pairs=semi?split(text4,";"):[...text4.matchAll(/\S+/g)].map(m=>({text:m[0],start:m.index})),endpoints2=[],portAt,port;for(let p of pairs){let eq=p.text.indexOf("=");if(eq<0)continue;let key=p.text.slice(0,eq).trim().toLowerCase(),valueStart=p.start+eq+1,value=p.text.slice(eq+1);if(key==="port"){let v=value.trim();port=Number(v),portAt={start:valueStart+value.indexOf(v),end:valueStart+value.indexOf(v)+v.length}}else if(["host","server","data source","address","addr","network address","hostaddr"].includes(key))for(let e of split(value,",",valueStart)){let one2=endpoint(e.text,e.start);one2&&endpoints2.push(one2)}}for(let e of endpoints2)e.port===void 0&&port&&(e.port=port);return{form:"pairs",kind,endpoints:endpoints2,...portAt?{portAt}:{},semi}}let parts2=split(text4,","),hosts=parts2.filter(p=>!p.text.includes("=")),options=parts2.filter(p=>p.text.includes("="));if(!hosts.length||parts2.indexOf(hosts.at(-1))!==hosts.length-1)return null;let endpoints=hosts.map(p=>endpoint(p.text,p.start));return endpoints.some(e=>!e||!HOST_LIKE.test(e.host))||!options.some(o=>REDIS_OPTIONS.test(o.text.slice(0,o.text.indexOf("=")).trim()))&&!(redis&&(endpoints.every(e=>e.port)||redis==="address"&&!options.length))?null:{form:"list",kind:"redis",endpoints}}var HOST_LIKE=/^(?!(?:true|false|yes|no|on|off|none|null)$)(?!\d+$)[a-z0-9][\w.-]*$|:/i,REDIS_NAME=/redis/i,ADDRESS_NAME=/conn|url|uri|host|server|endpoint|address|addr|config|^connectionstrings__/i;function redisBy(name){return REDIS_NAME.test(name)?ADDRESS_NAME.test(name)?"address":!0:!1}function addressKind(name,value){return hostsOf(value.trim(),redisBy(name))?.kind??null}function isThisComputer(host3){let h=host3.trim().toLowerCase().replace(/^\[(.*)\]$/,"$1");return normalHost(h)==="localhost"||h.startsWith("/")||/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)||h.endsWith(".localhost")||h==="::ffff:127.0.0.1"}function serviceOf(host3,services){let h=host3.trim().toLowerCase();return services.find(s=>s.names.includes(h))}function publishedPort(s,inside5){return s.ports[inside5]??(s.hostNetwork?inside5:void 0)}function swapServiceHost(address,services,redis=!1){let h=hostsOf(address,redis?"address":!1);if(!h||h.srv||!services.length)return null;let fallback=DEFAULT_PORT2[h.kind],edits=[],pairPort;for(let e of h.endpoints){let s=serviceOf(e.host,services),port=s?publishedPort(s,e.port??fallback):void 0;if(!port)continue;let said2=/:\d+$/.test(address.slice(e.start,e.end));!said2&&port!==fallback&&h.form==="list"||(!said2&&h.form==="pairs"&&(pairPort=port),edits.push({start:e.start,end:e.end,text:said2||port!==fallback&&h.form==="link"?`localhost:${port}`:"localhost"}))}if(!edits.length)return null;let tail4="";pairPort!==void 0&&(h.portAt?address.slice(h.portAt.start,h.portAt.end)!==String(pairPort)&&edits.push({...h.portAt,text:String(pairPort)}):pairPort!==fallback&&(tail4=h.semi?`${/;\s*$/.test(address)?"":";"}Port=${pairPort}`:` port=${pairPort}`));let out22=address;for(let e of edits.sort((a,b)=>b.start-a.start))out22=out22.slice(0,e.start)+e.text+out22.slice(e.end);return out22+tail4}function serverPlace(address,services,o={}){let h=hostsOf(address.trim(),o.redis?"address":!1),endpoints=h?.endpoints;if(h?.srv)return{here:!1,host:h.endpoints[0]?.host??"a DNS name"};if(!h){if(!o.kind||o.kind==="sqlite")return{here:!0,address};try{let a=readAddress(o.kind,address);if(a.srv)return{here:!1,host:a.host};endpoints=a.socket?[]:[{host:a.host,...a.port?{port:a.port}:{},start:0,end:0}]}catch{return{here:!0,address}}}let fallback=DEFAULT_PORT2[h?.kind??o.kind];for(let e of endpoints??[]){if(isThisComputer(e.host))continue;let s=serviceOf(e.host,services);if(!s)return{here:!1,host:e.host};if(!publishedPort(s,e.port??fallback))return{here:!1,host:e.host,service:!0}}return{here:!0,address:swapServiceHost(address,services,o.redis)??address}}function serverOf2(address,redis=!1){let h=hostsOf(address.trim(),redis?"address":!1),e=h?.endpoints[0];if(!h||!e)return null;let host3=isThisComputer(e.host)?"localhost":e.host.toLowerCase();return`${host3.includes(":")?`[${host3}]`:host3}:${e.port??DEFAULT_PORT2[h.kind]}`}function safeAddress(address){return address.replace(/^((?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:)([^@/]*)@/i,(_m,head,pass)=>pass?`${head}***@`:`${head}@`).replace(/((?:^|[;,\s?&])(?:password|pwd)\s*=)([^;,&\s]*)/gi,"$1***")}function envPairs2(text4){let out22=[];for(let line3 of text4.split(/\r?\n/)){let m=/^\s*(?:export\s+)?([A-Za-z_][\w.]*)\s*=\s*(.*?)\s*$/.exec(line3);if(!m)continue;let v=m[2];/^['"]/.test(v)||(v=v.replace(/\s+#.*$/,"")),out22.push([m[1],v.replace(/^(['"])(.*)\1$/s,"$2")])}return out22}function looseJson2(text4){return JSON.parse(text4.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g,(m,str5)=>str5??"").replace(/,(\s*[}\]])/g,"$1"))}function jsonStrings(text4){let doc;try{doc=looseJson2(text4)}catch{return[]}let out22=[],walk3=(o,at3)=>{if(!(!o||typeof o!="object"||Array.isArray(o)||at3.length>3))for(let[k,v]of Object.entries(o))typeof v=="string"||typeof v=="number"?out22.push([[...at3,k].join(":"),String(v)]):walk3(v,[...at3,k])};return walk3(doc,[]),out22}function envFilesOf2(doc,s){return(Array.isArray(s.env_file)?s.env_file:s.env_file?[s.env_file]:[]).map(f=>f&&typeof f=="object"?f.path:f).filter(f=>typeof f=="string").map(f=>path56.posix.normalize(path56.posix.join(path56.posix.dirname(doc.file),f))).filter(f=>!f.startsWith("..")&&!EXAMPLE2.test(f)&&!OTHER_PLACE2.test(path56.posix.basename(f)))}function settingsIn2(dir){let files=filesOf2(dir),docs=composeDocs(dir,files.compose),out22=[],add2=(name,value,file3,key,form)=>{let v=value?.trim();v&&!out22.some(s=>s.name===name)&&out22.push({name,value:v,file:file3,key,form})},services=docs.flatMap(d=>Object.values(d.services).map(s=>({doc:d,s:s??{}}))),envFiles=[...new Set([...envFilesIn(dir),...services.flatMap(({doc,s})=>envFilesOf2(doc,s)),...files.env])];for(let f of envFiles)for(let[k,v]of envPairs2(read2(path56.join(dir,f))))add2(k,v,f,k,"env");for(let{doc,s}of services){let env2=s.environment,pairs=Array.isArray(env2)?env2.map(e=>String(e)).filter(e=>e.includes("=")).map(e=>[e.slice(0,e.indexOf("=")),e.slice(e.indexOf("=")+1)]):env2&&typeof env2=="object"?Object.entries(env2):[];for(let[k,v]of pairs)add2(k,plainValue(v),doc.file,k,"compose")}for(let f of files.appsettings)for(let[k,v]of jsonStrings(read2(path56.join(dir,f))))add2(k.split(":").join("__"),v,f,k,"json");for(let f of files.properties)for(let[k,v]of envPairs2(read2(path56.join(dir,f)).replace(/^\s*([\w.-]+)\s*[:=]/gm,"$1=")))add2(k.toUpperCase().replace(/[.-]/g,"_"),v,f,k,"properties");return out22}function appSettings(dir,all=settingsIn2(dir)){return all.flatMap(s=>{let kind=addressKind(s.name,s.value);return kind?[{...s,kind}]:[]})}function appEnvHere(cwd,command,log3){try{return envHere(cwd,command,log3)}catch{return{}}}function envHere(cwd,command,log3){if(isCompose(command))return{};let services=composeServices(cwd);if(!services.length)return{};let own2=ownDbWired(cwd),partFiles=new Set(envFilesIn(cwd,!0)),out22={};for(let s of appSettings(cwd)){if(own2&&s.kind!=="redis"||partFiles.has(s.file))continue;let swapped=swapServiceHost(s.value,services,s.kind==="redis");if(!swapped)continue;out22[s.name]=swapped;let text4=`${s.name} in ${s.file} names a Docker service that only Docker's own network knows, so the app squad-kit starts on this computer gets ${safeAddress(swapped)}`;log3?log3(text4):told.has(`${path56.resolve(cwd)}
|
|
365
|
+
`:c==="r"?"\r":c}var NAME=/^[A-Za-z_][A-Za-z0-9_]*/,BRACED=/^([A-Za-z_][A-Za-z0-9_]*)(?:(:?[-+?])([\s\S]*))?$/;function fill(text4,get,unset2){let out22="",i=0;for(;i<text4.length;){let at3=text4.indexOf("$",i);if(at3<0)return out22+text4.slice(i);if(out22+=text4.slice(i,at3),i=at3+1,text4[i]==="$"){out22+="$",i+=1;continue}if(text4[i]!=="{"){let name2=NAME.exec(text4.slice(i))?.[0];if(name2){let v2=get(name2);v2===void 0&&unset2(),out22+=v2??"",i+=name2.length}else out22+="$";continue}let depth=0,end=-1;for(let j=i+1;j<text4.length&&end<0;j++)text4[j]==="{"?depth+=1:text4[j]==="}"&&depth?depth-=1:text4[j]==="}"&&(end=j);let m=end<0?null:BRACED.exec(text4.slice(i+1,end));if(!m){out22+="$";continue}let[,name,op,word2=""]=m,v=get(name),set2=v!==void 0,said2=op?.startsWith(":")?set2&&v!=="":set2;op?op.endsWith("-")?out22+=said2?v:fill(word2,get,unset2):op.endsWith("+")?out22+=said2?fill(word2,get,unset2):v??"":(said2||unset2(),out22+=said2?v:""):(set2||unset2(),out22+=v??""),i=end+1}return out22}function parseEnvFile(text4,o={}){let s=text4.replace(/^\uFEFF/,"").replace(/\r\n?/g,`
|
|
366
|
+
`),out22=new Map,env2=o.env??{},known=new Map(Object.entries(o.before??{})),get=name=>Object.hasOwn(env2,name)?env2[name]:known.get(name),put=(key,template,written=template)=>{let missing3=!1,value=fill(template,get,()=>missing3=!0);missing3&&o.unset==="keep"?(out22.set(key,written),known.delete(key)):(out22.set(key,value),known.set(key,value))},i=0;for(;i<s.length;){let nl=s.indexOf(`
|
|
367
|
+
`,i),lineEnd=nl<0?s.length:nl,m=/^[ \t]*(?:export[ \t]+)?([A-Za-z_][\w.-]*)[ \t]*=[ \t]*/.exec(s.slice(i,lineEnd));if(!m){i=lineEnd+1;continue}let key=m[1],at3=i+m[0].length,quote=s[at3];if(quote==='"'||quote==="'"){let v="",t="",j=at3+1;for(;j<s.length&&s[j]!==quote;){if(quote==='"'&&s[j]==="\\"&&j+1<s.length){v+=unescape(s[j+1]),t+=s[j+1]==="$"?"$$":unescape(s[j+1]),j+=2;continue}v+=s[j],t+=s[j],j+=1}if(j<s.length){quote==="'"?(out22.set(key,v),known.set(key,v)):put(key,t,v);let after=s.indexOf(`
|
|
368
|
+
`,j);i=after<0?s.length:after+1;continue}}let raw=s.slice(at3,lineEnd),spaced=m[0].length>m[0].trimEnd().length;put(key,(spaced&&raw.startsWith("#")?"":raw.split(/[ \t]#/)[0]).trim()),i=lineEnd+1}return[...out22]}function readEnvFiles(top,files,o={}){let out22={env:{},fileOf:{},read:[],missing:[],refused:[]},real2;try{real2=fs62.realpathSync(top)}catch{return{...out22,missing:[...files]}}for(let f of files){let file3;try{file3=fs62.realpathSync(path53.join(real2,f))}catch{out22.missing.push(f);continue}let stat=fs62.statSync(file3,{throwIfNoEntry:!1});if(!file3.startsWith(`${real2}${path53.sep}`)||!stat?.isFile()||stat.size>ENV_FILE_BYTES){out22.refused.push(f);continue}for(let[k,v]of parseEnvFile(fs62.readFileSync(file3,"utf8"),{...o,before:out22.env}))out22.env[k]=v,out22.fileOf[k]=f;out22.read.push(f)}return out22}function readText2(file3){try{return fs62.readFileSync(file3,"utf8")}catch{return""}}function gitTop(dir){let at3;try{at3=fs62.realpathSync(dir)}catch{return null}for(;;){let dotGit=path53.join(at3,".git"),stat=fs62.lstatSync(dotGit,{throwIfNoEntry:!1});if(stat?.isDirectory())return{top:at3,source:at3,copy:!1};if(stat?.isFile()){let own2=/^gitdir:\s*(.+)$/m.exec(readText2(dotGit))?.[1]?.trim();if(!own2)return null;let gitDir=path53.resolve(at3,own2),common=path53.resolve(gitDir,readText2(path53.join(gitDir,"commondir")).trim()||".");if(path53.basename(common)!==".git")return null;try{return{top:at3,source:fs62.realpathSync(path53.dirname(common)),copy:!0}}catch{return null}}let up=path53.dirname(at3);if(up===at3)return null;at3=up}}function envFilesFor(dir){try{let g=gitTop(dir),root=g?findSquadRoot(g.source):null;if(!g||!root)return null;let meta=readMeta(boardPaths(path53.join(root,SQUAD_DIR)));for(let r of reposOf(meta)){let source2;try{source2=fs62.realpathSync(path53.join(root,r.path))}catch{continue}if(source2!==g.source)continue;let files=envFilesOf(r.commands);return files.length?{top:g.top,repo:r.path,files,copy:g.copy}:null}}catch{}return null}function envFilesIn(dir,copies=!1){let part=envFilesFor(dir);if(!part||copies&&!part.copy)return[];let from;try{from=fs62.realpathSync(dir)}catch{return[]}return part.files.map(f=>path53.relative(from,path53.join(part.top,f)).split(path53.sep).join("/")).filter(f=>f&&!f.startsWith(".."))}import fs65 from"fs";import path56 from"path";import yaml8 from"js-yaml";import{createHash as createHash5}from"crypto";import fs63 from"fs";import path54 from"path";var DbError=class extends Error{constructor(message,reason){super(message);this.reason=reason;this.name="DbError"}reason};var LOCAL=new Set(["","localhost","127.0.0.1","::1","[::1]","0.0.0.0","host.docker.internal"]);function normalHost(host3){let h=(host3??"").trim().toLowerCase();return LOCAL.has(h)?"localhost":h}var DEFAULT_PORT={postgres:5432,mysql:3306,mongo:27017},SCHEMES={postgres:"postgres",postgresql:"postgres",mysql:"mysql",mysql2:"mysql",mariadb:"mysql",mongodb:"mongo","mongodb+srv":"mongo",sqlite:"sqlite",sqlite3:"sqlite",file:"sqlite"};function kindOfAddress(address){let a=address.trim(),scheme=/^(?:jdbc:)?([a-z][a-z0-9+.-]*):/i.exec(a)?.[1]?.toLowerCase();if(scheme&&SCHEMES[scheme])return SCHEMES[scheme];let pairs=readPairs(a);if(pairs){let keys=new Set(pairs.map(p=>p.key));return keys.has("data source")&&/\.(db|sqlite3?|db3)$/i.test(pairs.find(p=>p.key==="data source").value)||keys.has("filename")?"sqlite":keys.has("dbname")||keys.has("sslmode")?"postgres":keys.has("uid")||keys.has("pwd")?"mysql":null}return/\.(db|sqlite3?|db3)$/i.test(a)?"sqlite":null}function safePart(text4){return text4.normalize("NFKD").replace(/[\u0300-\u036f]/g,"").toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")}var MAX_NAME=63,shortHash=text4=>createHash5("sha256").update(text4).digest("hex").slice(0,6);function fitName(base2,tail4){let full=`${base2}_${tail4}`;if(full.length<=MAX_NAME)return full;let t=tail4.length>30?`${tail4.slice(0,23)}_${shortHash(tail4)}`:tail4,room=MAX_NAME-t.length-1-7;return`${base2.slice(0,Math.max(1,room)).replace(/_+$/,"")}_${shortHash(base2)}_${t}`}function baseName(sourceName){return safePart(sourceName)||"db"}function storyName(sourceName,storyId){let story2=safePart(storyId);if(!story2)throw new DbError(`The story id "${storyId}" has no letters or digits to name a database with.`,"bad-address");return fitName(baseName(sourceName),story2)}function templateName(sourceName){return fitName(baseName(sourceName),"sk_template")}var SYSTEM_NAMES={postgres:new Set(["postgres","template0","template1"]),mysql:new Set(["mysql","information_schema","performance_schema","sys"]),mongo:new Set(["admin","local","config"]),sqlite:new Set};function readPairs(text4){if(!/^\s*[A-Za-z][\w ]*=/.test(text4)||/^[a-z][a-z0-9+.-]*:\/\//i.test(text4))return null;let semi=text4.includes(";")||!/^\s*\w+=\S*(\s+\w+=\S*)*\s*$/.test(text4.replace(/'(?:[^'\\]|\\.)*'/g,"x")),pairs=[],i=0;for(;i<text4.length;){for(;i<text4.length&&(text4[i]===";"||/\s/.test(text4[i]));)i++;if(i>=text4.length)break;let eq=text4.indexOf("=",i);if(eq<0)return null;let key=text4.slice(i,eq).trim().toLowerCase();if(i=eq+1,!semi)for(;i<text4.length&&text4[i]===" ";)i++;let start=i,value="",q5=text4[i];if(q5==='"'||q5==="'"){for(i++,start=i;i<text4.length;){if(text4[i]==="\\"&&!semi&&i+1<text4.length){value+=text4[i+1],i+=2;continue}if(text4[i]===q5){if(semi&&text4[i+1]===q5){value+=q5,i+=2;continue}break}value+=text4[i],i++}pairs.push({key,value,start,end:i}),i++}else{for(;i<text4.length&&(semi?text4[i]!==";":!/\s/.test(text4[i]));)i++;value=text4.slice(start,i).trim(),pairs.push({key,value,start,end:start+text4.slice(start,i).trimEnd().length})}}return pairs.length?pairs:null}var pick=(pairs,...keys)=>pairs.find(p=>keys.includes(p.key));function putPair(text4,p,value){let v=text4[p.start-1]==='"'||text4[p.start-1]==="'"?value:/[;\s'"]/.test(value)?`"${value.replace(/"/g,'""')}"`:value;return text4.slice(0,p.start)+v+text4.slice(p.end)}function readLink(text4){let m=/^((?:jdbc:)?)([a-z][a-z0-9+.-]*):\/\//i.exec(text4);if(!m)return null;let rest=text4.slice(m[0].length),endAuthority=rest.search(/[/?#]/),authEnd=endAuthority<0?rest.length:endAuthority,at3=rest.lastIndexOf("@",authEnd),auth2=at3>=0?rest.slice(0,at3):"",hosts=rest.slice(at3+1,authEnd),tail4=rest.slice(authEnd),pm=/^(\/[^?#]*)?(\?[^#]*)?(#.*)?$/.exec(tail4),p=pm?.[1]??"",query=pm?.[2]??"",colon=auth2.indexOf(":"),dec2=s=>{try{return decodeURIComponent(s)}catch{return s}},head=text4.slice(0,m[0].length+authEnd);return{scheme:m[2].toLowerCase(),...auth2?{user:dec2(colon>=0?auth2.slice(0,colon):auth2)}:{},...colon>=0?{password:dec2(auth2.slice(colon+1))}:{},hosts,path:dec2(p.replace(/^\//,"")),query,before:head+"/",after:query+(pm?.[3]??"")}}function queryParams(query){let out22={};for(let[k,v]of new URLSearchParams(query.replace(/^\?/,"")))out22[k.toLowerCase()]=v;return out22}function linkWithoutPassword(text4,l){if(l.password===void 0)return text4.replace(/([?&](?:password|pwd)=)[^&#]*/gi,"$1***");let m=/^((?:jdbc:)?[a-z][a-z0-9+.-]*:\/\/)/i.exec(text4),rest=text4.slice(m[0].length),authEnd=(()=>{let e=rest.search(/[/?#]/);return e<0?rest.length:e})(),at3=rest.lastIndexOf("@",authEnd),auth2=rest.slice(0,at3),user=auth2.slice(0,auth2.indexOf(":"));return(m[1]+`${user}:***@`+rest.slice(at3+1)).replace(/([?&](?:password|pwd)=)[^&#]*/gi,"$1***")}function serverOf(kind,host3,port,socket){return socket?`${kind}:socket:${path54.resolve(socket)}`:`${kind}:${normalHost(host3)}:${port??DEFAULT_PORT[kind]??""}`}function hostPort(text4){let t=text4.trim(),v6=/^\[([^\]]+)\](?::(\d+))?$/.exec(t);if(v6)return{host:v6[1],...v6[2]?{port:Number(v6[2])}:{}};let m=/^([^:]*)(?::(\d+))?$/.exec(t);if(!m)return{host:t};let host3=m[1];try{host3=decodeURIComponent(host3)}catch{}return{host:host3,...m[2]?{port:Number(m[2])}:{}}}function bad(kind,why){throw new DbError(`squad-kit could not read the ${label(kind)} address: ${why}.`,"bad-address")}function label(kind){return{sqlite:"SQLite",postgres:"Postgres",mysql:"MySQL",mongo:"MongoDB"}[kind]}function readAddress(kind,address){let text4=address.trim();if(text4||bad(kind,"it is empty"),kind==="sqlite")return readSqlite(text4);let link=readLink(text4);if(link){if(kind==="mongo")return readMongoLink(text4,link);let params2=queryParams(link.query),first2=link.hosts.split(",")[0]??"",hp2=hostPort(first2),socket2=params2.host?.startsWith("/")?params2.host:params2.socket,name=link.path||params2.dbname||params2.database||"";name||bad(kind,"it names no database");let user2=link.user??params2.user??params2.username,password5=link.password??params2.password??params2.pwd;return{kind,name,host:hp2.host||(socket2?"":"localhost"),...hp2.port?{port:hp2.port}:params2.port?{port:Number(params2.port)}:{},...socket2?{socket:socket2}:{},...user2?{user:user2}:{},...password5!==void 0?{password:password5}:{},params:params2,withName:n=>link.path||!params2.dbname&&!params2.database?link.before+encodeURIComponent(n)+link.after:text4.replace(/([?&](?:dbname|database)=)[^&#]*/i,`$1${encodeURIComponent(n)}`),safe:linkWithoutPassword(text4,link),server:serverOf(kind,hp2.host,hp2.port??(params2.port?Number(params2.port):void 0),socket2)}}let pairs=readPairs(text4);pairs||bad(kind,"it is not a link or a list of key=value settings");let nameP=pick(pairs,"database","dbname","initial catalog","db");(!nameP||!nameP.value)&&bad(kind,"it names no database");let hostP=pick(pairs,"host","server","data source","hostaddr","address","addr","network address"),portP=pick(pairs,"port"),hp=hostPort(hostP?.value??"localhost"),socket=pick(pairs,"socket","unix_socket")?.value||(hp.host.startsWith("/")?hp.host:void 0),user=pick(pairs,"user","username","user id","userid","uid","user name")?.value,passP=pick(pairs,"password","pwd"),params={};for(let p of pairs)params[p.key]=p.value;let port=portP?Number(portP.value):hp.port,safe2=passP?putPair(text4,passP,"***"):text4;return{kind,name:nameP.value,host:socket?"":hp.host,...port?{port}:{},...socket?{socket}:{},...user?{user}:{},...passP?{password:passP.value}:{},params,withName:n=>putPair(text4,nameP,n),safe:safe2,server:serverOf(kind,hp.host,port,socket)}}function readMongoLink(text4,link){link.scheme!=="mongodb"&&link.scheme!=="mongodb+srv"&&bad("mongo","it is not a mongodb:// link");let params=queryParams(link.query);link.path||bad("mongo","it names no database (add it after the hosts: mongodb://host:27017/name)");let hosts=link.hosts.split(",").map(h=>{let hp=hostPort(h);return`${normalHost(hp.host)}:${hp.port??(link.scheme==="mongodb+srv"?"":27017)}`}).sort().join(","),first2=hostPort(link.hosts.split(",")[0]??"");return{kind:"mongo",name:link.path,host:first2.host,...first2.port?{port:first2.port}:{},...link.user?{user:link.user}:{},...link.password!==void 0?{password:link.password}:{},params,hosts:link.hosts,srv:link.scheme==="mongodb+srv",withName:n=>{let sep=link.query?/[?&]$/.test(link.query)?"":"&":"?",keep=link.user&&!params.authsource?`${sep}authSource=${encodeURIComponent(link.path||"admin")}`:"",q5=link.query+keep;return link.before+encodeURIComponent(n)+q5+link.after.slice(link.query.length)},safe:linkWithoutPassword(text4,link),server:`mongo:${link.scheme==="mongodb+srv"?"srv:":""}${hosts}`}}function readSqlite(text4){let file3=text4,rewrite=p=>p,pairs=/^\s*(data source|datasource|filename)\s*=/i.test(text4)?readPairs(text4):null;if(pairs){let p=pick(pairs,"data source","datasource","filename");file3=p.value,rewrite=n=>putPair(text4,p,n)}else{let m=/^(jdbc:sqlite:|sqlite3?:(?:\/\/)?|file:(?:\/\/(?=\/))?)(.*)$/i.exec(text4);if(m){let prefix=m[1],[p,q5=""]=m[2].split("?");file3=decodeURIComponent(p),rewrite=n=>prefix+n+(q5?`?${q5}`:"")}}return(!file3||file3===":memory:")&&bad("sqlite","it names no file"),path54.isAbsolute(file3)||bad("sqlite",`the file must be given by its full path, not "${file3}"`),{kind:"sqlite",name:path54.resolve(file3),host:"",params:{},withName:rewrite,safe:text4,server:"sqlite"}}function realFile(file3){try{return fs63.realpathSync.native(file3)}catch{try{return path54.join(fs63.realpathSync.native(path54.dirname(file3)),path54.basename(file3))}catch{return path54.resolve(file3)}}}function sameFile(a,b){let ra=realFile(a),rb=realFile(b);if(ra===rb||process.platform!=="linux"&&ra.toLowerCase()===rb.toLowerCase())return!0;try{let sa=fs63.statSync(a),sb2=fs63.statSync(b);return sa.ino===sb2.ino&&sa.dev===sb2.dev}catch{return!1}}function isPersonsOwn(kind,address,personsAddress2){let a,p;try{a=readAddress(kind,address),p=readAddress(kind,personsAddress2)}catch{return!0}return kind==="sqlite"?sameFile(a.name,p.name):a.name.toLowerCase()===p.name.toLowerCase()}function refuseOwn(kind,address,personsAddress2,doing){if(isPersonsOwn(kind,address,personsAddress2))throw new DbError(`squad-kit never ${doing} your own database. It only reads it, to make the template.`,"persons-own");let name=readAddress(kind,address).name;if(SYSTEM_NAMES[kind].has(name.toLowerCase()))throw new DbError(`squad-kit never ${doing} "${name}": the ${label(kind)} server needs it for itself.`,"persons-own")}import fs64 from"fs";import path55 from"path";import yaml7 from"js-yaml";var SKIP_DIRS=new Set(["node_modules",".git",".hg",".svn",".squad","dist","build","out",".next",".nuxt",".svelte-kit",".output",".turbo",".cache","coverage",".venv","venv","__pycache__","target","vendor","Pods",".gradle",".idea",".vscode","bin","obj","wwwroot"]),LIMITS={depth:6,entries:8e3,fileBytes:256*1024,code:600},COMPOSE=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$/i,ENV_FILE=/^\.env(?:\.[\w.-]+)?$/,EXAMPLE=/(?:^|\.)(?:example|sample|template|dist|defaults|tpl)$/i,OTHER_PLACE=/\.env\.(?:[\w-]+\.)*(?:prod|production|beta|staging|stage|uat|qa|preprod|live|release)(?:\.[\w.-]+)?$/i,APPSETTINGS=/^appsettings(?:\.[\w-]+)?\.json$/i,SPRING=/^application(?:-[\w-]+)?\.(?:properties|ya?ml)$/,CODE=/\.(?:m?[jt]sx?|c[jt]s|py|cs)$/i,DB_ENV=/^(?:[A-Z0-9]+_)*(?:DATABASE|DB|MONGO|MONGODB|POSTGRES|POSTGRESQL|PG|MYSQL|MARIADB|SQLITE)(?:_[A-Z0-9]+)*_(?:URL|URI|DSN|CONNECTION|CONNECTION_STRING|CONN|CONN_STRING|PATH|FILE)$/,NOT_THE_APPS=/(?:^|_)(?:SHADOW|TEST|TESTS|TESTING|CI|E2E|READ|REPLICA|DIRECT)(?:_|$)/,SCHEMES2={postgres:"postgres",postgresql:"postgres",postgis:"postgres",mysql:"mysql",mysql2:"mysql",mysqlx:"mysql",mariadb:"mysql",mongodb:"mongo","mongodb+srv":"mongo",sqlite:"sqlite",sqlite3:"sqlite",file:"sqlite",sqlserver:"sqlserver",mssql:"sqlserver"},unquote=s=>s.trim().replace(/^(['"`])(.*)\1$/s,"$2").trim();function pairsOf(address){if(!/^[\w\s]+=/.test(address)||/^[a-z][\w+.-]*:\/\//i.test(address))return null;let out22=new Map;for(let part of address.split(";")){let at3=part.indexOf("=");at3>0&&out22.set(part.slice(0,at3).trim().toLowerCase(),part.slice(at3+1).trim())}return out22.size?out22:null}var SQLITE_FILE=/\.(?:db|sqlite3?|db3)$/i;function kindOf(address){let a=unquote(address),scheme=/^(?:jdbc:)?([a-z][a-z0-9+.-]*):/i.exec(a)?.[1]?.toLowerCase();if(scheme&&SCHEMES2[scheme])return SCHEMES2[scheme];let pairs=pairsOf(a);if(pairs){let has4=(...keys)=>keys.some(k=>pairs.has(k));return SQLITE_FILE.test(pairs.get("data source")??pairs.get("filename")??"")?"sqlite":has4("trusted_connection","integrated security","initial catalog","encrypt","trustservercertificate","multipleactiveresultsets")?"sqlserver":has4("host")&&has4("database","username")?"postgres":has4("uid","pwd")||pairs.get("port")==="3306"?"mysql":null}return SQLITE_FILE.test(a)&&!/\s/.test(a)?"sqlite":null}function readAddress2(address){let a=unquote(address).replace(/^jdbc:/i,""),file3=/^(?:file|sqlite3?):(?:\/\/)?(.+)$/i.exec(a);if(file3)return{name:file3[1].split("?")[0],password:!1};let link=/^[a-z][\w+.-]*:\/\/(?:([^:@/]*)(?::([^@/]*))?@)?([^/?#]*)(?:\/([^?#]*))?/i.exec(a);if(link){let first2=(link[3]??"").split(",")[0]??"",[host3,port]=first2.startsWith("[")?[first2.slice(0,first2.indexOf("]")+1),first2.slice(first2.indexOf("]")+2)]:first2.split(":"),name=decodeURIComponent((link[4]??"").split("/")[0]??"");return{...name?{name}:{},...host3?{host:host3}:{},...port?{port}:{},password:!!link[2]}}let pairs=pairsOf(a);if(pairs){let name=pairs.get("database")??pairs.get("initial catalog")??pairs.get("dbname")??(SQLITE_FILE.test(pairs.get("data source")??"")?pairs.get("data source"):void 0)??pairs.get("filename"),server=pairs.get("host")??pairs.get("server")??pairs.get("address")??pairs.get("addr"),[host3,port]=(server??"").replace(/^tcp:/i,"").split(/[:,]/);return{...name?{name}:{},...host3?{host:host3}:{},...pairs.get("port")??port?{port:pairs.get("port")??port}:{},password:!!(pairs.get("password")??pairs.get("pwd"))}}return SQLITE_FILE.test(a)?{name:a,password:!1}:{password:!1}}function withoutPassword(address){return address.replace(/^((?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:)([^@/]*)@/i,(_m,head,pass)=>pass?`${head}***@`:`${head}@`).replace(/((?:^|;)\s*(?:password|pwd)\s*=)([^;]*)/gi,"$1***")}function hasPassword(address){return readAddress2(address).password}function filesOf(dir){let out22={prisma:[],compose:[],env:[],appsettings:[],spring:[],csproj:[],packages:[],code:[]},entries=0,visit=(rel,depth)=>{if(depth>LIMITS.depth||entries>=LIMITS.entries)return;let list8;try{list8=fs64.readdirSync(path55.join(dir,rel),{withFileTypes:!0})}catch{return}if(!list8.some(e=>e.name==="pyvenv.cfg"))for(let e of list8){if(++entries>=LIMITS.entries)return;let p=rel?`${rel}/${e.name}`:e.name;if(e.isDirectory()){!SKIP_DIRS.has(e.name)&&!e.name.startsWith(".")&&visit(p,depth+1);continue}e.isFile()&&(e.name.endsWith(".prisma")?out22.prisma.push(p):COMPOSE.test(e.name)?out22.compose.push(p):ENV_FILE.test(e.name)&&depth<=3?out22.env.push(p):APPSETTINGS.test(e.name)?out22.appsettings.push(p):SPRING.test(e.name)&&(rel.endsWith("resources")||rel==="config"||rel==="")?out22.spring.push(p):e.name.endsWith(".csproj")?out22.csproj.push(p):e.name==="package.json"&&depth<=3?out22.packages.push(p):CODE.test(e.name)&&out22.code.length<LIMITS.code&&out22.code.push(p))}};return visit("",0),out22}function read(file3){try{return fs64.statSync(file3).size>LIMITS.fileBytes?"":fs64.readFileSync(file3,"utf8")}catch{return""}}var PLACEHOLDER=/\$\{[^}]*\}|\$[A-Z_][A-Z0-9_]*/;function envPairs(text4){return new Map(parseEnvFile(text4,{unset:"keep"}))}function composeValue(v){if(v==null)return;let s=String(v),m=/^\$\{[A-Za-z_]\w*:?-([^}]*)\}$/.exec(s);return m?m[1]:PLACEHOLDER.test(s)?void 0:s}function composeEnv(env2){let out22=new Map;if(Array.isArray(env2))for(let item of env2){let s=String(item),at3=s.indexOf("=");at3>0&&out22.set(s.slice(0,at3),composeValue(s.slice(at3+1)))}else if(env2&&typeof env2=="object")for(let[k,v]of Object.entries(env2))out22.set(k,composeValue(v));return out22}var IMAGE_KIND=[[/(?:^|\/)(?:postgres|postgresql|postgis|timescaledb[\w-]*)(?::|$)/i,"postgres"],[/(?:^|\/)(?:mysql|mariadb|percona)(?::|$)/i,"mysql"],[/(?:^|\/)(?:mongo|mongodb(?:-community-server)?)(?::|$)/i,"mongo"],[/mssql|sql-?server|azure-sql-edge/i,"sqlserver"]],KIND_PORT={postgres:5432,mysql:3306,mongo:27017,sqlite:0,sqlserver:1433};function published(ports,target){for(let p of Array.isArray(ports)?ports:[]){if(p&&typeof p=="object"){let o=p;if(Number(o.target)===target&&o.published!==void 0)return String(o.published);continue}let parts2=String(p).split("/")[0].split(":"),inside5=Number(parts2.at(-1));if(inside5===target)return parts2.length>=2?parts2.at(-2):String(inside5)}}function composeDbs(dir,files){let dbs=[],settings=[];for(let file3 of files){let doc;try{doc=yaml7.load(read(path55.join(dir,file3)))}catch{continue}let services=doc?.services;if(!(!services||typeof services!="object"))for(let[service,raw]of Object.entries(services)){let s=raw??{},env2=composeEnv(s.environment),kind=IMAGE_KIND.find(([re])=>re.test(String(s.image??"")))?.[1];if(!kind){for(let[k,v]of env2)DB_ENV.test(k)&&!NOT_THE_APPS.test(k)&&settings.push({file:file3,env:k,...v?{value:v}:{}});continue}let port=published(s.ports,KIND_PORT[kind]),get=(...keys)=>keys.map(k=>env2.get(k)).find(v=>v),at3=`localhost:${port??KIND_PORT[kind]}`,userPass=(user,pass)=>user?`${encodeURIComponent(user)}${pass?`:${encodeURIComponent(pass)}`:""}@`:"",name,address;if(kind==="postgres"){let user=get("POSTGRES_USER")??"postgres";name=get("POSTGRES_DB")??user,address=`postgres://${userPass(user,get("POSTGRES_PASSWORD"))}${at3}/${name}`}else if(kind==="mysql"){let user=get("MYSQL_USER","MARIADB_USER")??"root",pass=user==="root"?get("MYSQL_ROOT_PASSWORD","MARIADB_ROOT_PASSWORD"):get("MYSQL_PASSWORD","MARIADB_PASSWORD");name=get("MYSQL_DATABASE","MARIADB_DATABASE"),name&&(address=`mysql://${userPass(user,pass)}${at3}/${name}`)}else if(kind==="mongo"){name=get("MONGO_INITDB_DATABASE");let user=get("MONGO_INITDB_ROOT_USERNAME");name&&(address=`mongodb://${userPass(user,get("MONGO_INITDB_ROOT_PASSWORD"))}${at3}/${name}${user?"?authSource=admin":""}`)}dbs.push({file:file3,service,kind,...name?{name}:{},...port?{port}:{},...address?{address}:{}})}}return{dbs,settings}}var PROVIDER={postgresql:"postgres",postgres:"postgres",mysql:"mysql",sqlite:"sqlite",mongodb:"mongo",sqlserver:"sqlserver",cockroachdb:"postgres"};function prismaSources(dir,files){let out22=[];for(let file3 of files){let text4=read(path55.join(dir,file3)),block3=/datasource\s+\w+\s*\{([^}]*)\}/.exec(text4)?.[1];if(!block3)continue;let provider=/provider\s*=\s*"(\w+)"/.exec(block3)?.[1],env2=/url\s*=\s*env\(\s*"([^"]+)"\s*\)/.exec(block3)?.[1],url=/url\s*=\s*"([^"]+)"/.exec(block3)?.[1];out22.push({file:file3,...provider&&PROVIDER[provider]?{kind:PROVIDER[provider]}:{},...env2?{env:env2}:{},...url?{url}:{}})}return out22}function looseJson(text4){let bare=text4.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g,(m,str5)=>str5??"").replace(/,(\s*[}\]])/g,"$1");return JSON.parse(bare)}function appsettingsKeys(dir,file3){let doc;try{doc=looseJson(read(path55.join(dir,file3)))}catch{return[]}let out22=[],strings2=doc?.ConnectionStrings;if(strings2&&typeof strings2=="object")for(let[k,v]of Object.entries(strings2))typeof v=="string"&&v.trim()&&out22.push({file:file3,key:`ConnectionStrings:${k}`,value:v});let walk3=(o,at3,depth)=>{if(!(!o||typeof o!="object"||Array.isArray(o)||depth>2)){for(let[k,v]of Object.entries(o))if(!(!at3.length&&k==="ConnectionStrings"))if(typeof v=="string"&&kindOf(v)&&/:\/\/|=/.test(v)){let named=Object.entries(o).find(([n,x])=>/^(?:database(?:name)?|dbname)$/i.test(n)&&typeof x=="string")?.[1];out22.push({file:file3,key:[...at3,k].join(":"),value:v,...named?{name:named}:{}})}else walk3(v,[...at3,k],depth+1)}};return walk3(doc,[],0),out22}var SPRING_KEYS=["spring.datasource.url","spring.data.mongodb.uri","spring.r2dbc.url"];function springKeys(dir,file3){let text4=read(path55.join(dir,file3)),flat=new Map;if(file3.endsWith(".properties"))for(let line3 of text4.split(/\r?\n/)){let m=/^\s*([\w.-]+)\s*[=:]\s*(.*?)\s*$/.exec(line3);m&&!line3.trim().startsWith("#")&&flat.set(m[1],m[2])}else{let doc;try{doc=yaml7.load(text4)}catch{return[]}let walk3=(o,at3)=>{if(!(!o||typeof o!="object"||Array.isArray(o)))for(let[k,v]of Object.entries(o)){let key=at3?`${at3}.${k}`:k;v&&typeof v=="object"?walk3(v,key):v!=null&&flat.set(key,String(v))}};walk3(doc,"")}let out22=[];for(let key of SPRING_KEYS){let raw=flat.get(key);if(!raw)continue;let holder=/^\$\{([A-Za-z_][\w.]*)(?::([^}]*))?\}$/.exec(raw),value=holder?holder[2]??"":raw,user=flat.get("spring.datasource.username"),pass=flat.get("spring.datasource.password"),db=flat.get("spring.data.mongodb.database");out22.push({file:file3,key,value,...holder?{env:holder[1]}:{},...user&&!PLACEHOLDER.test(user)?{user}:{},...pass&&!PLACEHOLDER.test(pass)?{pass}:{},...db?{db}:{}})}return out22}var PACKAGE_KIND=[[/^(?:mongoose|mongodb|@nestjs\/mongoose|mongodb\.driver|mongodb\.entityframeworkcore|mongoengine|pymongo|motor)$/i,"mongo"],[/^(?:pg|postgres|pg-promise|@neondatabase\/serverless|npgsql(?:\.entityframeworkcore\.postgresql)?|psycopg2?(?:-binary)?|asyncpg)$/i,"postgres"],[/^(?:mysql|mysql2|pomelo\.entityframeworkcore\.mysql|mysql\.entityframeworkcore|mysqlconnector|pymysql|mysqlclient)$/i,"mysql"],[/^(?:better-sqlite3|sqlite3|sqlite|microsoft\.entityframeworkcore\.sqlite|microsoft\.data\.sqlite)$/i,"sqlite"],[/^(?:microsoft\.entityframeworkcore\.sqlserver|microsoft\.data\.sqlclient|system\.data\.sqlclient|mssql|tedious)$/i,"sqlserver"]];function packageKinds(dir,files){let names2=[];for(let f of files.packages)try{let p=JSON.parse(read(path55.join(dir,f)));names2.push(...Object.keys({...p.dependencies,...p.devDependencies}))}catch{}for(let f of files.csproj)for(let m of read(path55.join(dir,f)).matchAll(/<PackageReference\s+Include="([^"]+)"/g))names2.push(m[1]);let kinds=[];for(let n of names2){let k=PACKAGE_KIND.find(([re])=>re.test(n))?.[1];k&&!kinds.includes(k)&&kinds.push(k)}return kinds}var ENV_READS=[/process\.env\.([A-Z][A-Z0-9_]*)/g,/process\.env\[\s*['"`]([A-Z][A-Z0-9_]*)['"`]\s*\]/g,/(?:os\.environ\.get|os\.getenv|environ\.get|Deno\.env\.get|Environment\.GetEnvironmentVariable)\(\s*['"]([A-Z][A-Z0-9_]*)['"]/g,/os\.environ\[\s*['"]([A-Z][A-Z0-9_]*)['"]\s*\]/g];function codeReads(dir,files){let env2=new Set,connection=new Set;for(let f of files){let text4=read(path55.join(dir,f));if(text4){for(let re of ENV_READS)for(let m of text4.matchAll(re))(DB_ENV.test(m[1])||/MONGO/.test(m[1]))&&env2.add(m[1]);for(let m of text4.matchAll(/GetConnectionString\(\s*"([^"]+)"\s*\)/g))connection.add(m[1])}}return{env:env2,connection}}function realEnvFile(file3){let dir=path55.posix.dirname(file3),base2=path55.posix.basename(file3).replace(/\.(?:example|sample|template|dist|defaults|tpl)$/i,"");return dir==="."?base2:`${dir}/${base2}`}function findIn2(root,repo,partFiles=[]){let dir=path55.join(root,repo),files=filesOf(dir),prisma=prismaSources(dir,files.prisma),compose=composeDbs(dir,files.compose),kinds=packageKinds(dir,files),reads=codeReads(dir,files.code),part=partFiles.length?readEnvFiles(dir,partFiles,{unset:"keep"}):null,env2=[...[...part?.read??[]].reverse().map(f=>({file:f,example:!1,part:!0,pairs:new Map(Object.entries(part.env).filter(([k])=>part.fileOf[k]===f))})),...files.env.filter(f=>!part?.read.includes(f)).map(f=>({file:f,example:EXAMPLE.test(f),part:!1,pairs:envPairs(read(path55.join(dir,f)))}))],envValue2=name2=>{let hits=env2.filter(e=>e.pairs.has(name2)&&e.pairs.get(name2)&&!PLACEHOLDER.test(e.pairs.get(name2))),best2=hits.find(e=>e.part)??hits.find(e=>!e.example&&!OTHER_PLACE.test(e.file))??hits.find(e=>!e.example)??hits[0];return best2?{file:best2.file,value:best2.pairs.get(name2),example:best2.example,part:best2.part}:void 0},envFileOf=name2=>{let own3=env2.find(e=>e.part&&e.pairs.has(name2))??env2.find(e=>!e.example&&!OTHER_PLACE.test(e.file)&&e.pairs.has(name2))??env2.find(e=>!e.example&&e.pairs.has(name2))??env2.find(e=>e.pairs.has(name2));return own3?realEnvFile(own3.file):".env"},candidates=[];for(let p of prisma)if(p.env){let v=envValue2(p.env);candidates.push({setting:{env:p.env,file:envFileOf(p.env)},...v?{address:v.value}:{},...p.kind?{kind:p.kind}:{},from:[p.file,...v?[v.file]:[]],score:50})}else p.url&&candidates.push({setting:{file:p.file,key:"datasource url"},address:p.url,...p.kind?{kind:p.kind}:{},from:[p.file],score:35});let dotnet=[...files.appsettings].sort((a,b)=>Number(/development/i.test(b))-Number(/development/i.test(a))||a.length-b.length);for(let f of dotnet)for(let k of appsettingsKeys(dir,f)){let asked=[...reads.connection].some(n=>k.key===`ConnectionStrings:${n}`),kind2=kindOf(k.value)??void 0,name2=k.name??readAddress2(k.value).name;candidates.push({setting:{file:f,key:k.key,env:k.key.split(":").join("__")},address:k.value,...kind2?{kind:kind2}:{},...name2?{name:name2}:{},...k.name?{beside:k.name}:{},from:[f],score:40+(asked?5:0)+(/development/i.test(f)?2:0)})}for(let f of files.spring)for(let k of springKeys(dir,f)){let envName=k.env??k.key.toUpperCase().replace(/[.-]/g,"_"),address2=k.value||void 0;address2&&k.user&&!readAddress2(address2).password&&(address2=address2.replace(/^((?:jdbc:)?[a-z][\w+.-]*:\/\/)/i,`$1${encodeURIComponent(k.user)}${k.pass?`:${encodeURIComponent(k.pass)}`:""}@`));let kind2=address2?kindOf(address2)??void 0:void 0;candidates.push({setting:{file:f,key:k.key,env:envName},...address2?{address:address2}:{},...kind2?{kind:kind2}:{},...k.db?{name:k.db,beside:k.db}:{},from:[f],score:40+(/(?:^|\/)application\.(?:properties|ya?ml)$/.test(f)?2:0)})}let named=new Set;for(let e of env2)for(let[k,v]of e.pairs)(DB_ENV.test(k)&&!NOT_THE_APPS.test(k)||kindOf(v)&&/:\/\//.test(v)&&!NOT_THE_APPS.test(k))&&named.add(k);for(let c of candidates){let v=c.setting.key&&c.setting.env?envValue2(c.setting.env):void 0;if(!v||v.example||!v.part&&OTHER_PLACE.test(v.file)||!kindOf(v.value))continue;c.setting={env:c.setting.env,file:v.file},c.address=v.value;let name2=readAddress2(v.value).name??c.beside;name2?c.name=name2:delete c.name,c.from.push(v.file)}for(let k of named){if(candidates.some(c=>c.setting.env===k))continue;let v=envValue2(k),kind2=v?kindOf(v.value)??void 0:void 0;candidates.push({setting:{env:k,file:envFileOf(k)},...v?{address:v.value}:{},...kind2?{kind:kind2}:{},from:v?[v.file]:env2.filter(e=>e.pairs.has(k)).map(e=>e.file),score:25+(reads.env.has(k)?15:0)+(v?5:0)})}for(let k of reads.env)candidates.some(c=>c.setting.env===k)||NOT_THE_APPS.test(k)||candidates.push({setting:{env:k,file:".env"},from:[],score:22});for(let s of compose.settings){if(candidates.some(c=>c.setting.env===s.env)){candidates.find(c=>c.setting.env===s.env).from.push(s.file);continue}candidates.push({setting:{env:s.env,file:".env"},...s.value&&kindOf(s.value)?{kind:kindOf(s.value)}:{},from:[s.file],score:20})}let db=compose.dbs[0];if(!candidates.length&&!db)return null;let{beside:_beside,...best}=candidates.sort((a,b)=>b.score-a.score)[0]??{setting:{},from:[],score:10},kind=(best.address?kindOf(best.address):null)??best.kind??prisma.find(p=>p.kind)?.kind??db?.kind??kinds[0];if(!kind)return null;let own2=db&&db.kind===kind?db:void 0,address=best.address??own2?.address,said2=address?readAddress2(address):{password:!1,name:void 0,host:void 0,port:void 0},name=best.name??said2.name??own2?.name,where2=own2?`the ${own2.service} service in ${own2.file}${own2.port?`, on port ${own2.port}`:""}`:kind==="sqlite"?"a file in the project":said2.host?`${said2.host}${said2.port?`:${said2.port}`:""}`:void 0,from=[...new Set([...best.from,...own2?[own2.file]:[]])];return{...best,kind,...address?{address}:{},...name?{name}:{},...where2?{where:where2}:{},from,repo}}function findDatabase(root,repos,envFiles={}){let best=repos.map(r=>findIn2(root,r,envFiles[r])).filter(x=>!!x).sort((a,b)=>b.score-a.score)[0];if(!best)return{found:null};if(best.kind==="sqlserver")return{found:null,later:"This project uses SQL Server. A database of its own for each story comes for SQL Server in a later version; until then stories use it as before."};let{score:_score,kind,...rest}=best;return{found:{...rest,kind}}}var CODE_GLOBS=["*.cs","*.fs","*.vb","*.ts","*.tsx","*.mts","*.cts","*.js","*.jsx","*.mjs","*.cjs","*.py","*.java","*.kt","*.scala","*.go","*.rb","*.php","*.rs","*.ex","*.exs","*.dart","*.swift"],OPENS=/(?:(?:\b(?:GetDatabase|getDatabase|get_database|getSiblingDB|useDb)|\bclient\.db|\.Database)\(\s*|\bdbName\s*[:=]\s*)(["'`])([^"'`]+)\1/,OPENS_GREP="((GetDatabase|getDatabase|get_database|getSiblingDB|useDb|client\\.db|\\.Database)\\([[:space:]]*|dbName[[:space:]]*[:=][[:space:]]*)[\"'`]",DB_WORDS=/data ?base|db|mongo|catalog|schema|connect|client/i,COMMON_NAMES=new Set(["postgres","postgresql","mysql","mariadb","mongo","mongodb","test","admin","app","db","database","default","main","local","dev"]),TEST_PATH=/(?:^|\/)(?:tests?|__tests__|spec|specs|e2e)\/|[.-](?:test|spec)\.[a-z]+$|Tests?\.(?:cs|fs|java|kt)$|(?:^|\/)test_[^/]*\.py$|_test\.(?:py|go)$/i,DOTNET_TESTS=/(?:^|\/)(?:[^/]*\.)?(?:[A-Z][A-Za-z0-9]*)?Tests?\/|(?:^|\/)[^/]*\.tests?\//,isTestCode=file3=>TEST_PATH.test(file3)||DOTNET_TESTS.test(file3),SERVER_KINDS=["postgres","mysql","mongo"],GREP_MS=2e4;function grepLines(dir,args){return git(dir,["grep","-n","-I",...args,"--",...CODE_GLOBS],{allowFail:!0,timeoutMs:GREP_MS}).split(`
|
|
369
|
+
`).map(l=>/^(.+?):(\d+):(.*)$/.exec(l)).filter(m=>!!m).map(m=>({file:m[1],line:m[2],text:m[3]}))}var escapeRe=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function composeSets(dir,files,name){let passes=new RegExp(`^\\$(?:\\{${escapeRe(name)}(?::?-[^}]*)?\\}|${escapeRe(name)})$`);for(let file3 of files){let doc;try{doc=yaml7.load(read(path55.join(dir,file3)))}catch{continue}let services=doc?.services;if(!(!services||typeof services!="object"))for(let[service,raw]of Object.entries(services)){let env2=raw?.environment,value;if(Array.isArray(env2))for(let item of env2){let s=String(item),at3=s.indexOf("=");at3>0&&s.slice(0,at3)===name&&(value=s.slice(at3+1))}else env2&&typeof env2=="object"&&(value=env2[name]);if(!(value==null||!String(value).trim())&&!passes.test(String(value).trim()))return{file:file3,service}}}}function storyCopyIgnored(dir,db,address){if(!SERVER_KINDS.includes(db.kind))return;let setting=db.setting.env??db.setting.key??"its setting",takes=`squad-kit gives each story its own copy when the app takes the database's name from ${setting}.`,name=db.name??(address?readAddress2(address).name:void 0);if(name&&!COMMON_NAMES.has(name.toLowerCase())){let quoted4=[`"${name}"`,`'${name}'`,`\`${name}\``],reads=text4=>text4.replace(/\bGetConnectionString\s*\(\s*(["'`])[^"'`]*\1\s*\)|\bConnectionStrings[:_]+\w+/g,""),hits=grepLines(dir,["-F",...quoted4.flatMap(q5=>["-e",q5])]).filter(h=>!isTestCode(h.file)).map(h=>({...h,text:reads(h.text)})).filter(h=>quoted4.some(q5=>h.text.includes(q5))&&DB_WORDS.test(quoted4.reduce((t,q5)=>t.split(q5).join(""),h.text))),at3=h=>`${h.file}:${h.line}`;if(hits.length)return`The code names the database ${name} itself, in ${hits.length===1?at3(hits[0]):`${hits.length} places like ${at3(hits[0])}`}, so it would not use a story's own copy under another name. ${takes}`}for(let h of grepLines(dir,["-E","-e",OPENS_GREP])){let other=isTestCode(h.file)?void 0:OPENS.exec(h.text)?.[2];if(other)return`The code opens the database ${other} by its name, in ${h.file}:${h.line}, so it would not use a story's own copy under another name. ${takes}`}let files=filesOf(dir),values=[];if(db.setting.env)for(let f of files.env){let v=envPairs(read(path55.join(dir,f))).get(db.setting.env);v&&values.push({value:v,file:f})}address&&values.push({value:address,file:""});for(let{value,file:file3}of values)if(!(!(kindOf(value)||/:\/\//.test(value))||readAddress2(value).name))return`The address in ${file3||setting} names no database, so the app picks its database some other way and would not use a story's own copy. ${takes}`;if(db.setting.env){let set2=composeSets(dir,files.compose,db.setting.env);if(set2)return`${set2.file} sets ${db.setting.env} itself for the ${set2.service} service, from other settings, so the app there would not see a story's own database.`}}var COMPOSE2=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$/i,ENV_FILE2=/^\.env(?:\.[\w.-]+)?$/,EXAMPLE2=/(?:^|\.)(?:example|sample|template|dist|defaults|tpl)$/i,APPSETTINGS2=/^appsettings(?:\.[\w-]+)?\.json$/i,PROPERTIES=/^application(?:-[\w-]+)?\.properties$/,OTHER_PLACE2=/(?:^|[.\-_])(?:prod|production|beta|staging|stage|uat|qa|preprod|live|release|test|tests|testing|ci|e2e)(?:[.\-_]|$)/i,SKIP_DIRS2=new Set(["node_modules",".git",".squad","dist","build","out",".next",".nuxt","coverage",".venv","venv","__pycache__","target","vendor","Pods","bin","obj","wwwroot"]),LIMITS2={depth:4,entries:4e3,fileBytes:256*1024},DEFAULT_PORT2={postgres:5432,mysql:3306,mongo:27017,redis:6379},SCHEMES3={postgres:"postgres",postgresql:"postgres",mysql:"mysql",mysql2:"mysql",mariadb:"mysql",mongodb:"mongo","mongodb+srv":"mongo",redis:"redis",rediss:"redis"},REDIS_OPTIONS=/^(?:abortconnect|defaultdatabase|allowadmin|connectretry|connecttimeout|synctimeout|asynctimeout|ssl|sslhost|sslprotocols|servicename|keepalive|tiebreaker|resolvedns|channelprefix|configchannel|configcheckseconds)$/i;function read2(file3){try{return fs65.statSync(file3).size>LIMITS2.fileBytes?"":fs65.readFileSync(file3,"utf8")}catch{return""}}function filesOf2(dir){let out22={compose:[],env:[],appsettings:[],properties:[]},entries=0,visit=(rel,depth)=>{if(depth>LIMITS2.depth||entries>=LIMITS2.entries)return;let list8;try{list8=fs65.readdirSync(path56.join(dir,rel),{withFileTypes:!0})}catch{return}for(let e of list8.sort((a,b)=>a.name.localeCompare(b.name))){if(++entries>=LIMITS2.entries)return;let p=rel?`${rel}/${e.name}`:e.name;if(e.isDirectory()){!SKIP_DIRS2.has(e.name)&&!e.name.startsWith(".")&&visit(p,depth+1);continue}!e.isFile()||OTHER_PLACE2.test(e.name.replace(/\.(?:ya?ml|json|properties)$/i,""))||(COMPOSE2.test(e.name)?out22.compose.push(p):ENV_FILE2.test(e.name)&&!EXAMPLE2.test(e.name)&&!rel?out22.env.push(p):APPSETTINGS2.test(e.name)?out22.appsettings.push(p):PROPERTIES.test(e.name)&&out22.properties.push(p))}};visit("",0);let plainFirst=(a,b)=>path56.posix.basename(a).split(".").length-path56.posix.basename(b).split(".").length||a.localeCompare(b);return out22.compose.sort(plainFirst),out22.appsettings.sort((a,b)=>-plainFirst(a,b)),out22.properties.sort((a,b)=>-plainFirst(a,b)),out22}function plainValue(v){if(v==null||typeof v=="object")return;let s=String(v).replace(/\$\{[A-Za-z_]\w*:?-([^}]*)\}/g,"$1");return/\$\{|\$[A-Za-z_]/.test(s)?void 0:s}function composeDocs(dir,files){let out22=[];for(let file3 of files){let doc;try{doc=yaml8.load(read2(path56.join(dir,file3)))}catch{continue}let services=doc?.services;services&&typeof services=="object"&&out22.push({file:file3,services})}return out22}function publishes(ports,into){for(let p of Array.isArray(ports)?ports:[]){if(p&&typeof p=="object"){let o=p,target=Number(o.target),published2=Number(plainValue(o.published));(!o.protocol||o.protocol==="tcp")&&target&&published2&&(into[target]=published2);continue}let[spec="",protocol]=(plainValue(p)??"").split("/");if(protocol&&protocol!=="tcp")continue;let m=/^(?:(?:\[[^\]]*\]|[\d.]+):)?(\d+):(\d+)$/.exec(spec.trim());m&&(into[Number(m[2])]=Number(m[1]))}}function composeServices(dir){let byName=new Map;for(let doc of composeDocs(dir,filesOf2(dir).compose))for(let[name,raw]of Object.entries(doc.services)){let s=raw??{},one2=byName.get(name)??{names:[],ports:{}};byName.set(name,one2);let aliases=s.networks&&typeof s.networks=="object"&&!Array.isArray(s.networks)?Object.values(s.networks).flatMap(n=>Array.isArray(n?.aliases)?n.aliases:[]):[];for(let n of[name,s.container_name,s.hostname,...aliases]){let v=plainValue(n)?.trim().toLowerCase();v&&!one2.names.includes(v)&&one2.names.push(v)}publishes(s.ports,one2.ports),s.network_mode==="host"&&(one2.hostNetwork=!0)}return[...byName.values()]}function endpoint(text4,start){let t=text4.trim();if(!t)return null;let at3=start+text4.indexOf(t),v6=/^\[([^\]]+)\](?::(\d+))?$/.exec(t);if(v6)return{host:v6[1],...v6[2]?{port:Number(v6[2])}:{},start:at3,end:at3+t.length};if(/^[\da-f]*:[\da-f]*:[\da-f:.]*$/i.test(t))return{host:t,start:at3,end:at3+t.length};let m=/^([^:/\s]+)(?::(\d+))?$/.exec(t);return m?{host:m[1],...m[2]?{port:Number(m[2])}:{},start:at3,end:at3+t.length}:null}function split(text4,by2,from=0){let out22=[],start=0;for(let part of text4.split(by2))out22.push({text:part,start:from+start}),start+=part.length+by2.length;return out22}function hostsOf(address,redis=!1){let text4=address,link=/^((?:jdbc:)?)([a-z][a-z0-9+.-]*):\/\//i.exec(text4);if(link){let scheme=link[2].toLowerCase(),kind=SCHEMES3[scheme];if(!kind)return null;let from=link[0].length,rest=text4.slice(from),stop=rest.search(/[/?#]/),authEnd=stop<0?rest.length:stop,at3=rest.lastIndexOf("@",authEnd),hostsStart=from+at3+1,raw=text4.slice(hostsStart,from+authEnd),endpoints2=raw?split(raw,",",hostsStart).map(p=>endpoint(p.text,p.start)):[];return endpoints2.some(e=>!e)?null:{form:"link",kind,endpoints:endpoints2,...scheme==="mongodb+srv"?{srv:!0}:{}}}if(/^\s*[A-Za-z][\w ]*=/.test(text4)){let kind=kindOf(text4)??kindOfAddress(text4);if(kind!=="postgres"&&kind!=="mysql")return null;let semi=text4.includes(";"),pairs=semi?split(text4,";"):[...text4.matchAll(/\S+/g)].map(m=>({text:m[0],start:m.index})),endpoints2=[],portAt,port;for(let p of pairs){let eq=p.text.indexOf("=");if(eq<0)continue;let key=p.text.slice(0,eq).trim().toLowerCase(),valueStart=p.start+eq+1,value=p.text.slice(eq+1);if(key==="port"){let v=value.trim();port=Number(v),portAt={start:valueStart+value.indexOf(v),end:valueStart+value.indexOf(v)+v.length}}else if(["host","server","data source","address","addr","network address","hostaddr"].includes(key))for(let e of split(value,",",valueStart)){let one2=endpoint(e.text,e.start);one2&&endpoints2.push(one2)}}for(let e of endpoints2)e.port===void 0&&port&&(e.port=port);return{form:"pairs",kind,endpoints:endpoints2,...portAt?{portAt}:{},semi}}let parts2=split(text4,","),hosts=parts2.filter(p=>!p.text.includes("=")),options=parts2.filter(p=>p.text.includes("="));if(!hosts.length||parts2.indexOf(hosts.at(-1))!==hosts.length-1)return null;let endpoints=hosts.map(p=>endpoint(p.text,p.start));return endpoints.some(e=>!e||!HOST_LIKE.test(e.host))||!options.some(o=>REDIS_OPTIONS.test(o.text.slice(0,o.text.indexOf("=")).trim()))&&!(redis&&(endpoints.every(e=>e.port)||redis==="address"&&!options.length))?null:{form:"list",kind:"redis",endpoints}}var HOST_LIKE=/^(?!(?:true|false|yes|no|on|off|none|null)$)(?!\d+$)[a-z0-9][\w.-]*$|:/i,REDIS_NAME=/redis/i,ADDRESS_NAME=/conn|url|uri|host|server|endpoint|address|addr|config|^connectionstrings__/i;function redisBy(name){return REDIS_NAME.test(name)?ADDRESS_NAME.test(name)?"address":!0:!1}function addressKind(name,value){return hostsOf(value.trim(),redisBy(name))?.kind??null}function isThisComputer(host3){let h=host3.trim().toLowerCase().replace(/^\[(.*)\]$/,"$1");return normalHost(h)==="localhost"||h.startsWith("/")||/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)||h.endsWith(".localhost")||h==="::ffff:127.0.0.1"}function serviceOf(host3,services){let h=host3.trim().toLowerCase();return services.find(s=>s.names.includes(h))}function publishedPort(s,inside5){return s.ports[inside5]??(s.hostNetwork?inside5:void 0)}function swapServiceHost(address,services,redis=!1){let h=hostsOf(address,redis?"address":!1);if(!h||h.srv||!services.length)return null;let fallback=DEFAULT_PORT2[h.kind],edits=[],pairPort;for(let e of h.endpoints){let s=serviceOf(e.host,services),port=s?publishedPort(s,e.port??fallback):void 0;if(!port)continue;let said2=/:\d+$/.test(address.slice(e.start,e.end));!said2&&port!==fallback&&h.form==="list"||(!said2&&h.form==="pairs"&&(pairPort=port),edits.push({start:e.start,end:e.end,text:said2||port!==fallback&&h.form==="link"?`localhost:${port}`:"localhost"}))}if(!edits.length)return null;let tail4="";pairPort!==void 0&&(h.portAt?address.slice(h.portAt.start,h.portAt.end)!==String(pairPort)&&edits.push({...h.portAt,text:String(pairPort)}):pairPort!==fallback&&(tail4=h.semi?`${/;\s*$/.test(address)?"":";"}Port=${pairPort}`:` port=${pairPort}`));let out22=address;for(let e of edits.sort((a,b)=>b.start-a.start))out22=out22.slice(0,e.start)+e.text+out22.slice(e.end);return out22+tail4}function serverPlace(address,services,o={}){let h=hostsOf(address.trim(),o.redis?"address":!1),endpoints=h?.endpoints;if(h?.srv)return{here:!1,host:h.endpoints[0]?.host??"a DNS name"};if(!h){if(!o.kind||o.kind==="sqlite")return{here:!0,address};try{let a=readAddress(o.kind,address);if(a.srv)return{here:!1,host:a.host};endpoints=a.socket?[]:[{host:a.host,...a.port?{port:a.port}:{},start:0,end:0}]}catch{return{here:!0,address}}}let fallback=DEFAULT_PORT2[h?.kind??o.kind];for(let e of endpoints??[]){if(isThisComputer(e.host))continue;let s=serviceOf(e.host,services);if(!s)return{here:!1,host:e.host};if(!publishedPort(s,e.port??fallback))return{here:!1,host:e.host,service:!0}}return{here:!0,address:swapServiceHost(address,services,o.redis)??address}}function serverOf2(address,redis=!1){let h=hostsOf(address.trim(),redis?"address":!1),e=h?.endpoints[0];if(!h||!e)return null;let host3=isThisComputer(e.host)?"localhost":e.host.toLowerCase();return`${host3.includes(":")?`[${host3}]`:host3}:${e.port??DEFAULT_PORT2[h.kind]}`}function safeAddress(address){return address.replace(/^((?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:)([^@/]*)@/i,(_m,head,pass)=>pass?`${head}***@`:`${head}@`).replace(/((?:^|[;,\s?&])(?:password|pwd)\s*=)([^;,&\s]*)/gi,"$1***")}function envPairs2(text4){let out22=[];for(let line3 of text4.split(/\r?\n/)){let m=/^\s*(?:export\s+)?([A-Za-z_][\w.]*)\s*=\s*(.*?)\s*$/.exec(line3);if(!m)continue;let v=m[2];/^['"]/.test(v)||(v=v.replace(/\s+#.*$/,"")),out22.push([m[1],v.replace(/^(['"])(.*)\1$/s,"$2")])}return out22}function looseJson2(text4){return JSON.parse(text4.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g,(m,str5)=>str5??"").replace(/,(\s*[}\]])/g,"$1"))}function jsonStrings(text4){let doc;try{doc=looseJson2(text4)}catch{return[]}let out22=[],walk3=(o,at3)=>{if(!(!o||typeof o!="object"||Array.isArray(o)||at3.length>3))for(let[k,v]of Object.entries(o))typeof v=="string"||typeof v=="number"?out22.push([[...at3,k].join(":"),String(v)]):walk3(v,[...at3,k])};return walk3(doc,[]),out22}function envFilesOf2(doc,s){return(Array.isArray(s.env_file)?s.env_file:s.env_file?[s.env_file]:[]).map(f=>f&&typeof f=="object"?f.path:f).filter(f=>typeof f=="string").map(f=>path56.posix.normalize(path56.posix.join(path56.posix.dirname(doc.file),f))).filter(f=>!f.startsWith("..")&&!EXAMPLE2.test(f)&&!OTHER_PLACE2.test(path56.posix.basename(f)))}function settingsIn2(dir){let files=filesOf2(dir),docs=composeDocs(dir,files.compose),out22=[],add2=(name,value,file3,key,form)=>{let v=value?.trim();v&&!out22.some(s=>s.name===name)&&out22.push({name,value:v,file:file3,key,form})},services=docs.flatMap(d=>Object.values(d.services).map(s=>({doc:d,s:s??{}}))),envFiles=[...new Set([...envFilesIn(dir),...services.flatMap(({doc,s})=>envFilesOf2(doc,s)),...files.env])],seen={};for(let f of envFiles)for(let[k,v]of parseEnvFile(read2(path56.join(dir,f)),{before:seen,unset:"keep"}))seen[k]=v,add2(k,v,f,k,"env");for(let{doc,s}of services){let env2=s.environment,pairs=Array.isArray(env2)?env2.map(e=>String(e)).filter(e=>e.includes("=")).map(e=>[e.slice(0,e.indexOf("=")),e.slice(e.indexOf("=")+1)]):env2&&typeof env2=="object"?Object.entries(env2):[];for(let[k,v]of pairs)add2(k,plainValue(v),doc.file,k,"compose")}for(let f of files.appsettings)for(let[k,v]of jsonStrings(read2(path56.join(dir,f))))add2(k.split(":").join("__"),v,f,k,"json");for(let f of files.properties)for(let[k,v]of envPairs2(read2(path56.join(dir,f)).replace(/^\s*([\w.-]+)\s*[:=]/gm,"$1=")))add2(k.toUpperCase().replace(/[.-]/g,"_"),v,f,k,"properties");return out22}function appSettings(dir,all=settingsIn2(dir)){return all.flatMap(s=>{let kind=addressKind(s.name,s.value);return kind?[{...s,kind}]:[]})}function appEnvHere(cwd,command,log3){try{return envHere(cwd,command,log3)}catch{return{}}}function envHere(cwd,command,log3){if(isCompose(command))return{};let services=composeServices(cwd);if(!services.length)return{};let own2=ownDbWired(cwd),partFiles=new Set(envFilesIn(cwd,!0)),out22={};for(let s of appSettings(cwd)){if(own2&&s.kind!=="redis"||partFiles.has(s.file))continue;let swapped=swapServiceHost(s.value,services,s.kind==="redis");if(!swapped)continue;out22[s.name]=swapped;let text4=`${s.name} in ${s.file} names a Docker service that only Docker's own network knows, so the app squad-kit starts on this computer gets ${safeAddress(swapped)}`;log3?log3(text4):told.has(`${path56.resolve(cwd)}
|
|
370
370
|
${text4}`)||(told.add(`${path56.resolve(cwd)}
|
|
371
|
-
${text4}`),writeLog({level:"info",text:text4}))}return out22}var told=new Set;function settingsFileAddress(dir,setting){let name=setting.env??setting.key?.split(":").join("__");if(!name)return;let first2=settingsIn2(dir).find(s=>s.name===name||s.form==="json"&&setting.key!==void 0&&s.key.toLowerCase()===setting.key.toLowerCase());if(!first2||first2.form!=="json"&&first2.form!=="properties")return;let kind=addressKind(name,first2.value);return kind&&kind!=="redis"?{file:first2.file,value:first2.value}:void 0}function partEnv(command,cwd){if(isCompose(command))return{};try{let part=envFilesFor(cwd);if(!part?.copy)return{};let got=readEnvFiles(part.top,part.files),services,swapped=[];for(let[name,value]of Object.entries(got.env)){let kind=addressKind(name,value);if(!kind)continue;services??=composeServices(part.top);let here=swapServiceHost(value,services,kind==="redis");here&&(got.env[name]=here,swapped.push(name))}let names2=Object.keys(got.env),named=names2.length>20?`${names2.slice(0,20).join(", ")} and ${names2.length-20} more`:names2.join(", ")||"none";return tell(part.top,[...got.read.length?[`squad-kit gives the commands it runs in ${path57.basename(part.top)} the settings in ${got.read.join(", ")}: ${named}. Its own database, Redis and port go on top.`]:[],...swapped.length?[`${swapped.join(", ")} in ${got.read.join(", ")} ${swapped.length===1?"names":"name"} a Docker service, so the app squad-kit starts on this computer reaches it through the port that service publishes here.`]:[],...got.missing.length?[`${path57.basename(part.top)} has no ${got.missing.join(", ")}, so its commands run without ${got.missing.length===1?"that env file":"those env files"}. An untracked file needs values for stories: add it as a local file in Settings.`]:[],...got.refused.length?[`squad-kit did not read ${got.refused.join(", ")} in ${path57.basename(part.top)}: an env file must be a small file inside the copy.`]:[]]),got.env}catch{return{}}}var told2=new Set;function tell(top,lines3){for(let text4 of lines3)told2.has(`${top}
|
|
371
|
+
${text4}`),writeLog({level:"info",text:text4}))}return out22}var told=new Set;function settingsFileAddress(dir,setting){let name=setting.env??setting.key?.split(":").join("__");if(!name)return;let first2=settingsIn2(dir).find(s=>s.name===name||s.form==="json"&&setting.key!==void 0&&s.key.toLowerCase()===setting.key.toLowerCase());if(!first2||first2.form!=="json"&&first2.form!=="properties")return;let kind=addressKind(name,first2.value);return kind&&kind!=="redis"?{file:first2.file,value:first2.value}:void 0}function partEnv(command,cwd,env2={}){if(isCompose(command))return{};try{let part=envFilesFor(cwd);if(!part?.copy)return{};let got=readEnvFiles(part.top,part.files,{env:env2}),services,swapped=[];for(let[name,value]of Object.entries(got.env)){let kind=addressKind(name,value);if(!kind)continue;services??=composeServices(part.top);let here=swapServiceHost(value,services,kind==="redis");here&&(got.env[name]=here,swapped.push(name))}let names2=Object.keys(got.env),named=names2.length>20?`${names2.slice(0,20).join(", ")} and ${names2.length-20} more`:names2.join(", ")||"none";return tell(part.top,[...got.read.length?[`squad-kit gives the commands it runs in ${path57.basename(part.top)} the settings in ${got.read.join(", ")}: ${named}. Its own database, Redis and port go on top.`]:[],...swapped.length?[`${swapped.join(", ")} in ${got.read.join(", ")} ${swapped.length===1?"names":"name"} a Docker service, so the app squad-kit starts on this computer reaches it through the port that service publishes here.`]:[],...got.missing.length?[`${path57.basename(part.top)} has no ${got.missing.join(", ")}, so its commands run without ${got.missing.length===1?"that env file":"those env files"}. An untracked file needs values for stories: add it as a local file in Settings.`]:[],...got.refused.length?[`squad-kit did not read ${got.refused.join(", ")} in ${path57.basename(part.top)}: an env file must be a small file inside the copy.`]:[]]),got.env}catch{return{}}}var told2=new Set;function tell(top,lines3){for(let text4 of lines3)told2.has(`${top}
|
|
372
372
|
${text4}`)||(told2.add(`${top}
|
|
373
373
|
${text4}`),writeLog({level:"info",text:text4}))}import fs67 from"fs";import os9 from"os";import path59 from"path";var KNOWN_BAD_GROUPS=[{name:"Tunnels that open this computer to the internet",hosts:["ngrok.io","*.ngrok.io","ngrok.app","*.ngrok.app","ngrok-free.app","*.ngrok-free.app","ngrok.dev","*.ngrok.dev","ngrok-free.dev","*.ngrok-free.dev","*.ngrok.com","ngrok-agent.com","*.ngrok-agent.com","trycloudflare.com","*.trycloudflare.com","loca.lt","*.loca.lt","localtunnel.me","*.localtunnel.me","serveo.net","*.serveo.net","serveousercontent.com","*.serveousercontent.com","pagekite.me","*.pagekite.me","pagekite.net","*.pagekite.net","localhost.run","*.localhost.run","lhr.life","*.lhr.life","pinggy.io","*.pinggy.io","pinggy.link","*.pinggy.link","bore.pub","*.bore.pub","tunnelmole.net","*.tunnelmole.net","localxpose.io","*.localxpose.io","loclx.io","*.loclx.io","zrok.io","*.zrok.io","*.devtunnels.ms","sharedwithexpose.com","*.sharedwithexpose.com","localtonet.com","*.localtonet.com","localto.net","*.localto.net","playit.gg","*.playit.gg"]},{name:"Paste and file-drop sites",hosts:["pastebin.com","*.pastebin.com","paste.ee","hastebin.com","termbin.com","ix.io","sprunge.us","paste.rs","dpaste.com","dpaste.org","rentry.co","rentry.org","controlc.com","justpaste.it","privatebin.net","pastes.io","paste.c-net.org","transfer.sh","*.transfer.sh","file.io","0x0.st","temp.sh","bashupload.com","oshi.at","keep.sh","filebin.net","tmpfiles.org","catbox.moe","*.catbox.moe","gofile.io","*.gofile.io","uguu.se","*.uguu.se","pixeldrain.com"]},{name:"Request catchers and out-of-band test hosts",hosts:["webhook.site","*.webhook.site","requestbin.net","*.requestbin.net","*.m.pipedream.net","requestcatcher.com","*.requestcatcher.com","hookbin.com","postb.in","ptsv2.com","ptsv3.com","requestrepo.com","*.requestrepo.com","interact.sh","*.interact.sh","interactsh.com","*.interactsh.com","*.oast.pro","*.oast.live","*.oast.site","*.oast.online","*.oast.fun","*.oast.me","burpcollaborator.net","*.burpcollaborator.net","oastify.com","*.oastify.com","canarytokens.com","*.canarytokens.com","canarytokens.org","*.canarytokens.org","dnslog.cn","*.dnslog.cn","ceye.io","*.ceye.io","*.xss.ht"]}],RECOMMENDED_GROUPS=[{name:"npm",hosts:["registry.npmjs.org","registry.npmjs.com","npmjs.org","npmjs.com","skimdb.npmjs.com","npm.pkg.github.com","registry.yarnpkg.com","repo.yarnpkg.com","yarnpkg.com","get.pnpm.io","registry.bower.io"]},{name:"Node.js, Bun and Deno",hosts:["nodejs.org","deb.nodesource.com","bun.sh","deno.land","dl.deno.land","jsr.io"]},{name:"Prisma",hosts:["binaries.prisma.sh"]},{name:"PyPI",hosts:["pypi.org","pypi.python.org","files.pythonhosted.org","*.pythonhosted.org","pip.pypa.io","bootstrap.pypa.io","install.python-poetry.org","conda.anaconda.org","repo.anaconda.com","anaconda.org","conda.binstar.org","binstar.org","repo.continuum.io","download.pytorch.org"]},{name:"RubyGems",hosts:["rubygems.org","index.rubygems.org","api.rubygems.org","bundler.rubygems.org","rubygems.pkg.github.com","gems.rubyonrails.org","cache.ruby-lang.org","*.rvm.io"]},{name:"Maven and Gradle",hosts:["repo.maven.apache.org","repo1.maven.org","maven.apache.org","search.maven.org","maven-central.storage-download.googleapis.com","maven.pkg.github.com","oss.sonatype.org","repo.spring.io","maven.oracle.com","repo.grails.org","download.eclipse.org","services.gradle.org","plugins.gradle.org","plugins-artifacts.gradle.org","downloads.gradle.org","downloads.gradle-dn.com","gradle.org"]},{name:"Java and Kotlin downloads",hosts:["jdk.java.net","api.adoptium.net","adoptium.net","download.oracle.com","www.java.com","cache-redirector.jetbrains.com","download.jetbrains.com","packages.jetbrains.team","maven.pkg.jetbrains.space"]},{name:"Google Maven and Android",hosts:["maven.google.com","dl.google.com","dl-ssl.google.com"]},{name:"NuGet and .NET",hosts:["api.nuget.org","globalcdn.nuget.org","nuget.org","www.nuget.org","dist.nuget.org","nuget.pkg.github.com","pkgs.dev.azure.com","nugetregistryv2prod.blob.core.windows.net","azuresearch-usnc.nuget.org","azuresearch-ussc.nuget.org","dot.net","dotnet.microsoft.com","builds.dotnet.microsoft.com","ci.dot.net","dotnetcli.blob.core.windows.net","dotnetcli.azureedge.net","dotnetbuilds.azureedge.net","download.visualstudio.microsoft.com","aka.ms"]},{name:"Dart and Flutter",hosts:["pub.dev","pub.dartlang.org","storage.googleapis.com","storage.flutter-io.cn"]},{name:"Packagist",hosts:["repo.packagist.org","packagist.org","getcomposer.org"]},{name:"Go modules",hosts:["proxy.golang.org","sum.golang.org","go.dev","golang.org","pkg.go.dev","goproxy.io"]},{name:"crates.io",hosts:["crates.io","index.crates.io","static.crates.io","static.rust-lang.org","sh.rustup.rs"]},{name:"CocoaPods and Swift",hosts:["cocoapods.org","cdn.cocoapods.org","trunk.cocoapods.org","swift.org","download.swift.org"]},{name:"Haskell and Perl",hosts:["haskell.org","*.hackage.haskell.org","hackage.haskell.org","get-ghcup.haskell.org","downloads.haskell.org","cpan.org","www.cpan.org","metacpan.org","cpan.metacpan.org"]},{name:"Elixir, R and Julia",hosts:["hex.pm","repo.hex.pm","builds.hex.pm","cdn.hex.pm","cran.r-project.org","cloud.r-project.org","pkg.julialang.org","julialang-s3.julialang.org"]},{name:"Bazel",hosts:["bcr.bazel.build","releases.bazel.build","mirror.bazel.build"]},{name:"GitHub downloads",hosts:["github.com","codeload.github.com","objects.githubusercontent.com","release-assets.githubusercontent.com","raw.githubusercontent.com","*.githubusercontent.com","lfs.github.com","github-cloud.s3.amazonaws.com","ghcr.io"]},{name:"Browsers for browser tests",hosts:["cdn.playwright.dev","playwright.download.prss.microsoft.com","playwright.azureedge.net","playwright-akamai.azureedge.net","playwright-verizon.azureedge.net","download.cypress.io","cdn.cypress.io","googlechromelabs.github.io","edgedl.me.gvt1.com"]},{name:"Container images",hosts:["registry.hub.docker.com","*.docker.io","*.docker.com","quay.io","mcr.microsoft.com","gcr.io","public.ecr.aws"]},{name:"Linux packages",hosts:["archive.ubuntu.com","security.ubuntu.com","ppa.launchpad.net","keyserver.ubuntu.com","deb.debian.org","security.debian.org","apt.llvm.org","dl.fedoraproject.org","mirrors.fedoraproject.org","download.fedoraproject.org","dl-cdn.alpinelinux.org","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","formulae.brew.sh"]},{name:"Terraform and Kubernetes",hosts:["releases.hashicorp.com","registry.terraform.io","dl.k8s.io","pkgs.k8s.io"]},{name:"JSON schemas",hosts:["json-schema.org","json.schemastore.org"]},{name:"Certificate checks",hosts:["ocsp.digicert.com","crl3.digicert.com","crl4.digicert.com","ocsp.sectigo.com","crl.sectigo.com","ocsp.usertrust.com","crl.usertrust.com","ocsp.globalsign.com","crl.globalsign.com","ocsp.identrust.com","crl.identrust.com","ocsp.ssl.com","crls.ssl.com","oneocsp.microsoft.com","*.o.lencr.org","*.c.lencr.org"]}],KNOWN_BAD=[...new Set(KNOWN_BAD_GROUPS.flatMap(g=>g.hosts))],RECOMMENDED=[...new Set(RECOMMENDED_GROUPS.flatMap(g=>g.hosts))];function hostMatches(host3,pattern){let h=host3.toLowerCase().replace(/\.$/,""),p=pattern.toLowerCase();return p==="*"?!0:p.startsWith("*.")?h.endsWith(p.slice(1)):h===p}function knownBadGroup(host3){let bare=host3.toLowerCase().replace(/^\*\./,"");return KNOWN_BAD_GROUPS.find(g=>g.hosts.some(p=>hostMatches(bare,p)))?.name??null}import fs66 from"fs";import path58 from"path";function flutterOnPath(env2=process.env){for(let dir of(env2.PATH??"").split(path58.delimiter)){if(!dir||!path58.isAbsolute(dir))continue;let program2=path58.join(dir,process.platform==="win32"?"flutter.bat":"flutter");try{if(!fs66.statSync(program2).isFile())continue;return{program:program2,root:path58.dirname(path58.dirname(fs66.realpathSync(program2)))}}catch{}}return null}var FLUTTER_STAMPS=[{file:"bin/cache/engine.stamp",regex:String.raw`bin/cache/engine\.stamp`,glob:"bin/cache/engine.stamp",why:"the engine's version, which Flutter writes on every run"},{file:"bin/cache/engine.stamp.tmp.<number>",regex:String.raw`bin/cache/engine\.stamp\.tmp\.[0-9]+`,glob:"bin/cache/engine.stamp.tmp.*",why:"the same, first written under the number of the run, then moved onto engine.stamp"},{file:"bin/cache/engine.realm",regex:String.raw`bin/cache/engine\.realm`,glob:"bin/cache/engine.realm",why:"where the engine comes from, which Flutter writes on every run (empty for its own)"}];function flutterStampRegexes(root){let base2=root.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/\/$/,"");return FLUTTER_STAMPS.map(s=>`^${base2}/${s.regex}$`)}function flutterStampGlobs(root){return FLUTTER_STAMPS.map(s=>path58.join(root,s.glob))}function flutterStampFolders(root){return[...new Set(FLUTTER_STAMPS.map(s=>path58.join(root,path58.dirname(s.file))))]}var PACKAGE_HOSTS=RECOMMENDED,DEFAULT_NETWORK="standard",NETWORK_LEVELS=["standard","strict","off"],NETWORK_NAME={standard:"Standard",strict:"Strict",off:"Off"},NETWORK_WORDS={standard:"Any website but known-bad ones such as tunnels, paste sites and request catchers, with your keys hidden so a harmful package has nothing to take.",strict:"Only the official package and download sites of common tools and the websites you allow, while your project's own tests and app still reach any site for now.",off:"No internet, only this computer, so the setup, the tests and the app must work with what is already here."};function parseNetwork(s){let v=s?.trim().toLowerCase();return NETWORK_LEVELS.find(l=>l===v)??null}function projectNetwork(root){try{return parseNetwork(readMeta(boardPaths(path59.join(root,".squad"))).network)??DEFAULT_NETWORK}catch{return DEFAULT_NETWORK}}function networkPolicy(level=DEFAULT_NETWORK,sites=[],platform=process.platform){let own2=sites.filter(h=>!knownBadGroup(h)),hosts=level==="off"?[]:[...new Set([...level==="standard"?["*"]:[],...PACKAGE_HOSTS,...own2])];return{level,hosts,noHosts:KNOWN_BAD,commands:level==="off"&&platform==="darwin"?"this-computer":"internet"}}var MAX_BLOCKED_SITES=50;function blockedSitesFile(bp){return path59.join(bp.localDir,"blocked-sites.json")}function blockedSites(bp){try{let list8=JSON.parse(fs67.readFileSync(blockedSitesFile(bp),"utf8"));return Array.isArray(list8)?list8.filter(b=>b&&typeof b.host=="string").reverse():[]}catch{return[]}}function noteBlockedSite(bp,site){try{let list8=blockedSites(bp).reverse().filter(b=>b.host!==site.host||b.story!==site.story);list8.push({host:site.host,story:site.story,level:site.level,at:site.at??new Date().toISOString()}),fs67.mkdirSync(bp.localDir,{recursive:!0}),fs67.writeFileSync(blockedSitesFile(bp),`${JSON.stringify(list8.slice(-MAX_BLOCKED_SITES),null,1)}
|
|
374
374
|
`)}catch{}}var PACKAGE_CACHES=[".npm/_cacache",".yarn/berry/cache","Library/Caches/Yarn",".cache/yarn","Library/pnpm/store",".local/share/pnpm/store","Library/Caches/pnpm",".cache/pnpm",".cargo/registry",".cargo/git","go/pkg/mod"],PRIVATE=[".ssh",".aws",".gnupg",".config/gh",".azure",".kube",".docker/config.json",".netrc",".git-credentials"],PUSH_ADDRESSES=["/",".","file://","https://","http://","ssh://","git://","git@"];function noPushEnv(env2=process.env){let from=Number(env2.GIT_CONFIG_COUNT)||0,out22={GIT_CONFIG_COUNT:String(from+PUSH_ADDRESSES.length)};return PUSH_ADDRESSES.forEach((address,i)=>{out22[`GIT_CONFIG_KEY_${from+i}`]="url.squad-kit-no-push://.pushInsteadOf",out22[`GIT_CONFIG_VALUE_${from+i}`]=address}),out22}function ticketCacheDir(bp,id2){return path59.join(bp.localDir,"caches",id2.toLowerCase())}function projectCacheDir(bp){return path59.join(bp.localDir,"caches","project")}function dropTicketCache(bp,id2){fs67.rmSync(ticketCacheDir(bp,id2),{recursive:!0,force:!0})}function packageCacheEnv(dir,env2=process.env){let at3=(...p)=>path59.join(dir,...p),gems=[env2.GEM_HOME,...(env2.GEM_PATH??"").split(path59.delimiter)].filter(p=>!!p?.trim());return{NUGET_PACKAGES:at3("nuget","packages"),NUGET_HTTP_CACHE_PATH:at3("nuget","http-cache"),NUGET_PLUGINS_CACHE_PATH:at3("nuget","plugins-cache"),MAVEN_OPTS:[env2.MAVEN_OPTS?.trim(),`-Dmaven.repo.local=${at3("maven","repository")}`].filter(Boolean).join(" "),GRADLE_USER_HOME:at3("gradle"),GEM_HOME:at3("ruby","gems"),GEM_PATH:`${[...new Set(gems)].join(path59.delimiter)}${path59.delimiter}`,GEM_SPEC_CACHE:at3("ruby","specs"),BUNDLE_PATH:at3("ruby","bundle"),BUNDLE_USER_CACHE:at3("ruby","bundle-cache")}}function gitPlaces(cwd){let link=path59.join(cwd,".git");try{let own2=path59.resolve(cwd,/^gitdir: (.*)$/m.exec(fs67.readFileSync(link,"utf8"))[1]);return[link,own2,path59.resolve(own2,fs67.readFileSync(path59.join(own2,"commondir"),"utf8").trim())]}catch{return[link]}}function withReal(dirs){let out22=new Set;for(let d of dirs){out22.add(d);try{out22.add(fs67.realpathSync(d))}catch{}}return[...out22]}function sandboxAsk(o){let home2=o.home??os9.homedir(),temp=withReal([os9.tmpdir(),...process.platform==="win32"?[]:["/tmp"]]),caches=PACKAGE_CACHES.map(c=>path59.join(home2,c)).filter(c=>fs67.existsSync(c)),env2=[".env",".env.*","**/.env","**/.env.*"].map(p=>`${o.root}/${p}`),own2=o.cache?withReal([o.cache]):[],net5=networkPolicy(o.network??projectNetwork(o.root),o.hosts??[]),flutter2=o.flutter===void 0?flutterOnPath()?.root:o.flutter;return{write:[...new Set([...withReal([o.proof]),...own2,...temp,...caches,...flutter2?flutterStampGlobs(flutter2):[]])],noRead:[...new Set([...PRIVATE.map(p=>path59.join(home2,p)),...withReal([squadHome()]),...o.secrets,...env2])],read:[...new Set(withReal([o.cwd,o.proof,...o.cache?[o.cache]:[],...o.read,...androidReads()]))],noWrite:[...new Set(withReal([...gitPlaces(o.cwd),...copyDirsIn(o.cwd).flatMap(gitPlaces)]))],hosts:net5.hosts,noHosts:net5.noHosts}}function sandboxOf(tool5){return tool5.sandbox?.()??{available:!1,why:`${tool5.label} has no sandbox squad-kit can use`}}function sandboxStatus(){return AI_TOOLS.filter(t=>t.canRun).map(t=>({tool:t.label,...sandboxOf(t)}))}function guardNote(platform=process.platform,tools4=sandboxStatus()){return platform!=="win32"||tools4.some(t=>t.available)?null:{title:"Windows: less protected",text:"On Windows, squad-kit does not guard the agents' own commands, or the project's own tests and app, so they can reach other files on this computer. Use it on projects you trust."}}function shown(file3,home2){return file3===home2?"~":file3.startsWith(`${home2}${path59.sep}`)?`~${file3.slice(home2.length)}`:file3}function blockedLine(b,home2=os9.homedir()){if(b.kind==="host")return`A download from ${b.target} was not allowed for this project; the work goes on without it.`;let file3=shown(b.target,home2);if(b.kind==="write")return`Writing ${file3} was blocked: the squad writes only in the ticket's copy, the proof folder and temp folders.`;let base2=path59.basename(b.target),why=/^\.env(\.|$)/.test(base2)?"the project's .env files stay private":b.target.startsWith(squadHome())?"squad-kit's own files stay private":"keys and sign-ins stay private";return`Reading ${file3} was blocked: ${why}.`}function blockedIn(output){if(!output)return[];for(let t of AI_TOOLS){let found2=t.blocked?.(output)??[];if(found2.length)return found2}return[]}function parseHosts(list8){let hosts=[],bad2=[],closed=[];for(let raw of list8.flatMap(x=>String(x).split(/[\s,]+/))){let s=raw.trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//,"").replace(/[/?#].*$/,"").replace(/:\d+$/,"");raw.trim()&&(!/^(\*\.)?([a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(s)||s==="*"?bad2.push(raw.trim()):knownBadGroup(s)?closed.push(s):hosts.push(s))}return{hosts:[...new Set(hosts)],bad:bad2,closed}}function hostsProblem(p){return p.bad.length?`"${p.bad[0]}" is not a host name. Write it like nexus.example.com, or *.example.com for all of its subdomains.`:p.closed.length?`${p.closed[0]} stays closed for every project: it is one of the ${knownBadGroup(p.closed[0]).toLowerCase()}, which a build never needs.`:null}import{createHash as createHash6}from"crypto";import fs68 from"fs";import os10 from"os";import path60 from"path";function storiesCacheDir(bp){return path60.join(bp.localDir,"caches","stories")}function ownFolder(dir){let at3=fs68.lstatSync(dir,{throwIfNoEntry:!1});at3&&!at3.isDirectory()&&fs68.rmSync(dir,{force:!0}),fs68.mkdirSync(dir,{recursive:!0})}function inside3(dir,p){try{let home2=fs68.realpathSync(dir),at3=path60.dirname(p);for(;!fs68.existsSync(at3);)at3=path60.dirname(at3);let real2=fs68.realpathSync(at3);return real2===home2||real2.startsWith(`${home2}${path60.sep}`)}catch{return!1}}function toolCacheEnv(dir){let at3=p=>path60.join(dir,p);return{XDG_CACHE_HOME:at3("xdg"),PIP_CACHE_DIR:at3("pip"),POETRY_CACHE_DIR:at3("poetry"),UV_CACHE_DIR:at3("uv"),PUB_CACHE:at3("pub"),CP_REPOS_DIR:at3(path60.join("cocoapods","repos")),CP_CACHE_DIR:at3(path60.join("cocoapods","cache")),COMPOSER_CACHE_DIR:at3("composer"),MAVEN_USER_HOME:at3("maven"),GOCACHE:at3("go-build"),DENO_DIR:at3("deno"),BUN_INSTALL_CACHE_DIR:at3("bun"),npm_config_devdir:at3("node-gyp"),PUPPETEER_CACHE_DIR:at3("puppeteer"),CYPRESS_CACHE_FOLDER:at3("cypress"),electron_config_cache:at3("electron")}}function installedEnv(){return{FLUTTER_ALREADY_LOCKED:"true"}}function noReportsEnv(){return{FLUTTER_SUPPRESS_ANALYTICS:"true",DOTNET_CLI_TELEMETRY_OPTOUT:"1",COCOAPODS_DISABLE_STATS:"true"}}function pnpmStoreEnv(dir){return{pnpm_config_store_dir:path60.join(dir,"pnpm-store"),npm_config_store_dir:path60.join(dir,"pnpm-store")}}function npmCacheEnv(dir,home2){let npm=path60.join(dir,"npm");try{if(!inside3(dir,npm))return{};fs68.mkdirSync(npm,{recursive:!0});let shared=path60.join(home2,".npm","_cacache"),link=path60.join(npm,"_cacache");return fs68.existsSync(shared)&&!fs68.lstatSync(link,{throwIfNoEntry:!1})&&fs68.symlinkSync(shared,link),{npm_config_cache:npm}}catch{return{}}}function linkVersions(from,to,name){fs68.mkdirSync(to,{recursive:!0});for(let entry of fs68.existsSync(from)?fs68.readdirSync(from):[])if(name.test(entry))try{fs68.symlinkSync(path60.join(from,entry),path60.join(to,entry))}catch{}}function theirCaches(home2,env2,mac=process.platform==="darwin"){return mac?path60.join(home2,"Library","Caches"):env2.XDG_CACHE_HOME??path60.join(home2,".cache")}function browserCacheEnv(dir,home2,env2){let set2=env2.PLAYWRIGHT_BROWSERS_PATH;if(set2==="0"||process.platform==="win32")return{};let playwright=path60.join(dir,"ms-playwright");try{if(!inside3(dir,playwright))return{};linkVersions(set2?path60.resolve(set2):path60.join(theirCaches(home2,env2),"ms-playwright"),playwright,/^[a-z_]+-\d+$/);let puppeteer=path60.join(home2,".cache","puppeteer");for(let kind of fs68.existsSync(puppeteer)?fs68.readdirSync(puppeteer):[])/^chrome(-headless-shell)?$|^chromedriver$|^firefox$/.test(kind)&&linkVersions(path60.join(puppeteer,kind),path60.join(dir,"puppeteer",kind),/^[\w.-]+-[\w.]+$/);return{PLAYWRIGHT_BROWSERS_PATH:playwright}}catch{return{}}}function theirPub(home2,env2,win=process.platform==="win32"){return env2.PUB_CACHE?path60.resolve(env2.PUB_CACHE):win?path60.join(env2.LOCALAPPDATA??path60.join(home2,"AppData","Local"),"Pub","Cache"):path60.join(home2,".pub-cache")}function pubLinks(dir,cwd,home2,env2){if(!cwd||!fs68.existsSync(path60.join(cwd,"pubspec.yaml")))return;let theirs=theirPub(home2,env2),mine2=path60.join(dir,"pub");try{if(!inside3(dir,mine2)||path60.resolve(theirs)===path60.resolve(mine2))return;for(let host3 of fs68.existsSync(path60.join(theirs,"hosted"))?fs68.readdirSync(path60.join(theirs,"hosted")):[])host3.startsWith(".")||(linkVersions(path60.join(theirs,"hosted",host3),path60.join(mine2,"hosted",host3),/^[a-z0-9_]+-\d[\w.+-]*$/),linkVersions(path60.join(theirs,"hosted-hashes",host3),path60.join(mine2,"hosted-hashes",host3),/^[a-z0-9_]+-\d[\w.+-]*\.sha256$/))}catch{}}function specRepoLinks(dir,home2,env2){let theirs=env2.CP_REPOS_DIR?path60.resolve(env2.CP_REPOS_DIR):path60.join(env2.CP_HOME_DIR?path60.resolve(env2.CP_HOME_DIR):path60.join(home2,".cocoapods"),"repos"),mine2=path60.join(dir,"cocoapods","repos");try{if(!fs68.existsSync(theirs)||!inside3(dir,mine2)||theirs===path60.resolve(mine2))return;linkVersions(theirs,mine2,/^(?!trunk$|Spec_Lock$)[^.]/)}catch{}}var START_AS_COPY=[{mine:()=>path60.join("xdg","prisma"),theirs:(home2,env2)=>path60.join(env2.XDG_CACHE_HOME??path60.join(home2,".cache"),"prisma"),most:400*2**20}],LINKED_SETTINGS=[{mine:"gradle/gradle.properties",theirs:".gradle/gradle.properties"},{mine:"gradle/init.d",theirs:".gradle/init.d"},{mine:"maven/settings.xml",theirs:".m2/settings.xml"},{mine:"maven/settings-security.xml",theirs:".m2/settings-security.xml"},{mine:"maven/toolchains.xml",theirs:".m2/toolchains.xml"}];function sizeOf2(p,most){let total=0,walk3=at3=>{for(let e of fs68.readdirSync(at3,{withFileTypes:!0})){if(total>most)return;let full=path60.join(at3,e.name);e.isDirectory()?walk3(full):e.isFile()&&(total+=fs68.statSync(full).size)}};return walk3(p),total}function madeWhole(to,make){let part=`${to}.making-${process.pid}`;fs68.rmSync(part,{recursive:!0,force:!0});try{make(part),fs68.renameSync(part,to)}catch{fs68.rmSync(part,{recursive:!0,force:!0})}}function copyWhole(from,to){madeWhole(to,part=>fs68.cpSync(from,part,{recursive:!0,verbatimSymlinks:!0,preserveTimestamps:!0,mode:fs68.constants.COPYFILE_FICLONE}))}function startHome(dir,home2,env2){for(let c of START_AS_COPY){let to=path60.join(dir,c.mine(home2,env2)),from=c.theirs(home2,env2);try{if(fs68.lstatSync(to,{throwIfNoEntry:!1})||!fs68.existsSync(from)||!inside3(dir,to)||sizeOf2(from,c.most)>c.most)continue;fs68.mkdirSync(path60.dirname(to),{recursive:!0}),copyWhole(from,to)}catch{}}for(let s of LINKED_SETTINGS){let to=path60.join(dir,s.mine),from=path60.join(home2,s.theirs);try{if(fs68.lstatSync(to,{throwIfNoEntry:!1})||!fs68.existsSync(from)||!inside3(dir,to))continue;fs68.mkdirSync(path60.dirname(to),{recursive:!0}),fs68.symlinkSync(from,to)}catch{}}}function gradleWrapper(dir,cwd,home2,env2){let said2="";for(let at3 of[cwd,path60.join(cwd,"android")])try{said2=fs68.readFileSync(path60.join(at3,"gradle","wrapper","gradle-wrapper.properties"),"utf8");break}catch{}if(!said2)return;let value=key=>new RegExp(`^${key}\\s*=\\s*(.+)$`,"m").exec(said2)?.[1]?.trim().replace(/\\(.)/g,"$1"),url=value("distributionUrl");if(!url||(value("distributionBase")??"GRADLE_USER_HOME")!=="GRADLE_USER_HOME"||(value("distributionPath")??"wrapper/dists")!=="wrapper/dists")return;let name=path60.basename(url).replace(/\.zip$/,""),hash2=BigInt(`0x${createHash6("md5").update(url).digest("hex")}`).toString(36),rel=path60.join("wrapper","dists",name,hash2),mine2=path60.join(dir,"gradle",rel),ok2=`${name}.zip.ok`,theirs=[path60.join(env2.GRADLE_USER_HOME??path60.join(home2,".gradle"),rel),path60.join(gradleHome(),rel)].find(p=>fs68.existsSync(path60.join(p,ok2)));if(fs68.lstatSync(mine2,{throwIfNoEntry:!1})||!theirs||!inside3(dir,mine2))return;let unpacked=fs68.readdirSync(theirs,{withFileTypes:!0}).filter(e=>e.isDirectory());unpacked.length===1&&(fs68.mkdirSync(path60.dirname(mine2),{recursive:!0}),madeWhole(mine2,part=>{fs68.mkdirSync(part),fs68.symlinkSync(path60.join(theirs,unpacked[0].name),path60.join(part,unpacked[0].name)),fs68.copyFileSync(path60.join(theirs,ok2),path60.join(part,ok2))}))}function theirsToRead(home2,env2,mavenOpts){let out22={},gradle=path60.join(env2.GRADLE_USER_HOME??path60.join(home2,".gradle"),"caches");!env2.GRADLE_RO_DEP_CACHE&&fs68.existsSync(path60.join(gradle,"modules-2"))&&(out22.GRADLE_RO_DEP_CACHE=gradle);let nuget=env2.NUGET_PACKAGES??path60.join(home2,".nuget","packages");fs68.existsSync(nuget)&&(out22.NUGET_FALLBACK_PACKAGES=[env2.NUGET_FALLBACK_PACKAGES?.trim(),nuget].filter(Boolean).join(";"));let maven=path60.join(home2,".m2","repository");return fs68.existsSync(maven)&&!/-Dmaven\.repo\.local\.tail=/.test(mavenOpts)&&(out22.MAVEN_OPTS=`${mavenOpts} -Dmaven.repo.local.tail=${maven}`),out22}function cacheHomeEnv(dir,o){let env2=o.env??process.env,home2=o.home??os10.homedir();try{ownFolder(dir),startHome(dir,home2,env2),o.cwd&&gradleWrapper(dir,o.cwd,home2,env2),pubLinks(dir,o.cwd,home2,env2),process.platform==="darwin"&&specRepoLinks(dir,home2,env2)}catch{}let packages2=packageCacheEnv(dir,env2);return{...packages2,...theirsToRead(home2,env2,packages2.MAVEN_OPTS),...toolCacheEnv(dir),...installedEnv(),...noReportsEnv(),...npmCacheEnv(dir,home2),...browserCacheEnv(dir,home2,env2),...o.wall==="project"?pnpmStoreEnv(dir):{}}}var HEALED_LINE="A tool could not save its downloads in your home folder, so squad-kit gave it the project's own folder and ran it again.",NOT_A_CACHE=new Set([".config",".local",".Trash",".nvm",".rvm",".rbenv",".pyenv",".sdkman",".rustup",".volta",".asdf",".jenv",".nodenv",".goenv",".fnm"]),ON_JAVA=new Set([".gradle",".m2",".android",".kotlin",".konan",".sbt",".ivy2",".java",".skiko"]);function homeCachePlace(target,home2,hidden){let rel=path60.relative(home2,target);if(!rel||rel.startsWith("..")||path60.isAbsolute(rel))return null;let parts2=rel.split(path60.sep),top=parts2[0],depth=top===".cache"?2:top==="Library"&&parts2[1]==="Caches"?3:top.startsWith(".")&&!NOT_A_CACHE.has(top)?1:0;if(!depth)return null;let place=path60.join(home2,...parts2.slice(0,Math.min(depth,parts2.length))),near=(a,b)=>a===b||a.startsWith(`${b}${path60.sep}`);return hidden.some(h=>near(place,h)||near(h,place))?null:place}function realFolderAt(p){let at3=fs68.lstatSync(p,{throwIfNoEntry:!1});at3?.isDirectory()||(at3&&fs68.rmSync(p,{force:!0}),fs68.mkdirSync(p))}function linkEntries(from,to,skip,hidden){let names2=[];try{names2=fs68.readdirSync(from)}catch{return}for(let name of names2){let theirs=path60.join(from,name);if(skip.has(name)||hidden.has(theirs))continue;let mine2=path60.join(to,name),at3=fs68.lstatSync(mine2,{throwIfNoEntry:!1});at3&&!at3.isSymbolicLink()||at3&&fs68.readlinkSync(mine2)===theirs||(at3&&fs68.rmSync(mine2,{force:!0}),fs68.symlinkSync(theirs,mine2))}}var COPY_MOST=200*2**20;function ownPlace(from,to,hidden){let at3=fs68.lstatSync(to,{throwIfNoEntry:!1});if(at3&&!at3.isSymbolicLink())return;at3&&fs68.rmSync(to,{force:!0});let theirs=fs68.statSync(from,{throwIfNoEntry:!1});if(theirs){if(theirs.isFile())return fs68.copyFileSync(from,to);if(theirs.isDirectory()){if(sizeOf2(from,COPY_MOST)<=COPY_MOST)return copyWhole(from,to);fs68.mkdirSync(to),linkEntries(from,to,new Set,hidden)}}}function healHome(dir,places,o){let own2=path60.join(dir,"home");ownFolder(dir),realFolderAt(own2);let hidden=new Set(o.hidden),ways=new Map;for(let place of places){let parts2=path60.relative(o.home,place).split(path60.sep);parts2.forEach((part,i)=>{let at3=parts2.slice(0,i).join(path60.sep);ways.set(at3,(ways.get(at3)??new Set).add(part))})}let placed=new Set(places.map(p=>path60.relative(o.home,p))),walk3=rel=>{let mine2=path60.join(own2,rel);linkEntries(path60.join(o.home,rel),mine2,ways.get(rel)??new Set,hidden);for(let name of ways.get(rel)??[]){let next=rel?path60.join(rel,name):name;placed.has(next)?ownPlace(path60.join(o.home,next),path60.join(own2,next),hidden):(realFolderAt(path60.join(own2,next)),walk3(next))}};return walk3(""),own2}function healEnv(dir,places,o){let home2=o.home??os10.homedir(),own2=healHome(dir,places,{home:home2,hidden:o.hidden}),java2=places.some(p=>ON_JAVA.has(path60.relative(home2,p).split(path60.sep)[0])),opts=(o.env??process.env).JAVA_TOOL_OPTIONS?.trim();return{HOME:own2,...java2?{JAVA_TOOL_OPTIONS:[opts,`-Duser.home=${own2}`].filter(Boolean).join(" ")}:{}}}function learnedFile(dir){return`${dir}.healed.json`}function healedBefore(dir,command,o){let home2=o.home??os10.homedir();try{let said2=JSON.parse(fs68.readFileSync(learnedFile(dir),"utf8"))[command];return Array.isArray(said2)?said2.filter(p=>typeof p=="string"&&homeCachePlace(p,home2,o.hidden)===p):[]}catch{return[]}}function rememberHealed(dir,command,places){let file3=learnedFile(dir),all={};try{all=JSON.parse(fs68.readFileSync(file3,"utf8"))}catch{}all[command]=[...new Set(places)];try{fs68.mkdirSync(path60.dirname(file3),{recursive:!0}),fs68.writeFileSync(file3,`${JSON.stringify(all,null,2)}
|
|
375
375
|
`)}catch{}}var SEATBELT="/usr/bin/sandbox-exec";function writableCaches(home2){return PACKAGE_CACHES.map(c=>path61.join(home2,c))}function browserProfiles(home2,platform){if(platform==="darwin"){let support3=path61.join(home2,"Library","Application Support");return["Google/Chrome","Chromium","Firefox","Microsoft Edge","BraveSoftware","Arc","Vivaldi","com.operasoftware.Opera"].map(b=>path61.join(support3,b)).concat(path61.join(home2,"Library","Safari"),path61.join(home2,"Library","Application Support","Firefox"))}return[".config/google-chrome",".config/chromium",".config/BraveSoftware",".config/microsoft-edge",".mozilla"].map(b=>path61.join(home2,b))}var FINCH_ON_MAC="/Applications/Finch/lima/data";function runtimeSockets(home2,platform,env2=process.env){let inHome2=[".docker",".colima",".config/colima",".lima",".rd",".orbstack",".finch",".local/share/containers/podman"];if(platform==="darwin")for(let app of["Containers/com.docker.docker","Group Containers/group.com.docker","Group Containers/HUAUUMT7XC.dev.orbstack","Application Support/rancher-desktop"])inHome2.push(path61.join("Library",app));let inRun=["docker.sock","podman.sock","podman","finch.sock","containerd"],uid=process.getuid?.(),own2=[env2.XDG_RUNTIME_DIR,...platform==="linux"&&uid!==void 0?[`/run/user/${uid}`]:[]].filter(d=>!!d),named=/^unix:\/\/(\/.+)$/.exec(env2.DOCKER_HOST?.trim()??"")?.[1];return[...inHome2.map(p=>path61.join(home2,p)),...["/var/run","/run",...own2].flatMap(d=>inRun.map(p=>path61.join(d,p))),...[os11.tmpdir(),"/tmp"].map(d=>path61.join(d,"podman")),...platform==="darwin"?[path61.join(FINCH_ON_MAC,"finch/sock")]:[],...named?[named]:[]]}var RUNTIME_KEYS=[".lima/_config",".colima/_lima/_config",".orbstack/ssh",".local/share/containers/podman/machine/machine",".finch/lima/data/_config",".local/share/rancher-desktop/lima/_config","Library/Application Support/rancher-desktop/lima/_config"],RUNTIME_PORTS=[2375,2376];function tokenFiles(home2){let out22=[path61.join(home2,".pypirc"),path61.join(home2,".composer","auth.json"),path61.join(home2,".config","composer","auth.json"),path61.join(home2,".gem","credentials")],npmrc=path61.join(home2,".npmrc");try{/(_authToken|_auth|_password)\s*=/.test(fs69.readFileSync(npmrc,"utf8"))&&out22.push(npmrc)}catch{}return out22}function aiToolPrivate(home2){return AI_TOOLS.flatMap(t=>t.privateFiles?.(home2)??[])}function squadOwnFiles(){return BROWSERS.flatMap(b=>b.ownFiles?.()??[])}function ticketGuard(bp,o){let home2=o.home??os11.homedir(),cache4=storiesCacheDir(bp);return ownFolder(cache4),{...buildGuard({cwd:o.cwd,root:bp.root,cache:cache4,extraWrite:[bp.proofDir,cache4],extraRead:[cache4,...o.read??[]],home:home2,platform:o.platform??process.platform,...o.allow?{allow:o.allow}:{}}),noPush:!0}}function projectGuardFor(bp,o){let home2=o.home??os11.homedir(),cache4=projectCacheDir(bp);return ownFolder(cache4),buildGuard({cwd:o.cwd,root:bp.root,cache:cache4,extraWrite:[bp.proofDir,cache4],extraRead:[cache4,...o.read??[]],home:home2,platform:o.platform??process.platform,...o.allow?{allow:o.allow}:{}})}function realPlace(p){let rest=[];for(let at3=p;;at3=path61.dirname(at3))try{return path61.join(fs69.realpathSync(at3),...rest)}catch{if(at3===path61.dirname(at3))return p;rest.unshift(path61.basename(at3))}}function withReal2(dirs){let out22=new Set;for(let d of dirs)d&&(out22.add(d),out22.add(realPlace(d)));return[...out22]}function allowPath(p,home2=os11.homedir()){return path61.resolve(p.trim().replace(/^~(?=$|\/)/,home2))}function sharedPath(p,home2=os11.homedir()){let full=allowPath(p,home2);return full===home2||full.startsWith(`${home2}${path61.sep}`)?`~${full.slice(home2.length).split(path61.sep).join("/")}`:full}function privatePlaces(home2,platform){return[...PRIVATE.map(p=>path61.join(home2,p)),path61.join(home2,".config","gcloud"),path61.join(home2,".docker"),...RUNTIME_KEYS.map(p=>path61.join(home2,p)),...platform==="darwin"?[path61.join(FINCH_ON_MAC,"_config")]:[],path61.join(home2,"Library","Keychains"),...browserProfiles(home2,platform),...tokenFiles(home2),...aiToolPrivate(home2)]}function agentPrivatePlaces(home2=os11.homedir(),platform=process.platform){let own2=squadHome();return[...privatePlaces(home2,platform),...["secrets.yaml","app.json","console.json"].map(f=>path61.join(own2,f)),...["secrets.yaml","console.json"].map(f=>path61.join(own2,"projects","*",f))]}function buildGuard(o){let home2=o.home,flutter2=o.flutter===void 0?flutterOnPath()?.root:o.flutter,temp=withReal2([os11.tmpdir(),...o.platform==="win32"?[]:["/tmp","/private/tmp","/private/var/folders"]]),noRead=[...privatePlaces(home2,o.platform),...withReal2([squadHome()])],real2=p=>{try{return fs69.realpathSync(p)}catch{return p}};return{cwd:real2(o.cwd),root:o.root,write:[...new Set(withReal2([...o.extraWrite??[],...temp,...writableCaches(home2)]))],read:[...new Set(withReal2([...o.extraRead??[],...androidReads()]))],noRead:[...new Set(noRead)],noWrite:[...new Set(withReal2([...squadOwnFiles(),...gitPlaces(real2(o.cwd))]))],noConnect:[...new Set(withReal2(runtimeSockets(home2,o.platform)))],...o.cache?{cache:real2(o.cache)}:{},...flutter2?{writeExactly:flutterStampRegexes(real2(flutter2)),writeExactlyIn:flutterStampFolders(real2(flutter2))}:{},...o.allow?{allow:{...o.allow.allowRead?{allowRead:withReal2(o.allow.allowRead.map(p=>allowPath(p,home2)))}:{},...o.allow.allowWrite?{allowWrite:withReal2(o.allow.allowWrite.map(p=>allowPath(p,home2)))}:{}}}:{},...networkPolicy(o.network??projectNetwork(o.root),[],o.platform).commands==="this-computer"?{offline:!0}:{}}}function sb(s){return`"${s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}function sbRegex(s){return`#"${s.replace(/"/g,'\\"')}"`}function ancestorsOf(paths){let out22=new Set;for(let p of paths){let dir=path61.dirname(p);for(;dir&&dir!==path61.dirname(dir);)out22.add(dir),dir=path61.dirname(dir)}return[...out22]}function envRegexFor(root){return`^${root.replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/\/$/,"")}/(.*/)?\\.env(\\.[^/]*)?$`}function profileText(spec){let lines3=["(version 1)","(allow default)"],allowRead=spec.allow?.allowRead??[],allowWrite=spec.allow?.allowWrite??[],reAllow=[spec.cwd,...spec.read,...allowRead,...spec.hookGit?[spec.hookGit]:[]],envRule=`(deny file-read* (regex ${sbRegex(envRegexFor(realPlace(spec.root)))}))`;lines3.push(";; reads: hide keys, sign-ins and squad-kit, then allow the copy back");for(let d of spec.noRead)lines3.push(`(deny file-read* (subpath ${sb(d)}))`);lines3.push(envRule);let writable=[spec.cwd,...spec.write,...allowWrite];for(let a of ancestorsOf([...reAllow,...writable]))lines3.push(`(allow file-read-metadata (literal ${sb(a)}))`);for(let r of reAllow)lines3.push(`(allow file-read* (subpath ${sb(r)}))`);let realRoot=realPlace(spec.root);if(!spec.ownEnv&&(spec.cwd===realRoot||spec.cwd.startsWith(`${realRoot}/`))){lines3.push(envRule);for(let a of allowRead)lines3.push(`(allow file-read* (subpath ${sb(a)}))`)}lines3.push(";; writes: only the copy and the caches"),lines3.push('(deny file-write* (subpath "/"))');for(let w of writable)lines3.push(`(allow file-write* (subpath ${sb(w)}))`);for(let dev of["/dev/null","/dev/zero","/dev/dtracehelper","/dev/tty","/dev/random","/dev/urandom","/dev/fd","/dev/stdout","/dev/stderr"])lines3.push(`(allow file-write* (subpath ${sb(dev)}))`);for(let s of spec.noConnect)lines3.push(`(deny file-write* (subpath ${sb(s)}))`);for(let a of allowWrite)lines3.push(`(allow file-write* (subpath ${sb(a)}))`);for(let r of spec.writeExactly??[])lines3.push(`(allow file-write* (regex ${sbRegex(r)}))`);for(let a of[spec.cwd,...ancestorsOf([spec.cwd,...spec.noWrite])])lines3.push(`(deny file-write-unlink (literal ${sb(a)}))`,`(deny file-write-create (literal ${sb(a)}))`);for(let n of spec.noWrite)lines3.push(`(deny file-write* (subpath ${sb(n)}))`);spec.hookGit&&lines3.push(`(allow file-write* (subpath ${sb(spec.hookGit)}))`),lines3.push(";; connections: nothing in a hidden place, and no container runtime, unless an engineer allowed its folder");for(let d of spec.noRead)lines3.push(`(deny network-outbound (subpath ${sb(d)}))`);for(let a of[spec.cwd,...spec.read,...spec.write,...spec.hookGit?[spec.hookGit]:[]])lines3.push(`(allow network-outbound (subpath ${sb(a)}))`);for(let s of spec.noConnect)lines3.push(`(deny network-outbound (subpath ${sb(s)}))`);spec.offline&&lines3.push('(deny network-outbound (remote ip "*:*"))','(allow network-outbound (remote ip "localhost:*"))');for(let port of RUNTIME_PORTS)lines3.push(`(deny network-outbound (remote tcp "localhost:${port}"))`);for(let a of[...allowRead,...allowWrite])lines3.push(`(allow network-outbound (subpath ${sb(a)}))`);return spec.openSocket&&lines3.push(`(allow network-outbound (literal ${sb(spec.openSocket)}))`),lines3.push("(deny signal)","(allow signal (target same-sandbox))"),lines3.push("(deny job-creation)"),`${lines3.join(`
|
|
376
376
|
`)}
|
|
377
|
-
`}function bubblewrapArgs(spec,command){let args=["--ro-bind","/","/","--dev","/dev","--proc","/proc","--tmpfs","/tmp","--die-with-parent","--new-session","--unshare-pid","--unshare-uts","--unshare-ipc"],at3=places=>[...new Set(places.map(realPlace))],hide=places=>{for(let d of at3(places))fs69.existsSync(d)&&(looksLikeFile(d)?args.push("--ro-bind-try","/dev/null",d):args.push("--tmpfs",d))};for(let dir of at3(spec.writeExactlyIn??[]))args.push(...throwawayFolder(dir,spec.writeExactly??[]));hide(spec.noRead),args.push("--bind",spec.cwd,spec.cwd);for(let r of at3(spec.read))args.push("--ro-bind-try",r,r);for(let w of at3(spec.write))args.push("--bind-try",w,w);let own2=realPlace(spec.cwd);hide([...spec.noConnect,...envFilesIn2(spec.root).filter(f=>!spec.ownEnv||!realPlace(f).startsWith(`${own2}/`))]);for(let r of at3(spec.allow?.allowRead??[]))args.push("--ro-bind-try",r,r);for(let w of at3(spec.allow?.allowWrite??[]))args.push("--bind-try",w,w);for(let n of at3(spec.noWrite))args.push("--ro-bind-try",n,n);return spec.hookGit&&(args.push("--bind",spec.hookGit,spec.hookGit),fs69.lstatSync(path61.join(spec.cwd,".git"),{throwIfNoEntry:!1})?.isFile()&&args.push("--ro-bind-try",path61.join(spec.hookGit,HOOK_GIT_LINK),path61.join(spec.cwd,".git"))),args.push("/bin/sh","-c",command),args}function throwawayFolder(dir,leaveOut2){let names2;try{if(!fs69.statSync(dir).isDirectory())return[];names2=fs69.readdirSync(dir)}catch{return[]}let out22=["--tmpfs",dir];for(let name of names2){let at3=path61.join(dir,name);if(!leaveOut2.some(r=>new RegExp(r).test(at3)))try{fs69.lstatSync(at3).isSymbolicLink()?out22.push("--symlink",fs69.readlinkSync(at3),at3):out22.push("--ro-bind-try",at3,at3)}catch{}}return out22}var NO_ENV_IN=new Set(["node_modules","vendor","dist","build","target","venv",".venv",".git",".squad"]);function envFilesIn2(root,depth=3){let entries;try{entries=fs69.readdirSync(root,{withFileTypes:!0})}catch{return[]}return entries.flatMap(e=>{let at3=path61.join(root,e.name);return e.isDirectory()?depth>0&&!NO_ENV_IN.has(e.name)?envFilesIn2(at3,depth-1):[]:/^\.env(\.|$)/.test(e.name)?[at3]:[]})}function looksLikeFile(p){try{return!fs69.statSync(p).isDirectory()}catch{return/\.[A-Za-z0-9]{1,8}$/.test(path61.basename(p))}}function guardSupportOn(h){return h.platform==="darwin"?h.has(SEATBELT)?{on:!0,why:"squad-kit's guard (macOS Seatbelt)"}:{on:!1,why:"macOS sandbox-exec is missing on this Mac"}:h.platform==="win32"?{on:!1,why:"squad-kit's guard does not run on Windows yet"}:h.platform!=="linux"?{on:!1,why:"squad-kit's guard does not run on this system"}:/-Microsoft$/i.test(h.release)?{on:!1,why:"WSL 1 has no guard",fix:"Move this Linux to WSL 2: wsl --set-version <name> 2"}:h.has("bwrap")?h.bwrapWorks()?{on:!0,why:"squad-kit's guard (bubblewrap)"}:{on:!1,why:"bubblewrap cannot make a guard on this Linux",fix:"Allow unprivileged user namespaces for bwrap (on Ubuntu 24.04 this needs an AppArmor profile for it)"}:{on:!1,why:"bubblewrap is not installed",fix:"Install it, for example: sudo apt install bubblewrap"}}var support2=null;function guardSupport(){return support2??=guardSupportOn({platform:process.platform,release:os11.release(),has:onPath3,bwrapWorks:bwrapWorks2})}function onPath3(program2){return path61.isAbsolute(program2)?fs69.existsSync(program2):(process.env.PATH??"").split(path61.delimiter).some(d=>d&&fs69.existsSync(path61.join(d,program2)))}function bwrapWorks2(){try{return execFileSync7("bwrap",["--ro-bind","/","/","--dev","/dev","true"],{stdio:"ignore",timeout:5e3}),!0}catch{return!1}}function secretEnvNames(){return[...new Set(["SQUAD_*",...AI_TOOLS.flatMap(t=>t.secretEnv??[]),"ANTHROPIC_*","OPENAI_*","CLAUDE_*","GITHUB_TOKEN","GH_TOKEN","GH_ENTERPRISE_TOKEN","GITHUB_ENTERPRISE_TOKEN","AWS_*","AZURE_*","GOOGLE_*","GCLOUD_*","GCP_*","JIRA_*","NPM_TOKEN","NODE_AUTH_TOKEN"])]}function isSecretEnv(name,names2=secretEnvNames()){let upper=name.toUpperCase();return names2.some(n=>n.endsWith("*")?upper.startsWith(n.slice(0,-1).toUpperCase()):upper===n.toUpperCase())}function projectEnv(extra={}){let names2=secretEnvNames(),env2=childEnv();for(let key of Object.keys(env2))isSecretEnv(key,names2)&&delete env2[key];return{...env2,...extra}}function guardEnv(guard,cwd){return{...guard?.cache?cacheHomeEnv(guard.cache,{wall:"project",cwd}):{},...guard?.noPush?noPushEnv():{}}}function spawnProject(command,o){let env2={...
|
|
377
|
+
`}function bubblewrapArgs(spec,command){let args=["--ro-bind","/","/","--dev","/dev","--proc","/proc","--tmpfs","/tmp","--die-with-parent","--new-session","--unshare-pid","--unshare-uts","--unshare-ipc"],at3=places=>[...new Set(places.map(realPlace))],hide=places=>{for(let d of at3(places))fs69.existsSync(d)&&(looksLikeFile(d)?args.push("--ro-bind-try","/dev/null",d):args.push("--tmpfs",d))};for(let dir of at3(spec.writeExactlyIn??[]))args.push(...throwawayFolder(dir,spec.writeExactly??[]));hide(spec.noRead),args.push("--bind",spec.cwd,spec.cwd);for(let r of at3(spec.read))args.push("--ro-bind-try",r,r);for(let w of at3(spec.write))args.push("--bind-try",w,w);let own2=realPlace(spec.cwd);hide([...spec.noConnect,...envFilesIn2(spec.root).filter(f=>!spec.ownEnv||!realPlace(f).startsWith(`${own2}/`))]);for(let r of at3(spec.allow?.allowRead??[]))args.push("--ro-bind-try",r,r);for(let w of at3(spec.allow?.allowWrite??[]))args.push("--bind-try",w,w);for(let n of at3(spec.noWrite))args.push("--ro-bind-try",n,n);return spec.hookGit&&(args.push("--bind",spec.hookGit,spec.hookGit),fs69.lstatSync(path61.join(spec.cwd,".git"),{throwIfNoEntry:!1})?.isFile()&&args.push("--ro-bind-try",path61.join(spec.hookGit,HOOK_GIT_LINK),path61.join(spec.cwd,".git"))),args.push("/bin/sh","-c",command),args}function throwawayFolder(dir,leaveOut2){let names2;try{if(!fs69.statSync(dir).isDirectory())return[];names2=fs69.readdirSync(dir)}catch{return[]}let out22=["--tmpfs",dir];for(let name of names2){let at3=path61.join(dir,name);if(!leaveOut2.some(r=>new RegExp(r).test(at3)))try{fs69.lstatSync(at3).isSymbolicLink()?out22.push("--symlink",fs69.readlinkSync(at3),at3):out22.push("--ro-bind-try",at3,at3)}catch{}}return out22}var NO_ENV_IN=new Set(["node_modules","vendor","dist","build","target","venv",".venv",".git",".squad"]);function envFilesIn2(root,depth=3){let entries;try{entries=fs69.readdirSync(root,{withFileTypes:!0})}catch{return[]}return entries.flatMap(e=>{let at3=path61.join(root,e.name);return e.isDirectory()?depth>0&&!NO_ENV_IN.has(e.name)?envFilesIn2(at3,depth-1):[]:/^\.env(\.|$)/.test(e.name)?[at3]:[]})}function looksLikeFile(p){try{return!fs69.statSync(p).isDirectory()}catch{return/\.[A-Za-z0-9]{1,8}$/.test(path61.basename(p))}}function guardSupportOn(h){return h.platform==="darwin"?h.has(SEATBELT)?{on:!0,why:"squad-kit's guard (macOS Seatbelt)"}:{on:!1,why:"macOS sandbox-exec is missing on this Mac"}:h.platform==="win32"?{on:!1,why:"squad-kit's guard does not run on Windows yet"}:h.platform!=="linux"?{on:!1,why:"squad-kit's guard does not run on this system"}:/-Microsoft$/i.test(h.release)?{on:!1,why:"WSL 1 has no guard",fix:"Move this Linux to WSL 2: wsl --set-version <name> 2"}:h.has("bwrap")?h.bwrapWorks()?{on:!0,why:"squad-kit's guard (bubblewrap)"}:{on:!1,why:"bubblewrap cannot make a guard on this Linux",fix:"Allow unprivileged user namespaces for bwrap (on Ubuntu 24.04 this needs an AppArmor profile for it)"}:{on:!1,why:"bubblewrap is not installed",fix:"Install it, for example: sudo apt install bubblewrap"}}var support2=null;function guardSupport(){return support2??=guardSupportOn({platform:process.platform,release:os11.release(),has:onPath3,bwrapWorks:bwrapWorks2})}function onPath3(program2){return path61.isAbsolute(program2)?fs69.existsSync(program2):(process.env.PATH??"").split(path61.delimiter).some(d=>d&&fs69.existsSync(path61.join(d,program2)))}function bwrapWorks2(){try{return execFileSync7("bwrap",["--ro-bind","/","/","--dev","/dev","true"],{stdio:"ignore",timeout:5e3}),!0}catch{return!1}}function secretEnvNames(){return[...new Set(["SQUAD_*",...AI_TOOLS.flatMap(t=>t.secretEnv??[]),"ANTHROPIC_*","OPENAI_*","CLAUDE_*","GITHUB_TOKEN","GH_TOKEN","GH_ENTERPRISE_TOKEN","GITHUB_ENTERPRISE_TOKEN","AWS_*","AZURE_*","GOOGLE_*","GCLOUD_*","GCP_*","JIRA_*","NPM_TOKEN","NODE_AUTH_TOKEN"])]}function isSecretEnv(name,names2=secretEnvNames()){let upper=name.toUpperCase();return names2.some(n=>n.endsWith("*")?upper.startsWith(n.slice(0,-1).toUpperCase()):upper===n.toUpperCase())}function projectEnv(extra={}){let names2=secretEnvNames(),env2=childEnv();for(let key of Object.keys(env2))isSecretEnv(key,names2)&&delete env2[key];return{...env2,...extra}}function guardEnv(guard,cwd){return{...guard?.cache?cacheHomeEnv(guard.cache,{wall:"project",cwd}):{},...guard?.noPush?noPushEnv():{}}}function spawnProject(command,o){let start=projectEnv(),env2={...start,...partEnv(command,o.cwd,start),...guardEnv(o.guard,o.cwd),...usesAndroid(command)?androidCommandEnv(o.guard?.cache?{cache:o.guard.cache}:{}):{},...o.env??{},...copyDbEnv(o.cwd)},stdio=o.stdio??["ignore","pipe","pipe"],base2={cwd:o.cwd,detached:OWN_GROUP,stdio,env:env2};return o.guard&&process.platform==="darwin"?track(spawn10(SEATBELT,["-p",profileText(o.guard),"/bin/sh","-c",command],base2)):o.guard&&process.platform==="linux"?(makeOwnFolders(o.guard),track(spawn10("bwrap",bubblewrapArgs(o.guard,command),base2))):track(spawn10(command,{...base2,shell:!0}))}var HOOK_GIT_LINK="dotgit";function commitGuard(guard,at3){return{...guard,noWrite:[...new Set([...guard.noWrite,at3.common,at3.own])],hookGit:at3.gitDir}}function makeOwnFolders(guard,home2=squadHome()){for(let w of guard.write)if(!path61.relative(home2,w).startsWith(".."))try{fs69.mkdirSync(w,{recursive:!0})}catch{}}var BLOCKED_LINES=[/(?:EPERM|EROFS)[^\n]*?['"]([^'"\n]+)['"]/i,/(?:operation not permitted|read-only file system)[^\n]*?['"]([^'"\n]+)['"]/i,/['"]([^'"\n]+)['"]: (?:Operation not permitted|Read-only file system)/i,/([^\s'":]+): (?:Operation not permitted|Read-only file system)/i,/(?:Operation not permitted|Read-only file system)[^\n]*?(\/[^\s'":]+)/i,/['"](\/[^'"\n]+)['"][^\n]*?(?:Operation not permitted|Read-only file system)/i],NPM_REFUSED=/npm (?:error|ERR!) code (?:EPERM|EROFS|EACCES)[\s\S]*?npm (?:error|ERR!) path (\/[^\n]+)/,SOCKET_REFUSED=[/\bunix:\/\/(\/[^\s'":]+?)\.?(?=[\s'":]|$)/,/dial unix (\/[^\s'":]+)/,/connect E[A-Z]+ (\/[^\s'":]+)/],PORT_REFUSED=/(?:dial tcp |connect E[A-Z]+ |daemon at tcp:\/\/)(?:127\.0\.0\.1|\[?::1\]?|localhost):(2375|2376)\b/,RUNTIME_NOT_FOUND=/Could not find a (?:working container runtime|valid Docker environment)|Error while fetching server API version|docker: (?:unknown command: docker compose|'compose' is not a docker command)/;function runtimeRefused(output,guard){let under=(place,base2)=>place===base2||place.startsWith(`${base2.replace(/\/$/,"")}/`),allowed=[...guard.allow?.allowRead??[],...guard.allow?.allowWrite??[]],open3=guard.noConnect.some(s=>allowed.some(a=>under(s,a)||under(a,s)));for(let line3 of output.split(`
|
|
378
378
|
`)){for(let re of SOCKET_REFUSED){let place=re.exec(line3)?.[1];if(place&&guard.noConnect.some(s=>under(place,s))&&!allowed.some(a=>under(place,a)))return place}let port=PORT_REFUSED.exec(line3)?.[1];if(port)return`localhost:${port}`;if(!open3&&RUNTIME_NOT_FOUND.test(line3))return"Docker"}return null}function guardBlockedPath(output,guard){if(!output)return null;let runtime=runtimeRefused(output,guard);if(runtime)return runtime;let npm=NPM_REFUSED.exec(output)?.[1]?.trim();if(npm&&isBlocked(npm,guard))return npm;for(let line3 of output.split(`
|
|
379
379
|
`))for(let re of BLOCKED_LINES){let said2=re.exec(line3)?.[1]?.trim(),target=said2&&path61.resolve(guard.cwd,said2);if(target&&isBlocked(target,guard))return target}return null}function homeCachesRefused(output,guard,home2=os11.homedir()){if(!output)return[];let places=[NPM_REFUSED.exec(output)?.[1]?.trim(),...output.split(`
|
|
380
380
|
`).flatMap(line3=>BLOCKED_LINES.map(re=>re.exec(line3)?.[1]?.trim()))].flatMap(p=>{let target=p&&path61.resolve(guard.cwd,p),place=target&&isBlocked(target,guard)?homeCachePlace(target,home2,guard.noRead):null;return place?[place]:[]});return[...new Set(places)]}function isBlocked(target,guard){let under=base2=>target===base2||target.startsWith(`${base2.replace(/\/$/,"")}/`);return guard.noWrite.some(under)?!0:(guard.writeExactly??[]).some(r=>new RegExp(r).test(target))||[guard.cwd,...guard.read,...guard.cache?[guard.cache]:[],...guard.allow?.allowRead??[],...guard.allow?.allowWrite??[]].some(under)?!1:guard.noRead.some(under)||under(guard.root)&&/\/\.env(\.|$)/.test(target)?!0:!guard.write.some(under)&&!/^\/(?:dev|proc|sys)\//.test(target)}var GUARD_BLOCKED="A check needed a file outside its copy, and squad-kit blocked it. An engineer can allow it in Settings.",GUARD_BLOCKED_DOCKER="A check tried to use Docker, which is not open to the project's own commands. An engineer can allow it in Settings.";function dockerBlockedWords(output,guard){if(!guard)return null;let target=guardBlockedPath(output,guard);return target&&blockedDocker(target,guard)?GUARD_BLOCKED_DOCKER:null}function blockedDocker(target,guard){return!target.startsWith("/")||guard.noConnect.some(s=>target===s||target.startsWith(`${s}/`))}function guardBlockedDetail(target,command,guard){if(!guard||!blockedDocker(target,guard))return`squad-kit's guard blocked ${target} for: ${command}
|
|
@@ -465,7 +465,7 @@ ${parts2.join(`
|
|
|
465
465
|
`),found2=[];for(let read9 of[playwrightFailed,vitestFailed,jestFailed,pytestFailed,goFailed]){let some=read9(lines3);if(some===null)return[];found2.push(...some)}return found2}function quoted3(word2){return`'${word2.replace(/'/g,"'\\''")}'`}function exactly2(text4){return text4.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function wordsOf(text4){let out22=[],word2=null;for(let i=0;i<text4.length;i+=1){let ch=text4[i];if(/\s/.test(ch))word2!==null&&out22.push(word2),word2=null;else if(ch==="'"){let end=text4.indexOf("'",i+1);if(end<0)return null;word2=(word2??"")+text4.slice(i+1,end),i=end}else if(ch==='"'){let j=i+1,inside5="";for(;j<text4.length&&text4[j]!=='"';j+=1){if("$`".includes(text4[j]))return null;text4[j]==="\\"&&j+1<text4.length&&(j+=1),inside5+=text4[j]}if(j>=text4.length)return null;word2=(word2??"")+inside5,i=j}else if(ch==="\\"){if(i+1>=text4.length)return null;i+=1,word2=(word2??"")+text4[i]}else{if("$`|;&<>(){}#\n".includes(ch))return null;word2=(word2??"")+ch}}return word2!==null&&out22.push(word2),out22}function splitAnd(text4){let parts2=[],quote="",start=0;for(let i=0;i<text4.length;i+=1){let ch=text4[i];quote?ch===quote?quote="":ch==="\\"&"e==='"'&&(i+=1):ch==="'"||ch==='"'?quote=ch:ch==="\\"?i+=1:ch==="&"&&text4[i+1]==="&"&&(parts2.push(text4.slice(start,i)),start=i+2,i+=1)}return parts2.push(text4.slice(start)),parts2.map(p=>p.trim())}function scriptsIn(dir){try{return JSON.parse(fs80.readFileSync(path73.join(dir,"package.json"),"utf8")).scripts??{}}catch{return null}}var TOOL_BINS=new Set(["playwright","vitest","jest","pytest","py.test"]),MAY_RUN_ANYTHING=new Set(["npm","pnpm","yarn","bun","bunx","npx","cd","pushd","sh","bash","zsh","make","turbo","nx","lerna","env","exec","eval","xargs","concurrently","npm-run-all","run-s","run-p","dotenv","env-cmd","cross-env-shell","poetry","uv","pipenv","tox","nox"]);function scriptOf(words2,scripts){let[tool5,...rest]=words2;if(!["npm","pnpm","yarn","bun"].includes(tool5))return;let i=0;for(;rest[i]?.startsWith("-");)i+=1;let name=rest[i];if(!name)return null;if(name==="exec"||name==="dlx"||name==="x")return;if(name==="run"||name==="run-script"){for(i+=1;rest[i]?.startsWith("-");)i+=1;name=rest[i]}else if(tool5==="npm"&&name==="t")name="test";else{if(tool5==="npm"&&!["test","start"].includes(name))return null;if(tool5==="bun"&&name==="test")return null}if(!name||!scripts)return null;let text4=scripts[name];if(text4===void 0)return tool5!=="npm"&&TOOL_BINS.has(name)?[rest.slice(i).map(quoted3).join(" ")]:null;let extra=rest.slice(i+1).filter((w,k)=>!(k===0&&w==="--"));return[scripts[`pre${name}`],`${text4}${extra.length?` ${extra.map(quoted3).join(" ")}`:""}`,scripts[`post${name}`]].filter(s=>!!s?.trim())}function chainOf(dir,command,scripts=scriptsIn(dir),depth=0){if(depth>5)return null;let out22=[];for(let part of splitAnd(command)){let words2=wordsOf(part);if(!words2?.length)return null;let script=scriptOf(words2,scripts);if(script===null)return null;if(script===void 0){out22.push(words2);continue}for(let text4 of script){let inner=chainOf(dir,text4,scripts,depth+1);if(!inner)return null;out22.push(...inner)}}return out22}var CARRY={playwright:{value:["-c","--config","--retries"],flag:[]},vitest:{value:["-c","--config","-r","--root","--dir","--mode","--environment"],flag:[]},jest:{value:["-c","--config","--rootDir","--selectProjects","--env"],flag:["-i","--runInBand"]},pytest:{value:["-c","--rootdir","-p","--ds","--dc","-o","--confcutdir"],flag:[]},go:{value:["-tags"],flag:["-race"]}};function toolRunOf(words0){let words2=[...words0],env2=[],takeEnv=()=>{for(;words2[0]&&/^[A-Za-z_]\w*=/.test(words2[0]);){let[name,...value2]=words2.shift().split("=");env2.push(`${name}=${quoted3(value2.join("="))}`)}};if(takeEnv(),words2[0]==="cross-env"&&(words2.shift(),takeEnv()),words2[0]==="npx"||words2[0]==="bunx")for(words2.shift();words2[0]?.startsWith("-");)words2.shift();else(words2[0]==="pnpm"||words2[0]==="yarn"||words2[0]==="npm")&&words2[1]==="exec"?(words2=words2.slice(2),words2[0]==="--"&&words2.shift()):(words2[0]==="pnpm"||words2[0]==="yarn")&&TOOL_BINS.has(words2[1]??"")&&words2.shift();let program2=path73.basename(words2[0]??""),tool5=null,args=[];if(program2==="playwright"&&words2[1]==="test"?[tool5,args]=["playwright",words2.slice(2)]:program2==="vitest"&&(!words2[1]||words2[1]==="run"||words2[1].startsWith("-"))?[tool5,args]=["vitest",words2.slice(words2[1]==="run"?2:1)]:program2==="jest"?[tool5,args]=["jest",words2.slice(1)]:program2==="pytest"||program2==="py.test"?[tool5,args]=["pytest",words2.slice(1)]:/^python(\d+(\.\d+)?)?$/.test(program2)&&words2[1]==="-m"&&words2[2]==="pytest"?[tool5,args]=["pytest",words2.slice(3)]:program2==="go"&&words2[1]==="test"&&([tool5,args]=["go",words2.slice(2)]),!tool5&&(program2==="playwright"&&words2[1]==="install"||program2==="go"&&["vet","build","fmt"].includes(words2[1]??"")))return"other";if(!tool5)return MAY_RUN_ANYTHING.has(program2)||TOOL_BINS.has(program2)||program2==="go"?null:"other";let carry=[],projects=[],{value,flag}=CARRY[tool5];for(let i=0;i<args.length;i+=1){let[name,given]=args[i].includes("=")?[args[i].slice(0,args[i].indexOf("=")),args[i].slice(args[i].indexOf("=")+1)]:[args[i],void 0],isProject=name==="--project"&&(tool5==="playwright"||tool5==="vitest");if(flag.includes(name))carry.push(name);else if(value.includes(name)||isProject){let v=given??args[i+=1];if(v===void 0)return null;isProject?projects.push(v):carry.push(tool5==="go"||name.startsWith("--")?`${name}=${quoted3(v)}`:`${name} ${quoted3(v)}`)}}return{tool:tool5,env:env2,carry,projects}}function toolRuns(dir,command){let chain=chainOf(dir,command);if(!chain?.length)return null;let runs2=[];for(let words2 of chain){let run7=toolRunOf(words2);if(run7===null)return null;runs2.push(run7)}return runs2}function installed(dir,tool5){if(tool5==="go")return fs80.existsSync(path73.join(dir,"go.mod"))?{}:null;if(tool5==="pytest"){for(let venv of[".venv","venv","env"])if(fs80.existsSync(path73.join(dir,venv,"bin","pytest"))&&fs80.existsSync(path73.join(dir,venv,"bin","python")))return{python:`${venv}/bin/python`};return null}return fs80.existsSync(path73.join(dir,"node_modules",".bin",tool5))?{}:null}function goModule(dir){try{return fs80.readFileSync(path73.join(dir,"go.mod"),"utf8").match(/^module\s+(\S+)/m)?.[1]??null}catch{return null}}function goFolder(dir,pkg){let mod=goModule(dir);return mod?pkg===mod?".":pkg.startsWith(`${mod}/`)?`./${pkg.slice(mod.length+1)}`:null:null}function commandFor(dir,run7,t){let has4=installed(dir,run7.tool);if(!has4)return null;let env2=run7.env.length?`${run7.env.join(" ")} `:"",carry=run7.carry.length?` ${run7.carry.join(" ")}`:"",projects=(t.project?[t.project]:run7.projects).map(p=>` --project=${quoted3(p)}`).join("");switch(run7.tool){case"playwright":return`${env2}npx playwright test${carry}${projects} ${quoted3(`${exactly2(t.file)}${t.line?`:${t.line}`:""}`)}`;case"vitest":return`${env2}npx vitest run${carry}${projects} ${quoted3(t.file)}${t.name?` -t ${quoted3(`${exactly2(t.name.split(" > ").join(" "))}$`)}`:""}`;case"jest":return`${env2}npx jest${carry} ${quoted3(exactly2(t.file))}${t.name?` -t ${quoted3(`^${exactly2(t.name.split(" \u203A ").join(" "))}$`)}`:""}`;case"pytest":return`${env2}${has4.python} -m pytest${carry} ${quoted3(t.name?`${t.file}::${t.name}`:t.file)}`;case"go":{let folder=t.pkg?goFolder(dir,t.pkg):null;return!folder||!t.name?null:`${env2}go test${carry} -count=1 -run ${quoted3(`^${exactly2(t.name.split("/")[0])}$`)} ${quoted3(folder)}`}}}function testAlone(dir,testCommand,t){let runs2=toolRuns(dir,testCommand),last=runs2?.[runs2.length-1];return!runs2||!last||last==="other"||last.tool!==t.tool||runs2.slice(0,-1).some(r=>r!=="other"&&r.tool===t.tool)||t.file.startsWith("-")||t.name?.startsWith("-")?null:commandFor(dir,last,t)}function playwrightFolder(dir,run7){let said2=run7.carry.map(c=>c.match(/^(?:-c |--config=)'(.*)'$/)?.[1]).find(Boolean),files=said2?[said2]:["playwright.config.ts","playwright.config.js","playwright.config.mjs","playwright.config.cjs"];for(let f of files)try{let folder=fs80.readFileSync(path73.join(dir,f),"utf8").match(/testDir\s*:\s*['"`]([^'"`$]+)['"`]/)?.[1];return folder?path73.normalize(path73.join(path73.dirname(f),folder)):null}catch{}return null}function fileAlone(dir,testCommand,asked){let m=asked.trim().match(/^(.+?)(?::(\d+))?$/);if(!m)return null;let file3=path73.normalize(m[1]).replace(/^\.\//,"");if(!file3||file3.startsWith("-")||file3.startsWith("..")||path73.isAbsolute(file3)||!fs80.existsSync(path73.join(dir,file3)))return null;let runs2=(toolRuns(dir,testCommand)??[]).filter(r=>r!=="other"&&r.tool!=="go"),kinds=[...new Set(runs2.map(r=>r.tool))],tool5=kinds.length===1?kinds[0]:void 0;if(kinds.length===2&&kinds.includes("playwright")){let folder=playwrightFolder(dir,runs2.find(r=>r.tool==="playwright"));folder&&folder!=="."&&(tool5=file3===folder||file3.startsWith(`${folder}${path73.sep}`)?"playwright":kinds.find(k=>k!=="playwright"))}let run7=runs2.find(r=>r.tool===tool5);if(!run7)return null;let command=commandFor(dir,run7,{file:file3,...m[2]&&run7.tool==="playwright"?{line:Number(m[2])}:{}});return command?{command,tool:run7.tool}:null}function inChanged(t,changed,dir){if(t.tool==="go"){let folder=t.pkg?goFolder(dir,t.pkg):null;if(!folder)return!0;let rel=folder==="."?"":`${folder.slice(2)}/`;return changed.some(f=>f.startsWith(rel)&&!f.slice(rel.length).includes("/")&&f.endsWith("_test.go"))}let file3=t.file.replace(/^\.\//,"");return changed.some(f=>f===file3||f.endsWith(`/${file3}`))}import{spawn as spawn14}from"child_process";import fs83 from"fs";import os13 from"os";import path77 from"path";import fs82 from"fs";import path76 from"path";import fs81 from"fs";import path74 from"path";var MAX_PARTS=30,repoPaths=meta=>meta.repos?.length?meta.repos.map(r=>r.path):["."],commandsOn=(meta,repo)=>meta.repos?.length?meta.repos.find(r=>r.path===repo)?.commands:meta.commands,aloneInRepo=(parts2,p)=>!p.shared&&parts2.filter(x=>!x.shared&&x.repo===p.repo).length===1,withoutUrl=({url:_url,...rest})=>rest,idFrom=text4=>text4.trim().toLowerCase().replace(/[^\p{L}\p{N}_-]+/gu,"-").replace(/^-+|-+$/g,"").slice(0,40);function partOfRepo(meta,repo){let words2=repo.replace(/[-_.]+/g," ").trim(),name=repo==="."||!words2?meta.project:`${words2[0].toUpperCase()}${words2.slice(1)}`;return{id:idFrom(repo)||"app",name,repo,needs:[]}}function partsOf2(meta){let parts2=meta.parts?.length?meta.parts:repoPaths(meta).map(r=>partOfRepo(meta,r));return parts2.map(p=>{let url=aloneInRepo(parts2,p)?commandsOn(meta,p.repo)?.url?.trim():void 0;return url?{...p,url}:p})}function severalParts(meta){return partsOf2(meta).filter(p=>!p.shared).length>1}var wordsOf2=names2=>names2.length<2?names2.join(""):`${names2.slice(0,-1).join(", ")} and ${names2.at(-1)}`;function productLine(meta){if(!meta.parts?.length||!severalParts(meta))return;let on=partsOf2(meta).filter(p=>!p.off&&!p.noRun),running5=on.filter(p=>!p.shared).map(p=>p.name),shared=on.filter(p=>p.shared).map(p=>p.name),first2=running5.length===1?`The product has one part in use: ${running5[0]}.`:`The product has ${running5.length} parts: ${wordsOf2(running5)}.`;return shared.length?`${first2} ${running5.length===1?"It uses":"They share"} ${wordsOf2(shared)}.`:first2}function runsAsProduct(meta){return!!meta.parts?.length&&partsOf2(meta).filter(p=>!p.shared&&!p.off&&!p.noRun).length>1}function partsToRun(meta,changed){let parts2=partsOf2(meta).filter(p=>!p.off),want=new Set(changed.filter(id2=>parts2.some(p=>p.id===id2))),grow=more3=>{for(let before=-1;before!==want.size;){before=want.size;for(let p of parts2)for(let id2 of more3(p))parts2.some(x=>x.id===id2)&&want.add(id2)}};grow(p=>p.needs.some(n=>want.has(n))?[p.id]:[]),grow(p=>want.has(p.id)?p.needs:[]);let ordered=[],seen=new Set,visit=p=>{if(!seen.has(p.id)){seen.add(p.id);for(let n of p.needs){let dep=parts2.find(x=>x.id===n);dep&&want.has(dep.id)&&visit(dep)}ordered.push(p)}};return parts2.filter(p=>want.has(p.id)).forEach(visit),ordered.filter(p=>!p.shared&&!p.noRun)}function pictureInUse(meta,picture){let known=partsOf2(meta),off=known.filter(p=>p.off),parts2=picture.parts.filter(p=>!off.some(o=>o.id===p.id||o.name.toLowerCase()===p.name.toLowerCase())).map(p=>{let run7=known.find(k=>k.byHand&&k.id===p.id);return run7?{...p,name:run7.name}:p});return{parts:parts2,links:picture.links.filter(l=>parts2.some(p=>p.id===l.from)&&parts2.some(p=>p.id===l.to))}}function repoInUse(meta,repo){let parts2=(meta.parts??[]).filter(p=>p.repo===repo);return!parts2.length||parts2.some(p=>!p.off)}function repoRuns(meta,repo){let parts2=(meta.parts??[]).filter(p=>p.repo===repo);return!parts2.length||parts2.some(p=>!p.off&&!p.noRun)}function repoLabel(meta,repo){let names2=(meta.parts??[]).filter(p=>p.repo===repo&&!p.off&&!p.shared).map(p=>p.name);return names2.length?names2.join(", "):void 0}var isAddress=text4=>/^https?:\/\/[^\s/]+\S*$/i.test(text4);function folderInRepo(text4){let parts2=text4.trim().replace(/\\/g,"/").split("/").filter(x=>x&&x!==".");return text4.trim().startsWith("/")||/^[a-z]:/i.test(text4.trim())||parts2.includes("..")?null:parts2.join("/")}function reaches(parts2,from,to,seen=new Set){return from===to?!0:seen.has(from)?!1:(seen.add(from),(parts2.find(p=>p.id===from)?.needs??[]).some(n=>reaches(parts2,n,to,seen)))}function cleanParts(found2,meta,root){let repos=repoPaths(meta),parts2=[];for(let raw of(Array.isArray(found2)?found2:[]).slice(0,MAX_PARTS)){if(!raw||typeof raw!="object")continue;let p=raw,name=typeof p.name=="string"?p.name.trim():"",id2=idFrom(typeof p.id=="string"?p.id:name);if(!id2||!name||parts2.some(x=>x.id===id2))continue;let url=typeof p.url=="string"&&isAddress(p.url.trim())?p.url.trim():"";if(p.shared===!0){parts2.push({id:id2,name,shared:!0,needs:[],...url?{url}:{}});continue}let said2=typeof p.repo=="string"?p.repo.trim().replace(/\/+$/,"").replace(/^\.\//,""):"",repo=repos.includes(said2)?said2:repos.length===1?repos[0]:"";if(!repo)continue;let folder=typeof p.folder=="string"?folderInRepo(p.folder):"",real2=folder&&fs81.existsSync(path74.join(root,repo,folder))?folder:"";parts2.push({id:id2,name,repo,needs:[],...real2?{folder:real2}:{},...url?{url}:{}})}if(!parts2.some(p=>!p.shared))return[];for(let repo of meta.repos?.length?repos:[]){if(parts2.some(p2=>p2.repo===repo))continue;let p=partOfRepo(meta,repo);parts2.push({...p,id:parts2.some(x=>x.id===p.id)?`${p.id}-repo`:p.id,noRun:!0})}for(let raw of Array.isArray(found2)?found2:[]){let p=raw&&typeof raw=="object"?raw:{},part=parts2.find(x=>x.id===idFrom(typeof p.id=="string"?p.id:typeof p.name=="string"?p.name:""));for(let need of Array.isArray(p.needs)?p.needs:[]){let id2=typeof need=="string"?idFrom(need):"";part&&parts2.some(x=>x.id===id2)&&!part.needs.includes(id2)&&!reaches(parts2,id2,part.id)&&part.needs.push(id2)}}return oneAddress(meta,parts2).map(p=>p.folder&&aloneInRepo(parts2,p)?(({folder:_folder,...rest})=>rest)(p):p)}function oneAddress(meta,parts2){return parts2.map(p=>aloneInRepo(parts2,p)&&commandsOn(meta,p.repo)?.url?.trim()?withoutUrl(p):p)}var samePart=(a,b)=>a.id===b.id||a.name.toLowerCase()===b.name.toLowerCase();function partsAfterRead(found2,kept2=[]){let mine2=kept2.filter(p=>p.byHand),parts2=[...found2.map(f=>mine2.find(m=>samePart(m,f))??f),...mine2.filter(m=>!found2.some(f=>samePart(m,f)))],needed=kept2.filter(k=>!parts2.some(p=>samePart(p,k))&&mine2.some(m=>m.needs.includes(k.id))),all=[...parts2,...needed],before=[...found2,...kept2],idNow=need=>{let was=before.find(p=>p.id===need);return all.find(p=>p.id===need)?.id??(was?all.find(p=>samePart(p,was))?.id:void 0)};return all.map(p=>({...p,needs:[...new Set(p.needs.map(idNow).filter(n=>!!n))]}))}function findPart(parts2,which){let said2=which.trim().toLowerCase(),part=parts2.find(p=>p.id===said2||p.id===idFrom(said2)||p.name.toLowerCase()===said2);if(!part)throw new Error(`There is no part "${which}". The parts: ${parts2.map(p=>p.id).join(", ")}.`);return part}function applied(bp,meta,parts2,part,change){let name=change.name?.trim()??part.name;if(!name)throw new Error("Give the part a name.");if(parts2.some(p=>p.id!==part.id&&p.name.toLowerCase()===name.toLowerCase()))throw new Error(`There is a part called ${name} already.`);let shared=change.shared??!!part.shared,url=(change.url??part.url??"").trim();if(url&&!isAddress(url))throw new Error(`"${url}" is not a web address. Write it like http://localhost:5180`);let off=change.off??!!part.off;if(off&&!part.shared&&!parts2.some(p=>p.id!==part.id&&!p.shared&&!p.off))throw new Error(`${name} is the only part in use, so the squad would have nothing to work on.`);let needs=(change.needs??part.needs).map(n=>findPart(parts2,n).id);for(let id2 of needs){let other=findPart(parts2,id2);if(id2===part.id)throw new Error(`${name} cannot need itself.`);if(reaches(parts2.map(p=>p.id===part.id?{...p,needs}:p),other.id,part.id))throw new Error(`${name} and ${other.name} need each other, so neither could start first. Take one of them out.`)}let base2={id:part.id,name,needs:[...new Set(needs)],byHand:!0,...url?{url}:{},...off?{off}:{}};if(shared)return{...base2,shared};let repo=change.repo?.trim()||part.repo||(repoPaths(meta).length===1?repoPaths(meta)[0]:"");if(!repoPaths(meta).includes(repo))throw new Error(repo?`There is no code repo at ${repo}. The repos: ${repoPaths(meta).join(", ")}.`:`Say which repo it is in: ${repoPaths(meta).join(", ")}.`);let folder=folderInRepo(change.folder??part.folder??"");if(folder===null)throw new Error("The folder must be inside the repo, like apps/web.");if(folder&&!fs81.existsSync(path74.join(bp.root,repo,folder)))throw new Error(`There is no folder ${folder} in ${repo==="."?"the project":repo}.`);return{...base2,repo,...folder?{folder}:{}}}function saveParts(bp,meta,parts2,changed){let next=meta;if(changed&&aloneInRepo(parts2,changed)){let withUrl=(c={})=>{let{url:_old,...rest}=c;return changed.url?{...rest,url:changed.url}:rest};next=meta.repos?.length?{...meta,repos:meta.repos.map(r=>r.path===changed.repo?{...r,commands:withUrl(r.commands)}:r)}:{...meta,commands:withUrl(meta.commands)}}writeMeta(bp,{...next,parts:oneAddress(next,parts2)})}function addPart(bp,change){let meta=readMeta(bp),parts2=partsOf2(meta),name=change.name?.trim()??"";if(!name)throw new Error("Give the part a name.");let base2=idFrom(name)||"part",id2=base2;for(let n=2;parts2.some(p=>p.id===id2);n+=1)id2=`${base2}-${n}`;let part=applied(bp,meta,parts2,{id:id2,name,needs:[]},change);return saveParts(bp,meta,[...parts2,part],part),part}function changePart(bp,which,change){let meta=readMeta(bp),parts2=partsOf2(meta),part=applied(bp,meta,parts2,findPart(parts2,which),change);return saveParts(bp,meta,parts2.map(p=>p.id===part.id?part:p),part),part}function removePart(bp,which){let meta=readMeta(bp),parts2=partsOf2(meta),part=findPart(parts2,which);if(!part.byHand)throw new Error(`The Architect found ${part.name}, so it would come back at the next read. Mark it Not used instead.`);let left2=parts2.filter(p=>p.id!==part.id);if(!left2.some(p=>p.repo===part.repo)&&!part.shared)throw new Error(`${part.name} is the only part in ${part.repo==="."?"the project":part.repo}. Mark it Not used instead.`);saveParts(bp,meta,left2.map(p=>({...p,needs:p.needs.filter(n=>n!==part.id)})))}import path75 from"path";function copiesOf(meta,t){if(!t.worktree)return[];let first2=repoFor(meta,t),named=(repo,dir,baseCommit)=>({repo,dir,...baseCommit?{baseCommit}:{},label:repoLabel(meta,repo.path)??repo.path});return[named(first2,t.worktree,t.baseCommit),...(t.copies??[]).map(c=>named(reposOf(meta).find(r=>r.path===c.repo)??{path:c.repo,base:first2.base,remote:"",commands:{}},c.dir,c.baseCommit))]}function workFolder(t){return t.storyDir??t.worktree}function storyCopyDir(root,id2,repo){return path75.join(worktreeRoot(root),id2.toLowerCase(),repo)}function reposOfParts(meta,parts2){let known=partsOf2(meta),repos=(parts2??[]).map(id2=>known.find(p=>p.id===id2&&!p.shared&&!p.off)?.repo).filter(r=>!!r);return[...new Set(repos)].filter(r=>reposOf(meta).some(x=>x.path===r))}function repoOrder(meta){let parts2=partsOf2(meta),quiet=parts2.filter(p=>p.noRun&&p.repo).map(p=>p.repo),running5=partsToRun(meta,parts2.map(p=>p.id)).map(p=>p.repo).filter(r=>!!r);return[...new Set([...quiet,...running5])]}function repoReliesOn(meta,later,first2){let parts2=partsOf2(meta),seen=new Set,reach2=id2=>{if(seen.has(id2))return!1;seen.add(id2);let p=parts2.find(x=>x.id===id2);return!!p&&(p.repo===first2||p.needs.some(reach2))};return parts2.filter(p=>p.repo===later).some(p=>p.needs.some(reach2))}function copyViews(meta,t){if(!t.copies?.length)return[t];let{copies,...one2}=t,views=[one2,...copies.map(c=>({...one2,repo:c.repo,worktree:c.dir,...c.baseCommit?{baseCommit:c.baseCommit}:{}}))],order=repoOrder(meta),place=v=>{let at3=order.indexOf(v.repo??"");return at3<0?order.length:at3};return views.map((v,i)=>({v,i})).sort((a,b)=>place(a.v)-place(b.v)||a.i-b.i).map(x=>x.v)}function appCopyOf(meta,t){let copies=copiesOf(meta,t);return copies.find(c=>c.repo.commands.start?.trim()&&c.repo.commands.url?.trim())??copies[0]}var read4=file3=>{try{return fs82.readFileSync(file3,"utf8")}catch{return null}},has=(dir,...p)=>fs82.existsSync(path76.join(dir,...p));function packages(dir){let text4=read4(path76.join(dir,"package.json"));if(text4===null)return null;try{let j=JSON.parse(text4);return{...j.dependencies,...j.devDependencies,...j.peerDependencies}}catch{return{}}}function xcodeProject(dir){let here=d=>{try{return fs82.readdirSync(d).some(f=>f.endsWith(".xcodeproj")||f.endsWith(".xcworkspace"))}catch{return!1}};if(here(dir))return!0;try{return fs82.readdirSync(dir,{withFileTypes:!0}).some(e=>e.isDirectory()&&!e.name.startsWith(".")&&e.name!=="node_modules"&&here(path76.join(dir,e.name)))}catch{return!1}}var GRADLE=["build.gradle","build.gradle.kts"],androidApp=file3=>/com\.android\.application/.test(read4(file3)??"");function kindsOf(dir){let kinds=[],platforms=new Set,deps=packages(dir),pubspec=read4(path76.join(dir,"pubspec.yaml")),flutter2=pubspec!==null&&/^\s+sdk:\s*["']?flutter\b/m.test(pubspec),reactNative=!!(deps&&("react-native"in deps||"expo"in deps));return flutter2&&kinds.push("flutter"),reactNative&&kinds.push("react-native"),flutter2||reactNative?(has(dir,"ios")&&platforms.add("ios"),has(dir,"android")&&platforms.add("android"),has(dir,"web")&&platforms.add("web"),reactNative&&deps&&"expo"in deps&&!has(dir,"ios")&&!has(dir,"android")&&(platforms.add("ios"),platforms.add("android"))):(deps&&(kinds.push("web"),platforms.add("web")),xcodeProject(dir)&&(kinds.push("ios"),platforms.add("ios")),GRADLE.some(g=>has(dir,"android","app",g)||androidApp(path76.join(dir,"app",g))||androidApp(path76.join(dir,g)))&&(kinds.push("android"),platforms.add("android"))),["docker-compose.yml","docker-compose.yaml","compose.yml","compose.yaml"].some(f=>has(dir,f))&&kinds.push("docker"),{kinds,platforms:[...platforms]}}function placesOf(root,meta,story2){let repos=meta?reposOf(meta):[{path:".",base:"main",remote:"",commands:{}}],copies=meta&&story2?.worktree&&fs82.existsSync(story2.worktree)?copiesOf(meta,story2):[],storyRepos=new Set(copies.length?copies.map(c=>c.repo.path):story2?.repo?[story2.repo]:[]),parts2=meta?partsOf2(meta).filter(p=>!p.shared&&!p.off):[],wanted=story2?.parts?.length?new Set(story2.parts):null,out22=[];for(let r of repos){if(storyRepos.size&&!storyRepos.has(r.path))continue;let top=copies.find(c=>c.repo.path===r.path)?.dir??path76.join(root,r.path),inRepo2=parts2.filter(p=>(p.repo??".")===r.path&&(!wanted||wanted.has(p.id)));inRepo2.length||out22.push({dir:top,place:r.path});for(let p of inRepo2)out22.push({dir:path76.join(top,p.folder??""),place:path76.join(r.path,p.folder??""),part:p.name})}let seen=new Set;return out22.filter(p=>seen.has(p.dir)?!1:(seen.add(p.dir),!0))}function projectKinds(root,meta,story2){return placesOf(root,meta,story2).map(p=>({place:p.place,...p.part?{part:p.part}:{},...kindsOf(p.dir)})).filter(p=>p.kinds.length)}var PRECACHE_MS=20*6e4;function runsFlutter(command){return/(?:^|[\s;&|(])flutter(?:\s|$)/.test(command)}function precacheArgs(platforms,platform=process.platform,command){let android2=platforms.includes("android")&&usesAndroid(command),want=[...platforms.includes("ios")&&platform==="darwin"?["--ios"]:[],...platforms.includes("web")?["--web"]:[],...android2?["--android"]:[]];return want.length?["precache",...want]:null}function engineOf(root){try{return fs83.readFileSync(path77.join(root,"bin","internal","engine.version"),"utf8").trim()}catch{return""}}var realRunner=(program2,args,o)=>new Promise(resolve2=>{let output="",child=track(spawn14(program2,args,{cwd:o.cwd,env:o.env,detached:OWN_GROUP,stdio:["ignore","pipe","pipe"]})),keep=chunk=>{output=(output+chunk.toString("utf8")).slice(-4e3),o.onOutput?.()};child.stdout.on("data",keep),child.stderr.on("data",keep);let end=()=>endGroup(child),timer=setTimeout(end,PRECACHE_MS);o.signal?.addEventListener("abort",end,{once:!0}),child.on("error",err2=>output+=`
|
|
466
466
|
${err2.message}`),whenExited(child).then(code=>{clearTimeout(timer),o.signal?.removeEventListener("abort",end),resolve2({ok:code===0,output})})}),runner=realRunner;var done=new Set,running2=new Map;async function flutterDownloads(dir,command,o={}){if(!runsFlutter(command))return"not-needed";let{kinds,platforms}=kindsOf(dir),args=kinds.includes("flutter")?precacheArgs(platforms,o.platform,command):null,env2=o.env??projectEnv(),flutter2=args?flutterOnPath(env2):null;if(!args||!flutter2)return"not-needed";let key=[flutter2.root,engineOf(flutter2.root),...args].join(`
|
|
467
467
|
`);if(done.has(key))return"had";let run7=running2.get(key);return run7||(run7=runner(flutter2.program,args,{cwd:o.home??os13.homedir(),env:{...env2,FLUTTER_SUPPRESS_ANALYTICS:"true"},...o.signal?{signal:o.signal}:{},...o.onOutput?{onOutput:o.onOutput}:{}}).then(r=>(r.ok?done.add(key):writeLog({level:"warn",text:`squad-kit could not get Flutter's downloads (flutter ${args.join(" ")}), so the command runs without them:
|
|
468
|
-
${r.output.trim()}`}),r.ok)).catch(()=>!1).finally(()=>running2.delete(key)),running2.set(key,run7)),await run7?"done":"failed"}import fs84 from"fs";import path78 from"path";var SKIP_DIRS3=new Set(["node_modules",".git",".hg",".svn",".squad","dist","build","out",".next",".nuxt",".svelte-kit",".output",".turbo",".parcel-cache",".cache","coverage",".venv","venv","__pycache__",".tox",".mypy_cache",".pytest_cache","target","vendor","Pods",".gradle",".idea",".vscode"]),CODE2=/\.(m?[jt]sx?|c[jt]s|py|rb|go|rs|php|sh|bash|zsh)$/i,SHELLISH=/(^|[\\/])(package\.json|Procfile|Makefile|[^\\/]+\.(sh|bash|zsh))$/i,LIMITS3={depth:6,files:3e3,fileBytes:256*1024,totalBytes:12*1024*1024},
|
|
468
|
+
${r.output.trim()}`}),r.ok)).catch(()=>!1).finally(()=>running2.delete(key)),running2.set(key,run7)),await run7?"done":"failed"}import fs84 from"fs";import path78 from"path";var SKIP_DIRS3=new Set(["node_modules",".git",".hg",".svn",".squad","dist","build","out",".next",".nuxt",".svelte-kit",".output",".turbo",".parcel-cache",".cache","coverage",".venv","venv","__pycache__",".tox",".mypy_cache",".pytest_cache","target","vendor","Pods",".gradle",".idea",".vscode"]),CODE2=/\.(m?[jt]sx?|c[jt]s|py|rb|go|rs|php|sh|bash|zsh)$/i,SHELLISH=/(^|[\\/])(package\.json|Procfile|Makefile|[^\\/]+\.(sh|bash|zsh))$/i,LIMITS3={depth:6,files:3e3,fileBytes:256*1024,totalBytes:12*1024*1024},NAME2="([A-Z][A-Z0-9_]*PORT)",readsOf=name=>[new RegExp(`process\\.env\\.${name}\\b`,"g"),new RegExp(`process\\.env\\[\\s*['"\`]${name}['"\`]\\s*\\]`,"g"),new RegExp(`(?:os\\.environ\\.get|os\\.getenv|environ\\.get|getenv|os\\.Getenv|os\\.LookupEnv|Deno\\.env\\.get|ENV\\.fetch|env::var)\\(\\s*['"]${name}['"]`,"g"),new RegExp(`(?:os\\.environ|ENV|\\$_ENV|\\$_SERVER)\\[\\s*['"]${name}['"]\\s*\\]`,"g")],shellReadsOf=name=>[new RegExp(`\\$\\{?${name}\\b`,"g"),new RegExp(`%${name}%`,"g")],READS=readsOf(NAME2),SHELL_READS=shellReadsOf(NAME2),PORT_READS=readsOf("PORT"),PORT_SHELL_READS=shellReadsOf("PORT"),DEFAULT=/^['"`\]\s)]*(?:,|\|\||\?\?|:-|:=|\bor\b|\{)\s*['"]?(\d{2,5})\b/,OTHER=/(^|_)(DB|DATABASE|POSTGRES|POSTGRESQL|PG|MYSQL|MARIADB|MSSQL|MONGO|MONGODB|REDIS|MEMCACHED?|CACHE|SMTP|MAIL|EMAIL|IMAP|POP3?|AMQP|RABBIT|RABBITMQ|KAFKA|NATS|MQTT|QUEUE|ELASTIC|ELASTICSEARCH|OPENSEARCH|SOLR|CASSANDRA|NEO4J|CLICKHOUSE|INFLUX|INFLUXDB|LDAP|SSH|SFTP|FTP|GRPC|DEBUG|INSPECT|INSPECTOR|HMR|LIVERELOAD|WS|WEBSOCKET|METRICS|PROMETHEUS|OTEL|STATSD|JAEGER|ZIPKIN|MINIO|S3|VAULT|CONSUL|ETCD)(_|$)/;function findPortVariables(root){return readPorts(root).vars}function portSettings(root,usualPort){let{vars,plain}=readPorts(root),own2=chooseVariables(vars,usualPort);return{vars:own2,reads:plain||own2.length>0}}function readPorts(root){let found2=new Map,plain=!1,files=0,bytes=0,visit=(dir,depth)=>{if(depth>LIMITS3.depth||files>=LIMITS3.files||bytes>=LIMITS3.totalBytes)return;let entries;try{entries=fs84.readdirSync(dir,{withFileTypes:!0})}catch{return}if(!entries.some(e=>e.name==="pyvenv.cfg"))for(let e of entries){if(files>=LIMITS3.files||bytes>=LIMITS3.totalBytes)return;let p=path78.join(dir,e.name);if(e.isDirectory()){SKIP_DIRS3.has(e.name)||visit(p,depth+1);continue}if(!e.isFile())continue;let shell2=SHELLISH.test(p);if(!shell2&&!CODE2.test(e.name))continue;let size2;try{size2=fs84.statSync(p).size}catch{continue}if(size2>LIMITS3.fileBytes)continue;files+=1,bytes+=size2;let text4;try{text4=fs84.readFileSync(p,"utf8")}catch{continue}if(!text4.includes("PORT"))continue;let scripted=shell2||/\.(sh|bash|zsh)$/i.test(e.name),patterns=scripted?[...READS,...SHELL_READS]:READS;plain||=(scripted?[...PORT_READS,...PORT_SHELL_READS]:PORT_READS).some(re=>text4.search(re)>=0);for(let re of patterns){re.lastIndex=0;for(let m of text4.matchAll(re)){let name=m[1];if(name==="PORT"||OTHER.test(name))continue;let seen=found2.get(name)??new Set,d=DEFAULT.exec(text4.slice(m.index+m[0].length,m.index+m[0].length+40));d&&seen.add(Number(d[1])),found2.set(name,seen)}}}};return visit(root,0),{vars:[...found2].map(([name,d])=>({name,defaults:[...d]})).sort((a,b)=>a.name.localeCompare(b.name)),plain}}function appPortVariables(root,usualPort){return chooseVariables(findPortVariables(root),usualPort)}function chooseVariables(vars,usualPort){let usual=usualPort?vars.filter(v=>v.defaults.includes(usualPort)):[];return usual.length?usual.map(v=>v.name):vars.filter(v=>!v.defaults.length).map(v=>v.name)}import fs86 from"fs";import path80 from"path";import fs85 from"fs";import path79 from"path";import yaml12 from"js-yaml";var COMPOSE_FILE=/^(?:docker-)?compose(?:[.-][\w.-]+)?\.ya?ml$/i,OWN_FILES=["compose.yaml","compose.yml","docker-compose.yaml","docker-compose.yml"],OWN_OVERRIDES2=["compose.override.yaml","compose.override.yml","docker-compose.override.yaml","docker-compose.override.yml"],NOT_HERE=/(?:^|[.\-_])(?:prod|production|beta|staging|stage|uat|preprod|live|release|example|sample|template|dist)(?:[.\-_]|$)/i,SKIP_DIRS4=new Set(["node_modules","bin","obj","dist","build","vendor","target","coverage"]),FILE_BYTES=256*1024;function read5(file3){try{return fs85.statSync(file3).size>FILE_BYTES?"":fs85.readFileSync(file3,"utf8")}catch{return""}}function inRepo(p){let clean4=path79.posix.normalize(p.trim().replace(/\\/g,"/")).replace(/\/$/,"");return clean4==="."||clean4===".."||clean4.startsWith("../")||path79.posix.isAbsolute(clean4)?"":clean4}function composeCommand(command){return/^\s*(?:[A-Za-z_]\w*=\S*\s+)*(?:docker\s+compose|docker-compose)(?:\s|$)/.test(command??"")}function composeFiles(dir){let out22=[],look=rel=>{let list8;try{list8=fs85.readdirSync(path79.join(dir,rel),{withFileTypes:!0})}catch{return}for(let e of list8)e.isFile()&&COMPOSE_FILE.test(e.name)&&!NOT_HERE.test(e.name.replace(/\.ya?ml$/i,""))?out22.push(rel?`${rel}/${e.name}`:e.name):!rel&&e.isDirectory()&&!e.name.startsWith(".")&&!SKIP_DIRS4.has(e.name)&&look(e.name)};look("");let dots=f=>path79.posix.basename(f).split(".").length;return out22.sort((a,b)=>a.split("/").length-b.split("/").length||dots(a)-dots(b)||a.localeCompare(b))}function servicesIn(dir,files){let services=[],project;for(let file3 of files){let doc;try{doc=yaml12.load(read5(path79.join(dir,file3)))}catch{continue}if(!doc||typeof doc!="object")continue;typeof doc.name=="string"&&doc.name.trim()&&(project??=doc.name.trim());let from=path79.posix.dirname(file3);for(let[name,raw]of Object.entries(doc.services&&typeof doc.services=="object"?doc.services:{})){let s=raw??{},build=typeof s.build=="string"?s.build:s.build&&typeof s.build=="object"?String(s.build.context??"."):void 0,envFiles=(Array.isArray(s.env_file)?s.env_file:s.env_file?[s.env_file]:[]).map(f=>f&&typeof f=="object"?f.path:f).filter(f=>typeof f=="string").map(f=>inRepo(path79.posix.join(from,f))).filter(f=>f&&!NOT_HERE.test(path79.posix.basename(f))),container=typeof s.container_name=="string"&&s.container_name.trim()?s.container_name.trim():void 0;services.push({name,file:file3,...build!==void 0?{build:inRepo(path79.posix.join(from,build))}:{},...container?{container}:{},envFiles})}}return{services,...project?{project}:{}}}function envFileFlags(text4){let out22=[];for(let line3 of text4.split(`
|
|
469
469
|
`))if(/(?:docker\s+compose|docker-compose)(?:\s|$)/.test(line3))for(let m of line3.matchAll(/\s--env-file(?:=|\s+)(['"]?)([^\s'"`]+)\1/g)){let f=inRepo(m[2]);f&&!NOT_HERE.test(path79.posix.basename(f))&&!out22.includes(f)&&out22.push(f)}return out22}var escape=s=>s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");function envFileToLearn(dir,commands){let start=commands.start?.trim();if(!start||composeCommand(start)||envFilesOf(commands).length)return null;let apps2=servicesIn(dir,composeFiles(dir)).services.filter(s=>s.build!==void 0&&s.envFiles.length),named=s=>!!(s.build&&start.includes(s.build))||new RegExp(`(?:^|[^\\w-])${escape(s.name)}(?:[^\\w-]|$)`,"i").test(start),app=apps2.find(named)??apps2[0];if(app)return{envFile:app.envFiles.join(", "),from:`${app.file} gives the ${app.name} service its settings from it`};let readme=["README.md","readme.md","Readme.md","README"].find(f=>fs85.existsSync(path79.join(dir,f))),places=[...readme?[[readme,read5(path79.join(dir,readme))]]:[],["Makefile",read5(path79.join(dir,"Makefile"))],["its commands",Object.values(commands).join(`
|
|
470
470
|
`)]];for(let[where2,text4]of places){let flags=envFileFlags(text4);if(flags.length)return{envFile:flags.join(", "),from:`a compose command in ${where2} reads it with --env-file`}}return null}function composeStartRisk(dir,command){if(!command||!composeCommand(command))return null;let words2=command.trim().split(/\s+/),named=[],project;for(let i=0;i<words2.length&&words2[i]!=="up";i+=1){let w=words2[i];w.startsWith("COMPOSE_PROJECT_NAME=")?project=w.slice(21):w==="-p"||w==="--project-name"?project=words2[i+1]:w.startsWith("--project-name=")?project=w.slice(15):w==="-f"||w==="--file"?named.push(words2[i+=1]??""):w.startsWith("--file=")&&named.push(w.slice(7))}let here=f=>fs85.existsSync(path79.join(dir,f)),files=named.length?named.map(inRepo).filter(Boolean):[...OWN_FILES.filter(here).slice(0,1),...OWN_OVERRIDES2.filter(here)],found2=servicesIn(dir,files),fixed=found2.services.flatMap(s=>s.container?[s.container]:[]);if(fixed.length)return`fixed container names (${fixed.slice(0,3).join(", ")}${fixed.length>3?", \u2026":""})`;let name=project?.replace(/^['"]|['"]$/g,"")||found2.project;return name?`the project name ${name}`:null}function programOf(command){return command.trim().split(/\s+/).find(w=>!/^[A-Za-z_]\w*=/.test(w))??""}function onThisComputer(program2){return!!(program2&&onPath(realHost(),program2))}function learnFromCompose(dir,commands,found2,here=onThisComputer){let changed=[],risk=composeStartRisk(dir,commands.start);if(risk){let was=commands.start;delete commands.start,delete commands.url;let host3=found2.start&&!composeCommand(found2.start)?found2.start:void 0,program2=host3?programOf(host3):"",why=`The read's start command (${was}) runs Docker Compose with ${risk}, which your own Docker stack uses too, so squad-kit never saves it as the start`;host3&&here(program2)?(commands.start=host3,found2.url&&(commands.url=found2.url),changed.push(`${why}: it starts the app on this computer instead, with ${host3}.`)):changed.push(`${why}. ${host3?`Its start for this computer needs ${program2}, which is not on this computer: ${host3}`:"It found no start for this computer"}. An engineer can set one in Settings.`)}let learned=envFileToLearn(dir,commands);return learned&&(commands.envFile=learned.envFile,changed.push(`The app starts on this computer, and ${learned.from}, so squad-kit gives the app the settings in ${learned.envFile}, with its own database, Redis and port on top.`)),changed}function read6(file3){try{return fs86.readFileSync(file3,"utf8")}catch{return""}}var has2=(dir,name)=>fs86.existsSync(path80.join(dir,name)),at=port=>port?`http://localhost:${port}`:void 0,USUAL_PORT=[[/^next$/,3e3],[/^react-scripts$/,3e3],[/^nuxt$/,3e3],[/^@remix-run\//,3e3],[/^@angular\/cli$/,4200],[/^astro$/,4321],[/^gatsby$/,8e3],[/^@sveltejs\/kit$/,5173],[/^vite$/,5173],[/^express$|^fastify$|^koa$|^hono$/,3e3]];function portInScript(script){let m=/(?:--port[= ]|\s-p[= ]?|\bPORT=)(\d{2,5})\b/.exec(` ${script}`);return m?Number(m[1]):null}function portInFiles(dir){let own2=findPortVariables(dir).flatMap(v=>v.defaults)[0];if(own2)return own2;let words2=/(?:process\.env\.PORT\s*(?:\|\||\?\?)\s*['"]?|\.listen\(\s*)(\d{2,5})\b/;for(let f of["server.js","server.ts","server.mjs","app.js","app.ts","index.js","index.ts","main.js","main.ts","src/server.js","src/server.ts","src/app.js","src/app.ts","src/index.js","src/index.ts","src/main.js","src/main.ts"]){let m=words2.exec(read6(path80.join(dir,f)));if(m)return Number(m[1])}let said2=/https?:\/\/(?:localhost|127\.0\.0\.1):(\d{2,5})\b/.exec(read6(path80.join(dir,"README.md")));return said2?Number(said2[1]):null}var BROWSER_SCRIPT=/^(?:test[:-])?(?:e2e|browser|playwright|cypress)(?:[:-]test)?$/i,SHOWS_A_WINDOW=/--ui\b|--headed\b|--debug\b|cypress open|\bcodegen\b/,NO_TESTS=/no test specified|no tests? yet|echo\s+['"]?no tests?\b/i,RUNS_BROWSER=/playwright\s+test|cypress\s+run/;function browserScript(scripts){return Object.keys(scripts).find(k=>BROWSER_SCRIPT.test(k)&&!SHOWS_A_WINDOW.test(scripts[k])&&!NO_TESTS.test(scripts[k]))}function scriptsOf(dir){if(!has2(dir,"package.json"))return null;try{let pkg=JSON.parse(read6(path80.join(dir,"package.json")));return Object.fromEntries(Object.entries(pkg.scripts??{}).filter(e=>typeof e[1]=="string"))}catch{return null}}function runnerOf(dir){return has2(dir,"pnpm-lock.yaml")?{install:"pnpm install",run:s=>`pnpm run ${s}`,test:"pnpm test",start:"pnpm start"}:has2(dir,"yarn.lock")?{install:"yarn install",run:s=>`yarn ${s}`,test:"yarn test",start:"yarn start"}:has2(dir,"bun.lockb")||has2(dir,"bun.lock")?{install:"bun install",run:s=>`bun run ${s}`,test:"bun run test",start:"bun run start"}:{install:"npm install",run:s=>`npm run ${s}`,test:"npm test",start:"npm start"}}function nodeTests(dir,scripts){let r=runnerOf(dir),main=scripts.test&&!NO_TESTS.test(scripts.test)?r.test:scripts["test:unit"]?r.run("test:unit"):void 0,browser=browserScript(scripts);return main?withBrowserTests(dir,main):browser?r.run(browser):void 0}var OPENS_A_PHONE=/\breact-native\s+(?:start|run-ios|run-android)\b|\bexpo\s+(?:start\b(?![^&|;]*--web)|run:(?:ios|android)\b)|\bmetro\b/,EXPO_WEB_PORT=8081,EXPO_WEB_START=`npx expo start --web --port \${PORT:-${EXPO_WEB_PORT}}`;function nodeCommands(dir,scripts){let r=runnerOf(dir),lint=["lint","typecheck","type-check","check-types","types"].find(s=>scripts[s]),test=nodeTests(dir,scripts),deps=(()=>{try{let pkg=JSON.parse(read6(path80.join(dir,"package.json")));return[...Object.keys(pkg.dependencies??{}),...Object.keys(pkg.devDependencies??{})]}catch{return[]}})(),phone=deps.includes("react-native")||deps.includes("expo"),startScript=["dev","start","serve","preview"].find(s=>scripts[s]&&!(phone&&OPENS_A_PHONE.test(scripts[s]))),expoWeb=deps.includes("expo")&&deps.includes("react-native-web"),usual=USUAL_PORT.find(([re])=>deps.some(d=>re.test(d)))?.[1],script=startScript?scripts[startScript]:"",port=expoWeb?EXPO_WEB_PORT:startScript?portInScript(script)??portInFiles(dir)??usual??null:null,url=at(port),start=expoWeb?EXPO_WEB_START:startScript?startScript==="start"?r.start:r.run(startScript):void 0;return{...Object.keys(scripts).length||deps.length?{setup:r.install}:{},...test?{test}:{},...lint?{lint:r.run(lint)}:{},...scripts.build?{build:r.run("build")}:{},...start?{start}:{},...url?{url}:{}}}var FLUTTER_COMMANDS={setup:"flutter pub get",lint:"flutter analyze --no-fatal-infos",test:"flutter test"},FLUTTER_WEB_BUILD="flutter build web",FLUTTER_PHONE_BUILD=/(?:^|[\s;&|(])flutter\s+build\s+(?:apk|appbundle|ios|ipa)\b/,FLUTTER_WEB_PORT=8080;function flutterWebStart(port=FLUTTER_WEB_PORT){return`flutter run -d web-server --web-port \${PORT:-${port}}`}function dartCommands(dir){if(!has2(dir,"pubspec.yaml"))return null;let tests=anyFile(path80.join(dir,"test"),f=>f.endsWith("_test.dart")),{kinds,platforms}=kindsOf(dir);if(!kinds.includes("flutter"))return{setup:"dart pub get",lint:"dart analyze",...tests?{test:"dart test"}:{}};let port=platforms.includes("web")?portInFiles(dir)??FLUTTER_WEB_PORT:null;return{setup:FLUTTER_COMMANDS.setup,...tests?{test:FLUTTER_COMMANDS.test}:{},lint:FLUTTER_COMMANDS.lint,...platforms.includes("web")?{build:FLUTTER_WEB_BUILD}:{},...port?{start:flutterWebStart(port),url:at(port)}:{}}}var IPHONE_SLOT="{iphone}";var XCODE_BUILD_DIR="build/squad-kit-ios",word=s=>/^[\w./@+-]+$/.test(s)?s:`'${s.replace(/'/g,"'\\''")}'`,XCODE_NAME=String.raw`(?:[\w@+][\w./@+-]*|'(?!-)[^'\n]*')`,OWN_XCODE=new RegExp(`^xcodebuild (?:build|test) -(?:workspace|project) (${XCODE_NAME}) -scheme ${XCODE_NAME} -destination '(?:generic/platform=iOS Simulator|platform=iOS Simulator,id=(?:\\{iphone\\}|[\\w-]+))' -derivedDataPath ${XCODE_BUILD_DIR}(?: -packageCachePath ${XCODE_BUILD_DIR}/packages)?$`);function ownXcodeCommand(command){let where2=OWN_XCODE.exec(command.trim())?.[1]?.replace(/^'|'$/g,"");return!!(where2&&!where2.startsWith("/")&&!where2.split("/").includes(".."))}var NOT_THE_APP=new Set(["Pods","Carthage","build","DerivedData","node_modules","vendor"]);function xcodeContainer(dir){let list8=d=>{try{return fs86.readdirSync(path80.join(dir,d)).filter(f=>!f.startsWith(".")&&f!=="Pods.xcodeproj")}catch{return[]}},folders=["",...(()=>{try{return fs86.readdirSync(dir,{withFileTypes:!0}).filter(e=>e.isDirectory()&&!e.name.startsWith(".")&&!NOT_THE_APP.has(e.name)&&!/\.(xcodeproj|xcworkspace)$/.test(e.name)).map(e=>e.name)}catch{return[]}})()];for(let ending2 of[".xcworkspace",".xcodeproj"])for(let d of folders){let found2=list8(d).find(f=>f.endsWith(ending2));if(found2)return{kind:ending2===".xcworkspace"?"workspace":"project",rel:path80.join(d,found2)}}return null}function sharedSchemes(dir,rel){let folder=path80.join(dir,path80.dirname(rel)),holders=[path80.join(dir,rel),...(()=>{try{return fs86.readdirSync(folder).filter(f=>f.endsWith(".xcodeproj")&&f!=="Pods.xcodeproj").map(f=>path80.join(folder,f))}catch{return[]}})()],out22=[];for(let h of[...new Set(holders)]){let at3=path80.join(h,"xcshareddata","xcschemes");for(let f of fs86.existsSync(at3)?fs86.readdirSync(at3).filter(x=>x.endsWith(".xcscheme")).sort():[])out22.push({name:f.replace(/\.xcscheme$/,""),tests:/<TestableReference\b/.test(read6(path80.join(at3,f)))})}return out22}function projectFiles(dir,rel){let folder=path80.join(dir,path80.dirname(rel));try{return fs86.readdirSync(folder).filter(f=>f.endsWith(".xcodeproj")&&f!=="Pods.xcodeproj").map(f=>read6(path80.join(folder,f,"project.pbxproj"))).join(`
|
|
471
471
|
`)}catch{return""}}function xcodeCommands(dir){let c=xcodeContainer(dir);if(!c)return null;let app=path80.basename(c.rel).replace(/\.(xcworkspace|xcodeproj)$/,""),schemes=sharedSchemes(dir,c.rel),pbx=projectFiles(dir,c.rel),scheme=schemes.find(s=>s.tests&&s.name===app)??schemes.find(s=>s.tests)??schemes.find(s=>s.name===app)??schemes[0],name=scheme?.name??app,tests=scheme?scheme.tests:/com\.apple\.product-type\.bundle\.(?:unit-test|ui-testing)/.test(pbx),packages2=/XCRemoteSwiftPackageReference/.test(pbx)?` -packageCachePath ${XCODE_BUILD_DIR}/packages`:"",base2=`-${c.kind} ${word(c.rel)} -scheme ${word(name)}`,into=`-derivedDataPath ${XCODE_BUILD_DIR}${packages2}`,pods=path80.dirname(c.rel);return{...has2(path80.join(dir,pods),"Podfile")?{setup:pods==="."?"pod install":`pod install --project-directory=${word(pods)}`}:{},...tests?{test:`xcodebuild test ${base2} -destination 'platform=iOS Simulator,id=${IPHONE_SLOT}' ${into}`}:{},build:`xcodebuild build ${base2} -destination 'generic/platform=iOS Simulator' ${into}`}}function swiftCommands(dir){return has2(dir,"Package.swift")?{...fs86.existsSync(path80.join(dir,"Tests"))?{test:"swift test --disable-sandbox"}:{},build:"swift build --disable-sandbox"}:null}function pythonCommands(dir){let reqs=has2(dir,"requirements.txt"),pyproject=has2(dir,"pyproject.toml"),manage=has2(dir,"manage.py"),entry=["app.py","main.py","server.py","wsgi.py","asgi.py"].find(f=>has2(dir,f));if(!reqs&&!pyproject&&!manage&&!entry)return null;let text4=`${read6(path80.join(dir,"requirements.txt"))}
|
|
@@ -667,7 +667,7 @@ ${text4};`,`Reading ${path93.basename(a.name)}`),making2=startFolder(o.folder);t
|
|
|
667
667
|
`)+`
|
|
668
668
|
`,`Making the template of ${path93.basename(a.name)}`);let notes=[...plan.notes];shadow.size&¬es.push("Search indexes kept by virtual tables are made again from the kept rows.");let info2=finishFolder(making2,o.folder,{kind:"sqlite",source:a.safe,sourceName:path93.basename(a.name),madeAt:new Date().toISOString(),smallRows:o.smallRows,sampleRows:o.sampleRows,hidePersonal:o.hidePersonal,tables:describeTables(plan,counts3),hidden,linksComplete:plan.complete,notes});return{kind:"sqlite",folder:o.folder,source:source2,info:info2}}catch(e){throw dropFolder(making2),e}},async createStoryDb(template,story2){let a=readAddress("sqlite",template.source),file3=storyFile(a.name,story2),address=a.withName(file3);refuseOwn("sqlite",address,template.source,"makes or changes");let real2=realFile(file3);if(fs98.existsSync(file3)){let mark=readMark("sqlite","sqlite",real2);if(mark?.role==="story"&&mark.story===story2.id.trim())return{kind:"sqlite",name:file3,address,existed:!0};throw new DbError(`A file named ${path93.basename(file3)} is already in the story\u2019s folder, and squad-kit did not make it for this story. squad-kit leaves it alone.`,"not-ours")}if(!readTemplate(template.folder,template.source,"sqlite"))throw new DbError("The template\u2019s files are not on this computer any more. Make the template again.","failed");return fs98.mkdirSync(path93.dirname(file3),{recursive:!0}),fs98.copyFileSync(path93.join(template.folder,TEMPLATE_FILE(a.name)),file3,fs98.constants.COPYFILE_EXCL),putMark({kind:"sqlite",server:"sqlite",name:realFile(file3),role:"story",story:story2.id.trim(),sourceName:path93.basename(a.name),madeAt:new Date().toISOString()}),{kind:"sqlite",name:file3,address,existed:!1}},async dropStoryDb(address,personsAddress2){refuseOwn("sqlite",address,personsAddress2,"drops");let a=readAddress("sqlite",address),p=readAddress("sqlite",personsAddress2),real2=realFile(a.name),mark=readMark("sqlite","sqlite",real2);if(!fs98.existsSync(a.name))return mark?.role==="story"&&dropMark("sqlite","sqlite",real2),!1;let ext=path93.extname(p.name);if(mark?.role!=="story"||!mark.story||path93.basename(a.name)!==`${storyName(path93.basename(p.name,ext),mark.story)}${ext||".db"}`)throw new DbError(`squad-kit did not make ${path93.basename(a.name)} for a story of this project, so it leaves it alone.`,"not-ours");for(let f of[a.name,`${a.name}-wal`,`${a.name}-shm`,`${a.name}-journal`])fs98.rmSync(f,{force:!0});return dropMark("sqlite","sqlite",real2),!0},async removeTemplate(template){fs98.rmSync(template.folder,{recursive:!0,force:!0})}};var ADAPTERS={sqlite,postgres,mysql,mongo};function dbAdapter(kind){let a=ADAPTERS[kind];if(!a)throw new Error(`squad-kit has no adapter for ${String(kind)} databases.`);return a}function readTemplate2(folder,source2){return readTemplate(folder,source2)}import{createHash as createHash11}from"crypto";import fs99 from"fs";import path94 from"path";var MIN=1,MAX=15,recordFile=bp=>path94.join(bp.localDir,"database","redis.json");function storyRedisNumbers(bp){try{let list8=JSON.parse(fs99.readFileSync(recordFile(bp),"utf8")).stories;return Array.isArray(list8)?list8:[]}catch{return[]}}function changeRecords(bp,change){let next=change(storyRedisNumbers(bp));fs99.mkdirSync(path94.dirname(recordFile(bp)),{recursive:!0,mode:448});let tmp=`${recordFile(bp)}.${process.pid}.tmp`;fs99.writeFileSync(tmp,JSON.stringify({stories:next},null,2),{mode:384}),fs99.renameSync(tmp,recordFile(bp))}function chooseRedisNumber(story2,taken){let start=parseInt(createHash11("sha1").update(story2).digest("hex").slice(0,8),16)%(MAX-MIN+1);for(let i=0;i<=MAX-MIN;i+=1){let n=MIN+(start+i)%(MAX-MIN+1);if(!taken.has(n))return n}return null}function redisNumberIn(address){let t=address.trim(),link=/^rediss?:\/\/[^/?#]*\/(\d+)/i.exec(t);return Number(link?link[1]:/(?:^|,)\s*defaultdatabase\s*=\s*(\d+)/i.exec(t)?.[1]??0)}function withRedisNumber(address,n){let t=address.trim(),link=/^(rediss?:\/\/[^/?#]*)(\/[^?#]*)?(.*)$/i.exec(t);return link?`${link[1]}/${n}${link[3]}`:[...t.split(",").filter(p=>!/^\s*defaultdatabase\s*=/i.test(p)),`defaultDatabase=${n}`].join(",")}var HOST_ONLY=/(?:^|_)(?:HOST|HOSTNAME|SERVER|ADDR)$/i,NUMBER_NAME=/^(?:[A-Za-z0-9]+_+)*redis_+(?:db|database|default_?database|db_?index|index)$/i;function fileOf2(dir,s){let form=s.form==="env"?"env":s.form==="json"?"json":s.form==="properties"?"properties":null;return!form||!git(dir,["check-ignore","--",s.file],{allowFail:!0})?null:{path:s.file,key:s.key,form,value:""}}var said=new Set;function logOnce(story2,text4,level="info"){said.has(`${story2}
|
|
669
669
|
${text4}`)||(said.add(`${story2}
|
|
670
|
-
${text4}`),writeLog({level,ticket:story2,text:text4}))}function setUpStoryRedis(bp,story2,dir,repo){let all=settingsIn2(dir),redis=appSettings(dir,all).filter(s=>s.kind==="redis");if(!redis.length)return unwireCopyRedis(dir),null;let services=composeServices(dir),places=redis.map(s=>({s,place:serverPlace(s.value,services,{redis:!0})})),away=places.find(p=>!p.place.here);away&&!away.place.here&&logOnce(story2,`The app's Redis (${away.s.name}) is on another computer (${away.place.host}), so squad-kit leaves it as it is: ${story2} shares it.`);let local=places.flatMap(p=>p.place.here?[{s:p.s,address:p.place.address}]:[]),server=local.length?serverOf2(local[0].address,!0):null;if(!server)return unwireCopyRedis(dir),null;let records=storyRedisNumbers(bp),mine2=records.find(r=>r.story===story2&&r.server===server),onServer2=local.filter(l=>serverOf2(l.address,!0)===server),taken=new Set([0,...onServer2.map(l=>redisNumberIn(l.s.value)),...records.filter(r=>r.story!==story2&&r.server===server).map(r=>r.number)]),number=mine2?.number??chooseRedisNumber(story2,taken);if(!number)return logOnce(story2,`Every Redis database number from ${MIN} to ${MAX} on ${server} is taken by other stories, so ${story2} shares the app's own.`,"warn"),unwireCopyRedis(dir),null;let w={env:{},files:[]},put=(s,envValue2,fileValue)=>{w.env[s.name]=envValue2;let f=fileOf2(dir,s);f&&w.files.push({...f,value:fileValue})};for(let{s,address}of onServer2)HOST_ONLY.test(s.name)||put(s,withRedisNumber(address,number),withRedisNumber(s.value,number));for(let s of all)NUMBER_NAME.test(s.name)&&/^\d+$/.test(s.value)&&put(s,String(number),String(number));if(!Object.keys(w.env).length)return logOnce(story2,`The app's Redis settings have no place for a database number, so ${story2} shares the app's own Redis database.`),unwireCopyRedis(dir),null;wireCopyRedis(dir,w);let record={story:story2,repo,dir:path94.resolve(dir),number,server,at:mine2?.at??now2()};return changeRecords(bp,list8=>[...list8.filter(r=>!(r.story===story2&&r.repo===repo)),record]),mine2||logOnce(story2,`${story2}'s app has its own Redis database number, ${number}, on ${server} (${safeAddress(Object.values(w.env)[0])})`),record}function rewireStoryRedis(bp,story2,dir,repo){return storyRedisNumbers(bp).find(r=>r.story===story2&&r.repo===repo&&path94.resolve(r.dir)===path94.resolve(dir))?!!setUpStoryRedis(bp,story2,dir,repo):!1}function signIn(address){let t=address.trim(),link=/^(rediss?):\/\/(?:([^:@/]*)(?::([^@/]*))?@)?/i.exec(t),dec2=s=>{try{return s?decodeURIComponent(s):void 0}catch{return s}};if(link)return{...link[2]?{user:dec2(link[2])}:{},...link[3]?{password:dec2(link[3])}:{},tls:link[1].toLowerCase()==="rediss"};let opt=name=>new RegExp(`(?:^|,)\\s*${name}\\s*=([^,]*)`,"i").exec(t)?.[1]?.trim(),user=opt("user"),password5=opt("password");return{...user?{user}:{},...password5?{password:password5}:{},tls:/^true$/i.test(opt("ssl")??"")}}var FLUSH_MS=3e4,realCli=(tool5,args,env2,secrets)=>runTool(tool5,args,{env:env2,secrets,doing:"Clearing a story's Redis database",timeoutMs:FLUSH_MS});async function dropStoryRedis(bp,story2,cli=realCli){let mine2=storyRedisNumbers(bp).filter(r=>r.story===story2);if(!mine2.length)return!1;for(let r of mine2)unwireCopyRedis(r.dir);changeRecords(bp,list8=>list8.filter(r=>r.story!==story2));let left2=storyRedisNumbers(bp),cleared=!1;for(let r of mine2.filter((x,i)=>mine2.findIndex(y=>y.server===x.server&&y.number===x.number)===i)){if(!Number.isInteger(r.number)||r.number<MIN||r.number>MAX||left2.some(o=>o.server===r.server&&o.number===r.number))continue;let tool5=findTool("redis-cli");if(!tool5){writeLog({level:"info",ticket:story2,text:`redis-cli is not on this computer, so squad-kit did not clear Redis database ${r.number} on ${r.server}, ${story2}'s own. Its keys stay until Redis drops them.`});continue}let known=[...appSettings(path94.join(bp.root,r.repo)),...fs99.existsSync(r.dir)?appSettings(r.dir):[]].filter(s=>s.kind==="redis"),services=composeServices(path94.join(bp.root,r.repo)),same5=known.map(s=>serverPlace(s.value,services,{redis:!0})).find(p=>p.here&&serverOf2(p.address,!0)===r.server),how2=same5?.here?signIn(same5.address):{tls:!1},[,host3="localhost",port="6379"]=/^\[?([^\]]*?)\]?:(\d+)$/.exec(r.server)??[],args=["-h",host3,"-p",port,"-n",String(r.number),...how2.tls?["--tls"]:[],...how2.user?["--user",how2.user]:[],"FLUSHDB"];try{await cli(tool5,args,how2.password?{REDISCLI_AUTH:how2.password}:{},secretsOf(how2.password)),writeLog({level:"info",ticket:story2,text:`Cleared Redis database ${r.number} on ${r.server}, ${story2}'s own`}),cleared=!0}catch(err2){writeLog({level:"warn",ticket:story2,text:`squad-kit could not clear Redis database ${r.number} on ${r.server}, ${story2}'s own: ${err2.message}`})}}return cleared}function redisView(bp,repos){for(let repo of repos){let dir=path94.join(bp.root,repo),s=appSettings(dir).find(x=>x.kind==="redis");if(!s)continue;let place=serverPlace(s.value,composeServices(dir),{redis:!0});if(!place.here)return{server:place.host,here:!1,own:redisNumberIn(s.value),stories:[],words:`The app's Redis is on another computer (${place.host}). squad-kit leaves it as it is, so stories share it with your own app.`};let server=serverOf2(place.address,!0)??"localhost:6379",own2=redisNumberIn(s.value),stories=storyRedisNumbers(bp).filter(r=>r.server===server).map(r=>({story:r.story,number:r.number})),unique=stories.filter((x,i)=>stories.findIndex(y=>y.story===x.story)===i);return{server,here:!0,own:own2,stories:unique,words:`Each story's app gets its own Redis database number on ${server}, from ${MIN} to ${MAX}, so cache keys and locks never mix. Your own app keeps ${own2}. A story's keys are cleared when its copy goes.`}}return null}var OWN_DB_WORDS="The story has its own copy of the database.",DB_KINDS=["postgres","mysql","mongo","sqlite"],KIND_NAME={postgres:"Postgres",mysql:"MySQL",mongo:"Mongo",sqlite:"SQLite"},dbDir=bp=>path95.join(bp.localDir,"database"),templateFolder=bp=>path95.join(dbDir(bp),"template"),stateFile2=bp=>path95.join(dbDir(bp),"state.json");function readState(bp){try{let s=JSON.parse(fs100.readFileSync(stateFile2(bp),"utf8"));return{...s,stories:Array.isArray(s.stories)?s.stories:[],notes:Array.isArray(s.notes)?s.notes:[]}}catch{return{stories:[],notes:[]}}}function changeState(bp,change){let s=readState(bp);change(s),fs100.mkdirSync(dbDir(bp),{recursive:!0,mode:448});let tmp=`${stateFile2(bp)}.${process.pid}.tmp`;return fs100.writeFileSync(tmp,JSON.stringify(s,null,2),{mode:384}),fs100.renameSync(tmp,stateFile2(bp)),s}function dbSecrets(bp){try{return loadSecrets(bp.secretsFile).database??{}}catch{return{}}}function changeSecrets(bp,change){let all=loadSecrets(bp.secretsFile),d={...all.database??{},stories:{...all.database?.stories??{}}};change(d),d.address||delete d.address,Object.keys(d.stories).length||delete d.stories;let{database:_old,...rest}=all;saveSecrets(bp.secretsFile,Object.keys(d).length?{...rest,database:d}:rest)}function keptAddress(bp,db){return dbSecrets(bp).address??db?.address}function personsAddress(bp,db=readMeta(bp).database){let a=keptAddress(bp,db);if(!a||db?.kind!=="sqlite")return a;let file3=readAddress2(a).name;if(!file3||path95.isAbsolute(file3))return a;let prisma=db.from?.find(f=>f.endsWith(".prisma"));return sqliteAt(a,file3,path95.join(bp.root,db.repo,prisma?path95.posix.dirname(prisma):".",file3))}function scrub2(bp,text4){let s=dbSecrets(bp),out22=text4;for(let a of[s.address,...Object.values(s.stories??{})]){if(!a)continue;let pass=/^(?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:([^@/]+)@/i.exec(a)?.[1]??/(?:^|;)\s*(?:password|pwd)\s*=([^;]+)/i.exec(a)?.[1];pass&&(out22=out22.split(pass).join("***").split(decodeURIComponent(pass)).join("***"))}return out22}function noteForEngineers(bp,text4,story2){let clean4=scrub2(bp,text4);if(readState(bp).notes.some(n=>n.story===story2&&n.text===clean4))return;changeState(bp,s=>s.notes=[...s.notes,{at:now2(),...story2?{story:story2}:{},text:clean4}].slice(-30));let project;try{project=readMeta(bp).project}catch{}writeLog({level:"warn",...project?{project}:{},...story2?{ticket:story2}:{},text:clean4})}function placeOf(bp,db,address){return db.kind==="sqlite"?{here:!0,address}:serverPlace(address,composeServices(path95.join(bp.root,db.repo)),{kind:db.kind})}function awayWords(place,who4="story"){let then=who4==="story"?"so this story uses no copy of its own":who4==="stories"?"so stories use it as before":"so it makes no template of it";return place.service?`This project's database runs in Docker as the ${place.host} service, which does not publish its port on this computer. squad-kit makes a story's own database only where it reaches it on this computer, ${then}. An engineer can publish the port in the compose file, or point the project at a local database in Settings.`:`This project's database is on another computer (${place.host}). squad-kit makes a story's own database only on this computer, ${then}. An engineer can point the project at a local database in Settings.`}var sameSetting=(a,b)=>a.env===b.env&&a.file===b.file&&a.key===b.key,sameDatabase=(a,b)=>!!a&&a.kind===b.kind&&a.repo===b.repo&&a.name===b.name&&sameSetting(a.setting,b.setting);function learnDatabase(bp,opts={}){let meta=readMeta(bp),before=meta.database;if(before?.confirmed&&!opts.again)return{database:before,changed:!1};let look=findDatabase(bp.root,reposOf(meta).map(r=>r.path));if(!look.found)return look.later&¬eForEngineers(bp,look.later),{...before?{database:before}:{},changed:!1,...look.later?{later:look.later}:{}};let f=look.found,secret=f.address&&hasPassword(f.address)?f.address:void 0,next={kind:f.kind,repo:f.repo,...f.name?{name:f.name}:{},...f.where?{where:f.where}:{},setting:f.setting,...f.address&&!secret?{address:f.address}:{},...f.from.length?{from:f.from}:{},...before?.hidePersonal?{hidePersonal:!0}:{},...before?.sampleRows?{sampleRows:before.sampleRows}:{}};return before?.confirmed&&sameDatabase(before,next)&&(next.confirmed=before.confirmed),writeMeta(bp,{...meta,database:next}),f.address&&changeSecrets(bp,d=>d.address=secret),{database:next,changed:!sameDatabase(before,next)}}function learnAfterRead(bp){try{learnDatabase(bp)}catch(err2){writeLog({level:"warn",text:scrub2(bp,`Learning the project's database did not work: ${err2.message}`)})}}var ENV_NAME=/^[A-Za-z_][A-Za-z0-9_]*$/;function confirmDatabase(bp,by2,change={}){let meta=readMeta(bp),repos=reposOf(meta).map(r=>r.path),kind=change.kind?.trim().toLowerCase();if(kind&&!DB_KINDS.includes(kind))throw new Error(`"${change.kind}" is not a kind squad-kit can copy. Use postgres, mysql, mongo or sqlite.`);let before=meta.database;if(!before&&!kind)throw new Error("squad-kit has not found the project's database. Name it: squad db confirm --kind postgres --env DATABASE_URL --address postgres://localhost:5432/app");let repo=change.repo?.trim()||before?.repo||(repos.length===1?repos[0]:"");if(!repos.includes(repo))throw new Error(repo?`There is no code repo at ${repo}. The repos: ${repos.join(", ")}.`:`Say which repo reads the database: ${repos.join(", ")}.`);let setting={...before?.setting??{}};if(change.env!==void 0){let env2=change.env.trim();if(env2&&!ENV_NAME.test(env2))throw new Error(`"${env2}" is not a setting name. Write it as the app reads it, like DATABASE_URL.`);env2?setting.env=env2:delete setting.env}if(change.file!==void 0){let file3=change.file.trim().replace(/\\/g,"/");if(file3&&(file3.startsWith("/")||/^[a-z]:/i.test(file3)||file3.split("/").includes("..")))throw new Error("The settings file must be inside the repo, like .env or appsettings.Development.json.");file3?setting.file=file3:delete setting.file}if(change.key!==void 0){let key=change.key.trim();key?setting.key=key:delete setting.key}if(!setting.env&&!(setting.file&&setting.key))throw new Error("Name the setting that points the app at the database: --env DATABASE_URL, or --file appsettings.Development.json --key ConnectionStrings:Default.");let address=change.address?.trim(),finalKind=kind??before.kind;if(address){let said2=kindOf(address);if(said2&&said2!==finalKind)throw new Error(`That address is for ${said2==="sqlserver"?"SQL Server":KIND_NAME[said2]}, not ${KIND_NAME[finalKind]}.`)}let name=change.name?.trim()||(address?readAddress2(address).name:void 0)||(kind&&kind!==before?.kind?void 0:before?.name),shared=address!==void 0?address&&!hasPassword(address)?address:void 0:before?.address,next={kind:finalKind,repo,...name?{name}:{},...before?.where&&finalKind===before.kind?{where:before.where}:{},setting,...shared?{address:shared}:{},...before?.from?{from:before.from}:{},confirmed:{by:by2,at:now2()},...before?.hidePersonal?{hidePersonal:!0}:{},...before?.sampleRows?{sampleRows:before.sampleRows}:{}};return writeMeta(bp,{...meta,database:next}),address!==void 0&&changeSecrets(bp,d=>d.address=address&&hasPassword(address)?address:void 0),next}function setDatabaseOptions(bp,o){let meta=readMeta(bp),db=meta.database;if(!db)throw new Error("squad-kit has not found the project's database yet. Find it first: squad db find");let next={...db};if(o.hidePersonal===!0?next.hidePersonal=!0:o.hidePersonal===!1&&delete next.hidePersonal,o.sampleRows===null)delete next.sampleRows;else if(o.sampleRows!==void 0){let n=Math.round(Number(o.sampleRows));if(!Number.isFinite(n)||n<10||n>1e6)throw new Error("The sample size is a number of rows from 10 to 1,000,000, like 1000.");next.sampleRows=n}return writeMeta(bp,{...meta,database:next}),next}function templateInfo(bp){try{return JSON.parse(fs100.readFileSync(path95.join(templateFolder(bp),"template.json"),"utf8"))}catch{return null}}function currentTemplate(bp,source2){try{return readTemplate2(templateFolder(bp),source2)}catch{return null}}var making=new Map;function makeDbTemplate(bp,by2){let run7=making.get(bp.squadDir);if(run7)return run7;let next=makeOnce(bp,by2).finally(()=>making.delete(bp.squadDir));return making.set(bp.squadDir,next),next}function templateReady(bp){let db=readMeta(bp).database;if(!db)throw new Error("squad-kit has not found the project's database yet. Find it first: squad db find");if(!db.confirmed)throw new Error("An engineer confirms the database first: squad db confirm");let source2=personsAddress(bp,db);if(!source2)throw new Error("squad-kit needs the database's address to make the template. Give it with: squad db confirm --address \u2026");let place=placeOf(bp,db,source2);if(!place.here)throw new Error(awayWords(place,"template"));let ignored=storyCopyIgnored(path95.join(bp.root,db.repo),db,source2);if(ignored)throw new Error(ignored);let adapter=dbAdapter(db.kind),missing3=adapter.missingTools();if(missing3.length)throw new Error(`Making a template of a ${KIND_NAME[db.kind]} database needs ${missing3.join(" and ")} on this computer.`);return{db,source:place.address,adapter}}async function makeOnce(bp,by2){let{db,source:source2,adapter}=templateReady(bp);changeState(bp,s=>s.making={since:now2(),by:by2});try{let t=await adapter.makeTemplate(source2,{folder:templateFolder(bp),hidePersonal:!!db.hidePersonal,...db.sampleRows?{sampleRows:db.sampleRows}:{}});return changeState(bp,s=>delete s.failed),writeLog({level:"info",project:readMeta(bp).project,text:`Made the template of the ${KIND_NAME[db.kind]} database ${t.info.sourceName}: ${t.info.tables.length} tables, ${t.info.bytes} bytes`}),t.info}catch(err2){let text4=scrub2(bp,err2.message);throw changeState(bp,s=>s.failed={at:now2(),text:text4}),new Error(text4)}finally{changeState(bp,s=>delete s.making)}}function makeTemplateBeside(bp,by2){templateReady(bp),makeDbTemplate(bp,by2).catch(err2=>noteForEngineers(bp,`The template of the database was not made: ${err2.message}`))}function formOf(file3){let base2=path95.posix.basename(file3);return/^\.env(?:\.|$)/.test(base2)?"env":base2.endsWith(".properties")?"properties":base2.endsWith(".json")?"json":null}function copyFile(dir,setting){if(!setting.file)return;let form=formOf(setting.file),key=form==="env"?setting.key??setting.env:setting.key;if(!(!form||!key)&&git(dir,["check-ignore","--",setting.file],{allowFail:!0}))return{path:setting.file,key,form}}function wire(dir,db,address,written=address){let file3=copyFile(dir,db.setting);wireCopy(dir,{env:db.setting.env?{[db.setting.env]:address}:{},value:written,...file3?{file:file3}:{}})}function asWritten(adapter,source2,here,address,story2){if(source2===here)return address;try{return adapter.storyAddress(source2,{id:story2})}catch{return address}}var SQLITE_BESIDE=["","-wal","-shm","-journal"];function keepOutOfCommits(dir,db,address){if(db.kind!=="sqlite")return;let file3=readAddress2(address).name;file3&&markDbMade(dir,SQLITE_BESIDE.map(end=>`${path95.resolve(dir,file3)}${end}`))}function asBefore(bp,story2,why,say){return noteForEngineers(bp,why,story2),{state:"as-before",why:scrub2(bp,why),...say?{say:scrub2(bp,say)}:{}}}function personsOwn(adapter,address,source2,dir,kind){if(address.trim()===source2.trim())return!0;try{if(adapter.isPersonsOwn(address,source2))return!0}catch{return!0}if(kind==="sqlite"){let file3=adapter.nameOf(address);return!path95.resolve(file3).startsWith(`${path95.resolve(dir)}${path95.sep}`)}return!1}function sqliteAt(address,from,to){return address.includes(from)?address.split(from).join(to):to}async function setUpStoryDb(bp,story2,dir,repo){try{setUpStoryRedis(bp,story2,dir,repo)}catch(err2){writeLog({level:"warn",ticket:story2,text:scrub2(bp,`The story's own Redis number was not set up: ${err2.message}`)})}let r=await ownDatabase(bp,story2,dir,repo);return r.state==="own"||r.state==="as-before"&&r.say?r:fellBack(bp,story2,dir,repo)??r}function fellBack(bp,story2,dir,repo){let db=readMeta(bp).database;if(!db||db.repo!==repo||db.kind==="sqlite")return null;let missing3=[db.setting.file,...db.from??[],...repoEnvFiles(readMeta(bp),repo)].find(f=>!!f&&fs100.existsSync(path95.join(bp.root,repo,f))&&!fs100.existsSync(path95.join(dir,f))),got=missing3?settingsFileAddress(dir,db.setting):void 0;if(!missing3||!got)return null;let place=placeOf(bp,db,got.value);if(place.here)return null;let say=`This story's copy has no ${missing3}, so its app falls back to the database in ${got.file}, which is on another computer (${place.host}). An engineer can add ${missing3} as a local file in Settings, with values for stories.`;return asBefore(bp,story2,say,say)}async function ownDatabase(bp,story2,dir,repo){let db=readMeta(bp).database;if(!db||db.repo!==repo)return{state:"none"};if(!db.confirmed&&db.kind==="sqlite")return{state:"none"};if(!db.confirmed)return asBefore(bp,story2,`squad-kit found the project's database, and no engineer has confirmed it yet, so ${story2} uses it as before. Confirm it on Harness, under Safety, or with: squad db confirm`);if(!db.setting.env&&!(db.setting.file&&db.setting.key))return asBefore(bp,story2,`squad-kit does not know which setting points the app at the database, so ${story2} uses it as before. Name it with: squad db confirm --env DATABASE_URL`);let written=personsAddress(bp,db);if(!written)return asBefore(bp,story2,`squad-kit does not know the database's address, so ${story2} uses it as before. Give it with: squad db confirm --address \u2026`);let place=placeOf(bp,db,written),source2=place.here?place.address:written,adapter;try{adapter=dbAdapter(db.kind)}catch(err2){return asBefore(bp,story2,`${story2} uses the database as before: ${err2.message}`)}let ref2={id:story2,folder:dir},had=readState(bp).stories.find(s=>s.story===story2),kept2=dbSecrets(bp).stories?.[story2];if(had&&kept2&&(db.kind!=="sqlite"||path95.resolve(had.dir)===path95.resolve(dir)))return personsOwn(adapter,kept2,source2,dir,db.kind)?asBefore(bp,story2,`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`):(path95.resolve(had.dir)!==path95.resolve(dir)&&(unwireCopy(had.dir),changeState(bp,s=>s.stories=s.stories.map(x=>x.story===story2?{...x,dir}:x))),wire(dir,db,kept2,asWritten(adapter,written,source2,kept2,story2)),keepOutOfCommits(dir,db,kept2),{state:"own",name:had.name,made:!1});if(!place.here)return asBefore(bp,story2,awayWords(place).replace("this story",story2),awayWords(place));let ignored=storyCopyIgnored(dir,db,source2);if(ignored)return asBefore(bp,story2,`${story2} uses the database as before. ${ignored}`);let template=currentTemplate(bp,source2);if(!template){if(db.kind!=="sqlite")return asBefore(bp,story2,`There is no template of the database yet, so ${story2} uses it as before. An engineer makes one on Harness, under Safety, or with: squad db template`);let raw=keptAddress(bp,db),file3=readAddress2(raw).name,own2=file3&&path95.isAbsolute(file3)?path95.relative(path95.join(bp.root,repo),file3):"";return own2&&!own2.startsWith("..")&&wire(dir,db,sqliteAt(raw,file3,path95.join(dir,own2))),{state:"none"}}let made;try{made=await adapter.createStoryDb(template,ref2)}catch(err2){let words2=err2 instanceof DbError&&err2.reason==="persons-own"?`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`:`${story2} uses the database as before: ${err2.message}`;return asBefore(bp,story2,words2)}return personsOwn(adapter,made.address,source2,dir,db.kind)?asBefore(bp,story2,`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`):(changeSecrets(bp,d=>d.stories={...d.stories,[story2]:made.address}),changeState(bp,s=>s.stories=[...s.stories.filter(x=>x.story!==story2),{story:story2,repo,dir,kind:db.kind,name:made.name,at:now2()}]),wire(dir,db,made.address,asWritten(adapter,written,source2,made.address,story2)),keepOutOfCommits(dir,db,made.address),{state:"own",name:made.name,made:!made.existed})}function rewireStoryDbs(bp){let db;try{db=readMeta(bp).database}catch{return}for(let r of storyRedisNumbers(bp))fs100.existsSync(r.dir)&&rewireStoryRedis(bp,r.story,r.dir,r.repo);if(db)for(let r of readState(bp).stories)fs100.existsSync(r.dir)&&rewireStoryDb(bp,r.story,r.dir,r.repo)}function rewireStoryDb(bp,story2,dir,repo){let db;try{rewireStoryRedis(bp,story2,dir,repo),db=readMeta(bp).database}catch{return!1}let had=readState(bp).stories.find(s=>s.story===story2&&s.repo===repo),kept2=dbSecrets(bp).stories?.[story2];if(!db||!had||!kept2||path95.resolve(had.dir)!==path95.resolve(dir))return!1;let written=personsAddress(bp,db);if(!written)return!1;let adapter,source2;try{adapter=dbAdapter(db.kind);let place=placeOf(bp,db,written);if(source2=place.here?place.address:written,personsOwn(adapter,kept2,source2,dir,db.kind))return!1}catch{return!1}return wire(dir,db,kept2,asWritten(adapter,written,source2,kept2,story2)),keepOutOfCommits(dir,db,kept2),!0}async function remakeStoryDb(bp,story2,dir,repo){return!readState(bp).stories.find(s=>s.story===story2&&s.repo===repo&&path95.resolve(s.dir)===path95.resolve(dir))||!await dropDatabase(bp,story2)?!1:(await setUpStoryDb(bp,story2,dir,repo)).state==="own"}function setAsideCopyDb(dir){let found2=findDatabase(dir,["."]).found,name=found2?.kind==="sqlite"&&found2.address?readAddress2(found2.address).name:void 0;if(!name)return[];let schemas=git(dir,["ls-files","--","*schema.prisma"],{allowFail:!0}).split(`
|
|
670
|
+
${text4}`),writeLog({level,ticket:story2,text:text4}))}function setUpStoryRedis(bp,story2,dir,repo){let all=settingsIn2(dir),redis=appSettings(dir,all).filter(s=>s.kind==="redis");if(!redis.length)return unwireCopyRedis(dir),null;let services=composeServices(dir),places=redis.map(s=>({s,place:serverPlace(s.value,services,{redis:!0})})),away=places.find(p=>!p.place.here);away&&!away.place.here&&logOnce(story2,`The app's Redis (${away.s.name}) is on another computer (${away.place.host}), so squad-kit leaves it as it is: ${story2} shares it.`);let local=places.flatMap(p=>p.place.here?[{s:p.s,address:p.place.address}]:[]),server=local.length?serverOf2(local[0].address,!0):null;if(!server)return unwireCopyRedis(dir),null;let records=storyRedisNumbers(bp),mine2=records.find(r=>r.story===story2&&r.server===server),onServer2=local.filter(l=>serverOf2(l.address,!0)===server),taken=new Set([0,...onServer2.map(l=>redisNumberIn(l.s.value)),...records.filter(r=>r.story!==story2&&r.server===server).map(r=>r.number)]),number=mine2?.number??chooseRedisNumber(story2,taken);if(!number)return logOnce(story2,`Every Redis database number from ${MIN} to ${MAX} on ${server} is taken by other stories, so ${story2} shares the app's own.`,"warn"),unwireCopyRedis(dir),null;let w={env:{},files:[]},put=(s,envValue2,fileValue)=>{w.env[s.name]=envValue2;let f=fileOf2(dir,s);f&&w.files.push({...f,value:fileValue})};for(let{s,address}of onServer2)HOST_ONLY.test(s.name)||put(s,withRedisNumber(address,number),withRedisNumber(s.value,number));for(let s of all)NUMBER_NAME.test(s.name)&&/^\d+$/.test(s.value)&&put(s,String(number),String(number));if(!Object.keys(w.env).length)return logOnce(story2,`The app's Redis settings have no place for a database number, so ${story2} shares the app's own Redis database.`),unwireCopyRedis(dir),null;wireCopyRedis(dir,w);let record={story:story2,repo,dir:path94.resolve(dir),number,server,at:mine2?.at??now2()};return changeRecords(bp,list8=>[...list8.filter(r=>!(r.story===story2&&r.repo===repo)),record]),mine2||logOnce(story2,`${story2}'s app has its own Redis database number, ${number}, on ${server} (${safeAddress(Object.values(w.env)[0])})`),record}function rewireStoryRedis(bp,story2,dir,repo){return storyRedisNumbers(bp).find(r=>r.story===story2&&r.repo===repo&&path94.resolve(r.dir)===path94.resolve(dir))?!!setUpStoryRedis(bp,story2,dir,repo):!1}function signIn(address){let t=address.trim(),link=/^(rediss?):\/\/(?:([^:@/]*)(?::([^@/]*))?@)?/i.exec(t),dec2=s=>{try{return s?decodeURIComponent(s):void 0}catch{return s}};if(link)return{...link[2]?{user:dec2(link[2])}:{},...link[3]?{password:dec2(link[3])}:{},tls:link[1].toLowerCase()==="rediss"};let opt=name=>new RegExp(`(?:^|,)\\s*${name}\\s*=([^,]*)`,"i").exec(t)?.[1]?.trim(),user=opt("user"),password5=opt("password");return{...user?{user}:{},...password5?{password:password5}:{},tls:/^true$/i.test(opt("ssl")??"")}}var FLUSH_MS=3e4,realCli=(tool5,args,env2,secrets)=>runTool(tool5,args,{env:env2,secrets,doing:"Clearing a story's Redis database",timeoutMs:FLUSH_MS});async function dropStoryRedis(bp,story2,cli=realCli){let mine2=storyRedisNumbers(bp).filter(r=>r.story===story2);if(!mine2.length)return!1;for(let r of mine2)unwireCopyRedis(r.dir);changeRecords(bp,list8=>list8.filter(r=>r.story!==story2));let left2=storyRedisNumbers(bp),cleared=!1;for(let r of mine2.filter((x,i)=>mine2.findIndex(y=>y.server===x.server&&y.number===x.number)===i)){if(!Number.isInteger(r.number)||r.number<MIN||r.number>MAX||left2.some(o=>o.server===r.server&&o.number===r.number))continue;let tool5=findTool("redis-cli");if(!tool5){writeLog({level:"info",ticket:story2,text:`redis-cli is not on this computer, so squad-kit did not clear Redis database ${r.number} on ${r.server}, ${story2}'s own. Its keys stay until Redis drops them.`});continue}let known=[...appSettings(path94.join(bp.root,r.repo)),...fs99.existsSync(r.dir)?appSettings(r.dir):[]].filter(s=>s.kind==="redis"),services=composeServices(path94.join(bp.root,r.repo)),same5=known.map(s=>serverPlace(s.value,services,{redis:!0})).find(p=>p.here&&serverOf2(p.address,!0)===r.server),how2=same5?.here?signIn(same5.address):{tls:!1},[,host3="localhost",port="6379"]=/^\[?([^\]]*?)\]?:(\d+)$/.exec(r.server)??[],args=["-h",host3,"-p",port,"-n",String(r.number),...how2.tls?["--tls"]:[],...how2.user?["--user",how2.user]:[],"FLUSHDB"];try{await cli(tool5,args,how2.password?{REDISCLI_AUTH:how2.password}:{},secretsOf(how2.password)),writeLog({level:"info",ticket:story2,text:`Cleared Redis database ${r.number} on ${r.server}, ${story2}'s own`}),cleared=!0}catch(err2){writeLog({level:"warn",ticket:story2,text:`squad-kit could not clear Redis database ${r.number} on ${r.server}, ${story2}'s own: ${err2.message}`})}}return cleared}function redisView(bp,repos){for(let repo of repos){let dir=path94.join(bp.root,repo),s=appSettings(dir).find(x=>x.kind==="redis");if(!s)continue;let place=serverPlace(s.value,composeServices(dir),{redis:!0});if(!place.here)return{server:place.host,here:!1,own:redisNumberIn(s.value),stories:[],words:`The app's Redis is on another computer (${place.host}). squad-kit leaves it as it is, so stories share it with your own app.`};let server=serverOf2(place.address,!0)??"localhost:6379",own2=redisNumberIn(s.value),stories=storyRedisNumbers(bp).filter(r=>r.server===server).map(r=>({story:r.story,number:r.number})),unique=stories.filter((x,i)=>stories.findIndex(y=>y.story===x.story)===i);return{server,here:!0,own:own2,stories:unique,words:`Each story's app gets its own Redis database number on ${server}, from ${MIN} to ${MAX}, so cache keys and locks never mix. Your own app keeps ${own2}. A story's keys are cleared when its copy goes.`}}return null}var OWN_DB_WORDS="The story has its own copy of the database.",DB_KINDS=["postgres","mysql","mongo","sqlite"],KIND_NAME={postgres:"Postgres",mysql:"MySQL",mongo:"Mongo",sqlite:"SQLite"},dbDir=bp=>path95.join(bp.localDir,"database"),templateFolder=bp=>path95.join(dbDir(bp),"template"),stateFile2=bp=>path95.join(dbDir(bp),"state.json");function readState(bp){try{let s=JSON.parse(fs100.readFileSync(stateFile2(bp),"utf8"));return{...s,stories:Array.isArray(s.stories)?s.stories:[],notes:Array.isArray(s.notes)?s.notes:[]}}catch{return{stories:[],notes:[]}}}function changeState(bp,change){let s=readState(bp);change(s),fs100.mkdirSync(dbDir(bp),{recursive:!0,mode:448});let tmp=`${stateFile2(bp)}.${process.pid}.tmp`;return fs100.writeFileSync(tmp,JSON.stringify(s,null,2),{mode:384}),fs100.renameSync(tmp,stateFile2(bp)),s}function dbSecrets(bp){try{return loadSecrets(bp.secretsFile).database??{}}catch{return{}}}function changeSecrets(bp,change){let all=loadSecrets(bp.secretsFile),d={...all.database??{},stories:{...all.database?.stories??{}}};change(d),d.address||delete d.address,Object.keys(d.stories).length||delete d.stories;let{database:_old,...rest}=all;saveSecrets(bp.secretsFile,Object.keys(d).length?{...rest,database:d}:rest)}function keptAddress(bp,db){return dbSecrets(bp).address??db?.address}function personsAddress(bp,db=readMeta(bp).database){let a=keptAddress(bp,db);if(!a||db?.kind!=="sqlite")return a;let file3=readAddress2(a).name;if(!file3||path95.isAbsolute(file3))return a;let prisma=db.from?.find(f=>f.endsWith(".prisma"));return sqliteAt(a,file3,path95.join(bp.root,db.repo,prisma?path95.posix.dirname(prisma):".",file3))}function scrub2(bp,text4){let s=dbSecrets(bp),out22=text4;for(let a of[s.address,...Object.values(s.stories??{})]){if(!a)continue;let pass=/^(?:jdbc:)?[a-z][\w+.-]*:\/\/[^:@/]*:([^@/]+)@/i.exec(a)?.[1]??/(?:^|;)\s*(?:password|pwd)\s*=([^;]+)/i.exec(a)?.[1];pass&&(out22=out22.split(pass).join("***").split(decodeURIComponent(pass)).join("***"))}return out22}function noteForEngineers(bp,text4,story2){let clean4=scrub2(bp,text4);if(readState(bp).notes.some(n=>n.story===story2&&n.text===clean4))return;changeState(bp,s=>s.notes=[...s.notes,{at:now2(),...story2?{story:story2}:{},text:clean4}].slice(-30));let project;try{project=readMeta(bp).project}catch{}writeLog({level:"warn",...project?{project}:{},...story2?{ticket:story2}:{},text:clean4})}function placeOf(bp,db,address){return db.kind==="sqlite"?{here:!0,address}:serverPlace(address,composeServices(path95.join(bp.root,db.repo)),{kind:db.kind})}function awayWords(place,who4="story"){let then=who4==="story"?"so this story uses no copy of its own":who4==="stories"?"so stories use it as before":"so it makes no template of it";return place.service?`This project's database runs in Docker as the ${place.host} service, which does not publish its port on this computer. squad-kit makes a story's own database only where it reaches it on this computer, ${then}. An engineer can publish the port in the compose file, or point the project at a local database in Settings.`:`This project's database is on another computer (${place.host}). squad-kit makes a story's own database only on this computer, ${then}. An engineer can point the project at a local database in Settings.`}var sameSetting=(a,b)=>a.env===b.env&&a.file===b.file&&a.key===b.key,sameDatabase=(a,b)=>!!a&&a.kind===b.kind&&a.repo===b.repo&&a.name===b.name&&sameSetting(a.setting,b.setting);function learnDatabase(bp,opts={}){let meta=readMeta(bp),before=meta.database;if(before?.confirmed&&!opts.again)return{database:before,changed:!1};let repos=reposOf(meta).map(r=>r.path),look=findDatabase(bp.root,repos,Object.fromEntries(repos.map(r=>[r,repoEnvFiles(meta,r)])));if(!look.found)return look.later&¬eForEngineers(bp,look.later),{...before?{database:before}:{},changed:!1,...look.later?{later:look.later}:{}};let f=look.found,secret=f.address&&hasPassword(f.address)?f.address:void 0,next={kind:f.kind,repo:f.repo,...f.name?{name:f.name}:{},...f.where?{where:f.where}:{},setting:f.setting,...f.address&&!secret?{address:f.address}:{},...f.from.length?{from:f.from}:{},...before?.hidePersonal?{hidePersonal:!0}:{},...before?.sampleRows?{sampleRows:before.sampleRows}:{}};return before?.confirmed&&sameDatabase(before,next)&&(next.confirmed=before.confirmed),writeMeta(bp,{...meta,database:next}),f.address&&changeSecrets(bp,d=>d.address=secret),{database:next,changed:!sameDatabase(before,next)}}function learnAfterRead(bp){try{learnDatabase(bp)}catch(err2){writeLog({level:"warn",text:scrub2(bp,`Learning the project's database did not work: ${err2.message}`)})}}var ENV_NAME=/^[A-Za-z_][A-Za-z0-9_]*$/;function confirmDatabase(bp,by2,change={}){let meta=readMeta(bp),repos=reposOf(meta).map(r=>r.path),kind=change.kind?.trim().toLowerCase();if(kind&&!DB_KINDS.includes(kind))throw new Error(`"${change.kind}" is not a kind squad-kit can copy. Use postgres, mysql, mongo or sqlite.`);let before=meta.database;if(!before&&!kind)throw new Error("squad-kit has not found the project's database. Name it: squad db confirm --kind postgres --env DATABASE_URL --address postgres://localhost:5432/app");let repo=change.repo?.trim()||before?.repo||(repos.length===1?repos[0]:"");if(!repos.includes(repo))throw new Error(repo?`There is no code repo at ${repo}. The repos: ${repos.join(", ")}.`:`Say which repo reads the database: ${repos.join(", ")}.`);let setting={...before?.setting??{}};if(change.env!==void 0){let env2=change.env.trim();if(env2&&!ENV_NAME.test(env2))throw new Error(`"${env2}" is not a setting name. Write it as the app reads it, like DATABASE_URL.`);env2?setting.env=env2:delete setting.env}if(change.file!==void 0){let file3=change.file.trim().replace(/\\/g,"/");if(file3&&(file3.startsWith("/")||/^[a-z]:/i.test(file3)||file3.split("/").includes("..")))throw new Error("The settings file must be inside the repo, like .env or appsettings.Development.json.");file3?setting.file=file3:delete setting.file}if(change.key!==void 0){let key=change.key.trim();key?setting.key=key:delete setting.key}if(!setting.env&&!(setting.file&&setting.key))throw new Error("Name the setting that points the app at the database: --env DATABASE_URL, or --file appsettings.Development.json --key ConnectionStrings:Default.");let address=change.address?.trim(),finalKind=kind??before.kind;if(address){let said2=kindOf(address);if(said2&&said2!==finalKind)throw new Error(`That address is for ${said2==="sqlserver"?"SQL Server":KIND_NAME[said2]}, not ${KIND_NAME[finalKind]}.`)}let name=change.name?.trim()||(address?readAddress2(address).name:void 0)||(kind&&kind!==before?.kind?void 0:before?.name),shared=address!==void 0?address&&!hasPassword(address)?address:void 0:before?.address,next={kind:finalKind,repo,...name?{name}:{},...before?.where&&finalKind===before.kind?{where:before.where}:{},setting,...shared?{address:shared}:{},...before?.from?{from:before.from}:{},confirmed:{by:by2,at:now2()},...before?.hidePersonal?{hidePersonal:!0}:{},...before?.sampleRows?{sampleRows:before.sampleRows}:{}};return writeMeta(bp,{...meta,database:next}),address!==void 0&&changeSecrets(bp,d=>d.address=address&&hasPassword(address)?address:void 0),next}function setDatabaseOptions(bp,o){let meta=readMeta(bp),db=meta.database;if(!db)throw new Error("squad-kit has not found the project's database yet. Find it first: squad db find");let next={...db};if(o.hidePersonal===!0?next.hidePersonal=!0:o.hidePersonal===!1&&delete next.hidePersonal,o.sampleRows===null)delete next.sampleRows;else if(o.sampleRows!==void 0){let n=Math.round(Number(o.sampleRows));if(!Number.isFinite(n)||n<10||n>1e6)throw new Error("The sample size is a number of rows from 10 to 1,000,000, like 1000.");next.sampleRows=n}return writeMeta(bp,{...meta,database:next}),next}function templateInfo(bp){try{return JSON.parse(fs100.readFileSync(path95.join(templateFolder(bp),"template.json"),"utf8"))}catch{return null}}function currentTemplate(bp,source2){try{return readTemplate2(templateFolder(bp),source2)}catch{return null}}var making=new Map;function makeDbTemplate(bp,by2){let run7=making.get(bp.squadDir);if(run7)return run7;let next=makeOnce(bp,by2).finally(()=>making.delete(bp.squadDir));return making.set(bp.squadDir,next),next}function templateReady(bp){let db=readMeta(bp).database;if(!db)throw new Error("squad-kit has not found the project's database yet. Find it first: squad db find");if(!db.confirmed)throw new Error("An engineer confirms the database first: squad db confirm");let source2=personsAddress(bp,db);if(!source2)throw new Error("squad-kit needs the database's address to make the template. Give it with: squad db confirm --address \u2026");let place=placeOf(bp,db,source2);if(!place.here)throw new Error(awayWords(place,"template"));let ignored=storyCopyIgnored(path95.join(bp.root,db.repo),db,source2);if(ignored)throw new Error(ignored);let adapter=dbAdapter(db.kind),missing3=adapter.missingTools();if(missing3.length)throw new Error(`Making a template of a ${KIND_NAME[db.kind]} database needs ${missing3.join(" and ")} on this computer.`);return{db,source:place.address,adapter}}async function makeOnce(bp,by2){let{db,source:source2,adapter}=templateReady(bp);changeState(bp,s=>s.making={since:now2(),by:by2});try{let t=await adapter.makeTemplate(source2,{folder:templateFolder(bp),hidePersonal:!!db.hidePersonal,...db.sampleRows?{sampleRows:db.sampleRows}:{}});return changeState(bp,s=>delete s.failed),writeLog({level:"info",project:readMeta(bp).project,text:`Made the template of the ${KIND_NAME[db.kind]} database ${t.info.sourceName}: ${t.info.tables.length} tables, ${t.info.bytes} bytes`}),t.info}catch(err2){let text4=scrub2(bp,err2.message);throw changeState(bp,s=>s.failed={at:now2(),text:text4}),new Error(text4)}finally{changeState(bp,s=>delete s.making)}}function makeTemplateBeside(bp,by2){templateReady(bp),makeDbTemplate(bp,by2).catch(err2=>noteForEngineers(bp,`The template of the database was not made: ${err2.message}`))}function formOf(file3){let base2=path95.posix.basename(file3);return/^\.env(?:\.|$)/.test(base2)||base2.endsWith(".env")?"env":base2.endsWith(".properties")?"properties":base2.endsWith(".json")?"json":null}function copyFile(dir,setting){if(!setting.file)return;let form=formOf(setting.file),key=form==="env"?setting.key??setting.env:setting.key;if(!(!form||!key)&&git(dir,["check-ignore","--",setting.file],{allowFail:!0}))return{path:setting.file,key,form}}function wire(dir,db,address,written=address){let file3=copyFile(dir,db.setting);wireCopy(dir,{env:db.setting.env?{[db.setting.env]:address}:{},value:written,...file3?{file:file3}:{}})}function asWritten(adapter,source2,here,address,story2){if(source2===here)return address;try{return adapter.storyAddress(source2,{id:story2})}catch{return address}}var SQLITE_BESIDE=["","-wal","-shm","-journal"];function keepOutOfCommits(dir,db,address){if(db.kind!=="sqlite")return;let file3=readAddress2(address).name;file3&&markDbMade(dir,SQLITE_BESIDE.map(end=>`${path95.resolve(dir,file3)}${end}`))}function ownServerHere(bp){try{let db=readMeta(bp).database,own2=db&&db.kind!=="sqlite"?personsAddress(bp,db):void 0;return!!(db&&own2&&placeOf(bp,db,own2).here)}catch{return!1}}function asBefore(bp,story2,why,say,away=!1){return!away&&ownServerHere(bp)&&(why=`${why} That is your own database on this computer: the story's app and tests can change its data.`),noteForEngineers(bp,why,story2),{state:"as-before",why:scrub2(bp,why),...say?{say:scrub2(bp,say)}:{}}}function personsOwn(adapter,address,source2,dir,kind){if(address.trim()===source2.trim())return!0;try{if(adapter.isPersonsOwn(address,source2))return!0}catch{return!0}if(kind==="sqlite"){let file3=adapter.nameOf(address);return!path95.resolve(file3).startsWith(`${path95.resolve(dir)}${path95.sep}`)}return!1}function sqliteAt(address,from,to){return address.includes(from)?address.split(from).join(to):to}async function setUpStoryDb(bp,story2,dir,repo){try{setUpStoryRedis(bp,story2,dir,repo)}catch(err2){writeLog({level:"warn",ticket:story2,text:scrub2(bp,`The story's own Redis number was not set up: ${err2.message}`)})}let r=await ownDatabase(bp,story2,dir,repo);return r.state==="own"||r.state==="as-before"&&r.say?r:fellBack(bp,story2,dir,repo)??r}function fellBack(bp,story2,dir,repo){let db=readMeta(bp).database;if(!db||db.repo!==repo||db.kind==="sqlite")return null;let missing3=[db.setting.file,...db.from??[],...repoEnvFiles(readMeta(bp),repo)].find(f=>!!f&&fs100.existsSync(path95.join(bp.root,repo,f))&&!fs100.existsSync(path95.join(dir,f))),got=missing3?settingsFileAddress(dir,db.setting):void 0;if(!missing3||!got)return null;let place=placeOf(bp,db,got.value);if(place.here)return null;let say=`This story's copy has no ${missing3}, so its app falls back to the database in ${got.file}, which is on another computer (${place.host}). An engineer can add ${missing3} as a local file in Settings, with values for stories.`;return asBefore(bp,story2,say,say,!0)}async function ownDatabase(bp,story2,dir,repo){let db=readMeta(bp).database;if(!db||db.repo!==repo)return{state:"none"};if(!db.confirmed&&db.kind==="sqlite")return{state:"none"};if(!db.confirmed)return asBefore(bp,story2,`squad-kit found the project's database, and no engineer has confirmed it yet, so ${story2} uses it as before. Confirm it on Harness, under Safety, or with: squad db confirm`);if(!db.setting.env&&!(db.setting.file&&db.setting.key))return asBefore(bp,story2,`squad-kit does not know which setting points the app at the database, so ${story2} uses it as before. Name it with: squad db confirm --env DATABASE_URL`);let written=personsAddress(bp,db);if(!written)return asBefore(bp,story2,`squad-kit does not know the database's address, so ${story2} uses it as before. Give it with: squad db confirm --address \u2026`);let place=placeOf(bp,db,written),source2=place.here?place.address:written,adapter;try{adapter=dbAdapter(db.kind)}catch(err2){return asBefore(bp,story2,`${story2} uses the database as before: ${err2.message}`)}let ref2={id:story2,folder:dir},had=readState(bp).stories.find(s=>s.story===story2),kept2=dbSecrets(bp).stories?.[story2];if(had&&kept2&&(db.kind!=="sqlite"||path95.resolve(had.dir)===path95.resolve(dir)))return personsOwn(adapter,kept2,source2,dir,db.kind)?asBefore(bp,story2,`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`):(path95.resolve(had.dir)!==path95.resolve(dir)&&(unwireCopy(had.dir),changeState(bp,s=>s.stories=s.stories.map(x=>x.story===story2?{...x,dir}:x))),wire(dir,db,kept2,asWritten(adapter,written,source2,kept2,story2)),keepOutOfCommits(dir,db,kept2),{state:"own",name:had.name,made:!1});if(!place.here)return asBefore(bp,story2,awayWords(place).replace("this story",story2),awayWords(place));let ignored=storyCopyIgnored(dir,db,source2);if(ignored)return asBefore(bp,story2,`${story2} uses the database as before. ${ignored}`);let template=currentTemplate(bp,source2);if(!template){if(db.kind!=="sqlite")return asBefore(bp,story2,`There is no template of the database yet, so ${story2} uses it as before. An engineer makes one on Harness, under Safety, or with: squad db template`);let raw=keptAddress(bp,db),file3=readAddress2(raw).name,own2=file3&&path95.isAbsolute(file3)?path95.relative(path95.join(bp.root,repo),file3):"";return own2&&!own2.startsWith("..")&&wire(dir,db,sqliteAt(raw,file3,path95.join(dir,own2))),{state:"none"}}let made;try{made=await adapter.createStoryDb(template,ref2)}catch(err2){let words2=err2 instanceof DbError&&err2.reason==="persons-own"?`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`:`${story2} uses the database as before: ${err2.message}`;return asBefore(bp,story2,words2)}return personsOwn(adapter,made.address,source2,dir,db.kind)?asBefore(bp,story2,`squad-kit refused to point ${story2} at your own database, so the story uses it as before.`):(changeSecrets(bp,d=>d.stories={...d.stories,[story2]:made.address}),changeState(bp,s=>s.stories=[...s.stories.filter(x=>x.story!==story2),{story:story2,repo,dir,kind:db.kind,name:made.name,at:now2()}]),wire(dir,db,made.address,asWritten(adapter,written,source2,made.address,story2)),keepOutOfCommits(dir,db,made.address),{state:"own",name:made.name,made:!made.existed})}function rewireStoryDbs(bp){let db;try{db=readMeta(bp).database}catch{return}for(let r of storyRedisNumbers(bp))fs100.existsSync(r.dir)&&rewireStoryRedis(bp,r.story,r.dir,r.repo);if(db)for(let r of readState(bp).stories)fs100.existsSync(r.dir)&&rewireStoryDb(bp,r.story,r.dir,r.repo)}function rewireStoryDb(bp,story2,dir,repo){let db;try{rewireStoryRedis(bp,story2,dir,repo),db=readMeta(bp).database}catch{return!1}let had=readState(bp).stories.find(s=>s.story===story2&&s.repo===repo),kept2=dbSecrets(bp).stories?.[story2];if(!db||!had||!kept2||path95.resolve(had.dir)!==path95.resolve(dir))return!1;let written=personsAddress(bp,db);if(!written)return!1;let adapter,source2;try{adapter=dbAdapter(db.kind);let place=placeOf(bp,db,written);if(source2=place.here?place.address:written,personsOwn(adapter,kept2,source2,dir,db.kind))return!1}catch{return!1}return wire(dir,db,kept2,asWritten(adapter,written,source2,kept2,story2)),keepOutOfCommits(dir,db,kept2),!0}async function remakeStoryDb(bp,story2,dir,repo){return!readState(bp).stories.find(s=>s.story===story2&&s.repo===repo&&path95.resolve(s.dir)===path95.resolve(dir))||!await dropDatabase(bp,story2)?!1:(await setUpStoryDb(bp,story2,dir,repo)).state==="own"}function setAsideCopyDb(dir){let found2=findDatabase(dir,["."]).found,name=found2?.kind==="sqlite"&&found2.address?readAddress2(found2.address).name:void 0;if(!name)return[];let schemas=git(dir,["ls-files","--","*schema.prisma"],{allowFail:!0}).split(`
|
|
671
671
|
`).filter(Boolean),files=path95.isAbsolute(name)?[name]:[dir,...schemas.map(s=>path95.dirname(path95.join(dir,s)))].map(b=>path95.resolve(b,name)),inside5=`${path95.resolve(dir)}${path95.sep}`,moved=[];for(let file3 of new Set(files))if(!(!file3.startsWith(inside5)||!fs100.existsSync(file3)||git(dir,["ls-files","--",path95.relative(dir,file3)],{allowFail:!0})))for(let end of SQLITE_BESIDE)fs100.existsSync(`${file3}${end}`)&&(fs100.renameSync(`${file3}${end}`,`${file3}.out-of-step${end}`),moved.push(`${file3}.out-of-step${end}`));return moved.length&&markDbMade(dir,moved),moved}async function dropStoryDb(bp,story2){let redis=await dropStoryRedis(bp,story2).catch(()=>!1);return await dropDatabase(bp,story2)||redis}async function dropDatabase(bp,story2){let had=readState(bp).stories.find(s=>s.story===story2);if(!had)return!1;let address=dbSecrets(bp).stories?.[story2],forget=()=>{let file3=had.kind==="sqlite"&&address?readAddress2(address).name:void 0;file3&&forgetDbMade(path95.join(bp.root,had.repo),SQLITE_BESIDE.map(end=>path95.relative(had.dir,path95.resolve(had.dir,file3))+end)),changeState(bp,s=>s.stories=s.stories.filter(x=>x.story!==story2)),changeSecrets(bp,d=>{d.stories&&delete d.stories[story2]}),unwireCopy(had.dir)},db=readMeta(bp).database,source2=personsAddress(bp,db);if(!address)return forget(),!1;if(!source2)return noteForEngineers(bp,`squad-kit could not drop ${had.name}, ${story2}'s own database: it does not know your database's address any more, to make sure it is not yours. Drop it yourself when you no longer need it.`,story2),!1;let away=placeOf(bp,had,address);if(!away.here)return forget(),noteForEngineers(bp,`squad-kit left ${had.name} alone: it is on another computer (${away.host}), and squad-kit drops a story's database only on this one. Drop it yourself when you no longer need it.`,story2),!1;try{let gone=await dbAdapter(had.kind).dropStoryDb(address,source2);return forget(),gone&&writeLog({level:"info",project:readMeta(bp).project,ticket:story2,text:`Dropped ${had.name}, the story's own database`}),gone}catch(err2){return err2 instanceof DbError&&(err2.reason==="not-ours"||err2.reason==="persons-own"||err2.reason==="bad-address")?(forget(),noteForEngineers(bp,`squad-kit left ${had.name} alone: ${err2.message}`,story2),!1):(noteForEngineers(bp,`squad-kit could not drop ${had.name}, ${story2}'s own database, yet. It tries again later: ${err2.message}`,story2),!1)}}function doneWith(bp,story2){if(story2==="lead")return!1;let t;try{t=readTicket(bp,story2)}catch{return!1}return!t||!!t.mergedAt||t.stage==="done"||!!t.archived}async function tidyStoryDbs(bp){let out22=[],ids=new Set([...readState(bp).stories.map(r=>r.story),...storyRedisNumbers(bp).map(r=>r.story)]);for(let id2 of ids)doneWith(bp,id2)&&await dropStoryDb(bp,id2)&&out22.push(id2);return out22}var TIDY_MS=2*6e4,tidies=new Map;function tidyStoryDbsBeside(bp,at3=Date.now(),soon=!1){let t=tidies.get(bp.squadDir)??{at:0,busy:!1};if(tidies.set(bp.squadDir,t),t.busy||!soon&&at3<t.at)return;if(!readState(bp).stories.length&&!storyRedisNumbers(bp).length){t.at=at3+TIDY_MS;return}t.busy=!0;let go=()=>{tidyStoryDbs(bp).catch(()=>{}).finally(()=>{t.busy=!1,t.at=Date.now()+TIDY_MS})};soon?setTimeout(go,0).unref():go()}function databaseView(bp){let db=readMeta(bp).database,s=readState(bp),address=personsAddress(bp,db),info2=templateInfo(bp),missing3=[];try{db&&(missing3=dbAdapter(db.kind).missingTools())}catch{missing3=[]}let live=s.stories.map(r=>({...r,...readTicket(bp,r.story)?.title?{title:readTicket(bp,r.story).title}:{}})),place=db&&address?placeOf(bp,db,address):void 0,away=place&&!place.here?place:void 0,blocked=away?awayWords(away,"stories"):db?storyCopyIgnored(path95.join(bp.root,db.repo),db,address):void 0,ready=!!(db?.confirmed&&info2&&!blocked),redis=null;try{redis=redisView(bp,reposOf(readMeta(bp)).map(r=>r.path))}catch{redis=null}return{database:db?{...db,...address?{address:withoutPassword(address)}:{},secret:!!dbSecrets(bp).address,kindName:KIND_NAME[db.kind]}:null,template:info2?{...info2,source:withoutPassword(info2.source)}:null,...s.making?{making:s.making}:{},...s.failed?{failed:s.failed}:{},...blocked?{blocked:scrub2(bp,blocked)}:{},...redis?{redis}:{},stories:live,notes:[...s.notes].reverse().slice(0,10),missing:missing3,words:ready?"Each story gets its own copy of the database, made from a small template. Your own database is never changed.":away?"Stories use the database as before. It is on another computer, and squad-kit makes each story's own copy only on this one.":blocked?"Stories use the database as before. The app chooses its database itself, so squad-kit cannot give each story a copy.":db?"Stories use the database as before, until an engineer sets up a copy for each story.":"squad-kit has not found a database in this project."}}var PullRequestClash=class extends Error{constructor(tickets,base2){super(`The pull request of ${tickets.join(", ")} clashes with newer changes in ${base2}.`);this.tickets=tickets;this.base=base2}tickets;base},PIPELINE_MS=20*6e4,ticketMessage=t=>`${t.title} (${t.id})`,ticketWords=t=>conventionalWords(t.kind==="bug"?"fix":"feat",t.title,t.id);async function commitTicket(ticket,how2={}){if(!ticket.worktree||!fs101.existsSync(ticket.worktree))return null;await stageAllLater(ticket.worktree);let owed=ticket.hookOwed&&ticket.baseCommit;return!owed&&!somethingToSave(ticket.worktree)?null:(await commitChecked(ticket.worktree,ticketMessage(ticket),ticketWords(ticket),HOOK_MS,{...how2,...owed?{from:ticket.baseCommit}:{}}),git(ticket.worktree,["rev-parse","--short","HEAD"]))}function commitWithoutCheck(ticket){return!ticket.worktree||!fs101.existsSync(ticket.worktree)||(stageAll(ticket.worktree),!somethingToSave(ticket.worktree))?null:(commitWithoutHooks(ticket.worktree,ticketMessage(ticket)),git(ticket.worktree,["rev-parse","--short","HEAD"]))}async function commitWithoutCheckLater(ticket){return!ticket.worktree||!fs101.existsSync(ticket.worktree)||(await stageAllLater(ticket.worktree),!somethingToSave(ticket.worktree))?null:(await commitLater(ticket.worktree,ticketMessage(ticket)),git(ticket.worktree,["rev-parse","--short","HEAD"]))}function hooksStillOwed(ticket){return ticket.hookOwed?!0:!ticket.worktree||!fs101.existsSync(ticket.worktree)?!1:(stageAll(ticket.worktree),bringHookRunner(ticket.worktree),somethingToSave(ticket.worktree)&&commitHooks(ticket.worktree).length>0)}async function commitRelease(dir,version,log3,how2={}){let message=keepsCommitRules(dir)?conventionalWords("chore",`release ${version}`):`release ${version}`;try{await commitChecked(dir,message,conventionalWords("chore",`release ${version}`),SHORT_HOOK_MS,{...how2,noRunner:!0})}catch(err2){if(!(err2 instanceof HookRefused))throw err2;await commitLater(dir,message),log3.push({ok:!1,text:`The project's own check said no to the release's own save, which holds only the version and the changelog, so squad-kit saved it without that check: ${err2.said.split(`
|
|
672
672
|
`).slice(-1)[0]??""}`.trim()})}}function basesNow(bp){let meta=readMeta(bp);return meta.engine!=="live"?new Map:new Map(reposOf(meta).map(r=>{let root=path96.join(projectRoot(bp),r.path);return[r.path,git(root,["rev-parse","--verify","-q",`refs/heads/${r.base||currentBranch(root)}`],{allowFail:!0})]}))}async function liveRelease(bp,release,tickets,from=new Map){let root=projectRoot(bp),meta=readMeta(bp),groups=new Map;for(let t of tickets)for(let view of copyViews(meta,t)){let repo=repoFor(meta,view),g=groups.get(repo.path)??{repo,tickets:[]};g.tickets.push(view),groups.set(repo.path,g)}let log3=[],unpushed=[],multi=groups.size>1||!!meta.repos?.length,leaving=(release.out??[]).filter(id2=>!release.tickets.includes(id2)).map(id2=>readTicket(bp,id2)).filter(t=>!!(t?.mergedAt&&t.stage!=="done")).flatMap(t=>copyViews(meta,t)),order=repoOrder(meta),place=repo=>order.includes(repo)?order.indexOf(repo):order.length,plans=[...groups.values()].sort((a,b)=>place(a.repo.path)-place(b.repo.path)).map(({repo,tickets:list8})=>{let repoRoot=path96.join(root,repo.path),remote=repo.remote||remotes(repoRoot)[0],url=remote?git(repoRoot,["config","--get",`remote.${remote}.url`],{allowFail:!0}):"",host3=url?repoHostFor(bp,url):null;return{repo,repoRoot,list:list8,host:host3,env:host3?.gitEnv()??{},leave:leaving.filter(t=>repoFor(meta,t).path===repo.path)}});for(let{repo,repoRoot,list:list8,host:host3,leave}of plans){let clashing=[];for(let t of list8.filter(x=>host3&&x.pr?.host===host3.host&&!x.mergedAt))await host3.mergeState(t.pr.id)==="clash"&&clashing.push(t.id);let base2=repo.base||currentBranch(repoRoot);for(let t of list8.filter(x=>x.branch&&!x.mergedAt&&!x.pr))mergeWouldClash(repoRoot,base2,t.branch)&&clashing.push(t.id);if(clashing.length)throw new PullRequestClash(clashing,repo.base||currentBranch(repoRoot));let problem2=leave.length?await leaveOutProblem(repoRoot,repo.base||currentBranch(repoRoot),leave):null;if(problem2)throw new Error(`${problem2} Put it back into ${release.version}, or take that work out too.`)}startJournal(bp,release.version,tickets.map(t=>t.id));let named=r=>repoLabel(meta,r)??r;for(let{repo,repoRoot,list:list8,host:host3,env:env2,leave}of plans){let waitsFor=unpushed.find(u=>repoReliesOn(meta,repo.path,u.repo))?.repo,docs=docsInGit(bp)&&path96.resolve(docsRepo(bp))===path96.resolve(repoRoot)?dir=>stageReleaseDocs(bp,dir,release,list8,repo.base||currentBranch(repoRoot)):void 0,keep=path96.relative(repoRoot,bp.squadDir),note2=patch=>noteRepo(bp,repo.path,patch),done2=await releaseRepo(repoRoot,repo,release,list8,meta.commands?.deploy,host3,env2,docs,keep&&!keep.startsWith("..")?keep:void 0,{leave,note:note2,...from.get(repo.path)?{startAt:from.get(repo.path)}:{},...waitsFor?{holdPush:{plain:`${named(repo.path)} did not go out yet: it goes after ${named(waitsFor)}.`}}:{},merged:id2=>notePrMerged(bp,id2),guardFor:dir=>guardSupport().on?projectGuardFor(bp,{cwd:dir,...meta.guard?{allow:meta.guard}:{}}):void 0});log3.push(...done2.log.map(l=>multi?{...l,text:`${repo.path}: ${l.text}`}:l)),done2.unpushed&&unpushed.push(done2.unpushed);for(let t of list8)dropTicketCache(bp,t.id)}for(let t of tickets)t.storyDir&&!copyDirsIn(t.storyDir).length&&fs101.rmSync(t.storyDir,{recursive:!0,force:!0});for(let t of tickets)await dropStoryDb(bp,t.id).catch(()=>!1);return{log:log3,unpushed}}async function releaseRepo(root,repo,release,tickets,deployFallback,host3,env2,docs,keep,extra={}){let base2=repo.base||currentBranch(root),remote=repo.remote||remotes(root)[0],version=release.version,branch=`release/${version}`,dir=path96.join(worktreeRoot(root),`release-${version}`),log3=[],unpushed;for(let t of tickets.filter(x=>x.mergedAt))log3.push({ok:!0,text:`Already in the code: ${t.id} ${t.title}`});let viaPr=tickets.filter(t=>host3&&t.pr?.host===host3.host&&!t.mergedAt),note2=extra.note??(()=>{});for(let t of viaPr)await host3.merge(t.pr.id,`Merge ${t.title} (${t.id})`),extra.merged?.(t.id),log3.push({ok:!0,text:`Merged pull request ${t.pr.id}: ${t.title}`});let start=extra.startAt??base2;(viaPr.length||tickets.some(t=>t.pr&&t.mergedAt))&&remote&&(start=await fetchedTip(root,remote,base2,{env:env2})),await removeWorktree(root,dir),await gitLater(root,["worktree","prune"],{allowFail:!0}),git(root,["branch","-D",branch],{allowFail:!0}),await copyTurn(dir,()=>makeCopy(root,dir,["-b",branch],start)),note2({base:base2,...remote?{remote}:{},dir,branch});try{for(let t of tickets)if(!(!t.branch||t.mergedAt||viaPr.includes(t)))try{await gitLater(dir,[...commitArgsFor(dir),"merge","--no-ff","-m",`Merge ${t.title} (${t.id})`,t.branch]),log3.push({ok:!0,text:`Merged ${t.id} ${t.title}`})}catch(err2){let clashes=await gitLater(dir,["diff","--name-only","--diff-filter=U"],{allowFail:!0});throw await gitLater(dir,["merge","--abort"],{allowFail:!0}),clashes?new PullRequestClash([t.id],base2):(writeLog({level:"error",ticket:t.id,text:`Merging into ${base2} for ${release.version}: ${err2.message}`}),new Error(`${t.id} could not be merged into ${base2}, so the release stopped. An engineer can see why in the log.`))}let reverts=extra.leave?.length?await leaveOut(dir,root,base2,extra.leave,version,log3):[];release.addons.changelog&&(writeChangelog(dir,version,release.notes),log3.push({ok:!0,text:"Changelog updated"}));let pkg=path96.join(dir,"package.json");if(fs101.existsSync(pkg)){let json2=JSON.parse(fs101.readFileSync(pkg,"utf8"));json2.version&&(json2.version=version.replace(/^v/,""),fs101.writeFileSync(pkg,`${JSON.stringify(json2,null,2)}
|
|
673
673
|
`,"utf8"),log3.push({ok:!0,text:`Set the version to ${json2.version}`}))}docs&&remote&&await netGitLater(dir,["fetch","--quiet",remote,base2],{env:env2,allowFail:!0});for(let text4 of docs?.(dir)??[])log3.push({ok:!0,text:text4});await stageAllLater(dir),somethingToSave(dir)&&await commitRelease(dir,version,log3,extra.guardFor?.(dir)?{guard:extra.guardFor(dir)}:{}),note2({base:base2,commit:git(dir,["rev-parse","HEAD"]),...release.addons.tag?{tag:version}:{}}),release.addons.tag&&(git(dir,[...commitArgsFor(dir),"tag","-f","-a",version,"-m",`Release ${version}`]),log3.push({ok:!0,text:`Marked the code as ${version}`})),reverts.length&&await bringBack(dir,reverts,version,base2);let head=git(dir,["rev-parse","HEAD"]);note2({base:base2,head});let sent=head;if(remote&&extra.holdPush)unpushed={repo:repo.path,remote,base:base2,commit:head,...release.addons.tag?{tag:version}:{},error:extra.holdPush.plain,plain:extra.holdPush.plain},log3.push({ok:!1,text:extra.holdPush.plain});else if(remote)try{sent=await pushRelease(dir,remote,base2,version,release.notes,host3,env2,log3),note2({base:base2,head:sent,pushed:!0}),release.addons.tag&&await netGitLater(dir,["push",remote,`refs/tags/${version}`],{env:env2}),log3.push({ok:!0,text:"Sent to your team's shared copy of the code"})}catch(err2){let error2=err2.message,plain=pushWords(error2);unpushed={repo:repo.path,remote,base:base2,commit:head,...release.addons.tag?{tag:version}:{},error:pushDetail(error2,{remote,base:base2,version}),plain},log3.push({ok:!1,text:`${plain} Press Send again, or run: squad release ${version} --push-again`})}else log3.push({ok:!0,text:"Saved on this computer only: this project has no online copy to send it to."});if(await moveLocalBase(root,base2,sent,version,keep,log3),note2({base:base2,moved:!0}),release.addons.pipeline){let deploy=repo.commands.deploy?.trim()||deployFallback?.trim();if(deploy)try{sent!==head&&await gitLater(dir,["checkout","--quiet","--detach",sent]);let r=await runCommand(dir,deploy,PIPELINE_MS,extra.guardFor?.(dir)?{guard:extra.guardFor(dir)}:{});r.ok||writeLog({level:"error",text:`The after-release step of ${version} failed (${deploy}): ${tail2(r.output,3)}`}),log3.push({ok:r.ok,text:r.ok?"Ran your after-release step":"Your after-release step failed. An engineer can see why in the log."})}catch(err2){writeLog({level:"error",text:`The after-release step of ${version} did not run: ${err2.message}`}),log3.push({ok:!1,text:"Your after-release step did not run. An engineer can see why in the log."})}else log3.push({ok:!1,text:"No after-release step is set up in Settings, so none ran"})}}finally{await removeWorktree(root,dir)}let tip=git(root,["rev-parse","--verify","-q",`refs/heads/${branch}`],{allowFail:!0});!unpushed&&tip&&isAncestor(root,tip,base2)&&git(root,["branch","-D",branch],{allowFail:!0});for(let t of tickets)t.worktree&&await removeWorktree(root,t.worktree);return{log:log3,...unpushed?{unpushed}:{}}}function moveLocalBase(root,base2,head,version,keep,log3){return baseTurn(root,async()=>{let dirty=currentBranch(root)===base2?await gitLater(root,["status","--porcelain","--untracked-files=no","--",".",...keep?[`:(exclude)${keep}`]:[]],{allowFail:!0,env:{GIT_OPTIONAL_LOCKS:"0"}}):"";moveBaseNow(root,base2,head,version,keep,log3,dirty)})}function moveBaseNow(root,base2,head,version,keep,log3,dirty,again2=!1){let branch=`release/${version}`,before=git(root,["rev-parse",base2]);if(!isAncestor(root,before,head)){try{let r=mergeIntoBase(root,base2,head,`Merge release ${version} into ${base2}`,keep);r.note&&writeLog({level:"warn",text:`Moving ${base2} for ${version}: ${r.note}`}),log3.push(r.moved?{ok:!0,text:"The code here has work accepted while the release went out, so the release was merged into it"}:{ok:!1,text:`Your own copy of the code was not updated. An engineer can bring it in with: git merge ${branch}`})}catch(err2){err2 instanceof MergeClash||writeLog({level:"error",text:`Moving ${base2} for ${version}: ${err2.message}`});let why=err2 instanceof MergeClash?"it has work accepted while the release went out, and the two clash":"something technical stopped it";log3.push({ok:!1,text:`The code on this computer was not updated: ${why}. An engineer can bring it in with: git merge ${branch}`})}return}if(currentBranch(root)===base2){if(dirty)log3.push({ok:!1,text:`Your own copy of the code has changes, so it was not updated. An engineer can bring it in with: git merge ${branch}`});else try{keepingFiles(root,keep,before,head,()=>git(root,["merge","--ff-only",head])),log3.push({ok:!0,text:"Your own copy of the code now has the release"})}catch(err2){writeLog({level:"warn",text:`Moving ${base2} for ${version}: ${err2.message}`}),log3.push({ok:!1,text:`Your own copy of the code was not updated. An engineer can bring it in with: git merge --ff-only ${head.slice(0,8)}`})}return}try{git(root,["update-ref",`refs/heads/${base2}`,head,before])}catch(err2){if(!again2)return moveBaseNow(root,base2,head,version,keep,log3,dirty,!0);writeLog({level:"error",text:`Moving ${base2} for ${version}: ${err2.message}`}),log3.push({ok:!1,text:`The code on this computer was not updated. An engineer can bring it in with: git merge ${branch}`})}}async function pushAgain(bp,release){let root=projectRoot(bp),list8=release.unpushed??[],multi=list8.length>1||!!readMeta(bp).repos?.length,log3=[],unpushed=[];for(let u of list8){if(unpushed.some(x=>repoReliesOn(readMeta(bp),u.repo,x.repo))){unpushed.push(u);continue}let repoRoot=path96.join(root,u.repo),url=git(repoRoot,["config","--get",`remote.${u.remote}.url`],{allowFail:!0}),host3=url?repoHostFor(bp,url):null,env2=host3?.gitEnv()??{},lines3=[];try{await netGitLater(repoRoot,["fetch",u.remote,u.base],{env:env2,allowFail:!0});let there=git(repoRoot,["rev-parse","--verify","-q",`refs/remotes/${u.remote}/${u.base}`],{allowFail:!0});if(!there||!isAncestor(repoRoot,u.commit,there)){let sent=await pushRelease(repoRoot,u.remote,u.base,release.version,release.notes,host3,env2,lines3,u.commit);sent!==u.commit&&await moveLocalBase(repoRoot,u.base,sent,release.version,keepOf(bp,repoRoot),lines3)}u.tag&&await netGitLater(repoRoot,["push",u.remote,`refs/tags/${u.tag}`],{env:env2}),lines3.push({ok:!0,text:"Sent to your team's shared copy of the code"})}catch(err2){let error2=err2.message,plain=pushWords(error2);unpushed.push({...u,error:pushDetail(error2,{remote:u.remote,base:u.base,version:release.version}),plain}),lines3.push({ok:!1,text:plain})}log3.push(...lines3.map(l=>multi?{...l,text:`${u.repo}: ${l.text}`}:l))}return{log:log3,unpushed}}function keepOf(bp,repoRoot){let keep=path96.relative(repoRoot,bp.squadDir);return keep&&!keep.startsWith("..")?keep:void 0}var PushClash=class extends Error{};function isBehind(message){return/fetch first|fast-forward|Updates were rejected|before pushing again|tip of your current branch is behind/i.test(message)}function isProtected(message){return/protected branch|GH00\d|GH013|remote rejected|pre-receive hook declined|through a pull request|merge request/i.test(message)}var SHARED_COPY="your team's shared copy of the code";function pushWords(message){return/same lines as this release/.test(message)?message.split(`
|
|
@@ -1468,7 +1468,7 @@ ${projectContext(meta,decisions,ctx)}
|
|
|
1468
1468
|
|
|
1469
1469
|
${ticketContext(t)}${breakdown}`}var BUILDS_BY_ITSELF="Builds by itself",NO_QUESTIONS="Builds by itself: no questions, the story was approved with the breakdown",APPROVING="Builds by itself: approving the plan",ACCEPTING="Accepting it by itself";var PR_WAITS="Waits for someone to approve the pull request";function mergeWait(t){return!t||t.stage!=="release"||isMerged(t)||t.squad||t.waiting||t.live||t.deleting||t.archived?null:t.pr?t.track==="auto"?"pr":null:t.branch?"checkout":null}function mergeWaitWords(wait){return wait==="pr"?PR_WAITS:CHECKOUT_WAITS}function sameDecided(a,b){let plain=s=>s.toLowerCase().normalize("NFKD").replace(new RegExp("\\p{M}+","gu"),"").replace(/[^\p{L}\p{N}]+/gu," ").trim();return plain(a)===plain(b)||saysTheSame(a,b)}var STOP_WORDS={gap:"Missing in the BRD",clash:"Two parts clash",check:"A check failed"},MAX_DECIDED=6;function askWords(who4,kind){return kind?{status:`Needs you: ${STOP_WORDS[kind].charAt(0).toLowerCase()}${STOP_WORDS[kind].slice(1)}`,summary:STOP_WORDS[kind]}:{status:`${who4} has a question for you`,summary:`The ${who4} needs an answer to carry on`}}var MAX_SOURCES=4;function cleanSources(raw){if(!Array.isArray(raw))return[];let out22=[];for(let r of raw){if(out22.length>=MAX_SOURCES)break;let o=r??{},text4=typeof o.text=="string"?o.text.replace(/\s+/g," ").trim().slice(0,160):"";if(!text4)continue;let ticket=typeof o.ticket=="string"?o.ticket.trim().toUpperCase():"",section=typeof o.section=="string"||typeof o.section=="number"?String(o.section).trim().slice(0,40):"",page=Number(o.page);out22.push({text:text4,.../^[A-Z][A-Z0-9]{0,9}-\d{1,6}$/.test(ticket)?{ticket}:{},...section?{section}:{},...Number.isInteger(page)&&page>0?{page}:{}})}return out22}function stopOf(t){if(!t.waiting)return;if(t.waiting.kind==="ask"&&t.agentQuestion?.kind)return t.agentQuestion.kind;let gate=t.waiting.kind==="stuck"?t.stuck?.gate:void 0;return gate==="checks"||gate==="review"||gate==="proof"?"check":"other"}function addDecided(t,by2,texts){if(t.track!=="auto"||!Array.isArray(texts))return[];let list8=t.decided??[],seen=list8.map(d=>d.text),n=list8.reduce((m,d)=>Math.max(m,Number(/^d(\d+)$/.exec(d.id)?.[1]??0)),0),added2=[];for(let raw of texts.slice(0,MAX_DECIDED)){let text4=typeof raw=="string"?raw.replace(/\s+/g," ").trim().slice(0,300):"";!text4||seen.some(d=>sameDecided(d,text4))||(seen.push(text4),n+=1,added2.push({id:`d${n}`,text:text4,by:by2,at:now2(),...t.breakdown?{item:t.breakdown.item}:{},ticket:t.id}))}if(!added2.length)return added2;t.decided=[...list8,...added2];for(let d of added2)addActivity(t,by2==="answer"?"squad":by2,`Decided for you (${d.id}): ${d.text}`);return added2}function changeDecided(bp,id2,decidedId,text4){let want=text4?.trim();if(!want)throw new BoardActionError("Say what it should be instead.");let t0=requireTicket(bp,id2),key=decidedId?.trim().toLowerCase(),d=t0.decided?.find(x=>x.id===key);if(!d)throw new BoardActionError(`${t0.id} has nothing decided for you as ${decidedId}.${t0.decided?.length?` It has ${t0.decided.map(x=>x.id).join(", ")}.`:""}`);let meta=readMeta(bp),became;return shipped(t0)?became=changeAfterRelease(bp,t0.id,`${want}
|
|
1470
1470
|
|
|
1471
|
-
Instead of "${d.text}", which the squad decided on ${t0.id}.`,"ticket").id:became=openChange(proposeChange(bp,t0.id,`Instead of "${d.text}" (decided for you): ${want}`)).id,updateTicket(bp,t0.id,t=>{let e=t.decided?.find(x=>x.id===d.id);e&&(e.changed={text:want,by:meta.user.name,at:now2(),ticket:became}),addActivity(t,"you",`Changed what was decided for you (${d.id}): ${want}. It became ${/^c\d+$/.test(became)?"change":"ticket"} ${became}`)})}function and2(xs){return xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs.at(-1)}`}function acceptedLine(t){let g=t.gates,passed=[g?.checks==="pass"?"checks":"",g?.review==="pass"?"review":"",(t.proof??[]).some(p=>p.after)?"screenshots":""].filter(Boolean),head=passed.length?`${and2(passed).charAt(0).toUpperCase()}${and2(passed).slice(1)} passed: accepted by itself`:"Accepted by itself",notes=[g?.checks==="unchecked"?"No tests ran: you chose to go on without tests in this project":"",g?.checks==="skipped"?"The checks did not pass, and you chose to go on anyway":"",g?.review==="skipped"?"The review was still open, and you chose to go on anyway":""].filter(Boolean);return[head,...notes].join(". ")}function acceptNext(t,seconds2,speed){t.live=!0,t.stage="review",t.waiting=null,t.status=ACCEPTING,scheduleSquad(t,"accept",seconds2,speed)}async function acceptByItselfBeside(bp,id2,signal2){let t=requireTicket(bp,id2);return(x=>x.track!=="auto"||shipped(x)||!!openChange(x)||(x.proof??[]).some(p=>p.shows&&p.shows!=="met"&&p.shows!=="off-screen")||beforeDependency(bp,x))(t)?signal2?.aborted||t.stopRequested||t.deleting?t:acceptByItself(bp,id2):holdingSave(bp,t.id,async closing=>await saveBeforeAccept(bp,id2,closing,signal2)?acceptInTurn(bp,id2,closing,()=>{let now4=requireTicket(bp,id2);return signal2?.aborted||now4.stopRequested||now4.deleting||now4.takenOver||now4.chat?.open&&!now4.chat.beside?now4:acceptByItself(bp,id2)}):requireTicket(bp,id2))}function beforeDependency(bp,t){try{return refuseBeforeDependency(bp,t),!1}catch{return!0}}function acceptByItself(bp,id2){let t=requireTicket(bp,id2);if(t.track!=="auto")throw new BoardActionError(`${t.id} is not on the ${BUILDS_BY_ITSELF} track. Check the proof and press Looks right.`);if(shipped(t))return updateTicket(bp,id2,x=>x.squad?.phase==="accept"&&(x.squad=null));let c=openChange(t);if(c)return updateTicket(bp,id2,x=>{x.selfAccept={...x.selfAccept,waitsFor:c.id},x.live=!1,x.squad=null,x.waiting=null,x.stage="review",x.status=`Waits for change ${c.id}, then it is accepted by itself`,addActivity(x,"squad",`The checks, the review and the proof passed, but change ${c.id} is still open: it is accepted by itself once that change is settled`)});let open3=(t.proof??[]).filter(p=>p.shows&&p.shows!=="met"&&p.shows!=="off-screen");if(open3.length){let broken=open3.some(p=>p.shows==="broken");return updateTicket(bp,id2,x=>{x.live=!1,x.squad=null,x.stage="review",x.status="Proof ready to check",x.waiting={kind:"proof",summary:`Ready for you to check: a "done when" is ${broken?"not working":"not shown"} yet`},addActivity(x,"squad",broken?'Not accepted by itself: the screenshots show a "done when" is not working yet':'Not accepted by itself: the screenshots do not show a "done when" yet')})}try{refuseBeforeDependency(bp,t)}catch(err2){return updateTicket(bp,id2,x=>{x.live=!1,x.squad=null,x.stage="review",x.status="Proof ready to check",x.waiting={kind:"proof",summary:"The proof is ready for you to check"},addActivity(x,"squad",`Not accepted by itself: ${err2.message}`)})}return t.selfAccept?.waitsFor&&updateTicket(bp,id2,x=>delete x.selfAccept?.waitsFor),acceptProof(bp,requireTicket(bp,id2),acceptedLine(t))}function afterPullRequest(bp,id2,clash){if(clash){bringInAgain(bp,id2);return}let t=readTicket(bp,id2);t?.track!=="auto"||t.mergedAt||!t.pr||updateTicket(bp,id2,x=>{x.status=PR_WAITS,addActivity(x,"release",`Pull request ${x.pr?.id??""} waits for someone to approve it. That is a normal wait, not a failure`)})}function resumeAccept(bp,t){if(t.track!=="auto"||!t.selfAccept?.waitsFor||t.squad||t.waiting||t.takenOver||t.deleting||shipped(t)||openChange(t))return!1;let meta=readMeta(bp);return updateTicket(bp,t.id,x=>{delete x.selfAccept?.waitsFor,addActivity(x,"squad","The change it waited for is settled, so it is accepted by itself now"),acceptNext(x,.2,meta.engine==="live"?1:meta.speed)}),!0}function writeFile2(file3,text4){if(fs110.existsSync(file3)&&fs110.readFileSync(file3,"utf8")===text4)return;fs110.mkdirSync(path102.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs110.writeFileSync(tmp,text4,"utf8"),fs110.renameSync(tmp,file3)}function dumpYaml2(data2){return yaml16.dump(JSON.parse(JSON.stringify(data2)),{lineWidth:110,noRefs:!0})}var cache3=new Map;function readYaml2(file3){if(!fs110.existsSync(file3))return;let st=fs110.statSync(file3),stamp=`${st.mtimeMs}:${st.size}`,hit=cache3.get(file3);if(hit?.stamp===stamp)return structuredClone(hit.value);let value=yaml16.load(fs110.readFileSync(file3,"utf8"));return cache3.set(file3,{stamp,value}),structuredClone(value)}var ID=/^BR-(\d{1,9})$/;function breakdownId(id2){return id2.trim().toUpperCase()}function idNumber2(id2){return Number(ID.exec(id2)?.[1]??0)}function fill(b){b.questions??=[],b.epics??=[],b.decided??=[],b.leftOut??=[],b.next??={epic:1,feature:1,story:1};for(let e of b.epics){e.features??=[],e.stories??=[];for(let f of e.features)f.stories??=[]}for(let s of allStories(b))s.story.criteria??=[],s.story.dependsOn??=[];return b}function sourcePath(f){return typeof f!="string"||!f.startsWith("source/")||f.includes("\\")||f.includes("\0")||f.split("/").some(x=>!x||x==="."||x==="..")?null:f}var STATUSES=new Set(["reading","questions","drafting","ready","approved","failed"]),KINDS=new Set(["pdf","docx","markdown","text","pasted"]),BREAKDOWN_FIELDS={title:"text",createdAt:"text",createdBy:"text",owner:"text",summary:"text",error:"text",questions:"objects",epics:"objects",decided:"objects",leftOut:"objects",next:"object",approved:"object"},QUESTION_FIELDS={id:"text",text:"text",options:"texts",inputOption:"text",thread:"objects"},DECIDED_FIELDS={id:"text",text:"text",item:"text",ticket:"text",changed:"object"},EPIC_FIELDS={key:"text",title:"text",goal:"text",summary:"text",features:"objects",stories:"objects",ref:"object",tracker:"object"},STORY_FIELDS={key:"text",title:"text",story:"text",criteria:"objects",size:"text",dependsOn:"texts",ref:"object",ticket:"text",tracker:"object",setsUp:"yes-no"};function first(list8,check){for(let x of list8){let wrong=check(x);if(wrong)return wrong}return null}function partProblem(x,fields){if(typeof x.key!="string"||!x.key)return"A part of the tree has no key.";if(typeof x.title!="string")return`${x.key} has no title.`;let wrong=wrongField(x,fields);return wrong&&`${x.key}: ${wrong}`}function storyProblem(x){return partProblem(x,STORY_FIELDS)??first(x.criteria??[],c=>["given","when","then"].every(k=>c[k]===void 0||typeof c[k]=="string")?null:`${x.key}: a "done when" is not in the right form.`)}function breakdownShapeProblem(b){if(!b.id)return"It has no id.";if(!isPlain(b.source))return"It does not say which document it came from.";if(!sourcePath(b.source.file))return"The document it names is not in its own source folder.";if(typeof b.source.name!="string"||!KINDS.has(String(b.source.kind)))return"It does not say what the document is.";if(typeof b.title!="string"||!b.title)return"It has no title.";if(!STATUSES.has(String(b.status)))return`"${String(b.status)}" is not a state of a breakdown.`;let wrong=wrongField(b,BREAKDOWN_FIELDS);return wrong||(b.source.extra!==void 0&&!Array.isArray(b.source.extra)?'"extra" is not in the right form.':first(b.questions??[],q5=>wrongField(q5,QUESTION_FIELDS)??(typeof q5.text=="string"?null:"A question has no words."))??first(b.decided??[],d=>wrongField(d,DECIDED_FIELDS))??first(b.leftOut??[],l=>typeof l.text=="string"?null:"A left out item has no words.")??first(b.epics??[],e=>partProblem(e,EPIC_FIELDS)??first(e.stories??[],storyProblem)??first(e.features??[],f=>partProblem(f,EPIC_FIELDS)??first(f.stories??[],storyProblem))))}var looked=new Map;function breakdownFileProblem(bp,key){let file3=breakdownFile(bp,key);if(!fs110.existsSync(file3))return null;let st=fs110.statSync(file3),stamp=`${st.mtimeMs}:${st.size}`,hit=looked.get(file3);if(hit?.stamp===stamp)return hit.problem;let problem2;try{if(hasMergeMarks(fs110.readFileSync(file3,"utf8")))problem2="It still has the marks of a merge in it.";else{let b=readYaml2(file3);problem2=isPlain(b)?breakdownShapeProblem(b):"It is not a breakdown."}}catch(err2){problem2=err2.message}return looked.set(file3,{stamp,problem:problem2}),problem2}function readBreakdown(bp,id2){let key=breakdownId(id2);if(!ID.test(key)||breakdownFileProblem(bp,key))return null;try{let b=readYaml2(breakdownFile(bp,key));return b?(b.id=key,b.source.extra&&(b.source.extra=b.source.extra.filter(f=>sourcePath(f))),fill(b)):null}catch{return null}}function breakdownProblem(bp,id2){let key=breakdownId(id2);return ID.test(key)&&breakdownFileProblem(bp,key)?recordProblemWords("breakdown",key):null}function requireBreakdown(bp,id2){let b=readBreakdown(bp,id2);if(!b)throw new BoardActionError(breakdownProblem(bp,id2)??`No breakdown ${breakdownId(id2)} in this project.`);return b}function brokenBreakdowns(bp){let dir=requirementsDir(bp);return fs110.existsSync(dir)?fs110.readdirSync(dir).filter(d=>ID.test(d)).flatMap(d=>{let detail=breakdownFileProblem(bp,d);return detail?[{kind:"breakdown",id:d,problem:recordProblemWords("breakdown",d),file:path102.relative(bp.root,breakdownFile(bp,d)),detail}]:[]}).sort((a,b)=>idNumber2(a.id)-idNumber2(b.id)):[]}function listBreakdowns(bp){let dir=requirementsDir(bp);return fs110.existsSync(dir)?fs110.readdirSync(dir).filter(d=>ID.test(d)).map(d=>readBreakdown(bp,d)).filter(b=>!!b).sort((a,b)=>idNumber2(a.id)-idNumber2(b.id)):[]}function writeBreakdown(bp,b){b.source.kind!=="pdf"&&dropPages(b),writeFile2(breakdownFile(bp,b.id),dumpYaml2(b))}function dropPages(b){let refs=[...b.questions.flatMap(q5=>q5.refs??[]),...b.leftOut.map(l=>l.ref),...b.epics.flatMap(e=>[e.ref,...e.stories.map(s=>s.ref),...e.features.flatMap(f=>[f.ref,...f.stories.map(s=>s.ref)])])];for(let r of refs)r&&delete r.page}function updateBreakdown(bp,id2,change){let b=requireBreakdown(bp,id2);return change(b),writeBreakdown(bp,b),b}function nextBreakdownId(bp){let taken=[requirementsDir(bp),path102.dirname(breakdownStateFile(bp,"BR-1"))].filter(d=>fs110.existsSync(d)).flatMap(d=>fs110.readdirSync(d).map(f=>idNumber2(f.replace(/\.yaml$/,""))));return`BR-${Math.max(0,...taken)+1}`}function removeBreakdown(bp,id2){fs110.rmSync(breakdownDir(bp,breakdownId(id2)),{recursive:!0,force:!0}),fs110.rmSync(breakdownStateFile(bp,breakdownId(id2)),{force:!0})}var STATE_FIELDS={job:"object",sessions:"object",build:"object",sync:"object",usage:"objects",budgetOk:"text"};function readBreakdownState(bp,id2){if(!ID.test(breakdownId(id2)))return{activity:[]};try{let raw=readYaml2(breakdownStateFile(bp,breakdownId(id2)));if(raw===void 0)return{activity:[]};if(!isPlain(raw)||wrongField(raw,STATE_FIELDS))throw new Error("not its shape");let s=raw;return{...s,activity:Array.isArray(s.activity)?s.activity:[]}}catch{return{activity:[],lost:!0}}}function writeBreakdownState(bp,id2,state){let{lost:_lost,...kept2}=state;writeFile2(breakdownStateFile(bp,breakdownId(id2)),dumpYaml2(kept2))}function updateBreakdownState(bp,id2,change){let s=readBreakdownState(bp,id2);return change(s),writeBreakdownState(bp,id2,s),s}var KEEP_ACTIVITY=200;function addBreakdownActivity(s,who4,text4){s.activity=[{at:now2(),who:who4,text:text4},...s.activity??[]].slice(0,KEEP_ACTIVITY)}function updateJob(bp,id2,change){return updateBreakdownState(bp,id2,s=>{s.job&&change(s.job,s)})}function jobBusy(job){return job?.status==="pending"||job?.status==="running"||job?.status==="stopping"}function breakdownJobs(bp,meta){let dir=path102.dirname(breakdownStateFile(bp,"BR-1"));if(!fs110.existsSync(dir))return[];let out22=[];for(let f of fs110.readdirSync(dir)){let id2=f.replace(/\.yaml$/,"");if(!ID.test(id2))continue;let job=readBreakdownState(bp,id2).job;if(!job||!jobBusy(job))continue;let b=readBreakdown(bp,id2);b&&isBreakdownMine(b,meta)&&out22.push({id:id2,job})}return out22}function ownerOfBreakdown(b){return b.owner?.trim()||b.createdBy}function isBreakdownMine(b,meta){return sameName(ownerOfBreakdown(b),meta.user?.name)}var PREFIX2={epic:"E",feature:"F",story:"S"};function itemKey(key){return key.trim().toUpperCase()}function nextKey(b,level){let used=new Set(allKeys(b)),key=`${PREFIX2[level]}-${b.next[level]}`;for(;used.has(key);)b.next[level]+=1,key=`${PREFIX2[level]}-${b.next[level]}`;return b.next[level]+=1,key}function allStories(b){return(b.epics??[]).flatMap(epic=>[...(epic.features??[]).flatMap(feature=>(feature.stories??[]).map(story2=>({story:story2,epic,feature}))),...(epic.stories??[]).map(story2=>({story:story2,epic}))])}function allKeys(b){return(b.epics??[]).flatMap(e=>[e.key,...(e.features??[]).map(f=>f.key)]).concat(allStories(b).map(s=>s.story.key))}function findItem(b,key){let k=itemKey(key);for(let epic of b.epics){if(epic.key===k)return{level:"epic",item:epic,epic,list:b.epics};for(let feature of epic.features){if(feature.key===k)return{level:"feature",item:feature,epic,feature,list:epic.features};let story3=feature.stories.find(s=>s.key===k);if(story3)return{level:"story",item:story3,epic,feature,story:story3,list:feature.stories}}let story2=epic.stories.find(s=>s.key===k);if(story2)return{level:"story",item:story2,epic,story:story2,list:epic.stories}}return null}function requireItem(b,key){let f=findItem(b,key);if(!f)throw new BoardActionError(`${b.id} has no ${itemKey(key)}.`);return f}function storiesIn(f){return f.level==="story"?[f.story]:f.level==="feature"?f.feature.stories:[...f.epic.features.flatMap(x=>x.stories),...f.epic.stories]}function redoHolding(b,job,key){if(!jobBusy(job)||job.step!=="redo")return null;if(job.item===b.id)return"the breakdown";let target=findItem(b,job.item??"");return target&&storiesIn(target).some(s=>s.key===itemKey(key))?target.item.key:null}function rewrittenStories(before,after){let words2=s=>JSON.stringify([s.title,s.story,s.criteria,s.size,s.dependsOn]),was=new Map(allStories(before).map(({story:story2})=>[story2.key,words2(story2)]));return allStories(after).filter(({story:story2})=>!story2.ticket&&was.get(story2.key)!==words2(story2)).map(({story:story2})=>story2.key)}function unreadStories(b,state){return allStories(b).filter(({story:story2})=>!story2.ticket&&state.rewritten?.includes(story2.key)).map(({story:story2})=>story2.key)}function wholeRedoCost(state){let lines3=(state.usage??[]).filter(l=>l.step==="spec"&&typeof l.costUsd=="number"),runs2=lines3.reduce((n,l)=>n+l.runs,0),each=runs2?lines3.reduce((n,l)=>n+(l.costUsd??0),0)/runs2:0;return Math.round(Math.max(1,each)*10)/10}function wholeRedoWords(state){return`This decision is not about one part, so the Analyst rewrites the whole breakdown. Stories that started stay as they are. That uses about $${wholeRedoCost(state).toFixed(2)} of AI at API prices.`}function placeWords(p){return[p.epic.title,p.feature?.title,p.story.key].filter(Boolean).join(" \u203A ")}function refWords(ref2){return ref2?[[ref2.section,ref2.title].filter(Boolean).join(" "),ref2.page?`page ${ref2.page}`:""].filter(Boolean).join(", "):""}function findLoop(b){let needs=new Map(allStories(b).map(({story:story2})=>[story2.key,story2.dependsOn])),state=new Map,trail=[],visit=k=>{if(state.get(k)==="done")return null;if(state.get(k)==="open")return[...trail.slice(trail.indexOf(k)),k];state.set(k,"open"),trail.push(k);for(let n of needs.get(k)??[]){if(!needs.has(n))continue;let loop=visit(n);if(loop)return loop}return trail.pop(),state.set(k,"done"),null};for(let k of needs.keys()){let loop=visit(k);if(loop)return loop}return null}function loopWords(loop){return`That makes a loop: ${loop.slice(0,-1).map((k,i)=>`${k} needs ${loop[i+1]}`).join(", ")}. Take one need away.`}function approveProblems(b){let stories=allStories(b);if(!stories.length)return[`${b.id} has no stories yet. Add one, or ask the Analyst to.`];let out22=[],keys=new Set(stories.map(s=>s.story.key));for(let{story:story2}of stories){story2.criteria.length||out22.push(`${story2.key} has no "done when" yet.`);for(let n of story2.dependsOn)keys.has(n)||out22.push(`${story2.key} needs ${n}, which is not in ${b.id}.`)}let loop=findLoop(b);return loop&&out22.push(loopWords(loop)),out22}function clockWords(iso2){let d=new Date(iso2),today=new Date;return d.toDateString()===today.toDateString()?`at ${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`:`on ${d.getDate()} ${d.toLocaleString("en",{month:"short"})}`}function andWords(xs){return xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs.at(-1)}`}function stopOf2(t){if(t.agentQuestion&&!t.agentQuestion.answer&&t.agentQuestion.kind)return t.agentQuestion.kind;let gate=t.waiting?.kind==="stuck"?t.stuck?.gate:void 0;return gate==="checks"||gate==="review"||gate==="proof"?"check":"other"}var STOP_WORDS2={gap:"missing in the BRD",clash:"two parts clash",check:"a check failed",other:""};function ticketsOf(bp,b){let out22=new Map;for(let{story:story2}of allStories(b)){if(!story2.ticket)continue;let t=readableTicket(bp,story2.ticket);t&&!t.deleting&&out22.set(story2.key,t)}return out22}function releasedIn(releases,id2){return releases.find(r=>r.status==="released"&&r.tickets.includes(id2))}function storyMerged(bp,s){if(!s.ticket)return!1;let t=readableTicket(bp,s.ticket);return!!(t&&isMerged(t))}function storyProgress(bp,b,state){let tickets=ticketsOf(bp,b),releases=listReleases(bp),merged2=key=>{let t=tickets.get(key);return!!(t&&isMerged(t))};return allStories(b).map(({story:story2})=>{let t=tickets.get(story2.key);if(!t){if(story2.ticket&&ticketProblem(bp,story2.ticket))return{key:story2.key,state:"needs-you",line:"Its file could not be read. An engineer can look at it",ticket:story2.ticket,stop:"other"};let waitsOn=story2.dependsOn.filter(k=>!merged2(k));if(waitsOn.length&&(b.status==="approved"||state.build?.on))return{key:story2.key,state:"waits",line:`Waits for ${andWords(waitsOn)} to merge`,waitsOn};if(story2.ticket)return{key:story2.key,state:"not-started",line:"Its ticket was deleted. Press Start to build it again"};if(!state.build?.on)return{key:story2.key,state:"not-started",line:"Not started"};if(state.rewritten?.includes(story2.key))return{key:story2.key,state:"waits",line:"Rewritten after you approved. Read it, then press Build continuously"};let redo=redoHolding(b,state.job,story2.key);return redo?{key:story2.key,state:"waits",line:`Waits for the Analyst to redo ${redo}`}:{key:story2.key,state:"next",line:"Next in line"}}let r=releasedIn(releases,t.id);if(r||t.stage==="done")return{key:story2.key,state:"released",line:r?`Released in ${r.version}`:"Released",ticket:t.id};if(isMerged(t))return{key:story2.key,state:"merged",line:t.mergedAt?`Merged ${clockWords(t.mergedAt)}`:"Merged",ticket:t.id};let wait=mergeWait(t);if(wait)return{key:story2.key,state:"needs-you",line:mergeWaitWords(wait),ticket:t.id,stop:"other"};if(t.waiting){let stop=stopOf2(t);return{key:story2.key,state:"needs-you",line:`Needs you: ${STOP_WORDS2[stop]||t.waiting.summary.replace(/^./,c=>c.toLowerCase())}`,ticket:t.id,stop}}return{key:story2.key,state:"building",line:t.status||"Building",ticket:t.id}})}var SIZE_FACTOR={S:.6,M:1,L:1.8},ROUGH_STORY={minutes:45,costUsd:4};function mean(xs){return xs.reduce((s,x)=>s+x,0)/xs.length}function estimateBreakdown(bp,b,payment,opts={}){let finished=(opts.tickets??listTickets(bp)).filter(t=>isMerged(t)&&!t.deleting),costs=finished.map(t=>totalOf(t.usage).costUsd).filter(c=>typeof c=="number"&&c>0),minutes4=finished.map(t=>(workSeconds(t)??0)/60).filter(m=>m>0),basedOn=finished.filter(t=>(totalOf(t.usage).costUsd??0)>0||(workSeconds(t)??0)>0).length,perCost=costs.length?mean(costs):ROUGH_STORY.costUsd,perMinutes=minutes4.length?mean(minutes4):ROUGH_STORY.minutes,left2=allStories(b).filter(({story:story2})=>!storyMerged(bp,story2)),factor=left2.reduce((s,{story:story2})=>s+(SIZE_FACTOR[story2.size]??1),0),parallel=opts.parallel??parallelLimit(readMeta(bp).parallel);return{stories:left2.length,...payment==="api-key"?{costUsd:Math.round(perCost*factor*100)/100}:{},minutes:Math.round(perMinutes*factor/parallel),parallel,basedOn,rough:basedOn===0,payment}}function decidedView(bp,b){let onTickets=new Map;for(let{story:story2}of allStories(b)){let t=story2.ticket?readableTicket(bp,story2.ticket):null;t?.decided?.length&&onTickets.set(t.id,t.decided.map(d=>({...d,item:d.item??story2.key,ticket:t.id})))}let shown2=new Set,out22=b.decided.map(d=>{let copy2=d.ticket?onTickets.get(d.ticket)?.find(x=>x.at===d.at&&x.text===d.text):void 0;return copy2?(shown2.add(copy2),copy2):d});for(let list8 of onTickets.values())out22.push(...list8.filter(x=>!shown2.has(x)));return out22}function sendTarget(bp){return sendTargetOf(bp)}function countsOf(b,progress2){return{epics:b.epics.length,features:b.epics.reduce((n,e)=>n+e.features.length,0),stories:progress2.length,merged:progress2.filter(p=>p.state==="merged"||p.state==="released").length,building:progress2.filter(p=>p.state==="building").length,needsYou:progress2.filter(p=>p.state==="needs-you").length}}function strandedWords(b,state,meta){return b.status!=="reading"&&b.status!=="drafting"||state.job||!isBreakdownMine(b,meta)?null:"squad-kit lost track of the Analyst's work on this computer, so it stopped. Your document is safe. Press Try again."}function breakdownView(bp,id2,payment){let read9=requireBreakdown(bp,id2),state=readBreakdownState(bp,read9.id),meta=readMeta(bp),stranded=strandedWords(read9,state,meta),b=stranded?{...read9,status:"failed",error:stranded}:read9,progress2=storyProgress(bp,b,state),tracker2=sendTarget(bp);return{...b,decided:decidedView(bp,b),state,estimate:estimateBreakdown(bp,b,payment),progress:progress2,counts:countsOf(b,progress2),...tracker2?{tracker:tracker2}:{},mine:isBreakdownMine(b,meta),problems:b.status==="ready"?approveProblems(b):[],...b.status==="ready"||b.status==="approved"?{wholeRedo:wholeRedoWords(state)}:{}}}async function viewBreakdown(bp,id2){return requireBreakdown(bp,id2),breakdownView(bp,id2,(await howPaid(bp)).payment)}function breakdownRows(bp){let meta=readMeta(bp);return listBreakdowns(bp).map(b=>{let state=readBreakdownState(bp,b.id),counts3=countsOf(b,storyProgress(bp,b,state));return{id:b.id,title:b.title,status:strandedWords(b,state,meta)?"failed":b.status,source:{name:b.source.name,kind:b.source.kind},counts:counts3,building:!!state.build?.on||counts3.building>0,createdAt:b.createdAt,owner:ownerOfBreakdown(b)}})}function breakdownWaits(bp,meta=readMeta(bp)){return listBreakdowns(bp).filter(b=>b.status==="questions"&&isBreakdownMine(b,meta)).map(b=>({b,open:b.questions.filter(q5=>!q5.answer).length})).filter(({open:open3})=>open3>0).map(({b,open:open3})=>({id:b.id,title:b.title,questions:open3,since:readBreakdownState(bp,b.id).activity[0]?.at??b.createdAt}))}function holdBreakdownJob(bp,id2,text4){updateJob(bp,id2,j=>{j.status==="pending"&&j.now!==text4&&(j.now=text4)})}var STORY_WORDS={epic:"Set up the product",goal:"squad-kit added this: the project has no code yet. This story picks the tools and makes a first version that starts. The other stories wait for it, so they all build on the same start.",title:"Set up the product",story:"As the team building this product, we want a first version that starts, so that every other story builds on the same tools.",criteria:[{given:"the project has no code yet",when:"this story is merged",then:"the product starts and shows that it is running"},{given:"the product has started",when:"the squad runs its checks",then:"they pass, so the next stories can build on them"}]},isFirstStory=s=>!!s.setsUp,firstStoryOf=b=>allStories(b).map(p=>p.story).find(isFirstStory);function withFirstStory(bp,x){if(!readMeta(bp).scan?.empty)return;let first2=firstStoryOf(x);if(!first2){if(listBreakdowns(bp).some(o=>o.id!==x.id&&firstStoryOf(o)))return;first2={key:nextKey(x,"story"),title:STORY_WORDS.title,story:STORY_WORDS.story,criteria:STORY_WORDS.criteria.map(c=>({...c})),size:"S",dependsOn:[],setsUp:!0};let epic={key:nextKey(x,"epic"),title:STORY_WORDS.epic,goal:STORY_WORDS.goal,features:[],stories:[first2]};x.epics.unshift(epic)}first2.dependsOn=[];for(let{story:story2}of allStories(x))story2!==first2&&!story2.dependsOn.length&&(story2.dependsOn=[first2.key])}function withoutFirstStory(b){if(!firstStoryOf(b))return b;let copy2=structuredClone(b);return takeOut(copy2),copy2}function takeOut(x){let first2=firstStoryOf(x);if(!first2)return;for(let e of x.epics){e.stories=e.stories.filter(s=>s!==first2);for(let f of e.features)f.stories=f.stories.filter(s=>s!==first2)}x.epics=x.epics.filter(e=>e.stories.length||e.features.length);let left2=new Set(allKeys(x));for(let{story:story2}of allStories(x))story2.dependsOn=story2.dependsOn.filter(n=>left2.has(n))}function firstCodeMerged(bp){let meta=readMeta(bp);if(meta.scan?.empty){writeMeta(bp,{...meta,scan:{...meta.scan,empty:!1,fromNothing:!0}});for(let b of listBreakdowns(bp)){let first2=firstStoryOf(b);first2&&!first2.ticket&&updateBreakdown(bp,b.id,takeOut)}}}function firstOpenTicket(bp,t){let oldest=listTickets(bp).filter(x=>!isMerged(x)&&!x.archived&&!x.deleting).reduce((a,x)=>!a||x.createdAt<a.createdAt?x:a,void 0);return oldest&&oldest.id!==t.id?oldest:void 0}var waitsForFirstWords=id2=>`Waits for ${id2} to merge: it picks the tools for this project. Starts by itself.`,waitsForFirst=status=>/^Waits for \S+ to merge: it picks the tools/.test(status);function itemId(t,tracker2){return t.source.type!==tracker2.name?null:t.source.id??tracker2.parseRef(t.source.ref??"")}function note(bp,id2,text4){try{updateTicket(bp,id2,t=>addActivity(t,"release",text4))}catch{}}function saidToEngineers(bp,id2,text4){writeLog({level:"warn",project:readMeta(bp).project,ticket:id2,text:text4})}async function tellTracker(bp,ticketId,event,tracker2=trackerFor(bp),again2=!1){let t;try{t=requireTicket(bp,ticketId)}catch{return!0}if(!tracker2||readMeta(bp).engine!=="live")return!0;if(event.kind==="file-bug")return fileBug(bp,t,tracker2,again2);let id2=itemId(t,tracker2);if(!id2)return!0;let ref2=tracker2.refOf(id2);try{switch(event.kind){case"started":{let state=await tracker2.moveTo(id2,"in-progress");await tracker2.comment(id2,`squad-kit started work on this as ticket **${t.id}**.`),state&¬e(bp,ticketId,`Moved ${ref2} to ${state} on ${tracker2.label}`);break}case"answered":{let answered=(t.questions??[]).filter(q5=>q5.answer);if(!answered.length)return!0;let items3=answered.map((q5,i)=>`${i+1}. ${q5.text}
|
|
1471
|
+
Instead of "${d.text}", which the squad decided on ${t0.id}.`,"ticket").id:became=openChange(proposeChange(bp,t0.id,`Instead of "${d.text}" (decided for you): ${want}`)).id,updateTicket(bp,t0.id,t=>{let e=t.decided?.find(x=>x.id===d.id);e&&(e.changed={text:want,by:meta.user.name,at:now2(),ticket:became}),addActivity(t,"you",`Changed what was decided for you (${d.id}): ${want}. It became ${/^c\d+$/.test(became)?"change":"ticket"} ${became}`)})}function and2(xs){return xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs.at(-1)}`}function acceptedLine(t){let g=t.gates,passed=[g?.checks==="pass"?"checks":"",g?.review==="pass"?"review":"",(t.proof??[]).some(p=>p.after)?"screenshots":""].filter(Boolean),head=passed.length?`${and2(passed).charAt(0).toUpperCase()}${and2(passed).slice(1)} passed: accepted by itself`:"Accepted by itself",notes=[g?.checks==="unchecked"?"No tests ran: you chose to go on without tests in this project":"",g?.checks==="skipped"?"The checks did not pass, and you chose to go on anyway":"",g?.review==="skipped"?"The review was still open, and you chose to go on anyway":""].filter(Boolean);return[head,...notes].join(". ")}function acceptNext(t,seconds2,speed){t.live=!0,t.stage="review",t.waiting=null,t.status=ACCEPTING,scheduleSquad(t,"accept",seconds2,speed)}async function acceptByItselfBeside(bp,id2,signal2){let t=requireTicket(bp,id2);return(x=>x.track!=="auto"||shipped(x)||!!openChange(x)||(x.proof??[]).some(p=>p.shows&&p.shows!=="met"&&p.shows!=="off-screen")||beforeDependency(bp,x))(t)?signal2?.aborted||t.stopRequested||t.deleting?t:acceptByItself(bp,id2):holdingSave(bp,t.id,async closing=>await saveBeforeAccept(bp,id2,closing,signal2)?acceptInTurn(bp,id2,closing,()=>{let now4=requireTicket(bp,id2);return signal2?.aborted||now4.stopRequested||now4.deleting||now4.takenOver||now4.chat?.open&&!now4.chat.beside?now4:acceptByItself(bp,id2)}):requireTicket(bp,id2))}function beforeDependency(bp,t){try{return refuseBeforeDependency(bp,t),!1}catch{return!0}}function acceptByItself(bp,id2){let t=requireTicket(bp,id2);if(t.track!=="auto")throw new BoardActionError(`${t.id} is not on the ${BUILDS_BY_ITSELF} track. Check the proof and press Looks right.`);if(shipped(t))return updateTicket(bp,id2,x=>x.squad?.phase==="accept"&&(x.squad=null));let c=openChange(t);if(c)return updateTicket(bp,id2,x=>{x.selfAccept={...x.selfAccept,waitsFor:c.id},x.live=!1,x.squad=null,x.waiting=null,x.stage="review",x.status=`Waits for change ${c.id}, then it is accepted by itself`,addActivity(x,"squad",`The checks, the review and the proof passed, but change ${c.id} is still open: it is accepted by itself once that change is settled`)});let open3=(t.proof??[]).filter(p=>p.shows&&p.shows!=="met"&&p.shows!=="off-screen");if(open3.length){let broken=open3.some(p=>p.shows==="broken");return updateTicket(bp,id2,x=>{x.live=!1,x.squad=null,x.stage="review",x.status="Proof ready to check",x.waiting={kind:"proof",summary:`Ready for you to check: a "done when" is ${broken?"not working":"not shown"} yet`},addActivity(x,"squad",broken?'Not accepted by itself: the screenshots show a "done when" is not working yet':'Not accepted by itself: the screenshots do not show a "done when" yet')})}try{refuseBeforeDependency(bp,t)}catch(err2){return updateTicket(bp,id2,x=>{x.live=!1,x.squad=null,x.stage="review",x.status="Proof ready to check",x.waiting={kind:"proof",summary:"The proof is ready for you to check"},addActivity(x,"squad",`Not accepted by itself: ${err2.message}`)})}return t.selfAccept?.waitsFor&&updateTicket(bp,id2,x=>delete x.selfAccept?.waitsFor),acceptProof(bp,requireTicket(bp,id2),acceptedLine(t))}function afterPullRequest(bp,id2,clash){if(clash){bringInAgain(bp,id2);return}let t=readTicket(bp,id2);t?.track!=="auto"||t.mergedAt||!t.pr||updateTicket(bp,id2,x=>{x.status=PR_WAITS,addActivity(x,"release",`Pull request ${x.pr?.id??""} waits for someone to approve it. That is a normal wait, not a failure`)})}function resumeAccept(bp,t){if(t.track!=="auto"||!t.selfAccept?.waitsFor||t.squad||t.waiting||t.takenOver||t.deleting||shipped(t)||openChange(t))return!1;let meta=readMeta(bp);return updateTicket(bp,t.id,x=>{delete x.selfAccept?.waitsFor,addActivity(x,"squad","The change it waited for is settled, so it is accepted by itself now"),acceptNext(x,.2,meta.engine==="live"?1:meta.speed)}),!0}function writeFile2(file3,text4){if(fs110.existsSync(file3)&&fs110.readFileSync(file3,"utf8")===text4)return;fs110.mkdirSync(path102.dirname(file3),{recursive:!0});let tmp=`${file3}.${process.pid}.tmp`;fs110.writeFileSync(tmp,text4,"utf8"),fs110.renameSync(tmp,file3)}function dumpYaml2(data2){return yaml16.dump(JSON.parse(JSON.stringify(data2)),{lineWidth:110,noRefs:!0})}var cache3=new Map;function readYaml2(file3){if(!fs110.existsSync(file3))return;let st=fs110.statSync(file3),stamp=`${st.mtimeMs}:${st.size}`,hit=cache3.get(file3);if(hit?.stamp===stamp)return structuredClone(hit.value);let value=yaml16.load(fs110.readFileSync(file3,"utf8"));return cache3.set(file3,{stamp,value}),structuredClone(value)}var ID=/^BR-(\d{1,9})$/;function breakdownId(id2){return id2.trim().toUpperCase()}function idNumber2(id2){return Number(ID.exec(id2)?.[1]??0)}function fill2(b){b.questions??=[],b.epics??=[],b.decided??=[],b.leftOut??=[],b.next??={epic:1,feature:1,story:1};for(let e of b.epics){e.features??=[],e.stories??=[];for(let f of e.features)f.stories??=[]}for(let s of allStories(b))s.story.criteria??=[],s.story.dependsOn??=[];return b}function sourcePath(f){return typeof f!="string"||!f.startsWith("source/")||f.includes("\\")||f.includes("\0")||f.split("/").some(x=>!x||x==="."||x==="..")?null:f}var STATUSES=new Set(["reading","questions","drafting","ready","approved","failed"]),KINDS=new Set(["pdf","docx","markdown","text","pasted"]),BREAKDOWN_FIELDS={title:"text",createdAt:"text",createdBy:"text",owner:"text",summary:"text",error:"text",questions:"objects",epics:"objects",decided:"objects",leftOut:"objects",next:"object",approved:"object"},QUESTION_FIELDS={id:"text",text:"text",options:"texts",inputOption:"text",thread:"objects"},DECIDED_FIELDS={id:"text",text:"text",item:"text",ticket:"text",changed:"object"},EPIC_FIELDS={key:"text",title:"text",goal:"text",summary:"text",features:"objects",stories:"objects",ref:"object",tracker:"object"},STORY_FIELDS={key:"text",title:"text",story:"text",criteria:"objects",size:"text",dependsOn:"texts",ref:"object",ticket:"text",tracker:"object",setsUp:"yes-no"};function first(list8,check){for(let x of list8){let wrong=check(x);if(wrong)return wrong}return null}function partProblem(x,fields){if(typeof x.key!="string"||!x.key)return"A part of the tree has no key.";if(typeof x.title!="string")return`${x.key} has no title.`;let wrong=wrongField(x,fields);return wrong&&`${x.key}: ${wrong}`}function storyProblem(x){return partProblem(x,STORY_FIELDS)??first(x.criteria??[],c=>["given","when","then"].every(k=>c[k]===void 0||typeof c[k]=="string")?null:`${x.key}: a "done when" is not in the right form.`)}function breakdownShapeProblem(b){if(!b.id)return"It has no id.";if(!isPlain(b.source))return"It does not say which document it came from.";if(!sourcePath(b.source.file))return"The document it names is not in its own source folder.";if(typeof b.source.name!="string"||!KINDS.has(String(b.source.kind)))return"It does not say what the document is.";if(typeof b.title!="string"||!b.title)return"It has no title.";if(!STATUSES.has(String(b.status)))return`"${String(b.status)}" is not a state of a breakdown.`;let wrong=wrongField(b,BREAKDOWN_FIELDS);return wrong||(b.source.extra!==void 0&&!Array.isArray(b.source.extra)?'"extra" is not in the right form.':first(b.questions??[],q5=>wrongField(q5,QUESTION_FIELDS)??(typeof q5.text=="string"?null:"A question has no words."))??first(b.decided??[],d=>wrongField(d,DECIDED_FIELDS))??first(b.leftOut??[],l=>typeof l.text=="string"?null:"A left out item has no words.")??first(b.epics??[],e=>partProblem(e,EPIC_FIELDS)??first(e.stories??[],storyProblem)??first(e.features??[],f=>partProblem(f,EPIC_FIELDS)??first(f.stories??[],storyProblem))))}var looked=new Map;function breakdownFileProblem(bp,key){let file3=breakdownFile(bp,key);if(!fs110.existsSync(file3))return null;let st=fs110.statSync(file3),stamp=`${st.mtimeMs}:${st.size}`,hit=looked.get(file3);if(hit?.stamp===stamp)return hit.problem;let problem2;try{if(hasMergeMarks(fs110.readFileSync(file3,"utf8")))problem2="It still has the marks of a merge in it.";else{let b=readYaml2(file3);problem2=isPlain(b)?breakdownShapeProblem(b):"It is not a breakdown."}}catch(err2){problem2=err2.message}return looked.set(file3,{stamp,problem:problem2}),problem2}function readBreakdown(bp,id2){let key=breakdownId(id2);if(!ID.test(key)||breakdownFileProblem(bp,key))return null;try{let b=readYaml2(breakdownFile(bp,key));return b?(b.id=key,b.source.extra&&(b.source.extra=b.source.extra.filter(f=>sourcePath(f))),fill2(b)):null}catch{return null}}function breakdownProblem(bp,id2){let key=breakdownId(id2);return ID.test(key)&&breakdownFileProblem(bp,key)?recordProblemWords("breakdown",key):null}function requireBreakdown(bp,id2){let b=readBreakdown(bp,id2);if(!b)throw new BoardActionError(breakdownProblem(bp,id2)??`No breakdown ${breakdownId(id2)} in this project.`);return b}function brokenBreakdowns(bp){let dir=requirementsDir(bp);return fs110.existsSync(dir)?fs110.readdirSync(dir).filter(d=>ID.test(d)).flatMap(d=>{let detail=breakdownFileProblem(bp,d);return detail?[{kind:"breakdown",id:d,problem:recordProblemWords("breakdown",d),file:path102.relative(bp.root,breakdownFile(bp,d)),detail}]:[]}).sort((a,b)=>idNumber2(a.id)-idNumber2(b.id)):[]}function listBreakdowns(bp){let dir=requirementsDir(bp);return fs110.existsSync(dir)?fs110.readdirSync(dir).filter(d=>ID.test(d)).map(d=>readBreakdown(bp,d)).filter(b=>!!b).sort((a,b)=>idNumber2(a.id)-idNumber2(b.id)):[]}function writeBreakdown(bp,b){b.source.kind!=="pdf"&&dropPages(b),writeFile2(breakdownFile(bp,b.id),dumpYaml2(b))}function dropPages(b){let refs=[...b.questions.flatMap(q5=>q5.refs??[]),...b.leftOut.map(l=>l.ref),...b.epics.flatMap(e=>[e.ref,...e.stories.map(s=>s.ref),...e.features.flatMap(f=>[f.ref,...f.stories.map(s=>s.ref)])])];for(let r of refs)r&&delete r.page}function updateBreakdown(bp,id2,change){let b=requireBreakdown(bp,id2);return change(b),writeBreakdown(bp,b),b}function nextBreakdownId(bp){let taken=[requirementsDir(bp),path102.dirname(breakdownStateFile(bp,"BR-1"))].filter(d=>fs110.existsSync(d)).flatMap(d=>fs110.readdirSync(d).map(f=>idNumber2(f.replace(/\.yaml$/,""))));return`BR-${Math.max(0,...taken)+1}`}function removeBreakdown(bp,id2){fs110.rmSync(breakdownDir(bp,breakdownId(id2)),{recursive:!0,force:!0}),fs110.rmSync(breakdownStateFile(bp,breakdownId(id2)),{force:!0})}var STATE_FIELDS={job:"object",sessions:"object",build:"object",sync:"object",usage:"objects",budgetOk:"text"};function readBreakdownState(bp,id2){if(!ID.test(breakdownId(id2)))return{activity:[]};try{let raw=readYaml2(breakdownStateFile(bp,breakdownId(id2)));if(raw===void 0)return{activity:[]};if(!isPlain(raw)||wrongField(raw,STATE_FIELDS))throw new Error("not its shape");let s=raw;return{...s,activity:Array.isArray(s.activity)?s.activity:[]}}catch{return{activity:[],lost:!0}}}function writeBreakdownState(bp,id2,state){let{lost:_lost,...kept2}=state;writeFile2(breakdownStateFile(bp,breakdownId(id2)),dumpYaml2(kept2))}function updateBreakdownState(bp,id2,change){let s=readBreakdownState(bp,id2);return change(s),writeBreakdownState(bp,id2,s),s}var KEEP_ACTIVITY=200;function addBreakdownActivity(s,who4,text4){s.activity=[{at:now2(),who:who4,text:text4},...s.activity??[]].slice(0,KEEP_ACTIVITY)}function updateJob(bp,id2,change){return updateBreakdownState(bp,id2,s=>{s.job&&change(s.job,s)})}function jobBusy(job){return job?.status==="pending"||job?.status==="running"||job?.status==="stopping"}function breakdownJobs(bp,meta){let dir=path102.dirname(breakdownStateFile(bp,"BR-1"));if(!fs110.existsSync(dir))return[];let out22=[];for(let f of fs110.readdirSync(dir)){let id2=f.replace(/\.yaml$/,"");if(!ID.test(id2))continue;let job=readBreakdownState(bp,id2).job;if(!job||!jobBusy(job))continue;let b=readBreakdown(bp,id2);b&&isBreakdownMine(b,meta)&&out22.push({id:id2,job})}return out22}function ownerOfBreakdown(b){return b.owner?.trim()||b.createdBy}function isBreakdownMine(b,meta){return sameName(ownerOfBreakdown(b),meta.user?.name)}var PREFIX2={epic:"E",feature:"F",story:"S"};function itemKey(key){return key.trim().toUpperCase()}function nextKey(b,level){let used=new Set(allKeys(b)),key=`${PREFIX2[level]}-${b.next[level]}`;for(;used.has(key);)b.next[level]+=1,key=`${PREFIX2[level]}-${b.next[level]}`;return b.next[level]+=1,key}function allStories(b){return(b.epics??[]).flatMap(epic=>[...(epic.features??[]).flatMap(feature=>(feature.stories??[]).map(story2=>({story:story2,epic,feature}))),...(epic.stories??[]).map(story2=>({story:story2,epic}))])}function allKeys(b){return(b.epics??[]).flatMap(e=>[e.key,...(e.features??[]).map(f=>f.key)]).concat(allStories(b).map(s=>s.story.key))}function findItem(b,key){let k=itemKey(key);for(let epic of b.epics){if(epic.key===k)return{level:"epic",item:epic,epic,list:b.epics};for(let feature of epic.features){if(feature.key===k)return{level:"feature",item:feature,epic,feature,list:epic.features};let story3=feature.stories.find(s=>s.key===k);if(story3)return{level:"story",item:story3,epic,feature,story:story3,list:feature.stories}}let story2=epic.stories.find(s=>s.key===k);if(story2)return{level:"story",item:story2,epic,story:story2,list:epic.stories}}return null}function requireItem(b,key){let f=findItem(b,key);if(!f)throw new BoardActionError(`${b.id} has no ${itemKey(key)}.`);return f}function storiesIn(f){return f.level==="story"?[f.story]:f.level==="feature"?f.feature.stories:[...f.epic.features.flatMap(x=>x.stories),...f.epic.stories]}function redoHolding(b,job,key){if(!jobBusy(job)||job.step!=="redo")return null;if(job.item===b.id)return"the breakdown";let target=findItem(b,job.item??"");return target&&storiesIn(target).some(s=>s.key===itemKey(key))?target.item.key:null}function rewrittenStories(before,after){let words2=s=>JSON.stringify([s.title,s.story,s.criteria,s.size,s.dependsOn]),was=new Map(allStories(before).map(({story:story2})=>[story2.key,words2(story2)]));return allStories(after).filter(({story:story2})=>!story2.ticket&&was.get(story2.key)!==words2(story2)).map(({story:story2})=>story2.key)}function unreadStories(b,state){return allStories(b).filter(({story:story2})=>!story2.ticket&&state.rewritten?.includes(story2.key)).map(({story:story2})=>story2.key)}function wholeRedoCost(state){let lines3=(state.usage??[]).filter(l=>l.step==="spec"&&typeof l.costUsd=="number"),runs2=lines3.reduce((n,l)=>n+l.runs,0),each=runs2?lines3.reduce((n,l)=>n+(l.costUsd??0),0)/runs2:0;return Math.round(Math.max(1,each)*10)/10}function wholeRedoWords(state){return`This decision is not about one part, so the Analyst rewrites the whole breakdown. Stories that started stay as they are. That uses about $${wholeRedoCost(state).toFixed(2)} of AI at API prices.`}function placeWords(p){return[p.epic.title,p.feature?.title,p.story.key].filter(Boolean).join(" \u203A ")}function refWords(ref2){return ref2?[[ref2.section,ref2.title].filter(Boolean).join(" "),ref2.page?`page ${ref2.page}`:""].filter(Boolean).join(", "):""}function findLoop(b){let needs=new Map(allStories(b).map(({story:story2})=>[story2.key,story2.dependsOn])),state=new Map,trail=[],visit=k=>{if(state.get(k)==="done")return null;if(state.get(k)==="open")return[...trail.slice(trail.indexOf(k)),k];state.set(k,"open"),trail.push(k);for(let n of needs.get(k)??[]){if(!needs.has(n))continue;let loop=visit(n);if(loop)return loop}return trail.pop(),state.set(k,"done"),null};for(let k of needs.keys()){let loop=visit(k);if(loop)return loop}return null}function loopWords(loop){return`That makes a loop: ${loop.slice(0,-1).map((k,i)=>`${k} needs ${loop[i+1]}`).join(", ")}. Take one need away.`}function approveProblems(b){let stories=allStories(b);if(!stories.length)return[`${b.id} has no stories yet. Add one, or ask the Analyst to.`];let out22=[],keys=new Set(stories.map(s=>s.story.key));for(let{story:story2}of stories){story2.criteria.length||out22.push(`${story2.key} has no "done when" yet.`);for(let n of story2.dependsOn)keys.has(n)||out22.push(`${story2.key} needs ${n}, which is not in ${b.id}.`)}let loop=findLoop(b);return loop&&out22.push(loopWords(loop)),out22}function clockWords(iso2){let d=new Date(iso2),today=new Date;return d.toDateString()===today.toDateString()?`at ${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`:`on ${d.getDate()} ${d.toLocaleString("en",{month:"short"})}`}function andWords(xs){return xs.length<=1?xs[0]??"":`${xs.slice(0,-1).join(", ")} and ${xs.at(-1)}`}function stopOf2(t){if(t.agentQuestion&&!t.agentQuestion.answer&&t.agentQuestion.kind)return t.agentQuestion.kind;let gate=t.waiting?.kind==="stuck"?t.stuck?.gate:void 0;return gate==="checks"||gate==="review"||gate==="proof"?"check":"other"}var STOP_WORDS2={gap:"missing in the BRD",clash:"two parts clash",check:"a check failed",other:""};function ticketsOf(bp,b){let out22=new Map;for(let{story:story2}of allStories(b)){if(!story2.ticket)continue;let t=readableTicket(bp,story2.ticket);t&&!t.deleting&&out22.set(story2.key,t)}return out22}function releasedIn(releases,id2){return releases.find(r=>r.status==="released"&&r.tickets.includes(id2))}function storyMerged(bp,s){if(!s.ticket)return!1;let t=readableTicket(bp,s.ticket);return!!(t&&isMerged(t))}function storyProgress(bp,b,state){let tickets=ticketsOf(bp,b),releases=listReleases(bp),merged2=key=>{let t=tickets.get(key);return!!(t&&isMerged(t))};return allStories(b).map(({story:story2})=>{let t=tickets.get(story2.key);if(!t){if(story2.ticket&&ticketProblem(bp,story2.ticket))return{key:story2.key,state:"needs-you",line:"Its file could not be read. An engineer can look at it",ticket:story2.ticket,stop:"other"};let waitsOn=story2.dependsOn.filter(k=>!merged2(k));if(waitsOn.length&&(b.status==="approved"||state.build?.on))return{key:story2.key,state:"waits",line:`Waits for ${andWords(waitsOn)} to merge`,waitsOn};if(story2.ticket)return{key:story2.key,state:"not-started",line:"Its ticket was deleted. Press Start to build it again"};if(!state.build?.on)return{key:story2.key,state:"not-started",line:"Not started"};if(state.rewritten?.includes(story2.key))return{key:story2.key,state:"waits",line:"Rewritten after you approved. Read it, then press Build continuously"};let redo=redoHolding(b,state.job,story2.key);return redo?{key:story2.key,state:"waits",line:`Waits for the Analyst to redo ${redo}`}:{key:story2.key,state:"next",line:"Next in line"}}let r=releasedIn(releases,t.id);if(r||t.stage==="done")return{key:story2.key,state:"released",line:r?`Released in ${r.version}`:"Released",ticket:t.id};if(isMerged(t))return{key:story2.key,state:"merged",line:t.mergedAt?`Merged ${clockWords(t.mergedAt)}`:"Merged",ticket:t.id};let wait=mergeWait(t);if(wait)return{key:story2.key,state:"needs-you",line:mergeWaitWords(wait),ticket:t.id,stop:"other"};if(t.waiting){let stop=stopOf2(t);return{key:story2.key,state:"needs-you",line:`Needs you: ${STOP_WORDS2[stop]||t.waiting.summary.replace(/^./,c=>c.toLowerCase())}`,ticket:t.id,stop}}return{key:story2.key,state:"building",line:t.status||"Building",ticket:t.id}})}var SIZE_FACTOR={S:.6,M:1,L:1.8},ROUGH_STORY={minutes:45,costUsd:4};function mean(xs){return xs.reduce((s,x)=>s+x,0)/xs.length}function estimateBreakdown(bp,b,payment,opts={}){let finished=(opts.tickets??listTickets(bp)).filter(t=>isMerged(t)&&!t.deleting),costs=finished.map(t=>totalOf(t.usage).costUsd).filter(c=>typeof c=="number"&&c>0),minutes4=finished.map(t=>(workSeconds(t)??0)/60).filter(m=>m>0),basedOn=finished.filter(t=>(totalOf(t.usage).costUsd??0)>0||(workSeconds(t)??0)>0).length,perCost=costs.length?mean(costs):ROUGH_STORY.costUsd,perMinutes=minutes4.length?mean(minutes4):ROUGH_STORY.minutes,left2=allStories(b).filter(({story:story2})=>!storyMerged(bp,story2)),factor=left2.reduce((s,{story:story2})=>s+(SIZE_FACTOR[story2.size]??1),0),parallel=opts.parallel??parallelLimit(readMeta(bp).parallel);return{stories:left2.length,...payment==="api-key"?{costUsd:Math.round(perCost*factor*100)/100}:{},minutes:Math.round(perMinutes*factor/parallel),parallel,basedOn,rough:basedOn===0,payment}}function decidedView(bp,b){let onTickets=new Map;for(let{story:story2}of allStories(b)){let t=story2.ticket?readableTicket(bp,story2.ticket):null;t?.decided?.length&&onTickets.set(t.id,t.decided.map(d=>({...d,item:d.item??story2.key,ticket:t.id})))}let shown2=new Set,out22=b.decided.map(d=>{let copy2=d.ticket?onTickets.get(d.ticket)?.find(x=>x.at===d.at&&x.text===d.text):void 0;return copy2?(shown2.add(copy2),copy2):d});for(let list8 of onTickets.values())out22.push(...list8.filter(x=>!shown2.has(x)));return out22}function sendTarget(bp){return sendTargetOf(bp)}function countsOf(b,progress2){return{epics:b.epics.length,features:b.epics.reduce((n,e)=>n+e.features.length,0),stories:progress2.length,merged:progress2.filter(p=>p.state==="merged"||p.state==="released").length,building:progress2.filter(p=>p.state==="building").length,needsYou:progress2.filter(p=>p.state==="needs-you").length}}function strandedWords(b,state,meta){return b.status!=="reading"&&b.status!=="drafting"||state.job||!isBreakdownMine(b,meta)?null:"squad-kit lost track of the Analyst's work on this computer, so it stopped. Your document is safe. Press Try again."}function breakdownView(bp,id2,payment){let read9=requireBreakdown(bp,id2),state=readBreakdownState(bp,read9.id),meta=readMeta(bp),stranded=strandedWords(read9,state,meta),b=stranded?{...read9,status:"failed",error:stranded}:read9,progress2=storyProgress(bp,b,state),tracker2=sendTarget(bp);return{...b,decided:decidedView(bp,b),state,estimate:estimateBreakdown(bp,b,payment),progress:progress2,counts:countsOf(b,progress2),...tracker2?{tracker:tracker2}:{},mine:isBreakdownMine(b,meta),problems:b.status==="ready"?approveProblems(b):[],...b.status==="ready"||b.status==="approved"?{wholeRedo:wholeRedoWords(state)}:{}}}async function viewBreakdown(bp,id2){return requireBreakdown(bp,id2),breakdownView(bp,id2,(await howPaid(bp)).payment)}function breakdownRows(bp){let meta=readMeta(bp);return listBreakdowns(bp).map(b=>{let state=readBreakdownState(bp,b.id),counts3=countsOf(b,storyProgress(bp,b,state));return{id:b.id,title:b.title,status:strandedWords(b,state,meta)?"failed":b.status,source:{name:b.source.name,kind:b.source.kind},counts:counts3,building:!!state.build?.on||counts3.building>0,createdAt:b.createdAt,owner:ownerOfBreakdown(b)}})}function breakdownWaits(bp,meta=readMeta(bp)){return listBreakdowns(bp).filter(b=>b.status==="questions"&&isBreakdownMine(b,meta)).map(b=>({b,open:b.questions.filter(q5=>!q5.answer).length})).filter(({open:open3})=>open3>0).map(({b,open:open3})=>({id:b.id,title:b.title,questions:open3,since:readBreakdownState(bp,b.id).activity[0]?.at??b.createdAt}))}function holdBreakdownJob(bp,id2,text4){updateJob(bp,id2,j=>{j.status==="pending"&&j.now!==text4&&(j.now=text4)})}var STORY_WORDS={epic:"Set up the product",goal:"squad-kit added this: the project has no code yet. This story picks the tools and makes a first version that starts. The other stories wait for it, so they all build on the same start.",title:"Set up the product",story:"As the team building this product, we want a first version that starts, so that every other story builds on the same tools.",criteria:[{given:"the project has no code yet",when:"this story is merged",then:"the product starts and shows that it is running"},{given:"the product has started",when:"the squad runs its checks",then:"they pass, so the next stories can build on them"}]},isFirstStory=s=>!!s.setsUp,firstStoryOf=b=>allStories(b).map(p=>p.story).find(isFirstStory);function withFirstStory(bp,x){if(!readMeta(bp).scan?.empty)return;let first2=firstStoryOf(x);if(!first2){if(listBreakdowns(bp).some(o=>o.id!==x.id&&firstStoryOf(o)))return;first2={key:nextKey(x,"story"),title:STORY_WORDS.title,story:STORY_WORDS.story,criteria:STORY_WORDS.criteria.map(c=>({...c})),size:"S",dependsOn:[],setsUp:!0};let epic={key:nextKey(x,"epic"),title:STORY_WORDS.epic,goal:STORY_WORDS.goal,features:[],stories:[first2]};x.epics.unshift(epic)}first2.dependsOn=[];for(let{story:story2}of allStories(x))story2!==first2&&!story2.dependsOn.length&&(story2.dependsOn=[first2.key])}function withoutFirstStory(b){if(!firstStoryOf(b))return b;let copy2=structuredClone(b);return takeOut(copy2),copy2}function takeOut(x){let first2=firstStoryOf(x);if(!first2)return;for(let e of x.epics){e.stories=e.stories.filter(s=>s!==first2);for(let f of e.features)f.stories=f.stories.filter(s=>s!==first2)}x.epics=x.epics.filter(e=>e.stories.length||e.features.length);let left2=new Set(allKeys(x));for(let{story:story2}of allStories(x))story2.dependsOn=story2.dependsOn.filter(n=>left2.has(n))}function firstCodeMerged(bp){let meta=readMeta(bp);if(meta.scan?.empty){writeMeta(bp,{...meta,scan:{...meta.scan,empty:!1,fromNothing:!0}});for(let b of listBreakdowns(bp)){let first2=firstStoryOf(b);first2&&!first2.ticket&&updateBreakdown(bp,b.id,takeOut)}}}function firstOpenTicket(bp,t){let oldest=listTickets(bp).filter(x=>!isMerged(x)&&!x.archived&&!x.deleting).reduce((a,x)=>!a||x.createdAt<a.createdAt?x:a,void 0);return oldest&&oldest.id!==t.id?oldest:void 0}var waitsForFirstWords=id2=>`Waits for ${id2} to merge: it picks the tools for this project. Starts by itself.`,waitsForFirst=status=>/^Waits for \S+ to merge: it picks the tools/.test(status);function itemId(t,tracker2){return t.source.type!==tracker2.name?null:t.source.id??tracker2.parseRef(t.source.ref??"")}function note(bp,id2,text4){try{updateTicket(bp,id2,t=>addActivity(t,"release",text4))}catch{}}function saidToEngineers(bp,id2,text4){writeLog({level:"warn",project:readMeta(bp).project,ticket:id2,text:text4})}async function tellTracker(bp,ticketId,event,tracker2=trackerFor(bp),again2=!1){let t;try{t=requireTicket(bp,ticketId)}catch{return!0}if(!tracker2||readMeta(bp).engine!=="live")return!0;if(event.kind==="file-bug")return fileBug(bp,t,tracker2,again2);let id2=itemId(t,tracker2);if(!id2)return!0;let ref2=tracker2.refOf(id2);try{switch(event.kind){case"started":{let state=await tracker2.moveTo(id2,"in-progress");await tracker2.comment(id2,`squad-kit started work on this as ticket **${t.id}**.`),state&¬e(bp,ticketId,`Moved ${ref2} to ${state} on ${tracker2.label}`);break}case"answered":{let answered=(t.questions??[]).filter(q5=>q5.answer);if(!answered.length)return!0;let items3=answered.map((q5,i)=>`${i+1}. ${q5.text}
|
|
1472
1472
|
**${q5.answer.choice.replace(/:$/,"")}${q5.answer.value?`: ${q5.answer.value}`:""}**`).join(`
|
|
1473
1473
|
`);await tracker2.comment(id2,`**Questions answered** by ${readMeta(bp).user.name}
|
|
1474
1474
|
|
|
@@ -1948,7 +1948,7 @@ body.dark ul{background:#0004}
|
|
|
1948
1948
|
|
|
1949
1949
|
`),proof:()=>proofPrompt(meta,t,"after",ctx)},agent=readAgent(bp,role);return{ticket:{id:t.id,title:t.title},prompts:STEPS[role].map(s=>({step:s.id,name:s.name,prompt:text4[s.id]?.()??"",team:agentInstructions(agent,s.id)}))}}import fs167 from"fs";import path159 from"path";import yaml22 from"js-yaml";var MAX_FOLDER_FILES=200,MAX_FOLDER_FILE_BYTES=2e5,DEEPEST=6,NOT_LOOKED_IN=new Set(["node_modules",".git","dist","build","out","vendor","target","coverage",".next",".venv","venv","__pycache__",".ssh",".aws",".gnupg",".azure",".kube",".docker"]),HOLDS_KEYS=/^\.env|\.(pem|key|p12|pfx|jks|keystore|crt|cer|gpg)$|^id_(rsa|dsa|ecdsa|ed25519)|secret|credential|password|token|\.npmrc$|\.netrc$/i,OURS="squad-kit already does this, and two processes would fight.",OUR_HEADING=/\b(contract|workflow|code review|review process|reviews?|release|deploy|commit|push|pull request|merge request|branch|merging)\b/i,OUR_STEP=/\b(git (commit|push|merge|rebase|checkout)|gh pr|open (a|your( own)?) (pull request|pr)\b|create (a|your( own)?) (pull request|pr)\b)/i,OUR_SCRIPT=/^(pre|post)?(release|publish|deploy|commit|push|version)\b/i,NEVER_A_GATE=/^(pre|post)?(install|prepare|start|dev|serve|watch|preview)\b/i;function textOf2(file3){if(fs167.statSync(file3).size>MAX_FOLDER_FILE_BYTES)return null;let data2=fs167.readFileSync(file3);if(data2.includes(0))return null;let text4=data2.toString("utf8");return text4.includes("\uFFFD")?null:text4}function sectionsOf2(text4,title2){let out22=[],at3={name:title2,body:""};for(let line3 of text4.split(/\r?\n/)){let h=/^#{1,6}\s+(.+?)\s*#*$/.exec(line3);h?(at3.body.trim()&&out22.push(at3),at3={name:h[1],body:""}):at3.body+=`${line3}
|
|
1950
1950
|
`}return at3.body.trim()&&out22.push(at3),out22}var boxesOf=body11=>[...body11.matchAll(/^\s*[-*]\s+\[[ xX]\]\s+(.+)$/gm)].map(m=>m[1].trim()),namedFrom=file3=>path159.basename(file3).replace(/\.[^.]+$/,"");function fromMarkdown(source2,text4,asRules){return sectionsOf2(text4,namedFrom(source2)).flatMap((s,i)=>{let key=`${source2}#${i}`,name=s.name.slice(0,MAX_TYPED_CHARS).trim(),body11=scrubSecrets(s.body.trim()),boxes=boxesOf(s.body),ours=OUR_HEADING.test(s.name)||OUR_STEP.test(body11)?{out:OURS}:{};if(boxes.length>=2){let questions=boxes.slice(0,MAX_CHECKLIST_QUESTIONS).map(q5=>scrubSecrets(q5).slice(0,MAX_TYPED_CHARS));return[{key,kind:"checklist",name,source:source2,questions,...ours}]}if(!asRules)return[];let tooLong=body11.length>MAX_RULE_CHARS?{out:`Too long for one rule (the limit is ${MAX_RULE_CHARS} characters). You can add a shorter one by hand.`}:{};return[{key,kind:"rule",name,source:source2,text:body11,...tooLong,...ours}]})}function fromOurFormat(source2,text4){let doc;try{doc=yaml22.load(text4)??{}}catch{return[]}return Array.isArray(doc.additions)?doc.additions.flatMap((a,i)=>{let name=typeof a?.name=="string"?a.name.trim().slice(0,MAX_TYPED_CHARS):"",key=`${source2}#${i}`;return name?a.kind==="rule"&&typeof a.text=="string"?[{key,kind:"rule",name,source:source2,text:scrubSecrets(a.text),...a.text.length>MAX_RULE_CHARS?{out:`Too long for one rule (the limit is ${MAX_RULE_CHARS} characters).`}:{}}]:a.kind==="checklist"&&Array.isArray(a.questions)?[{key,kind:"checklist",name,source:source2,questions:a.questions.filter(q5=>typeof q5=="string").slice(0,MAX_CHECKLIST_QUESTIONS).map(q5=>scrubSecrets(q5))}]:a.kind==="gate"&&typeof a.command=="string"?[{key,kind:"gate",name,source:source2,command:a.command.trim(),when:["checks","merge","release"].includes(a.when)?a.when:"checks"}]:[]:[]}):[]}function runnerFor(root){return fs167.existsSync(path159.join(root,"pnpm-lock.yaml"))?s=>`pnpm run ${s}`:fs167.existsSync(path159.join(root,"yarn.lock"))?s=>`yarn ${s}`:s=>`npm run ${s}`}function fromPackage(root,source2,text4,commands){let scripts;try{scripts=JSON.parse(text4).scripts??{}}catch{return[]}let run7=runnerFor(root),own2=[commands.test,commands.lint,commands.build].filter(Boolean).join(" ");return Object.keys(scripts).filter(s=>!NEVER_A_GATE.test(s)).map(s=>({key:`${source2}#${s}`,kind:"gate",name:s,source:source2,command:run7(s),when:"checks",...OUR_SCRIPT.test(s)?{out:OURS}:new RegExp(`\\b${s.replace(/[^\w:-]/g,"")}\\b`).test(own2)?{out:"squad-kit already runs this with the checks."}:{}}))}function lookInFolder(folder,have,commands){let root;try{if(root=fs167.realpathSync(path159.resolve(folder)),!fs167.statSync(root).isDirectory())throw new Error("not a folder")}catch{throw new Error("squad-kit can't open that folder. Check the path and try again.")}let finds=[],left2=0,seen=0,looked3=0,more3=!1,walk3=(dir,depth)=>{let entries;try{entries=fs167.readdirSync(dir,{withFileTypes:!0})}catch{left2+=1;return}for(let e of entries.sort((a,b)=>a.name.localeCompare(b.name))){if((looked3+=1)>MAX_FOLDER_FILES*50&&(more3=!0),more3)return;let at3=path159.join(dir,e.name),source2=path159.relative(root,at3).split(path159.sep).join("/");if(HOLDS_KEYS.test(e.name)||NOT_LOOKED_IN.has(e.name)){left2+=1;continue}let real2=at3;if(e.isSymbolicLink()){try{real2=fs167.realpathSync(at3)}catch{real2=""}if(!real2||!`${real2}${path159.sep}`.startsWith(`${root}${path159.sep}`)){left2+=1;continue}}let stat;try{stat=fs167.statSync(real2)}catch{left2+=1;continue}if(stat.isDirectory()){depth<DEEPEST&&!e.isSymbolicLink()&&walk3(at3,depth+1);continue}if(!stat.isFile())continue;let isScriptCheck=source2==="scripts/check",isMarkdown=/\.(md|mdc|markdown)$/i.test(e.name),isOurs=source2==="harness.yaml"||source2===".squad/harness.yaml";if(!isMarkdown&&!isOurs&&!isScriptCheck&&source2!=="package.json"&&!(/(^|\/)rules\//.test(source2)&&/\.txt$/i.test(e.name)))continue;if((seen+=1)>MAX_FOLDER_FILES){more3=!0;return}let text4=null;try{text4=textOf2(real2)}catch{}if(text4===null){left2+=1;continue}isOurs?finds.push(...fromOurFormat(source2,text4)):source2==="package.json"?finds.push(...fromPackage(root,source2,text4,commands)):isScriptCheck?finds.push({key:source2,kind:"gate",name:"scripts/check",source:source2,command:"./scripts/check",when:"checks"}):finds.push(...fromMarkdown(source2,text4,/^(AGENTS|CLAUDE)\.md$/.test(e.name)||/(^|\/)rules\//.test(source2)))}};walk3(root,0);let named=new Set(have.map(a=>`${a.kind}\0${a.name.trim().toLowerCase()}`));return{name:path159.basename(root),path:root,finds:finds.map(f=>named.has(`${f.kind}\0${f.name.trim().toLowerCase()}`)?{...f,already:!0}:f).slice(0,MAX_FOLDER_FILES),left:left2,more:more3}}function additionOf(f,pick2){return f.kind==="rule"?{kind:"rule",name:f.name,agents:[pick2.agent||"all"],text:f.text??""}:f.kind==="checklist"?{kind:"checklist",name:f.name,agent:pick2.agent==="tester"?"tester":"reviewer",questions:f.questions??[]}:{kind:"gate",name:f.name,command:f.command??"",when:pick2.when??f.when??"checks",on_fail:pick2.on_fail??"stop",...pick2.part?{part:pick2.part}:{}}}var SEEN_DAYS=30,WEEK_DAYS=7,ONE_DAY=864e5,STOP_LINES={checks:/^Checks failed: /,weakened:/^Stopped before the checks: the tests were made weaker/,"no-tests":/^Not checked: .*Asked you once/,guard:/^A check (needed a file outside|tried to use Docker)/,"test-change":/^squad-kit keeps the current way of testing/,review:/^(Asked for \d+ fix|Still asks for )/,proof:/^(Could not take the screenshots|No screenshots yet)/,"bad-proof":/^The screenshots did not pass squad-kit's check/,"not-shown":/^(The screenshots do not show .* clearly yet|Could not show .* on a screen after)/,broken:/^(The screenshots show .* not working yet|Still not working after)/,window:/^The \w+ asked .*squad-kit refused\.$/,questions:/did not bother you with it\.$|^The \w+ asked a technical question/,sandbox:/^(Reaching|Reading|Writing) .* was blocked: |^A download from \S+ was not allowed for this project/,lane:/^The (Builder|Tester|Reviewer|project's checks) changed .*squad-kit put |^Stopped the \w+: /,hook:/^The project's own check said no/,merge:/^(Could not merge into |It clashes with newer changes)|clashes with newer changes in \S+, so it left/,cap:/^Paused: /,budget:/^Paused before the next step/},PASSED2={checks:t=>t.gates?.checks==="pass"?lineAt(t,/^All checks pass/):void 0,"test-change":t=>lineAt(t,/^squad-kit keeps the new way of testing/),review:t=>t.gates?.review==="pass"?lineAt(t,/^Approved: /):void 0,proof:t=>t.gates?.proof==="pass"&&t.proof?.length?t.history?.filter(h=>h.step==="proof").at(-1)?.to??t.activity[0]?.at:void 0,merge:t=>t.mergedAt},DID_LINES={spec:[/^Approved the spec/,"You approved"],plan:[/^Approved the (design|plan)/,"You approved"],"looks-right":[/^Checked the proof\. Looks right/,"You accepted"],ask:[/^Asked you: /,"Asked on"],chat:[/^Started talking it through/,"You talked on"],decisions:[/\. Decision saved as /,"Saved decisions on"],depends:[/so the build waits for you to choose/,"Waited for you on"],change:[/^Approved change \S+ \(big\)/,"You approved a big change on"],"reviewer-floor":[/as strong as the Builder\)$/,"Raised the model on"],"also-changed":[/^Also changed, not in the plan/,"Named something on"],"off-screen":[/^"Done when" \d+ is not on a screen/,"Marked on"],"api-proof":[/^squad-kit asked .* and kept the answer/,"Used on"],"take-over":[/^Took it over/,"You took over"],"pull-request":[/^Opened pull request/,"Opened on"],checkout:[/^It goes in once your own copy/,"Waited on"],release:[/^Released in /,"Sent out"],watchdog:[/^Looks stuck: /,"Flagged"],"step-up":[/^Runs on .*after \d+ failed tr/,"Moved up on"],"plain-words":[/^The Tester wrote something technical/,"Kept words out of the proof on"],interrupted:[/^Carried on.* after (a restart|the computer slept)|^The computer slept|^Resumed after sleep/,"Carried on after it on"],blips:[/^The connection or the AI service had a problem/,"Waited on"],leftovers:[/^Stopped (a program|\d+ programs) left running/,"Stopped programs on"]},HIT_LINES={MAX_CHECK_TRIES:/^Checks failed: .*\(try (\d+) of \1\)$/,MAX_REVIEW_ROUNDS:/^Still asks for /,MAX_RETAKES:/^Could not show .* on a screen after \d+ more tries/,MAX_PROOF_ROUNDS:/^Still not working after /,MAX_BASE_TRIES:/came in \d+ times, so it waits for you$/,REFUSALS_TO_STOP:/^Stopped: .*kept asking for things it may not do/,MISTAKES_TO_STOP:/^Stopped: .*kept using squad-kit's tools the wrong way/,TECHNICAL_TRIES:/^Stopped: .*kept asking a technical question/,BEFORE_SHOTS_MS:/^Stopped the before screenshots after/},WAIT_ROW={questions:"questions",spec:"spec",plan:"plan",proof:"looks-right",release:"release",ask:"ask",yours:"take-over",chat:"chat",change:"change",depends:"depends"},lineAt=(t,re)=>t.activity.find(a=>re.test(a.text))?.at,stopRow=s=>s.kind??s.gate??(s.budget?"budget":"ways-out"),overrideRow=o=>o.added?"gate-cant-run":o.weakened?.length?"weakened":o.gate==="hooks"?"hook":o.gate;function needRow(t){let gate=t.waiting?.kind==="stuck"?t.stuck?.added?.find(g=>g.id!==HARNESS_FILE_GATE):void 0;return gate?`add:${gate.id}`:t.waiting?t.waiting.kind==="stuck"?t.stuck?stopRow(t.stuck):"ways-out":WAIT_ROW[t.waiting.kind]:t.quiet?.kind==="stuck"?"watchdog":t.outdated?.length?"decisions":t.testChange?.ask?"test-change":"change"}var storyWord=n=>`${n} ${n===1?"story":"stories"}`;function harnessSeen(bp,meta,now4=Date.now()){let from=now4-SEEN_DAYS*ONE_DAY,week=now4-WEEK_DAYS*ONE_DAY,fresh=at3=>!!at3&&Date.parse(at3)>=from,rows2={},weekIds={},hits={},did={},said2={},tickets=listTickets(bp).filter(t=>!t.deleting),counted=0,wentPast=0;for(let t of tickets){let lines3=t.activity.filter(a=>fresh(a.at));(lines3.length||fresh(t.createdAt))&&(counted+=1);let stops=new Map,stop=(row3,at3)=>{fresh(at3)&&!(stops.get(row3)>at3)&&stops.set(row3,at3)};for(let[row3,re]of Object.entries(STOP_LINES))stop(row3,lines3.find(a=>re.test(a.text))?.at);t.stuck&&t.waiting?.kind==="stuck"&&stopRow(t.stuck)!=="ways-out"&&stop(stopRow(t.stuck),t.activity[0]?.at??t.createdAt);for(let o of t.overrides??[])stop(overrideRow(o),o.at);for(let r of t.added??[])r.stopped&&stop(`add:${r.id}`,r.stopped);t.overrides?.some(o=>fresh(o.at))&&(wentPast+=1);for(let[row3,at3]of stops){let r=rows2[row3]??={};r.stopped=(r.stopped??0)+1,Date.parse(at3)>=week&&(r.week=(r.week??0)+1,(weekIds[row3]??=[]).push(t.id)),(!r.last||at3>r.last.at)&&(r.last={id:t.id,title:t.title,at:at3})}for(let[row3,at3]of Object.entries(PASSED2).map(([row4,of])=>[row4,of(t)]))!stops.has(row3)&&fresh(at3)&&((rows2[row3]??={}).passed=(rows2[row3].passed??0)+1);for(let r of(t.added??[]).filter(x=>fresh(x.at))){let row3=`add:${r.id}`,s=said2[row3]??={would:0,warned:0},no=r.answers?.some(a=>a.answer==="no");(r.result==="would-stop"||no&&r.state==="try")&&(s.would+=1),r.result==="warned"&&(s.warned+=1),r.result==="cant-run"&&!(s.cant?.at>r.at)&&(s.cant={id:t.id,title:t.title,at:r.at}),(r.result==="passed"||r.answers?.length&&r.answers.every(a=>a.answer==="yes"))&&!stops.has(row3)&&((rows2[row3]??={}).passed=(rows2[row3].passed??0)+1)}for(let[row3,[re]]of Object.entries(DID_LINES))lines3.some(a=>re.test(a.text))&&(did[row3]=(did[row3]??0)+1);for(let[limit,re]of Object.entries(HIT_LINES)){let at3=lines3.find(a=>re.test(a.text))?.at;at3&&!(hits[limit]?.at>at3)&&(hits[limit]={id:t.id,title:t.title,at:at3})}}for(let row3 of Object.keys(PASSED2))rows2[row3]&&(rows2[row3]={passed:0,stopped:0,...rows2[row3]});for(let[row3,s]of Object.entries(said2)){let words2=[s.would?`Would have stopped ${storyWord(s.would)}`:"",s.warned?`Warned on ${storyWord(s.warned)}`:"",s.cant?`Could not run on ${s.cant.id}`:""].filter(Boolean);rows2[row3]={passed:0,stopped:0,...rows2[row3],...words2.length?{did:words2.join(". ")}:{}}}for(let[row3,n]of Object.entries(did))(rows2[row3]??={}).did=`${DID_LINES[row3][1]} ${storyWord(n)}`;wentPast&&((rows2["ways-out"]??={}).did=`You went past a check on ${storyWord(wentPast)}`);let pause2=readPause();pause2&&pause2.project===meta.project&&fresh(pause2.since)&&((rows2["usage-limit"]??={}).did=`Last waited ${pause2.since.slice(0,10)}`);let needs=inbox(bp).filter(t=>!t.deleting&&!t.archived).map(t=>({id:t.id,summary:t.waiting?.summary??t.quiet?.detail??t.status,row:needRow(t),proof:t.waiting?.kind==="proof"}));return{stories:counted,rows:rows2,weekIds,hits,needs}}import fs168 from"fs";import path160 from"path";var MAX_LEAD_NOTES=40,NOTE_CHARS=1200;function notesFile(bp){return path160.join(leadDir(bp),"notes.json")}function readNotes(bp){try{let raw=JSON.parse(fs168.readFileSync(notesFile(bp),"utf8"));return Array.isArray(raw)?raw.filter(n=>!!n&&typeof n.text=="string"&&typeof n.question=="string"):[]}catch{return[]}}function scrubbed(bp,text4){let known=[...new Set([...knownSecrets([bp.secretsFile,path160.join(bp.squadDir,"secrets.yaml"),bp.consoleFile]),...localFileSecrets(projectLocalFiles(bp))])].sort((a,b)=>b.length-a.length);return scrubSecrets(text4,known)}function saveNote(bp,note2){let clean4={...note2,text:scrubbed(bp,note2.text.trim()).slice(0,NOTE_CHARS),at:new Date().toISOString()},same5=sameQuestion(note2.question),all=[...readNotes(bp).filter(n=>!(note2.about==="sign-in"&&n.about==="sign-in")&&sameQuestion(n.question)!==same5),clean4].slice(-MAX_LEAD_NOTES);fs168.mkdirSync(leadDir(bp),{recursive:!0});let file3=notesFile(bp),tmp=`${file3}.${process.pid}.tmp`;return fs168.writeFileSync(tmp,`${JSON.stringify(all,null,1)}
|
|
1951
|
-
`,{mode:384}),fs168.renameSync(tmp,file3),clean4}function sameQuestion(text4){return(isArabic(text4)?plainArabic(text4):text4.toLowerCase()).replace(/[^\p{L}\p{N}]+/gu," ").trim()}var SIGN_IN_EN=/\b(sign(?:ing)?[\s-]?in|log(?:ging)?[\s-]?in|login|log on|sign on|password|credentials|test account|test user|which user|what (?:should|do) i type|get in(?:to)?|account to use)\b/i,SIGN_IN_AR=/(تسجيل الدخول|سجل الدخول|ادخل|الدخول|كلمه المرور|كلمه السر|الحساب|اسم المستخدم)/;function asksSignIn(text4){return isArabic(text4)?SIGN_IN_AR.test(plainArabic(text4)):SIGN_IN_EN.test(text4)}function noteFor(bp,question){let newest=readNotes(bp).reverse(),same5=sameQuestion(question);return newest.find(n=>sameQuestion(n.question)===same5)??(asksSignIn(question)?newest.find(n=>n.about==="sign-in")??null:null)}function signInNote(bp){return readNotes(bp).reverse().find(n=>n.about==="sign-in")?.text}function lineStages(){let you=sub=>({kind:"you",sub}),squad=sub=>({kind:"squad",sub}),skip=sub=>({kind:"skip",sub}),on=(full,express=full,auto=full)=>({full,express,auto}),stage=(id2,name,about2,when2,gates)=>({id:id2,name,about:about2,on:when2,gates}),g=(id2,name,what,fail,more3)=>({id:id2,name,what,...fail?{fail}:{},...more3}),y=(id2,name,what,fail,more3)=>g(id2,name,what,fail,{you:!0,...more3}),ON_ITS_OWN2=["express","auto"],floor=QUALITIES.filter(q5=>QUALITY_PRESETS[q5].reviewerFloor).map(q5=>q5.charAt(0).toUpperCase()+q5.slice(1)).join(" and ");return[stage("setup","Setup","Once for each project, before any story.",on(you("You confirm")),[g("tool-ready","Your AI tool is here","It must be installed, or nothing runs.","It says what to do."),g("first-run","A first run on untouched code","Your tests and app run once in squad-kit's own copy. What already fails is noted, so the Builder leaves other screens alone.","The setup goes on. A story must still pass its checks."),y("overview","You confirm what it found","The Architect explains your project in plain words.","You say what is wrong, and it reads again."),g("project-rules","Your project's own rules","Every agent first reads your rule files and your team's wiki. squad-kit runs the process, and those files cover the code.")]),stage("breakdown","Breakdown","Your requirements become a tree of stories. Skip it if you start from one story.",on({...skip("Optional"),off:!0},{...skip("Optional"),off:!0},you("You approve")),[g("brd-questions","Only real gaps and clashes",`At most ${MAX_QUESTIONS} questions on a document, where it leaves a gap or disagrees with itself.`,"A technical one goes back to the agent."),y("brd-approve","You approve the breakdown",'Nothing is built or sent to your tracker before that. Every story needs its "done when".',"Approve is refused and says what to fix."),g("brd-queue","Stories start in order","The next starts when what it needs is merged and a place is free.","Your budget, your AI limit or two stopped stories hold the queue, with a reason."),g("first-story","A project with no code starts with one story","squad-kit adds Set up the product: it picks the tools and makes a first version. Nothing else starts until it merges, so every story builds on the same start.","Another ticket waits, and says which one it waits for.")]),stage("story","Story","The Analyst reads your story and asks only what it leaves open.",on(you("You answer"),skip("No questions"),skip("Approved before")),[g("questions","Only business questions reach you","The Analyst asks about gaps and clashes. Asks about settings, secrets or code history are refused before you see them.",void 0,{skip:ON_ITS_OWN2}),y("spec","You approve what gets built",'The story and its "done when" list wait for you.',"You ask for a change and it is rewritten.",{skip:ON_ITS_OWN2,note:{auto:"Approved with the breakdown."}}),y("ask","An agent can stop and ask you","The Architect, Builder or Reviewer can ask one plain question. It passes the same filter.",void 0,{note:{auto:"Only for a critical gap or a clash. Small choices go under Decided for you."}}),y("chat","Talk it through","With any agent that worked on the story. It waits while you talk.")]),stage("plan","Plan","The Architect reads your project and writes the plan in plain words.",on(you("You approve"),skip("On its own"),skip("On its own")),[y("plan","You approve the plan","Nothing is built before you approve how.","You ask for a change and it is rewritten.",{skip:ON_ITS_OWN2}),g("decisions","Saved decisions stay rules","Each plan says which ones it follows or replaces."),y("depends","Stories that depend on each other","A story that needs unmerged work waits: build on it, build anyway, or wait."),y("change","A big change comes back to you","The Builder pauses, and the updated design waits for your yes.")]),stage("build","Build","The Builder writes the code in small steps, in a locked room.",on(squad("Builder")),[g("sandbox","The guard","Each story has its own copy. No agent can read your keys or sign-ins. The Builder and Tester can't read .env files either, and reach only the websites the project's network level allows.","Blocked, with a line on the story. A website it may not reach stops nothing: the work goes on without it."),g("guard","A second guard for your project","Your tests, app, setup and release run locked in too, with keys hidden and Docker closed.","A card: an engineer allows the file, or the plan changes."),g("flutter-stamps","Flutter's own version stamps","Flutter writes its version in a few small files of its own folder on every run. Those files, and nothing else of Flutter, are open to the squad's commands."),g("xcode","iPhone builds",OUTSIDE_THE_GUARD_WORDS),g("phone-builds","squad-kit's own phone builds","For the pictures, Start and Try it yourself, squad-kit builds the app for its phone itself, outside the guard: Xcode for the iPhone, Gradle or Flutter for Android, in one shared Gradle home of its own. What the build changes in the story's files goes back, so the story's changes stay the Builder's. An Expo app gets its phone folder made for the build, and no commit ever takes it.","An app that does not build: a card, and you decide."),g("lane","Files no agent can change","squad-kit's folder, AI instruction files, commit hooks, files an engineer locked. No commit or branch change, and a push is refused.","A file goes back, with a line. A moved branch stops the step."),g("local-files","Test values, not your own","A file marked local gets values kept for stories. No agent can read it."),g("env-file","A part's env file","squad-kit gives the app the story's values from it, then its own database, Redis and the app's address on top."),y("new-tests","New tests",`The Builder writes tests for every "done when". On a new story you can turn off Write new tests: your project's own checks, the review and the proof still run, and the story says "Built without new tests". An engineer can keep it on for every story.`,void 0,{note:{auto:"Always on: a story from the breakdown writes its tests."}})]),stage("checks","Checks","squad-kit runs your project's own checks. No agent is asked if they passed.",on(squad("squad-kit")),[g("checks","Your project's own checks","Test, lint and build, plus scripts/check when your project has one. The Builder's word doesn't count. A failed check runs once more, so a flaky test doesn't stop the story. For the tests, the failed ones run on their own first: one that still fails fails the check at once, and if they pass, all the tests run once more and decide.",`The Builder fixes it, up to ${MAX_CHECK_TRIES} tries. Then a card.`),g("setup-again","Set up again when the code changes","New packages or database changes came into a story's copy, from main or from the Builder: its setup runs again before the checks and before the app starts, so the app never runs on an old database.","A database of the story's own that is out of step is made again, and the setup runs once more. Then the Builder fixes it."),g("old-tests","An old bug is not the story's","A failed test in a file the story didn't change runs on its own on main's code. If it fails there too, it is an old bug: the story goes on, the checks say so, and squad-kit makes one new story to fix that test, which waits on the board until you start it. A story only goes past a test that main already fails.","If it passes on main, the Builder fixes it, as for any failed check."),g("weakened","Weakened tests","Tests deleted, switched off or reduced, test settings changed, or a change to what a must-pass gate of your team runs.","It stops. Only you can go past it, with a reason on the record."),g("no-tests","No tests, no green","No tests never means a green pass. Lint, build and the app starting must still pass.","Asked once per project: add tests, or go on without."),g("test-change","Test commands stay strong","A new test command stays only if it passes and runs at least as many tests.","If squad-kit can't tell, you choose."),g("tool-missing","A missing tool is not the Builder's","A check that fails only because a tool is missing (the Android SDK, Java, Xcode, a program the command runs) says nothing about the code. squad-kit looks at this computer's tools again and runs the checks once more: no try counts, and the Builder is never asked.","Still missing: the story waits for it with the Tools page's line and goes on by itself once it is there, or a card says what to install."),g("gate-cant-run","A check that can't run","One of your team's gates that can't run, or a file of additions that can't be read, says nothing about the code. It never counts against the story, and it is never dropped without a word.","It retries by itself, then a card: try again, let an engineer look, or go on with a reason. On Builds by itself it waits for a person.")]),stage("review","Review","A Reviewer that never saw the build reads the Builder's work.",on(squad("Reviewer")),[g("review","A fresh Reviewer",'It checks every "done when" and saved decision, and looks for bugs, security holes and missing or weakened tests. It changes nothing.',`Back to the Builder, up to ${MAX_REVIEW_ROUNDS} rounds. Then you decide.`),g("reviewer-floor","Never weaker than the Builder",`On ${floor}, its model is at least as strong.`),g("also-changed","Also changed","It names what the change touches beyond the story and plan.","It doesn't block. It shows at Your check.")]),stage("proof","Proof",'The Tester runs your real app and shows each "done when" working.',on(squad("Tester")),[g("proof","Pictures of the real app",`Up to ${6} for each "done when".`,"None: a card, and you decide."),g("bad-proof","squad-kit checks each picture",`Real, at least ${MIN_SIZE.width} by ${MIN_SIZE.height}, not blank, taken in this step.`,"Back to the Tester once. Then a card."),g("not-shown",'A second look at every "done when"',"A separate run judges its pictures: met, not working, not shown, or not on a screen.",`Not shown: up to ${2} retakes. Then a card.`),g("broken","Not working yet","Pictures that show it does not work never pass.",`Back to the Builder ${"once"}. Then a card.`),g("off-screen","Not on a screen",'Takes two voices: the Tester and the check. If every "done when" ends that way, you decide.'),g("api-proof","Answers from your API","Work with no screen can show a saved answer from this story's own API."),g("phone-pictures","Phone apps on a real phone","A phone app's pictures come from squad-kit's own hidden phone, each named with its device: its iPhone and iPad on a Mac, or its Android phone and tablet (Google's own emulator). An app for both uses the iPhone on a Mac when its tools are ready, else Android; Settings can choose. One phone runs at a time, and it shuts down after 10 minutes unused. With its tools missing, pictures of the web version stand in, and the story says so.","With no web version either, it waits for the tools and goes on once they are there. An app that does not build or a phone that does not start: a card, and you decide."),g("window","Fixed tools only","The Tester uses squad-kit's own tools, on this story's app only.",`Refused. ${REFUSALS_TO_STOP} refusals stop it.`)]),stage("yours","Your check","You look at the proof, try the app, and ask anything.",on(you("You decide"),you("You decide"),skip("On its own")),[y("looks-right","You look and decide","Where to look, Try it yourself, Ask a question, Something's wrong. Nothing merges before Looks right.","Something's wrong sends it back with your words.",{skip:["auto"],note:{auto:"It merges when the checks, review and proof pass. If not, it comes to you."}}),y("take-over","Take it over","Work in the story's copy yourself. The squad keeps off until you give it back."),y("outside","Work in another AI tool","Copy a prompt, and a conversation in any AI tool carries on from this step. The squad keeps off until it comes back. It keeps a label on its record, and Check runs squad-kit's own checks, review and proof on it. What is spent there is yours, and the guard and the limits do not apply there."),g("accept-order","First things first","A story built on another is accepted after it, and an open change is settled first.","Accept says what comes first.")]),stage("merge","Merge","The story joins the rest of your code.",on(squad("squad-kit")),[g("hook","Your check on every save","Your commit hooks run on every save, inside the guard.","The Builder fixes it, or you save without it and say why."),g("merge","A clean merge","The latest code comes in first. A clash is never forced.",`The newest code comes in by itself, up to ${MAX_BASE_TRIES} times. Then a card.`),g("pull-request","Pull requests",`squad-kit opens and merges them. Waiting for an approver is a normal wait. One merged on the code host is noticed within ${OUTSIDE_LOOK_MS/6e4} minutes, or at once with Check again.`,"Try again, or let the release merge the work."),g("checkout","Your own copy is safe","With changes in the same files, the merge waits. It never overwrites them.")]),stage("release","Release","You choose what goes out and when. Every track stops here.",on(you("You decide")),[y("release","Release stays yours","Deploying never happens by itself.","If it can't be sent, it isn't marked as sent. Send again finishes it."),y("leave-out","Leave a story out","It stays in your code and goes out with the next release."),g("release-clash","Nothing moves on a clash","Every merge is tried first. What the others rely on goes first.","A clash: one card, nothing moved. A failed part: the ones after it wait."),g("release-lock","One release at a time","One cut off by a restart is finished or rolled back on the next start.")])]}function alwaysOnRows(){let a=(id2,name,what,then)=>({id:id2,name,what,...then?{then}:{}});return[a("watchdog","Nothing hangs in silence","When a step goes quiet, the watchdog looks at what is really running, to tell busy from stuck.","You get a card with ways out."),a("usage-limit","Your AI limit is respected","At the limit every project waits together, then carries on from the same step.","Never a failed try, and you can end the wait yourself."),a("step-up","A stronger model after a failed try","The Builder moves up, unless someone chose an exact model.",`On Thrifty, after ${QUALITY_PRESETS.thrifty.stepUp.after} failed tries.`),a("cap","A run can't go on forever","Each run has a most it can use. See Limits.","It pauses with its work kept. Give it more doubles it."),a("budget","Your budget",`If you pay per use, a story and a month can have one. At ${WARN_AT*100}% the board says so.`,"At 100% the story or the Analyst stops between two steps, never mid-step."),a("ways-out","Every stop has a way out","Try again, do it yourself, go around it, or stop. Only you can go past a failed check.","Your reason stays on the story, the pull request and the release notes."),a("plain-words","Plain words only","Raw errors never reach you.","The exact error goes to the engineer details and the log."),a("interrupted","An interruption is not a failure","A restart, a Stop or a sleeping computer never counts as a failed try.","The step carries on where it was."),a("blips","A lost connection is not a failure","The step runs again by itself.","If it does not pass, you get a card."),a("leftovers","Nothing left behind","A program a step leaves running is stopped. Programs of your own are never touched.","On Windows it stops only what it started itself."),a("product-lock","One story runs the product at a time","Stories that need the whole product take turns, and say who they wait for."),a("room-for-copy","Room on the disk","Before a first copy of the code, squad-kit compares the free space with the size of the project.","Too little: a card says how much it needs and how much is free. Free up some space, then press Try again."),a("save-trouble","A full disk holds the work","If squad-kit can't save its files, the project waits and says so on every page.","It carries on once saving works."),a("board-lock","Your board is locked","It opens only on your own computer's address, with your link's key.","A request from any other website is refused."),a("report","Reports leave your secrets out",NEVER2),a("lead","The Squad lead changes nothing on its own","It reads and looks: the stories, the code, squad-kit's records and guide, and the app in its own copy with its own database, never yours. Everything else is a card you press, and some things it never does, whoever asks.","A card it may not offer is refused in squad-kit's own words. Only your own messages count, never what it reads."),{id:"tool-license",name:"A tool maker's license is yours to accept",what:"squad-kit gets Google's Android phone tools only after you read Google's license, in full, and press Accept. It never accepts a license for you.",then:"Get it waits until you do.",you:!0},a("phone-tools","squad-kit's own phone tools","Get it looks at the free room first (about 20 GB for Android) and says so. A download that gets no new bytes for 2 minutes is stopped and tried once more. An Android SDK of yours is used as it is when it has everything, and never changed.","Too little room, or a download that stops twice: a plain line on the Tools page, and nothing half installed. Press Get it again.")]}function limitRows(meta){let row3=(group,show3=String)=>(limit,name,what="")=>{let[id2,n]=Object.entries(limit)[0];return{id:id2,group,n:show3(n),name,what}},tries=row3("tries"),pics=row3("pictures"),asks=row3("asks"),time2=row3("time",ms2=>`${ms2/6e4} min`),size2=row3("sizes"),bytes=row3("sizes",b=>`${b/1024/1024} MB`);return[tries({MAX_CHECK_TRIES},"Tries at the checks","Then a card."),tries({MAX_RUNS_ON_MAIN},"Runs of a failed test on main's code","It stops at the first that fails there too."),size2({MAX_ALONE},"Failed tests run on their own first","With more, all the tests run again at once."),size2({MAX_OLD},"Failed tests run on main's code","With more, the Builder fixes them, as before."),tries({MAX_REVIEW_ROUNDS},"Review rounds","Then you decide."),tries({MAX_RETAKES:2},"Retakes",`For a "done when" the pictures don't show clearly.`),tries({MAX_PROOF_ROUNDS:1},"Returns after a broken picture","To the Builder. Then you decide."),tries({MAX_BASE_TRIES},"Clash retries","While the code keeps moving."),tries({BLIP_WAITS_MIN:BLIP_WAITS_MIN.length},"Waits after a lost connection",`After ${BLIP_WAITS_MIN.join(", ")} minutes. Then a card.`),pics({MAX_PICTURES_PER_DONE_WHEN:6},'Pictures for each "done when"',"So each part can show."),pics({MAX_PICTURES:40},"Pictures in one proof step","Counting each one taken again."),pics({MAX_SHOTS_PER_CALL},"Pictures in one call","More has to be split over calls."),pics({MAX_API_CALLS},"Calls to your API in one proof","Only this story's own app."),asks({REFUSALS_TO_STOP},"Refusals","Forbidden asks, then the agent is stopped."),asks({MISTAKES_TO_STOP},"Calls in the wrong shape","Harmless, but past this the step ends."),asks({MAX_STUCK},"Times a run can say it is stuck","After that, squad-kit says it already tried."),asks({TECHNICAL_TRIES:2},"Tries for a technical question","Back to its agent, never to you. Then it stops."),asks({MAX_QUESTIONS},"Questions on one document","Only real gaps and clashes."),asks({MAX_DECIDED},"Small choices in one answer","Logged under Decided for you."),time2({WORDS_STEP_MS},"Analyst and Architect","One run of words: questions, a spec, a design."),time2({BUILD_STEP_MS},"Builder","One run of building or fixing."),time2({REVIEW_STEP_MS},"Reviewer","One round, or one look at the pictures."),time2({PROOF_STEP_MS},"Tester"),time2({BRD_READ_MS},"Reading a document","A long document gets twice its usual read."),time2({LONGEST_READ_MS},"Reading a very long document","The most, however long it is."),time2({BRD_WRITE_MS},"Writing a breakdown"),time2({CHECK_MS},"One check"),time2({CHECK_SILENT_MS},"A silent check","It prints nothing for this long."),time2({APP_WAIT_MS},"Waiting for your app"),time2({TURN_WAIT_MS},"Tests and your app in one copy","One waits for the other, then it stops and says so."),time2({KEPT_APP_MS},"Your app kept for the proof","After the review, while nothing uses it. Then it stops."),time2({SETUP_MS},"Setting up your app"),time2({DOCKER_WAIT_MS},"Starting in Docker"),time2({BEFORE_SHOTS_MS},"Before pictures","Beside the build, holding nothing up."),time2({ON_MAIN_MS},"Failed tests on main's code","All their runs together. Then the Builder fixes them, as before."),time2({HOOK_MS},"Your check on a save"),time2({NETWORK_GIT_MS},"Reaching your code host","One push, pull or fetch."),time2({PIPELINE_MS},"Your after-release step","Inside the guard, after a release."),time2({MAX_LANDING_WAIT_MS},"Waiting for a story to merge","Before the newest code comes in."),time2({OUTSIDE_LOOK_MS},"Looking for merges on your code host","Pull requests merged there, outside squad-kit. Check again looks now."),time2({WAIT_LIMIT_MS},"Waiting for the product"),time2({TRY_YIELDS_AFTER_MS},"Trying a story yourself","Once another story needs it."),time2({QUIET_MS},"Quiet before a look","Then the watchdog looks."),time2({LONG_MS},"Busy before Taking long","Working, with no progress."),time2({RETRY_MS},"Looking again at your AI limit","When your AI tool doesn't say when it ends."),size2({parallel:parallelLimit(meta?.parallel)},"Stories at once","Across your projects. Change it in Settings."),size2({MAX_PARTS},"Parts of one product"),size2({MAX_UPLOADS},"Files on one story"),size2({MAX_RULES},"Rules your team can add","Remove one to add another."),size2({MAX_RULE_CHARS},"Characters in one rule","Agents read every rule at every step, so each stays short."),size2({MAX_CHECKLIST_QUESTIONS},"Questions on one checklist"),size2({MAX_TYPED_CHARS},"Characters in a name, a question or a command"),size2({MAX_FOLDER_FILES},"Files looked at in a folder you bring in","The rest are left out, and the sheet says so."),row3("sizes",b=>`${b/1e3} KB`)({MAX_FOLDER_FILE_BYTES},"Largest file read in that folder","A bigger one is left out."),bytes({MAX_UPLOAD_BYTES},"One file"),bytes({MAX_NOTE_BYTES},"One voice or video note"),time2({LEAD_ANSWER_MS:48e4},"The Squad lead, one answer","Looking at the app in its own copy included."),row3("asks",usd4=>`$${usd4}`)({MAX_LEAD_DAY_USD:10},"What the Squad lead may use in a day","At API prices. Then it answers again the next day; its notes still answer at once."),asks({MAX_LEAD_CARDS:4},"Cards in one answer of the Squad lead"),pics({MAX_LEAD_PICTURES:4},"Pictures in one answer of the Squad lead","Taken in its own copy of the app."),size2({MAX_LEAD_NOTES},"Notes the Squad lead keeps","The oldest go first."),size2({MAX_QUESTION_CHARS:4e3},"Characters in one question to the Squad lead")]}var HARNESS_CAP_NAME={analyst:"Analyst",architect:"Architect",builder:"Builder","builder.fix":"Builder, fixes",reviewer:"Reviewer",tester:"Tester",chat:"Talking a story through",lead:"The Squad lead, one question"},LEAD_MAY_READ=["The board: every story, where it stands and what happens on it",`Each story: its spec, plan, "done when", activity, the Reviewer's notes, its proof and how the Tester went through the app`,"The decisions, the requirements, the releases, what the squad used, and the Tools page","squad-kit's log lines for a story, with secrets taken out","squad-kit's own guide and the Harness rules","The code, read only, and its history. Never your keys, sign-ins or .env files","The app, in its own copy of main with its own database: pictures, pages and your API. Never your app or your real data"];function leadRows(meta,bp){let caps=QUALITY_PRESETS[meta?.quality??DEFAULT_QUALITY].caps.lead,always=(meta?.lead?.always??[]).filter(a=>ALLOWABLE.includes(a));return{reads:LEAD_MAY_READ,levels:leadCan().levels,always:{words:ALWAYS_WORDS,can:ALLOWABLE.map(id2=>({id:id2,name:ACTIONS2[id2].name})),on:bp?always:[]},limits:[{name:"One question",n:`$${caps.spendUsd} or ${caps.turns} turns`,what:"From the Quality setting, like every step."},{name:"One day",n:`$${10}`,what:"At API prices. Its notes and the board still answer at once after it."},{name:"One answer",n:`${48e4/6e4} min`,what:"Looking at the app in its own copy included."}],model:'The balanced level by default. A person can change it for themselves, under the name "lead".'}}var HARNESS_PRIVATE_NAME={".ssh":"SSH keys",".aws":"AWS sign-in",".gnupg":"GPG keys",".config/gh":"GitHub sign-in",".azure":"Azure sign-in",".kube":"Kubernetes sign-in",".docker/config.json":"Docker sign-in",".netrc":"Saved website passwords",".git-credentials":"Saved git passwords"},HARNESS_REFUSED={settings:"Change a squad-kit setting or a protected file",secret:"Hand over a password or a key",history:"Change the history of the code"},HARNESS_TOOL_NAME={take_screenshots:"Take pictures",look_at_page:"Look at a page",start_app:"Start the app",stop_app:"Stop the app",app_status:"See if the app runs",run_checks:"Run the project's checks",add_part:"Add a part of the product",ask_api:"Ask your API",i_am_stuck:"Say it is stuck"};function levelSites(level){return level==="off"?["Only this computer"]:level==="standard"?["Any website but the closed ones"]:RECOMMENDED_GROUPS.map(g=>g.name)}function agentPowers(){let yes=label3=>[label3,!0],no=label3=>[label3,!1],reads=[yes("Read your project"),no("Change code"),no("Run commands"),no("No internet")];return{analyst:{can:reads},architect:{can:reads},builder:{can:[yes("Read your project"),yes("Change code"),yes("Run commands"),yes("The websites your network level allows"),yes("squad-kit's tools: pictures, checks, the app")]},reviewer:{can:[...reads,yes("Look at a page of this story's app")]},tester:{can:[...reads,yes("squad-kit's tools: pictures, the app, your API")]}}}function rowDetails(meta,root){let min=ms2=>`${ms2/6e4} min`,repos=meta?reposOf(meta):[],run7=(...keys)=>repos.map(r=>[repos.length>1?`${r.path}: `:"",keys.map(k=>r.commands[k]).filter(Boolean).join(", ")]).filter(([,c])=>c).map(([p,c])=>`${p}${c}`).join("; ")||"Not set yet",guard=guardSupport(),tools4=sandboxStatus(),level=parseNetwork(meta?.network)??DEFAULT_NETWORK,state=(on,why)=>`${on?"On":"Off"}: ${why}`,names2=list8=>list8?.map(f=>f.path).join(", ")||"None yet",tab=name=>`The story's ${name} tab`,code=(file3,...more3)=>[...more3,["In the code",`src/${file3}`]];return{"tool-ready":code("engine/onboard.ts",["Found",meta?.tools?.map(t=>`${t.name}: ${t.ready?"ready":"not found"}`).join(", ")||"Not looked yet"]),"first-run":code("engine/onboard.ts",["Runs",run7("setup","test","start")],["See it in","The Overview page"]),overview:code("board/first-run.ts",["See it in","The Overview page"]),"gate-cant-run":code("engine/gate-run.ts",["File",".squad/harness.yaml"]),"tool-missing":code("engine/tool-missing.ts",["Reads","No Android SDK found, JAVA_HOME is not set, xcrun: error, command not found"],["Runs again","Once, with no try counted"]),"project-rules":code("engine/repos.ts",["Reads",(root?rulesFiles(root).map(f=>f.slice(root.length+1)):[]).join(", ")||"None found yet"]),"brd-questions":code("engine/breakdown-job.ts",["Limit",`${MAX_QUESTIONS} questions`]),"brd-approve":code("board/requirements-actions.ts",["See it in","The Requirements page"]),"brd-queue":code("board/requirements-build.ts",["Places",`${parallelLimit(meta?.parallel)} at once`]),"first-story":code("board/first-story.ts"),questions:code("engine/question-filter.ts",["Refused",Object.values(HARNESS_REFUSED).join(", ")],["Tries",`${2} back to its agent`]),spec:code("board/actions.ts",["See it in",tab("Spec")]),ask:code("engine/live.ts"),chat:code("board/actions.ts"),plan:code("board/actions.ts",["See it in",tab("Design")]),decisions:code("board/actions.ts",["See it in","The Architecture page"]),depends:code("board/depends.ts"),change:code("board/actions.ts"),sandbox:code("engine/sandbox.ts",["On this computer",tools4.map(t=>state(t.available,t.why)).join(", ")||"Off"],["Network level",NETWORK_NAME[level]],["Also allowed",meta?.allowedHosts?.join(", ")||"None yet"],["Lists","src/engine/network-lists.ts"]),"flutter-stamps":code("engine/flutter-sdk.ts",...FLUTTER_STAMPS.map(f=>[f.file,f.why]),["In",flutterOnPath()?.root??"No Flutter on this computer"],["On Linux","Each command writes them in a throwaway bin/cache that goes when it ends, so Flutter's own folder stays as it was"]),xcode:code("engine/checks.ts",["Runs outside the guard","xcodebuild build and test, only as squad-kit writes them for a native iPhone app (outsideTheGuard)"],["Keeps","Xcode's own sandboxes: for a package's manifest and plugins, and for build scripts when the project turns that on"],["Builds in","build/squad-kit-ios in the story's copy, with the guard's caches"]),guard:code("engine/project-guard.ts",["On this computer",state(guard.on,guard.why)],["Network",networkPolicy(level).commands==="this-computer"?"Only this computer (Off)":"Open"],["Also allowed",[...meta?.guard?.allowRead??[],...meta?.guard?.allowWrite??[]].join(", ")||"None yet"]),lane:code("engine/lane.ts",["Locked here",names2(meta?.lockedFiles)]),"local-files":code("engine/local-files.ts",["Local here",names2(meta?.localFiles)]),"env-file":code("engine/env-file.ts",["Here",repos.flatMap(r=>envFilesOf(r.commands).map(f=>`${repos.length>1?`${r.path}: `:""}${f}`)).join(", ")||"None yet"],["Set in","Settings, beside the commands"],["Order","The file's values first, then squad-kit's own: a Docker service reached through the port it publishes here, the story's own database and Redis number, and the port it picked"],["In the log","The names only, never a value"]),"new-tests":code("engine/prompts.ts",["Here",meta?.alwaysNewTests?"Always on: every story writes new tests":"Your choice on each new story, on by default"],["Set in","Settings"]),checks:code("engine/checks.ts",["Runs",run7("test","lint","build")],["Limit",`${MAX_CHECK_TRIES} tries, ${min(CHECK_MS)} each, ${min(CHECK_SILENT_MS)} with no output`],["Decided by","The exit code, never an agent"],["See it in",tab("Developer")]),"old-tests":code("engine/old-tests.ts",["Runs",run7("test")],["Limit",`${MAX_OLD} failed tests, each up to ${MAX_RUNS_ON_MAIN} times, ${min(ON_MAIN_MS)} in all`],["Reads","Playwright, Vitest, Jest, pytest and go test"],["See it in","The proof page and the Releases page"]),weakened:code("engine/weakened.ts",["Looks at","Deleted tests, skip marks, fewer tests, test settings"],["See it in",tab("Changes")]),"no-tests":code("engine/not-checked.ts",["Here",meta?.noTests?"You chose to go on without tests":"Not asked yet"]),"setup-again":code("engine/live.ts",["Runs",run7("setup")],["Looks at","Package files and database changes, saved or not"]),"test-change":code("engine/test-change.ts",["Runs",run7("test")],["Compares","Pass or fail, and how many tests ran"]),review:code("engine/live.ts",["Limit",`${MAX_REVIEW_ROUNDS} rounds, ${min(REVIEW_STEP_MS)} each`],["See it in",tab("Conversation")]),"reviewer-floor":code("engine/levels.ts"),"also-changed":code("board/also-changed.ts",["Blocks","Nothing"]),proof:code("engine/live.ts",["Runs",run7("start")],["Limit",`${6} for each, ${40} in one step, ${min(PROOF_STEP_MS)}`]),"bad-proof":code("engine/proof-check.ts"),"not-shown":code("engine/proof-verdict.ts",["Limit",`${2} retakes`]),broken:code("engine/proof-verdict.ts",["Limit",`${2} rounds`]),"off-screen":code("engine/proof-verdict.ts"),"api-proof":code("engine/window.ts",["Limit",`${MAX_API_CALLS} calls in one proof`]),"phone-pictures":code("engine/phone/session.ts",["Devices","squad-kit iPhone and squad-kit iPad, squad-kit Android phone and squad-kit Android tablet, its own, one started at a time"],["Which phone",meta?.phone==="iphone"?"The iPhone":meta?.phone==="android"?"Android":"Automatic"],["Set in","Settings"],["Taps with","Maestro"]),"phone-builds":code("engine/phone/build.ts",["Builds in","build/squad-kit-ios and build/squad-kit-android in the story's copy"],["Android","flutter build apk --debug, or ./gradlew assembleDebug (React Native: assembleRelease); then gradlew --stop"],["Puts back","What the build changed in the story's files"]),"tool-license":code("engine/tools/android.ts",["Licenses","android-sdk-license, and android-sdk-arm-dbt-license for an Arm system image"],["See it in","The Tools page"]),"phone-tools":code("engine/tools/android.ts",["Room","About 20 GB"],["No new bytes for","2 min, then once more"],["See it in","The Tools page"]),window:code("engine/window.ts",["Limit",`${REFUSALS_TO_STOP} refusals, ${MISTAKES_TO_STOP} calls in the wrong shape`]),"looks-right":code("board/actions.ts",["See it in","The proof page"]),"take-over":code("board/actions.ts"),outside:code("board/outside.ts",["File",".squad/how-to-work-here.md"]),"accept-order":code("board/depends.ts"),hook:code("engine/git.ts",["Limit",min(HOOK_MS)]),merge:code("board/bring-in.ts",["Limit",`${MAX_BASE_TRIES} tries`]),"pull-request":code("engine/sync.ts",["Limit",`${min(NETWORK_GIT_MS)} to reach your code host`]),checkout:code("board/auto.ts"),release:code("engine/release.ts",["Runs",run7("deploy")],["Limit",min(PIPELINE_MS)]),"leave-out":code("engine/leave-out.ts"),"release-clash":code("engine/release.ts"),"release-lock":code("board/lock.ts"),"room-for-copy":code("engine/git.ts"),watchdog:code("engine/watchdog.ts",["Limit",`Looks after ${min(QUIET_MS)} quiet, says taking long after ${min(LONG_MS)}`]),"usage-limit":code("engine/usage-limit.ts",["Looks again",`Every ${min(RETRY_MS)} when the tool does not say when`]),"step-up":code("engine/levels.ts"),cap:code("engine/caps.ts"),budget:code("board/budget.ts",["Set in","Settings"]),"ways-out":code("board/actions.ts"),"plain-words":code("engine/plain-stop.ts"),interrupted:code("engine/interrupt.ts"),blips:code("engine/live.ts",["Waits",`${BLIP_WAITS_MIN.join(", ")} min`]),leftovers:code("engine/leftovers.ts"),"product-lock":code("engine/product-lock.ts",["Limit",min(WAIT_LIMIT_MS)]),"save-trouble":code("engine/save-trouble.ts"),"board-lock":code("console/auth.ts"),report:code("engine/report.ts"),lead:code("engine/lead/ask.ts",["Runs as","Read only and git read: it never edits, runs the project or commits"],["Always allow here",(meta?.lead?.always??[]).map(a=>ACTIONS2[a]?.name??a).join(", ")||"Nothing yet"],["Its notes","squad-kit's own folder on this computer, never your repo"],["Refusals","engine/lead/can.ts"])}}function trackCell(stage,g,track2){let note2=g.note?.[track2]?{note:g.note[track2]}:{};return g.skip?.includes(track2)?{word:"By itself",...note2}:stage.on[track2].off?{word:"Not used"}:{word:g.you?"You":"Runs",...note2}}var RULE_STAGE={all:"build",analyst:"story",architect:"plan",builder:"build",reviewer:"review",tester:"proof"},additionStage=a=>a.kind==="gate"?a.when:a.kind==="checklist"?a.agent==="reviewer"?"review":"proof":RULE_STAGE[a.agents.length===1?a.agents[0]:"all"];function additionRow(a){let by2=[["Added by",a.added_by],["From",a.from==="typed"?"Typed here":`The folder ${a.from}`]],team={state:a.state,kind:a.kind},row3={id:`add:${a.id}`,name:a.name,team};if(a.kind==="rule")return{...row3,what:a.text,d:by2};if(a.kind==="checklist"){let fail2=a.state==="try"?"The answers show on the story. A no stops nothing.":a.agent==="reviewer"?"A no counts as a review finding.":'A no goes the same way as a "done when" not shown.';return{...row3,what:`The ${a.agent==="reviewer"?"Reviewer":"Tester"} answers: ${a.questions.join(" ")}`,fail:fail2,d:by2}}let fail=a.state==="try"?'It is noted as "would have stopped". Nothing stops.':a.on_fail==="warn"?"It is noted. Nothing stops.":{checks:`The Builder fixes it, up to ${MAX_CHECK_TRIES} tries. Then a card.`,merge:"The change is not saved, and a card has the ways out.",release:"The release does not go out, and the page has the ways out."}[a.when];return{...row3,what:"A command squad-kit runs itself. No agent is asked if it passed.",fail,d:[["Command",a.command],...a.part?[["Part",a.part]]:[],...by2]}}function harnessView(meta,bp){let quality=meta?.quality??DEFAULT_QUALITY,caps=QUALITY_PRESETS[quality].caps,seen=meta&&bp?harnessSeen(bp,meta):void 0,details=rowDetails(meta,bp?.root),addedOn=new Set(bp?readHarness(bp).additions.filter(a=>a.state!=="off").map(a=>`add:${a.id}`):[]),waiting2=seen?.needs.map(n=>n.row.startsWith("add:")&&!addedOn.has(n.row)?{...n,row:"gate-cant-run"}:n),fill2=r=>{let needs=waiting2?.filter(n=>n.row===r.id);return{...r,d:details[r.id]??r.d??[],...seen?.rows[r.id]?{seen:seen.rows[r.id]}:{},...needs?.length?{needs}:{}}},file3=bp?readHarness(bp):{additions:[],log:[]},live=file3.additions.filter(a=>a.state!=="off"),addedCell=a=>({word:"Runs",...a.state==="try"?{note:"Try first"}:{}}),stages=lineStages().map(s=>{let gates=[...s.gates.map(g=>({...fill2(g),cells:{full:trackCell(s,g,"full"),express:trackCell(s,g,"express"),auto:trackCell(s,g,"auto")}})),...live.filter(a=>additionStage(a)===s.id).map(a=>({...fill2(additionRow(a)),cells:{full:addedCell(a),express:addedCell(a),auto:addedCell(a)}}))];return{...s,gates,stopped:new Set(gates.flatMap(g=>seen?.weekIds[g.id]??[])).size}}),always=alwaysOnRows().map(fill2),rows2=[...stages.flatMap(s=>s.gates),...always],offHere=(sandboxStatus().some(t=>t.available)?0:1)+(guardSupport().on?0:1),level=parseNetwork(meta?.network)??DEFAULT_NETWORK;return{quality,qualityNote:QUALITY_NOTE,range:{days:SEEN_DAYS,weekDays:WEEK_DAYS,stories:seen?.stories??0},tiles:{working:rows2.length-offHere,stopped:rows2.filter(r=>r.seen?.week).length,needs:seen?.needs??[]},tracks:[{id:"full",name:"Full",words:"On Full you answer the questions and approve the plan."},{id:"express",name:"Express",words:"On Express there are no questions and the plan is approved on its own. You still check the work."},{id:"auto",name:BUILDS_BY_ITSELF,words:"Approved in advance, it merges when the checks, the review and the proof pass. Release stays yours."}],stages,alwaysOn:always,limits:limitRows(meta).map(l=>seen?.hits[l.id]?{...l,hit:seen.hits[l.id]}:l),caps:Object.keys(HARNESS_CAP_NAME).map(key=>({key,name:HARNESS_CAP_NAME[key],...caps[key]})),agents:agentPowers(),lead:leadRows(meta,bp),additions:{items:file3.additions.map(a=>({...a,stage:additionStage(a),...seen?.rows[`add:${a.id}`]?{seen:seen.rows[`add:${a.id}`]}:{},...a.kind==="rule"&&ruleNote(a.text)?{note:ruleNote(a.text)}:{},...a.kind==="gate"&&a.part&&meta&&!partsOf2(meta).some(p=>p.id===a.part)?{partGone:!0}:{}})),log:file3.log.slice(-200).reverse(),..."broken"in file3&&file3.broken?{broken:file3.broken}:{},parts:meta&&severalParts(meta)?partsOf2(meta).filter(p=>!p.shared).map(p=>({id:p.id,name:p.name})):[],guarded:guardSupport().on,demo:meta?.engine!=="live",shared:bp?docsInGit(bp):!1,limits:{rules:MAX_RULES,ruleChars:MAX_RULE_CHARS,questions:MAX_CHECKLIST_QUESTIONS}},safety:{hidden:[...PRIVATE.map(p=>HARNESS_PRIVATE_NAME[p]??p),"Your AI tools' sign-ins","Browser sign-ins","Keys in package settings",".env files","squad-kit's own folder and keys"],locked:["squad-kit's own folder","AI instruction files","Commit hooks and their settings","Code history: commit and branches. A push is refused, and agents have no keys to push with"],sites:levelSites(level),network:{level,levels:NETWORK_LEVELS.map(id2=>({id:id2,name:NETWORK_NAME[id2],words:NETWORK_WORDS[id2]})),closed:KNOWN_BAD_GROUPS.map(g=>g.name),commands:networkPolicy(level).commands},refused:Object.values(HARNESS_REFUSED),tools:Object.values(HARNESS_TOOL_NAME),local:(meta?.localFiles??[]).map(f=>f.path),lockedByYou:(meta?.lockedFiles??[]).map(f=>f.path)}}}import path161 from"path";var STEP_NAME={questions:"Questions",spec:"Spec",design:"Design",build:"Build",fix:"Fixes",checks:"Checks",review:"Review",proof:"Proof"},SESSION={questions:"intake",spec:"intake",design:"intake",build:"build",fix:"build",checks:void 0,review:"review",proof:"tester"},ENDS=[{step:"questions",who:"analyst",by:["analyst"],re:/^Asked \d+ questions?$/},{step:"spec",who:"analyst",by:["analyst"],re:/^(Wrote|Updated) the spec\b/},{step:"design",who:"architect",by:["architect","spec"],re:/^(Wrote|Updated) the (design|plan)\b/},{step:"build",who:"builder",by:["builder"],re:/^Finished all steps$/},{step:"fix",who:"builder",by:["builder"],re:/^Fixed (the|what)\b/},{step:"checks",who:"squad",by:["tester"],re:/^(All (\d+ )?checks pass|Checks failed|Checks still fail|No test, lint or build commands)/},{step:"review",who:"reviewer",by:["reviewer"],re:/^(Approved\b|Asked for \d+ fix|Still asks for)/},{step:"proof",who:"tester",by:["tester"],re:/^(Saved \d+ screenshots?|No screenshots were taken|No screens to show|No screens change)/}],AFTER={questions:[{re:/^Answered \d+ questions?/,who:["you"],note:"you answered"}],spec:[{re:/^Approved the spec/,who:["you"],note:"you approved"}],design:[{re:/^Approved the (design|plan)/,who:["you"],note:"you approved"},{re:/^Express track: approved the plan on its own/,who:["squad"],note:"approved on its own (Express track)"},{re:/^Builds by itself: approved the plan on its own/,who:["squad"],note:"approved on its own (Builds by itself)"}],proof:[{re:/^Checked the proof\. Looks right/,who:["you"],note:"you checked it: Looks right"},{re:/^(.+ passed: a|A)ccepted by itself/,who:["squad"],note:"accepted by itself"}]},TOOLISH=/^(Reading|Running|Editing|Writing|Looking|Searching|Listing|Runs on|Asked|Started|Finished|squad-kit|The answer was not complete)\b/,REFUSED=/User refused permission to run tool|^Permission denied: |haven't granted it yet/i,ms=iso2=>iso2?Date.parse(iso2):NaN;function oldestFirst(t){return[...t.activity].sort((a,b)=>a.at.localeCompare(b.at))}function runsOf2(t,log3){let history=t.history??[],first2=history.length?ms(history[0].from):1/0,fromLog=[];for(let a of log3){if(ms(a.at)>=first2)break;let end=ENDS.find(e=>e.by.includes(a.who)&&e.re.test(a.text));end&&fromLog.push({who:end.who,step:end.step,to:a.at,line:a})}let timed=history.map(h=>({...h,line:endLine(log3,h)}));return[...fromLog,...timed]}function endLine(log3,h){let end=ENDS.find(e=>e.step===h.step),lo=ms(h.from)-1e3,hi=ms(h.to)+6e4;return log3.find(a=>ms(a.at)>=lo&&ms(a.at)<=hi&&end.by.includes(a.who)&&end.re.test(a.text))}var READING=/^Reading the changes \(round (\d+) of/;function reviewRound(log3,run7){if(!run7.from)return;let lo=ms(run7.from)-6e4,hi=ms(run7.to),m=log3.find(a=>a.who==="reviewer"&&ms(a.at)>=lo&&ms(a.at)<=hi&&READING.test(a.text))?.text.match(READING);return m?Number(m[1]):void 0}function plural4(n,one2,many2=`${one2}s`){return`${n} ${n===1?one2:many2}`}function analystSummary(log3,run7){let end=ms(run7.line?.at??run7.to);return log3.filter(a=>a.who==="analyst"&&Math.abs(ms(a.at)-end)<=5e3&&a!==run7.line&&!TOOLISH.test(a.text)).at(-1)?.text}function summaryOf2(t,run7,log3){let line3=run7.line?.text??"";switch(run7.step){case"questions":{let n=t.questions?.length??Number(/^Asked (\d+)/.exec(line3)?.[1]??0),read9=analystSummary(log3,run7),asked=n?`asked you ${plural4(n,"question")}`:"had no questions";return read9?`${read9.replace(/\.?$/,".")} Then ${asked}.`:`Read the ticket and ${asked}.`}case"spec":{if(/^Updated the spec: /.test(line3))return line3.replace(/\.?$/,".");let answers2=(t.questions??[]).filter(q5=>q5.answer).length,points=t.criteria?.length??0;return`Wrote the spec${/from your talk/.test(line3)?" from your talk":answers2?` from your ${plural4(answers2,"answer")}`:""}${points?`, with ${plural4(points,'"done when" point')}`:""}.`}case"design":{if(/^Updated the (design|plan): /.test(line3))return line3.replace(/\.?$/,".");let steps=t.steps?.length??0,said2=t.decision?.options.find(o=>o.chosen)?.name?.replace(/^(A|An|The|One|Its|Their|Our|Your)\b/,w=>w.toLowerCase()),parts2=[said2?`Chose ${said2}`:"Wrote the design",steps?`planned ${plural4(steps,"step")} to build`:"",t.mockup?"made a mockup":""].filter(Boolean);return`${parts2.length>1?`${parts2.slice(0,-1).join(", ")} and ${parts2.at(-1)}`:parts2[0]}.`}case"build":{let steps=t.steps?.length??0;return steps?`Did the ${plural4(steps,"step")} of the design.`:"Built the change."}case"fix":return line3?line3.replace(/\.?$/,"."):"Fixed what the checks or the review found.";case"checks":return/^All/.test(line3)?`squad-kit ran the project's checks: ${line3.replace(/^All /,"all ").replace(/\.?$/,".")}`:line3?`squad-kit ran the project's checks. ${line3.replace(/\.?$/,".")}`:"squad-kit ran the project's checks.";case"review":return/^Approved: /.test(line3)?line3.replace(/\.?$/,"."):/^Approved/.test(line3)?"Approved the changes.":line3?`${line3.split(":")[0]}.`:"Read the changes.";case"proof":{let n=t.proof?.length??0,pictures=(t.proof??[]).reduce((sum,p)=>sum+1+(p.more?.length??0),0);return pictures>n?`Opened the app and took ${plural4(pictures,"screenshot")} of the ${plural4(n,'"done when" point')} it could show.`:n?`Opened the app and took ${plural4(n,"screenshot")}, one for each "done when" point it could show.`:line3?line3.replace(/\.?$/,"."):"Checked the change."}}}function filesFrom(conv,cwd,from,to){let added2=new Set,changed=new Set;if(!conv)return{added:[],changed:[]};let lo=ms(from),hi=ms(to),rel=p=>cwd&&p.startsWith(`${cwd}/`)?p.slice(cwd.length+1):path161.basename(p);for(let e of conv.updates){let at3=ms(e.at);if(Number.isFinite(lo)&&Number.isFinite(at3)&&(at3<lo||at3>hi))continue;let u=e.update;if(!(u.sessionUpdate!=="tool_call"&&u.sessionUpdate!=="tool_call_update"))for(let c of u.content??[]){if(c.type!=="diff"||!c.path)continue;let p=rel(c.path);c.oldText==null&&!changed.has(p)?added2.add(p):added2.has(p)||changed.add(p)}}return{added:[...added2],changed:[...changed]}}function handedIn(t,run7,files,conv){switch(run7.step){case"questions":return t.questions?.length?{kind:"questions",items:t.questions.map(q5=>({from:q5.from,text:q5.text,...q5.answer?{answer:q5.answer.value?`${q5.answer.choice.replace(/:$/,"")}: ${q5.answer.value}`:q5.answer.choice}:{}}))}:void 0;case"spec":return t.story||t.criteria?.length?{kind:"spec",...t.story?{story:t.story}:{},criteria:(t.criteria??[]).map(c=>c.then)}:void 0;case"design":{if(!t.decision&&!t.steps?.length)return;let chosen2=t.decision?.options.find(o=>o.chosen)?.name;return{kind:"design",...t.decision?{title:t.decision.title}:{},...chosen2?{chosen:chosen2}:{},...t.decision?.adr?{adr:t.decision.adr}:{},steps:(t.steps??[]).map(s=>s.title),mockup:!!t.mockup,follows:t.follows??[]}}case"build":case"fix":{let mine2=run7.step==="fix"||!files?filesFrom(conv,conv?.cwd,run7.from,run7.to):null,got=mine2?{...mine2,removed:[]}:files,steps=run7.step==="build"?(t.steps??[]).filter(s=>s.status==="done").map(s=>s.title):[];return!got.added.length&&!got.changed.length&&!got.removed.length&&!steps.length?void 0:{kind:"files",added:got.added,changed:got.changed,removed:got.removed,steps}}case"checks":return t.gates?.checks&&t.gates.checks!=="pending"?{kind:"checks",ok:t.gates.checks==="pass"||t.gates.checks==="skipped",text:t.gates.checksText??(t.gates.checks==="pass"?"All checks pass":"Some checks failed")}:void 0;case"review":{let line3=run7.line?.text??"",approved=/^Approved/.test(line3)||!line3&&t.gates?.review==="pass",notes=approved?[]:t.reviewNotes?.length?t.reviewNotes:line3.includes(": ")?[line3.slice(line3.indexOf(": ")+2)]:[];return{kind:"verdict",approved,notes}}case"proof":return t.proof?.length?{kind:"shots",shots:t.proof.flatMap(p=>[p.after,...(p.more??[]).map(m=>m.after)].filter(file3=>!file3.endsWith(".api.json")).map(file3=>({criterion:p.criterion,file:file3})))}:void 0}}var TAB={spec:"plan",design:"plan",build:"changes",fix:"changes",checks:"proof",proof:"proof"};function counts2(conv,from,to){if(!conv||!from)return null;let lo=ms(from),hi=to?ms(to):1/0,inside5=at3=>{let x=ms(at3);return Number.isFinite(x)&&x>=lo-1e3&&x<=hi+1e3},actions=0,skipped=0;for(let e of conv.updates){if(!inside5(e.at))continue;let u=e.update;u.sessionUpdate==="tool_call"&&(actions+=1),u.sessionUpdate==="tool_call_update"&&u.status==="failed"&&(u.content??[]).some(c=>c.type==="content"&&c.content?.type==="text"&&REFUSED.test(c.content.text??""))&&(skipped+=1)}return{actions,skipped}}function whatHappened(bp,t,src={}){let now4=src.now??Date.now(),log3=oldestFirst(t),runs2=runsOf2(t,log3),read9=src.conversation??(id2=>readConversation(id2,3e3,void 0,bp.chatsDir)),convs=new Map,conv=key=>{let id2=key?t.sessions?.[key]:void 0;if(!id2)return null;if(!convs.has(id2))try{convs.set(id2,read9(id2))}catch{convs.set(id2,null)}return convs.get(id2)??null},files,ticketFiles=()=>{if(files===void 0){try{files=src.files?.()??null}catch{files=null}files&&!files.added.length&&!files.changed.length&&!files.removed.length&&(files=null)}return files},latest=new Map;runs2.forEach((r,i)=>latest.set(r.step,i));let reviews=runs2.filter(r=>r.step==="review").length,steps=runs2.map((run7,i)=>{let next=runs2[i+1],session=SESSION[run7.step],c=conv(session),isLatest=latest.get(run7.step)===i,seconds2=run7.from?Math.max(0,(ms(run7.to)-ms(run7.from))/1e3):void 0,notes=[],until=next?ms(next.from??next.to):1/0;for(let a of AFTER[run7.step]??[])log3.some(l=>a.who.includes(l.who)&&a.re.test(l.text)&&ms(l.at)>=ms(run7.to)-1e3&&ms(l.at)<=until)&¬es.push(a.note);if(run7.step==="review"){let k=runs2.slice(0,i+1).filter(r=>r.step==="review").length,round=reviewRound(log3,run7),fresh=round!==void 0?round===1:k===1;notes.push(reviews>1?`round ${k} of ${reviews}, ${fresh?"a fresh session":"the same Reviewer as the round before"}`:"a second, separate session")}let got=isLatest?handedIn(t,run7,run7.step==="build"?ticketFiles():null,c):void 0,changed=got?.kind==="files"?got.added.length+got.changed.length+got.removed.length:0,n=counts2(c,run7.from,run7.to),tab=TAB[run7.step];return{who:run7.who,step:run7.step,name:`${WHO[run7.who]} \xB7 ${STEP_NAME[run7.step]}`,...run7.from?{from:run7.from}:{},to:run7.to,...seconds2!==void 0?{seconds:seconds2}:{},summary:run7.step==="build"&&changed?summaryOf2(t,run7,log3).replace(/\.$/,` and changed ${plural4(changed,"file")}.`):summaryOf2(t,run7,log3),...got?{handedIn:got}:{},...got&&tab?{tab}:{},notes,...session&&t.sessions?.[session]?{session}:{},...n?{actions:n.actions,skipped:n.skipped}:{}}}),ck=t.clock;if(ck&&!t.waiting){let from=ck.began??ck.since,session=SESSION[ck.step],n=counts2(conv(session),from),part=ck.step==="build"&&ck.part&&t.steps?.length?` Step ${ck.part} of ${t.steps.length}.`:"";steps.push({who:ck.who,step:ck.step,name:`${WHO[ck.who]} \xB7 ${STEP_NAME[ck.step]}`,from,seconds:Math.max(0,(now4-ms(from))/1e3),running:!0,summary:`${(ck.doing??t.status)||"Working on it"}${ck.doing?".":""}${part}`.replace(/\.\./g,"."),notes:[],...session&&t.sessions?.[session]?{session}:{},...n?{actions:n.actions,skipped:n.skipped}:{}})}return{steps,seconds:steps.reduce((s,x)=>s+(x.seconds??0),0)}}async function body2(c){try{return await c.req.json()}catch{return{}}}async function changedFiles3(bp,id2){let{files}=await ticketChanges(bp,id2);return files.length?{added:files.filter(f=>f.status==="added").map(f=>f.path),changed:files.filter(f=>f.status==="changed"||f.status==="renamed").map(f=>f.path),removed:files.filter(f=>f.status==="deleted").map(f=>f.path)}:null}function mountCostApi(app,bp){app.get("/api/v1/tickets/:id/happened",async c=>{let t=readTicket(bp,c.req.param("id"));if(!t)return c.json({error:"not found"},404);let files=await changedFiles3(bp,t.id).catch(()=>null);return c.json(whatHappened(bp,t,{files:()=>files}))}),app.get("/api/v1/tickets/:id/cost",async c=>{let t=readTicket(bp,c.req.param("id"));return t?c.json(ticketCost(bp,t,await howPaid(bp))):c.json({error:"not found"},404)}),app.get("/api/v1/cost",async c=>boardExists(bp)?c.json(costOverview(bp,await howPaid(bp))):c.json({error:"No board here yet."},404)),app.put("/api/v1/cost/payment",async c=>{let b=await body2(c);return b.payment!=="subscription"&&b.payment!=="api-key"&&b.payment!=="auto"?c.json({error:"Pick subscription, api-key or auto."},400):(setPaymentChoice(b.payment),c.json(costOverview(bp,await howPaid(bp))))}),app.put("/api/v1/cost/budget",async c=>{let b=await body2(c);try{let change={};"ticketUsd"in b&&(change.ticketUsd=parseUsd(b.ticketUsd)),"monthUsd"in b&&(change.monthUsd=parseUsd(b.monthUsd));let budget=setBudget(bp,change);return c.json({...costOverview(bp,await howPaid(bp)),budget})}catch(err2){return c.json({error:plainWords(err2,"Saving the budget")},400)}})}import path162 from"path";function checkWhereCodeGoes(dir,base2,remote){if(!isRepo(dir))return;let remotesHere=remotes(dir);if(remote&&!remotesHere.includes(remote))throw new Error(`There is no remote called "${remote}" here. ${remotesHere.length?`It has: ${remotesHere.join(", ")}.`:"It has none yet."} Nothing was saved.`);let branches=git(dir,["for-each-ref","--format=%(refname:short)","refs/heads",...remote?[`refs/remotes/${remote}`]:[]],{allowFail:!0}).split(`
|
|
1951
|
+
`,{mode:384}),fs168.renameSync(tmp,file3),clean4}function sameQuestion(text4){return(isArabic(text4)?plainArabic(text4):text4.toLowerCase()).replace(/[^\p{L}\p{N}]+/gu," ").trim()}var SIGN_IN_EN=/\b(sign(?:ing)?[\s-]?in|log(?:ging)?[\s-]?in|login|log on|sign on|password|credentials|test account|test user|which user|what (?:should|do) i type|get in(?:to)?|account to use)\b/i,SIGN_IN_AR=/(تسجيل الدخول|سجل الدخول|ادخل|الدخول|كلمه المرور|كلمه السر|الحساب|اسم المستخدم)/;function asksSignIn(text4){return isArabic(text4)?SIGN_IN_AR.test(plainArabic(text4)):SIGN_IN_EN.test(text4)}function noteFor(bp,question){let newest=readNotes(bp).reverse(),same5=sameQuestion(question);return newest.find(n=>sameQuestion(n.question)===same5)??(asksSignIn(question)?newest.find(n=>n.about==="sign-in")??null:null)}function signInNote(bp){return readNotes(bp).reverse().find(n=>n.about==="sign-in")?.text}function lineStages(){let you=sub=>({kind:"you",sub}),squad=sub=>({kind:"squad",sub}),skip=sub=>({kind:"skip",sub}),on=(full,express=full,auto=full)=>({full,express,auto}),stage=(id2,name,about2,when2,gates)=>({id:id2,name,about:about2,on:when2,gates}),g=(id2,name,what,fail,more3)=>({id:id2,name,what,...fail?{fail}:{},...more3}),y=(id2,name,what,fail,more3)=>g(id2,name,what,fail,{you:!0,...more3}),ON_ITS_OWN2=["express","auto"],floor=QUALITIES.filter(q5=>QUALITY_PRESETS[q5].reviewerFloor).map(q5=>q5.charAt(0).toUpperCase()+q5.slice(1)).join(" and ");return[stage("setup","Setup","Once for each project, before any story.",on(you("You confirm")),[g("tool-ready","Your AI tool is here","It must be installed, or nothing runs.","It says what to do."),g("first-run","A first run on untouched code","Your tests and app run once in squad-kit's own copy. What already fails is noted, so the Builder leaves other screens alone.","The setup goes on. A story must still pass its checks."),y("overview","You confirm what it found","The Architect explains your project in plain words.","You say what is wrong, and it reads again."),g("project-rules","Your project's own rules","Every agent first reads your rule files and your team's wiki. squad-kit runs the process, and those files cover the code.")]),stage("breakdown","Breakdown","Your requirements become a tree of stories. Skip it if you start from one story.",on({...skip("Optional"),off:!0},{...skip("Optional"),off:!0},you("You approve")),[g("brd-questions","Only real gaps and clashes",`At most ${MAX_QUESTIONS} questions on a document, where it leaves a gap or disagrees with itself.`,"A technical one goes back to the agent."),y("brd-approve","You approve the breakdown",'Nothing is built or sent to your tracker before that. Every story needs its "done when".',"Approve is refused and says what to fix."),g("brd-queue","Stories start in order","The next starts when what it needs is merged and a place is free.","Your budget, your AI limit or two stopped stories hold the queue, with a reason."),g("first-story","A project with no code starts with one story","squad-kit adds Set up the product: it picks the tools and makes a first version. Nothing else starts until it merges, so every story builds on the same start.","Another ticket waits, and says which one it waits for.")]),stage("story","Story","The Analyst reads your story and asks only what it leaves open.",on(you("You answer"),skip("No questions"),skip("Approved before")),[g("questions","Only business questions reach you","The Analyst asks about gaps and clashes. Asks about settings, secrets or code history are refused before you see them.",void 0,{skip:ON_ITS_OWN2}),y("spec","You approve what gets built",'The story and its "done when" list wait for you.',"You ask for a change and it is rewritten.",{skip:ON_ITS_OWN2,note:{auto:"Approved with the breakdown."}}),y("ask","An agent can stop and ask you","The Architect, Builder or Reviewer can ask one plain question. It passes the same filter.",void 0,{note:{auto:"Only for a critical gap or a clash. Small choices go under Decided for you."}}),y("chat","Talk it through","With any agent that worked on the story. It waits while you talk.")]),stage("plan","Plan","The Architect reads your project and writes the plan in plain words.",on(you("You approve"),skip("On its own"),skip("On its own")),[y("plan","You approve the plan","Nothing is built before you approve how.","You ask for a change and it is rewritten.",{skip:ON_ITS_OWN2}),g("decisions","Saved decisions stay rules","Each plan says which ones it follows or replaces."),y("depends","Stories that depend on each other","A story that needs unmerged work waits: build on it, build anyway, or wait."),y("change","A big change comes back to you","The Builder pauses, and the updated design waits for your yes.")]),stage("build","Build","The Builder writes the code in small steps, in a locked room.",on(squad("Builder")),[g("sandbox","The guard","Each story has its own copy. No agent can read your keys or sign-ins. The Builder and Tester can't read .env files either, and reach only the websites the project's network level allows.","Blocked, with a line on the story. A website it may not reach stops nothing: the work goes on without it."),g("guard","A second guard for your project","Your tests, app, setup and release run locked in too, with keys hidden and Docker closed.","A card: an engineer allows the file, or the plan changes."),g("flutter-stamps","Flutter's own version stamps","Flutter writes its version in a few small files of its own folder on every run. Those files, and nothing else of Flutter, are open to the squad's commands."),g("xcode","iPhone builds",OUTSIDE_THE_GUARD_WORDS),g("phone-builds","squad-kit's own phone builds","For the pictures, Start and Try it yourself, squad-kit builds the app for its phone itself, outside the guard: Xcode for the iPhone, Gradle or Flutter for Android, in one shared Gradle home of its own. What the build changes in the story's files goes back, so the story's changes stay the Builder's. An Expo app gets its phone folder made for the build, and no commit ever takes it.","An app that does not build: a card, and you decide."),g("lane","Files no agent can change","squad-kit's folder, AI instruction files, commit hooks, files an engineer locked. No commit or branch change, and a push is refused.","A file goes back, with a line. A moved branch stops the step."),g("local-files","Test values, not your own","A file marked local gets values kept for stories. No agent can read it."),g("env-file","A part's env file","squad-kit gives the app the story's values from it, then its own database, Redis and the app's address on top."),y("new-tests","New tests",`The Builder writes tests for every "done when". On a new story you can turn off Write new tests: your project's own checks, the review and the proof still run, and the story says "Built without new tests". An engineer can keep it on for every story.`,void 0,{note:{auto:"Always on: a story from the breakdown writes its tests."}})]),stage("checks","Checks","squad-kit runs your project's own checks. No agent is asked if they passed.",on(squad("squad-kit")),[g("checks","Your project's own checks","Test, lint and build, plus scripts/check when your project has one. The Builder's word doesn't count. A failed check runs once more, so a flaky test doesn't stop the story. For the tests, the failed ones run on their own first: one that still fails fails the check at once, and if they pass, all the tests run once more and decide.",`The Builder fixes it, up to ${MAX_CHECK_TRIES} tries. Then a card.`),g("setup-again","Set up again when the code changes","New packages or database changes came into a story's copy, from main or from the Builder: its setup runs again before the checks and before the app starts, so the app never runs on an old database.","A database of the story's own that is out of step is made again, and the setup runs once more. Then the Builder fixes it."),g("old-tests","An old bug is not the story's","A failed test in a file the story didn't change runs on its own on main's code. If it fails there too, it is an old bug: the story goes on, the checks say so, and squad-kit makes one new story to fix that test, which waits on the board until you start it. A story only goes past a test that main already fails.","If it passes on main, the Builder fixes it, as for any failed check."),g("weakened","Weakened tests","Tests deleted, switched off or reduced, test settings changed, or a change to what a must-pass gate of your team runs.","It stops. Only you can go past it, with a reason on the record."),g("no-tests","No tests, no green","No tests never means a green pass. Lint, build and the app starting must still pass.","Asked once per project: add tests, or go on without."),g("test-change","Test commands stay strong","A new test command stays only if it passes and runs at least as many tests.","If squad-kit can't tell, you choose."),g("tool-missing","A missing tool is not the Builder's","A check that fails only because a tool is missing (the Android SDK, Java, Xcode, a program the command runs) says nothing about the code. squad-kit looks at this computer's tools again and runs the checks once more: no try counts, and the Builder is never asked.","Still missing: the story waits for it with the Tools page's line and goes on by itself once it is there, or a card says what to install."),g("gate-cant-run","A check that can't run","One of your team's gates that can't run, or a file of additions that can't be read, says nothing about the code. It never counts against the story, and it is never dropped without a word.","It retries by itself, then a card: try again, let an engineer look, or go on with a reason. On Builds by itself it waits for a person.")]),stage("review","Review","A Reviewer that never saw the build reads the Builder's work.",on(squad("Reviewer")),[g("review","A fresh Reviewer",'It checks every "done when" and saved decision, and looks for bugs, security holes and missing or weakened tests. It changes nothing.',`Back to the Builder, up to ${MAX_REVIEW_ROUNDS} rounds. Then you decide.`),g("reviewer-floor","Never weaker than the Builder",`On ${floor}, its model is at least as strong.`),g("also-changed","Also changed","It names what the change touches beyond the story and plan.","It doesn't block. It shows at Your check.")]),stage("proof","Proof",'The Tester runs your real app and shows each "done when" working.',on(squad("Tester")),[g("proof","Pictures of the real app",`Up to ${6} for each "done when".`,"None: a card, and you decide."),g("bad-proof","squad-kit checks each picture",`Real, at least ${MIN_SIZE.width} by ${MIN_SIZE.height}, not blank, taken in this step.`,"Back to the Tester once. Then a card."),g("not-shown",'A second look at every "done when"',"A separate run judges its pictures: met, not working, not shown, or not on a screen.",`Not shown: up to ${2} retakes. Then a card.`),g("broken","Not working yet","Pictures that show it does not work never pass.",`Back to the Builder ${"once"}. Then a card.`),g("off-screen","Not on a screen",'Takes two voices: the Tester and the check. If every "done when" ends that way, you decide.'),g("api-proof","Answers from your API","Work with no screen can show a saved answer from this story's own API."),g("phone-pictures","Phone apps on a real phone","A phone app's pictures come from squad-kit's own hidden phone, each named with its device: its iPhone and iPad on a Mac, or its Android phone and tablet (Google's own emulator). An app for both uses the iPhone on a Mac when its tools are ready, else Android; Settings can choose. One phone runs at a time, and it shuts down after 10 minutes unused. With its tools missing, pictures of the web version stand in, and the story says so.","With no web version either, it waits for the tools and goes on once they are there. An app that does not build or a phone that does not start: a card, and you decide."),g("window","Fixed tools only","The Tester uses squad-kit's own tools, on this story's app only.",`Refused. ${REFUSALS_TO_STOP} refusals stop it.`)]),stage("yours","Your check","You look at the proof, try the app, and ask anything.",on(you("You decide"),you("You decide"),skip("On its own")),[y("looks-right","You look and decide","Where to look, Try it yourself, Ask a question, Something's wrong. Nothing merges before Looks right.","Something's wrong sends it back with your words.",{skip:["auto"],note:{auto:"It merges when the checks, review and proof pass. If not, it comes to you."}}),y("take-over","Take it over","Work in the story's copy yourself. The squad keeps off until you give it back."),y("outside","Work in another AI tool","Copy a prompt, and a conversation in any AI tool carries on from this step. The squad keeps off until it comes back. It keeps a label on its record, and Check runs squad-kit's own checks, review and proof on it. What is spent there is yours, and the guard and the limits do not apply there."),g("accept-order","First things first","A story built on another is accepted after it, and an open change is settled first.","Accept says what comes first.")]),stage("merge","Merge","The story joins the rest of your code.",on(squad("squad-kit")),[g("hook","Your check on every save","Your commit hooks run on every save, inside the guard.","The Builder fixes it, or you save without it and say why."),g("merge","A clean merge","The latest code comes in first. A clash is never forced.",`The newest code comes in by itself, up to ${MAX_BASE_TRIES} times. Then a card.`),g("pull-request","Pull requests",`squad-kit opens and merges them. Waiting for an approver is a normal wait. One merged on the code host is noticed within ${OUTSIDE_LOOK_MS/6e4} minutes, or at once with Check again.`,"Try again, or let the release merge the work."),g("checkout","Your own copy is safe","With changes in the same files, the merge waits. It never overwrites them.")]),stage("release","Release","You choose what goes out and when. Every track stops here.",on(you("You decide")),[y("release","Release stays yours","Deploying never happens by itself.","If it can't be sent, it isn't marked as sent. Send again finishes it."),y("leave-out","Leave a story out","It stays in your code and goes out with the next release."),g("release-clash","Nothing moves on a clash","Every merge is tried first. What the others rely on goes first.","A clash: one card, nothing moved. A failed part: the ones after it wait."),g("release-lock","One release at a time","One cut off by a restart is finished or rolled back on the next start.")])]}function alwaysOnRows(){let a=(id2,name,what,then)=>({id:id2,name,what,...then?{then}:{}});return[a("watchdog","Nothing hangs in silence","When a step goes quiet, the watchdog looks at what is really running, to tell busy from stuck.","You get a card with ways out."),a("usage-limit","Your AI limit is respected","At the limit every project waits together, then carries on from the same step.","Never a failed try, and you can end the wait yourself."),a("step-up","A stronger model after a failed try","The Builder moves up, unless someone chose an exact model.",`On Thrifty, after ${QUALITY_PRESETS.thrifty.stepUp.after} failed tries.`),a("cap","A run can't go on forever","Each run has a most it can use. See Limits.","It pauses with its work kept. Give it more doubles it."),a("budget","Your budget",`If you pay per use, a story and a month can have one. At ${WARN_AT*100}% the board says so.`,"At 100% the story or the Analyst stops between two steps, never mid-step."),a("ways-out","Every stop has a way out","Try again, do it yourself, go around it, or stop. Only you can go past a failed check.","Your reason stays on the story, the pull request and the release notes."),a("plain-words","Plain words only","Raw errors never reach you.","The exact error goes to the engineer details and the log."),a("interrupted","An interruption is not a failure","A restart, a Stop or a sleeping computer never counts as a failed try.","The step carries on where it was."),a("blips","A lost connection is not a failure","The step runs again by itself.","If it does not pass, you get a card."),a("leftovers","Nothing left behind","A program a step leaves running is stopped. Programs of your own are never touched.","On Windows it stops only what it started itself."),a("product-lock","One story runs the product at a time","Stories that need the whole product take turns, and say who they wait for."),a("room-for-copy","Room on the disk","Before a first copy of the code, squad-kit compares the free space with the size of the project.","Too little: a card says how much it needs and how much is free. Free up some space, then press Try again."),a("save-trouble","A full disk holds the work","If squad-kit can't save its files, the project waits and says so on every page.","It carries on once saving works."),a("board-lock","Your board is locked","It opens only on your own computer's address, with your link's key.","A request from any other website is refused."),a("report","Reports leave your secrets out",NEVER2),a("lead","The Squad lead changes nothing on its own","It reads and looks: the stories, the code, squad-kit's records and guide, and the app in its own copy with its own database, never yours. Everything else is a card you press, and some things it never does, whoever asks.","A card it may not offer is refused in squad-kit's own words. Only your own messages count, never what it reads."),{id:"tool-license",name:"A tool maker's license is yours to accept",what:"squad-kit gets Google's Android phone tools only after you read Google's license, in full, and press Accept. It never accepts a license for you.",then:"Get it waits until you do.",you:!0},a("phone-tools","squad-kit's own phone tools","Get it looks at the free room first (about 20 GB for Android) and says so. A download that gets no new bytes for 2 minutes is stopped and tried once more. An Android SDK of yours is used as it is when it has everything, and never changed.","Too little room, or a download that stops twice: a plain line on the Tools page, and nothing half installed. Press Get it again.")]}function limitRows(meta){let row3=(group,show3=String)=>(limit,name,what="")=>{let[id2,n]=Object.entries(limit)[0];return{id:id2,group,n:show3(n),name,what}},tries=row3("tries"),pics=row3("pictures"),asks=row3("asks"),time2=row3("time",ms2=>`${ms2/6e4} min`),size2=row3("sizes"),bytes=row3("sizes",b=>`${b/1024/1024} MB`);return[tries({MAX_CHECK_TRIES},"Tries at the checks","Then a card."),tries({MAX_RUNS_ON_MAIN},"Runs of a failed test on main's code","It stops at the first that fails there too."),size2({MAX_ALONE},"Failed tests run on their own first","With more, all the tests run again at once."),size2({MAX_OLD},"Failed tests run on main's code","With more, the Builder fixes them, as before."),tries({MAX_REVIEW_ROUNDS},"Review rounds","Then you decide."),tries({MAX_RETAKES:2},"Retakes",`For a "done when" the pictures don't show clearly.`),tries({MAX_PROOF_ROUNDS:1},"Returns after a broken picture","To the Builder. Then you decide."),tries({MAX_BASE_TRIES},"Clash retries","While the code keeps moving."),tries({BLIP_WAITS_MIN:BLIP_WAITS_MIN.length},"Waits after a lost connection",`After ${BLIP_WAITS_MIN.join(", ")} minutes. Then a card.`),pics({MAX_PICTURES_PER_DONE_WHEN:6},'Pictures for each "done when"',"So each part can show."),pics({MAX_PICTURES:40},"Pictures in one proof step","Counting each one taken again."),pics({MAX_SHOTS_PER_CALL},"Pictures in one call","More has to be split over calls."),pics({MAX_API_CALLS},"Calls to your API in one proof","Only this story's own app."),asks({REFUSALS_TO_STOP},"Refusals","Forbidden asks, then the agent is stopped."),asks({MISTAKES_TO_STOP},"Calls in the wrong shape","Harmless, but past this the step ends."),asks({MAX_STUCK},"Times a run can say it is stuck","After that, squad-kit says it already tried."),asks({TECHNICAL_TRIES:2},"Tries for a technical question","Back to its agent, never to you. Then it stops."),asks({MAX_QUESTIONS},"Questions on one document","Only real gaps and clashes."),asks({MAX_DECIDED},"Small choices in one answer","Logged under Decided for you."),time2({WORDS_STEP_MS},"Analyst and Architect","One run of words: questions, a spec, a design."),time2({BUILD_STEP_MS},"Builder","One run of building or fixing."),time2({REVIEW_STEP_MS},"Reviewer","One round, or one look at the pictures."),time2({PROOF_STEP_MS},"Tester"),time2({BRD_READ_MS},"Reading a document","A long document gets twice its usual read."),time2({LONGEST_READ_MS},"Reading a very long document","The most, however long it is."),time2({BRD_WRITE_MS},"Writing a breakdown"),time2({CHECK_MS},"One check"),time2({CHECK_SILENT_MS},"A silent check","It prints nothing for this long."),time2({APP_WAIT_MS},"Waiting for your app"),time2({TURN_WAIT_MS},"Tests and your app in one copy","One waits for the other, then it stops and says so."),time2({KEPT_APP_MS},"Your app kept for the proof","After the review, while nothing uses it. Then it stops."),time2({SETUP_MS},"Setting up your app"),time2({DOCKER_WAIT_MS},"Starting in Docker"),time2({BEFORE_SHOTS_MS},"Before pictures","Beside the build, holding nothing up."),time2({ON_MAIN_MS},"Failed tests on main's code","All their runs together. Then the Builder fixes them, as before."),time2({HOOK_MS},"Your check on a save"),time2({NETWORK_GIT_MS},"Reaching your code host","One push, pull or fetch."),time2({PIPELINE_MS},"Your after-release step","Inside the guard, after a release."),time2({MAX_LANDING_WAIT_MS},"Waiting for a story to merge","Before the newest code comes in."),time2({OUTSIDE_LOOK_MS},"Looking for merges on your code host","Pull requests merged there, outside squad-kit. Check again looks now."),time2({WAIT_LIMIT_MS},"Waiting for the product"),time2({TRY_YIELDS_AFTER_MS},"Trying a story yourself","Once another story needs it."),time2({QUIET_MS},"Quiet before a look","Then the watchdog looks."),time2({LONG_MS},"Busy before Taking long","Working, with no progress."),time2({RETRY_MS},"Looking again at your AI limit","When your AI tool doesn't say when it ends."),size2({parallel:parallelLimit(meta?.parallel)},"Stories at once","Across your projects. Change it in Settings."),size2({MAX_PARTS},"Parts of one product"),size2({MAX_UPLOADS},"Files on one story"),size2({MAX_RULES},"Rules your team can add","Remove one to add another."),size2({MAX_RULE_CHARS},"Characters in one rule","Agents read every rule at every step, so each stays short."),size2({MAX_CHECKLIST_QUESTIONS},"Questions on one checklist"),size2({MAX_TYPED_CHARS},"Characters in a name, a question or a command"),size2({MAX_FOLDER_FILES},"Files looked at in a folder you bring in","The rest are left out, and the sheet says so."),row3("sizes",b=>`${b/1e3} KB`)({MAX_FOLDER_FILE_BYTES},"Largest file read in that folder","A bigger one is left out."),bytes({MAX_UPLOAD_BYTES},"One file"),bytes({MAX_NOTE_BYTES},"One voice or video note"),time2({LEAD_ANSWER_MS:48e4},"The Squad lead, one answer","Looking at the app in its own copy included."),row3("asks",usd4=>`$${usd4}`)({MAX_LEAD_DAY_USD:10},"What the Squad lead may use in a day","At API prices. Then it answers again the next day; its notes still answer at once."),asks({MAX_LEAD_CARDS:4},"Cards in one answer of the Squad lead"),pics({MAX_LEAD_PICTURES:4},"Pictures in one answer of the Squad lead","Taken in its own copy of the app."),size2({MAX_LEAD_NOTES},"Notes the Squad lead keeps","The oldest go first."),size2({MAX_QUESTION_CHARS:4e3},"Characters in one question to the Squad lead")]}var HARNESS_CAP_NAME={analyst:"Analyst",architect:"Architect",builder:"Builder","builder.fix":"Builder, fixes",reviewer:"Reviewer",tester:"Tester",chat:"Talking a story through",lead:"The Squad lead, one question"},LEAD_MAY_READ=["The board: every story, where it stands and what happens on it",`Each story: its spec, plan, "done when", activity, the Reviewer's notes, its proof and how the Tester went through the app`,"The decisions, the requirements, the releases, what the squad used, and the Tools page","squad-kit's log lines for a story, with secrets taken out","squad-kit's own guide and the Harness rules","The code, read only, and its history. Never your keys, sign-ins or .env files","The app, in its own copy of main with its own database: pictures, pages and your API. Never your app or your real data"];function leadRows(meta,bp){let caps=QUALITY_PRESETS[meta?.quality??DEFAULT_QUALITY].caps.lead,always=(meta?.lead?.always??[]).filter(a=>ALLOWABLE.includes(a));return{reads:LEAD_MAY_READ,levels:leadCan().levels,always:{words:ALWAYS_WORDS,can:ALLOWABLE.map(id2=>({id:id2,name:ACTIONS2[id2].name})),on:bp?always:[]},limits:[{name:"One question",n:`$${caps.spendUsd} or ${caps.turns} turns`,what:"From the Quality setting, like every step."},{name:"One day",n:`$${10}`,what:"At API prices. Its notes and the board still answer at once after it."},{name:"One answer",n:`${48e4/6e4} min`,what:"Looking at the app in its own copy included."}],model:'The balanced level by default. A person can change it for themselves, under the name "lead".'}}var HARNESS_PRIVATE_NAME={".ssh":"SSH keys",".aws":"AWS sign-in",".gnupg":"GPG keys",".config/gh":"GitHub sign-in",".azure":"Azure sign-in",".kube":"Kubernetes sign-in",".docker/config.json":"Docker sign-in",".netrc":"Saved website passwords",".git-credentials":"Saved git passwords"},HARNESS_REFUSED={settings:"Change a squad-kit setting or a protected file",secret:"Hand over a password or a key",history:"Change the history of the code"},HARNESS_TOOL_NAME={take_screenshots:"Take pictures",look_at_page:"Look at a page",start_app:"Start the app",stop_app:"Stop the app",app_status:"See if the app runs",run_checks:"Run the project's checks",add_part:"Add a part of the product",ask_api:"Ask your API",i_am_stuck:"Say it is stuck"};function levelSites(level){return level==="off"?["Only this computer"]:level==="standard"?["Any website but the closed ones"]:RECOMMENDED_GROUPS.map(g=>g.name)}function agentPowers(){let yes=label3=>[label3,!0],no=label3=>[label3,!1],reads=[yes("Read your project"),no("Change code"),no("Run commands"),no("No internet")];return{analyst:{can:reads},architect:{can:reads},builder:{can:[yes("Read your project"),yes("Change code"),yes("Run commands"),yes("The websites your network level allows"),yes("squad-kit's tools: pictures, checks, the app")]},reviewer:{can:[...reads,yes("Look at a page of this story's app")]},tester:{can:[...reads,yes("squad-kit's tools: pictures, the app, your API")]}}}function rowDetails(meta,root){let min=ms2=>`${ms2/6e4} min`,repos=meta?reposOf(meta):[],run7=(...keys)=>repos.map(r=>[repos.length>1?`${r.path}: `:"",keys.map(k=>r.commands[k]).filter(Boolean).join(", ")]).filter(([,c])=>c).map(([p,c])=>`${p}${c}`).join("; ")||"Not set yet",guard=guardSupport(),tools4=sandboxStatus(),level=parseNetwork(meta?.network)??DEFAULT_NETWORK,state=(on,why)=>`${on?"On":"Off"}: ${why}`,names2=list8=>list8?.map(f=>f.path).join(", ")||"None yet",tab=name=>`The story's ${name} tab`,code=(file3,...more3)=>[...more3,["In the code",`src/${file3}`]];return{"tool-ready":code("engine/onboard.ts",["Found",meta?.tools?.map(t=>`${t.name}: ${t.ready?"ready":"not found"}`).join(", ")||"Not looked yet"]),"first-run":code("engine/onboard.ts",["Runs",run7("setup","test","start")],["See it in","The Overview page"]),overview:code("board/first-run.ts",["See it in","The Overview page"]),"gate-cant-run":code("engine/gate-run.ts",["File",".squad/harness.yaml"]),"tool-missing":code("engine/tool-missing.ts",["Reads","No Android SDK found, JAVA_HOME is not set, xcrun: error, command not found"],["Runs again","Once, with no try counted"]),"project-rules":code("engine/repos.ts",["Reads",(root?rulesFiles(root).map(f=>f.slice(root.length+1)):[]).join(", ")||"None found yet"]),"brd-questions":code("engine/breakdown-job.ts",["Limit",`${MAX_QUESTIONS} questions`]),"brd-approve":code("board/requirements-actions.ts",["See it in","The Requirements page"]),"brd-queue":code("board/requirements-build.ts",["Places",`${parallelLimit(meta?.parallel)} at once`]),"first-story":code("board/first-story.ts"),questions:code("engine/question-filter.ts",["Refused",Object.values(HARNESS_REFUSED).join(", ")],["Tries",`${2} back to its agent`]),spec:code("board/actions.ts",["See it in",tab("Spec")]),ask:code("engine/live.ts"),chat:code("board/actions.ts"),plan:code("board/actions.ts",["See it in",tab("Design")]),decisions:code("board/actions.ts",["See it in","The Architecture page"]),depends:code("board/depends.ts"),change:code("board/actions.ts"),sandbox:code("engine/sandbox.ts",["On this computer",tools4.map(t=>state(t.available,t.why)).join(", ")||"Off"],["Network level",NETWORK_NAME[level]],["Also allowed",meta?.allowedHosts?.join(", ")||"None yet"],["Lists","src/engine/network-lists.ts"]),"flutter-stamps":code("engine/flutter-sdk.ts",...FLUTTER_STAMPS.map(f=>[f.file,f.why]),["In",flutterOnPath()?.root??"No Flutter on this computer"],["On Linux","Each command writes them in a throwaway bin/cache that goes when it ends, so Flutter's own folder stays as it was"]),xcode:code("engine/checks.ts",["Runs outside the guard","xcodebuild build and test, only as squad-kit writes them for a native iPhone app (outsideTheGuard)"],["Keeps","Xcode's own sandboxes: for a package's manifest and plugins, and for build scripts when the project turns that on"],["Builds in","build/squad-kit-ios in the story's copy, with the guard's caches"]),guard:code("engine/project-guard.ts",["On this computer",state(guard.on,guard.why)],["Network",networkPolicy(level).commands==="this-computer"?"Only this computer (Off)":"Open"],["Also allowed",[...meta?.guard?.allowRead??[],...meta?.guard?.allowWrite??[]].join(", ")||"None yet"]),lane:code("engine/lane.ts",["Locked here",names2(meta?.lockedFiles)]),"local-files":code("engine/local-files.ts",["Local here",names2(meta?.localFiles)]),"env-file":code("engine/env-file.ts",["Here",repos.flatMap(r=>envFilesOf(r.commands).map(f=>`${repos.length>1?`${r.path}: `:""}${f}`)).join(", ")||"None yet"],["Set in","Settings, beside the commands"],["Order","The file's values first, then squad-kit's own: a Docker service reached through the port it publishes here, the story's own database and Redis number, and the port it picked"],["In the log","The names only, never a value"]),"new-tests":code("engine/prompts.ts",["Here",meta?.alwaysNewTests?"Always on: every story writes new tests":"Your choice on each new story, on by default"],["Set in","Settings"]),checks:code("engine/checks.ts",["Runs",run7("test","lint","build")],["Limit",`${MAX_CHECK_TRIES} tries, ${min(CHECK_MS)} each, ${min(CHECK_SILENT_MS)} with no output`],["Decided by","The exit code, never an agent"],["See it in",tab("Developer")]),"old-tests":code("engine/old-tests.ts",["Runs",run7("test")],["Limit",`${MAX_OLD} failed tests, each up to ${MAX_RUNS_ON_MAIN} times, ${min(ON_MAIN_MS)} in all`],["Reads","Playwright, Vitest, Jest, pytest and go test"],["See it in","The proof page and the Releases page"]),weakened:code("engine/weakened.ts",["Looks at","Deleted tests, skip marks, fewer tests, test settings"],["See it in",tab("Changes")]),"no-tests":code("engine/not-checked.ts",["Here",meta?.noTests?"You chose to go on without tests":"Not asked yet"]),"setup-again":code("engine/live.ts",["Runs",run7("setup")],["Looks at","Package files and database changes, saved or not"]),"test-change":code("engine/test-change.ts",["Runs",run7("test")],["Compares","Pass or fail, and how many tests ran"]),review:code("engine/live.ts",["Limit",`${MAX_REVIEW_ROUNDS} rounds, ${min(REVIEW_STEP_MS)} each`],["See it in",tab("Conversation")]),"reviewer-floor":code("engine/levels.ts"),"also-changed":code("board/also-changed.ts",["Blocks","Nothing"]),proof:code("engine/live.ts",["Runs",run7("start")],["Limit",`${6} for each, ${40} in one step, ${min(PROOF_STEP_MS)}`]),"bad-proof":code("engine/proof-check.ts"),"not-shown":code("engine/proof-verdict.ts",["Limit",`${2} retakes`]),broken:code("engine/proof-verdict.ts",["Limit",`${2} rounds`]),"off-screen":code("engine/proof-verdict.ts"),"api-proof":code("engine/window.ts",["Limit",`${MAX_API_CALLS} calls in one proof`]),"phone-pictures":code("engine/phone/session.ts",["Devices","squad-kit iPhone and squad-kit iPad, squad-kit Android phone and squad-kit Android tablet, its own, one started at a time"],["Which phone",meta?.phone==="iphone"?"The iPhone":meta?.phone==="android"?"Android":"Automatic"],["Set in","Settings"],["Taps with","Maestro"]),"phone-builds":code("engine/phone/build.ts",["Builds in","build/squad-kit-ios and build/squad-kit-android in the story's copy"],["Android","flutter build apk --debug, or ./gradlew assembleDebug (React Native: assembleRelease); then gradlew --stop"],["Puts back","What the build changed in the story's files"]),"tool-license":code("engine/tools/android.ts",["Licenses","android-sdk-license, and android-sdk-arm-dbt-license for an Arm system image"],["See it in","The Tools page"]),"phone-tools":code("engine/tools/android.ts",["Room","About 20 GB"],["No new bytes for","2 min, then once more"],["See it in","The Tools page"]),window:code("engine/window.ts",["Limit",`${REFUSALS_TO_STOP} refusals, ${MISTAKES_TO_STOP} calls in the wrong shape`]),"looks-right":code("board/actions.ts",["See it in","The proof page"]),"take-over":code("board/actions.ts"),outside:code("board/outside.ts",["File",".squad/how-to-work-here.md"]),"accept-order":code("board/depends.ts"),hook:code("engine/git.ts",["Limit",min(HOOK_MS)]),merge:code("board/bring-in.ts",["Limit",`${MAX_BASE_TRIES} tries`]),"pull-request":code("engine/sync.ts",["Limit",`${min(NETWORK_GIT_MS)} to reach your code host`]),checkout:code("board/auto.ts"),release:code("engine/release.ts",["Runs",run7("deploy")],["Limit",min(PIPELINE_MS)]),"leave-out":code("engine/leave-out.ts"),"release-clash":code("engine/release.ts"),"release-lock":code("board/lock.ts"),"room-for-copy":code("engine/git.ts"),watchdog:code("engine/watchdog.ts",["Limit",`Looks after ${min(QUIET_MS)} quiet, says taking long after ${min(LONG_MS)}`]),"usage-limit":code("engine/usage-limit.ts",["Looks again",`Every ${min(RETRY_MS)} when the tool does not say when`]),"step-up":code("engine/levels.ts"),cap:code("engine/caps.ts"),budget:code("board/budget.ts",["Set in","Settings"]),"ways-out":code("board/actions.ts"),"plain-words":code("engine/plain-stop.ts"),interrupted:code("engine/interrupt.ts"),blips:code("engine/live.ts",["Waits",`${BLIP_WAITS_MIN.join(", ")} min`]),leftovers:code("engine/leftovers.ts"),"product-lock":code("engine/product-lock.ts",["Limit",min(WAIT_LIMIT_MS)]),"save-trouble":code("engine/save-trouble.ts"),"board-lock":code("console/auth.ts"),report:code("engine/report.ts"),lead:code("engine/lead/ask.ts",["Runs as","Read only and git read: it never edits, runs the project or commits"],["Always allow here",(meta?.lead?.always??[]).map(a=>ACTIONS2[a]?.name??a).join(", ")||"Nothing yet"],["Its notes","squad-kit's own folder on this computer, never your repo"],["Refusals","engine/lead/can.ts"])}}function trackCell(stage,g,track2){let note2=g.note?.[track2]?{note:g.note[track2]}:{};return g.skip?.includes(track2)?{word:"By itself",...note2}:stage.on[track2].off?{word:"Not used"}:{word:g.you?"You":"Runs",...note2}}var RULE_STAGE={all:"build",analyst:"story",architect:"plan",builder:"build",reviewer:"review",tester:"proof"},additionStage=a=>a.kind==="gate"?a.when:a.kind==="checklist"?a.agent==="reviewer"?"review":"proof":RULE_STAGE[a.agents.length===1?a.agents[0]:"all"];function additionRow(a){let by2=[["Added by",a.added_by],["From",a.from==="typed"?"Typed here":`The folder ${a.from}`]],team={state:a.state,kind:a.kind},row3={id:`add:${a.id}`,name:a.name,team};if(a.kind==="rule")return{...row3,what:a.text,d:by2};if(a.kind==="checklist"){let fail2=a.state==="try"?"The answers show on the story. A no stops nothing.":a.agent==="reviewer"?"A no counts as a review finding.":'A no goes the same way as a "done when" not shown.';return{...row3,what:`The ${a.agent==="reviewer"?"Reviewer":"Tester"} answers: ${a.questions.join(" ")}`,fail:fail2,d:by2}}let fail=a.state==="try"?'It is noted as "would have stopped". Nothing stops.':a.on_fail==="warn"?"It is noted. Nothing stops.":{checks:`The Builder fixes it, up to ${MAX_CHECK_TRIES} tries. Then a card.`,merge:"The change is not saved, and a card has the ways out.",release:"The release does not go out, and the page has the ways out."}[a.when];return{...row3,what:"A command squad-kit runs itself. No agent is asked if it passed.",fail,d:[["Command",a.command],...a.part?[["Part",a.part]]:[],...by2]}}function harnessView(meta,bp){let quality=meta?.quality??DEFAULT_QUALITY,caps=QUALITY_PRESETS[quality].caps,seen=meta&&bp?harnessSeen(bp,meta):void 0,details=rowDetails(meta,bp?.root),addedOn=new Set(bp?readHarness(bp).additions.filter(a=>a.state!=="off").map(a=>`add:${a.id}`):[]),waiting2=seen?.needs.map(n=>n.row.startsWith("add:")&&!addedOn.has(n.row)?{...n,row:"gate-cant-run"}:n),fill3=r=>{let needs=waiting2?.filter(n=>n.row===r.id);return{...r,d:details[r.id]??r.d??[],...seen?.rows[r.id]?{seen:seen.rows[r.id]}:{},...needs?.length?{needs}:{}}},file3=bp?readHarness(bp):{additions:[],log:[]},live=file3.additions.filter(a=>a.state!=="off"),addedCell=a=>({word:"Runs",...a.state==="try"?{note:"Try first"}:{}}),stages=lineStages().map(s=>{let gates=[...s.gates.map(g=>({...fill3(g),cells:{full:trackCell(s,g,"full"),express:trackCell(s,g,"express"),auto:trackCell(s,g,"auto")}})),...live.filter(a=>additionStage(a)===s.id).map(a=>({...fill3(additionRow(a)),cells:{full:addedCell(a),express:addedCell(a),auto:addedCell(a)}}))];return{...s,gates,stopped:new Set(gates.flatMap(g=>seen?.weekIds[g.id]??[])).size}}),always=alwaysOnRows().map(fill3),rows2=[...stages.flatMap(s=>s.gates),...always],offHere=(sandboxStatus().some(t=>t.available)?0:1)+(guardSupport().on?0:1),level=parseNetwork(meta?.network)??DEFAULT_NETWORK;return{quality,qualityNote:QUALITY_NOTE,range:{days:SEEN_DAYS,weekDays:WEEK_DAYS,stories:seen?.stories??0},tiles:{working:rows2.length-offHere,stopped:rows2.filter(r=>r.seen?.week).length,needs:seen?.needs??[]},tracks:[{id:"full",name:"Full",words:"On Full you answer the questions and approve the plan."},{id:"express",name:"Express",words:"On Express there are no questions and the plan is approved on its own. You still check the work."},{id:"auto",name:BUILDS_BY_ITSELF,words:"Approved in advance, it merges when the checks, the review and the proof pass. Release stays yours."}],stages,alwaysOn:always,limits:limitRows(meta).map(l=>seen?.hits[l.id]?{...l,hit:seen.hits[l.id]}:l),caps:Object.keys(HARNESS_CAP_NAME).map(key=>({key,name:HARNESS_CAP_NAME[key],...caps[key]})),agents:agentPowers(),lead:leadRows(meta,bp),additions:{items:file3.additions.map(a=>({...a,stage:additionStage(a),...seen?.rows[`add:${a.id}`]?{seen:seen.rows[`add:${a.id}`]}:{},...a.kind==="rule"&&ruleNote(a.text)?{note:ruleNote(a.text)}:{},...a.kind==="gate"&&a.part&&meta&&!partsOf2(meta).some(p=>p.id===a.part)?{partGone:!0}:{}})),log:file3.log.slice(-200).reverse(),..."broken"in file3&&file3.broken?{broken:file3.broken}:{},parts:meta&&severalParts(meta)?partsOf2(meta).filter(p=>!p.shared).map(p=>({id:p.id,name:p.name})):[],guarded:guardSupport().on,demo:meta?.engine!=="live",shared:bp?docsInGit(bp):!1,limits:{rules:MAX_RULES,ruleChars:MAX_RULE_CHARS,questions:MAX_CHECKLIST_QUESTIONS}},safety:{hidden:[...PRIVATE.map(p=>HARNESS_PRIVATE_NAME[p]??p),"Your AI tools' sign-ins","Browser sign-ins","Keys in package settings",".env files","squad-kit's own folder and keys"],locked:["squad-kit's own folder","AI instruction files","Commit hooks and their settings","Code history: commit and branches. A push is refused, and agents have no keys to push with"],sites:levelSites(level),network:{level,levels:NETWORK_LEVELS.map(id2=>({id:id2,name:NETWORK_NAME[id2],words:NETWORK_WORDS[id2]})),closed:KNOWN_BAD_GROUPS.map(g=>g.name),commands:networkPolicy(level).commands},refused:Object.values(HARNESS_REFUSED),tools:Object.values(HARNESS_TOOL_NAME),local:(meta?.localFiles??[]).map(f=>f.path),lockedByYou:(meta?.lockedFiles??[]).map(f=>f.path)}}}import path161 from"path";var STEP_NAME={questions:"Questions",spec:"Spec",design:"Design",build:"Build",fix:"Fixes",checks:"Checks",review:"Review",proof:"Proof"},SESSION={questions:"intake",spec:"intake",design:"intake",build:"build",fix:"build",checks:void 0,review:"review",proof:"tester"},ENDS=[{step:"questions",who:"analyst",by:["analyst"],re:/^Asked \d+ questions?$/},{step:"spec",who:"analyst",by:["analyst"],re:/^(Wrote|Updated) the spec\b/},{step:"design",who:"architect",by:["architect","spec"],re:/^(Wrote|Updated) the (design|plan)\b/},{step:"build",who:"builder",by:["builder"],re:/^Finished all steps$/},{step:"fix",who:"builder",by:["builder"],re:/^Fixed (the|what)\b/},{step:"checks",who:"squad",by:["tester"],re:/^(All (\d+ )?checks pass|Checks failed|Checks still fail|No test, lint or build commands)/},{step:"review",who:"reviewer",by:["reviewer"],re:/^(Approved\b|Asked for \d+ fix|Still asks for)/},{step:"proof",who:"tester",by:["tester"],re:/^(Saved \d+ screenshots?|No screenshots were taken|No screens to show|No screens change)/}],AFTER={questions:[{re:/^Answered \d+ questions?/,who:["you"],note:"you answered"}],spec:[{re:/^Approved the spec/,who:["you"],note:"you approved"}],design:[{re:/^Approved the (design|plan)/,who:["you"],note:"you approved"},{re:/^Express track: approved the plan on its own/,who:["squad"],note:"approved on its own (Express track)"},{re:/^Builds by itself: approved the plan on its own/,who:["squad"],note:"approved on its own (Builds by itself)"}],proof:[{re:/^Checked the proof\. Looks right/,who:["you"],note:"you checked it: Looks right"},{re:/^(.+ passed: a|A)ccepted by itself/,who:["squad"],note:"accepted by itself"}]},TOOLISH=/^(Reading|Running|Editing|Writing|Looking|Searching|Listing|Runs on|Asked|Started|Finished|squad-kit|The answer was not complete)\b/,REFUSED=/User refused permission to run tool|^Permission denied: |haven't granted it yet/i,ms=iso2=>iso2?Date.parse(iso2):NaN;function oldestFirst(t){return[...t.activity].sort((a,b)=>a.at.localeCompare(b.at))}function runsOf2(t,log3){let history=t.history??[],first2=history.length?ms(history[0].from):1/0,fromLog=[];for(let a of log3){if(ms(a.at)>=first2)break;let end=ENDS.find(e=>e.by.includes(a.who)&&e.re.test(a.text));end&&fromLog.push({who:end.who,step:end.step,to:a.at,line:a})}let timed=history.map(h=>({...h,line:endLine(log3,h)}));return[...fromLog,...timed]}function endLine(log3,h){let end=ENDS.find(e=>e.step===h.step),lo=ms(h.from)-1e3,hi=ms(h.to)+6e4;return log3.find(a=>ms(a.at)>=lo&&ms(a.at)<=hi&&end.by.includes(a.who)&&end.re.test(a.text))}var READING=/^Reading the changes \(round (\d+) of/;function reviewRound(log3,run7){if(!run7.from)return;let lo=ms(run7.from)-6e4,hi=ms(run7.to),m=log3.find(a=>a.who==="reviewer"&&ms(a.at)>=lo&&ms(a.at)<=hi&&READING.test(a.text))?.text.match(READING);return m?Number(m[1]):void 0}function plural4(n,one2,many2=`${one2}s`){return`${n} ${n===1?one2:many2}`}function analystSummary(log3,run7){let end=ms(run7.line?.at??run7.to);return log3.filter(a=>a.who==="analyst"&&Math.abs(ms(a.at)-end)<=5e3&&a!==run7.line&&!TOOLISH.test(a.text)).at(-1)?.text}function summaryOf2(t,run7,log3){let line3=run7.line?.text??"";switch(run7.step){case"questions":{let n=t.questions?.length??Number(/^Asked (\d+)/.exec(line3)?.[1]??0),read9=analystSummary(log3,run7),asked=n?`asked you ${plural4(n,"question")}`:"had no questions";return read9?`${read9.replace(/\.?$/,".")} Then ${asked}.`:`Read the ticket and ${asked}.`}case"spec":{if(/^Updated the spec: /.test(line3))return line3.replace(/\.?$/,".");let answers2=(t.questions??[]).filter(q5=>q5.answer).length,points=t.criteria?.length??0;return`Wrote the spec${/from your talk/.test(line3)?" from your talk":answers2?` from your ${plural4(answers2,"answer")}`:""}${points?`, with ${plural4(points,'"done when" point')}`:""}.`}case"design":{if(/^Updated the (design|plan): /.test(line3))return line3.replace(/\.?$/,".");let steps=t.steps?.length??0,said2=t.decision?.options.find(o=>o.chosen)?.name?.replace(/^(A|An|The|One|Its|Their|Our|Your)\b/,w=>w.toLowerCase()),parts2=[said2?`Chose ${said2}`:"Wrote the design",steps?`planned ${plural4(steps,"step")} to build`:"",t.mockup?"made a mockup":""].filter(Boolean);return`${parts2.length>1?`${parts2.slice(0,-1).join(", ")} and ${parts2.at(-1)}`:parts2[0]}.`}case"build":{let steps=t.steps?.length??0;return steps?`Did the ${plural4(steps,"step")} of the design.`:"Built the change."}case"fix":return line3?line3.replace(/\.?$/,"."):"Fixed what the checks or the review found.";case"checks":return/^All/.test(line3)?`squad-kit ran the project's checks: ${line3.replace(/^All /,"all ").replace(/\.?$/,".")}`:line3?`squad-kit ran the project's checks. ${line3.replace(/\.?$/,".")}`:"squad-kit ran the project's checks.";case"review":return/^Approved: /.test(line3)?line3.replace(/\.?$/,"."):/^Approved/.test(line3)?"Approved the changes.":line3?`${line3.split(":")[0]}.`:"Read the changes.";case"proof":{let n=t.proof?.length??0,pictures=(t.proof??[]).reduce((sum,p)=>sum+1+(p.more?.length??0),0);return pictures>n?`Opened the app and took ${plural4(pictures,"screenshot")} of the ${plural4(n,'"done when" point')} it could show.`:n?`Opened the app and took ${plural4(n,"screenshot")}, one for each "done when" point it could show.`:line3?line3.replace(/\.?$/,"."):"Checked the change."}}}function filesFrom(conv,cwd,from,to){let added2=new Set,changed=new Set;if(!conv)return{added:[],changed:[]};let lo=ms(from),hi=ms(to),rel=p=>cwd&&p.startsWith(`${cwd}/`)?p.slice(cwd.length+1):path161.basename(p);for(let e of conv.updates){let at3=ms(e.at);if(Number.isFinite(lo)&&Number.isFinite(at3)&&(at3<lo||at3>hi))continue;let u=e.update;if(!(u.sessionUpdate!=="tool_call"&&u.sessionUpdate!=="tool_call_update"))for(let c of u.content??[]){if(c.type!=="diff"||!c.path)continue;let p=rel(c.path);c.oldText==null&&!changed.has(p)?added2.add(p):added2.has(p)||changed.add(p)}}return{added:[...added2],changed:[...changed]}}function handedIn(t,run7,files,conv){switch(run7.step){case"questions":return t.questions?.length?{kind:"questions",items:t.questions.map(q5=>({from:q5.from,text:q5.text,...q5.answer?{answer:q5.answer.value?`${q5.answer.choice.replace(/:$/,"")}: ${q5.answer.value}`:q5.answer.choice}:{}}))}:void 0;case"spec":return t.story||t.criteria?.length?{kind:"spec",...t.story?{story:t.story}:{},criteria:(t.criteria??[]).map(c=>c.then)}:void 0;case"design":{if(!t.decision&&!t.steps?.length)return;let chosen2=t.decision?.options.find(o=>o.chosen)?.name;return{kind:"design",...t.decision?{title:t.decision.title}:{},...chosen2?{chosen:chosen2}:{},...t.decision?.adr?{adr:t.decision.adr}:{},steps:(t.steps??[]).map(s=>s.title),mockup:!!t.mockup,follows:t.follows??[]}}case"build":case"fix":{let mine2=run7.step==="fix"||!files?filesFrom(conv,conv?.cwd,run7.from,run7.to):null,got=mine2?{...mine2,removed:[]}:files,steps=run7.step==="build"?(t.steps??[]).filter(s=>s.status==="done").map(s=>s.title):[];return!got.added.length&&!got.changed.length&&!got.removed.length&&!steps.length?void 0:{kind:"files",added:got.added,changed:got.changed,removed:got.removed,steps}}case"checks":return t.gates?.checks&&t.gates.checks!=="pending"?{kind:"checks",ok:t.gates.checks==="pass"||t.gates.checks==="skipped",text:t.gates.checksText??(t.gates.checks==="pass"?"All checks pass":"Some checks failed")}:void 0;case"review":{let line3=run7.line?.text??"",approved=/^Approved/.test(line3)||!line3&&t.gates?.review==="pass",notes=approved?[]:t.reviewNotes?.length?t.reviewNotes:line3.includes(": ")?[line3.slice(line3.indexOf(": ")+2)]:[];return{kind:"verdict",approved,notes}}case"proof":return t.proof?.length?{kind:"shots",shots:t.proof.flatMap(p=>[p.after,...(p.more??[]).map(m=>m.after)].filter(file3=>!file3.endsWith(".api.json")).map(file3=>({criterion:p.criterion,file:file3})))}:void 0}}var TAB={spec:"plan",design:"plan",build:"changes",fix:"changes",checks:"proof",proof:"proof"};function counts2(conv,from,to){if(!conv||!from)return null;let lo=ms(from),hi=to?ms(to):1/0,inside5=at3=>{let x=ms(at3);return Number.isFinite(x)&&x>=lo-1e3&&x<=hi+1e3},actions=0,skipped=0;for(let e of conv.updates){if(!inside5(e.at))continue;let u=e.update;u.sessionUpdate==="tool_call"&&(actions+=1),u.sessionUpdate==="tool_call_update"&&u.status==="failed"&&(u.content??[]).some(c=>c.type==="content"&&c.content?.type==="text"&&REFUSED.test(c.content.text??""))&&(skipped+=1)}return{actions,skipped}}function whatHappened(bp,t,src={}){let now4=src.now??Date.now(),log3=oldestFirst(t),runs2=runsOf2(t,log3),read9=src.conversation??(id2=>readConversation(id2,3e3,void 0,bp.chatsDir)),convs=new Map,conv=key=>{let id2=key?t.sessions?.[key]:void 0;if(!id2)return null;if(!convs.has(id2))try{convs.set(id2,read9(id2))}catch{convs.set(id2,null)}return convs.get(id2)??null},files,ticketFiles=()=>{if(files===void 0){try{files=src.files?.()??null}catch{files=null}files&&!files.added.length&&!files.changed.length&&!files.removed.length&&(files=null)}return files},latest=new Map;runs2.forEach((r,i)=>latest.set(r.step,i));let reviews=runs2.filter(r=>r.step==="review").length,steps=runs2.map((run7,i)=>{let next=runs2[i+1],session=SESSION[run7.step],c=conv(session),isLatest=latest.get(run7.step)===i,seconds2=run7.from?Math.max(0,(ms(run7.to)-ms(run7.from))/1e3):void 0,notes=[],until=next?ms(next.from??next.to):1/0;for(let a of AFTER[run7.step]??[])log3.some(l=>a.who.includes(l.who)&&a.re.test(l.text)&&ms(l.at)>=ms(run7.to)-1e3&&ms(l.at)<=until)&¬es.push(a.note);if(run7.step==="review"){let k=runs2.slice(0,i+1).filter(r=>r.step==="review").length,round=reviewRound(log3,run7),fresh=round!==void 0?round===1:k===1;notes.push(reviews>1?`round ${k} of ${reviews}, ${fresh?"a fresh session":"the same Reviewer as the round before"}`:"a second, separate session")}let got=isLatest?handedIn(t,run7,run7.step==="build"?ticketFiles():null,c):void 0,changed=got?.kind==="files"?got.added.length+got.changed.length+got.removed.length:0,n=counts2(c,run7.from,run7.to),tab=TAB[run7.step];return{who:run7.who,step:run7.step,name:`${WHO[run7.who]} \xB7 ${STEP_NAME[run7.step]}`,...run7.from?{from:run7.from}:{},to:run7.to,...seconds2!==void 0?{seconds:seconds2}:{},summary:run7.step==="build"&&changed?summaryOf2(t,run7,log3).replace(/\.$/,` and changed ${plural4(changed,"file")}.`):summaryOf2(t,run7,log3),...got?{handedIn:got}:{},...got&&tab?{tab}:{},notes,...session&&t.sessions?.[session]?{session}:{},...n?{actions:n.actions,skipped:n.skipped}:{}}}),ck=t.clock;if(ck&&!t.waiting){let from=ck.began??ck.since,session=SESSION[ck.step],n=counts2(conv(session),from),part=ck.step==="build"&&ck.part&&t.steps?.length?` Step ${ck.part} of ${t.steps.length}.`:"";steps.push({who:ck.who,step:ck.step,name:`${WHO[ck.who]} \xB7 ${STEP_NAME[ck.step]}`,from,seconds:Math.max(0,(now4-ms(from))/1e3),running:!0,summary:`${(ck.doing??t.status)||"Working on it"}${ck.doing?".":""}${part}`.replace(/\.\./g,"."),notes:[],...session&&t.sessions?.[session]?{session}:{},...n?{actions:n.actions,skipped:n.skipped}:{}})}return{steps,seconds:steps.reduce((s,x)=>s+(x.seconds??0),0)}}async function body2(c){try{return await c.req.json()}catch{return{}}}async function changedFiles3(bp,id2){let{files}=await ticketChanges(bp,id2);return files.length?{added:files.filter(f=>f.status==="added").map(f=>f.path),changed:files.filter(f=>f.status==="changed"||f.status==="renamed").map(f=>f.path),removed:files.filter(f=>f.status==="deleted").map(f=>f.path)}:null}function mountCostApi(app,bp){app.get("/api/v1/tickets/:id/happened",async c=>{let t=readTicket(bp,c.req.param("id"));if(!t)return c.json({error:"not found"},404);let files=await changedFiles3(bp,t.id).catch(()=>null);return c.json(whatHappened(bp,t,{files:()=>files}))}),app.get("/api/v1/tickets/:id/cost",async c=>{let t=readTicket(bp,c.req.param("id"));return t?c.json(ticketCost(bp,t,await howPaid(bp))):c.json({error:"not found"},404)}),app.get("/api/v1/cost",async c=>boardExists(bp)?c.json(costOverview(bp,await howPaid(bp))):c.json({error:"No board here yet."},404)),app.put("/api/v1/cost/payment",async c=>{let b=await body2(c);return b.payment!=="subscription"&&b.payment!=="api-key"&&b.payment!=="auto"?c.json({error:"Pick subscription, api-key or auto."},400):(setPaymentChoice(b.payment),c.json(costOverview(bp,await howPaid(bp))))}),app.put("/api/v1/cost/budget",async c=>{let b=await body2(c);try{let change={};"ticketUsd"in b&&(change.ticketUsd=parseUsd(b.ticketUsd)),"monthUsd"in b&&(change.monthUsd=parseUsd(b.monthUsd));let budget=setBudget(bp,change);return c.json({...costOverview(bp,await howPaid(bp)),budget})}catch(err2){return c.json({error:plainWords(err2,"Saving the budget")},400)}})}import path162 from"path";function checkWhereCodeGoes(dir,base2,remote){if(!isRepo(dir))return;let remotesHere=remotes(dir);if(remote&&!remotesHere.includes(remote))throw new Error(`There is no remote called "${remote}" here. ${remotesHere.length?`It has: ${remotesHere.join(", ")}.`:"It has none yet."} Nothing was saved.`);let branches=git(dir,["for-each-ref","--format=%(refname:short)","refs/heads",...remote?[`refs/remotes/${remote}`]:[]],{allowFail:!0}).split(`
|
|
1952
1952
|
`).map(b=>b.replace(`${remote}/`,"")).filter(b=>b&&b!=="HEAD"&&b!==remote);if(branches.length&&!branches.includes(base2))throw new Error(`There is no branch called "${base2}" here. It has: ${[...new Set(branches)].slice(0,8).join(", ")}. Nothing was saved.`)}function saveSettings(bp,b){let meta=readMeta(bp);if(b.allowedHosts!==void 0){let parsed=parseHosts(Array.isArray(b.allowedHosts)?b.allowedHosts:[]),problem2=hostsProblem(parsed);if(problem2)throw new Error(problem2);meta.allowedHosts=parsed.hosts}if(b.network!==void 0){let level=parseNetwork(b.network);if(!level)throw new Error(`"${b.network}" is not a network level. Use standard, strict or off.`);meta.network=level}if(b.guard!==void 0){let clean4=list8=>[...new Set((Array.isArray(list8)?list8:[]).map(p=>String(p).trim()).filter(Boolean).map(p=>sharedPath(p)))],read9=clean4(b.guard.allowRead),write2=clean4(b.guard.allowWrite);meta.guard=read9.length||write2.length?{...read9.length?{allowRead:read9}:{},...write2.length?{allowWrite:write2}:{}}:void 0}if(b.quality!==void 0){let q5=parseQuality(b.quality);if(!q5)throw new Error(`"${b.quality}" is not a Quality setting. Use best, balanced or thrifty.`);meta.quality=q5}if(b.commands&&(meta.commands=Object.fromEntries(Object.entries(b.commands).map(([k,v])=>[k,String(v??"").trim()]).filter(([,v])=>v))),b.git){let next={base:b.git.base?.trim()||meta.git?.base||"main",remote:b.git.remote?.trim()??meta.git?.remote??""};checkWhereCodeGoes(bp.root,next.base,next.remote),meta.git=next}if(typeof b.parallel=="number"&&b.parallel>=1&&b.parallel<=5&&(meta.parallel=Math.round(b.parallel)),b.delivery===null?delete meta.delivery:(b.delivery==="pr"||b.delivery==="push")&&(meta.delivery=b.delivery),b.atOnce===null?delete meta.atOnce:typeof b.atOnce=="number"&&b.atOnce>=1&&b.atOnce<=5&&(meta.atOnce=Math.round(b.atOnce)),(b.runner==="acp"||b.runner==="direct")&&(meta.runner=b.runner),typeof b.documentsInGit=="boolean"&&(meta.documentsInGit=b.documentsInGit),b.alwaysNewTests===!0?meta.alwaysNewTests=!0:b.alwaysNewTests===!1&&delete meta.alwaysNewTests,b.phone!==void 0){let choice=parseChoice(b.phone);if(!choice)throw new Error(`"${b.phone}" is not a phone here. Use iphone, android or auto.`);choice==="auto"?delete meta.phone:meta.phone=choice}return b.repos&&meta.repos&&(meta.repos=meta.repos.map(r=>{let u=b.repos.find(x=>x.path===r.path);if(!u)return r;let commands=Object.fromEntries(Object.entries(u.commands??r.commands).map(([k,v])=>[k,String(v??"").trim()]).filter(([,v])=>v)),next={base:u.base?.trim()||r.base,remote:u.remote?.trim()??r.remote};return checkWhereCodeGoes(path162.join(bp.root,r.path),next.base,next.remote),{...r,...next,commands}})),checkEnvFiles(meta),writeMeta(bp,meta),meta}import{createHash as createHash15}from"crypto";import fs169 from"fs";import path163 from"path";function docsHash(bp,id2){let said2=["spec.md","design.md","questions.md","proof.md"].flatMap(f=>{try{return[f,fs169.readFileSync(path163.join(ticketDir(bp,id2),f),"utf8")]}catch{return[]}});return said2.length?createHash15("sha256").update(said2.join("\0")).digest("hex").slice(0,16):NO_DOCS}var MOST_HASHED=20*1024*1024;async function workOf2(meta,t){let hash2=createHash15("sha256"),built=!1;for(let c of copiesOf(meta,t)){if(!c.baseCommit||!fs169.existsSync(c.dir))continue;let files=(await workSoFar(c.dir,c.baseCommit)).sort();if(files.length){built=!0,hash2.update(await gitLater(c.dir,["rev-parse","HEAD"],{allowFail:!0}));for(let file3 of files){hash2.update(`\0${file3}\0`);try{let full=path163.join(c.dir,file3),seen=await fs169.promises.stat(full);hash2.update(seen.size>MOST_HASHED?`${seen.size}:${seen.mtimeMs}`:await fs169.promises.readFile(full))}catch{}}}}return{built,hash:built?hash2.digest("hex").slice(0,16):"none"}}function refuseLeaving(bp,t,meta){if(t.takenOver)throw new BoardActionError(`${t.id} is yours already. Give it back first.`);if(!isMine(t,meta))throw new BoardActionError(`${t.id} is ${ownerOf(t)}'s. Only they can move it.`);if(t.deleting)throw new BoardActionError(`${t.id} is being deleted.`);if(t.archived)throw new BoardActionError(`${t.id} is archived.`);if(shipped(t))throw new BoardActionError(`${t.id} is accepted already. Report a bug or add a new ticket instead.`);if(t.chat?.open&&!t.chat.beside)throw new BoardActionError(`${t.id} has a chat open. Finish the talk first.`);let change=openChange(t);if(change)throw new BoardActionError(`${t.id} has a change still open (${change.id}). Settle it first.`);if(squadAlive(bp)&&(t.live||t.squad||t.stopRequested||t.questions?.some(q5=>q5.thinking)))throw new BoardActionError(`${t.id} is working right now. Wait until it needs you, or press Stop first.`)}async function workOutside(bp,id2){refuseWhileSaving(bp,id2);let meta=readMeta(bp);if(meta.engine!=="live")throw new BoardActionError("The sample board has no real project to work on outside.");let before=requireTicket(bp,id2);if(before.takenOver?.free)return before;refuseLeaving(bp,before,meta);let work=await workOf2(meta,before),docs=docsHash(bp,id2);return updateTicket(bp,id2,t=>{t.takenOver?.free||(refuseLeaving(bp,t,meta),leaveForOutside(t,meta.user.name,new Date().toISOString(),{docs,work:work.hash}),addActivity(t,"you","Moved it to a conversation outside squad-kit. The squad waits"))})}function asItWas(t,left2){t.stage=left2.stage,t.status=left2.status,t.waiting=left2.waiting,t.live=!1,left2.phase&&!left2.waiting&&(t.live=!0,scheduleSquad(t,left2.phase,.2,1)),addActivity(t,"you","Brought it back. Nothing changed outside")}var BUILT_OUTSIDE="Built outside squad-kit, and squad-kit has not checked it";async function bringBack2(bp,id2){let meta=readMeta(bp),before=requireTicket(bp,id2);if(!before.takenOver?.free)throw new BoardActionError(`${before.id} is not in a conversation outside squad-kit.`);let work=await workOf2(meta,before),docs=docsHash(bp,id2),approvedBy=`${meta.user.name} (outside squad-kit)`,approveDesign=!1,back=updateTicket(bp,id2,t=>{let taken=t.takenOver,left2=taken?.free;if(!taken||!left2)throw new BoardActionError(`${t.id} is back already.`);if(t.takenOver=void 0,t.stopRequested=void 0,t.retryOnStop=void 0,t.waiting=null,t.squad=null,docs===left2.docs&&work.hash===left2.work&&!left2.approved)return asItWas(t,left2);t.outside=t.outside?{...t.outside,checked:void 0}:{...left2.from?{from:left2.from}:{},by:taken.by,at:taken.at},t.stuck=void 0,t.agentQuestion=void 0,t.retryPhase=void 0,t.tries=0,t.reviewRound=0,t.reviewNotes=void 0,t.proof=void 0,t.offScreen=void 0,t.proofRound=void 0,t.live=!1,work.built?(left2.designApproved&&(t.decision=t.decision&&{...t.decision,adr:t.decision.adr??saveDesignDecision(bp,t,approvedBy)}),t.steps=t.steps?.map(s=>({...s,status:"done"})),t.gates={checks:"pending",review:"pending",proof:"pending"},t.stage="review",t.status="Back from outside. squad-kit has not checked it",t.waiting={kind:"proof",summary:BUILT_OUTSIDE},addActivity(t,"you","Brought it back with code built outside squad-kit")):hasDesign(t)?(t.stage="specs",addActivity(t,"you","Brought it back with a design written outside squad-kit"),left2.designApproved?(approveDesign=!0,t.status="Design approved in the conversation",t.waiting={kind:"plan",summary:"The design was approved in the conversation"}):t.track==="express"||t.track==="auto"?(t.live=!0,t.status="Approving the plan",scheduleSquad(t,"express-approve",.2,1)):(t.status="Design ready for you",t.waiting={kind:"plan",summary:"The design was written outside squad-kit: is this how to build it?"})):hasSpec(t)?(t.stage="specs",addActivity(t,"you","Brought it back with a spec written outside squad-kit"),needsSpec(t)?(t.status="Spec ready for you",t.waiting={kind:"spec",summary:"The spec was written outside squad-kit: is this what you want?"}):(t.live=!0,t.status="Architect is writing the design",scheduleSquad(t,"plan-write",.2,1))):(t.stage="tickets",t.live=!0,t.status="Analyst is reading the ticket",addActivity(t,"you","Brought it back. The squad carries on from the start"),scheduleSquad(t,"analyst-read",.2,1))});return approveDesign?approve(bp,back.id):back}function recordApproval(bp,id2,what){let meta=readMeta(bp);return updateTicket(bp,id2,t=>{let left2=t.takenOver?.free;if(!left2)throw new BoardActionError(`${t.id} is not in a conversation outside squad-kit.`);if(what==="spec"?!hasSpec(t):!hasDesign(t))throw new BoardActionError(`${t.id} has no ${what} written yet, so there is nothing to approve.`);t.specApproved??={by:`${meta.user.name} (outside squad-kit)`,at:new Date().toISOString()},left2.approved=!0,what==="design"&&(left2.designApproved=!0),addActivity(t,"you",`Approved the ${what} in the conversation outside squad-kit`)})}function checkOutside(bp,id2){return refuseWhileSaving(bp,id2),updateTicket(bp,id2,t=>{if(!t.outside)throw new BoardActionError(`${t.id} was not worked on outside squad-kit.`);if(t.takenOver)throw new BoardActionError(`${t.id} is still in the conversation. Bring it back first.`);if(shipped(t))throw new BoardActionError(`${t.id} is accepted already.`);if(!waitsAtReview(t))throw new BoardActionError(`${t.id} has no built work waiting to be checked right now.`);if(!t.criteria?.length)throw new BoardActionError(`${t.id} has no "Done when" list in its spec.md, so there is nothing to check it against. Add one first.`);t.outside={...t.outside,checked:void 0},t.waiting=null,t.stuck=void 0,t.agentQuestion=void 0,t.retryPhase=void 0,t.tries=0,t.reviewRound=0,t.reviewNotes=void 0,t.proof=void 0,t.proofRound=void 0,t.live=!0,t.gates={checks:"pending",review:"pending",proof:"pending"},t.stage="test",t.status="Running the project checks on the work done outside",addActivity(t,"you","Asked squad-kit to check the work done outside"),scheduleSquad(t,"test",.2,1)})}import{execFileSync as execFileSync17}from"child_process";import fs170 from"fs";import path164 from"path";var GUIDE_FILE="how-to-work-here.md",SAMPLE_DOCS=ticketDocs({id:"SK-1",title:"Sign in with a phone number",story:"People with no email can sign in with their phone number.",criteria:[{given:"someone on the sign-in page",when:"they enter their phone number and the code sent to it",then:"they are signed in"},{given:"someone on the sign-in page",when:"they enter a wrong code three times",then:"they wait ten minutes before trying again"}],releaseNote:"You can now sign in with your phone number.",questions:[{id:"q1",from:"analyst",text:"Should a code expire?",options:["After 5 minutes","After 30 minutes"],answer:{choice:"After 5 minutes"}}],decision:{title:"Where the codes are kept",area:"Data",options:[{name:"In the database",note:"Survives a restart",chosen:!0},{name:"In memory",note:"Simple, but lost on a restart"}],why:"A code must still work after a restart."},steps:[{title:"Add the codes table",status:"todo"},{title:"Send a code and check it",status:"todo"},{title:"Add the phone number to the sign-in page",status:"todo"}]});function shape(file3){return`\`\`\`\`markdown
|
|
1953
1953
|
${SAMPLE_DOCS[file3].trimEnd()}
|
|
1954
1954
|
\`\`\`\``}function commandsOf(bp){let meta=readMeta(bp),repos=reposOf(meta);return repos.flatMap(r=>{let said2=Object.entries(r.commands??{}).filter(([,v])=>typeof v=="string"&&v.trim()),where2=repos.length>1?`${r.path}: `:"";return said2.length?said2.map(([k,v])=>`- ${where2}${k}: \`${String(v).trim()}\``):[`- ${where2}none written down yet. Find them in the project's own files.`]}).join(`
|