@north-light/cli 0.3.519 → 0.3.521
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/api/command-manifest/result.d.ts +20 -8
- package/dist/api/command-manifest/result.js +1 -1
- package/dist/api/command-manifest/schema.js +1 -1
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/health.d.ts +3 -0
- package/dist/api/plugin-manifest-schema.d.ts +7 -3
- package/dist/builtin-memory/crouter-plugin/deploying.md +3 -1
- package/dist/builtin-memory/crouter-plugin/output.md +1 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +6 -0
- package/dist/clients/attach/viewer.js +533 -533
- package/dist/commands/pkg/browse/model.d.ts +1 -1
- package/dist/commands/pkg/plugin-manage.js +1 -1
- package/dist/commands/sys/daemon.js +1 -1
- package/dist/core/canvas/migrations.js +28 -22
- package/dist/core/command-hooks/transport/exec-invoke.js +1 -1
- package/dist/core/command-plugins/transport/exec-invoke.js +1 -1
- package/dist/core/command-plugins/transport/http-plan.js +1 -1
- package/dist/core/command.js +9 -9
- package/dist/core/graph/documents.d.ts +6 -5
- package/dist/core/graph/documents.js +7 -7
- package/dist/core/graph/edges.d.ts +16 -0
- package/dist/core/graph/edges.js +1 -1
- package/dist/core/graph/repo-sync/exchange.js +2 -2
- package/dist/core/help.d.ts +4 -2
- package/dist/core/help.js +7 -7
- package/dist/core/plugin-app.js +1 -1
- package/dist/core/plugin-objects-manifest.d.ts +5 -3
- package/dist/core/plugin-objects-manifest.js +1 -1
- package/dist/core/providers/call.d.ts +2 -1
- package/dist/core/providers/call.js +1 -1
- package/dist/core/spaces/cache-env.d.ts +1 -1
- package/dist/core/spaces/cache-env.js +1 -1
- package/dist/daemon/api/handlers/crons.js +1 -1
- package/dist/daemon/api/handlers/docs.js +1 -1
- package/dist/daemon/api/handlers/health.js +3 -3
- package/docs/ci-runner.md +4 -5
- package/docs/plugin/deploying.md +3 -1
- package/docs/plugin/output.md +1 -1
- package/docs/sdk/release-notes.md +6 -0
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -38,7 +38,7 @@ export interface CommandNode {
|
|
|
38
38
|
/** The selection rubric — when to reach for this command. */
|
|
39
39
|
whenToUse: string;
|
|
40
40
|
summary: string;
|
|
41
|
-
tier?: "normal" | "common" | "important";
|
|
41
|
+
tier?: "hidden" | "normal" | "common" | "important";
|
|
42
42
|
/** Empty for a leaf and for a passthrough branch. */
|
|
43
43
|
children: CommandNode[];
|
|
44
44
|
/** Branch only: forwards every argv token to an external binary instead of
|
|
@@ -12,7 +12,7 @@ ${t.map(i=>`- ${i}`).join(`
|
|
|
12
12
|
${c.join(`
|
|
13
13
|
`)}`,{issues:l,next:`Correct the package's memory documents, then retry \u2014 "${e.name}" was not activated.`})}return{docs:a.length,...o==="linked"?{linkedSource:Je(r)}:{}}}d(An,"migratePluginMemory");async function P(e,n,t,i,o,r,a="package"){try{en(e)}catch(m){const h=m instanceof Error?m.message:String(m);throw f(`plugin manifest at ${o} declares invalid memory_extensions: ${h}`)}if(xn(e,o),Tn(e,o),Sn(e,o),En(e,o),e.env!==void 0){const m=Me(e.env);if(m.length)throw f(`plugin manifest at ${o} declares an invalid env block:
|
|
14
14
|
${m.map(h=>`- ${h}`).join(`
|
|
15
|
-
`)}`,{issues:m})}const[l,c]=await Promise.all([I(),N()]),s=Q({name:e.name,scope:t,root:n,manifest:e,enabled:!0},new Set(D),l,c);if(s.issues.length>0)throw W(`plugin command behavior at ${o} is invalid`,s.issues);const u=s.manifest?.roots??[],y=wn(e,{name:e.name,leafPaths:kn(e.name,u),streamingLeafPaths:bn(e.name,u)});if(y.length>0)throw f(`plugin manifest at ${o} declares an invalid object_kinds or scopes block:
|
|
15
|
+
`)}`,{issues:m})}const[l,c]=await Promise.all([I(),N()]),s=Q({name:e.name,scope:t,root:n,manifest:e,enabled:!0},new Set(D),l,c);if(s.issues.length>0)throw W(`plugin command behavior at ${o} is invalid`,s.issues);const u=s.manifest?.roots??[],y=wn(e,{name:e.name,leafPaths:kn(e.name,e,u),streamingLeafPaths:bn(e.name,e,u)});if(y.length>0)throw f(`plugin manifest at ${o} declares an invalid object_kinds or scopes block:
|
|
16
16
|
${y.map(m=>`- ${m}`).join(`
|
|
17
17
|
`)}`,{issues:y});await qn(e,n,t,i,o,r);const p=v(n,"memory"),k=Xe(t,i,{candidate:e});for(const m of Ye(p,h=>h.endsWith(".md"),h=>h.startsWith("."))){let h;try{h=on(Ge(m)).data}catch(R){throw f(`plugin memory document ${L(n,m)} has invalid YAML frontmatter: ${R instanceof Error?R.message.split(`
|
|
18
18
|
`)[0]:String(R)}`)}const w=nn(h);if(w!==null)throw f(`plugin memory document ${L(n,m)} is invalid: ${w}`);for(const R of Ze(h?.extensions,k))throw f(`plugin memory document ${L(n,m)} is invalid: ${R.path}: ${R.message}`)}return An(e,n,t,o,a)}d(P,"assertPluginCandidateValid");function Mn(e){return{kind:"http",endpoint:new URL(e.endpoint).origin,...e.authEnv!==void 0?{authEnv:e.authEnv}:{}}}d(Mn,"invocationTransport");async function _n(e,n,t,i,o){const r=await P(o,t,n,i,`${t}/.crouter-plugin/plugin.json`),a={name:e,scope:n,root:t,manifest:o,enabled:!0},[l,c]=await Promise.all([I(),N()]),s=Q(a,new Set(D),l,c);if(s.issues.length>0)throw W(`bundle staged package for "${e}" is invalid`,s.issues);return r}d(_n,"validateStagedBundle");function ye(e){return e.sourceMarketplace!==void 0||Y(e.root)||O(e.root)}d(ye,"isSourceInstalled");function ke(e){throw f(`HTTP plugin "${e}" uses the removed commands-only endpoint format`,{next:`Run \`crtr pkg plugin remove ${e}\`, then reinstall it with \`crtr pkg plugin install --name ${e} --endpoint <archive-url>\`.`})}d(ke,"rejectLegacyHttpPlugin");function Ln(e,n,t){if(!$(t))return;const i=C(e,n);if(i?.manifest.bundle!==void 0)return i;throw i?.manifest.transport?.kind==="http"&&i!==void 0&&!ye(i)&&ke(e),f(`plugin "${e}" already exists at ${t}; run \`crtr pkg plugin remove ${e}\` before installing the archive plugin.`)}d(Ln,"existingBundleRoot");function we(e,n,t,i){const o=ge(e,n.endpoint,n.authEnv),r={endpoint:o.endpoint,...o.authEnv!==void 0?{authEnv:o.authEnv}:{}},a=v(A(t),e);return Ln(e,t,a),{bundle:r,transport:Mn(r),scopeRootPath:i,root:a}}d(we,"resolveBundleTarget");function be(e,n){return{name:e,endpoint:n.endpoint,...n.authEnv!==void 0?{authEnv:n.authEnv}:{}}}d(be,"fetchTarget");const Bn=6e4,Hn=1e4;async function $e(e,n,t,i){const o=n==="project"?z():S();return _(v(o,"tmp")),Ke(ae(o,e),()=>i(o),{timeoutMs:t,timeoutError:d(()=>f(`timed out waiting for another crtr process to finish replacing plugin "${e}"`,{next:`Re-run once that process has finished, or check for a stalled crtr holding ${ae(o,e)}.`}),"timeoutError")})}d($e,"withBundleSwapLock");async function je(e,n,t,i){return $e(e,t,Bn,async o=>{const r=we(e,n,t,o),a=await pe(be(e,r.bundle));if(a.status!==200){const l=`HTTP plugin bundle fetch failed for "${e}": ${a.message}`;throw a.status==="auth_env_missing"?b(l):a.status==="cli_unreachable"?He(l):f(l)}return Pe(e,t,r,a.raw,a.etag,i)})}d(je,"replaceBundlePlugin");async function Rt(e,n,t,i){return $e(e,t,Hn,async o=>{try{const r=we(e,n,t,o),a=i.conditional?Ne(t).plugins[e]?.etag:void 0,l={...a!==void 0?{ifNoneMatch:a}:{},...i.timeoutMs!==void 0?{timeoutMs:i.timeoutMs}:{}},c=await pe(be(e,r.bundle),l);if(c.status===304)return;if(c.status!==200)throw f(`HTTP plugin bundle revalidation failed for "${e}": ${c.message}`);const s=un(c.raw);if(fn(e,t,r.root,s)){mn(t,e,c.etag,{swapped:!1});return}return await Pe(e,t,r,c.raw,c.etag,i,s)}finally{Dn(t,e)}})}d(Rt,"revalidateBundlePlugin");function Dn(e,n){V(e,t=>{t.plugins[n]===void 0&&(t.plugins[n]={}),t.plugins[n].checked_at=J()})}d(Dn,"recordPluginCheck");async function Pe(e,n,t,i,o,r,a){const{bundle:l,transport:c,scopeRootPath:s,root:u}=t,y=await In(e,i,{scope:n,scopeRootPath:s,root:u,bundle:l,transport:c,etag:o,enable:r.enable,...a!==void 0?{version:a}:{}}),{validated:p,version:k,staged:m}=y,h=await X(e,n);if(h===void 0)throw f(`bundle plugin "${e}" has no staged command report`);const w=await Z(e,n);return{name:e,scope:n,path:u,transport:"http",version:k,docs:p.memory.length,kinds:Object.keys(p.kinds??{}).length,pageComponents:(p.page_components??[]).length,objectKinds:Object.keys(p.object_kinds??{}).length,scopes:(p.scopes??[]).length,commands:h,...w!==void 0?{hooks:w}:{},...m!==void 0?{memory:m}:{}}}d(Pe,"applyBundleArchive");async function xt(e){const[n,t]=await Promise.all([I(),N()]);return hn(e,{reservedCoreNames:new Set(D),coreCommandPaths:n,extensibleCoreBranches:t})}d(xt,"validatePluginArchive");async function In(e,n,t){const[i,o]=await Promise.all([I(),N()]);return pn(e,n,{...t,validation:{reservedCoreNames:new Set(D),coreCommandPaths:i,extensibleCoreBranches:o},validateStaged:d((r,a)=>_n(e,t.scope,r,t.scopeRootPath,a),"validateStaged"),invalid:d((r,a)=>W(r,a),"invalid")})}d(In,"installPluginArchive");async function Nn(e,n,t,i){const o=ge(e,n,t);return je(e,{endpoint:o.endpoint,...o.authEnv!==void 0?{authEnv:o.authEnv}:{}},i,{enable:!0})}d(Nn,"installHttpPlugin");function Un(e){if(e.startsWith("./")||e.startsWith("../")||e.startsWith("/"))return{kind:"local",value:e};if(me(e))return{kind:"git",value:e};const n=e.split("/");if(n.length===2&&n[0]!==""&&n[1]!=="")return{kind:"marketplace",value:{marketplace:n[0],plugin:n[1]}};throw b(`invalid plugin install ref: ${e}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var c=Object.defineProperty;var s=(r,e)=>c(r,"name",{value:e,configurable:!0});import{defineLeaf as d,defineBranch as w}from"../../core/command.js";import{InputError as o}from"../../core/io.js";import{parseDurationMs as f}from"../../core/wake.js";import{DaemonNotPreparedError as g,probeDaemonHealth as u,probeErrorMessage as y,spawnDaemon as h,stopDaemonProcess as b,sweepStrayDaemons as v}from"../../daemon/manage.js";import{isDaemonRunning as _,readPidfile as l,isPidAlive as m}from"../../daemon/pidfile.js";import{cliClient as k}from"../api-client.js";import{localClient as T}from"../../api/node-transport.js";import{defineSysBranch as q}from"./branch.js";const x=d({name:"start",description:"start the daemon in the background",whenToUse:"bringing the crtrd supervisor up for the first time in a session so node window-exits get handled (no-op only when its Unix API socket already serves)",help:{name:"sys daemon start",summary:"start the crtrd supervisor daemon in the background (no-op only if its Unix API socket serves)",params:[{kind:"flag",name:"prepare",type:"bool",required:!1,constraint:"Hold normal daemon work after storage preparation until `sys daemon admit`; omit for ordinary startup."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,constraint:"Start even while `crtr sys join` holds the daemon down. Joining then fails `daemon_running`; use only to clear a join that is stuck."}],output:[{name:"started",type:"boolean",required:!0,constraint:"True when a new daemon process was spawned; false when one was already running."},{name:"pid",type:"number",required:!1,constraint:"PID of the newly spawned daemon."},{name:"existing_pid",type:"number",required:!1,constraint:"PID of the already-running daemon (when started=false)."},{name:"running",type:"boolean",required:!1,constraint:"True only when a live pidfile owner does not serve its Unix API socket."},{name:"serving",type:"boolean",required:!1,constraint:"False only when a live pidfile owner does not answer /healthz."},{name:"probe_error",type:"string",required:!1,constraint:"The final /healthz probe failure when a live owner missed readiness."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon phase when it was reachable."}],outputKind:"object",effects:["Refuses `join_in_progress` while a live `crtr sys join` holds the daemon down, unless `--force`. `sys daemon restart` and `ensureDaemon` stand down the same way.","Spawns dist/daemon/crtrd-cli.js detached + unref'd; it outlives this process.","Writes a pidfile at CRTR_PIDFILE if set, otherwise crtrHome()/crtrd.pid."]},run:s(async r=>{try{return await h({targetPhase:r.prepare===!0?"prepared":"ready",ignoreJoinLock:r.force===!0})}catch(e){throw e instanceof g?new o({error:"daemon_not_prepared",message:e.message,next:"Stop the live daemon, then start a new one with `crtr sys daemon start --prepare` before setup."}):e}},"run")}),D=100;async function I(r,e){const a=Date.now(),n=a+e;let i="initializing",p="the daemon socket was not reachable";for(;Date.now()<n;){try{const t=await u(Math.min(1e3,Math.max(1,n-Date.now())));if(i=t.startup_phase,p=t.startup_blocked??`daemon is ${t.startup_phase}`,t.startup_phase==="blocked")throw new o({error:"startup_blocked",message:t.startup_blocked??"daemon startup is blocked",received:t.startup_phase,field:"phase",next:"Repair the reported blocker paths with `crtr sys migrate`, then retry the await."});if(t.startup_phase===r)return{startup_phase:r,waited_ms:Date.now()-a};if(r==="prepared"&&t.startup_phase==="ready")throw new o({error:"await_missed_phase",message:"daemon is already ready and cannot return to prepared in this process",received:t.startup_phase,field:"phase",next:"Start a new daemon with `crtr sys daemon start --prepare` before awaiting prepared."})}catch(t){if(t instanceof o)throw t;p=t instanceof Error?t.message:String(t)}await new Promise(t=>setTimeout(t,Math.min(D,Math.max(0,n-Date.now()))))}throw new o({error:"await_timeout",message:`daemon did not reach ${r} within ${e}ms (last phase: ${i}; last observation: ${p})`,received:i,field:"phase",next:"Inspect `crtr sys daemon status` and resolve any reported startup blocker before retrying."})}s(I,"awaitDaemonPhase");const S=d({name:"await",description:"wait for a daemon startup phase",whenToUse:"a caller started or was given a daemon that must reach prepared or ready before its next step.",help:{name:"sys daemon await",summary:"wait for the running daemon to report one startup phase",params:[{kind:"flag",name:"phase",type:"enum",choices:["prepared","ready"],required:!0,constraint:"The phase that permits the caller\u2019s next step."},{kind:"flag",name:"timeout",type:"string",required:!1,default:"5m",constraint:"Maximum wait as a positive duration such as 90s, 5m, or 1h30m."}],output:[{name:"startup_phase",type:"string",required:!0,constraint:"The requested phase."},{name:"waited_ms",type:"number",required:!0,constraint:"Elapsed wall-clock time before the phase was observed."}],outputKind:"object",effects:["Read-only: polls /healthz until the requested phase is observed or the caller-bounded timeout expires."]},run:s(async r=>{const e=r.timeout,a=f(e);if(a===null||a<=0)throw new o({error:"invalid_duration",message:`not a positive duration: ${e}`,received:e,field:"timeout",next:"Use a positive duration such as 90s, 5m, or 1h30m."});return I(r.phase,a)},"run")}),E=d({name:"admit",description:"admit normal work after daemon preparation",whenToUse:"the caller has completed its setup against a prepared daemon and is ready for normal daemon work to start.",help:{name:"sys daemon admit",summary:"ask a prepared daemon to start normal services",params:[],output:[{name:"admitted",type:"boolean",required:!0,constraint:"True only when this request began admission."},{name:"already_admitted",type:"boolean",required:!0,constraint:"True when admission had already begun or completed."},{name:"startup_phase",type:"string",required:!0,constraint:"The phase after admission settles."}],outputKind:"object",effects:["Starts normal daemon services when the daemon is prepared."]},run:s(async r=>await T({autostart:!1}).admitDaemon(),"run")}),R=d({name:"status",description:"check whether the daemon is running",whenToUse:"operator/dev diagnostic \u2014 probing whether the crtrd supervisor process is alive while troubleshooting revive/supervision behavior",help:{name:"sys daemon status",summary:"check whether the crtrd supervisor daemon is currently running",params:[],output:[{name:"running",type:"boolean",required:!0,constraint:"True when the daemon pidfile owner is alive."},{name:"serving",type:"boolean",required:!1,constraint:"True when the live daemon answers /healthz; false when one probe does not."},{name:"pid",type:"number",required:!1,constraint:"PID of the running daemon."},{name:"probe_error",type:"string",required:!1,constraint:"The failed /healthz observation when the live owner did not answer."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon startup phase when the socket answered."}],outputKind:"object",effects:["Read-only: reads the pidfile, probes the pid, and requests /healthz without autostart."]},run:s(async r=>{const e=l();if(!(e!==null&&m(e)))return{running:!1};try{const n=await u();return{running:!0,serving:n.ok,pid:e,...n.startup_phase===void 0?{}:{startup_phase:n.startup_phase}}}catch(n){return{running:!0,serving:!1,pid:e,probe_error:y(n)}}},"run")}),P=d({name:"restart",description:"put the selected runtime generation in service via a daemon-owned handover",whenToUse:"adopting a newly installed runtime generation \u2014 the routine restart. Use this instead of stop+start: the daemon answers before it hands over, so a caller running inside a node (whose broker the handover tears down) still gets a settled result and is resumed automatically.",help:{name:"sys daemon restart",summary:"ask the running daemon to replace itself with a successor on the selected runtime generation",params:[],output:[{name:"restarted",type:"boolean",required:!0,constraint:"True when a running daemon accepted the handover. False when none was running \u2014 one was started instead."},{name:"started",type:"boolean",required:!1,constraint:"True when no daemon was running and this call started one (restarted=false)."},{name:"pid",type:"number",required:!1,constraint:"The daemon that accepted the handover, or the one just started."},{name:"grace_ms",type:"number",required:!1,constraint:"How long the daemon waits after answering before teardown begins."},{name:"note",type:"string",required:!1,constraint:"What happens next to the calling node. Read it before acting on an interrupted turn."}],outputKind:"object",effects:["The daemon acknowledges immediately, then after a short grace tears its whole broker fleet down, releases its claim, spawns a successor on the runtime generation currently selected, and exits.","Each torn-down broker hands its foreground bash commands to the file-backed background job system; they keep running and report completion by urgent inbox message.","Every node it tore down \u2014 including the caller \u2014 is resolved by the successor through the ordinary row-driven supervision tick.","The calling process is killed as part of that teardown, AFTER this result is returned."]},run:s(async r=>{if(!_()){const e=await h();return{restarted:!1,started:e.started,...e.pid!==void 0?{pid:e.pid}:{},...e.existing_pid!==void 0?{pid:e.existing_pid}:{},note:"No daemon was running, so there was nothing to hand over; one was started on the selected runtime generation."}}try{const e=await k().restartDaemon();return{restarted:e.accepted,pid:e.predecessor_pid,grace_ms:e.grace_ms,note:"Handover accepted. If you are running inside a node, your broker is about to be torn down and resumed on the new generation \u2014 THIS RESTART ALREADY SUCCEEDED. When your turn resumes with this result in the transcript, do not re-run it."}}catch(e){throw new o({error:"daemon_restart_failed",message:`the daemon did not accept a handover: ${e.message}`,next:"Run `crtr sys daemon status` to inspect the daemon."})}},"run")}),j=d({name:"stop",description:"stop the running daemon",whenToUse:"shutting the crtrd supervisor down for good, ending auto-revival of nodes on window exit. Not the way to pick up a new runtime generation \u2014 `restart` is, because stopping kills every broker of this epoch, including the caller's own, so this command cannot report its result to an agent that runs it.",help:{name:"sys daemon stop",summary:"stop the crtrd supervisor daemon and confirm it exited",params:[],output:[{name:"stopped",type:"boolean",required:!0,constraint:"True when a daemon (recorded or stray) was found and killed. False when none was running."},{name:"pid",type:"number",required:!1,constraint:"The recorded pidfile pid that exited (when one was recorded and alive)."},{name:"strays_killed",type:"array",required:!1,constraint:"PIDs of stray crtrd daemons (alive but not in the pidfile) swept alongside the recorded one \u2014 the backstop against a daemon that survived a prior stop."},{name:"force_killed",type:"boolean",required:!1,constraint:"True when the recorded daemon ignored SIGTERM and had to be SIGKILLed."}],outputKind:"object",effects:["Sends SIGTERM to the recorded pid and waits for it to exit and release its pidfile, escalating to SIGKILL if it does not.","Then sweeps any OTHER live crtrd daemon process (SIGTERM, escalating to SIGKILL after 2s) so a stray daemon orphaned from the pidfile cannot keep supervising."]},run:s(async r=>{const e=l(),a=e!==null&&m(e);try{let n=!1;a&&(n=await b(e)==="killed");const i=v(null);return!a&&i.length===0?{stopped:!1}:{stopped:!0,...a?{pid:e}:{},...i.length>0?{strays_killed:i}:{},...n?{force_killed:!0}:{}}}catch(n){throw new o({error:"daemon_stop_failed",message:`failed to stop daemon${a?` pid ${e}`:""}: ${n.message}`,next:"Run `crtr sys daemon status` to inspect the daemon before retrying."})}},"run")}),A=w({name:"daemon",description:"manage the crtrd supervisor process",whenToUse:"managing the crtrd supervisor that auto-revives nodes on window exit \u2014 start it, restart it onto a new runtime generation, check its status, or stop it",help:{name:"sys daemon",summary:"manage the crtrd canvas supervisor daemon",model:"crtrd is a thin background daemon that polls active+idle nodes and acts on window exit: crashed windows become dead; refresh-yield windows get a fresh respawn. It holds no orchestration logic \u2014 just process supervision. These are operator/dev self-management, not something a normal agent turn needs to check. Picking up a newly installed runtime generation is `restart` (a handover the daemon performs on itself); `stop` is the terminal one, and it kills the caller along with the rest of the fleet."},children:[x,S,E,P,R,j]});function F(){return q([A])}s(F,"registerDaemonControlSys");export{A as daemonBranch,F as registerDaemonControlSys};
|
|
1
|
+
var w=Object.defineProperty;var o=(t,e)=>w(t,"name",{value:e,configurable:!0});import{defineLeaf as u,defineBranch as f}from"../../core/command.js";import{InputError as d}from"../../core/io.js";import{parseDurationMs as g}from"../../core/wake.js";import{DaemonNotPreparedError as y,probeDaemonHealth as p,probeErrorMessage as b,spawnDaemon as h,stopDaemonProcess as v,sweepStrayDaemons as _}from"../../daemon/manage.js";import{isDaemonRunning as k,readPidfile as l,isPidAlive as m}from"../../daemon/pidfile.js";import{cliClient as T}from"../api-client.js";import{localClient as c}from"../../api/node-transport.js";import{defineSysBranch as q}from"./branch.js";import{selectedGeneration as x}from"./shared.js";const I=u({name:"start",description:"start the daemon in the background",whenToUse:"bringing the crtrd supervisor up for the first time in a session so node window-exits get handled (no-op only when its Unix API socket already serves)",help:{name:"sys daemon start",summary:"start the crtrd supervisor daemon in the background (no-op only if its Unix API socket serves)",params:[{kind:"flag",name:"prepare",type:"bool",required:!1,constraint:"Hold normal daemon work after storage preparation until `sys daemon admit`; omit for ordinary startup."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,constraint:"Start even while `crtr sys join` holds the daemon down. Joining then fails `daemon_running`; use only to clear a join that is stuck."}],output:[{name:"started",type:"boolean",required:!0,constraint:"True when a new daemon process was spawned; false when one was already running."},{name:"pid",type:"number",required:!1,constraint:"PID of the newly spawned daemon."},{name:"existing_pid",type:"number",required:!1,constraint:"PID of the already-running daemon (when started=false)."},{name:"running",type:"boolean",required:!1,constraint:"True only when a live pidfile owner does not serve its Unix API socket."},{name:"serving",type:"boolean",required:!1,constraint:"False only when a live pidfile owner does not answer /healthz."},{name:"probe_error",type:"string",required:!1,constraint:"The final /healthz probe failure when a live owner missed readiness."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon phase when it was reachable."}],outputKind:"object",effects:["Refuses `join_in_progress` while a live `crtr sys join` holds the daemon down, unless `--force`. `sys daemon restart` and `ensureDaemon` stand down the same way.","Spawns dist/daemon/crtrd-cli.js detached + unref'd; it outlives this process.","Writes a pidfile at CRTR_PIDFILE if set, otherwise crtrHome()/crtrd.pid."]},run:o(async t=>{try{return await h({targetPhase:t.prepare===!0?"prepared":"ready",ignoreJoinLock:t.force===!0})}catch(e){throw e instanceof y?new d({error:"daemon_not_prepared",message:e.message,next:"Stop the live daemon, then start a new one with `crtr sys daemon start --prepare` before setup."}):e}},"run")}),D=100;async function S(t,e){const a=Date.now(),r=a+e;let s="initializing",i="the daemon socket was not reachable";for(;Date.now()<r;){try{const n=await p(Math.min(1e3,Math.max(1,r-Date.now())));if(s=n.startup_phase,i=n.startup_blocked??`daemon is ${n.startup_phase}`,n.startup_phase==="blocked")throw new d({error:"startup_blocked",message:n.startup_blocked??"daemon startup is blocked",received:n.startup_phase,field:"phase",next:"Repair the reported blocker paths with `crtr sys migrate`, then retry the await."});if(n.startup_phase===t)return{startup_phase:t,waited_ms:Date.now()-a};if(t==="prepared"&&n.startup_phase==="ready")throw new d({error:"await_missed_phase",message:"daemon is already ready and cannot return to prepared in this process",received:n.startup_phase,field:"phase",next:"Start a new daemon with `crtr sys daemon start --prepare` before awaiting prepared."})}catch(n){if(n instanceof d)throw n;i=n instanceof Error?n.message:String(n)}await new Promise(n=>setTimeout(n,Math.min(D,Math.max(0,r-Date.now()))))}throw new d({error:"await_timeout",message:`daemon did not reach ${t} within ${e}ms (last phase: ${s}; last observation: ${i})`,received:s,field:"phase",next:"Inspect `crtr sys daemon status` and resolve any reported startup blocker before retrying."})}o(S,"awaitDaemonPhase");const E=u({name:"await",description:"wait for a daemon startup phase",whenToUse:"a caller started or was given a daemon that must reach prepared or ready before its next step.",help:{name:"sys daemon await",summary:"wait for the running daemon to report one startup phase",params:[{kind:"flag",name:"phase",type:"enum",choices:["prepared","ready"],required:!0,constraint:"The phase that permits the caller\u2019s next step."},{kind:"flag",name:"timeout",type:"string",required:!1,default:"5m",constraint:"Maximum wait as a positive duration such as 90s, 5m, or 1h30m."}],output:[{name:"startup_phase",type:"string",required:!0,constraint:"The requested phase."},{name:"waited_ms",type:"number",required:!0,constraint:"Elapsed wall-clock time before the phase was observed."}],outputKind:"object",effects:["Read-only: polls /healthz until the requested phase is observed or the caller-bounded timeout expires."]},run:o(async t=>{const e=t.timeout,a=g(e);if(a===null||a<=0)throw new d({error:"invalid_duration",message:`not a positive duration: ${e}`,received:e,field:"timeout",next:"Use a positive duration such as 90s, 5m, or 1h30m."});return S(t.phase,a)},"run")}),R=u({name:"admit",description:"admit normal work after daemon preparation",whenToUse:"the caller has completed its setup against a prepared daemon and is ready for normal daemon work to start.",help:{name:"sys daemon admit",summary:"ask a prepared daemon to start normal services",params:[],output:[{name:"admitted",type:"boolean",required:!0,constraint:"True only when this request began admission."},{name:"already_admitted",type:"boolean",required:!0,constraint:"True when admission had already begun or completed."},{name:"startup_phase",type:"string",required:!0,constraint:"The phase after admission settles."}],outputKind:"object",effects:["Starts normal daemon services when the daemon is prepared."]},run:o(async t=>await c({autostart:!1}).admitDaemon(),"run")});async function P(){try{return(await c({autostart:!1}).status()).generation_id??void 0}catch{return}}o(P,"servingGenerationId");const A=u({name:"status",description:"check whether the daemon is running",whenToUse:"operator/dev diagnostic \u2014 probing whether the crtrd supervisor process is alive while troubleshooting revive/supervision behavior",help:{name:"sys daemon status",summary:"check whether the crtrd supervisor daemon is currently running",params:[],output:[{name:"running",type:"boolean",required:!0,constraint:"True when the daemon pidfile owner is alive."},{name:"serving",type:"boolean",required:!1,constraint:"True when the live daemon answers /healthz; false when one probe does not."},{name:"pid",type:"number",required:!1,constraint:"PID of the running daemon."},{name:"probe_error",type:"string",required:!1,constraint:"The failed /healthz observation when the live owner did not answer."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon startup phase when the socket answered."},{name:"serving_generation",type:"string",required:!1,constraint:"Id of the runtime generation the daemon process was launched from. Absent when the daemon reports none (it predates the field, or runs from a development checkout) or its status could not be read."},{name:"selected_generation",type:"string",required:!1,constraint:"Id of the selected runtime generation. Absent when none is selected or it cannot be read."},{name:"generation_drift",type:"string",required:!1,constraint:"`none` when the serving and selected generation ids are equal; `differs` when they are not, meaning the daemon runs older bytes than the install selected until `sys daemon restart`; `unknown` when either id is absent. Never `none` on missing evidence."}],outputKind:"object",effects:["Read-only: reads the pidfile, probes the pid, and requests /healthz and /v1/status without autostart."]},run:o(async t=>{const e=l();if(!(e!==null&&m(e)))return{running:!1};try{const r=await p(),s=await P(),i=x().generation?.id;return{running:!0,serving:r.ok,pid:e,...r.startup_phase===void 0?{}:{startup_phase:r.startup_phase},...s===void 0?{}:{serving_generation:s},...i===void 0?{}:{selected_generation:i},generation_drift:s===void 0||i===void 0?"unknown":s===i?"none":"differs"}}catch(r){return{running:!0,serving:!1,pid:e,probe_error:b(r)}}},"run")}),j=u({name:"restart",description:"put the selected runtime generation in service via a daemon-owned handover",whenToUse:"adopting a newly installed runtime generation \u2014 the routine restart. Use this instead of stop+start: the daemon answers before it hands over, so a caller running inside a node (whose broker the handover tears down) still gets a settled result and is resumed automatically.",help:{name:"sys daemon restart",summary:"ask the running daemon to replace itself with a successor on the selected runtime generation",params:[],output:[{name:"restarted",type:"boolean",required:!0,constraint:"True when a running daemon accepted the handover. False when none was running \u2014 one was started instead."},{name:"started",type:"boolean",required:!1,constraint:"True when no daemon was running and this call started one (restarted=false)."},{name:"pid",type:"number",required:!1,constraint:"The daemon that accepted the handover, or the one just started."},{name:"grace_ms",type:"number",required:!1,constraint:"How long the daemon waits after answering before teardown begins."},{name:"note",type:"string",required:!1,constraint:"What happens next to the calling node. Read it before acting on an interrupted turn."}],outputKind:"object",effects:["The daemon acknowledges immediately, then after a short grace tears its whole broker fleet down, releases its claim, spawns a successor on the runtime generation currently selected, and exits.","Each torn-down broker hands its foreground bash commands to the file-backed background job system; they keep running and report completion by urgent inbox message.","Every node it tore down \u2014 including the caller \u2014 is resolved by the successor through the ordinary row-driven supervision tick.","The calling process is killed as part of that teardown, AFTER this result is returned."]},run:o(async t=>{if(!k()){const e=await h();return{restarted:!1,started:e.started,...e.pid!==void 0?{pid:e.pid}:{},...e.existing_pid!==void 0?{pid:e.existing_pid}:{},note:"No daemon was running, so there was nothing to hand over; one was started on the selected runtime generation."}}try{const e=await T().restartDaemon();return{restarted:e.accepted,pid:e.predecessor_pid,grace_ms:e.grace_ms,note:"Handover accepted. If you are running inside a node, your broker is about to be torn down and resumed on the new generation \u2014 THIS RESTART ALREADY SUCCEEDED. When your turn resumes with this result in the transcript, do not re-run it."}}catch(e){throw new d({error:"daemon_restart_failed",message:`the daemon did not accept a handover: ${e.message}`,next:"Run `crtr sys daemon status` to inspect the daemon."})}},"run")}),U=u({name:"stop",description:"stop the running daemon",whenToUse:"shutting the crtrd supervisor down for good, ending auto-revival of nodes on window exit. Not the way to pick up a new runtime generation \u2014 `restart` is, because stopping kills every broker of this epoch, including the caller's own, so this command cannot report its result to an agent that runs it.",help:{name:"sys daemon stop",summary:"stop the crtrd supervisor daemon and confirm it exited",params:[],output:[{name:"stopped",type:"boolean",required:!0,constraint:"True when a daemon (recorded or stray) was found and killed. False when none was running."},{name:"pid",type:"number",required:!1,constraint:"The recorded pidfile pid that exited (when one was recorded and alive)."},{name:"strays_killed",type:"array",required:!1,constraint:"PIDs of stray crtrd daemons (alive but not in the pidfile) swept alongside the recorded one \u2014 the backstop against a daemon that survived a prior stop."},{name:"force_killed",type:"boolean",required:!1,constraint:"True when the recorded daemon ignored SIGTERM and had to be SIGKILLed."}],outputKind:"object",effects:["Sends SIGTERM to the recorded pid and waits for it to exit and release its pidfile, escalating to SIGKILL if it does not.","Then sweeps any OTHER live crtrd daemon process (SIGTERM, escalating to SIGKILL after 2s) so a stray daemon orphaned from the pidfile cannot keep supervising."]},run:o(async t=>{const e=l(),a=e!==null&&m(e);try{let r=!1;a&&(r=await v(e)==="killed");const s=_(null);return!a&&s.length===0?{stopped:!1}:{stopped:!0,...a?{pid:e}:{},...s.length>0?{strays_killed:s}:{},...r?{force_killed:!0}:{}}}catch(r){throw new d({error:"daemon_stop_failed",message:`failed to stop daemon${a?` pid ${e}`:""}: ${r.message}`,next:"Run `crtr sys daemon status` to inspect the daemon before retrying."})}},"run")}),L=f({name:"daemon",description:"manage the crtrd supervisor process",whenToUse:"managing the crtrd supervisor that auto-revives nodes on window exit \u2014 start it, restart it onto a new runtime generation, check its status, or stop it",help:{name:"sys daemon",summary:"manage the crtrd canvas supervisor daemon",model:"crtrd is a thin background daemon that polls active+idle nodes and acts on window exit: crashed windows become dead; refresh-yield windows get a fresh respawn. It holds no orchestration logic \u2014 just process supervision. These are operator/dev self-management, not something a normal agent turn needs to check. Picking up a newly installed runtime generation is `restart` (a handover the daemon performs on itself); `stop` is the terminal one, and it kills the caller along with the rest of the fleet."},children:[I,E,R,j,A,U]});function W(){return q([L])}o(W,"registerDaemonControlSys");export{L as daemonBranch,W as registerDaemonControlSys};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var Te=Object.defineProperty;var E=(e,t)=>Te(e,"name",{value:t,configurable:!0});import{randomBytes as P}from"node:crypto";import{parseScope as ae}from"@north-light/identity";import{importNodeSqlAuthority as de,recreateNodeRowsView as _e}from"./node-sql-migration.js";import{migrateRecoveryWitnesses as
|
|
1
|
+
var Te=Object.defineProperty;var E=(e,t)=>Te(e,"name",{value:t,configurable:!0});import{randomBytes as P}from"node:crypto";import{parseScope as ae}from"@north-light/identity";import{importNodeSqlAuthority as de,recreateNodeRowsView as _e}from"./node-sql-migration.js";import{migrateRecoveryWitnesses as Ne}from"./recovery.js";import{migrateMessagingState as ce}from"./deliveries.js";import{migrateHumanRequestsState as Le}from"../human/requests.js";import{existsSync as U,readFileSync as M,readdirSync as k,realpathSync as le,statSync as ue}from"node:fs";import{basename as pe,join as j,resolve as Y}from"node:path";import{appSpace as Re}from"../layout.js";import{emitEvent as Ae}from"../events/emit.js";import{isSafeNodeId as Oe,nodesRoot as h,nodeMetaPath as x}from"./paths.js";import{convertLegacyEdges as Ie}from"../../migrations/004-canvas-documents/legacy-edges.js";import{customExtensionPaths as me}from"./extensions.js";import{readMetaObject as K,updateJsonFileDurably as q,withMetaLock as fe,writeMetaObjectLocked as Xe}from"./meta-file.js";function Ue(e){e.exec(`
|
|
2
2
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
3
3
|
node_id TEXT PRIMARY KEY,
|
|
4
4
|
name TEXT NOT NULL,
|
|
@@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS edges (
|
|
|
23
23
|
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(type, to_id);
|
|
24
24
|
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
|
|
25
25
|
CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status);
|
|
26
|
-
`)}E(Ue,"baselineSchema");function b(e,t){return new Set(e.prepare(`PRAGMA table_info(${t})`).all().map(n=>n.name))}E(b,"tableColumns");function $(e,t,n,o){t.has(n)||(e.exec(o),t.add(n))}E($,"addColumnIfMissing");function O(e){return b(e,"nodes")}E(O,"nodeColumns");const
|
|
26
|
+
`)}E(Ue,"baselineSchema");function b(e,t){return new Set(e.prepare(`PRAGMA table_info(${t})`).all().map(n=>n.name))}E(b,"tableColumns");function $(e,t,n,o){t.has(n)||(e.exec(o),t.add(n))}E($,"addColumnIfMissing");function O(e){return b(e,"nodes")}E(O,"nodeColumns");const c=$;function Ce(e){const t=O(e);c(e,t,"intent","ALTER TABLE nodes ADD COLUMN intent TEXT;"),c(e,t,"pi_pid","ALTER TABLE nodes ADD COLUMN pi_pid INTEGER;"),c(e,t,"window","ALTER TABLE nodes ADD COLUMN window TEXT;"),c(e,t,"tmux_session","ALTER TABLE nodes ADD COLUMN tmux_session TEXT;")}E(Ce,"addRuntimeColumns");function Se(e){const t=h();if(!U(t))return;const n=e.prepare('UPDATE nodes SET intent = ?, pi_pid = ?, "window" = ?, tmux_session = ? WHERE node_id = ?');for(const o of k(t)){const T=x(o);if(!U(T))continue;let i;try{i=JSON.parse(M(T,"utf8"))}catch{continue}n.run(i.intent??null,i.pi_pid??null,i.window??null,i.tmux_session??null,o)}}E(Se,"backfillRuntime");function De(e){B(e,3,()=>{if(!(e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'edges'").get()!==void 0)){e.exec(`
|
|
27
27
|
CREATE TABLE edges (
|
|
28
28
|
type TEXT NOT NULL,
|
|
29
29
|
from_id TEXT NOT NULL,
|
|
@@ -57,7 +57,7 @@ DROP TABLE edges;
|
|
|
57
57
|
ALTER TABLE edges_new RENAME TO edges;
|
|
58
58
|
CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(type, to_id);
|
|
59
59
|
CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(type, from_id);
|
|
60
|
-
`);const o=e.prepare("SELECT COUNT(*) AS n FROM edges").get().n;if(o!==n)throw new Error(`edges FK rebuild lost rows: expected ${n} non-orphan edge(s), got ${o}`);const T=e.prepare("PRAGMA foreign_key_check").all();if(T.length>0)throw new Error(`edges FK rebuild left ${T.length} foreign-key violation(s)`)})}E(De,"edgesForeignKeyCascade");function ge(e){const t=O(e);
|
|
60
|
+
`);const o=e.prepare("SELECT COUNT(*) AS n FROM edges").get().n;if(o!==n)throw new Error(`edges FK rebuild lost rows: expected ${n} non-orphan edge(s), got ${o}`);const T=e.prepare("PRAGMA foreign_key_check").all();if(T.length>0)throw new Error(`edges FK rebuild left ${T.length} foreign-key violation(s)`)})}E(De,"edgesForeignKeyCascade");function ge(e){const t=O(e);c(e,t,"pane","ALTER TABLE nodes ADD COLUMN pane TEXT;")}E(ge,"addPaneColumn");function we(e){e.exec(`
|
|
61
61
|
CREATE TABLE IF NOT EXISTS focuses (
|
|
62
62
|
focus_id TEXT PRIMARY KEY, -- stable internal id for the viewport
|
|
63
63
|
pane TEXT, -- the durable %pane_id realizing the focus
|
|
@@ -131,12 +131,12 @@ SELECT
|
|
|
131
131
|
FROM wakeups;
|
|
132
132
|
|
|
133
133
|
DROP TABLE wakeups;
|
|
134
|
-
`)}E(ve,"migrateWakeupsToTriggers");function he(e){const t=O(e);
|
|
134
|
+
`)}E(ve,"migrateWakeupsToTriggers");function he(e){const t=O(e);c(e,t,"host_kind","ALTER TABLE nodes ADD COLUMN host_kind TEXT;")}E(he,"addHostKindColumn");function xe(e){e.exec(`
|
|
135
135
|
CREATE TABLE IF NOT EXISTS canvas_meta (
|
|
136
136
|
key TEXT PRIMARY KEY,
|
|
137
137
|
value TEXT NOT NULL
|
|
138
138
|
);
|
|
139
|
-
`)}E(xe,"addCanvasMetaTable");function Fe(e){const t=O(e);
|
|
139
|
+
`)}E(xe,"addCanvasMetaTable");function Fe(e){const t=O(e);c(e,t,"profile_id","ALTER TABLE nodes ADD COLUMN profile_id TEXT;")}E(Fe,"addProfileIdColumn");function Me(e){const t=O(e);c(e,t,"pi_pid_identity","ALTER TABLE nodes ADD COLUMN pi_pid_identity TEXT;")}E(Me,"addPidIdentityColumn");function ke(e){const t=O(e);c(e,t,"waiting_for","ALTER TABLE nodes ADD COLUMN waiting_for TEXT;")}E(ke,"addMessageWaitColumn");function J(e){const t=O(e);if(t.size===0)return;c(e,t,"final_report","ALTER TABLE nodes ADD COLUMN final_report TEXT;"),c(e,t,"finalized_at","ALTER TABLE nodes ADD COLUMN finalized_at TEXT;");const n=e.prepare("SELECT node_id FROM nodes WHERE status = 'done' AND final_report IS NULL").all(),o=e.prepare("UPDATE nodes SET final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL");for(const{node_id:T}of n){const i=j(h(),T,"reports");if(!U(i))continue;let a=null;for(const N of k(i))if(N.endsWith(".md"))try{const L=M(j(i,N),"utf8"),l=/^---\nnode: [^\n]+\nkind: final\nts: ([^\n]+)\n---\n/.exec(L);if(l===null||Number.isNaN(Date.parse(l[1])))continue;const r={name:N,ts:l[1]};(a===null||r.ts>a.ts||r.ts===a.ts&&r.name>a.name)&&(a=r)}catch{}a!==null&&o.run(a.name,a.ts,T)}}E(J,"addCanonicalFinalColumns");function je(e){const t=h();if(U(t))for(const n of k(t)){const o=x(n);U(o)&&q(o,T=>{const i=T.launch;if(typeof i!="object"||i===null||Array.isArray(i))return!1;const a=i.extensions;if(!Array.isArray(a)||!a.every(L=>typeof L=="string"))return!1;const N=me(a);return N.length===a.length?!1:(i.extensions=N,!0)})}}E(je,"normalizePersistedLaunchExtensions");function He(e){const t=O(e);c(e,t,"waiting_for_generation","ALTER TABLE nodes ADD COLUMN waiting_for_generation INTEGER NOT NULL DEFAULT 0;")}E(He,"addMessageWaitGenerationColumn");function Ge(e){e.exec(`
|
|
140
140
|
CREATE TABLE IF NOT EXISTS consult_outbox (
|
|
141
141
|
node_id TEXT PRIMARY KEY, -- legacy work node id
|
|
142
142
|
dir TEXT NOT NULL, -- legacy ticket directory
|
|
@@ -216,19 +216,19 @@ CREATE INDEX IF NOT EXISTS idx_cron_runs_cron ON cron_runs(cron_id, started);
|
|
|
216
216
|
`)}E(We,"addCronsTables");function g(e){return`'${e.replace(/'/g,`'"'"'`)}'`}E(g,"shellSingleQuote");function Pe(e,t){let n=0;for(;;){const o=`${t}_${n}`;if(!e.split(`
|
|
217
217
|
`).includes(o))return o;n++}}E(Pe,"uniqueHeredocDelimiter");function Q(e,t,n){const o=Pe(t,n);return`${e} <<'${o}'
|
|
218
218
|
${t}
|
|
219
|
-
${o}`}E(Q,"heredocCommand");function Ye(e){if(typeof e!="string")return{};try{const t=JSON.parse(e);return typeof t!="object"||t===null||Array.isArray(t)?{}:t}catch{return{}}}E(Ye,"migrationPayload");function X(e,t,n){const o=e[t];if(typeof o!="string"||o.trim()==="")throw new Error(`trigger ${n} payload requires non-empty ${t}`);return o}E(X,"requiredString");function H(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}E(H,"optionalString");function w(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}E(w,"optionalColumn");function z(e,t,n,o){return Q(`crtr node message send --to ${g(e)} --tier ${g(t)}`,n,`CRTR_MIGRATED_${o.replace(/[^A-Za-z0-9]/g,"_")}`)}E(z,"migratedMessageCommand");function Ke(e,t){return`printf '%s\\n' "$CRTR_GATE_STDOUT" | crtr node message send --to ${g(e)} --tier ${g(t)}`}E(Ke,"migratedGateStdoutMessageCommand");function qe(e,t){const n=[`crtr node new --kind ${g(X(e,"kind",t))}`],o=[["mode","mode"],["cwd","cwd"],["name","name"],["parent","parent"],["worktreeBase","worktree"],["forkFrom","fork-from"],["model","model"],["profile","profile"],["situationalContext","situational-context"]];for(const[i,a]of o){const
|
|
219
|
+
${o}`}E(Q,"heredocCommand");function Ye(e){if(typeof e!="string")return{};try{const t=JSON.parse(e);return typeof t!="object"||t===null||Array.isArray(t)?{}:t}catch{return{}}}E(Ye,"migrationPayload");function X(e,t,n){const o=e[t];if(typeof o!="string"||o.trim()==="")throw new Error(`trigger ${n} payload requires non-empty ${t}`);return o}E(X,"requiredString");function H(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}E(H,"optionalString");function w(e,t){const n=e[t];return typeof n=="string"&&n.trim()!==""?n:void 0}E(w,"optionalColumn");function z(e,t,n,o){return Q(`crtr node message send --to ${g(e)} --tier ${g(t)}`,n,`CRTR_MIGRATED_${o.replace(/[^A-Za-z0-9]/g,"_")}`)}E(z,"migratedMessageCommand");function Ke(e,t){return`printf '%s\\n' "$CRTR_GATE_STDOUT" | crtr node message send --to ${g(e)} --tier ${g(t)}`}E(Ke,"migratedGateStdoutMessageCommand");function qe(e,t){const n=[`crtr node new --kind ${g(X(e,"kind",t))}`],o=[["mode","mode"],["cwd","cwd"],["name","name"],["parent","parent"],["worktreeBase","worktree"],["forkFrom","fork-from"],["model","model"],["profile","profile"],["situationalContext","situational-context"]];for(const[i,a]of o){const N=H(e,i);N!==void 0&&n.push(`--${a} ${g(N)}`)}e.root===!0&&n.push("--root");const T=X(e,"prompt",t);return Q(n.join(" "),T,`CRTR_MIGRATED_${t.replace(/[^A-Za-z0-9]/g,"_")}`)}E(qe,"migratedSpawnCommand");const V="migration.v22.skipped";function $e(e){const t=e.prepare("SELECT cwd FROM nodes WHERE parent IS NULL ORDER BY created LIMIT 1").get();return(t!==void 0?w(t,"cwd"):void 0)??process.cwd()}E($e,"ambientMigrationCwd");function Je(e){let t;try{t=e.prepare("SELECT eval_pid FROM triggers WHERE eval_state = 'running' AND eval_pid IS NOT NULL").all()}catch{return}for(const n of t){const o=n.eval_pid;if(!(typeof o!="number"||!Number.isInteger(o)||o<=1))try{process.kill(-o,"SIGTERM")}catch{}}}E(Je,"reapRunningPredicateEvaluations");function Qe(e){const t=[],n=e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'triggers'").get()!==void 0;n&&Je(e);let o=[];if(n)try{o=e.prepare("SELECT * FROM triggers ORDER BY created, trigger_id").all()}catch(a){t.push({trigger_id:"<unreadable table>",reason:`triggers table could not be read: ${a instanceof Error?a.message:String(a)}`})}const T=$e(e),i=e.prepare(`
|
|
220
220
|
INSERT OR REPLACE INTO crons (
|
|
221
221
|
cron_id, name, created_by, command, fire_at, recur, tz, expires_at,
|
|
222
222
|
anchor_node, cancel_on_wake, cwd, env_json, profile, scope,
|
|
223
223
|
run_timeout_s, overlap, on_output, sink, tier, created, updated
|
|
224
|
-
) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, NULL, ?, 'profile', ?, 'skip', 'on-failure', '{}', ?, ?, ?)`);for(const a of o){const
|
|
224
|
+
) VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, NULL, ?, 'profile', ?, 'skip', 'on-failure', '{}', ?, ?, ?)`);for(const a of o){const N=w(a,"trigger_id");if(N===void 0){t.push({trigger_id:"<unidentified row>",reason:"row carries no trigger_id"});continue}try{ze(e,i,a,N,T)}catch(L){t.push({trigger_id:N,reason:L instanceof Error?L.message:String(L)})}}t.length>0&&(e.prepare("INSERT INTO canvas_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(V,JSON.stringify(t)),console.warn(`[canvas migration v22] ${t.length} retired schedule(s) could not be converted to crons and were dropped; see canvas_meta['${V}']: `+t.map(a=>`${a.trigger_id} (${a.reason})`).join("; "))),e.exec(`
|
|
225
225
|
DROP INDEX IF EXISTS idx_triggers_fire;
|
|
226
226
|
DROP INDEX IF EXISTS idx_triggers_node;
|
|
227
227
|
DROP INDEX IF EXISTS idx_triggers_owner;
|
|
228
228
|
DROP INDEX IF EXISTS idx_triggers_eval;
|
|
229
229
|
DROP INDEX IF EXISTS idx_triggers_deadline;
|
|
230
230
|
DROP TABLE IF EXISTS triggers;
|
|
231
|
-
`)}E(Qe,"migrateTriggersToCrons");function ze(e,t,n,o,T){const i=X(n,"owner_id",o),a=e.prepare("SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(i),
|
|
231
|
+
`)}E(Qe,"migrateTriggersToCrons");function ze(e,t,n,o,T){const i=X(n,"owner_id",o),a=e.prepare("SELECT cwd, profile_id FROM nodes WHERE node_id = ?").get(i),N=(a===void 0?void 0:w(a,"cwd"))??T,L=a===void 0?null:w(a,"profile_id")??null,l=w(n,"node_id")??null,r=X(n,"action",o),_=X(n,"action_mode",o),d=X(n,"trigger_type",o),u=Ye(n.payload),p=w(n,"created")??new Date().toISOString(),I=_==="deadline_message"?"urgent":H(u,"tier")??"normal",R=H(u,"label");let s,A,m=!1;if(r==="msg"&&(_==="inbox_message"||_==="deadline_message")){if(l===null)throw new Error("has no message target");const f=H(u,"body");f!==void 0?s=z(l,I,f,o):d==="when"?(m=!0,s=Ke(l,I)):s=z(l,I,R??"Scheduled message (no body was recorded when this schedule was armed).",o),A=R??(_==="deadline_message"?"deadline":"scheduled message")}else if(r==="msg"&&_==="fresh_revive"){if(l===null)throw new Error("has no revive target");s=`crtr node lifecycle revive ${g(l)} --fresh`,A=R??"scheduled fresh revive"}else if(r==="new"&&_==="node_birth")s=qe(u,o),A=H(u,"name")??`scheduled ${X(u,"kind",o)} node`;else throw new Error(`has unsupported action ${r}/${_}`);let D,y,G=null;if(d==="when"){D=X(n,"next_eval_at",o),y=X(n,"eval_every",o),G=w(n,"timeout_at")??null;const f=X(n,"when_script",o);s=`${m?`if ! CRTR_GATE_STDOUT="$(
|
|
232
232
|
${f}
|
|
233
233
|
)"; then`:`if ! (
|
|
234
234
|
${f}
|
|
@@ -237,7 +237,7 @@ ${f}
|
|
|
237
237
|
fi
|
|
238
238
|
{
|
|
239
239
|
${s}
|
|
240
|
-
} && crtr cron cancel "$CRTR_CRON_ID"`}else if(d==="at"||d==="every"||d==="until")D=X(n,"fire_at",o),y=d==="every"?X(n,"recur",o):null;else throw new Error(`has unknown timing type ${d}`);if(_==="deadline_message"&&l!==null){const f=e.prepare("SELECT cron_id FROM crons WHERE anchor_node = ? AND cancel_on_wake = 1 AND cron_id <> ?").get(l,o);if(f!==void 0)throw new Error(`node ${l} already has a migrated deadline (${String(f.cron_id)}); only one is allowed`)}t.run(o,A,i,s,D,y,G,l,_==="deadline_message"?1:0,
|
|
240
|
+
} && crtr cron cancel "$CRTR_CRON_ID"`}else if(d==="at"||d==="every"||d==="until")D=X(n,"fire_at",o),y=d==="every"?X(n,"recur",o):null;else throw new Error(`has unknown timing type ${d}`);if(_==="deadline_message"&&l!==null){const f=e.prepare("SELECT cron_id FROM crons WHERE anchor_node = ? AND cancel_on_wake = 1 AND cron_id <> ?").get(l,o);if(f!==void 0)throw new Error(`node ${l} already has a migrated deadline (${String(f.cron_id)}); only one is allowed`)}t.run(o,A,i,s,D,y,G,l,_==="deadline_message"?1:0,N,L,300,I,p,p)}E(ze,"convertTriggerRow");function Ve(e){const t=b(e,"crons");t.size===0||t.has("run_pid_identity")||e.exec("ALTER TABLE crons ADD COLUMN run_pid_identity TEXT;")}E(Ve,"addCronRunPidIdentityColumn");function Ze(e){const t=b(e,"crons");t.size!==0&&(t.has("run_out_of_band")||e.exec("ALTER TABLE crons ADD COLUMN run_out_of_band INTEGER NOT NULL DEFAULT 0;"),t.has("queued")&&e.exec("ALTER TABLE crons DROP COLUMN queued;"))}E(Ze,"addCronOutOfBandColumn");function et(e){e.exec(`
|
|
241
241
|
CREATE TABLE IF NOT EXISTS warm_pool (
|
|
242
242
|
node_id TEXT PRIMARY KEY REFERENCES nodes(node_id) ON DELETE CASCADE,
|
|
243
243
|
recipe_key TEXT NOT NULL, -- frozen launch tuple this spare was minted for
|
|
@@ -378,18 +378,18 @@ CREATE TABLE human_action_deliveries (
|
|
|
378
378
|
updated_at INTEGER NOT NULL
|
|
379
379
|
);
|
|
380
380
|
CREATE INDEX idx_human_action_deliveries_due ON human_action_deliveries(state, next_attempt_at);
|
|
381
|
-
`)}E(Tt,"addHumanActionDeliveries");function at(e){const t=O(e);
|
|
381
|
+
`)}E(Tt,"addHumanActionDeliveries");function at(e){const t=O(e);c(e,t,"pi_session_file","ALTER TABLE nodes ADD COLUMN pi_session_file TEXT;");for(const n of k(h(),{withFileTypes:!0}))if(n.isDirectory())try{const o=JSON.parse(M(x(n.name),"utf8"));typeof o.pi_session_file=="string"&&e.prepare("UPDATE nodes SET pi_session_file = ? WHERE node_id = ?").run(o.pi_session_file,n.name)}catch{}e.exec("CREATE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);")}E(at,"addNodeSessionFileIndex");function Z(e,t){const n=E(N=>{const L=Y(N);return U(L)?le(L):L},"canonical"),o=e.prepare("UPDATE nodes SET pi_session_file = ? WHERE node_id = ?");e.exec("DROP INDEX IF EXISTS idx_nodes_pi_session_file;");const T=e.prepare("SELECT node_id FROM nodes ORDER BY created ASC, node_id ASC").all();for(const{node_id:N}of T){const L=x(N);if(K(L)===null){o.run(null,N);continue}const l=fe(L,()=>{const r=K(L);if(r===null)throw new Error(`node ${N} session metadata changed during ownership migration`);if(typeof r.pi_session_file!="string")return null;const _=r.pi_session_file,d=n(_);return d!==_&&(r.pi_session_file=d,Xe(L,r)),d});o.run(l,N)}const i=e.prepare(`
|
|
382
382
|
SELECT pi_session_file
|
|
383
383
|
FROM nodes
|
|
384
384
|
WHERE pi_session_file IS NOT NULL
|
|
385
385
|
GROUP BY pi_session_file
|
|
386
386
|
HAVING COUNT(*) > 1
|
|
387
|
-
`).all();if(!t&&i.length>0){const
|
|
387
|
+
`).all();if(!t&&i.length>0){const N=i.map(({pi_session_file:L})=>L).join(", ");throw new Error(`cannot restore unique Pi session ownership: authoritative metadata has concurrent claimants for ${N}`)}const a=e.prepare("UPDATE nodes SET pi_session_file = NULL WHERE node_id = ?");for(const{pi_session_file:N}of i){const L=e.prepare(`
|
|
388
388
|
SELECT node_id
|
|
389
389
|
FROM nodes
|
|
390
390
|
WHERE pi_session_file = ?
|
|
391
391
|
ORDER BY created ASC, node_id ASC
|
|
392
|
-
`).all(
|
|
392
|
+
`).all(N);for(const{node_id:l}of L.slice(1)){if(!q(x(l),_=>{const d=_.pi_session_file;if(typeof d!="string"||n(d)!==N)throw new Error(`node ${l} session metadata changed during ownership migration`);return _.pi_session_id=null,_.pi_session_file=null,!0}))throw new Error(`node ${l} session metadata changed during ownership migration`);a.run(l)}}e.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);")}E(Z,"reconcileNodeSessionFileOwners");function dt(e){Z(e,!0)}E(dt,"makeNodeSessionFileUnique");function _t(e){Z(e,!1)}E(_t,"repairNodeSessionFileUniqueIndex");function Nt(e){const t=O(e);c(e,t,"frozen_at","ALTER TABLE nodes ADD COLUMN frozen_at TEXT;"),c(e,t,"respawn_failures","ALTER TABLE nodes ADD COLUMN respawn_failures INTEGER NOT NULL DEFAULT 0;"),c(e,t,"respawn_not_before","ALTER TABLE nodes ADD COLUMN respawn_not_before TEXT;"),c(e,t,"launched_at","ALTER TABLE nodes ADD COLUMN launched_at TEXT;"),c(e,t,"last_settled_at","ALTER TABLE nodes ADD COLUMN last_settled_at TEXT;"),c(e,t,"terminal_reason","ALTER TABLE nodes ADD COLUMN terminal_reason TEXT;"),e.exec(`
|
|
393
393
|
UPDATE nodes SET terminal_reason = CASE
|
|
394
394
|
WHEN status = 'done' AND final_report IS NOT NULL THEN 'finalized'
|
|
395
395
|
WHEN status = 'done' AND intent = 'parked' THEN 'parked'
|
|
@@ -398,7 +398,7 @@ UPDATE nodes SET terminal_reason = CASE
|
|
|
398
398
|
WHEN status = 'dead' THEN 'crashed'
|
|
399
399
|
END
|
|
400
400
|
WHERE status IN ('done','canceled','dead') AND terminal_reason IS NULL;
|
|
401
|
-
`)}E(
|
|
401
|
+
`)}E(Nt,"addNodeLifecycleColumns");function ct(e){const t=O(e);c(e,t,"description","ALTER TABLE nodes ADD COLUMN description TEXT;"),c(e,t,"cycles","ALTER TABLE nodes ADD COLUMN cycles INTEGER;");const n=h();if(!U(n))return;const o=e.prepare("UPDATE nodes SET description = ?, cycles = ? WHERE node_id = ?");for(const T of k(n)){const i=x(T);if(!U(i))continue;let a;try{a=JSON.parse(M(i,"utf8"))}catch{continue}const N=typeof a.description=="string"?a.description:null,L=typeof a.cycles=="number"?a.cycles:null;o.run(N,L,T)}}E(ct,"addNodeLabelColumns");function Lt(e){const t=O(e);c(e,t,"outcome_kind","ALTER TABLE nodes ADD COLUMN outcome_kind TEXT;"),c(e,t,"outcome_reason","ALTER TABLE nodes ADD COLUMN outcome_reason TEXT;"),c(e,t,"outcome_at","ALTER TABLE nodes ADD COLUMN outcome_at TEXT;"),c(e,t,"outcome_revision","ALTER TABLE nodes ADD COLUMN outcome_revision INTEGER NOT NULL DEFAULT 0;"),c(e,t,"outcome_detail_json","ALTER TABLE nodes ADD COLUMN outcome_detail_json TEXT;"),c(e,t,"deadline_at","ALTER TABLE nodes ADD COLUMN deadline_at TEXT;"),e.exec(`
|
|
402
402
|
CREATE TABLE IF NOT EXISTS node_outcome_deliveries (
|
|
403
403
|
node_id TEXT PRIMARY KEY,
|
|
404
404
|
state TEXT NOT NULL,
|
|
@@ -431,17 +431,17 @@ UPDATE nodes SET outcome_kind='failure',
|
|
|
431
431
|
outcome_reason=COALESCE(terminal_reason,'crashed'),
|
|
432
432
|
outcome_at=COALESCE(finalized_at, created), outcome_revision=1
|
|
433
433
|
WHERE final_report IS NULL AND status IN ('done','dead','canceled') AND outcome_at IS NULL;
|
|
434
|
-
`)}E(Lt,"addNodeOutcomeAndDeadline");function lt(e){const t=O(e);
|
|
434
|
+
`)}E(Lt,"addNodeOutcomeAndDeadline");function lt(e){const t=O(e);c(e,t,"telemetry_tokens_in","ALTER TABLE nodes ADD COLUMN telemetry_tokens_in INTEGER;"),c(e,t,"telemetry_context_tokens","ALTER TABLE nodes ADD COLUMN telemetry_context_tokens INTEGER;"),c(e,t,"telemetry_last_activity","ALTER TABLE nodes ADD COLUMN telemetry_last_activity TEXT;"),c(e,t,"telemetry_updated_at","ALTER TABLE nodes ADD COLUMN telemetry_updated_at TEXT;");const n=e.prepare(`UPDATE nodes
|
|
435
435
|
SET telemetry_tokens_in = ?, telemetry_context_tokens = ?,
|
|
436
436
|
telemetry_last_activity = ?, telemetry_updated_at = ?
|
|
437
|
-
WHERE node_id = ?`);for(const{node_id:o}of e.prepare("SELECT node_id FROM nodes").all()){const T=j(h(),o,"job","telemetry.json");if(U(T))try{const i=JSON.parse(M(T,"utf8")),a=typeof i.tokens_in=="number"&&Number.isFinite(i.tokens_in)?i.tokens_in:null,
|
|
437
|
+
WHERE node_id = ?`);for(const{node_id:o}of e.prepare("SELECT node_id FROM nodes").all()){const T=j(h(),o,"job","telemetry.json");if(U(T))try{const i=JSON.parse(M(T,"utf8")),a=typeof i.tokens_in=="number"&&Number.isFinite(i.tokens_in)?i.tokens_in:null,N=typeof i.context_tokens=="number"&&Number.isFinite(i.context_tokens)?i.context_tokens:null,L=typeof i.last_activity=="string"?i.last_activity:null,l=ue(T).mtime.toISOString();n.run(a,N,L,l,o)}catch{}}}E(lt,"addNodeTelemetryColumns");function ut(e){e.exec(`
|
|
438
438
|
CREATE INDEX IF NOT EXISTS deliveries_external_due_order
|
|
439
439
|
ON deliveries(state, next_attempt_at, created_at, delivery_id)
|
|
440
440
|
WHERE destination_kind = 'external_action' AND canceled_at IS NULL;
|
|
441
441
|
DROP INDEX IF EXISTS deliveries_external_due;
|
|
442
442
|
CREATE INDEX IF NOT EXISTS idx_nodes_parent_created ON nodes(parent, created);
|
|
443
443
|
CREATE INDEX IF NOT EXISTS idx_edges_type_created ON edges(type, created);
|
|
444
|
-
`)}E(ut,"addRecurringLaneIndexes");function pt(e){_e(e)}E(pt,"projectLiveNodeState");function Rt(e){
|
|
444
|
+
`)}E(ut,"addRecurringLaneIndexes");function pt(e){_e(e)}E(pt,"projectLiveNodeState");function Rt(e){c(e,O(e),"scopes_json","ALTER TABLE nodes ADD COLUMN scopes_json TEXT;")}E(Rt,"addNodeScopes");function At(e){const t=O(e);c(e,t,"grantee","ALTER TABLE nodes ADD COLUMN grantee TEXT NOT NULL DEFAULT 'user:local';"),c(e,t,"sub","ALTER TABLE nodes ADD COLUMN sub TEXT;"),c(e,t,"grant_id","ALTER TABLE nodes ADD COLUMN grant_id TEXT;"),c(e,t,"client_id","ALTER TABLE nodes ADD COLUMN client_id TEXT;"),c(e,t,"metadata_json","ALTER TABLE nodes ADD COLUMN metadata_json TEXT;"),c(e,t,"run_id","ALTER TABLE nodes ADD COLUMN run_id TEXT;"),c(e,t,"event_cursor","ALTER TABLE nodes ADD COLUMN event_cursor INTEGER;"),c(e,t,"usage_json","ALTER TABLE nodes ADD COLUMN usage_json TEXT;"),c(e,t,"deleting_at","ALTER TABLE nodes ADD COLUMN deleting_at TEXT;"),e.exec(`
|
|
445
445
|
ALTER TABLE sessions ADD COLUMN event_offset INTEGER;
|
|
446
446
|
CREATE INDEX idx_nodes_run_id ON nodes(run_id);
|
|
447
447
|
-- Child nodes may be pruned while their root run and event history remain.
|
|
@@ -503,7 +503,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
|
|
|
503
503
|
);
|
|
504
504
|
CREATE UNIQUE INDEX idx_grants_grantee_sub ON grants(grantee, sub);
|
|
505
505
|
CREATE TABLE runtime_state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
506
|
-
`)}E(mt,"addGrantMirror");function ft(e){e.exec("UPDATE nodes SET grantee = 'app:terminal' WHERE grantee = 'user:local';")}E(ft,"migrateTerminalGrantees");function Xt(e){const t={llm:"crtr:llm",act:"crtr:act",schedule:"crtr:schedule","memory:read":"crtr:memory:read:user","memory:write":"crtr:memory:rw:user"},n=e.prepare("SELECT node_id, scopes_json FROM nodes WHERE scopes_json IS NOT NULL").all(),o=e.prepare("UPDATE nodes SET scopes_json = ? WHERE node_id = ?");for(const T of n){const i=JSON.parse(T.scopes_json);if(!Array.isArray(i))throw new Error(`invalid launched scopes for node ${T.node_id}`);const a=[];for(const
|
|
506
|
+
`)}E(mt,"addGrantMirror");function ft(e){e.exec("UPDATE nodes SET grantee = 'app:terminal' WHERE grantee = 'user:local';")}E(ft,"migrateTerminalGrantees");function Xt(e){const t={llm:"crtr:llm",act:"crtr:act",schedule:"crtr:schedule","memory:read":"crtr:memory:read:user","memory:write":"crtr:memory:rw:user"},n=e.prepare("SELECT node_id, scopes_json FROM nodes WHERE scopes_json IS NOT NULL").all(),o=e.prepare("UPDATE nodes SET scopes_json = ? WHERE node_id = ?");for(const T of n){const i=JSON.parse(T.scopes_json);if(!Array.isArray(i))throw new Error(`invalid launched scopes for node ${T.node_id}`);const a=[];for(const N of i){if(typeof N!="string")continue;const L=t[N]??N;try{ae(L),a.push(L)}catch(l){if(!(l instanceof TypeError))throw l}}o.run(JSON.stringify([...new Set(a)]),T.node_id)}}E(Xt,"migrateLaunchedScopes");function Ut(e){e.exec(`CREATE TABLE node_tokens (
|
|
507
507
|
broker_process_id TEXT PRIMARY KEY REFERENCES executions(execution_id) ON DELETE CASCADE,
|
|
508
508
|
node_id TEXT NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,
|
|
509
509
|
token_hash TEXT NOT NULL UNIQUE, grantee TEXT NOT NULL, uid INTEGER NOT NULL,
|
|
@@ -656,7 +656,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
|
|
|
656
656
|
ALTER TABLE messages ADD COLUMN level INTEGER;
|
|
657
657
|
ALTER TABLE messages ADD COLUMN actor_id TEXT;
|
|
658
658
|
`),ie(e),e.exec("ANALYZE"))}E(oe,"canvasObjectsSchema");function ie(e){const t=new Date().toISOString(),n=e.prepare("INSERT INTO objects (id, type, kind, owner, privacy, name, preview, created_at) VALUES (?, ?, NULL, NULL, 'public', ?, '', ?) RETURNING rowid"),o=e.prepare("INSERT INTO identities (object, scheme, external_id) VALUES (?, ?, ?)"),T=e.prepare(`SELECT DISTINCT n.grantee FROM nodes n
|
|
659
|
-
WHERE NOT EXISTS (SELECT 1 FROM identities i WHERE i.external_id = n.grantee AND i.scheme IN ('app', 'local-user'))`).all();for(const{grantee:i}of T){let a;if(i==="user:local")a={type:"person",scheme:"local-user",handle:"user"};else if(/^app:[a-z0-9][a-z0-9._-]*$/.test(i))a={type:"app",scheme:"app",handle:i.slice(4)};else throw new Error(`canvas objects migration: node grantee ${JSON.stringify(i)} is neither app:<name> nor user:local`);const{rowid:
|
|
659
|
+
WHERE NOT EXISTS (SELECT 1 FROM identities i WHERE i.external_id = n.grantee AND i.scheme IN ('app', 'local-user'))`).all();for(const{grantee:i}of T){let a;if(i==="user:local")a={type:"person",scheme:"local-user",handle:"user"};else if(/^app:[a-z0-9][a-z0-9._-]*$/.test(i))a={type:"app",scheme:"app",handle:i.slice(4)};else throw new Error(`canvas objects migration: node grantee ${JSON.stringify(i)} is neither app:<name> nor user:local`);const{rowid:N}=n.get(`${Date.now().toString(36)}-${P(4).toString("hex")}`,a.type,a.handle,t);o.run(N,a.scheme,i)}e.exec(`INSERT INTO objects (id, type, kind, owner, privacy, name, preview, created_at)
|
|
660
660
|
SELECT n.node_id, 'node', n.kind, i.object, 'public', n.name, '', n.created
|
|
661
661
|
FROM nodes n JOIN identities i ON i.external_id = n.grantee AND i.scheme IN ('app', 'local-user')
|
|
662
662
|
WHERE NOT EXISTS (SELECT 1 FROM objects o WHERE o.id = n.node_id)`)}E(ie,"copyNodesIntoObjects");function Dt(e){if(oe(e),!S(e,"nodes","name"))return;ie(e);const t=e.prepare("SELECT COUNT(*) AS n FROM nodes WHERE node_id NOT IN (SELECT id FROM objects)").get();if(t.n>0)throw new Error(`canvasObjectsCut: ${t.n} node(s) have no objects row`);e.exec(`
|
|
@@ -877,11 +877,11 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
|
|
|
877
877
|
JOIN objects o ON o.id = n.node_id AND o.type = 'node'
|
|
878
878
|
WHERE d.idempotency_key LIKE 'node-outcome:%'`).all(),a=e.prepare(`INSERT INTO objects
|
|
879
879
|
(id, type, kind, owner, privacy, name, preview, created_at, reception)
|
|
880
|
-
VALUES (?, 'action', NULL, ?, ?, ?, '', ?, 2) RETURNING rowid`),
|
|
880
|
+
VALUES (?, 'action', NULL, ?, ?, ?, '', ?, 2) RETURNING rowid`),N=e.prepare("INSERT INTO custom_objects (object, receiver_json, payload_json) VALUES (?, ?, ?)"),L=e.prepare(`INSERT INTO watches (watcher, object, in_tree, read_heads_key, created_at)
|
|
881
881
|
VALUES (?, ?, 1, NULL, ?) RETURNING rowid`),l=e.prepare(`INSERT INTO watch_subscriptions
|
|
882
882
|
(watch, pattern, filter_json, config_json, started_by, ends_on, created_at)
|
|
883
883
|
VALUES (?, 'outcome', NULL, '{"1":"after-turn"}', 'spawn', NULL, ?)`),r=e.prepare("UPDATE deliveries SET destination_id = ?, idempotency_key = NULL WHERE delivery_id = ?"),_=e.prepare("SELECT body_json FROM messages WHERE message_id = ?"),d=e.prepare(`UPDATE messages SET body_json = ?, event = 'outcome', object_id = ?, level = 1
|
|
884
|
-
WHERE message_id = ?`);for(const s of i){if(s.owner===null)throw new Error(`v67 outcome node ${s.node_id} has no owner`);const A=JSON.parse(s.adapter_json);if(A.adapter!=="subprocess"||!Array.isArray(A.argv)||typeof A.cwd!="string"||typeof A.config_origin!="string")throw new Error(`v67 invalid outcome adapter on ${s.delivery_id}`);const m=P(16).toString("hex"),D=new Date(s.created_at).toISOString(),y=a.get(m,s.owner,s.privacy,`outcome/${s.node_id}`,D);
|
|
884
|
+
WHERE message_id = ?`);for(const s of i){if(s.owner===null)throw new Error(`v67 outcome node ${s.node_id} has no owner`);const A=JSON.parse(s.adapter_json);if(A.adapter!=="subprocess"||!Array.isArray(A.argv)||typeof A.cwd!="string"||typeof A.config_origin!="string")throw new Error(`v67 invalid outcome adapter on ${s.delivery_id}`);const m=P(16).toString("hex"),D=new Date(s.created_at).toISOString(),y=a.get(m,s.owner,s.privacy,`outcome/${s.node_id}`,D);N.run(y.rowid,JSON.stringify({argv:A.argv,cwd:A.cwd,config_origin:A.config_origin,name:s.destination_id}),JSON.stringify(A.payload??null));const G=L.get(y.rowid,e.prepare("SELECT rowid FROM objects WHERE id = ?").get(s.node_id).rowid,D);if(l.run(G.rowid,D),s.message_id===null){if(s.state!=="armed")throw new Error(`v67 delivery ${s.delivery_id} has no message but is ${s.state}`);continue}const f=_.get(s.message_id);if(!f||f.body_json===null)throw new Error(`v67 delivery ${s.delivery_id} has no frozen outcome document`);if(e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE message_id = ?").get(s.message_id).n!==1)throw new Error(`v67 outcome message ${s.message_id} is shared`);const v=JSON.parse(f.body_json);if(v===null||Array.isArray(v)||typeof v!="object")throw new Error(`v67 invalid outcome body on ${s.delivery_id}`);delete v.action,delete v.delivery,d.run(JSON.stringify(v),s.node_id,s.message_id),r.run(m,s.delivery_id)}const u=e.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE state = 'armed'").get();if(u.n!==i.filter(s=>s.state==="armed").length)throw new Error(`v67 found ${u.n} armed deliveries but only ${i.length} mapped outcome rows`);const p=e.prepare("SELECT COUNT(*) AS n FROM deliveries").get().n-u.n,I=e.prepare("SELECT name, sql FROM sqlite_master WHERE type = 'view' AND sql LIKE '%deliveries%'").all();for(const s of I)e.exec(`DROP VIEW "${s.name.replaceAll('"','""')}"`);e.exec(`
|
|
885
885
|
CREATE TABLE deliveries_new (
|
|
886
886
|
delivery_id TEXT PRIMARY KEY,
|
|
887
887
|
message_id TEXT NOT NULL REFERENCES messages(message_id),
|
|
@@ -924,7 +924,7 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
|
|
|
924
924
|
state TEXT NOT NULL DEFAULT 'reserved' CHECK (state IN ('reserved', 'warm_claimed', 'complete'))
|
|
925
925
|
);
|
|
926
926
|
CREATE INDEX IF NOT EXISTS idx_create_requests_expires ON create_requests(expires_at);
|
|
927
|
-
`)}E(Ht,"addCreateRequests");function Gt(e){for(const[r,_]of[["sub","TEXT"],["grant_id","TEXT"],["scopes_json","TEXT"],["origin_run_id","TEXT"],["run_execution_id","TEXT"]])S(e,"crons",r)||e.exec(`ALTER TABLE crons ADD COLUMN ${r} ${_}`);const t=[],n=E(r=>e.prepare("SELECT id, sub, resolved_scopes_json FROM grants WHERE grantee = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r),"grantRow"),o=E(r=>e.prepare("SELECT grantee, sub, resolved_scopes_json FROM grants WHERE id = ?").get(r),"grantById"),T=E(r=>{try{const _=JSON.parse(r);return Array.isArray(_)?_.flatMap(d=>d&&d.mode==="always"&&typeof d.scope=="string"?[d.scope]:[]):[]}catch{return[]}},"snapshot"),i=E((r,_)=>{if(_===null)return null;const d=e.prepare("SELECT grantee, sub, grant_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(_);if(d===void 0||d.grantee!==r)return null;const u=r==="app:terminal";let p=null;if(d.scopes_json!==null){let A;try{A=JSON.parse(d.scopes_json)}catch{return{pause:"bad_scopes"}}if(!Array.isArray(A)||!A.every(m=>typeof m=="string"))return{pause:"bad_scopes"};p=A}const I=u?n(r):void 0;let R=d.grant_id??I?.id??null;R===null&&!u&&d.sub!==null&&(R=e.prepare("SELECT id FROM grants WHERE grantee = ? AND sub = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r,d.sub)?.id??null);const s=R===null?void 0:o(R);return R!==null&&(s===void 0||s.grantee!==r||d.sub!==null&&s.sub!==d.sub)?{pause:"no_grant"}:R===null&&!u?{pause:"no_grant"}:(p===null&&!u&&(p=T(s.resolved_scopes_json)),{principal:{sub:d.sub??s?.sub??I?.sub??"user:local",grant_id:R,scopes:p,origin_run_id:d.run_id}})},"fromCreator"),a=E(r=>{const _=n(r);return r==="app:terminal"?{sub:_?.sub??"user:local",grant_id:_?.id??null,scopes:null,origin_run_id:null}:_===void 0?null:{sub:_.sub,grant_id:_.id,scopes:T(_.resolved_scopes_json),origin_run_id:null}},"fromGrantee"),
|
|
927
|
+
`)}E(Ht,"addCreateRequests");function Gt(e){for(const[r,_]of[["sub","TEXT"],["grant_id","TEXT"],["scopes_json","TEXT"],["origin_run_id","TEXT"],["run_execution_id","TEXT"]])S(e,"crons",r)||e.exec(`ALTER TABLE crons ADD COLUMN ${r} ${_}`);const t=[],n=E(r=>e.prepare("SELECT id, sub, resolved_scopes_json FROM grants WHERE grantee = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r),"grantRow"),o=E(r=>e.prepare("SELECT grantee, sub, resolved_scopes_json FROM grants WHERE id = ?").get(r),"grantById"),T=E(r=>{try{const _=JSON.parse(r);return Array.isArray(_)?_.flatMap(d=>d&&d.mode==="always"&&typeof d.scope=="string"?[d.scope]:[]):[]}catch{return[]}},"snapshot"),i=E((r,_)=>{if(_===null)return null;const d=e.prepare("SELECT grantee, sub, grant_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(_);if(d===void 0||d.grantee!==r)return null;const u=r==="app:terminal";let p=null;if(d.scopes_json!==null){let A;try{A=JSON.parse(d.scopes_json)}catch{return{pause:"bad_scopes"}}if(!Array.isArray(A)||!A.every(m=>typeof m=="string"))return{pause:"bad_scopes"};p=A}const I=u?n(r):void 0;let R=d.grant_id??I?.id??null;R===null&&!u&&d.sub!==null&&(R=e.prepare("SELECT id FROM grants WHERE grantee = ? AND sub = ? ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, rev DESC LIMIT 1").get(r,d.sub)?.id??null);const s=R===null?void 0:o(R);return R!==null&&(s===void 0||s.grantee!==r||d.sub!==null&&s.sub!==d.sub)?{pause:"no_grant"}:R===null&&!u?{pause:"no_grant"}:(p===null&&!u&&(p=T(s.resolved_scopes_json)),{principal:{sub:d.sub??s?.sub??I?.sub??"user:local",grant_id:R,scopes:p,origin_run_id:d.run_id}})},"fromCreator"),a=E(r=>{const _=n(r);return r==="app:terminal"?{sub:_?.sub??"user:local",grant_id:_?.id??null,scopes:null,origin_run_id:null}:_===void 0?null:{sub:_.sub,grant_id:_.id,scopes:T(_.resolved_scopes_json),origin_run_id:null}},"fromGrantee"),N=e.prepare("SELECT cron_id, grantee, created_by, isolated, cwd FROM crons WHERE sub IS NULL").all(),L=e.prepare("UPDATE crons SET sub = ?, grant_id = ?, scopes_json = ?, origin_run_id = ? WHERE cron_id = ?"),l=new Map;for(const r of N){const _=i(r.grantee,r.created_by),d=_===null?a(r.grantee):"principal"in _?_.principal:null;if(d===null){const u=_!==null&&"pause"in _?_.pause:"no_grant";L.run("user:local",null,null,null,r.cron_id),e.prepare("UPDATE crons SET state = 'paused' WHERE cron_id = ?").run(r.cron_id),t.push({event:`cron.migrated.${u}`,fields:{cron_id:r.cron_id,grantee:r.grantee}})}else l.set(r.cron_id,d),L.run(d.sub,d.grant_id,d.scopes===null?null:JSON.stringify(d.scopes),d.origin_run_id,r.cron_id);if(r.isolated!==0){const u=j(Re(r.grantee).root,"crons",r.cron_id);e.prepare("UPDATE crons SET cwd = ? WHERE cron_id = ?").run(u,r.cron_id),t.push({event:"cron.migrated.isolated_cwd",fields:{cron_id:r.cron_id,old_cwd:r.cwd,cwd:u}})}}if(C(e,"daemon_actions")){const r=e.prepare("SELECT action_id, adapter_json FROM daemon_actions WHERE state IN ('pending', 'handed_off') AND json_extract(adapter_json, '$.adapter') = 'cron_spawn' AND json_extract(adapter_json, '$.principal') IS NULL").all();for(const _ of r){const d=JSON.parse(_.adapter_json),u=typeof d.cronId=="string"?d.cronId:null,p=u===null?void 0:e.prepare("SELECT grantee, sub, grant_id, scopes_json, origin_run_id, isolated, created_by FROM crons WHERE cron_id = ?").get(u);let I=null;if(p!==void 0&&!(p.grant_id===null&&p.grantee!=="app:terminal"))I={cron_id:u,grantee:p.grantee,sub:p.sub,grant_id:p.grant_id,scopes:p.scopes_json===null?null:JSON.parse(p.scopes_json),origin_run_id:p.origin_run_id,isolated:p.isolated!==0,creator:p.created_by};else if(p===void 0){const R=typeof d.parent=="string"?d.parent:null,s=R===null?void 0:e.prepare("SELECT grantee FROM node_rows WHERE node_id = ?").get(R),A=s===void 0?null:i(s.grantee,R),m=A===null||!("principal"in A)?null:A.principal;s!==void 0&&m!==null&&(I={cron_id:u,grantee:s.grantee,sub:m.sub,grant_id:m.grant_id,scopes:m.scopes,origin_run_id:m.origin_run_id,isolated:d.isolated===!0,creator:R})}if(I!==null)e.prepare("UPDATE daemon_actions SET adapter_json = ? WHERE action_id = ?").run(JSON.stringify({...d,principal:I}),_.action_id);else{const R=Date.now();e.prepare("UPDATE daemon_actions SET state = 'permanent_failed', failed_at = ?, updated_at = ?, claimed_at = NULL, claim_owner = NULL, next_attempt_at = NULL, last_failure_json = ? WHERE action_id = ?").run(R,R,JSON.stringify({kind:"grant_refused",message:"the cron has no derivable principal to authorize this birth"}),_.action_id),t.push({event:"cron.birth.refused",fields:{cron_id:u,action_id:_.action_id,reason:"grant_missing"}})}}}for(const{event:r,fields:_}of t)Ae({level:"warn",event:r,fields:_})}E(Gt,"addCronPrincipal");const F=[Ue,Ce,Se,De,ge,we,ye,he,ve,xe,Fe,Me,ke,J,je,J,He,Ge,be,Be,We,Qe,Ve,Ze,et,tt,nt,Et,ot,it,st,rt,Tt,at,Nt,dt,_t,ct,Lt,e=>{de(e),Ne(e),ce(e),Le(e)},ut,pt,lt,Rt,At,Ot,mt,Ut,ft,Xt,ee,te,ne,Ee,Ct,St,oe,Dt,Ie,gt,wt,yt,vt,kt,Mt,Ft,jt,xt,e=>e.exec(`
|
|
928
928
|
CREATE TABLE audit_rows (
|
|
929
929
|
id INTEGER PRIMARY KEY, request_id TEXT NOT NULL, time TEXT NOT NULL, grantee TEXT, sub TEXT,
|
|
930
930
|
client_id TEXT, class TEXT, operation TEXT NOT NULL, scope_checked TEXT,
|
|
@@ -1046,7 +1046,13 @@ CREATE INDEX idx_structured_requests_effects ON structured_requests(effects_done
|
|
|
1046
1046
|
CREATE UNIQUE INDEX objects_named ON objects(owner, name)
|
|
1047
1047
|
WHERE deleted_at IS NULL AND home_machine IS NULL AND (type = 'document' OR instr(type, '.') > 0);
|
|
1048
1048
|
CREATE UNIQUE INDEX objects_handle ON objects(type, name) WHERE home_machine IS NULL AND type IN ('person','app','repo','profile');
|
|
1049
|
-
`),e=>e.exec("ALTER TABLE grants ADD COLUMN inference_endpoint_json TEXT"),e=>e.exec("ALTER TABLE nodes ADD COLUMN reply_at TEXT; ALTER TABLE nodes ADD COLUMN read_at TEXT;"),Bt,bt
|
|
1049
|
+
`),e=>e.exec("ALTER TABLE grants ADD COLUMN inference_endpoint_json TEXT"),e=>e.exec("ALTER TABLE nodes ADD COLUMN reply_at TEXT; ALTER TABLE nodes ADD COLUMN read_at TEXT;"),Bt,bt,e=>e.exec(`
|
|
1050
|
+
CREATE TABLE IF NOT EXISTS unresolved_links (
|
|
1051
|
+
src INTEGER NOT NULL REFERENCES objects(rowid) ON DELETE CASCADE,
|
|
1052
|
+
name TEXT NOT NULL,
|
|
1053
|
+
PRIMARY KEY (src, name)
|
|
1054
|
+
) WITHOUT ROWID;
|
|
1055
|
+
`)];function bt(e){e.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'waits'").get()===void 0&&e.exec("CREATE TABLE waits (wait_id TEXT PRIMARY KEY, node_id TEXT NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE, kind TEXT, target_id TEXT, generation INTEGER, created_at INTEGER, satisfied_at INTEGER)");const t=e.prepare("SELECT COUNT(*) AS n FROM waits").get().n;e.exec(`
|
|
1050
1056
|
CREATE TABLE waits_new (
|
|
1051
1057
|
wait_id TEXT PRIMARY KEY,
|
|
1052
1058
|
node_id TEXT NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var R=Object.defineProperty;var f=(e,t)=>R(e,"name",{value:t,configurable:!0});import{spawn as T}from"node:child_process";import{socketFetch as A}from"../../../api/node-transport.js";import{apiSocketPath as
|
|
1
|
+
var R=Object.defineProperty;var f=(e,t)=>R(e,"name",{value:t,configurable:!0});import{spawn as T}from"node:child_process";import{socketFetch as A}from"../../../api/node-transport.js";import{apiSocketPath as j}from"../../canvas/paths.js";import{posixProcessGroupSpawnOptions as C,terminateProcessGroup as D}from"../../../hook-process.js";import{ExitCode as S}from"../../../types.js";import{isRecord as m}from"../../../shared/predicates.js";import{envExecutionId as G,envNodeId as K}from"../../../shared/env.js";import{validateDeclaredResult as M}from"../../../api/command-manifest/result.js";import{CrtrError as b}from"../../errors.js";import{diag as I}from"../../io.js";const q=10*1024*1024,V=15e3,B=new Set(["internal","unknown_path","command_collision","command_hook_collision","plugin_protocol_error","hook_protocol_error"]),H=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,L=/^[0-9a-f]{32}$/,$="Run `crtr pkg plugin show` or `crtr sys doctor` to inspect the plugin.";async function ie(e,t){const u=process.cwd(),o=F(e,t,u),r=await z(e,t,JSON.stringify(o),u);if(r.signal!==null)throw n(e,t,`executable was killed by signal ${r.signal}`,"a clean exit with one JSON envelope on stdout");const s=U(e,t,r.stdout);if(s.ok&&r.status!==0?I(`crtr: ${k(e)} returned ok:true but exited ${r.status} (honoring the envelope)`):!s.ok&&r.status===0&&I(`crtr: ${k(e)} returned ok:false but exited 0 (honoring the envelope)`),s.ok||X(e,t,s),e.phase!=="replace")return;const l=s.result;if(!m(l))throw n(e,t,"replacement success result was not an object","result: an object matching the target output declaration");const h=e.output;if(h===void 0)throw n(e,t,"replacement target output declaration was missing","the target leaf output declaration");const d=M(h,l);if(d!==null){const i=h.find(a=>a.name===d.field);throw d.receivedType!==void 0?n(e,t,`replacement output field "${d.field}" has the wrong type`,`${d.field}: ${d.expected??i?.type??"unknown"}`):n(e,t,`replacement result is missing required output field "${d.field}"`,`a "${d.field}" field of type ${i?.type??"unknown"}`)}return l}f(ie,"invokeExecHook");async function z(e,t,u,o){if(process.platform==="linux"){const s=K(),l=G(),h=process.env.CRTR_API_SOCKET??j();if(s&&!l)throw n(e,t,"executable requires a current node execution","a current node execution");let d;try{const i=s?`/v1/nodes/${encodeURIComponent(s)}/hooks/exec`:"/v1/hooks/exec";d=await A(h)(`http://localhost${i}`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({...s?{expected_execution_id:l}:{},hook_id:e.id,cwd:o,request:u})});const a=await d.json();if(!d.ok){const c=m(a)&&m(a.error)?a.error:null,g=c&&typeof c.message=="string"?c.message:`daemon refused hook execution (${d.status})`;throw c&&(c.code==="hook_timeout"||c.code==="hook_output_limit")?new b(c.code,`${k(e)}: ${g}`,S.GENERAL,N(e,t,void 0,$,g)):new Error(g)}if(!m(a)||typeof a.stdout!="string"||a.status!==null&&typeof a.status!="number"||a.signal!==null&&typeof a.signal!="string")throw new Error("invalid hook execution response");return{stdout:a.stdout,status:a.status,signal:a.signal}}catch(i){throw i instanceof b?i:n(e,t,`executable could not be run (${E(i)})`,"a registered runnable hook executable")}}let r;try{r=T(e.executable,["--crtr-hook-protocol","1"],{cwd:o,env:process.env,stdio:["pipe","pipe","inherit"],...C()})}catch(s){throw n(e,t,`executable could not be run (${E(s)})`,"a runnable hook executable")}return new Promise((s,l)=>{let h=0;const d=[];let i,a=!1,c=!1,g=null,P=null,w;const x=f(()=>{if(!(!a||i!==void 0&&!c)){if(w!==void 0&&clearTimeout(w),i?.kind==="timeout"){l(n(e,t,`executable timed out after ${V}ms`,"one JSON envelope before the execution timeout"));return}if(i?.kind==="output_limit"){l(n(e,t,"stdout exceeded the 10 MiB cap","one JSON envelope under 10 MiB on stdout"));return}if(i?.kind==="spawn"){l(n(e,t,`executable could not be run (${E(i.error)})`,"a runnable hook executable"));return}if(i?.kind==="cleanup"){l(n(e,t,`executable cleanup failed (${E(i.error)})`,"a hook executable whose process group can be terminated"));return}s({stdout:Buffer.concat(d).toString("utf8"),status:g,signal:P})}},"finish"),_=f(p=>{i===void 0&&(i=p,w!==void 0&&clearTimeout(w),D(r).then(()=>{c=!0,x()},O=>{i={kind:"cleanup",error:O},c=!0,x()}))},"terminate");r.stdout.on("data",p=>{if(h+=p.byteLength,h>q){_({kind:"output_limit"});return}d.push(p)}),r.once("error",p=>{if(r.pid===void 0){i={kind:"spawn",error:p},a=!0,c=!0,x();return}_({kind:"spawn",error:p})}),r.once("close",(p,O)=>{g=p,P=O,a=!0,x()}),w=setTimeout(()=>_({kind:"timeout"}),V),r.stdin.end(u)})}f(z,"runHookProcess");function F(e,t,u){if(!L.test(t.operationId))throw n(e,t,"operationId was not a 32-character lowercase hexadecimal string","a valid crouter operation identity");if(!y(t.input))throw n(e,t,"input was not JSON-compatible","a JSON object");if(!t.providedParams.every(r=>typeof r=="string"))throw n(e,t,"providedParams contained a non-string value","an array of parameter names");const o={protocolVersion:1,op:e.op,command:[...e.commandPath],operationId:t.operationId,input:t.input,context:{cwd:u,providedParams:[...t.providedParams]}};if(e.phase==="after"){if(t.result===void 0||!y(t.result))throw n(e,t,"after hook result was missing or not JSON-compatible","the completed primary result");return{...o,phase:"after",result:t.result}}if(t.result!==void 0)throw n(e,t,`${e.phase} hook invocation included a result`,"result only for after hooks");return e.phase==="before"?{...o,phase:"before"}:{...o,phase:"replace"}}f(F,"buildRequest");function U(e,t,u){let o;try{o=JSON.parse(u.trim())}catch{throw n(e,t,"stdout was not a single JSON envelope","exactly one JSON envelope on stdout")}if(!m(o))throw n(e,t,"stdout was not a JSON object","a { protocolVersion: 1, ok: boolean, ... } envelope");if(o.protocolVersion!==1)throw n(e,t,`unsupported protocolVersion ${String(o.protocolVersion)}`,"protocolVersion exactly 1");if(o.ok===!0){const s=e.phase==="replace"?["protocolVersion","ok","result"]:["protocolVersion","ok"];if(!J(o,s))throw n(e,t,"success envelope had missing or extra fields",`{ ${s.join(", ")} } only`);if(e.phase==="replace"){if(!y(o.result))throw n(e,t,"replacement result was not JSON-compatible","a JSON result");return{protocolVersion:1,ok:!0,result:o.result}}return{protocolVersion:1,ok:!0}}if(o.ok!==!1||!J(o,["protocolVersion","ok","error"]))throw n(e,t,"failure envelope had missing or extra fields","{ protocolVersion, ok, error } only");const r=o.error;if(!m(r)||!J(r,["code","message"],["field","next"]))throw n(e,t,"error envelope was missing fields or contained extra fields","error: { code, message, field?, next? }");if(typeof r.code!="string"||r.code===""||typeof r.message!="string"||r.message==="")throw n(e,t,"error envelope had an empty or non-string code/message","non-empty error code and message strings");if(r.field!==void 0&&typeof r.field!="string"||r.next!==void 0&&typeof r.next!="string")throw n(e,t,"error envelope had a non-string field/next","string field and next values when present");return{protocolVersion:1,ok:!1,error:{code:r.code,message:r.message,...r.field===void 0?{}:{field:r.field},...r.next===void 0?{}:{next:r.next}}}}f(U,"parseEnvelope");function X(e,t,u){const{code:o,message:r,field:s,next:l}=u.error;throw!H.test(o)||B.has(o)?n(e,t,`error code "${o}" was invalid or reserved`,"a non-reserved lowercase snake_case code"):new b(o,`${k(e)}: ${r}`,S.GENERAL,N(e,t,s,l))}f(X,"throwHookFailure");function n(e,t,u,o){return new b("hook_protocol_error",`${k(e)}: ${u}. Expected ${o}.`,S.GENERAL,N(e,t,void 0,$,u))}f(n,"protocolError");function N(e,t,u,o,r){const s={plugin:e.plugin,op:e.op,phase:e.phase,command:[...e.commandPath],...r===void 0?{}:{received:r},...u===void 0?{}:{field:u}};if(e.phase!=="after")return{...s,next:o??$};const l="Do not retry the original command; its primary action completed. Inspect and reconcile the failed after hook.";return{...s,primaryCompleted:!0,result:t.result,next:o===void 0||o===$?l:`${o} ${l}`}}f(N,"failureDetails");function k(e){return`plugin "${e.plugin}" ${e.phase} hook "${e.op}" for \`${e.commandPath.join(" ")}\``}f(k,"label");function J(e,t,u=[]){const o=Object.keys(e);return t.every(r=>Object.hasOwn(e,r))&&o.every(r=>t.includes(r)||u.includes(r))}f(J,"hasExactKeys");function y(e){return e===null||typeof e=="boolean"||typeof e=="string"?!0:typeof e=="number"?Number.isFinite(e):Array.isArray(e)?e.every(y):m(e)&&Object.values(e).every(y)}f(y,"isJson");function E(e){return e instanceof Error?e.message:String(e)}f(E,"describeError");export{V as EXEC_HOOK_TIMEOUT_MS,ie as invokeExecHook};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var w=Object.defineProperty;var u=(e,r)=>w(e,"name",{value:r,configurable:!0});import{spawnSync as v}from"node:child_process";import{validateDeclaredResult as $}from"../../../api/command-manifest/result.js";import{CrtrError as f}from"../../errors.js";import{diag as m}from"../../io.js";import{ExitCode as g}from"../../../types.js";import{isRecord as c}from"../../../shared/predicates.js";const E=10*1024*1024,h=new Set(["internal","unknown_path","command_collision","plugin_protocol_error"]),b=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,x="Run `crtr pkg plugin show` or `crtr sys doctor` to inspect the plugin.";function S(e,r,n){return{protocolVersion:1,command:e.commandPath,input:r,context:{cwd:n,...e.plugin!==void 0?{plugin:{name:e.plugin.name,version:e.plugin.version??e.plugin.manifest.version??null,scope:e.plugin.scope,root:e.plugin.root}}:{extension:e.extension}}}}u(S,"buildLeafRequest");function D(e,r){const n=S(e,r,process.cwd()),t=v(e.executable,["--crtr-command-protocol","1"],{input:JSON.stringify(n),cwd:process.cwd(),env:{...process.env,...e.appEnv},maxBuffer:E,stdio:["pipe","pipe","inherit"],encoding:"utf8"});if(t.error!==void 0){const o=t.error;throw o.code==="ENOBUFS"||o.code==="ERR_CHILD_PROCESS_STDIO_MAXBUFFER"?i(e,"stdout exceeded the 10 MiB cap","one JSON envelope under 10 MiB on stdout"):i(e,`executable could not be run (${o.code??o.message})`,"a runnable command executable")}return y(e,t.stdout??"",t.status,t.signal)}u(D,"executeExternalLeaf");function y(e,r,n,t){if(t!==null)throw i(e,`executable was killed by signal ${t}`,"a clean exit with one JSON envelope on stdout");let o;try{o=JSON.parse(r.trim())}catch{throw i(e,"stdout was not a single JSON envelope","exactly one JSON envelope on stdout")}if(!c(o))throw i(e,"stdout was not a JSON object","a { protocolVersion: 1, ok: boolean, ... } envelope");if(o.protocolVersion!==1)throw i(e,`unsupported protocolVersion ${String(o.protocolVersion)}`,"protocolVersion exactly 1");if(typeof o.ok!="boolean")throw i(e,'envelope is missing a boolean "ok"',"a boolean ok field");const a=o.ok;if(a&&n!==0?m(`crtr: ${p(e)} returned ok:true but exited ${n} (honoring the envelope)`):!a&&n===0&&m(`crtr: ${p(e)} returned ok:false but exited 0 (honoring the envelope)`),a){const s=o.result;if(!c(s))throw i(e,"success envelope has no result object","result: an object matching the declared output");return k(e,s),s}const l=o.error;if(!c(l)||typeof l.code!="string"||typeof l.message!="string")throw i(e,"error envelope is missing a { code, message }","error: { code, message, ... }");const d=l.code;throw!b.test(d)||h.has(d)?i(e,`error code "${d}" is invalid or reserved`,`a lowercase snake_case code not in {${[...h].join(", ")}}`):new f(d,l.message,g.GENERAL,{received:l.received,field:typeof l.field=="string"?l.field:void 0,next:typeof l.next=="string"?l.next:x})}u(y,"parseLeafEnvelope");function k(e,r){const n=$(e.output,r);if(n===null)return;const t=n.field,o=e.output.find(a=>a.name===t);throw n.receivedType!==void 0?i(e,`output field "${t}" has the wrong type`,`${t}: ${n.expected??o?.type??"unknown"}`):i(e,`result is missing required output field "${t}"`,`a "${t}" field of type ${o?.type??"unknown"}`)}u(k,"validateResult");function i(e,r,n){return new f("plugin_protocol_error",`${p(e)}: ${r}. Expected ${n}.`,g.GENERAL,{received:r,next:x})}u(i,"protocolError");function p(e){return e.plugin!==void 0?`plugin "${e.plugin.name}" command \`${e.commandPath.join(" ")}\``:`extension branch "${e.extension.branch}" command \`${e.commandPath.join(" ")}\``}u(p,"label");export{E as MAX_STDOUT,S as buildLeafRequest,D as executeExternalLeaf,p as label,y as parseLeafEnvelope,i as protocolError};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var R=Object.defineProperty;var
|
|
1
|
+
var R=Object.defineProperty;var c=(e,t)=>R(e,"name",{value:t,configurable:!0});import{readFileSync as O,statSync as k}from"node:fs";import{resolve as j}from"node:path";import{validateDeclaredResult as C}from"../../../api/command-manifest/result.js";import{CrtrError as w}from"../../errors.js";import{envelopeClaimsStale as P,staleErrorDetails as z}from"../../manifest-stale.js";import{ExitCode as h}from"../../../types.js";import{isRecord as x}from"../../../shared/predicates.js";import{trimHeaderValue as U}from"./sign.js";const q=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,T=new Set(["internal","unknown_path","command_collision","cli_protocol_error"]),D=new Set(["authorization","crtr-request-id","crtr-run-id","crtr-approval","crtr-upload-id","crtr-upload-link","accept","content-type","crtr-caller","crtr-signature"]),M=new Set(["crtr-caller","crtr-signature","crtr-request-id"]);function te(e,t,n,r){const i=new Map;for(const o of e.params)i.set(o.name,o);const f=Object.entries(e.rest.params),u=e.baseUrl??new URL(e.registration.endpoint).origin;let y=e.rest.path;for(const[o,a]of f){if(a.in!=="path")continue;const d=t[S(o)];y=y.split(`{${o}}`).join(I(d))}const b=new URL(y,u);for(const[o,a]of f){if(a.in!=="query")continue;const d=S(o);v(i.get(o),d,t,n)&&b.searchParams.append(a.as??o,String(t[d]))}const m=G(e,f,i,t,n,r.fileMode),p=Object.keys(m).length>0?JSON.stringify(m):void 0,l={},s=c((o,a)=>{const d=Object.keys(l).find(g=>g.toLowerCase()===o.toLowerCase());d!==void 0&&delete l[d],l[o]=U(a)},"setHeader");s("Accept","application/json"),s("Crtr-Request-Id",r.requestId),r.authorization!==null&&s("Authorization",r.authorization);for(const[o,a]of f){if(a.in!=="header")continue;const d=S(o);if(!v(i.get(o),d,t,n))continue;const g=a.as;if(M.has(g.toLowerCase())||r.fileMode==="pass-through"&&D.has(g.toLowerCase()))throw new w("invalid_type",`${E(e)}: header "${g}" is reserved for the runtime`,h.USAGE,{field:o,next:"Remove the runtime-owned header mapping from the manifest."});const A=String(t[d]);if(/[\r\n]/.test(A))throw new w("invalid_type",`${E(e)}: header "${g}" value must not contain a newline`,h.USAGE,{field:o,received:"(newline in header value)",next:"Remove newline characters from the value."});s(g,A)}p!==void 0&&s("Content-Type","application/json");for(const[o,a]of Object.entries(r.extraHeaders)){if(r.fileMode==="pass-through"&&/[\r\n]/.test(a))throw new w("invalid_type",`${E(e)}: header "${o}" value must not contain a newline`,h.USAGE);s(o,a)}if(r.fileMode==="pass-through"){if(s("Accept","application/json"),p!==void 0)s("Content-Type","application/json");else{const o=Object.keys(l).find(a=>a.toLowerCase()==="content-type");o!==void 0&&delete l[o]}if(s("Crtr-Request-Id",r.requestId),r.authorization!==null)s("Authorization",r.authorization);else{const o=Object.keys(l).find(a=>a.toLowerCase()==="authorization");o!==void 0&&delete l[o]}}return{url:b,method:e.rest.method,headers:l,...p!==void 0?{body:p}:{}}}c(te,"buildRequestPlan");function G(e,t,n,r,i,f){const u=new Map;for(const[m,p]of t){if(p.in!=="body")continue;const l=p.as??m,s=u.get(l);s!==void 0?s.push(m):u.set(l,[m])}const y={};for(const[m,p]of u){const l=p.map(d=>({name:d,param:n.get(d)})),s=l.find(d=>d.param.kind!=="stdin"),o=l.find(d=>d.param.kind==="stdin");let a;s!==void 0&&v(s.param,S(s.name),r,i)?a={param:s.param,value:r[S(s.name)]}:o!==void 0&&v(o.param,S(o.name),r,i)&&(a={param:o.param,value:r[S(o.name)]}),a!==void 0&&(y[m]=H(e,a.param,a.value,f))}const b={...e.rest.body};return e.rest.bodyRoot!==void 0?b[e.rest.bodyRoot]=y:Object.assign(b,y),b}c(G,"assembleBody");function H(e,t,n,r){return r==="pass-through"||t.kind==="context-file"?n:(t.kind==="positional"||t.kind==="flag")&&t.type==="path"?N(E(e),t,n):n}c(H,"materializeBodyValue");function N(e,t,n){if(Array.isArray(n))return n.map(u=>N(e,t,u));const r=String(n),i=j(process.cwd(),r);let f;try{f=k(i)}catch{throw _(e,t.name,r,"no file exists at that path","an existing local file path")}if(!f.isFile())throw _(e,t.name,r,"the path is not a regular file","a regular local file (not a directory, socket, or device)");try{return"encoding"in t&&t.encoding==="base64"?O(i).toString("base64"):O(i,"utf8")}catch{throw _(e,t.name,r,"the file could not be read","a readable local file path")}}c(N,"readPathParam");function _(e,t,n,r,i){return new w("invalid_type",`${e}: cannot read file for "${t}" \u2014 ${r}: ${n}`,h.USAGE,{field:t,received:n,next:`Pass ${i}, relative to the current directory or absolute.`})}c(_,"fileError");function v(e,t,n,r){return r!==void 0?r.providedParams.has(t):Object.prototype.hasOwnProperty.call(n,t)&&n[t]!==void 0}c(v,"isPresent");function I(e){const t=String(e);return t==="."||t===".."?t.replace(/\./g,"%2E"):encodeURIComponent(t)}c(I,"encodePathSegmentValue");function S(e){return e.replace(/-([a-z])/g,(t,n)=>n.toUpperCase())}c(S,"paramKey");function ne(e,t){let n;try{n=JSON.parse(t)}catch{throw $(e,"response body was not valid JSON","a JSON object matching the declared output")}if(!x(n))throw $(e,`response body was not a JSON object (got ${V(n)})`,"a JSON object matching the declared output");const r=C(e.output,n);if(r!==null){const f=e.output.find(u=>u.name===r.field)?.type??"unknown";throw r.receivedType!==void 0?$(e,`output field "${r.field}" has the wrong type`,`${r.field}: ${r.expected??f}`):$(e,`response is missing required output field "${r.field}"`,`a "${r.field}" field of type ${f}`)}return n}c(ne,"mapUnary2xx");function re(e,t,n){const r=L(t),i=J(n);if(i!==null){const f=q.test(i.code)&&!T.has(i.code);throw new w(f?i.code:"cli_protocol_error",i.message,r,{...i.received!==void 0?{received:i.received}:{},...i.field!==void 0?{field:i.field}:{},...i.manifestStale?z(e.registration.name):{},http_status:t,next:i.next??"Inspect the backend response, then retry if appropriate."})}throw new w("cli_http_error",`${E(e)}: request failed with HTTP ${t}`,r,{http_status:t,next:"Inspect the backend response, then retry if appropriate."})}c(re,"throwNon2xx");function J(e){let t;try{t=JSON.parse(e)}catch{return null}if(!x(t)||!x(t.error))return null;const n=t.error;return typeof n.code!="string"||typeof n.message!="string"?null:{code:n.code,message:n.message,...typeof n.field=="string"?{field:n.field}:{},...n.received!==void 0?{received:n.received}:{},...typeof n.next=="string"?{next:n.next}:{},manifestStale:P(n),...n.question!==void 0?{question:n.question}:{}}}c(J,"parseHttpError");function L(e){return e===400||e===401||e===403||e===422?h.USAGE:e===404?h.NOT_FOUND:e===409?h.AMBIGUOUS:h.GENERAL}c(L,"deriveExit");function $(e,t,n){return new w("cli_protocol_error",`${E(e)}: ${t}. Expected ${n}.`,h.GENERAL,{received:t,next:`Run \`crtr pkg plugin update ${e.registration.name}\` or inspect the backend response.`})}c($,"protocolError");function E(e){return`HTTP-transport plugin "${e.registration.name}" command \`${e.commandPath.join(" ")}\``}c(E,"label");function V(e){return e===null?"null":Array.isArray(e)?"array":typeof e}c(V,"typeName");export{te as buildRequestPlan,ne as mapUnary2xx,J as parseHttpError,N as readPathParam,re as throwNon2xx};
|