@north-light/cli 0.3.496 → 0.3.498
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/builtin-memory/crouter-sdk/release-notes.md +2 -1
- package/dist/clients/attach/viewer.js +9 -9
- package/dist/commands/cron.js +5 -5
- package/dist/core/canvas/migrations.js +61 -55
- package/dist/core/graph/documents.js +5 -5
- package/dist/core/graph/kinds/document.js +4 -4
- package/dist/core/graph/names.js +3 -3
- package/dist/core/graph/objects.d.ts +2 -0
- package/dist/core/graph/objects.js +8 -8
- package/dist/core/graph/reader.js +3 -3
- package/dist/core/graph/repo-sync/repos.js +3 -3
- package/dist/core/peer/stubs.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/reader.js +1 -1
- package/dist/daemon/cron/ended.d.ts +14 -0
- package/dist/daemon/cron/ended.js +2 -0
- package/dist/daemon/cron/passes.js +1 -1
- package/dist/launcher/limits/manager.js +1 -1
- package/dist/launcher/spec.d.ts +2 -2
- package/docs/sdk/release-notes.md +2 -1
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
package/dist/commands/cron.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
var W=Object.defineProperty;var o=(n,e)=>W(n,"name",{value:e,configurable:!0});import{envNodeId as P,envProfileId as U}from"../shared/env.js";import{existsSync as M,statSync as H}from"node:fs";import{resolve as Y}from"node:path";import{defineBranch as B,defineLeaf as h}from"../core/command.js";import{InputError as c,readStdinRaw as V}from"../core/io.js";import{parseWhen as z,parseCadence as Z,cadenceDisplay as k}from"../core/wake.js";import{profilesStateBlock as j,resolveProfileViaDaemon as J}from"./profile/daemon-profiles.js";import{cliClient as l,rethrowAsCliError as u}from"./api-client.js";async function L(n){return(await J(n.trim())).id}o(L,"canonicalProfileId");function b(n,e){return e!==void 0&&e.trim()!==""?{tz:e,now:n}:{now:n}}o(b,"parseOpts");function $(n){const e={wake_in_past:'Pick a future instant \u2014 a positive duration ("5m","2h") or an ISO time later than now.',bad_when:'Use a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00").',bad_cadence:'Use a duration ("6h"), a 5-field cron ("0 9 * * *"), or an @alias ("@daily").',unknown_zone:'Pass --tz with an IANA zone name (e.g. "America/New_York").',cadence_too_fast:'Use a cadence of at least 60s (e.g. "1m","5m","1h").'};throw new c({error:n.code,message:n.message,received:n.received,next:e[n.code]??"Fix the time value and retry."})}o($,"throwTimeError");function G(n){const e=/^(\d+)(s|m|h)?$/.exec(n.trim());if(e===null)throw new c({error:"bad_run_timeout",message:`"${n}" is not a valid run timeout`,field:"run-timeout",next:'Use a duration like "90s", "5m", "1h", or bare seconds ("300").'});const r=Number(e[1]),t=e[2]==="h"?3600:e[2]==="m"?60:1,i=r*t;if(i<=0)throw new c({error:"bad_run_timeout",message:"run timeout must be positive",field:"run-timeout",next:'Use a positive duration, e.g. "5m".'});return i}o(G,"parseRunTimeout");function f(n){const e=n??U();return e!==void 0&&e.trim()!==""?{profile:e}:void 0}o(f,"scopeQuery");function Q(n){if(n===void 0)return process.cwd();const e=Y(n);if(!M(e)||!H(e).isDirectory())throw new c({error:"bad_cwd",message:`${e} is not a directory`,field:"cwd",next:"Pass --cwd with an existing directory, or omit it to inherit this one."});return e}o(Q,"resolveCwd");function X(n){const e=Array.isArray(n)?n.map(String):typeof n=="string"&&n!==""?[n]:[];if(e.length===0)return null;const r={};for(const t of e){const i=t.indexOf("="),s=i===-1?"":t.slice(0,i);if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s))throw new c({error:"bad_env",message:`"${t}" is not KEY=VALUE`,field:"env",next:"Pass --env KEY=VALUE (repeat the flag per variable); KEY must be a shell-legal name."});r[s]=t.slice(i+1)}return JSON.stringify(r)}o(X,"resolveEnvJson");function p(n){const e=typeof n.cronId=="string"?n.cronId.trim():"";if(e==="")throw new c({error:"missing_cron_id",message:"a cron id is required",field:"cron-id",next:"Pass the cron id from `crtr cron list`."});return e}o(p,"requireCronId");function ee(n){if(n===null)return"never run";const e=n.finished??"",r=n.exit_code===0?"ok":n.exit_code===-1?"TIMEOUT":n.exit_code===null?"no exit":`exit ${n.exit_code}`;return e===""?r:`${r} ${e}`}o(ee,"healthDisplay");function ne(n){const e=n.exit_code===null?"no exit":n.exit_code===-1?"TIMEOUT":`exit ${n.exit_code}`,r=n.duration_ms!==null?`${n.duration_ms}ms`:"\u2014";return`- ${n.started} \u2014 ${e}, ${r} \u2014 ${n.delivered??"(no disposition recorded)"}`}o(ne,"renderRun");const te=h({name:"add",description:"arm a scheduled command (bash on stdin; one of --at / --every)",whenToUse:"you need to arm a new bash schedule, rather than inspect or control an existing cron.",help:{name:"cron add",summary:"arm a scheduled command",params:[{kind:"stdin",name:"command",required:!0,constraint:"Arbitrary bash the daemon runs at each fire. Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Runs via `bash -c` in this cwd with $CRTR_CRON_ID, $CRTR_CRON_NAME, and $CRTR_RUN_ID in the env ($CRTR_ANCHOR and the anchor node's $CRTR_CONTEXT_DIR too when anchored; an unanchored cron has no context dir). Use absolute paths: `~` is the app's HOME, not the arming node's. The scheduler does not interpret it as a node action; a bash predicate can decide whether this fire acts, not arm a native predicate trigger. A run that must branch on canvas state should call `crtr --json <cmd>` and read the leaf's declared outputs, rather than parsing the prose output."},{kind:"flag",name:"name",type:"string",required:!0,constraint:"Agent-legible label \u2014 how the cron shows in `cron list`."},{kind:"flag",name:"at",type:"string",required:!1,constraint:`One-shot: run the stored bash once at <when> \u2014 a duration ("90s","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", interpreted in --tz else host zone). Exactly one of --at / --every is required. The row is deleted after its run unless the run's disposition pauses it.`},{kind:"flag",name:"every",type:"string",required:!1,constraint:'Recurring: run the stored bash at each cadence \u2014 a fixed interval ("6h" \u2014 first fire one interval from now), a 5-field cron ("0 9 * * *"), or an @alias ("@daily"). Each fire is another bash run. Minimum cadence 60s. Exactly one of --at / --every is required. Use this for a bash condition that only the local filesystem knows: if it exits 75, the next cadence re-checks it. A host platform can also re-check it early by calling the daemon\'s `POST /v1/crons/poke` endpoint.'},{kind:"flag",name:"tz",type:"string",required:!1,constraint:'IANA zone (e.g. "America/New_York") for a bare-ISO --at or a calendar --every. Defaults to the host zone.'},{kind:"flag",name:"on-output",type:"enum",choices:["silent","on-failure","always","on-change"],required:!1,constraint:"What each run does with its output. Defaults to on-change for a node:<id> sink; otherwise on-failure. silent: record in the run log, tell nobody \u2014 never escalate. on-failure: no success delivery; a failing run pauses the cron and spawns a node to deal with it. always: deliver stdout every run to --sink at --tier. on-change: deliver stdout only when it differs from the previous run's \u2014 the shape polling wants."},{kind:"flag",name:"sink",type:"string",required:!1,constraint:`Where always/on-change stdout deliveries go: "node:<id>" (an existing node's inbox \u2014 must exist now; gone/finalized at fire time is a failure), "spawn:<kind>" (each delivered output creates a self-finishing node with stdout as kickoff \u2014 a managed child while its creator exists, otherwise a terminal parentless root), or "human" (the humanloop inbox). Required by always/on-change, rejected otherwise.`},{kind:"flag",name:"tier",type:"enum",choices:["normal","urgent","critical"],required:!1,default:"urgent",constraint:"Inbox priority of a node-sink delivery. urgent (default): steers the target mid-turn, or wakes it when dormant \u2014 a fire is news the node asked for on a schedule, so it should land now. normal: waits for the running turn to settle. critical: aborts the running turn outright."},{kind:"flag",name:"expires",type:"string",required:!1,constraint:"Clock bound (same grammar as --at): the row is deleted once this instant passes. The self-limiting half of a poll-until pattern. For a held one-shot (last run exited 75), expiry bounds the wait for eligibility \u2014 the row is deleted UNFIRED, never fired blind at expiry."},{kind:"flag",name:"anchor",type:"string",required:!1,constraint:"Couple the cron's lifetime to a node: the row is deleted when that node is deleted. Without an anchor it outlives every node; it ends through its one-shot run, --expires, explicit cancellation, or self-cancellation from its bash."},{kind:"flag",name:"anchor-self",type:"bool",required:!1,constraint:"Anchor to the calling node ($CRTR_NODE_ID). Errors outside a node."},{kind:"flag",name:"cancel-on-wake",type:"bool",required:!1,constraint:"Also delete the row when the anchor node revives: use only when the cron belongs to that current dormancy, not for standing work. Requires an anchor; at most one cancel-on-wake cron per anchor \u2014 arming a second replaces the anchor\u2019s pending one."},{kind:"flag",name:"run-timeout",type:"string",required:!1,default:"5m",constraint:'Kill a run that exceeds this ("90s","5m","1h", or bare seconds). A timeout is a failure.'},{kind:"flag",name:"overlap",type:"enum",choices:["skip","queue","replace"],required:!1,default:"skip",constraint:"A fire landing while the previous run is still going. skip (default): log the skip, do not start a second run. queue: run once more after the current finishes (depth 1; further fires collapse). replace: kill the running instance and start fresh."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Directory every run executes in. Defaults to this one, must exist now, and is snapshotted onto the row \u2014 a cron never re-resolves its context. In a hosted runtime, runs are sandboxed and see only the app's own folders: an omitted --cwd that is outside them becomes the node's directory or the app files directory, and an explicit --cwd outside them is refused now (`cron show` prints the directory the row got)."},{kind:"flag",name:"env",type:"string",required:!1,repeatable:!0,constraint:"KEY=VALUE added to every run's env (repeat per variable). Snapshotted at arm time; overlays the operational env, alongside $CRTR_CRON_ID/$CRTR_CRON_NAME/$CRTR_RUN_ID."},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile the runs resolve config and credentials under, and \u2014 under --scope profile \u2014 the profile this cron lists and cancels within. A profile id or manifest name from the <profiles> list below; a close-enough name resolves to the nearest profile. Defaults to the calling node's ($CRTR_PROFILE_ID)."},{kind:"flag",name:"scope",type:"enum",choices:["profile","global"],required:!1,constraint:"Who lists and cancels this cron: profile (default when a profile is resolved) confines it to that profile; global makes it canvas-home-wide, visible from every profile."}],output:[{name:"cron_id",type:"string",required:!0,constraint:"The server-minted armed cron id \u2014 inspect with `cron show <cron_id>`, cancel with `cron cancel <cron_id>`."},{name:"fire_at",type:"string",required:!0,constraint:"Next fire (UTC ISO)."},{name:"recur",type:"string",required:!0,constraint:'Cadence display; "none" for a one-shot.'}],outputKind:"object",effects:["Arms one cron row (daemon-run; survives this process and every node).","Each fire runs the command via bash in this cwd and records a run-log entry.","A failing run (nonzero exit, timeout, undeliverable sink) pauses the cron and spawns a node to deal with it \u2014 unless --on-output silent."]},helpState:{leaf:j},run:o(async n=>{let e=typeof n.command=="string"?n.command.trim():"";if(e===""&&(e=(await V()).trim()),e==="")throw new c({error:"missing_command",message:"no command on stdin",field:"command",next:"Pipe the bash command on stdin (e.g. from a single-quoted heredoc)."});const r=typeof n.name=="string"?n.name.trim():"";if(r==="")throw new c({error:"missing_name",message:"--name is required",field:"name",next:"Pass --name <label>."});const t=typeof n.at=="string"&&n.at!==""?n.at:void 0,i=typeof n.every=="string"&&n.every!==""?n.every:void 0;if(t===void 0==(i===void 0))throw new c({error:"bad_schedule",message:t===void 0?"one of --at / --every is required":"--at and --every are mutually exclusive",next:"Pass exactly one: --at <when> for a one-shot, or --every <cadence> for a recurrence."});const s=typeof n.tz=="string"&&n.tz!==""?n.tz:void 0,m=new Date;let d,T=null;if(i!==void 0){const a=Z(i,b(m,s));"error"in a&&$(a.error),d=a.firstFireAt,T=a.recur}else{const a=z(t,b(m,s));"error"in a&&$(a.error),d=a.fireAt}let R=null;const C=typeof n.expires=="string"&&n.expires!==""?n.expires:void 0;if(C!==void 0){const a=z(C,b(m,s));"error"in a&&$(a.error),R=a.fireAt}const q=typeof n.anchor=="string"&&n.anchor!==""?n.anchor:void 0,A=n.anchorSelf===!0;if(q!==void 0&&A)throw new c({error:"bad_anchor",message:"--anchor and --anchor-self are mutually exclusive",next:"Pass one anchor or the other."});let w=q??null;if(A){const a=P();if(a===void 0||a==="")throw new c({error:"no_self_node",message:"--anchor-self requires a calling node ($CRTR_NODE_ID is unset)",next:"Run from inside a node, or pass --anchor <node-id> explicitly."});w=a}const I=n.cancelOnWake===!0;if(I&&w===null)throw new c({error:"cancel_on_wake_needs_anchor",message:"--cancel-on-wake requires an anchor",next:"Add --anchor <node-id> or --anchor-self."});const O=typeof n.runTimeout=="string"&&n.runTimeout!==""?G(n.runTimeout):void 0,E=typeof n.cwd=="string"&&n.cwd!==""?n.cwd:void 0,g=P(),S=g?await l().getNode(g).catch(u):null,K=Q(E??(S?.isolated?S.cwd:void 0)),F=X(n.env),D=typeof n.profile=="string"&&n.profile!==""?n.profile:void 0,y=U(),_=D!==void 0?await L(D):y!==void 0&&y.trim()!==""?y:void 0,N=n.scope??(_!==void 0?"profile":"global");if(N==="profile"&&_===void 0)throw new c({error:"scope_needs_profile",message:"--scope profile names the profile this cron lists and cancels within, but no profile is resolved",field:"profile",next:"Pass --profile <id>, or use --scope global for a canvas-home-wide cron."});const v=g,x=await l().armCron({name:r,command:e,fire_at:d,recur:T,tz:s??null,created_by:v!==void 0&&v!==""?v:null,cwd:K,...E===void 0?{cwd_is_default:!0}:{},env_json:F,profile:_??null,scope:N,on_output:n.onOutput,sink:typeof n.sink=="string"&&n.sink!==""?n.sink:null,tier:n.tier??"urgent",expires_at:R,anchor_node:w,cancel_on_wake:I,...O!==void 0?{run_timeout_s:O}:{},overlap:n.overlap??"skip"}).catch(u);return{cron_id:x.cron_id,fire_at:x.fire_at,recur:k(x.recur)}},"run"),render:o(n=>{const e=String(n.recur),r=e==="none"?"one-shot (row deleted after the run)":e;return`Cron armed \u2014 ${String(n.cron_id)}.
|
|
1
|
+
var W=Object.defineProperty;var o=(n,e)=>W(n,"name",{value:e,configurable:!0});import{envNodeId as P,envProfileId as U}from"../shared/env.js";import{existsSync as M,statSync as H}from"node:fs";import{resolve as Y}from"node:path";import{defineBranch as B,defineLeaf as h}from"../core/command.js";import{InputError as c,readStdinRaw as V}from"../core/io.js";import{parseWhen as z,parseCadence as Z,cadenceDisplay as k}from"../core/wake.js";import{profilesStateBlock as j,resolveProfileViaDaemon as J}from"./profile/daemon-profiles.js";import{cliClient as l,rethrowAsCliError as u}from"./api-client.js";async function L(n){return(await J(n.trim())).id}o(L,"canonicalProfileId");function b(n,e){return e!==void 0&&e.trim()!==""?{tz:e,now:n}:{now:n}}o(b,"parseOpts");function $(n){const e={wake_in_past:'Pick a future instant \u2014 a positive duration ("5m","2h") or an ISO time later than now.',bad_when:'Use a duration ("5m","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00").',bad_cadence:'Use a duration ("6h"), a 5-field cron ("0 9 * * *"), or an @alias ("@daily").',unknown_zone:'Pass --tz with an IANA zone name (e.g. "America/New_York").',cadence_too_fast:'Use a cadence of at least 60s (e.g. "1m","5m","1h").'};throw new c({error:n.code,message:n.message,received:n.received,next:e[n.code]??"Fix the time value and retry."})}o($,"throwTimeError");function G(n){const e=/^(\d+)(s|m|h)?$/.exec(n.trim());if(e===null)throw new c({error:"bad_run_timeout",message:`"${n}" is not a valid run timeout`,field:"run-timeout",next:'Use a duration like "90s", "5m", "1h", or bare seconds ("300").'});const r=Number(e[1]),t=e[2]==="h"?3600:e[2]==="m"?60:1,i=r*t;if(i<=0)throw new c({error:"bad_run_timeout",message:"run timeout must be positive",field:"run-timeout",next:'Use a positive duration, e.g. "5m".'});return i}o(G,"parseRunTimeout");function f(n){const e=n??U();return e!==void 0&&e.trim()!==""?{profile:e}:void 0}o(f,"scopeQuery");function Q(n){if(n===void 0)return process.cwd();const e=Y(n);if(!M(e)||!H(e).isDirectory())throw new c({error:"bad_cwd",message:`${e} is not a directory`,field:"cwd",next:"Pass --cwd with an existing directory, or omit it to inherit this one."});return e}o(Q,"resolveCwd");function X(n){const e=Array.isArray(n)?n.map(String):typeof n=="string"&&n!==""?[n]:[];if(e.length===0)return null;const r={};for(const t of e){const i=t.indexOf("="),a=i===-1?"":t.slice(0,i);if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(a))throw new c({error:"bad_env",message:`"${t}" is not KEY=VALUE`,field:"env",next:"Pass --env KEY=VALUE (repeat the flag per variable); KEY must be a shell-legal name."});r[a]=t.slice(i+1)}return JSON.stringify(r)}o(X,"resolveEnvJson");function p(n){const e=typeof n.cronId=="string"?n.cronId.trim():"";if(e==="")throw new c({error:"missing_cron_id",message:"a cron id is required",field:"cron-id",next:"Pass the cron id from `crtr cron list`."});return e}o(p,"requireCronId");function ee(n){if(n===null)return"never run";const e=n.finished??"",r=n.exit_code===0?"ok":n.exit_code===-1?"TIMEOUT":n.exit_code===null?"no exit":`exit ${n.exit_code}`;return e===""?r:`${r} ${e}`}o(ee,"healthDisplay");function ne(n){const e=n.exit_code===null?"no exit":n.exit_code===-1?"TIMEOUT":`exit ${n.exit_code}`,r=n.duration_ms!==null?`${n.duration_ms}ms`:"\u2014";return`- ${n.started} \u2014 ${e}, ${r} \u2014 ${n.delivered??"(no disposition recorded)"}`}o(ne,"renderRun");const te=h({name:"add",description:"arm a scheduled command (bash on stdin; one of --at / --every)",whenToUse:"you need to arm a new bash schedule, rather than inspect or control an existing cron.",help:{name:"cron add",summary:"arm a scheduled command",params:[{kind:"stdin",name:"command",required:!0,constraint:"Arbitrary bash the daemon runs at each fire. Pipe it from a single-quoted heredoc (`<<'EOF'`) to preserve literal bytes. Runs via `bash -c` in this cwd with $CRTR_CRON_ID, $CRTR_CRON_NAME, and $CRTR_RUN_ID in the env ($CRTR_ANCHOR and the anchor node's $CRTR_CONTEXT_DIR too when anchored; an unanchored cron has no context dir). Use absolute paths: `~` is the app's HOME, not the arming node's. The scheduler does not interpret it as a node action; a bash predicate can decide whether this fire acts, not arm a native predicate trigger. A pipeline exits with its last stage, so a failing probe piped into grep or jq exits 0 with empty stdout unless the command starts with `set -o pipefail`; a poll that must notice its probe failing needs it (a run that fails escalates, one that exits 0 quietly does not). A run that must branch on canvas state should call `crtr --json <cmd>` and read the leaf's declared outputs, rather than parsing the prose output."},{kind:"flag",name:"name",type:"string",required:!0,constraint:"Agent-legible label \u2014 how the cron shows in `cron list`."},{kind:"flag",name:"at",type:"string",required:!1,constraint:`One-shot: run the stored bash once at <when> \u2014 a duration ("90s","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", interpreted in --tz else host zone). Exactly one of --at / --every is required. The row is deleted after its run unless the run's disposition pauses it.`},{kind:"flag",name:"every",type:"string",required:!1,constraint:'Recurring: run the stored bash at each cadence \u2014 a fixed interval ("6h" \u2014 first fire one interval from now), a 5-field cron ("0 9 * * *"), or an @alias ("@daily"). Each fire is another bash run. Minimum cadence 60s. Exactly one of --at / --every is required. Use this for a bash condition that only the local filesystem knows: if it exits 75, the next cadence re-checks it. A host platform can also re-check it early by calling the daemon\'s `POST /v1/crons/poke` endpoint.'},{kind:"flag",name:"tz",type:"string",required:!1,constraint:'IANA zone (e.g. "America/New_York") for a bare-ISO --at or a calendar --every. Defaults to the host zone.'},{kind:"flag",name:"on-output",type:"enum",choices:["silent","on-failure","always","on-change"],required:!1,constraint:"What each run does with its output. Defaults to on-change for a node:<id> sink; otherwise on-failure. silent: record in the run log, tell nobody \u2014 never escalate. on-failure: no success delivery; a failing run pauses the cron and spawns a node to deal with it. always: deliver stdout every run to --sink at --tier. on-change: deliver stdout only when it differs from the previous run's \u2014 the shape polling wants."},{kind:"flag",name:"sink",type:"string",required:!1,constraint:`Where always/on-change stdout deliveries go: "node:<id>" (an existing node's inbox \u2014 must exist now; gone/finalized at fire time is a failure), "spawn:<kind>" (each delivered output creates a self-finishing node with stdout as kickoff \u2014 a managed child while its creator exists, otherwise a terminal parentless root), or "human" (the humanloop inbox). Required by always/on-change, rejected otherwise.`},{kind:"flag",name:"tier",type:"enum",choices:["normal","urgent","critical"],required:!1,default:"urgent",constraint:"Inbox priority of a node-sink delivery. urgent (default): steers the target mid-turn, or wakes it when dormant \u2014 a fire is news the node asked for on a schedule, so it should land now. normal: waits for the running turn to settle. critical: aborts the running turn outright."},{kind:"flag",name:"expires",type:"string",required:!1,constraint:"Clock bound (same grammar as --at): the row is deleted once this instant passes. The self-limiting half of a poll-until pattern. A node:<id> sink is mailed (waking) that the cron expired, with its run count and last delivery. For a held one-shot (last run exited 75), expiry bounds the wait for eligibility \u2014 the row is deleted UNFIRED, never fired blind at expiry."},{kind:"flag",name:"anchor",type:"string",required:!1,constraint:"Couple the cron's lifetime to a node: the row is deleted when that node is deleted. Without an anchor it outlives every node; it ends through its one-shot run, --expires, explicit cancellation, or self-cancellation from its bash."},{kind:"flag",name:"anchor-self",type:"bool",required:!1,constraint:"Anchor to the calling node ($CRTR_NODE_ID). Errors outside a node."},{kind:"flag",name:"cancel-on-wake",type:"bool",required:!1,constraint:"Also delete the row when the anchor node revives: use only when the cron belongs to that current dormancy, not for standing work. Requires an anchor; at most one cancel-on-wake cron per anchor \u2014 arming a second replaces the anchor\u2019s pending one."},{kind:"flag",name:"run-timeout",type:"string",required:!1,default:"5m",constraint:'Kill a run that exceeds this ("90s","5m","1h", or bare seconds). A timeout is a failure.'},{kind:"flag",name:"overlap",type:"enum",choices:["skip","queue","replace"],required:!1,default:"skip",constraint:"A fire landing while the previous run is still going. skip (default): log the skip, do not start a second run. queue: run once more after the current finishes (depth 1; further fires collapse). replace: kill the running instance and start fresh."},{kind:"flag",name:"cwd",type:"path",required:!1,constraint:"Directory every run executes in. Defaults to this one, must exist now, and is snapshotted onto the row \u2014 a cron never re-resolves its context. In a hosted runtime, runs are sandboxed and see only the app's own folders: an omitted --cwd that is outside them becomes the node's directory or the app files directory, and an explicit --cwd outside them is refused now (`cron show` prints the directory the row got)."},{kind:"flag",name:"env",type:"string",required:!1,repeatable:!0,constraint:"KEY=VALUE added to every run's env (repeat per variable). Snapshotted at arm time; overlays the operational env, alongside $CRTR_CRON_ID/$CRTR_CRON_NAME/$CRTR_RUN_ID."},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile the runs resolve config and credentials under, and \u2014 under --scope profile \u2014 the profile this cron lists and cancels within. A profile id or manifest name from the <profiles> list below; a close-enough name resolves to the nearest profile. Defaults to the calling node's ($CRTR_PROFILE_ID)."},{kind:"flag",name:"scope",type:"enum",choices:["profile","global"],required:!1,constraint:"Who lists and cancels this cron: profile (default when a profile is resolved) confines it to that profile; global makes it canvas-home-wide, visible from every profile."}],output:[{name:"cron_id",type:"string",required:!0,constraint:"The server-minted armed cron id \u2014 inspect with `cron show <cron_id>`, cancel with `cron cancel <cron_id>`."},{name:"fire_at",type:"string",required:!0,constraint:"Next fire (UTC ISO)."},{name:"recur",type:"string",required:!0,constraint:'Cadence display; "none" for a one-shot.'}],outputKind:"object",effects:["Arms one cron row (daemon-run; survives this process and every node).","Each fire runs the command via bash in this cwd and records a run-log entry.","A failing run (nonzero exit, timeout, undeliverable sink) pauses the cron and spawns a node to deal with it \u2014 unless --on-output silent."]},helpState:{leaf:j},run:o(async n=>{let e=typeof n.command=="string"?n.command.trim():"";if(e===""&&(e=(await V()).trim()),e==="")throw new c({error:"missing_command",message:"no command on stdin",field:"command",next:"Pipe the bash command on stdin (e.g. from a single-quoted heredoc)."});const r=typeof n.name=="string"?n.name.trim():"";if(r==="")throw new c({error:"missing_name",message:"--name is required",field:"name",next:"Pass --name <label>."});const t=typeof n.at=="string"&&n.at!==""?n.at:void 0,i=typeof n.every=="string"&&n.every!==""?n.every:void 0;if(t===void 0==(i===void 0))throw new c({error:"bad_schedule",message:t===void 0?"one of --at / --every is required":"--at and --every are mutually exclusive",next:"Pass exactly one: --at <when> for a one-shot, or --every <cadence> for a recurrence."});const a=typeof n.tz=="string"&&n.tz!==""?n.tz:void 0,m=new Date;let d,T=null;if(i!==void 0){const s=Z(i,b(m,a));"error"in s&&$(s.error),d=s.firstFireAt,T=s.recur}else{const s=z(t,b(m,a));"error"in s&&$(s.error),d=s.fireAt}let R=null;const C=typeof n.expires=="string"&&n.expires!==""?n.expires:void 0;if(C!==void 0){const s=z(C,b(m,a));"error"in s&&$(s.error),R=s.fireAt}const q=typeof n.anchor=="string"&&n.anchor!==""?n.anchor:void 0,A=n.anchorSelf===!0;if(q!==void 0&&A)throw new c({error:"bad_anchor",message:"--anchor and --anchor-self are mutually exclusive",next:"Pass one anchor or the other."});let w=q??null;if(A){const s=P();if(s===void 0||s==="")throw new c({error:"no_self_node",message:"--anchor-self requires a calling node ($CRTR_NODE_ID is unset)",next:"Run from inside a node, or pass --anchor <node-id> explicitly."});w=s}const I=n.cancelOnWake===!0;if(I&&w===null)throw new c({error:"cancel_on_wake_needs_anchor",message:"--cancel-on-wake requires an anchor",next:"Add --anchor <node-id> or --anchor-self."});const O=typeof n.runTimeout=="string"&&n.runTimeout!==""?G(n.runTimeout):void 0,E=typeof n.cwd=="string"&&n.cwd!==""?n.cwd:void 0,g=P(),S=g?await l().getNode(g).catch(u):null,K=Q(E??(S?.isolated?S.cwd:void 0)),F=X(n.env),D=typeof n.profile=="string"&&n.profile!==""?n.profile:void 0,y=U(),_=D!==void 0?await L(D):y!==void 0&&y.trim()!==""?y:void 0,N=n.scope??(_!==void 0?"profile":"global");if(N==="profile"&&_===void 0)throw new c({error:"scope_needs_profile",message:"--scope profile names the profile this cron lists and cancels within, but no profile is resolved",field:"profile",next:"Pass --profile <id>, or use --scope global for a canvas-home-wide cron."});const v=g,x=await l().armCron({name:r,command:e,fire_at:d,recur:T,tz:a??null,created_by:v!==void 0&&v!==""?v:null,cwd:K,...E===void 0?{cwd_is_default:!0}:{},env_json:F,profile:_??null,scope:N,on_output:n.onOutput,sink:typeof n.sink=="string"&&n.sink!==""?n.sink:null,tier:n.tier??"urgent",expires_at:R,anchor_node:w,cancel_on_wake:I,...O!==void 0?{run_timeout_s:O}:{},overlap:n.overlap??"skip"}).catch(u);return{cron_id:x.cron_id,fire_at:x.fire_at,recur:k(x.recur)}},"run"),render:o(n=>{const e=String(n.recur),r=e==="none"?"one-shot (row deleted after the run)":e;return`Cron armed \u2014 ${String(n.cron_id)}.
|
|
2
2
|
|
|
3
3
|
- next fire: ${String(n.fire_at)}
|
|
4
4
|
- cadence: ${r}
|
|
5
5
|
|
|
6
|
-
It runs daemon-side; nothing more to do here. Inspect it with \`crtr cron show ${String(n.cron_id)}\`; cancel with \`crtr cron cancel ${String(n.cron_id)}\` from its profile, or pass \`--profile <profile>\` from another profile.`},"render")}),re=h({name:"list",description:"what is scheduled, with next fire and recent health",whenToUse:"you want to see every cron in scope \u2014 to find an id to show/cancel, to check what fires next and what is paused, or to spot one whose last run failed.",help:{name:"cron list",summary:"every cron in scope, next-fire order",params:[],output:[{name:"crons",type:"object[]",required:!0,constraint:"One row per cron: cron_id, name, fire_at (next fire, UTC), recur (cadence display), state (active|paused), held (true after an exit-75 gate; a recurring row re-checks at fire_at or when a host platform calls `POST /v1/crons/poke`, while a one-shot needs that call or `crtr cron run <cron-id>`), on_output, sink, expires_at, last_run (the most recent settled run: finished, exit_code, delivered \u2014 null if it never ran)."}],outputKind:"object",effects:["None. Read-only."]},run:o(async()=>({crons:(await l().listCrons(f()).catch(u)).map(e=>({cron_id:e.cron_id,name:e.name,fire_at:e.fire_at,recur:k(e.recur),state:e.state,held:e.held,on_output:e.on_output,sink:e.sink,expires_at:e.expires_at,last_run:e.last_run}))}),"run"),render:o(n=>{const e=n.crons;if(e.length===0)return"No crons armed.";const r=e.map(t=>{const i=[`next ${t.fire_at}`,t.recur==="none"?"one-shot":t.recur,t.sink!==null?`${t.on_output} \u2192 ${t.sink}`:t.on_output,`last: ${ee(t.last_run)}`];t.expires_at!==null&&i.push(`expires ${t.expires_at}`);const
|
|
6
|
+
It runs daemon-side; nothing more to do here. Inspect it with \`crtr cron show ${String(n.cron_id)}\`; cancel with \`crtr cron cancel ${String(n.cron_id)}\` from its profile, or pass \`--profile <profile>\` from another profile.`},"render")}),re=h({name:"list",description:"what is scheduled, with next fire and recent health",whenToUse:"you want to see every cron in scope \u2014 to find an id to show/cancel, to check what fires next and what is paused, or to spot one whose last run failed.",help:{name:"cron list",summary:"every cron in scope, next-fire order",params:[],output:[{name:"crons",type:"object[]",required:!0,constraint:"One row per cron: cron_id, name, fire_at (next fire, UTC), recur (cadence display), state (active|paused), held (true after an exit-75 gate; a recurring row re-checks at fire_at or when a host platform calls `POST /v1/crons/poke`, while a one-shot needs that call or `crtr cron run <cron-id>`), on_output, sink, expires_at, last_run (the most recent settled run: finished, exit_code, delivered \u2014 null if it never ran)."}],outputKind:"object",effects:["None. Read-only."]},run:o(async()=>({crons:(await l().listCrons(f()).catch(u)).map(e=>({cron_id:e.cron_id,name:e.name,fire_at:e.fire_at,recur:k(e.recur),state:e.state,held:e.held,on_output:e.on_output,sink:e.sink,expires_at:e.expires_at,last_run:e.last_run}))}),"run"),render:o(n=>{const e=n.crons;if(e.length===0)return"No crons armed.";const r=e.map(t=>{const i=[`next ${t.fire_at}`,t.recur==="none"?"one-shot":t.recur,t.sink!==null?`${t.on_output} \u2192 ${t.sink}`:t.on_output,`last: ${ee(t.last_run)}`];t.expires_at!==null&&i.push(`expires ${t.expires_at}`);const a=t.state==="paused"?t.held?" [PAUSED (held) \u2014 resume or cancel]":" [PAUSED \u2014 resume or cancel]":t.held?t.recur==="none"?` [HELD \u2014 last run exited 75; run manually with \`crtr cron run ${t.cron_id}\`, or wait for a host platform to call \`POST /v1/crons/poke\`]`:" [HELD \u2014 last run exited 75; re-checks at the next cadence or when a host platform calls `POST /v1/crons/poke`]":"";return`- ${t.name} (${t.cron_id}) \u2014 ${i.join(", ")}${a}`});return`${e.length} cron(s):
|
|
7
7
|
|
|
8
8
|
${r.join(`
|
|
9
|
-
`)}`},"render")}),oe=h({name:"show",description:"one cron: full config plus its run history",whenToUse:'you want to inspect one cron \u2014 its exact command and settings, and what its recent runs actually did (exit codes, output heads, where deliveries went). The run log is how you answer "did it silently stop working".',help:{name:"cron show",summary:"one cron with its run-log ring",params:[{kind:"positional",name:"cron-id",required:!0,constraint:"The cron to inspect (from `cron list`)."}],output:[{name:"cron",type:"object",required:!0,constraint:"The full cron config: command, schedule, snapshotted context (cwd, profile, scope, env var names \u2014 values stay server-side), disposition, sink, coupling, state."},{name:"runs",type:"object[]",required:!0,constraint:"The bounded run log, most recent first: started, exit_code (-1 = timeout kill), duration_ms, stdout/stderr heads, delivered (what the sink did, or why it did not)."}],outputKind:"object",effects:["None. Read-only."]},run:o(async n=>{const e=p(n),r=await l().showCron(e,f()).catch(t=>u(t,"Check the id against `crtr cron list`."));return{cron:r.cron,runs:r.runs}},"run"),render:o(n=>{const e=n.cron,r=n.runs,t=e.recur!==null?k(e.recur):e.held?"one-shot":`one-shot at ${e.fire_at}`,i=e.tz!==null?` (tz ${e.tz})`:"",
|
|
10
|
-
`)},"render")}),ie=h({name:"run",description:"execute now, out of band \u2014 does not advance the schedule",whenToUse:"you need the stored command to execute now because its own behavior has not been exercised, or you fixed the command behind a paused cron. This is not a scheduled-delivery check: once `cron add` has armed a valid row, trust the clock and node delivery rather than firing it again for reassurance.",help:{name:"cron run",summary:"fire one cron now, out of band, and report the settled run",params:[{kind:"positional",name:"cron-id",required:!0,constraint:"The cron to run (from `cron list`)."}],output:[{name:"exit_code",type:"number",required:!1,constraint:"The run's exit code (-1 = timeout kill; null when the process failed to start)."},{name:"duration_ms",type:"number",required:!0,constraint:"Wall-clock run time."},{name:"stdout_head",type:"string",required:!1,constraint:"Captured stdout (head)."},{name:"stderr_head",type:"string",required:!1,constraint:"Captured stderr (head)."},{name:"delivered",type:"string",required:!0,constraint:"What the sink did with the output, or why it did not."}],outputKind:"object",effects:["Runs the command NOW (blocking this call until it settles) and records the run in the cron's run log.","Delivers per the cron's disposition; never advances the schedule, consumes a one-shot, or escalates a failure."]},run:o(async n=>{const e=p(n),r=await l().showCron(e,f()).catch(
|
|
11
|
-
`)},"render")}),
|
|
9
|
+
`)}`},"render")}),oe=h({name:"show",description:"one cron: full config plus its run history",whenToUse:'you want to inspect one cron \u2014 its exact command and settings, and what its recent runs actually did (exit codes, output heads, where deliveries went). The run log is how you answer "did it silently stop working".',help:{name:"cron show",summary:"one cron with its run-log ring",params:[{kind:"positional",name:"cron-id",required:!0,constraint:"The cron to inspect (from `cron list`)."}],output:[{name:"cron",type:"object",required:!0,constraint:"The full cron config: command, schedule, snapshotted context (cwd, profile, scope, env var names \u2014 values stay server-side), disposition, sink, coupling, state."},{name:"runs",type:"object[]",required:!0,constraint:"The bounded run log, most recent first: started, exit_code (-1 = timeout kill), duration_ms, stdout/stderr heads, delivered (what the sink did, or why it did not)."}],outputKind:"object",effects:["None. Read-only."]},run:o(async n=>{const e=p(n),r=await l().showCron(e,f()).catch(t=>u(t,"Check the id against `crtr cron list`."));return{cron:r.cron,runs:r.runs}},"run"),render:o(n=>{const e=n.cron,r=n.runs,t=e.recur!==null?k(e.recur):e.held?"one-shot":`one-shot at ${e.fire_at}`,i=e.tz!==null?` (tz ${e.tz})`:"",a=e.held?e.recur!==null?`- schedule: ${t}${i} \u2014 next re-check ${e.fire_at} (held \u2014 also re-checks when a host platform calls \`POST /v1/crons/poke\`; last run exited 75)`:e.expires_at!==null?`- schedule: ${t}${i} \u2014 no scheduled re-check \u2014 expires ${e.expires_at} (held \u2014 run manually with \`crtr cron run ${e.cron_id}\`, or wait for a host platform to call \`POST /v1/crons/poke\`; deleted unfired at expiry)`:`- schedule: ${t}${i} \u2014 no scheduled re-check (held \u2014 run manually with \`crtr cron run ${e.cron_id}\`, or wait for a host platform to call \`POST /v1/crons/poke\`)`:`- schedule: ${t}${i} \u2014 next fire ${e.fire_at}`,m=e.state==="paused"?e.held?" \u2014 PAUSED (held)":" \u2014 PAUSED":e.held?" \u2014 HELD":"",d=[`# ${e.name} (${e.cron_id})${m}`,"",a,`- disposition: ${e.on_output}${e.sink!==null?` \u2192 ${e.sink} (tier ${e.tier})`:""}`,`- context: cwd ${e.cwd}${e.profile!==null?`, profile ${e.profile}`:""}, scope ${e.scope}${e.env_keys.length>0?`, env ${e.env_keys.join(", ")}`:""}`,`- run: timeout ${e.run_timeout_s}s, overlap ${e.overlap}${e.run_state==="running"?" \u2014 a run is IN FLIGHT now":""}`];return e.expires_at!==null&&d.push(`- expires: ${e.expires_at}`),e.anchor_node!==null&&d.push(`- anchored to node ${e.anchor_node}${e.cancel_on_wake?" (canceled when it wakes)":""}`),e.created_by!==null&&d.push(`- created by node ${e.created_by}`),d.push("","## Command","","```bash",e.command,"```","",`## Runs (most recent first, last ${r.length})`,""),d.push(...r.length>0?r.map(ne):["(no runs yet)"]),d.join(`
|
|
10
|
+
`)},"render")}),ie=h({name:"run",description:"execute now, out of band \u2014 does not advance the schedule",whenToUse:"you need the stored command to execute now because its own behavior has not been exercised, or you fixed the command behind a paused cron. This is not a scheduled-delivery check: once `cron add` has armed a valid row, trust the clock and node delivery rather than firing it again for reassurance.",help:{name:"cron run",summary:"fire one cron now, out of band, and report the settled run",params:[{kind:"positional",name:"cron-id",required:!0,constraint:"The cron to run (from `cron list`)."}],output:[{name:"exit_code",type:"number",required:!1,constraint:"The run's exit code (-1 = timeout kill; null when the process failed to start)."},{name:"duration_ms",type:"number",required:!0,constraint:"Wall-clock run time."},{name:"stdout_head",type:"string",required:!1,constraint:"Captured stdout (head)."},{name:"stderr_head",type:"string",required:!1,constraint:"Captured stderr (head)."},{name:"delivered",type:"string",required:!0,constraint:"What the sink did with the output, or why it did not."}],outputKind:"object",effects:["Runs the command NOW (blocking this call until it settles) and records the run in the cron's run log.","Delivers per the cron's disposition; never advances the schedule, consumes a one-shot, or escalates a failure."]},run:o(async n=>{const e=p(n),r=await l().showCron(e,f()).catch(a=>u(a,"Check the id against `crtr cron list`."));return await l({timeoutMs:r.cron.run_timeout_s*1e3+15e3}).runCron(e,f()).catch(a=>u(a))},"run"),render:o(n=>{const e=n,t=[`Run settled \u2014 exit ${e.exit_code===null?"no exit (process error)":e.exit_code===-1?"-1 (TIMEOUT)":String(e.exit_code)}, ${e.duration_ms??"?"}ms.`,"",`- delivered: ${e.delivered??"(nothing recorded)"}`];return e.stdout_head!==null&&t.push("","## stdout (head)","",e.stdout_head),e.stderr_head!==null&&t.push("","## stderr (head)","",e.stderr_head),t.join(`
|
|
11
|
+
`)},"render")}),se=h({name:"pause",description:"stop firing without losing config or history",whenToUse:"a cron should stop firing for now but its config and run history are worth keeping \u2014 a job under investigation, or a poll to silence temporarily. (A failing cron pauses itself.)",help:{name:"cron pause",summary:"pause one cron",params:[{kind:"positional",name:"cron-id",required:!0,constraint:"The cron to pause (from `cron list`)."}],output:[],outputKind:"object",effects:["Stops the cron firing until `cron resume`. An in-flight run settles normally; the row just stops being due."]},run:o(async n=>{const e=p(n);return await l().pauseCron(e,f()).catch(r=>u(r,"Check the id against `crtr cron list`.")),{}},"run"),render:o(()=>"Cron paused.","render")}),ae=h({name:"resume",description:"re-arm a paused cron",whenToUse:"a paused cron should fire again \u2014 you fixed what made it fail (verify first with `cron run`), or its silence is over. A fire that came due while paused fires immediately on resume.",help:{name:"cron resume",summary:"resume one paused cron",params:[{kind:"positional",name:"cron-id",required:!0,constraint:"The cron to resume (from `cron list`)."}],output:[{name:"fire_at",type:"string",required:!0,constraint:"Next fire \u2014 if already past, it fires on the next daemon tick."}],outputKind:"object",effects:["Re-arms the cron. A past-due fire_at fires immediately (instant verification after un-wedging); a recurrence then advances naturally."]},run:o(async n=>{const e=p(n);return{fire_at:(await l().resumeCron(e,f()).catch(t=>u(t,"Check the id against `crtr cron list`."))).fire_at}},"run"),render:o(n=>`Cron resumed \u2014 next fire ${String(n.fire_at)} (if that is already past, it fires on the next tick).`,"render")}),ce=h({name:"cancel",description:"remove one cron (idempotent)",whenToUse:"a scheduled command should stop firing \u2014 it is obsolete, wrong, or its purpose is served. Idempotent, so a script can safely self-cancel with `crtr cron cancel $CRTR_CRON_ID` even if a racing cancel got there first.",help:{name:"cron cancel",summary:"remove one cron",params:[{kind:"positional",name:"cron-id",required:!0,constraint:"The cron to remove (from `cron list`, or $CRTR_CRON_ID inside a run)."},{kind:"flag",name:"profile",type:"string",required:!1,constraint:"Profile scope to cancel within. Defaults to the calling node's profile; pass the profile id or manifest name for a cron armed under another profile."}],output:[],outputKind:"object",effects:["Deletes the cron row and its run history; kills any in-flight run. A no-op if already gone."]},helpState:{leaf:j},run:o(async n=>{const e=p(n),r=process.env.CRTR_RUN_ID;return await l().cancelCron(e,{...f(typeof n.profile=="string"&&n.profile.trim()!==""?await L(n.profile):void 0),...r===void 0||r===""?{}:{run_id:r}}).catch(t=>u(t,"Check the id against `crtr cron list`.")),{}},"run"),render:o(()=>"Cron canceled.","render")});function _e(){return B({name:"cron",rootEntry:{concept:"one scheduling primitive \u2014 run this bash command, then",desc:"arm, inspect, and control scheduled bash commands the daemon runs on the clock",useWhen:"bash must run later or on a cadence, rather than waiting for a runtime-delivered wake."},help:{name:"cron",summary:"scheduled bash commands, daemon-run",model:"A cron stores and daemon-runs arbitrary bash at its clock. Use ordinary bash for ordinary work; choose node:<id> for stdout as inbox information to an existing node (its disposition defaults to on-change), an existing node's lifecycle fresh-revive action in the bash for a clean re-check without inbox output, or spawn:<kind> to route each output-producing fire to a fresh self-finishing node (a managed child while its creator exists, otherwise a terminal parentless root). Per-fire fresh agent work belongs on the existing spawn:<kind> sink, not an independent resident-root birth. A gate is bash, at the top of the command. Exit 0 after deciding not to act and the occurrence is simply spent \u2014 right when the recurrence is your polling cadence. Exit 75 holds the row. A held recurring row re-checks at its next natural slot, or when a host platform calls the daemon's `POST /v1/crons/poke` endpoint. A held one-shot has no scheduled re-check: it stays held until that endpoint is called, or you execute its bash explicitly with `crtr cron run <cron-id>` (which does not unhold or reschedule the row). A local filesystem condition does not call the daemon, so use --every for that condition or run the cron explicitly after it changes. Any other nonzero exit is a real failure and escalates per --on-output. Keep gate errors nonzero so a broken probe is loud instead of silently held. Beyond the exit-code contract, cron has no native predicate-trigger semantics. --at runs once and deletes the row unless the run's disposition pauses it; --every runs each cadence. Output disposition (--on-output) records silently, escalates failures, or routes stdout to its --sink. Anchor a cron to delete it with a node; --cancel-on-wake instead deletes it when the anchor wakes. An unanchored recurring cron ends through --expires, explicit cancellation, or self-cancellation from its bash. Every run lands in a bounded per-cron run log (`cron show`); cwd, env, and profile are snapshotted at arm time, and --scope controls who lists and cancels it."},children:[te,re,oe,ie,se,ae,ce]})}o(_e,"registerCron");export{_e as registerCron};
|