@north-light/cli 0.3.488 → 0.3.490
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/internal/memory-loading.md +1 -1
- package/dist/clients/attach/viewer.js +1 -1
- package/dist/commands/doc/shared.js +2 -2
- package/dist/commands/profile/project.js +1 -1
- package/dist/core/graph/repo-sync/sync.d.ts +4 -3
- package/dist/core/graph/repo-sync/sync.js +2 -2
- package/dist/core/graph/spaces/repo.d.ts +7 -7
- package/dist/core/graph/spaces/repo.js +1 -1
- package/dist/core/graph/types.d.ts +3 -2
- package/dist/core/render.js +1 -2
- package/dist/daemon/api/handlers/docs.js +1 -1
- package/dist/launcher/accepted-socket.d.ts +11 -0
- package/dist/launcher/accepted-socket.js +1 -0
- package/dist/launcher/main.js +3 -3
- package/dist/launcher/validate.js +1 -1
- package/docs/release-local.md +1 -1
- package/package.json +3 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -49,7 +49,7 @@ Every delivery registers the document or listing and its level in the loaded con
|
|
|
49
49
|
|
|
50
50
|
## The reader's view
|
|
51
51
|
|
|
52
|
-
A rule applies to every node whose view includes the document's owner. A node's view is: its own node, the repos it works in (those enclosing its cwd, nearest first), the repos enclosing each project path of its profile (in manifest order; a path that is gone or is not in a repo adds nothing, and a Linux node sandbox sees none), its profile, its app, the user, and installed plugins (each an app that owns its shipped documents), with crtr's own documents (owned by the app `crtr`) last. A bare name resolves in that same order, which is what lets a repo's document shadow one of crtr's own. A rule's `if: {repo}` condition holds when that repo is among the node's eligible repos (cwd repos plus project repos), so a rule on any document, including a user- or profile-owned one, can target a repo the profile covers; match `cwd` to condition on where the node works. `--owner repo`
|
|
52
|
+
A rule applies to every node whose view includes the document's owner. A node's view is: its own node, the repos it works in (those enclosing its cwd, nearest first), the repos enclosing each project path of its profile (in manifest order; a path that is gone or is not in a repo adds nothing, and a Linux node sandbox sees none), its profile, its app, the user, and installed plugins (each an app that owns its shipped documents), with crtr's own documents (owned by the app `crtr`) last. A bare name resolves in that same order, which is what lets a repo's document shadow one of crtr's own. A rule's `if: {repo}` condition holds when that repo is among the node's eligible repos (cwd repos plus project repos), so a rule on any document, including a user- or profile-owned one, can target a repo the profile covers; match `cwd` to condition on where the node works. `--owner repo` follows only the repos enclosing the cwd. A node may read a repo's documents when its file permissions reach a checkout of the repo (enclosing its cwd or one of its profile's project paths), and change them when its file permissions allow writing that checkout. A repo-relative `./` `file-read` glob stays anchored to the checkout of the repo that owns the rule. A node's own documents are in no other node's view, so their rules apply only to that node.
|
|
53
53
|
|
|
54
54
|
Each owner the selected profile manages carries a delivery limit — `none`, `name`, `preview`, or `content` — and that value is the maximum level anything that owner holds delivers at `boot` and `workspace-open`, whatever directory the node is working in and whether the repo is in view through its cwd or the profile's project list. `crtr profile project add` starts the added repos at `preview`. It only lowers: a rule authored below the maximum delivers at its authored level. `none` contributes nothing to either automatic event, so a `none` owner cannot shadow a same-named document from a wider owner — that wider document becomes the winner. An owner with no limit set delivers exactly what it authored.
|
|
55
55
|
|
|
@@ -1732,7 +1732,7 @@ ${u}`,"1").then(l=>{let d=a.options[Number.parseInt(l,10)-1]?.id;if(d===void 0)t
|
|
|
1732
1732
|
WHERE delivery_id IN (${ogi})`});var kD,U6=h(()=>{"use strict";_o();w0();kD=["crtr:llm","crtr:act","crtr:schedule","crtr:memory:rw:user","crtr:memory:manage:user","crtr:files:rw:user:."]});var oge=h(()=>{"use strict";ld();Vo()});function gvn(t){if(!Array.isArray(t))throw new TypeError("grant scopes must be a list");return t.map(e=>{if(!e||typeof e!="object"||typeof e.scope!="string"||!["always","ask"].includes(e.mode))throw new TypeError("invalid grant scope entry");return{scope:e.scope,mode:e.mode}})}function agi(t){if(!t||typeof t!="object"||Array.isArray(t))return null;let{state:e,message:n,next:r,step:i}=t;return typeof e!="string"||!sgi.includes(e)||n!==void 0&&typeof n!="string"||i!==void 0&&typeof i!="string"||typeof r!="string"||!r?null:{state:e,...n===void 0?{}:{message:n},next:r,...i===void 0?{}:{step:i}}}function cgi(t){if(!t||typeof t!="object"||Array.isArray(t))return{};let e={};for(let[n,r]of Object.entries(t)){let i=agi(r);i&&(e[n]=i)}return e}function ugi(t){return{id:t.id,runtime:t.runtime,sub:t.sub,grantee:t.grantee,rev:t.rev,scopes:gvn(JSON.parse(t.scopes_json)),resolved_scopes:gvn(JSON.parse(t.resolved_scopes_json)),spend_cap:t.spend_cap,expires_at:t.expires_at,status:t.status,revoked_at:t.revoked_at,approved_at:t.approved_at,payer:t.payer,dependencies:cgi(JSON.parse(t.dependencies_json)),run_notice_url:t.run_notice_url??null,synced_at:t.synced_at}}function yvn(t){if(!t||t.status!=="active")return null;try{return dgi(ugi(t))}catch(e){if(e instanceof TypeError||e instanceof SyntaxError)return null;throw e}}function lgi(t){return t.expires_at!==null&&!(Date.parse(t.expires_at)>Date.now())}function dgi(t){if(t.status!=="active"||lgi(t))return null;try{let e=t.resolved_scopes.map(({scope:n})=>KU(n));return{...t,resolved:t.resolved_scopes.flatMap(({mode:n},r)=>n==="always"?[e[r]]:[])}}catch(e){if(e instanceof TypeError||e instanceof SyntaxError)return null;throw e}}function Q6(t){return yvn(ae(ir(),"SELECT * FROM grants WHERE id = ?").get(t))}function sge(t,e){return yvn(ae(ir(),"SELECT * FROM grants WHERE grantee = ? AND sub = ?").get(t,e))}var sgi,lU=h(()=>{"use strict";Ft();B0();di();oge();sgi=["not_started","pending","ready","failed","unavailable"]});var Avn=h(()=>{"use strict";tb()});var wvn=h(()=>{"use strict"});var xvn=h(()=>{"use strict";qs();tb()});var gXe=h(()=>{"use strict"});var bvn=h(()=>{"use strict";qs();tb();gXe();Nf()});var tb=h(()=>{"use strict";Vi();Avn();wvn();xvn();bvn();gXe()});var AXe=h(()=>{"use strict"});function age(t,e,n){if(n.length===0)return;let r=ae(t,"INSERT INTO memory_reads (node_id, name, scope, at, type) VALUES (?, ?, ?, ?, ?)");for(let i of n)r.run(e,i.name,i.scope,i.at,i.type)}var I7s,wXe=h(()=>{"use strict";Ft();di();I7s=720*60*60*1e3});function nb(t,e=ir()){let n=ae(e,`SELECT n.run_id, n.isolated, r.isolated AS root_isolated FROM nodes n
|
|
1733
1733
|
LEFT JOIN nodes r ON r.node_id = n.run_id WHERE n.node_id = ?`).get(t);if(!n){let r=ae(e,"SELECT run_id FROM deleted_isolated_nodes WHERE node_id = ?").get(t);return r?{run:r.run_id,isolated:!0}:null}return{run:n.run_id??t,isolated:n.isolated===1||n.root_isolated===1}}function cge(t,e=ir()){let n=ae(e,"SELECT registered_by FROM custom_objects WHERE object = ?").get(t);return n?.registered_by?nb(n.registered_by,e)??{run:n.registered_by,isolated:!1}:null}function uge(t,e){return t.run===e.run||!t.isolated&&!e.isolated}var W6=h(()=>{"use strict";di()});function lge(t,e,n,r,i,o){if(e.class==="person"||r.length===0||i!==null&&i.type!=="document")return null;let s=e.launched;if(s==null||o==="read"&&i?.kind==="report"&&r[0].type==="node"&&mgi(t,e,r[0].id))return null;let a=dge(t,_vn(t,n),o,r);return s.some(c=>la(c,a))?null:a}function mgi(t,e,n){let r=e.class==="node"?nb(e.node.id,t):e.class==="person"?null:e.place??null,i=nb(n,t);return r!==null&&i!==null&&r.run===i.run}function dge(t,e,n,r){let i=r[0];return i.type!=="profile"?`crtr:memory:${n}:${e}`:`crtr:memory:${n}:${e}:profile:${_vn(t,i)}`}var _vn,pge=h(()=>{"use strict";Ft();B0();W6();_vn=(t,e)=>ae(t,"SELECT external_id FROM identities WHERE object = ?").get(e.rowid).external_id});function Svn(t,e,n){if(e.class==="person"||n.length===0||n[0].type!=="node"||e.class==="app"&&e.place===void 0)return!0;let r=vvn.get(e);if(r===void 0&&(r=e.class==="app"?e.place:e.class==="node"?nb(e.node.id,t):e.place??cge(e.object.rowid,t),vvn.set(e,r)),r===null)return!0;let i=nb(n[0].id,t);return i===null?!r.isolated:uge(r,i)}var Cvn,vvn,Ivn=h(()=>{"use strict";B0();H6();pge();W6();Cvn={identity:gf,sync:"runtime",canRead(t,e,n,r,i){if(e.class==="person"||r.length===0)return!0;if(e.app.rowid===n.rowid)return Svn(t,e,r)&&lge(t,e,n,r,i,"read")===null;let o=gf(t,n).external_id,s=a=>e.scopes!==null&&e.scopes.some(c=>la(c,a));return r[0].type==="node"?s(`crtr:objects:read:${o}`)||i.type==="document"&&i.kind==="report"&&s(`crtr:conversations:read:${o}`):s(dge(t,o,"read",r))},canChange(t,e,n,r,i){if(e.class==="person")return!0;if(e.app.rowid===n.rowid)return r.length===0||Svn(t,e,r)&&lge(t,e,n,r,i,"rw")===null;if(r.length===0||r[0].type==="node")return!1;let o=dge(t,gf(t,n).external_id,"rw",r);return e.scopes!==null&&e.scopes.some(s=>la(s,o))}},vvn=new WeakMap});var Tvn,kvn=h(()=>{"use strict";Ft();B0();H6();Tvn={identity:gf,sync:"runtime",canRead(t,e,n,r,i){return e.class==="person"||e.class==="object"&&e.owner.rowid===n.rowid?!0:e.scopes===null?!1:e.app.rowid===n.rowid||r.length===0?!0:r[0].type!=="document"?!1:e.scopes.some(o=>la(o,"crtr:memory:manage:user"))||i.privacy==="public"?!0:i.privacy==="private"&&e.scopes.some(o=>la(o,"crtr:memory:read_private:user"))},canChange(t,e,n,r,i){if(e.class==="person"||e.class==="object"&&e.owner.rowid===n.rowid||e.app.rowid===n.rowid||e.scopes?.some(c=>la(c,"crtr:memory:manage:user")))return!0;if(r.length===0)return e.scopes?.some(c=>la(c,"crtr:memory:rw:user"))??!1;if(r[0].type!=="document")return!1;let o=`crtr:memory:rw:user:${i.name}`;if(e.scopes===null||!e.scopes.some(c=>la(c,o)))return!1;let s=ae(t,`SELECT author.type, author.owner AS app FROM revisions r JOIN objects author ON author.rowid = r.author
|
|
1734
1734
|
WHERE r.document = ? ORDER BY r.number, r.created_at LIMIT 1`).get(r[0].rowid);return s?s.type==="person"||s.app===e.app.rowid?!0:ae(t,`SELECT g.resolved_scopes_json FROM grants g JOIN identities i ON i.external_id = g.grantee
|
|
1735
|
-
WHERE i.object = ? AND i.scheme = 'app'`).all(s.app).some(({resolved_scopes_json:c})=>JSON.parse(c).some(({scope:u,mode:l})=>l==="always"&&la(u,"crtr:memory:manage:user"))):!1}}});import{join as xXe,relative as fge,resolve as hgi,sep as mge}from"node:path";function dU(t,e){let n=fge(t,e);return n===""||n!==".."&&!n.startsWith(`..${mge}`)&&!n.startsWith(mge)}function bXe(t,e,n){let r=hgi(e);if(t===null||process.platform!=="linux"&&t.app==="app:terminal"){if(process.platform!=="linux")return{root:"/"};let p=[T9(),Th()].find(f=>dU(f,r));return p?{root:p}:{missing:null}}let i=Ga(t.app);if(t.run&&dU(i.root,r)){let[p,f]=fge(i.root,r).split(mge);if(p==="nodes"&&f)return t.run.reaches(f)?{root:xXe(i.nodes,f)}:{hidden:!0};if(t.run.isolated)return{hidden:!0}}for(let p of[i.files,i.nodes])if(dU(p,r))return{root:p};let o=p=>t.resolved.some(f=>la(f,p))&&(t.launched===null||t.launched.some(f=>la(f,p))),s=(p,f)=>{let m=fge(f,r)||".",g=x=>x.flatMap(E=>{let _=/^crtr:files:(read|rw):(user|app:[A-Za-z0-9_-]{1,64}):(.+)$/.exec(E);return!_||_[2]!==p||n==="rw"&&_[1]!=="rw"?[]:_[3]==="."||_[3]===m||m.startsWith(`${_[3]}/`)?[xXe(f,_[3]==="."?"":_[3])]:[]}),y=g(t.resolved),w=t.launched===null?y:g(t.launched);return!y.length||!w.length?{missing:`crtr:files:${n}:${p}:${m}`}:{root:[...y,...w].reduce((x,E)=>E.length>x.length?E:x)}};if(dU(t1(),r))return s("user",t1());let[a,c,u,l]=fge(Th(),r).split(mge);if(!dU(Th(),r)||!a||!/^[A-Za-z0-9_-]{1,64}$/.test(a))return{missing:null};let d=Ga(`app:${a}`);return c==="files"?s(`app:${a}`,d.files):c!=="nodes"||n!=="read"?{missing:null}:o(`crtr:objects:read:app:${a}`)?{root:d.nodes}:u&&l==="attachments"&&o(`crtr:conversations:read:app:${a}`)?{root:xXe(d.nodes,u,l)}:{missing:`crtr:objects:read:app:${a}`}}var hge=h(()=>{"use strict";B0();Vi()});function gge(t,e,n,r){if(e.class==="object")return e.repos.some(s=>s.rowid===n.rowid);if(e.class!=="node")return!1;let i=[...e.checkouts,...
|
|
1735
|
+
WHERE i.object = ? AND i.scheme = 'app'`).all(s.app).some(({resolved_scopes_json:c})=>JSON.parse(c).some(({scope:u,mode:l})=>l==="always"&&la(u,"crtr:memory:manage:user"))):!1}}});import{join as xXe,relative as fge,resolve as hgi,sep as mge}from"node:path";function dU(t,e){let n=fge(t,e);return n===""||n!==".."&&!n.startsWith(`..${mge}`)&&!n.startsWith(mge)}function bXe(t,e,n){let r=hgi(e);if(t===null||process.platform!=="linux"&&t.app==="app:terminal"){if(process.platform!=="linux")return{root:"/"};let p=[T9(),Th()].find(f=>dU(f,r));return p?{root:p}:{missing:null}}let i=Ga(t.app);if(t.run&&dU(i.root,r)){let[p,f]=fge(i.root,r).split(mge);if(p==="nodes"&&f)return t.run.reaches(f)?{root:xXe(i.nodes,f)}:{hidden:!0};if(t.run.isolated)return{hidden:!0}}for(let p of[i.files,i.nodes])if(dU(p,r))return{root:p};let o=p=>t.resolved.some(f=>la(f,p))&&(t.launched===null||t.launched.some(f=>la(f,p))),s=(p,f)=>{let m=fge(f,r)||".",g=x=>x.flatMap(E=>{let _=/^crtr:files:(read|rw):(user|app:[A-Za-z0-9_-]{1,64}):(.+)$/.exec(E);return!_||_[2]!==p||n==="rw"&&_[1]!=="rw"?[]:_[3]==="."||_[3]===m||m.startsWith(`${_[3]}/`)?[xXe(f,_[3]==="."?"":_[3])]:[]}),y=g(t.resolved),w=t.launched===null?y:g(t.launched);return!y.length||!w.length?{missing:`crtr:files:${n}:${p}:${m}`}:{root:[...y,...w].reduce((x,E)=>E.length>x.length?E:x)}};if(dU(t1(),r))return s("user",t1());let[a,c,u,l]=fge(Th(),r).split(mge);if(!dU(Th(),r)||!a||!/^[A-Za-z0-9_-]{1,64}$/.test(a))return{missing:null};let d=Ga(`app:${a}`);return c==="files"?s(`app:${a}`,d.files):c!=="nodes"||n!=="read"?{missing:null}:o(`crtr:objects:read:app:${a}`)?{root:d.nodes}:u&&l==="attachments"&&o(`crtr:conversations:read:app:${a}`)?{root:xXe(d.nodes,u,l)}:{missing:`crtr:objects:read:app:${a}`}}var hge=h(()=>{"use strict";B0();Vi()});function gge(t,e,n,r){if(e.class==="object")return e.repos.some(s=>s.rowid===n.rowid);if(e.class!=="node")return!1;let i=[...e.checkouts,...e.projectCheckouts].filter(s=>s.repo.rowid===n.rowid);if(i.length===0)return!1;let o={app:gf(t,e.app).external_id,resolved:e.scopes??[],launched:null};return i.some(s=>"root"in bXe(o,s.root,r))}var Rvn,EXe=h(()=>{"use strict";hge();H6();Rvn={identity:gf,sync:"repo-ref",canRead(t,e,n,r){return e.class==="person"||r.length===0?!0:gge(t,e,n,"read")},canChange(t,e,n,r,i){return i.privacy!=="public"?!1:e.class==="person"?!0:gge(t,e,n,"rw")}}});function gf(t,e){let n=ae(t,"SELECT object, scheme, external_id FROM identities WHERE object = ?").get(e.rowid);if(!n)throw new Error(`${e.type} ${e.id} has no identity`);return n}var Dvn,H6=h(()=>{"use strict";Ft();Ivn();kvn();EXe();Dvn={person:Tvn,app:Cvn,repo:Rvn}});function yge(t,e){return ae(t,`WITH RECURSIVE chain(rowid, depth) AS (
|
|
1736
1736
|
SELECT owner, 1 FROM objects WHERE rowid = ? AND owner IS NOT NULL
|
|
1737
1737
|
UNION ALL
|
|
1738
1738
|
SELECT o.owner, c.depth + 1 FROM objects o JOIN chain c ON o.rowid = c.rowid WHERE o.owner IS NOT NULL
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var f=Object.defineProperty;var o=(e,t)=>f(e,"name",{value:t,configurable:!0});import{usage as m}from"../../core/errors.js";import{ApiError as w}from"../../api/errors.js";import{rethrowAsCliError as h}from"../api-client.js";import{RULE_ON as g,RULE_DELIVER as v,DOC_KINDS as y}from"../../api/dto/docs.js";import{PRIVACY_LEVELS_DTO as b}from"../../api/dto/objects.js";const j="A document name (with or without [[ ]]) or a raw object id.",p="node | user | profile | profile:<name> | app | repo | repo:<handle>";function O(e){return{kind:"flag",name:"owner",type:"string",required:!1,constraint:e,focusedHelp:{whenToUse:"Omit it for a document only this node and the nodes it points at need: the writing node owns it and it is deleted with the node, unless a kept document links to it. Give another owner when the document must outlive this node or reach other nodes by itself. A document worth another owner carries reusable current truth: a non-obvious procedure, a gotcha, a lasting preference, a convention across repos. Task notes, conversation summaries and facts the repo already records stay with the node or are not written. Before writing one, search for a document that already covers it (`crtr canvas search`) and revise that one with `crtr doc edit` instead of adding a near copy. When one document grows long, keep the most important content in it and move separate subjects into documents named under it (`topic/part`), each pointed at with a `[[link]]`. Choose the narrowest owner that still reaches the next agent who needs it.",value:`One of ${p}. \`node\` \u2014 the writing node; only this node loads it by rule, other nodes read it when pointed at it; deleted with the node. \`profile\` \u2014 the node's profile, or \`profile:<name>\`; loads for nodes running in that profile; stays on this runtime. \`app\` \u2014 the app that runs the node; kept until deleted; readable by that app's nodes. \`repo\` \u2014 the repo the node works in (from its working directory), or \`repo:<handle>\`; every clone and worktree of the repo shares it, and it syncs to collaborators over the repo's \`refs/crtr/docs\`. A repo owns only public documents: a private or friends document that should load in one repo is owned by \`user\` and carries a rule with \`if: {"repo": "<handle>"}\`. \`user\` \u2014 you, the person; loads for every node by its rules; readable by apps by privacy level. Default: the writing node; a person's own run (not a node) defaults to \`user\`. The owner becomes the start of the document's full name (\`<owner-handle>/<name>\`); \`crtr doc move --owner\` changes it later and keeps the id and every link.`,effects:["Decides who may read and change the document, where it syncs, and what deleting takes it with."]}}}o(O,"ownerParam");const k="`if` is a condition on the receiving node. Fields: `kind`, `mode`, `lifecycle`, `cwd`, `repo` (the handle of a repo the node is eligible for: those enclosing its cwd and those of its profile's projects; use `cwd` for where it works), `profile`, `depth`. A field map is ANDed across fields; dotted fields resolve nested values. A scalar matches by equality (a list value matches when any element is equal); a list matcher matches by membership or overlap; an object matcher takes `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, `lte`. `all`, `any` and `not` combine conditions; field matchers beside them are ANDed in. An empty condition always matches; an unknown operator never matches. `match_frontmatter` uses the same language against the read file's YAML frontmatter.";function S(e){return{kind:"flag",name:"rule",type:"string",required:!1,repeatable:!0,constraint:e,focusedHelp:{whenToUse:"Omit it for most documents: a document with no rule is read when a node is pointed at it (`[[name]]`), finds it with `crtr canvas search`, or sees it in a listing. Add a rule only when the document must load on its own because of what a node is doing; every rule costs context in every node it fires on. Prefer the narrowest trigger: a `file-read` rule with a real glob over `boot` for knowledge about specific code; `boot` for knowledge about a person or process with no file boundary. When the whole document is one sentence, deliver `content` \u2014 a preview line would be longer than the document.",value:`One JSON object per occurrence: \`{"on": \u2026, "match"?: [globs], "match_frontmatter"?: {\u2026}, "if"?: {\u2026}, "deliver": \u2026}\`. \`on\` is the node's activity: ${g.join(" | ")}. \`boot\` \u2014 every session start of the node. \`workspace-open\` \u2014 the node's first message when it works in the document's repo. \`file-read\` \u2014 the node reads the content of a file whose absolute path or basename matches a \`match\` glob, or whose frontmatter matches \`match_frontmatter\`; through the Read tool, or a bash segment of \`cat\`, \`sed\` (not \`-i\`), \`head\`, \`tail\` (not \`-f\`), \`nl\` or \`awk\` on a literal path (resolved after the command's own \`cd\`s; \`rg\`, \`grep\`, \`find\`, \`ls\`, \`wc\`, \`stat\` are searches, not reads, and never fire it). \`document-read\` \u2014 another document whose name matches is delivered at content, including an explicit read. \`command\` \u2014 a shell segment of a bash command matches (segments split on \`&&\`, \`||\`, \`;\`, \`|\` and newlines outside quotes, leading \`VAR=value\` dropped; \`*\` spans whole whitespace-separated tokens, \`?\` one character, quoted text is opaque). \`pre-command\` \u2014 as \`command\`, but runs before the command: when the node has not read the document, the command does not run and the document is returned instead; the node re-issues the command. \`slash-command\` \u2014 the document is a \`/<name>\` slash command whose body is the injected prompt. \`match\` is required on \`file-read\` (unless \`match_frontmatter\` is given), \`document-read\`, \`command\` and \`pre-command\`, and not allowed on \`boot\`, \`workspace-open\` and \`slash-command\`. `+k+` \`deliver\` is ${v.join(" | ")}: the bare name, the preview line, or the whole body. The flag set replaces the document's whole rule list; on \`crtr doc edit\`, \`--unset rules\` removes every rule.`,effects:["When several rules deliver one document to a node, the highest delivery wins: content over preview over name; content already in the node's context is not delivered again.","A document delivered at content is a read: it records the node as a watcher, so the node is pushed later edits.","The profile's delivery limit per owner caps what `boot` and `workspace-open` rules deliver for that owner.",'A rule that delivers at `preview` requires the preview in the shape "When <situation in the reader\'s work>, read this because <what the reader gains>"; `crtr doc lint` checks it. There the line alone decides whether a node that did not ask for the document opens it.']}}}o(S,"ruleParam");const N={preview:o(e=>({kind:"flag",name:"preview",type:"string",required:!1,constraint:e}),"preview"),kind:{kind:"flag",name:"kind",type:"enum",choices:[...y],required:!1,constraint:"knowledge is consulted for facts or procedures; preference directs behavior. Default knowledge."},summary:{kind:"flag",name:"summary",type:"string",required:!1,constraint:"A very short version of the content, shown in `crtr canvas list`."},privacy:o(e=>({kind:"flag",name:"privacy",type:"enum",choices:[...b],required:!1,constraint:e}),"privacy"),whyItExists:{kind:"flag",name:"why-it-exists",type:"string",required:!1,constraint:"The observed failure this document exists to prevent; shown only with `crtr canvas read --fields`."},unlisted:o(e=>({kind:"flag",name:"unlisted",type:"bool",required:!1,constraint:e}),"unlisted"),lintIgnore:{kind:"flag",name:"lint-ignore",type:"string",required:!1,repeatable:!0,constraint:"A `crtr doc lint` rule name to skip for this document."},extension:{kind:"flag",name:"extension",type:"string",required:!1,repeatable:!0,constraint:"One declared plugin field as `<plugin>.<field>=<json value>`."}};function c(e){return e===void 0?[]:Array.isArray(e)?e.map(String):[String(e)]}o(c,"list");function T(e){const t=[],n={};for(const[r,a]of[["preview","preview"],["kind","kind"],["summary","summary"],["privacy","privacy"],["whyItExists","why_it_exists"]])typeof e[r]=="string"&&(n[a]=e[r]);typeof n.preview=="string"&&/[\r\n]/.test(n.preview)&&t.push("--preview must be one line"),e.unlisted===!0&&(n.unlisted=!0);const i=c(e.lintIgnore);i.length>0&&(n.lint_ignore=i);const s=c(e.rule);if(s.length>0){const r=[];s.forEach((a,u)=>{try{const d=JSON.parse(a);typeof d!="object"||d===null||Array.isArray(d)?t.push(`--rule #${u+1} must be a JSON object`):r.push(d)}catch{t.push(`--rule #${u+1} is not valid JSON: ${a}`)}}),n.rules=r}const l={};for(const r of c(e.extension)){const a=/^([^.=]+)\.([^=]+)=(.*)$/s.exec(r);if(!a){t.push(`--extension ${r} is not <plugin>.<field>=<json value>`);continue}try{(l[a[1]]??={})[a[2]]=JSON.parse(a[3])}catch{t.push(`--extension ${r}: the value after = is not valid JSON`)}}if(Object.keys(l).length>0&&(n.extensions=l),t.length>0)throw m(t.join("; "),{next:"Fix every value above and rerun; `--rule -h` gives the rule grammar."});return n}o(T,"fieldsFromInput");function R(e){const t=e.owner;if(t===void 0)return{};const n=String(t);if(!/^(node|user|profile|app|repo)$|^(profile|repo):.+$/.test(n))throw m(`invalid --owner ${n}`,{field:"owner",next:`Pass one of ${p}.`});return{owner:n}}o(R,"ownerFromInput");function D(e,t){e instanceof w&&(e.code==="read_first"&&h(e,`Read it first with \`crtr canvas read ${t}\`, then rerun the edit.`),e.code==="stale_revision"&&h(e,`Someone edited it after your read. Read it again with \`crtr canvas read ${t}\`, make your change on the current text, and rerun the edit.`)),h(e)}o(D,"rethrowDocError");function I(e){const t=[],n=e.unresolved_links,i=e.remote_links;return n?.length&&t.push(`Unresolved links (no document by these names yet): ${n.map(s=>`[[${s}]]`).join(", ")}`),i?.length&&t.push(`Not on this runtime: ${i.map(s=>`[[${s}]]`).join(", ")} \u2014 each may be on another runtime, or the name may be misspelled.`),t.map(s=>`
|
|
2
|
-
${s}`).join("")}o(I,"renderLinkReport");export{N as FIELD_FLAGS,p as OWNER_VALUES,
|
|
1
|
+
var f=Object.defineProperty;var o=(e,t)=>f(e,"name",{value:t,configurable:!0});import{usage as m}from"../../core/errors.js";import{ApiError as w}from"../../api/errors.js";import{rethrowAsCliError as h}from"../api-client.js";import{RULE_ON as g,RULE_DELIVER as v,DOC_KINDS as y}from"../../api/dto/docs.js";import{PRIVACY_LEVELS_DTO as b}from"../../api/dto/objects.js";const q="A document name (with or without [[ ]]) or a raw object id.",p="node | user | profile | profile:<name> | app | repo | repo:<handle>";function O(e){return{kind:"flag",name:"owner",type:"string",required:!1,constraint:e,focusedHelp:{whenToUse:"Omit it for a document only this node and the nodes it points at need: the writing node owns it and it is deleted with the node, unless a kept document links to it. Give another owner when the document must outlive this node or reach other nodes by itself. A document worth another owner carries reusable current truth: a non-obvious procedure, a gotcha, a lasting preference, a convention across repos. Task notes, conversation summaries and facts the repo already records stay with the node or are not written. Before writing one, search for a document that already covers it (`crtr canvas search`) and revise that one with `crtr doc edit` instead of adding a near copy. When one document grows long, keep the most important content in it and move separate subjects into documents named under it (`topic/part`), each pointed at with a `[[link]]`. Choose the narrowest owner that still reaches the next agent who needs it.",value:`One of ${p}. \`node\` \u2014 the writing node; only this node loads it by rule, other nodes read it when pointed at it; deleted with the node. \`profile\` \u2014 the node's profile, or \`profile:<name>\`; loads for nodes running in that profile; stays on this runtime. \`app\` \u2014 the app that runs the node; kept until deleted; readable by that app's nodes. \`repo\` \u2014 the repo the node works in (from its working directory), or \`repo:<handle>\` for another repo whose checkout the node's file permissions let it write (such as a project of its profile); every clone and worktree of the repo shares it, and it syncs to collaborators over the repo's \`refs/crtr/docs\`. A repo owns only public documents: a private or friends document that should load in one repo is owned by \`user\` and carries a rule with \`if: {"repo": "<handle>"}\`. \`user\` \u2014 you, the person; loads for every node by its rules; readable by apps by privacy level. Default: the writing node; a person's own run (not a node) defaults to \`user\`. The owner becomes the start of the document's full name (\`<owner-handle>/<name>\`); \`crtr doc move --owner\` changes it later and keeps the id and every link.`,effects:["Decides who may read and change the document, where it syncs, and what deleting takes it with."]}}}o(O,"ownerParam");const k="`if` is a condition on the receiving node. Fields: `kind`, `mode`, `lifecycle`, `cwd`, `repo` (the handle of a repo the node is eligible for: those enclosing its cwd and those of its profile's projects; use `cwd` for where it works), `profile`, `depth`. A field map is ANDed across fields; dotted fields resolve nested values. A scalar matches by equality (a list value matches when any element is equal); a list matcher matches by membership or overlap; an object matcher takes `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, `lte`. `all`, `any` and `not` combine conditions; field matchers beside them are ANDed in. An empty condition always matches; an unknown operator never matches. `match_frontmatter` uses the same language against the read file's YAML frontmatter.";function S(e){return{kind:"flag",name:"rule",type:"string",required:!1,repeatable:!0,constraint:e,focusedHelp:{whenToUse:"Omit it for most documents: a document with no rule is read when a node is pointed at it (`[[name]]`), finds it with `crtr canvas search`, or sees it in a listing. Add a rule only when the document must load on its own because of what a node is doing; every rule costs context in every node it fires on. Prefer the narrowest trigger: a `file-read` rule with a real glob over `boot` for knowledge about specific code; `boot` for knowledge about a person or process with no file boundary. When the whole document is one sentence, deliver `content` \u2014 a preview line would be longer than the document.",value:`One JSON object per occurrence: \`{"on": \u2026, "match"?: [globs], "match_frontmatter"?: {\u2026}, "if"?: {\u2026}, "deliver": \u2026}\`. \`on\` is the node's activity: ${g.join(" | ")}. \`boot\` \u2014 every session start of the node. \`workspace-open\` \u2014 the node's first message when it works in the document's repo. \`file-read\` \u2014 the node reads the content of a file whose absolute path or basename matches a \`match\` glob, or whose frontmatter matches \`match_frontmatter\`; through the Read tool, or a bash segment of \`cat\`, \`sed\` (not \`-i\`), \`head\`, \`tail\` (not \`-f\`), \`nl\` or \`awk\` on a literal path (resolved after the command's own \`cd\`s; \`rg\`, \`grep\`, \`find\`, \`ls\`, \`wc\`, \`stat\` are searches, not reads, and never fire it). \`document-read\` \u2014 another document whose name matches is delivered at content, including an explicit read. \`command\` \u2014 a shell segment of a bash command matches (segments split on \`&&\`, \`||\`, \`;\`, \`|\` and newlines outside quotes, leading \`VAR=value\` dropped; \`*\` spans whole whitespace-separated tokens, \`?\` one character, quoted text is opaque). \`pre-command\` \u2014 as \`command\`, but runs before the command: when the node has not read the document, the command does not run and the document is returned instead; the node re-issues the command. \`slash-command\` \u2014 the document is a \`/<name>\` slash command whose body is the injected prompt. \`match\` is required on \`file-read\` (unless \`match_frontmatter\` is given), \`document-read\`, \`command\` and \`pre-command\`, and not allowed on \`boot\`, \`workspace-open\` and \`slash-command\`. `+k+` \`deliver\` is ${v.join(" | ")}: the bare name, the preview line, or the whole body. The flag set replaces the document's whole rule list; on \`crtr doc edit\`, \`--unset rules\` removes every rule.`,effects:["When several rules deliver one document to a node, the highest delivery wins: content over preview over name; content already in the node's context is not delivered again.","A document delivered at content is a read: it records the node as a watcher, so the node is pushed later edits.","The profile's delivery limit per owner caps what `boot` and `workspace-open` rules deliver for that owner.",'A rule that delivers at `preview` requires the preview in the shape "When <situation in the reader\'s work>, read this because <what the reader gains>"; `crtr doc lint` checks it. There the line alone decides whether a node that did not ask for the document opens it.']}}}o(S,"ruleParam");const N={preview:o(e=>({kind:"flag",name:"preview",type:"string",required:!1,constraint:e}),"preview"),kind:{kind:"flag",name:"kind",type:"enum",choices:[...y],required:!1,constraint:"knowledge is consulted for facts or procedures; preference directs behavior. Default knowledge."},summary:{kind:"flag",name:"summary",type:"string",required:!1,constraint:"A very short version of the content, shown in `crtr canvas list`."},privacy:o(e=>({kind:"flag",name:"privacy",type:"enum",choices:[...b],required:!1,constraint:e}),"privacy"),whyItExists:{kind:"flag",name:"why-it-exists",type:"string",required:!1,constraint:"The observed failure this document exists to prevent; shown only with `crtr canvas read --fields`."},unlisted:o(e=>({kind:"flag",name:"unlisted",type:"bool",required:!1,constraint:e}),"unlisted"),lintIgnore:{kind:"flag",name:"lint-ignore",type:"string",required:!1,repeatable:!0,constraint:"A `crtr doc lint` rule name to skip for this document."},extension:{kind:"flag",name:"extension",type:"string",required:!1,repeatable:!0,constraint:"One declared plugin field as `<plugin>.<field>=<json value>`."}};function c(e){return e===void 0?[]:Array.isArray(e)?e.map(String):[String(e)]}o(c,"list");function T(e){const t=[],n={};for(const[r,a]of[["preview","preview"],["kind","kind"],["summary","summary"],["privacy","privacy"],["whyItExists","why_it_exists"]])typeof e[r]=="string"&&(n[a]=e[r]);typeof n.preview=="string"&&/[\r\n]/.test(n.preview)&&t.push("--preview must be one line"),e.unlisted===!0&&(n.unlisted=!0);const i=c(e.lintIgnore);i.length>0&&(n.lint_ignore=i);const s=c(e.rule);if(s.length>0){const r=[];s.forEach((a,u)=>{try{const d=JSON.parse(a);typeof d!="object"||d===null||Array.isArray(d)?t.push(`--rule #${u+1} must be a JSON object`):r.push(d)}catch{t.push(`--rule #${u+1} is not valid JSON: ${a}`)}}),n.rules=r}const l={};for(const r of c(e.extension)){const a=/^([^.=]+)\.([^=]+)=(.*)$/s.exec(r);if(!a){t.push(`--extension ${r} is not <plugin>.<field>=<json value>`);continue}try{(l[a[1]]??={})[a[2]]=JSON.parse(a[3])}catch{t.push(`--extension ${r}: the value after = is not valid JSON`)}}if(Object.keys(l).length>0&&(n.extensions=l),t.length>0)throw m(t.join("; "),{next:"Fix every value above and rerun; `--rule -h` gives the rule grammar."});return n}o(T,"fieldsFromInput");function R(e){const t=e.owner;if(t===void 0)return{};const n=String(t);if(!/^(node|user|profile|app|repo)$|^(profile|repo):.+$/.test(n))throw m(`invalid --owner ${n}`,{field:"owner",next:`Pass one of ${p}.`});return{owner:n}}o(R,"ownerFromInput");function D(e,t){e instanceof w&&(e.code==="read_first"&&h(e,`Read it first with \`crtr canvas read ${t}\`, then rerun the edit.`),e.code==="stale_revision"&&h(e,`Someone edited it after your read. Read it again with \`crtr canvas read ${t}\`, make your change on the current text, and rerun the edit.`)),h(e)}o(D,"rethrowDocError");function I(e){const t=[],n=e.unresolved_links,i=e.remote_links;return n?.length&&t.push(`Unresolved links (no document by these names yet): ${n.map(s=>`[[${s}]]`).join(", ")}`),i?.length&&t.push(`Not on this runtime: ${i.map(s=>`[[${s}]]`).join(", ")} \u2014 each may be on another runtime, or the name may be misspelled.`),t.map(s=>`
|
|
2
|
+
${s}`).join("")}o(I,"renderLinkReport");export{N as FIELD_FLAGS,p as OWNER_VALUES,q as REF_CONSTRAINT,T as fieldsFromInput,R as ownerFromInput,O as ownerParam,I as renderLinkReport,D as rethrowDocError,S as ruleParam};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var p=Object.defineProperty;var t=(e,o)=>p(e,"name",{value:o,configurable:!0});import{defineLeaf as i,defineBranch as d}from"../../core/command.js";import{resolve as s}from"node:path";import{cliClient as
|
|
1
|
+
var p=Object.defineProperty;var t=(e,o)=>p(e,"name",{value:o,configurable:!0});import{defineLeaf as i,defineBranch as d}from"../../core/command.js";import{resolve as s}from"node:path";import{cliClient as n}from"../api-client.js";import{resolveProfileViaDaemon as a}from"./daemon-profiles.js";import{PROFILE_PROJECT_MEMORY_VALUES as l}from"../../api/dto/profiles.js";const c=i({name:"add",description:"add a project to a profile's purview",whenToUse:"a profile needs purview over another project directory \u2014 extend it after creation.",help:{name:"profile project add",summary:"add a project relationship to a profile, deduped by real path",params:[{kind:"positional",name:"profile",required:!0,constraint:"Profile id or manifest name; a close-enough name resolves to the nearest profile, reported on stderr."},{kind:"flag",name:"dir",type:"path",required:!0,constraint:"Directory to add. Must exist and be a directory; resolved to its absolute real path before storing. A path already on the profile keeps its position and nothing changes."}],output:[{name:"profile_id",type:"string",required:!0,constraint:"Stable directory id."},{name:"projects",type:"ProfileProject[]",required:!0,constraint:"The updated `{path}` relationships in profile order. `path` defines purview."}],outputKind:"object",effects:["Appends a new project relationship to the profile row. Purview follows `path` for config, and every repo enclosing `path` joins the view of every node of the profile: its documents are readable, listed, and delivered at boot and workspace-open. Each such repo starts at a `preview` delivery limit unless the profile already limits that handle (change it with `profile project limit`). Running brokers discover the updated projects on their next scope read."]},run:t(async e=>{const{id:o}=await a(e.profile),r=await n().updateProfile(o,{add_project:{path:s(e.dir)}});return{profile_id:r.id,projects:r.projects}},"run")}),h=i({name:"remove",description:"remove a project relationship from a profile's purview",whenToUse:"a profile no longer needs purview over one of its listed project directories \u2014 narrow it back. Works even if the directory itself was since deleted.",help:{name:"profile project remove",summary:"remove a project directory from a profile's manifest",params:[{kind:"positional",name:"profile",required:!0,constraint:"Profile id or manifest name; a close-enough name resolves to the nearest profile, reported on stderr."},{kind:"flag",name:"dir",type:"path",required:!0,constraint:"Directory to remove, matched against the stored project `path`. Does not require the directory to still exist on disk. The profile's app home does not change."}],output:[{name:"profile_id",type:"string",required:!0,constraint:"Stable directory id."},{name:"projects",type:"ProfileProject[]",required:!0,constraint:"The updated `{path}` relationships in profile order."}],outputKind:"object",effects:["Removes a project relationship from the profile row. This narrows purview and drops the path's repos from the view of the profile's nodes (unless a node's cwd is inside them); delivery limits are left in place. Running brokers discover the updated projects on their next scope read."]},run:t(async e=>{const{id:o}=await a(e.profile),r=await n().updateProfile(o,{remove_project:s(e.dir)});return{profile_id:r.id,projects:r.projects}},"run")}),m=i({name:"limit",description:"set how much of one owner's documents automatic delivery may disclose",whenToUse:"a profile's nodes should get less of one owner's documents (a repo, `user`, an app, a plugin) at boot and workspace-open \u2014 or that limit should be lifted.",help:{name:"profile project limit",summary:"set a profile's automatic delivery limit for one owner handle",params:[{kind:"positional",name:"profile",required:!0,constraint:"Profile id or manifest name; a close-enough name resolves to the nearest profile, reported on stderr."},{kind:"flag",name:"owner",type:"string",required:!0,constraint:"Owner the limit applies to. A bare handle (`crouter`, `user`) matches an owner of any type holding that handle. `<type>:<handle>` names one owner exactly \u2014 `repo:crouter`, `profile:work`, `app:terminal`, `person:user` \u2014 and wins over a bare key; use it when a repo and another owner share a handle. A profile's own documents are never capped, whichever key matches."},{kind:"flag",name:"level",type:"enum",choices:[...l],required:!0,constraint:"Maximum automatic delivery of this owner's documents at boot and workspace-open: `none`, `name`, `preview`, or `content`. `content` is no limit and removes the entry. Does not change explicit `crtr canvas read` access."}],output:[{name:"profile_id",type:"string",required:!0,constraint:"Stable directory id."},{name:"delivery_limits",type:"object",required:!0,constraint:"The profile\u2019s limits after the change, keyed by owner handle or `<type>:<handle>`; an owner not listed is not limited."}],outputKind:"object",effects:["Sets or removes one entry in the profile's `delivery_limits`. It caps only automatic `boot` and `workspace-open` delivery; a node's own documents and its own profile's documents are never capped. A `<type>:<handle>` key (for example `repo:crouter`) names one owner exactly and wins over a bare handle key."]},run:t(async e=>{const{id:o}=await a(e.profile),r=await n().updateProfile(o,{set_delivery_limit:{owner:e.owner,level:e.level}});return{profile_id:r.id,delivery_limits:r.delivery_limits}},"run")}),g=d({name:"project",description:"manage a profile's project purview and automatic delivery limits",whenToUse:"changing a profile\u2019s project purview after creation, or limiting how much of one owner's documents its nodes get automatically.",help:{name:"profile project",summary:"manage project purview and per-owner automatic delivery limits",model:"Each `{path}` relationship widens purview: the profile resolves config from `path`, and every node of the profile sees the repos enclosing `path` in addition to the repos enclosing its own cwd (cwd repos first, then projects in manifest order). Rule `if: {repo}` conditions hold for any of those repos (cwd repos and project repos); `--owner repo` stays with the cwd's repos, while a node may change a project repo's documents when its file permissions allow writing that checkout. `add` appends a new real path and starts its repos at a `preview` limit; `remove` narrows purview. `limit` sets the profile's `delivery_limits` entry for one owner handle, which caps automatic boot and workspace-open delivery of that owner's documents, whether the repo is in view through the cwd or the project list; explicit reads are not capped. Purview and delivery limits are both independent of a per-cwd default pin (`crtr profile default`)."},children:[c,h,m]});export{g as projectBranch};
|
|
@@ -31,9 +31,10 @@ export declare function noteNodeWorking(repos: ObjectRow[], cwd: string): Promis
|
|
|
31
31
|
* never fails a boot, and every failure is an event.
|
|
32
32
|
*/
|
|
33
33
|
export declare function noteNodeBooting(nodeId: string): Promise<void>;
|
|
34
|
-
/** A repo-owned document changed (write, edit, move at either end, delete). `
|
|
35
|
-
*
|
|
36
|
-
|
|
34
|
+
/** A repo-owned document changed (write, edit, move at either end, delete). `dirs`, the caller's
|
|
35
|
+
* cwd and the checkouts it reaches, record the fetch URL of any that is a checkout of the repo; the
|
|
36
|
+
* sync itself never depends on where the writer runs. */
|
|
37
|
+
export declare function noteRepoDocumentsChanged(repo: ObjectRow, dirs?: readonly string[]): void;
|
|
37
38
|
/**
|
|
38
39
|
* One sync: export canvas.db to a local commit, fetch the remote ref, fast-forward or merge,
|
|
39
40
|
* import the merged tree into canvas.db, push (never forced). A rejected push fetches again,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var B=Object.defineProperty;var
|
|
2
|
-
`)}}
|
|
1
|
+
var B=Object.defineProperty;var c=(t,r)=>B(t,"name",{value:r,configurable:!0});import{prepared as v}from"../../sqlite-statements.js";import{existsSync as D}from"node:fs";import{readFile as K,writeFile as q}from"node:fs/promises";import{join as S}from"node:path";import{emitEvent as G}from"../../events/emit.js";import{exportRepo as J,importTree as Q,pendingPaths as V}from"./exchange.js";import{checkoutChain as X}from"./identity.js";import{checkoutsForCwd as Y,reposForCwd as Z}from"./repos.js";import{commitTree as k,fetchDocs as ee,FIRST_FETCH_MARKER as te,isAncestor as L,mergeBase as re,openMirror as _,pushDocs as ne,readTree as p,remoteDocsTip as oe,resolveRef as h,setRef as y}from"./mirror.js";import{mergeTrees as se}from"./ref-format.js";const ie=2e4,A=3,H="remote-url",m="refs/crtr/docs",ae="refs/crtr/remote-docs",g="refs/crtr/imported";function T(t){try{G(t)}catch{}}c(T,"emitEvent");let M=null;const b=new Map,O=new Set;function Se(t,r){M={lifecycle:t,db:r}}c(Se,"startRepoSync");function P(t,r){return r.type!=="repo"?!1:v(t,"SELECT 1 FROM identities WHERE object = ? AND scheme = 'git'").get(r.rowid)!==void 0}c(P,"gitScheme");async function U(t,r){const e=new Map;for(const f of r)for(const o of X(f))o.ok&&o.scheme==="git"&&e.set(o.externalId,o.remoteUrl);if(e.size===0)return;const a=M.db(),n=v(a,"SELECT external_id FROM identities WHERE object = ? AND scheme = 'git'");for(const f of t){const o=n.get(f.rowid),i=o&&e.get(o.external_id);if(!i)continue;const s=await _(f.id);await q(S(s.dir,H),`${i}
|
|
2
|
+
`)}}c(U,"recordRemoteUrls");async function ce(t,r){const a=E().db(),n=t.filter(s=>P(a,s));if(n.length===0)return;await U(n,[r]);const f=[];for(const s of n){const l=!O.has(s.id)&&!D(S((await _(s.id)).dir,te)),u=R(s);l&&u&&f.push(u)}if(f.length===0)return;let o;const i=new Promise(s=>{o=setTimeout(s,ie)});await Promise.race([Promise.all(f),i]),clearTimeout(o)}c(ce,"noteNodeWorking");async function ke(t){try{const r=E().db(),e=v(r,"SELECT cwd FROM nodes WHERE node_id = ?").get(t);if(!e)return;const a=fe(r,()=>Z(r,e.cwd));a.length>0&&await ce(a,e.cwd)}catch(r){T({level:"warn",event:"repo_sync.failed",outcome:"failed",error:r,node_id:t,fields:{step:"fetch",reason:`the boot trigger failed: ${r instanceof Error?r.message:String(r)}`}})}}c(ke,"noteNodeBooting");function fe(t,r){if(t.isTransaction)return r();t.exec("BEGIN IMMEDIATE");try{const e=r();return t.exec("COMMIT"),e}catch(e){throw t.exec("ROLLBACK"),e}}c(fe,"inWrite");function _e(t,r=[]){const e=E();if(!P(e.db(),t))return;if(r.length===0){R(t);return}const a=U([t],r).then(()=>{R(t)},n=>{T({level:"error",event:"repo_sync.failed",error:n,fields:{repo:t.name,step:"fetch",reason:"recording the remote URL failed"}})});e.lifecycle.registerDetached(a)}c(_e,"noteRepoDocumentsChanged");function E(){if(!M)throw new Error("repo sync is not started: crtrd must call startRepoSync first");return M}c(E,"requireEngine");function R(t){const r=E(),e=b.get(t.id)??{running:null,again:!1};if(b.set(t.id,e),e.running)return e.again=!0,e.running.then(()=>b.get(t.id)?.running??null).then(n=>n??{ok:!0,pushed:!1,imported:$()});if(!r.lifecycle.acceptsDetachedWork())return null;O.add(t.id);const a=de(t).then(n=>(W(t,n),n),n=>{const f={ok:!1,step:"export",reason:n instanceof Error?n.message:String(n)};return W(t,f),f}).finally(()=>{e.running=null,e.again&&(e.again=!1,R(t))});return e.running=a,r.lifecycle.registerDetached(a.then(()=>{})),a}c(R,"schedule");function W(t,r){if(r.ok){const e=r.imported;T({level:"info",event:"repo_sync.completed",outcome:"succeeded",fields:{repo:t.name,pushed:r.pushed,created:e.created,revisions:e.revisions,deleted:e.deleted,needs_merge:e.needsMerge,refused:e.refused}})}else T({level:"warn",event:"repo_sync.failed",outcome:"failed",fields:{repo:t.name,step:r.step,reason:r.reason}})}c(W,"report");function $(){return{created:0,revisions:0,deleted:0,needsMerge:[],refused:[]}}c($,"emptyImport");class x extends Error{static{c(this,"StepError")}step;constructor(r,e){super(e),this.step=r}}async function C(t,r){try{return await r()}catch(e){throw e instanceof x?e:new x(t,e instanceof Error?e.message:String(e))}}c(C,"step");async function de(t){const r=E();try{const e=await _(t.id),a=await ue(e);if(a===null)return{ok:!1,step:"fetch",reason:`no checkout of ${t.name} with an origin remote has been seen by this runtime`};let n=$();const f=c(()=>C("import",async()=>{const o=await h(e,m),i=await h(e,g);if(o===null||i===o)return;const s=i?await p(e,g):new Map,l=r.db(),u=await p(e,m),d=Q(l,t,s,u,w=>Y(l,w));n={created:n.created+d.created,revisions:n.revisions+d.revisions,deleted:n.deleted+d.deleted,needsMerge:[...new Set([...n.needsMerge,...d.needsMerge])],refused:[...n.refused,...d.refused]},await j(e,o,u,s,new Set(d.refused.map(w=>w.path)),i)}),"importLocal");await f(),await C("export",async()=>{const o=await h(e,m),i=o?await p(e,m):null,s=await h(e,g),l=s?await p(e,g):null,u=J(r.db(),t,i,l);if(o?z(i,u):u.size===0)return;const d=await k(e,u,o?[o]:[],`crtr: export ${t.name}`);await y(e,m,d,o),await j(e,d,u,l??new Map,new Set(V(i,l)),s)});for(let o=1;;o++){const i=await oe(e,a);if(!i.ok)return{ok:!1,step:"fetch",reason:i.reason};let s=await h(e,m);if(i.value!==null&&!(s&&await le(e,i.value)&&await L(e,i.value,s))){const u=await ee(e,a);if(!u.ok)return{ok:!1,step:"fetch",reason:u.reason};const d=u.value;s=await C("merge",async()=>{if(!s||await L(e,s,d))return await y(e,m,d,s),d;const w=await re(e,s,d),F=se(w?await p(e,w):new Map,await p(e,m),await p(e,ae));for(const N of F.refused)n.refused.push(N);const I=await k(e,F.tree,[s,d],`crtr: merge ${t.name}`);return await y(e,m,I,s),I})}if(await f(),s===null||s===i.value)return{ok:!0,pushed:!1,imported:n};const l=await ne(e,a,s);if(l.ok)return{ok:!0,pushed:!0,imported:n};if(l.reason!=="rejected")return{ok:!1,step:"push",reason:l.reason};if(o>=A)return{ok:!1,step:"push",reason:`push rejected ${A} times`}}}catch(e){if(e instanceof x)return{ok:!1,step:e.step,reason:e.message};throw e}}c(de,"syncRepo");async function j(t,r,e,a,n,f){if(n.size===0){await y(t,g,r,f);return}const o=new Map(e);for(const i of n)a.has(i)?o.set(i,a.get(i)):o.delete(i);f!==null&&z(a,o)||await y(t,g,await k(t,o,[],"crtr: imported baseline (some files pending)"),f)}c(j,"advanceImported");async function ue(t){const r=S(t.dir,H);return D(r)&&(await K(r,"utf8")).trim()||null}c(ue,"readUrl");async function le(t,r){return await h(t,r)!==null}c(le,"hasCommit");function z(t,r){if(t.size!==r.size)return!1;for(const[e,a]of t)if(r.get(e)!==a)return!1;return!0}c(z,"sameTree");export{ie as FIRST_FETCH_WAIT_MS,ke as noteNodeBooting,ce as noteNodeWorking,_e as noteRepoDocumentsChanged,Se as startRepoSync,de as syncRepo};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Reader } from '../types.js';
|
|
2
2
|
import type { Space } from './index.js';
|
|
3
|
-
/** Whether a node
|
|
4
|
-
*
|
|
5
|
-
* in, since a node may be started in any
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
/** Whether a node has a checkout of `repo` that its file permissions reach: read to read the
|
|
4
|
+
* repo's documents, rw to change them. A checkout is one enclosing the node's cwd or one of its
|
|
5
|
+
* profile's project paths; the cwd alone never lets a run in, since a node may be started in any
|
|
6
|
+
* directory. A repo's documents change iff its files do. A repo-owned custom object is inside its
|
|
7
|
+
* own repo. A Mac terminal node reaches every path. */
|
|
8
8
|
export declare function insideRepo(db: Parameters<Space['canRead']>[0], reader: Reader, repo: {
|
|
9
9
|
rowid: number;
|
|
10
10
|
}, access: 'read' | 'rw'): boolean;
|
|
11
|
-
/** A repo: read and changed by a run
|
|
12
|
-
* permissions reach (see `insideRepo`). It owns only public documents, so a
|
|
11
|
+
/** A repo: read and changed by a run with a checkout of it (enclosing its cwd or one of its
|
|
12
|
+
* profile's project paths) that its file permissions reach (see `insideRepo`). It owns only public documents, so a
|
|
13
13
|
* non-public one is never changed. */
|
|
14
14
|
export declare const repoSpace: Space;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var u=Object.defineProperty;var
|
|
1
|
+
var u=Object.defineProperty;var i=(t,o)=>u(t,"name",{value:o,configurable:!0});import{fileRoot as f}from"../../spaces/file-root.js";import{identityOf as c}from"./index.js";function p(t,o,e,r){if(o.class==="object")return o.repos.some(n=>n.rowid===e.rowid);if(o.class!=="node")return!1;const s=[...o.checkouts,...o.projectCheckouts].filter(n=>n.repo.rowid===e.rowid);if(s.length===0)return!1;const l={app:c(t,o.app).external_id,resolved:o.scopes??[],launched:null};return s.some(n=>"root"in f(l,n.root,r))}i(p,"insideRepo");const d={identity:c,sync:"repo-ref",canRead(t,o,e,r){return o.class==="person"||r.length===0?!0:p(t,o,e,"read")},canChange(t,o,e,r,s){return s.privacy!=="public"?!1:o.class==="person"?!0:p(t,o,e,"rw")}};export{p as insideRepo,d as repoSpace};
|
|
@@ -74,10 +74,11 @@ export type Reader = {
|
|
|
74
74
|
/** The launched `scopes` list as given (see the app reader); null/absent = the run was launched with no list. */
|
|
75
75
|
launched?: readonly string[] | null;
|
|
76
76
|
/** The repos enclosing `cwd`, nearest first; `checkouts` pairs each with its checkout root. Only these are repos the node
|
|
77
|
-
* "works in": `--owner repo`
|
|
77
|
+
* "works in": `--owner repo` follows them; rule `if: {repo}` follows these plus `projectRepos`; read and write access
|
|
78
|
+
* to a repo's documents follow `checkouts` plus `projectCheckouts`, each gated by the node's file permissions. */
|
|
78
79
|
repos: ObjectRow[];
|
|
79
80
|
checkouts: Checkout[];
|
|
80
|
-
/** The repos of the profile's project paths that do not enclose `cwd`, in manifest order: in the node's view and
|
|
81
|
+
/** The repos of the profile's project paths that do not enclose `cwd`, in manifest order: in the node's view, readable and, where its file permissions allow writing the checkout, changeable; not worked in. */
|
|
81
82
|
projectRepos: ObjectRow[];
|
|
82
83
|
projectCheckouts: Checkout[];
|
|
83
84
|
profile: ObjectRow | null;
|
package/dist/core/render.js
CHANGED
|
@@ -12,7 +12,6 @@ ${c}`}u(b,"renderArray");function O(e,n){const s=n.output.map(t=>t.name);for(con
|
|
|
12
12
|
`));const d=c.length>1;for(const{name:t,text:r}of c)i.push(d?`**${t}:** ${r}`:r);for(const t of o)i.push(t);return i.join(`
|
|
13
13
|
|
|
14
14
|
`)}u(O,"renderResult");function _(e){const n=[e.message];return e.received!==void 0&&e.received!==null&&e.received!==""&&n.push(`received: ${f(e.received)}`),e.field!==void 0&&n.push(`field: ${e.field}`),e.http_status!==void 0&&n.push(`http status: ${e.http_status}`),e.retry_after_s!==void 0&&n.push(`retry after: ${e.retry_after_s}s`),e.details!==void 0&&Object.keys(e.details).length>0&&n.push(O(e.details,{output:[]})),"primaryCompleted"in e&&e.primaryCompleted===!0&&(n.push("primaryCompleted: true"),n.push(`Primary result (the command already completed):
|
|
15
|
-
${m(e.result)}`)),n.push(`Next: ${e.next}`),`<error code="${$(e.error)}"
|
|
16
|
-
${n.join(`
|
|
15
|
+
${m(e.result)}`)),n.push(`Next: ${e.next}`),`<error code="${$(e.error)}">${n.join(`
|
|
17
16
|
`)}
|
|
18
17
|
</error>`}u(_,"renderError");export{_ as renderError,O as renderResult};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var re=Object.defineProperty;var u=(t,e)=>re(t,"name",{value:e,configurable:!0});import{prepared as P}from"../../../core/sqlite-statements.js";import{ApiError as _}from"../../../api/errors.js";import{DOC_KINDS as q,DOC_UNSET_FIELDS as H}from"../../../api/dto/docs.js";import{PRIVACY_LEVELS_DTO as V}from"../../../api/dto/objects.js";import{withCanvasWrite as k}from"../../../core/canvas/db.js";import{CrtrError as ie}from"../../../core/errors.js";import{emitEvent as se}from"../../../core/events/emit.js";import{canChange as K,spaceOf as ae}from"../../../core/graph/access.js";import{get as A}from"../../../core/graph/bodies.js";import{deleteDocument as ce,documentRecord as F,editDocument as de,heads as I,history as ue,linksIn as U,moveDocument as fe,readBase as le,setDocumentRecord as me,withCurrentNames as pe,writeDocument as he}from"../../../core/graph/documents.js";import{resolveName as X}from"../../../core/graph/names.js";import{getByRowid as M}from"../../../core/graph/objects.js";import{noteRepoDocumentsChanged as we}from"../../../core/graph/repo-sync/index.js";import{checkoutsForCwd as ye}from"../../../core/graph/repo-sync/repos.js";import{lintRules as ve,rulesOf as ge}from"../../../core/graph/rules.js";import{unifiedDiff as _e}from"../../../core/graph/diff.js";import{ownerFromFlag as W,readerFor as E,resolveRef as z}from"../reader.js";function v(t,e,n,o,i={}){return new _(t,e,n,{...i,error:e,message:n,next:o})}u(v,"refuse");const b=u((t,e)=>new _(400,"invalid_request",t,e),"invalid");function D(t){const e=t.body;if(e===void 0)return{};if(typeof e!="object"||e===null||Array.isArray(e))throw b("the request body must be a JSON object");return e}u(D,"bodyOf");const L=u(t=>e=>ye(t,e),"reposFnFor");function C(t,e){if(t instanceof ie){const n=t.details;if(t.code==="read_first")throw v(409,"read_first",t.message,`Read it first with \`crtr canvas read ${e?.id??"<ref>"}\`, then rerun the edit.`,n??{});if(t.code==="conflict")throw new _(409,"name_taken",t.message,n);if(t.code==="forbidden")throw new _(403,"forbidden",t.message,n);if(t.code==="scope_missing")throw v(403,"scope_missing",t.message,`Launch the run with \`${String(n?.scope)}\` in its scopes list.`,n??{});if(n?.rule==="repo-public-only")throw v(422,"repo_documents_are_public",t.message,'Own it as `user` and add a rule with `if: {"repo": "<handle>"}` so it loads only in that repo.')}throw t}u(C,"mapGraphError");function x(t,e,n){const o=z(t,e,decodeURIComponent(n));if("remote"in o)throw v(404,"not_found",G(o.remote),"Check the name with `crtr canvas search`.");if("deleted"in o)throw new _(404,"not_found",`${o.deleted.name} is deleted`,{id:o.deleted.id,deleted:!0});if(o.type!=="document")throw b(`${n} is a ${o.type}, not a document`);return o}u(x,"refDocument");function G(t){return`no object named ${t} on this runtime; it may be on another runtime, or the name may be misspelled`}u(G,"remoteMessage");function be(t,e){const n=e.owner===null?null:M(t,e.owner);return n?n.type==="node"?n.id:n.name:""}u(be,"ownerHandle");function j(t,e){const n=be(t,e);return n?`${n}/${e.name}`:e.name}u(j,"canonicalName");function ke(t,e){const n=e.owner===null?null:M(t,e.owner),o=n?{id:n.id,type:n.type,handle:n.type==="node"?n.id:n.name}:null;return{id:e.id,type:e.type,kind:e.kind,owner:o,privacy:e.privacy,name:e.name,preview:e.preview??"",created_at:e.created_at,deleted_at:e.deleted_at}}u(ke,"objectDTO");function Ee(t,e){const n=e.owner===null?null:M(t,e.owner);return n?.type==="repo"?n:null}u(Ee,"repoOwner");function S(t,e,n,o){const i=[...new Map(n.flatMap(r=>{const f=Ee(t,r);return f?[[f.id,f]]:[]})).values()],a=e.class==="node"?e.cwd:o;return{repos:i,...a!==void 0?{cwd:a}:{}}}u(S,"repoChange");function T(t){for(const e of t.repos)try{we(e,t.cwd)}catch(n){se({level:"warn",event:"repo_sync.failed",outcome:"failed",error:n,fields:{repo:e.name,step:"export",reason:n instanceof Error?n.message:String(n)}})}}u(T,"syncChanged");const Y=/^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$/;function $e(t,e){if(t===void 0)return;if(!Array.isArray(t)){e.push("`rules` must be a list");return}const n=t.map(i=>{if(typeof i!="object"||i===null||Array.isArray(i))return i;const{match_frontmatter:a,...r}=i;return"matchFrontmatter"in r?{...r,match_frontmatter:r.matchFrontmatter}:a===void 0?r:{...r,matchFrontmatter:a}}),o=ve(n);if(o){e.push(o.replaceAll("matchFrontmatter","match_frontmatter"));return}return n}u($e,"rulesFromDTO");function Z(t,e){const n={},o=u(s=>{const c=t[s];if(c!==void 0){if(typeof c!="string"){e.push(`\`${s}\` must be a string`);return}return c}},"str"),i=o("preview");i!==void 0&&(i.trim()===""||/[\r\n]/.test(i)?e.push("`preview` must be one non-empty line"):n.preview=i);const a=o("summary");a!==void 0&&(n.summary=a);const r=o("why_it_exists");if(r!==void 0&&(n.whyItExists=r),t.privacy!==void 0&&(V.includes(t.privacy)?n.privacy=t.privacy:e.push(`\`privacy\` must be one of ${V.join(", ")}`)),t.unlisted!==void 0&&(typeof t.unlisted!="boolean"?e.push("`unlisted` must be a boolean"):n.unlisted=t.unlisted),t.lint_ignore!==void 0){const s=t.lint_ignore;!Array.isArray(s)||s.some(c=>typeof c!="string")?e.push("`lint_ignore` must be a list of strings"):n.lintIgnore=s}if(t.extensions!==void 0){const s=t.extensions;typeof s!="object"||s===null||Array.isArray(s)||Object.values(s).some(c=>typeof c!="object"||c===null||Array.isArray(c))?e.push("`extensions` must map each plugin to an object of fields"):n.extensions=s}const f=$e(t.rules,e);return f!==void 0&&(n.rules=f),n}u(Z,"fieldsFrom");function J(t){if(t.length>0)throw b(t.join("; "),{errors:t})}u(J,"failOn");function Q(t,e,n,o){const i=U(n).filter(a=>"remote"in X(t,e,a));return{...o.length>0?{unresolved_links:o}:{},...i.length>0?{remote_links:i}:{}}}u(Q,"linkReport");function Oe(t){const e=D(t),n=[],o=e.name;(typeof o!="string"||!Y.test(o))&&n.push("`name` must be segments of letters, digits, `_` or `-` joined by `/`"),typeof e.body!="string"&&n.push("`body` must be a string"),typeof e.preview!="string"&&n.push("`preview` is required");const i=e.kind??"knowledge";q.includes(i)||n.push(i==="report"?"reports are written only by `crtr push`":`\`kind\` must be one of ${q.join(", ")}`);const a=Z(e,n);e.cwd!==void 0&&typeof e.cwd!="string"&&n.push("`cwd` must be a string"),J(n);const r=e,{dto:f,change:s}=k(c=>{const h=E(c,t),d=W(c,h,r.owner,r.cwd);if(!K(c,h,d))throw new _(404,"not_found",`no owner ${d.name} you can write to`);try{const{object:p,unresolved:l}=he(c,h,{...a,owner:d,name:r.name,kind:i,preview:a.preview,privacy:a.privacy??"public"},r.body,L(c));return{dto:{id:p.id,name:j(c,p),...Q(c,h,r.body,l)},change:S(c,h,[p],r.cwd)}}catch(p){C(p)}});return T(s),f}u(Oe,"write");function Ae(t,e){let n=t;e.forEach(({old:o,new:i},a)=>{const r=o===""?-1:n.indexOf(o);if(r<0)throw v(422,"replace_no_match",`replace[${a}]: old text not found in the revision you read`,"Copy the old text exactly from your last read and rerun.",{index:a});if(n.indexOf(o,r+1)>=0)throw v(422,"replace_ambiguous",`replace[${a}]: old text occurs more than once`,"Include more surrounding text so the old text matches once, and rerun.",{index:a});n=n.slice(0,r)+i+n.slice(r+o.length)})}u(Ae,"checkReplacements");function je(t){const e=D(t),n=[],o=e.body!==void 0,i=e.replace!==void 0;o&&i&&n.push("give `body` or `replace`, not both"),o&&typeof e.body!="string"&&n.push("`body` must be a string");const a=e.replace;i&&(!Array.isArray(a)||a.length===0||a.some(d=>typeof d!="object"||d===null||typeof d.old!="string"||typeof d.new!="string"))&&n.push("`replace` must be a non-empty list of {old, new} strings"),(typeof e.rationale!="string"||e.rationale.trim()==="")&&n.push("`rationale` is required"),e.verbatim!==void 0&&typeof e.verbatim!="boolean"&&n.push("`verbatim` must be a boolean"),e.base!==void 0&&(typeof e.base!="string"||e.base==="")&&n.push("`base` must be a non-empty heads key"),e.base!==void 0&&!o&&!i&&n.push("`base` applies only to a `body` or `replace` edit");const r=Z(e,n),f=e.unset??[];if(!Array.isArray(f)||f.some(d=>!H.includes(d)))n.push(`\`unset\` takes only ${H.join(", ")}`);else for(const d of f)d==="summary"&&(r.summary=null),d==="why_it_exists"&&(r.whyItExists=null),d==="unlisted"&&(r.unlisted=!1),d==="lint_ignore"&&(r.lintIgnore=null),d==="extensions"&&(r.extensions=null),d==="rules"&&(r.rules=[]);n.length===0&&!o&&!i&&Object.keys(r).length===0&&n.push("nothing to change: give `body`, `replace`, or a field"),J(n);const s=e,{dto:c,synced:h}=k(d=>{const p=E(d,t),l=x(d,p,t.params.ref),g=j(d,l),O=S(d,p,[l]);try{if(!o&&!i)return me(d,p,l,r),{dto:{id:l.id,name:g,revision:F(d,l).revision},synced:O};const w=s.base??le(d,p,l);if(i&&w!==null){const oe=F(d,l),B=I(d,l);oe.heads_key===w&&B.length===1&&Ae(pe(d,l,A(B[0].body_hash)),s.replace)}const R=s.verbatim===!0,m=o?{body:s.body,verbatim:R}:{replace:s.replace,verbatim:R},y=de(d,p,l,m,s.rationale,w,r,L(d));if("stale"in y)throw v(409,"stale_revision",`${g} changed after your read; it is now at revision ${y.stale.revision}`,`Read it again with \`crtr canvas read ${g}\`, then redo your change on the current text.`,{stale:y.stale});const N=A(I(d,l)[0].body_hash);return{dto:{id:l.id,name:g,revision:y.revision,...Q(d,p,N,y.unresolved)},synced:O}}catch(w){C(w,l)}});return T(h),c}u(je,"edit");function Re(t){const e=D(t);if(e.new_name===void 0&&e.owner===void 0)throw b("give a new name, an owner, or both");if(e.new_name!==void 0&&(typeof e.new_name!="string"||!Y.test(e.new_name)))throw b("`new_name` must be segments of letters, digits, `_` or `-` joined by `/`");const{dto:n,change:o}=k(i=>{const a=E(i,t),r=x(i,a,t.params.ref),f=e.owner===void 0?void 0:W(i,a,e.owner,e.cwd);try{const s=fe(i,a,r,{...e.new_name!==void 0?{name:e.new_name}:{},...f?{owner:f}:{}});return{dto:{id:s.id,name:j(i,s)},change:S(i,a,[r,s],e.cwd)}}catch(s){C(s,r)}});return T(o),n}u(Re,"move");function De(t){const{dto:e,change:n}=k(o=>{const i=E(o,t),a=x(o,i,t.params.ref),r=j(o,a),f=S(o,i,[a]);try{ce(o,i,a,L(o))}catch(s){C(s,a)}return{dto:{id:a.id,name:r},change:f}});return T(n),e}u(De,"remove");function Ce(t){const e=t.query.get("diff"),n=e===null?void 0:Number(e);if(n!==void 0&&!(Number.isInteger(n)&&n>0))throw b("`diff` must be a revision number");return k(o=>{const i=E(o,t),a=z(o,i,decodeURIComponent(t.params.ref));if("remote"in a)throw v(404,"not_found",G(a.remote),"Check the name with `crtr canvas search`.");const r="deleted"in a?a.deleted:a;if(r.type!=="document")throw b(`${r.name} is a ${r.type}, not a document`);const f=ue(o,i,r),s={object:ke(o,r),revisions:f.map(c=>({rev_id:c.rev_id,number:c.number,author_id:c.author,rationale:c.rationale,verbatim:c.verbatim,created_at:c.created_at,parents:c.parents}))};if(n!==void 0){const c=f.find(d=>d.number===n);if(!c)throw new _(404,"not_found",`${r.name} has no revision ${n}`);const h=c.parents[0]===void 0?void 0:f.find(d=>d.rev_id===c.parents[0]);s.diff=_e(h?A(h.body_hash):"",A(c.body_hash))}return s})}u(Ce,"revisionHistory");const ee=1e3,ne=1e3,te=3e3,Se=/^When\b.+\bbecause\b.+/,Te=u(t=>{const e=t.trim();return e===""?0:e.split(/\s+/).length},"words");function Ne(t,e){if(t.some(o=>o.on==="boot"&&o.deliver==="content")&&e>ee)return`body is ${e} words, over the ${ee}-word cap for a boot rule at content (the whole body enters every node's context). Keep the core here and move separate subjects into [[linked]] documents.`;const n=[...new Set(t.filter(o=>o.on!=="boot"&&o.deliver==="content").map(o=>o.on))];return n.length>0&&e>ne?`body is ${e} words, over the ${ne}-word cap for a content rule on ${n.join("/")}. Keep the core here and move separate subjects into [[linked]] documents.`:t.some(o=>o.on==="boot"&&o.deliver==="preview")&&e>te?`body is ${e} words, over the ${te}-word cap for a boot rule at preview. Keep the core here and move separate subjects into [[linked]] documents.`:null}u(Ne,"lengthFinding");function Fe(t){const e=D(t);return k(n=>{const o=E(n,t),i=e.owner===void 0?null:W(n,o,e.owner,e.cwd),a=P(n,"SELECT * FROM objects WHERE type = 'document' AND deleted_at IS NULL AND kind != 'report' ORDER BY name").all(),r=[],f=new Map;for(const s of a){if(i&&s.owner!==i.rowid||!K(n,o,s))continue;const c=F(n,s),h=new Set(c.lint_ignore??[]),d=j(n,s),p=u((m,y,N)=>{h.has(m)||r.push({name:d,severity:y,rule:m,message:N})},"add"),l=ge(n,s),g=I(n,s)[0],O=g?A(g.body_hash):"",w=Ne(l,Te(O));w&&p("length","error",w),l.some(m=>m.deliver==="preview")&&!Se.test(s.preview??"")&&p("preview-shape","error",`a rule delivers this document at preview, so the preview must read "When <situation in the reader's work>, read this because <what the reader gains>"`);const R=new Set(P(n,"SELECT written_as FROM edges WHERE type = 'links-to' AND src = ?").all(s.rowid).map(m=>m.written_as));for(const m of U(O))R.has(m)||"remote"in X(n,o,m)||p("links","warning",`[[${m}]] names no object in your view`);if(l.some(m=>m.on==="workspace-open"&&m.deliver==="content")){const m=ae(n,s);m.type==="repo"&&!h.has("workspace-open")&&f.set(m.name,[...f.get(m.name)??[],d])}}for(const[s,c]of f)if(!(c.length<2))for(const h of c)r.push({name:h,severity:"warning",rule:"workspace-open",message:`repo ${s} has ${c.length} documents delivered at content on workspace-open (${c.join(", ")}); keep one and link the rest from it`});return{findings:r}})}u(Fe,"lint");const $=u((t,e=200)=>n=>({status:e,body:t(n)}),"ok"),Qe=[{method:"POST",pattern:"/v1/docs",handler:$(Oe,201)},{method:"PATCH",pattern:"/v1/docs/:ref",handler:$(je)},{method:"POST",pattern:"/v1/docs/:ref/move",handler:$(Re)},{method:"DELETE",pattern:"/v1/docs/:ref",handler:$(De)},{method:"GET",pattern:"/v1/docs/:ref/history",handler:$(Ce)},{method:"POST",pattern:"/v1/docs/lint",handler:$(Fe)}];export{Qe as docRoutes};
|
|
1
|
+
var re=Object.defineProperty;var u=(t,e)=>re(t,"name",{value:e,configurable:!0});import{prepared as P}from"../../../core/sqlite-statements.js";import{ApiError as _}from"../../../api/errors.js";import{DOC_KINDS as q,DOC_UNSET_FIELDS as H}from"../../../api/dto/docs.js";import{PRIVACY_LEVELS_DTO as V}from"../../../api/dto/objects.js";import{withCanvasWrite as k}from"../../../core/canvas/db.js";import{CrtrError as ie}from"../../../core/errors.js";import{emitEvent as se}from"../../../core/events/emit.js";import{canChange as K,spaceOf as ae}from"../../../core/graph/access.js";import{get as A}from"../../../core/graph/bodies.js";import{deleteDocument as ce,documentRecord as F,editDocument as de,heads as I,history as ue,linksIn as U,moveDocument as fe,readBase as le,setDocumentRecord as me,withCurrentNames as pe,writeDocument as he}from"../../../core/graph/documents.js";import{resolveName as X}from"../../../core/graph/names.js";import{getByRowid as M}from"../../../core/graph/objects.js";import{noteRepoDocumentsChanged as we}from"../../../core/graph/repo-sync/index.js";import{checkoutsForCwd as ye}from"../../../core/graph/repo-sync/repos.js";import{lintRules as ve,rulesOf as ge}from"../../../core/graph/rules.js";import{unifiedDiff as _e}from"../../../core/graph/diff.js";import{ownerFromFlag as W,readerFor as E,resolveRef as z}from"../reader.js";function v(t,e,n,o,s={}){return new _(t,e,n,{...s,error:e,message:n,next:o})}u(v,"refuse");const b=u((t,e)=>new _(400,"invalid_request",t,e),"invalid");function D(t){const e=t.body;if(e===void 0)return{};if(typeof e!="object"||e===null||Array.isArray(e))throw b("the request body must be a JSON object");return e}u(D,"bodyOf");const L=u(t=>e=>ye(t,e),"reposFnFor");function C(t,e){if(t instanceof ie){const n=t.details;if(t.code==="read_first")throw v(409,"read_first",t.message,`Read it first with \`crtr canvas read ${e?.id??"<ref>"}\`, then rerun the edit.`,n??{});if(t.code==="conflict")throw new _(409,"name_taken",t.message,n);if(t.code==="forbidden")throw new _(403,"forbidden",t.message,n);if(t.code==="scope_missing")throw v(403,"scope_missing",t.message,`Launch the run with \`${String(n?.scope)}\` in its scopes list.`,n??{});if(n?.rule==="repo-public-only")throw v(422,"repo_documents_are_public",t.message,'Own it as `user` and add a rule with `if: {"repo": "<handle>"}` so it loads only in that repo.')}throw t}u(C,"mapGraphError");function x(t,e,n){const o=z(t,e,decodeURIComponent(n));if("remote"in o)throw v(404,"not_found",G(o.remote),"Check the name with `crtr canvas search`.");if("deleted"in o)throw new _(404,"not_found",`${o.deleted.name} is deleted`,{id:o.deleted.id,deleted:!0});if(o.type!=="document")throw b(`${n} is a ${o.type}, not a document`);return o}u(x,"refDocument");function G(t){return`no object named ${t} on this runtime; it may be on another runtime, or the name may be misspelled`}u(G,"remoteMessage");function be(t,e){const n=e.owner===null?null:M(t,e.owner);return n?n.type==="node"?n.id:n.name:""}u(be,"ownerHandle");function j(t,e){const n=be(t,e);return n?`${n}/${e.name}`:e.name}u(j,"canonicalName");function ke(t,e){const n=e.owner===null?null:M(t,e.owner),o=n?{id:n.id,type:n.type,handle:n.type==="node"?n.id:n.name}:null;return{id:e.id,type:e.type,kind:e.kind,owner:o,privacy:e.privacy,name:e.name,preview:e.preview??"",created_at:e.created_at,deleted_at:e.deleted_at}}u(ke,"objectDTO");function Ee(t,e){const n=e.owner===null?null:M(t,e.owner);return n?.type==="repo"?n:null}u(Ee,"repoOwner");function S(t,e,n,o){const s=[...new Map(n.flatMap(f=>{const i=Ee(t,f);return i?[[i.id,i]]:[]})).values()],a=e.class==="node"?e.cwd:o,r=e.class==="node"?[...e.checkouts,...e.projectCheckouts].map(f=>f.root):[];return{repos:s,dirs:[...new Set([...a!==void 0?[a]:[],...r])]}}u(S,"repoChange");function T(t){for(const e of t.repos)try{we(e,t.dirs)}catch(n){se({level:"warn",event:"repo_sync.failed",outcome:"failed",error:n,fields:{repo:e.name,step:"export",reason:n instanceof Error?n.message:String(n)}})}}u(T,"syncChanged");const Y=/^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$/;function $e(t,e){if(t===void 0)return;if(!Array.isArray(t)){e.push("`rules` must be a list");return}const n=t.map(s=>{if(typeof s!="object"||s===null||Array.isArray(s))return s;const{match_frontmatter:a,...r}=s;return"matchFrontmatter"in r?{...r,match_frontmatter:r.matchFrontmatter}:a===void 0?r:{...r,matchFrontmatter:a}}),o=ve(n);if(o){e.push(o.replaceAll("matchFrontmatter","match_frontmatter"));return}return n}u($e,"rulesFromDTO");function Z(t,e){const n={},o=u(i=>{const c=t[i];if(c!==void 0){if(typeof c!="string"){e.push(`\`${i}\` must be a string`);return}return c}},"str"),s=o("preview");s!==void 0&&(s.trim()===""||/[\r\n]/.test(s)?e.push("`preview` must be one non-empty line"):n.preview=s);const a=o("summary");a!==void 0&&(n.summary=a);const r=o("why_it_exists");if(r!==void 0&&(n.whyItExists=r),t.privacy!==void 0&&(V.includes(t.privacy)?n.privacy=t.privacy:e.push(`\`privacy\` must be one of ${V.join(", ")}`)),t.unlisted!==void 0&&(typeof t.unlisted!="boolean"?e.push("`unlisted` must be a boolean"):n.unlisted=t.unlisted),t.lint_ignore!==void 0){const i=t.lint_ignore;!Array.isArray(i)||i.some(c=>typeof c!="string")?e.push("`lint_ignore` must be a list of strings"):n.lintIgnore=i}if(t.extensions!==void 0){const i=t.extensions;typeof i!="object"||i===null||Array.isArray(i)||Object.values(i).some(c=>typeof c!="object"||c===null||Array.isArray(c))?e.push("`extensions` must map each plugin to an object of fields"):n.extensions=i}const f=$e(t.rules,e);return f!==void 0&&(n.rules=f),n}u(Z,"fieldsFrom");function J(t){if(t.length>0)throw b(t.join("; "),{errors:t})}u(J,"failOn");function Q(t,e,n,o){const s=U(n).filter(a=>"remote"in X(t,e,a));return{...o.length>0?{unresolved_links:o}:{},...s.length>0?{remote_links:s}:{}}}u(Q,"linkReport");function Oe(t){const e=D(t),n=[],o=e.name;(typeof o!="string"||!Y.test(o))&&n.push("`name` must be segments of letters, digits, `_` or `-` joined by `/`"),typeof e.body!="string"&&n.push("`body` must be a string"),typeof e.preview!="string"&&n.push("`preview` is required");const s=e.kind??"knowledge";q.includes(s)||n.push(s==="report"?"reports are written only by `crtr push`":`\`kind\` must be one of ${q.join(", ")}`);const a=Z(e,n);e.cwd!==void 0&&typeof e.cwd!="string"&&n.push("`cwd` must be a string"),J(n);const r=e,{dto:f,change:i}=k(c=>{const h=E(c,t),d=W(c,h,r.owner,r.cwd);if(!K(c,h,d))throw new _(404,"not_found",`no owner ${d.name} you can write to`);try{const{object:p,unresolved:l}=he(c,h,{...a,owner:d,name:r.name,kind:s,preview:a.preview,privacy:a.privacy??"public"},r.body,L(c));return{dto:{id:p.id,name:j(c,p),...Q(c,h,r.body,l)},change:S(c,h,[p],r.cwd)}}catch(p){C(p)}});return T(i),f}u(Oe,"write");function Ae(t,e){let n=t;e.forEach(({old:o,new:s},a)=>{const r=o===""?-1:n.indexOf(o);if(r<0)throw v(422,"replace_no_match",`replace[${a}]: old text not found in the revision you read`,"Copy the old text exactly from your last read and rerun.",{index:a});if(n.indexOf(o,r+1)>=0)throw v(422,"replace_ambiguous",`replace[${a}]: old text occurs more than once`,"Include more surrounding text so the old text matches once, and rerun.",{index:a});n=n.slice(0,r)+s+n.slice(r+o.length)})}u(Ae,"checkReplacements");function je(t){const e=D(t),n=[],o=e.body!==void 0,s=e.replace!==void 0;o&&s&&n.push("give `body` or `replace`, not both"),o&&typeof e.body!="string"&&n.push("`body` must be a string");const a=e.replace;s&&(!Array.isArray(a)||a.length===0||a.some(d=>typeof d!="object"||d===null||typeof d.old!="string"||typeof d.new!="string"))&&n.push("`replace` must be a non-empty list of {old, new} strings"),(typeof e.rationale!="string"||e.rationale.trim()==="")&&n.push("`rationale` is required"),e.verbatim!==void 0&&typeof e.verbatim!="boolean"&&n.push("`verbatim` must be a boolean"),e.base!==void 0&&(typeof e.base!="string"||e.base==="")&&n.push("`base` must be a non-empty heads key"),e.base!==void 0&&!o&&!s&&n.push("`base` applies only to a `body` or `replace` edit");const r=Z(e,n),f=e.unset??[];if(!Array.isArray(f)||f.some(d=>!H.includes(d)))n.push(`\`unset\` takes only ${H.join(", ")}`);else for(const d of f)d==="summary"&&(r.summary=null),d==="why_it_exists"&&(r.whyItExists=null),d==="unlisted"&&(r.unlisted=!1),d==="lint_ignore"&&(r.lintIgnore=null),d==="extensions"&&(r.extensions=null),d==="rules"&&(r.rules=[]);n.length===0&&!o&&!s&&Object.keys(r).length===0&&n.push("nothing to change: give `body`, `replace`, or a field"),J(n);const i=e,{dto:c,synced:h}=k(d=>{const p=E(d,t),l=x(d,p,t.params.ref),g=j(d,l),O=S(d,p,[l]);try{if(!o&&!s)return me(d,p,l,r),{dto:{id:l.id,name:g,revision:F(d,l).revision},synced:O};const w=i.base??le(d,p,l);if(s&&w!==null){const oe=F(d,l),B=I(d,l);oe.heads_key===w&&B.length===1&&Ae(pe(d,l,A(B[0].body_hash)),i.replace)}const R=i.verbatim===!0,m=o?{body:i.body,verbatim:R}:{replace:i.replace,verbatim:R},y=de(d,p,l,m,i.rationale,w,r,L(d));if("stale"in y)throw v(409,"stale_revision",`${g} changed after your read; it is now at revision ${y.stale.revision}`,`Read it again with \`crtr canvas read ${g}\`, then redo your change on the current text.`,{stale:y.stale});const N=A(I(d,l)[0].body_hash);return{dto:{id:l.id,name:g,revision:y.revision,...Q(d,p,N,y.unresolved)},synced:O}}catch(w){C(w,l)}});return T(h),c}u(je,"edit");function Re(t){const e=D(t);if(e.new_name===void 0&&e.owner===void 0)throw b("give a new name, an owner, or both");if(e.new_name!==void 0&&(typeof e.new_name!="string"||!Y.test(e.new_name)))throw b("`new_name` must be segments of letters, digits, `_` or `-` joined by `/`");const{dto:n,change:o}=k(s=>{const a=E(s,t),r=x(s,a,t.params.ref),f=e.owner===void 0?void 0:W(s,a,e.owner,e.cwd);try{const i=fe(s,a,r,{...e.new_name!==void 0?{name:e.new_name}:{},...f?{owner:f}:{}});return{dto:{id:i.id,name:j(s,i)},change:S(s,a,[r,i],e.cwd)}}catch(i){C(i,r)}});return T(o),n}u(Re,"move");function De(t){const{dto:e,change:n}=k(o=>{const s=E(o,t),a=x(o,s,t.params.ref),r=j(o,a),f=S(o,s,[a]);try{ce(o,s,a,L(o))}catch(i){C(i,a)}return{dto:{id:a.id,name:r},change:f}});return T(n),e}u(De,"remove");function Ce(t){const e=t.query.get("diff"),n=e===null?void 0:Number(e);if(n!==void 0&&!(Number.isInteger(n)&&n>0))throw b("`diff` must be a revision number");return k(o=>{const s=E(o,t),a=z(o,s,decodeURIComponent(t.params.ref));if("remote"in a)throw v(404,"not_found",G(a.remote),"Check the name with `crtr canvas search`.");const r="deleted"in a?a.deleted:a;if(r.type!=="document")throw b(`${r.name} is a ${r.type}, not a document`);const f=ue(o,s,r),i={object:ke(o,r),revisions:f.map(c=>({rev_id:c.rev_id,number:c.number,author_id:c.author,rationale:c.rationale,verbatim:c.verbatim,created_at:c.created_at,parents:c.parents}))};if(n!==void 0){const c=f.find(d=>d.number===n);if(!c)throw new _(404,"not_found",`${r.name} has no revision ${n}`);const h=c.parents[0]===void 0?void 0:f.find(d=>d.rev_id===c.parents[0]);i.diff=_e(h?A(h.body_hash):"",A(c.body_hash))}return i})}u(Ce,"revisionHistory");const ee=1e3,ne=1e3,te=3e3,Se=/^When\b.+\bbecause\b.+/,Te=u(t=>{const e=t.trim();return e===""?0:e.split(/\s+/).length},"words");function Ne(t,e){if(t.some(o=>o.on==="boot"&&o.deliver==="content")&&e>ee)return`body is ${e} words, over the ${ee}-word cap for a boot rule at content (the whole body enters every node's context). Keep the core here and move separate subjects into [[linked]] documents.`;const n=[...new Set(t.filter(o=>o.on!=="boot"&&o.deliver==="content").map(o=>o.on))];return n.length>0&&e>ne?`body is ${e} words, over the ${ne}-word cap for a content rule on ${n.join("/")}. Keep the core here and move separate subjects into [[linked]] documents.`:t.some(o=>o.on==="boot"&&o.deliver==="preview")&&e>te?`body is ${e} words, over the ${te}-word cap for a boot rule at preview. Keep the core here and move separate subjects into [[linked]] documents.`:null}u(Ne,"lengthFinding");function Fe(t){const e=D(t);return k(n=>{const o=E(n,t),s=e.owner===void 0?null:W(n,o,e.owner,e.cwd),a=P(n,"SELECT * FROM objects WHERE type = 'document' AND deleted_at IS NULL AND kind != 'report' ORDER BY name").all(),r=[],f=new Map;for(const i of a){if(s&&i.owner!==s.rowid||!K(n,o,i))continue;const c=F(n,i),h=new Set(c.lint_ignore??[]),d=j(n,i),p=u((m,y,N)=>{h.has(m)||r.push({name:d,severity:y,rule:m,message:N})},"add"),l=ge(n,i),g=I(n,i)[0],O=g?A(g.body_hash):"",w=Ne(l,Te(O));w&&p("length","error",w),l.some(m=>m.deliver==="preview")&&!Se.test(i.preview??"")&&p("preview-shape","error",`a rule delivers this document at preview, so the preview must read "When <situation in the reader's work>, read this because <what the reader gains>"`);const R=new Set(P(n,"SELECT written_as FROM edges WHERE type = 'links-to' AND src = ?").all(i.rowid).map(m=>m.written_as));for(const m of U(O))R.has(m)||"remote"in X(n,o,m)||p("links","warning",`[[${m}]] names no object in your view`);if(l.some(m=>m.on==="workspace-open"&&m.deliver==="content")){const m=ae(n,i);m.type==="repo"&&!h.has("workspace-open")&&f.set(m.name,[...f.get(m.name)??[],d])}}for(const[i,c]of f)if(!(c.length<2))for(const h of c)r.push({name:h,severity:"warning",rule:"workspace-open",message:`repo ${i} has ${c.length} documents delivered at content on workspace-open (${c.join(", ")}); keep one and link the rest from it`});return{findings:r}})}u(Fe,"lint");const $=u((t,e=200)=>n=>({status:e,body:t(n)}),"ok"),Qe=[{method:"POST",pattern:"/v1/docs",handler:$(Oe,201)},{method:"PATCH",pattern:"/v1/docs/:ref",handler:$(je)},{method:"POST",pattern:"/v1/docs/:ref/move",handler:$(Re)},{method:"DELETE",pattern:"/v1/docs/:ref",handler:$(De)},{method:"GET",pattern:"/v1/docs/:ref/history",handler:$(Ce)},{method:"POST",pattern:"/v1/docs/lint",handler:$(Fe)}];export{Qe as docRoutes};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
/**
|
|
3
|
+
* Serves one accepted launcher connection. The listener uses `pauseOnConnect` because the request is
|
|
4
|
+
* read with the native `recvFds`, so Node never sees the peer's FIN: ending the reply alone leaves the
|
|
5
|
+
* accepted descriptor open for the life of the process. This closes the connection on every path.
|
|
6
|
+
*
|
|
7
|
+
* `handle` returns the single reply line and calls `requestRead` once the request is read, which
|
|
8
|
+
* resumes the socket so a peer that leaves during a long operation (`wait`) is noticed and destroyed.
|
|
9
|
+
* Destroying happens after the reply flushes, so an async reply is never truncated.
|
|
10
|
+
*/
|
|
11
|
+
export declare function serveAccepted(socket: Socket, handle: (requestRead: () => void) => Promise<string>): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var y=Object.defineProperty;var d=(e,r)=>y(e,"name",{value:r,configurable:!0});function t(e,r){e.on("error",()=>e.destroy());const n=d(o=>e.end(o,()=>e.destroy()),"endReply");r(()=>e.resume()).then(n,()=>e.destroy())}d(t,"serveAccepted");export{t as serveAccepted};
|
package/dist/launcher/main.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var q=Object.defineProperty;var h=(
|
|
2
|
-
`
|
|
3
|
-
`
|
|
1
|
+
var q=Object.defineProperty;var h=(l,d)=>q(l,"name",{value:d,configurable:!0});import{spawn as V}from"node:child_process";import{createHash as z}from"node:crypto";import{chownSync as O,chmodSync as P,closeSync as Q,existsSync as W,mkdirSync as A,readFileSync as X,rmSync as Y,statSync as Z}from"node:fs";import{createServer as ee}from"node:net";import{dirname as H,join as _}from"node:path";import{fileURLToPath as te}from"node:url";import{loadLinuxNative as re}from"../native/linux.js";import{envHomeOverride as oe}from"../shared/env.js";import{launchSandbox as ne}from"./bwrap.js";import{serveAccepted as se}from"./accepted-socket.js";import{installNetworkRule as ie}from"./netrule.js";import{LAUNCHER_SOCKET as v,LaunchRefused as f}from"./spec.js";import{systemIdentity as ae,AppUsers as ce}from"./users.js";import{SourceValidator as de}from"./validate.js";import{prepareDaemonScopeDirectories as ue}from"./scope-directories.js";import{LimitsManager as le}from"./limits/manager.js";import{CRTRD_OOM_SCORE_ADJ as pe}from"./limits/policy.js";import{writeLimitsLog as fe}from"./limits/log.js";const B=re();function U(l){const d=l._handle?.fd;if(d===void 0||d<0)throw new Error("launcher socket fd unavailable");return d}h(U,"socketFd");async function me(){if(process.platform!=="linux"||process.getuid?.()!==0||!B)throw new Error("launcher requires Linux root and its native addon");const{daemonUid:l,daemonGid:d,dataGid:T}=ae(),I=H(v);if(A(I,{recursive:!0,mode:493}),(Z(I).mode&511)!==493)throw new Error(`${I} must be mode 0755`);O(I,l,d),process.umask(7);const G=_(I,"nodes");A(G,{recursive:!0,mode:504}),O(G,0,d),P(G,504);const S=new de(oe()||"/",d);ue(_(S.root,"runtime"),l,d);const b=new ce(T);ie();const p=new le({runtimeRoot:S.runtime,appsRoot:_(S.root,"apps"),stateDir:_(S.runtime,"limits-state"),dataGid:T,users:b});p.init();const L=new Map,$=h(r=>{const u=p.takeEvents();for(const s of u)fe(s);return u.length===0?r:{...r,events:u}},"reply_"),k=new Map,x=new Map,F=h((r,u)=>{const s=u.capture,c=z("sha256"),e=h((a,n)=>{let m=0,w=!1;const R=[];return{take(M){n?.update(M);const C=Math.max(0,Math.min(M.length,a-m));C&&(R.push(M.subarray(0,C)),m+=C),C!==M.length&&(w=!0)},result:h(()=>({text:Buffer.concat(R).toString("utf8"),truncated:w}),"result")}},"chunks"),i=e(s.stdoutBytes,c),t=e(s.stderrBytes);let o=Buffer.alloc(0),y=0;return r.stdout?.on("data",a=>i.take(a)),r.stderr?.on("data",a=>{const n=a;s.stderrTail?(y+=n.length,o=n.length>=s.stderrBytes?n.subarray(n.length-s.stderrBytes):Buffer.concat([o,n]).subarray(Math.max(0,o.length+n.length-s.stderrBytes))):t.take(n)}),new Promise(a=>r.once("close",(n,m)=>{const w=i.result(),R=s.stderrTail?{text:o.toString("utf8"),truncated:y>s.stderrBytes}:t.result();a({exitCode:n,signal:m,stdout:w.text,stderr:R.text,stdoutTruncated:w.truncated,stderrTruncated:R.truncated,stdoutSha256:c.digest("hex")})}))},"captureExit"),J=h(r=>{const u=new Set;for(;r>1&&!u.has(r);){const s=k.get(r);if(s)return s;u.add(r);try{const c=X(`/proc/${r}/status`,"utf8");r=Number(/^PPid:\s*(\d+)/m.exec(c)?.[1]??0)}catch{return null}}return null},"sandboxForPid");let g,E=!1;const j=_(H(te(import.meta.url)),"../daemon/crtrd-cli.js"),D=h(()=>{E||(g=V("/bin/setpriv",["--reuid",String(l),"--regid",String(d),"--groups",String(T),"--",process.execPath,j,...process.argv.slice(2)],{stdio:"inherit",env:{...process.env,CRTR_LAUNCHER_SOCKET:v,CRTR_HOME:S.root}}),g.pid&&p.protectDaemon(g.pid,pe),g.once("exit",()=>{g=void 0,E||D()}),g.once("error",r=>{console.error("crtrd spawn failed:",r),process.exitCode=1,E=!0}))},"startDaemon");W(v)&&Y(v);const N=ee({pauseOnConnect:!0},r=>se(r,u=>{const s=[];return(async()=>{try{if(B.peerCred(U(r)).uid!==l)throw new f("invalid_spec","launcher peer is not crtrd");let c;for(;;)try{c=B.recvFds(U(r));break}catch(t){if(t.code!=="EINTR")throw t}u(),s.push(...c.fds);const e=JSON.parse(c.data);let i;switch(e.operation){case"launch":{if(s.length>1||!b.has(e.spec.uid))throw new f("invalid_uid","app uid is not registered");let t,o;const y=p.prepareRun(e.spec.uid,e.spec.nodeId);try{o=await ne(e.spec,s[0],S,T,a=>{if(!a.pid)throw new f("launch_failed","bwrap has no pid");t=a.pid;const n=a.pid;if(k.set(n,{child:a,uid:e.spec.uid,nodeId:e.spec.nodeId,mounts:e.spec.mounts,plan:y}),a.once("exit",()=>{k.delete(n);const m=p.finishRun(y);L.set(n,m.oomKilled),setTimeout(()=>L.delete(n),5*6e4).unref()}),e.spec.capture){const m=F(a,e.spec).then(w=>({...w,oomKilled:L.get(n)===!0}));x.set(n,m),m.then(()=>{setTimeout(()=>{x.get(n)===m&&x.delete(n)},3e5).unref()})}},y)}catch(a){throw t!==void 0?x.delete(t):p.finishRun(y),a}if(!o.pid)throw new f("launch_failed","bwrap has no pid");p.confirmStarted(y,o.pid),i={pid:o.pid};break}case"wait":{const t=x.get(e.pid);if(!t)throw new f("invalid_spec","unknown captured sandbox pid");const o=await t;x.delete(e.pid),i={result:o};break}case"stop":{const t=Number.isInteger(e.pid)?J(e.pid):null;if(!t||e.nodeId!==void 0&&t.nodeId!==e.nodeId)throw new f("invalid_spec","unknown sandbox pid");if(e.signal!==void 0&&e.signal!=="SIGTERM"&&e.signal!=="SIGKILL")throw new f("invalid_spec","invalid stop signal");const o=e.signal??"SIGKILL";t.child.kill(o),o==="SIGKILL"&&p.killRun(t.plan),i={ok:!0};break}case"user.create":i={uid:b.create(e.app,e.uid)};break;case"user.delete":{if([...k.values()].some(t=>t.uid===e.uid))throw new f("invalid_spec","stop this app\u2019s sandboxes before deleting its user");b.delete(e.uid),i={ok:!0};break}case"users.sync":b.sync(e.users),i={ok:!0};break;case"list":i={sandboxes:[...k].map(([t,o])=>({pid:t,uid:o.uid,nodeId:o.nodeId,mounts:o.mounts}))};break;case"limits.apply":{const t=b.entries().find(o=>o.app===e.app.replace(/^app:/,""));if(!t)throw new f("invalid_uid","app is not registered");p.applyApp(t.app,t.uid,!0),i={ok:!0};break}case"limits.show":i={limits:p.show()};break;case"cache.purge":i={purged:await p.cachePass()};break;case"exit.status":i={oomKilled:L.get(e.pid)===!0};break;case"daemon.handover":g?.kill("SIGTERM"),i={ok:!0};break;default:throw new f("invalid_spec","unknown launcher operation")}return`${JSON.stringify($(i))}
|
|
2
|
+
`}catch(c){const e=c instanceof f?c.refusal:"launch_failed";return console.error(`launcher refusal ${e}:`,c),`${JSON.stringify({refusal:e})}
|
|
3
|
+
`}finally{for(const c of s)Q(c)}})()}));await new Promise((r,u)=>{N.once("error",u),N.listen(v,r)}),O(v,l,d),P(v,384);const K=h(()=>{E=!0,g?.kill("SIGTERM");for(const{child:r}of k.values())r.kill("SIGKILL");N.close(),S.close()},"shutdown");process.once("SIGTERM",K),process.once("SIGINT",K),D()}h(me,"runLauncher"),await me().catch(l=>{console.error(l),process.exitCode=1});export{me as runLauncher};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var R=Object.defineProperty;var h=(n,t)=>R(n,"name",{value:t,configurable:!0});import{chownSync as P,chmodSync as $,closeSync as l,constants as _,lstatSync as
|
|
1
|
+
var R=Object.defineProperty;var h=(n,t)=>R(n,"name",{value:t,configurable:!0});import{chownSync as P,chmodSync as $,closeSync as l,constants as _,lstatSync as f,mkdirSync as E,openSync as L,realpathSync as A}from"node:fs";import{isAbsolute as G,join as e,relative as T,resolve as j,sep as S}from"node:path";import{loadLinuxNative as q,O_PATH as x,RESOLVE_BENEATH as z,RESOLVE_NO_SYMLINKS as V}from"../native/linux.js";import{APP_UID_MAX as W,APP_UID_MIN as Z,LaunchRefused as s}from"./spec.js";import{systemIdentity as b}from"./users.js";const a=q(),v=x|_.O_NOFOLLOW,g=z|V,M=32768,F=16384,I=49152;function c(n,t){return n===t||n.startsWith(t==="/"?"/":`${t}${S}`)}h(c,"within");function N(n){if(!G(n)||j(n)!==n||n.includes("\0")||n.split(S).includes(".."))throw new s("invalid_spec",`non-canonical source: ${n}`)}h(N,"assertPath");class Q{static{h(this,"SourceValidator")}daemonGid;socketDir;rootFd;runtimeDev;runtimeIno;runtimeFd;root;runtime;constructor(t,d=b().daemonGid,u="/run/crtr"){if(this.daemonGid=d,this.socketDir=u,!a)throw new Error("Linux native addon is required by the root launcher");this.root=A(t),this.runtime=A(e(this.root,"runtime")),this.rootFd=L("/",v),this.runtimeFd=L(this.runtime,v);const r=a.fstat(this.runtimeFd);this.runtimeDev=r.dev,this.runtimeIno=r.ino}close(){l(this.rootFd),l(this.runtimeFd)}openLog(t){if(!a)throw new Error("Linux native addon is required");return a.openat2(this.runtimeFd,`nodes/${t}/job/broker.log`,_.O_WRONLY|_.O_APPEND|_.O_NOFOLLOW,g)}isRuntimeAncestor(t){if(!a)throw new Error("Linux native addon is required");const d=t.split("/").filter(Boolean);for(let u=1;u<=d.length;u++){let r;try{r=a.openat2(this.rootFd,d.slice(0,u).join("/"),v,g)}catch{return!1}try{const m=a.fstat(r);if(m.dev===this.runtimeDev&&m.ino===this.runtimeIno)return!0}finally{l(r)}}return!1}pin(t){if(!a)throw new Error("Linux native addon is required");if(!Number.isInteger(t.uid)||t.uid<Z||t.uid>W)throw new s("invalid_uid","uid outside app range");if(!/^[a-zA-Z0-9][a-zA-Z0-9-]{0,127}$/.test(t.nodeId)||!Array.isArray(t.mounts)||!Array.isArray(t.command)||!t.command.length||!t.command.every(i=>typeof i=="string"&&i.length>0&&!i.includes("\0"))||!t.env||Object.entries(t.env).some(([i,o])=>!/^[A-Za-z_][A-Za-z0-9_]*$/.test(i)||typeof o!="string"||o.includes("\0")))throw new s("invalid_spec","invalid node id, command, environment or mount list");const d=e(this.socketDir,"nodes",t.nodeId),u=e(this.socketDir,"nodes");if(E(u,{recursive:!0,mode:493}),f(u).uid!==0||f(u).gid!==this.daemonGid||!f(u).isDirectory()||(f(u).mode&511)!==504)throw new s("run_source","nodes directory is not root-and-daemon-controlled");E(d,{recursive:!0,mode:504});const r=f(d);if(!r.isDirectory())throw new s("run_source","run folder is not a directory");if(r.uid===0||r.uid===b().daemonUid&&r.gid===this.daemonGid&&(r.mode&511)===504)P(d,t.uid,this.daemonGid),$(d,504);else if(r.uid!==t.uid||r.gid!==this.daemonGid||(r.mode&511)!==504)throw new s("run_source","run folder has wrong owner or mode");const m=[];try{for(const i of t.mounts){if(!i||typeof i.source!="string"||typeof i.writable!="boolean")throw new s("invalid_spec","invalid mount");const o=i.source;N(o);const p=t.pluginRoot===o&&!i.writable&&c(o,e(this.runtime,"plugins"))&&o!==e(this.runtime,"plugins");if(!p&&(c(o,this.runtime)||this.isRuntimeAncestor(o)))throw new s("runtime_source",o);const O=o===e(this.socketDir,"api.sock")||o===e(this.socketDir,"store.sock")||c(o,e(this.socketDir,"bin"))||o===d;if(c(o,this.socketDir)&&!O)throw new s("run_source",o);if(i.writable&&!c(o,e(this.root,"user"))&&!c(o,e(this.root,"apps"))&&o!==d&&o!==e(this.socketDir,"api.sock")&&o!==e(this.socketDir,"store.sock"))throw new s("writable_source",o);if(!c(o,e(this.root,"user"))&&!c(o,e(this.root,"apps"))&&!c(o,this.socketDir)&&!U(o)&&!p)throw new s("invalid_spec",`unknown read-only source: ${o}`);let w;try{w=a.openat2(this.rootFd,T("/",o)||".",v,g)}catch(k){const D=k.code;throw new s(D==="ENOENT"?"source_missing":D==="ELOOP"?"symlink_source":"launch_failed",`${o}: ${String(k)}`)}const y=a.fstat(w).mode&61440;if(![M,F,I].includes(y)||y===I&&!O)throw l(w),new s(y===40960?"symlink_source":"source_type",o);m.push({source:o,writable:i.writable,fd:w,kind:y})}if(N(t.cwd),!m.some(i=>i.kind===F&&c(t.cwd,i.source)))throw new s("invalid_spec","cwd is not mounted");return m}catch(i){for(const o of m)l(o.fd);throw i}}}function U(n){return["/usr","/bin","/lib","/lib64","/opt/crouter","/etc/passwd","/etc/group","/etc/resolv.conf","/etc/hosts","/etc/ssl","/etc/ca-certificates"].some(t=>c(n,t))}h(U,"isSystemSource");export{Q as SourceValidator};
|
package/docs/release-local.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use this only for a release explicitly authorized by Silas. `.github/workflows/publish.yml` is the source of truth; this is its manual path when Actions cannot run. Work on clean, current `main`, with `npm whoami` showing the publishing account. Verify the code first (`npm run build:api`, `npm run build`, `npx tsc --noEmit`, relevant `npm run test:isolated -- …`). Do not start a release while another agent owns uncommitted changes in this shared checkout.
|
|
4
4
|
|
|
5
|
-
1. Check the six latest registry versions with `npm view <name> version` for each of
|
|
5
|
+
1. Check the six latest registry versions with `npm view <name> version` for each of `@north-light/cli`, `@north-light/api`, `@north-light/env-docker`, `@north-light/identity`, `@north-light/sdk` and `@north-light/plugin`. Fetch `origin/main`, ensure the release commit is current, and choose a patch version greater than every published version. Do **not** use the workflow's `git reset --hard` in a shared checkout.
|
|
6
6
|
2. Run `node scripts/release-version.mjs "$VERSION"`. It sets all six manifests to `$VERSION`, pins every dependency between them (root → identity, SDK → api and identity, plugin → api and identity) to exactly `$VERSION` with no `^`, writes `SDK_VERSION` in `packages/crouter-sdk/src/version.ts` (informational: compatibility is the contract level), and renames the `## Next release` section of `docs/sdk/release-notes.md` to `## $VERSION`. One release is one version set: an app pinning `@north-light/sdk@$VERSION` gets exactly this release's api and identity. Run `npm install --package-lock-only --ignore-scripts`, then `node scripts/contract.mjs release`: it fails on an unacknowledged breaking `/v1` change or an undeclared watched file (the same check CI runs), then writes `contract/surface.txt`, `CONTRACT_LEVEL` in `src/api/contract.ts` (a minor bump only when the surface changed or a declaration says `bump`), `crouterContract` in the SDK manifest, and resets `contract/next.json`. Run `node scripts/release-version.mjs --check` (prints `one version set`). Check `git diff` and commit only the six manifests, `packages/crouter-sdk/src/version.ts`, `docs/sdk/release-notes.md`, `package-lock.json`, `contract/surface.txt`, `contract/next.json` and `src/api/contract.ts` as `chore: release v$VERSION`. Create `git tag -a "v$VERSION" -m "chore: release v$VERSION"`, then `git push --atomic origin HEAD:main "refs/tags/v$VERSION"`. If rejected due to a concurrent push, remove the *local* release tag, integrate fresh main, then recheck versions and retry; never push a tag separately.
|
|
7
7
|
3. From the tagged release commit run `npm ci`. Publish in dependency order, checking `npm view "$PACKAGE@$VERSION" version` before each and skipping only on a positive sighting: `npm run build:identity` then `npm publish` in `packages/crouter-identity`; `npm run build` then `CRTR_PACK_MINIFY=1 npm publish` at root (Docker must be running: the pack cross-builds the Linux x64 and arm64 native addons into `dist/native/linux/<arch>/addon.node` and fails the release if it cannot); `npm run build:api` then publish in `packages/crouter-api`; `npm run build:env-docker` then publish in `packages/crouter-env-docker`; `npm run build:api && npm run build:identity && npm run build:sdk` then `npm publish --tag next` in `packages/crouter-sdk` (its `prepublishOnly` guard refuses any other form, including `--tag latest`; every SDK release lands on `next`); `npm run build:api && npm run build:identity && npm run build:plugin` then `CRTR_PACK_MINIFY=1 npm publish` in `packages/crouter-plugin`. Use npm's interactive authentication/2FA where required (see `personal-operations/npm-2fa-cli`); a read-only token cannot publish. Provenance remains off because the source repository is private. Do not build or publish the Docker image in this no-Actions path.
|
|
8
8
|
4. Confirm all six exact versions with `npm view`. In a fresh temporary directory, `npm init -y && npm install @north-light/sdk@$VERSION`, then prove both `@north-light/sdk/errors` (`errorCodes`) and `@north-light/sdk/stores/postgres` resolve via `node --input-type=module -e '…'`. Record any partial publish and finish it from the exact release tag rather than cutting another version: `gh workflow run publish.yml -f release_tag=v$VERSION` runs `recover-release`, which verifies the tag is a release commit on main, checks it out, and publishes only the packages and image the registry does not yet show (SDK to `next`; it never writes the image `:latest`, so if the recovered tag is the newest release, `:latest` follows the next normal release).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.490",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"test:runtime-packaging": "node --test scripts/runtime-smoke.test.mjs scripts/pack-args.test.mjs scripts/ensure-native-linux.test.mjs scripts/release-version.test.mjs",
|
|
100
100
|
"contract:check": "node scripts/contract.mjs check",
|
|
101
101
|
"contract:surface": "node scripts/contract.mjs generate",
|
|
102
|
+
"docs:check-reference": "npm exec --workspace @north-light/docs -- tsx scripts/check-reference.ts",
|
|
102
103
|
"test:contract": "node --conditions=crtr-src --import tsx/esm --test scripts/contract.test.mjs scripts/promote-sdk.test.mjs scripts/release-version.test.mjs scripts/sdk-docs-commands.test.mjs scripts/ci-image.test.mjs scripts/ci-miss-triage.test.mjs scripts/ci-test-pick.test.mjs",
|
|
103
104
|
"test:build-output": "node scripts/build-concurrency-test.mjs",
|
|
104
105
|
"check:cli-no-canvas": "node scripts/check-cli-no-canvas.mjs",
|
|
@@ -137,7 +138,7 @@
|
|
|
137
138
|
},
|
|
138
139
|
"license": "UNLICENSED",
|
|
139
140
|
"dependencies": {
|
|
140
|
-
"@north-light/identity": "0.3.
|
|
141
|
+
"@north-light/identity": "0.3.490",
|
|
141
142
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
142
143
|
"@earendil-works/pi-ai": "1.1.0",
|
|
143
144
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.490",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.490",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@earendil-works/pi-ai": "1.1.0",
|
|
19
19
|
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
20
20
|
"@earendil-works/pi-tui": "1.1.0",
|
|
21
|
-
"@north-light/identity": "0.3.
|
|
21
|
+
"@north-light/identity": "0.3.490",
|
|
22
22
|
"cron-parser": "^5.6.0",
|
|
23
23
|
"esbuild": "^0.27.7",
|
|
24
24
|
"jose": "^6.2.12",
|
|
@@ -12116,17 +12116,17 @@
|
|
|
12116
12116
|
},
|
|
12117
12117
|
"packages/crouter-api": {
|
|
12118
12118
|
"name": "@north-light/api",
|
|
12119
|
-
"version": "0.3.
|
|
12119
|
+
"version": "0.3.490",
|
|
12120
12120
|
"license": "UNLICENSED"
|
|
12121
12121
|
},
|
|
12122
12122
|
"packages/crouter-env-docker": {
|
|
12123
12123
|
"name": "@north-light/env-docker",
|
|
12124
|
-
"version": "0.3.
|
|
12124
|
+
"version": "0.3.490",
|
|
12125
12125
|
"license": "UNLICENSED"
|
|
12126
12126
|
},
|
|
12127
12127
|
"packages/crouter-identity": {
|
|
12128
12128
|
"name": "@north-light/identity",
|
|
12129
|
-
"version": "0.3.
|
|
12129
|
+
"version": "0.3.490",
|
|
12130
12130
|
"license": "UNLICENSED",
|
|
12131
12131
|
"dependencies": {
|
|
12132
12132
|
"jose": "^6.2.1"
|
|
@@ -12134,20 +12134,20 @@
|
|
|
12134
12134
|
},
|
|
12135
12135
|
"packages/crouter-plugin": {
|
|
12136
12136
|
"name": "@north-light/plugin",
|
|
12137
|
-
"version": "0.3.
|
|
12137
|
+
"version": "0.3.490",
|
|
12138
12138
|
"license": "UNLICENSED",
|
|
12139
12139
|
"dependencies": {
|
|
12140
|
-
"@north-light/api": "0.3.
|
|
12141
|
-
"@north-light/identity": "0.3.
|
|
12140
|
+
"@north-light/api": "0.3.490",
|
|
12141
|
+
"@north-light/identity": "0.3.490"
|
|
12142
12142
|
}
|
|
12143
12143
|
},
|
|
12144
12144
|
"packages/crouter-sdk": {
|
|
12145
12145
|
"name": "@north-light/sdk",
|
|
12146
|
-
"version": "0.3.
|
|
12146
|
+
"version": "0.3.490",
|
|
12147
12147
|
"license": "UNLICENSED",
|
|
12148
12148
|
"dependencies": {
|
|
12149
|
-
"@north-light/api": "0.3.
|
|
12150
|
-
"@north-light/identity": "0.3.
|
|
12149
|
+
"@north-light/api": "0.3.490",
|
|
12150
|
+
"@north-light/identity": "0.3.490",
|
|
12151
12151
|
"jose": "^6.2.1"
|
|
12152
12152
|
},
|
|
12153
12153
|
"bin": {
|