@ai-ecoverse/slicc-man 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,8 @@ In SLICC, the package declares its commands in `slicc.commands`, so installing i
12
12
  pnpm add -g @ai-ecoverse/slicc-man
13
13
  ```
14
14
 
15
+ They are one self-contained ES module run by the kernel's JS ABI (`"abi": "js"`), so they need no Node in SLICC, and `engines.slicc-kernel` asks for slicc-kernel 1.91.0 or later, the oldest release the integration tests pass on (an older kernel's `fetch` can't reach `host.slicc.internal`, which they use).
16
+
15
17
  On macOS and Linux (get.sliccy.ai, once live):
16
18
 
17
19
  ```sh
@@ -140,7 +142,7 @@ Each site has its own directory in the cache, named by the base URL's host and p
140
142
 
141
143
  ## Fallback
142
144
 
143
- In SLICC only: when a topic has no page and is a command on `PATH`, `man` prints `No manual entry for <topic>; showing <topic> --help` to stderr, then the output of `<topic> --help` (stdout and stderr together, as plain text, through the pager when stdout is a terminal), and that topic counts as found, so `man` can exit 0. seven's own commands often have no page yet. The fallback doesn't apply to a section other than 1, `-w`, `-k`, `-f` or `-l`. On a host there is no fallback; the host has its own `man`.
145
+ In SLICC only: when a topic has no page and is a command on `PATH`, `man` prints `No manual entry for <topic>; showing <topic> --help` to stderr, then the output of `<topic> --help` (stdout and stderr together, as plain text with its escape sequences and other control characters dropped, through the pager when stdout is a terminal), and that topic counts as found, so `man` can exit 0. seven's own commands often have no page yet. The fallback doesn't apply to a section other than 1, `-w`, `-k`, `-f` or `-l`. On a host there is no fallback; the host has its own `man`.
144
146
 
145
147
  ## `--upgrade`
146
148
 
@@ -171,12 +173,12 @@ npm run test:unit
171
173
  ```
172
174
 
173
175
  - `npm run lint`: Biome, the no-comment check and the other [slicc-shared-web](https://github.com/ai-ecoverse/slicc-shared-web) checks, then `shellcheck -s sh` and `dash -n` on `install.sh`.
174
- - `npm test`: the integration tests in `test/integration/`. `host.test.mjs` runs `scman` (in-process, so a terminal can be faked, and as `host/scman.mjs`) against a local HTTP server on an ephemeral port standing in for www.sliccy.com (`SLICC_MAN_URL`), serving copies of the site's `tree.md` and `man.md` (`pages.json`, with their expected renderings), with a temporary cache and fake tools, `less` among them, on a temporary `PATH`; only CI touches the real site. `install.test.mjs` runs `install.sh` on a tarball packed from the tree, with the Node on `PATH` and with a Node download from a fake nodejs.org, and `--upgrade` against a release served from a local server; `install.ps1` is checked statically, and run when `pwsh` is on `PATH`. `repo.test.mjs` checks the workflow, the release config, the package's shape, what `npm pack` ships and the repo rules.
176
+ - `npm test`: the integration tests in `test/integration/`. `slicc.test.mjs` installs the files `npm pack` ships into a headless slicc-kernel in Node (`createNodeKernel`, with wasm-bash and wasm-coreutils), and runs `man`, `apropos`, `whatis` and `scman` there against a local HTTP server on an ephemeral port, reached through the kernel's network transport as `http://host.slicc.internal:<port>` (`SLICC_MAN_URL`), with fake `less` and `hello` commands on `PATH` and a terminal from `openTerminal` for the pager. `host.test.mjs` runs `scman` (in-process, so a terminal can be faked, and as `host/scman.mjs`) against a local HTTP server on an ephemeral port standing in for www.sliccy.com (`SLICC_MAN_URL`), serving copies of the site's `tree.md` and `man.md` (`pages.json`, with their expected renderings), with a temporary cache and fake tools, `less` among them, on a temporary `PATH`; only CI touches the real site. `install.test.mjs` runs `install.sh` on a tarball packed from the tree, with the Node on `PATH` and with a Node download from a fake nodejs.org, and `--upgrade` against a release served from a local server; `install.ps1` is checked statically, and run when `pwsh` is on `PATH`. `repo.test.mjs` checks the workflow, the release config, the package's shape, what `npm pack` ships and the repo rules.
175
177
  - `npm run test:unit`: the unit tests in the gitignored `test/unit/`, with Node's coverage in `coverage/unit/lcov.info`. The pre-commit hook wants 100% coverage of the changed lines.
176
178
 
177
- CI is `.github/workflows/ci.yml`, the only workflow, which runs [slicc-shared-web's pipeline](https://github.com/ai-ecoverse/slicc-shared-web#pipeline) at the devDependency's version, on Linux only: `npm run lint`, then `npm test`, then on a pull request the Claude and Codex reviews. The merge queue requires `ci / integration` and `ci / review`. On a push to `main`, semantic-release releases and publishes to npm through trusted publishing, with provenance from `ci.yml`, and attaches `slicc-man-<version>.tgz`, `SHA256SUMS`, `install.sh` and `install.ps1` to the GitHub release (`scripts/assets.mjs` renames the tarball and writes `SHA256SUMS`).
179
+ CI is `.github/workflows/ci.yml`, the only workflow, which runs [slicc-shared-web's pipeline](https://github.com/ai-ecoverse/slicc-shared-web#pipeline) at the devDependency's version, on Linux only, with Node 26, which the headless kernel needs: `npm run lint`, then `npm test`, then on a pull request the Claude and Codex reviews. The merge queue requires `ci / integration` and `ci / review`. On a push to `main`, semantic-release releases and publishes to npm through trusted publishing, with provenance from `ci.yml`, and attaches `slicc-man-<version>.tgz`, `SHA256SUMS`, `install.sh` and `install.ps1` to the GitHub release (`scripts/assets.mjs` renames the tarball and writes `SHA256SUMS`).
178
180
 
179
- The code is in `lib/` (options, the Markdown renderer, the cache and index, apropos and whatis, shared with SLICC) and `host/` (`scman.mjs`, the cache directory, the pager, `--upgrade`).
181
+ The code is in `lib/` (options, the Markdown renderer, the cache and index, apropos and whatis, shared with SLICC), `host/` (`scman.mjs`, the cache directory, the pager, `--upgrade`) and `slicc/` (`main.mjs`, the JS ABI entry: the cache in `~/.cache/slicc-man` through the kernel's file system, `fetch`, the pager, the fallback and `--upgrade`). `scripts/bundle.mjs` bundles `slicc/main.mjs` and `lib/` with esbuild into `dist/slicc.mjs`, the module `slicc.commands` names, with only the version of `package.json` in it. `dist/` is a build output, not in git: `npm pack` and `npm publish` build it (`prepack`), so a release always ships a bundle of its own sources, and `slicc.test.mjs` and `repo.test.mjs` check what `npm pack` ships.
180
182
 
181
183
  ## License
182
184
 
package/dist/slicc.mjs ADDED
@@ -0,0 +1,59 @@
1
+ var punctuation=/^[!-/:-@[-`{-~]$/,fence=/^( {0,3})(`{3,}|~{3,})(.*)$/,closing=/^ {0,3}(`{3,}|~{3,})[ \t]*$/,atx=/^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/,rule=/^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/,grid=/^ {0,3}\+[-=:+]*\+[ \t]*$/,delimiter=/^ {0,3}\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/,quote=/^ {0,3}> ?/,item=/^( {0,3})([-+*]|\d{1,9}[.)])(?:([ \t]+)(.*))?$/,setext=/^ {0,3}(=+|-+)[ \t]*$/,autolink=/^<([a-zA-Z][a-zA-Z0-9+.-]{1,31}:[^\s<>]*|[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+)>/,reference=/^&(?:#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6})|([a-zA-Z]+));/,entities={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:"\xA0"};function expand(line2){let out="";for(let ch of line2)out+=ch===" "?" ".repeat(4-out.length%4):ch;return out}function clean(text){return String(text).replace(/\r\n?/g,`
2
+ `).split(`
3
+ `).map(expand).join(`
4
+ `).replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g,"")}var blank=line2=>/^[ \t]*$/.test(line2),indentOf=line2=>line2.length-line2.trimStart().length;function starts(line2){return fence.test(line2)||atx.test(line2)||rule.test(line2)||quote.test(line2)||grid.test(line2)||/^ {0,3}[-+*][ \t]+\S/.test(line2)||/^ {0,3}1[.)][ \t]+\S/.test(line2)}function cells(line2){let text=line2.trim();return text.startsWith("|")&&(text=text.slice(1)),text.endsWith("|")&&!text.endsWith("\\|")&&(text=text.slice(0,-1)),text.split(/(?<!\\)\|/).map(cell=>cell.replaceAll("\\|","|").trim())}function matcher(first){let bullet=/^[-+*]$/.test(first[2]),kind=bullet?first[2]:first[2].slice(-1);return line2=>{let match=line2===void 0?null:item.exec(line2);return!match||rule.test(line2)?void 0:(bullet?match[2]===kind:/\d/.test(match[2])&&match[2].endsWith(kind))?match:void 0}}function entry(lines,at,match){let spaces=match[3]??"",wide=spaces.length>4,width2=match[1].length+match[2].length+(wide?1:spaces.length||1),body=[wide?spaces.slice(1)+match[4]:match[4]??""],i=at+1,gap=!1;for(;i<lines.length;){let line2=lines[i];if(blank(line2))body.push("");else if(indentOf(line2)>=width2)body.at(-1)===""&&(gap=!0),body.push(line2.slice(width2));else if(body.at(-1)!==""&&!starts(line2)&&!item.test(line2))body.push(line2);else break;i++}let trailing=0;for(;body.length>1&&body.at(-1)==="";)body.pop(),trailing++;return{item:{marker:match[2],blocks:blocks(body)},next:i,gap,trailing}}function list(lines,start){let same=matcher(item.exec(lines[start])),items=[],loose=!1,i=start,match=same(lines[i]);for(;match;){let found=entry(lines,i,match);items.push(found.item),loose||=found.gap,i=found.next,match=same(lines[i]),found.trailing>0&&match&&(loose=!0),found.trailing>0&&!match&&(i-=found.trailing)}return{block:{type:"list",items,loose},next:i}}function fenced(lines,i){let open=fence.exec(lines[i]);if(!open||open[2][0]==="`"&&open[3].includes("`"))return;let body=[],j=i+1;for(;j<lines.length;){let close=closing.exec(lines[j]);if(close&&close[1][0]===open[2][0]&&close[1].length>=open[2].length){j++;break}body.push(lines[j].slice(Math.min(indentOf(lines[j]),open[1].length))),j++}return{block:{type:"code",lines:body},next:j}}function heading(lines,i){let found=atx.exec(lines[i]);if(found)return{block:{type:"heading",level:found[1].length,text:found[2]??""},next:i+1}}function ruled(lines,i){return rule.test(lines[i])?{block:{type:"rule"},next:i+1}:void 0}function gridded(lines,i){if(!grid.test(lines[i]))return;let body=[],j=i;for(;j<lines.length&&/^ {0,3}[+|]/.test(lines[j]);)body.push(lines[j++].trim());let rows=[];for(let row of body)row.startsWith("+")?rows.push([]):rows.at(-1).push(row);let head=rows.find(row=>row.length>0)??[];return{block:{type:"grid",lines:body,first:head.map(row=>cells(row).join(" "))},next:j}}function piped(lines,i){if(!lines[i].includes("|")||!delimiter.test(lines[i+1]??""))return;let rows=[cells(lines[i])],columns2=cells(lines[i+1]).length,j=i+2;for(;j<lines.length&&!blank(lines[j])&&lines[j].includes("|")&&!starts(lines[j]);)rows.push(cells(lines[j++]));return{block:{type:"table",rows,columns:columns2},next:j}}function quoted(lines,i){if(!quote.test(lines[i]))return;let body=[],j=i;for(;j<lines.length&&!blank(lines[j]);){if(quote.test(lines[j]))body.push(lines[j].replace(quote,""));else{if(starts(lines[j]))break;body.push(lines[j])}j++}return{block:{type:"quote",blocks:blocks(body)},next:j}}function listed(lines,i){let match=item.exec(lines[i]);if(!(!match||match[3]===void 0&&!/^ {0,3}[-+*]$/.test(lines[i])))return list(lines,i)}function indented(lines,i){if(!/^ {4}/.test(lines[i]))return;let body=[],j=i;for(;j<lines.length&&(/^ {4}/.test(lines[j])||blank(lines[j]));)body.push(lines[j++].slice(4));for(;blank(body.at(-1));)body.pop();return{block:{type:"code",lines:body},next:j}}function paragraph(lines,i){let body=[lines[i]],j=i+1;for(;j<lines.length&&!blank(lines[j]);){let under=setext.exec(lines[j]);if(under){let text2=body.map(line2=>line2.trim()).join(" ");return{block:{type:"heading",level:under[1][0]==="="?1:2,text:text2},next:j+1}}if(starts(lines[j]))break;body.push(lines[j++])}return{block:{type:"paragraph",text:body.map(line2=>line2.trimStart()).join(`
5
+ `)},next:j}}var parsers=[fenced,heading,ruled,gridded,piped,quoted,listed,indented,paragraph];function blocks(lines){let out=[],i=0;for(;i<lines.length;){if(blank(lines[i])){i++;continue}for(let parser of parsers){let found=parser(lines,i);if(found){out.push(found.block),i=found.next;break}}}return out}function parse(markdown){return blocks(clean(markdown).split(`
6
+ `))}function entity(text,at){let match=reference.exec(text.slice(at));if(!match)return;let value=entities[match[3]?.toLowerCase()];if(match[1]!==void 0&&(value=String.fromCodePoint(Math.min(Number(match[1]),1114111))),match[2]!==void 0&&(value=String.fromCodePoint(Math.min(Number.parseInt(match[2],16),1114111))),value!==void 0)return{value:clean(value),length:match[0].length}}function ticks(text,at){let n=0;for(;text[at+n]==="`";)n++;return n}function span(text,at){let n=ticks(text,at),j=at+n;for(;j<text.length;){let m=ticks(text,j);if(m===n){let code=text.slice(at+n,j).replace(/\n/g," ");return/^ .*[^ ].* $|^ [^ ] $/.test(code)&&(code=code.slice(1,-1)),{code,end:j+m}}j+=Math.max(m,1)}return{end:at+n}}function skip(text,at){return text[at]==="\\"?at+2:text[at]==="`"?span(text,at).end:at+1}function bracket(text,at){let depth=0;for(let j=at;j<text.length;j=skip(text,j))if(text[j]==="["&&depth++,text[j]==="]"&&--depth===0)return j;return-1}function target(text,at){if(text[at]==="["){let close=text.indexOf("]",at);return close===-1?-1:close+1}if(text[at]!=="(")return-1;let depth=0;for(let j=at;j<text.length;j+=text[j]==="\\"?2:1)if(text[j]==="("&&depth++,text[j]===")"&&--depth===0)return j+1;return-1}var space=ch=>ch===void 0||/\s/u.test(ch),word=ch=>ch!==void 0&&/[\p{L}\p{N}]/u.test(ch),mark=ch=>ch!==void 0&&/[\p{P}\p{S}]/u.test(ch);function opens(text,at,n){let before=text[at-1],after=text[at+n];return space(after)||mark(after)&&!space(before)&&!mark(before)?!1:!(text[at]==="_"&&word(before))}function closes(text,at,n){let before=text[at-1],after=text[at+n];return space(before)||mark(before)&&!space(after)&&!mark(after)?!1:!(text[at]==="_"&&word(after))}function link(text,at){if(text[at]!=="["||text[at-1]==="!")return-1;let end=bracket(text,at);return end===-1?-1:target(text,end+1)}function closer(text,at,ch,n){let j=at;for(;j<text.length;){if(text[j]===ch){let m=run(text,j,ch);if(m>=n&&closes(text,j,m))return j;j+=m;continue}let after=link(text,j);j=after===-1?skip(text,j):after}return-1}function run(text,at,ch){let n=0;for(;text[at+n]===ch;)n++;return n}function escaped(text,i,state){let next=text[i+1];return next===`
7
+ `?state.br(i+2):next!==void 0&&punctuation.test(next)?state.add(next,i+2):state.add("\\",i+1)}function coded(text,i,state){let found=span(text,i);return found.code===void 0?state.add(text.slice(i,found.end),found.end):state.push({text:found.code,style:state.style|1},found.end)}function entityRef(text,i,state){let found=entity(text,i);return found?state.add(found.value,i+found.length):state.add("&",i+1)}function autolinked(text,i,state){let found=autolink.exec(text.slice(i));return found?state.add(found[1],i+found[0].length):state.add("<",i+1)}function imaged(text,i,state){return text[i+1]==="["?linked(text,i+1,state,i):state.add("!",i+1)}function linked(text,i,state,from=i){let end=bracket(text,i),after=end===-1?-1:target(text,end+1);return after===-1?state.add(text.slice(from,i+1),i+1):(state.flush(),inline(text.slice(i+1,end),state.style,state.out),after)}function emphasized(text,i,state){let ch=text[i],n=run(text,i,ch),use=Math.min(n,3),end=-1;for(;use>0&&end===-1;use--)end=opens(text,i+n-use,use)?closer(text,i+n,ch,use):-1;if(use++,end===-1)return state.add(ch.repeat(n),i+n);state.add(ch.repeat(n-use),i),state.flush();let flag=[0,2,1,3][use];return inline(text.slice(i+n,end),state.style|flag,state.out),end+use}function newline(text,i,state){return/ {2,}$/.test(state.buffer)?(state.buffer=state.buffer.replace(/ +$/,""),state.br(i+1)):state.add(" ",i+1)}var handlers={"\\":escaped,"`":coded,"&":entityRef,"<":autolinked,"!":imaged,"[":linked,"*":emphasized,_:emphasized,"\n":newline};function inline(text,style=0,out=[]){let state={style,out,buffer:"",flush(){state.buffer!==""&&out.push({text:state.buffer,style}),state.buffer=""},add(value,next){return state.buffer+=value,next},push(value,next){return state.flush(),out.push(value),next},br(next){return state.push({text:`
8
+ `,style:0,br:!0},next)}},i=0;for(;i<text.length;){let handler=handlers[text[i]];i=handler?handler(text,i,state):state.add(text[i],i+1)}return state.flush(),out}function plain(runs){return runs.map(run3=>run3.br?" ":run3.text).join("").replace(/\s+/g," ").trim()}var MAN_UNSUPPORTED_SHORT="tTHXZMmLCdDWcuepREKr",MAN_UNSUPPORTED_LONG=["troff","troff-device","html","gxditview","ditroff","manpath","systems","locale","config-file","debug","default","where-cat","catman","update","extension","preprocessor","recode","encoding","global-apropos","prompt","regex","wildcard","names-only","no-subpages"],SEARCH_UNSUPPORTED_SHORT="MmLCdv",SEARCH_UNSUPPORTED_LONG=["manpath","systems","locale","config-file","debug","verbose"],common={h:"help",V:"version"},commonLong={help:"help",version:"version",upgrade:"upgrade"},tables={man:{short:{...common,k:"apropos",f:"whatis",a:"all",w:"where",l:"local"},shortExtra:{i:"ignore",I:"match"},values:{s:"sections",P:"pager"},long:{...commonLong,apropos:"apropos",whatis:"whatis",all:"all",where:"where",path:"where",location:"where","local-file":"local","ignore-case":"ignore","match-case":"match",nh:"nh","no-hyphenation":"nh",nj:"nj","no-justification":"nj"},longValues:{sections:"sections",pager:"pager"},unsupportedShort:MAN_UNSUPPORTED_SHORT,unsupportedLong:MAN_UNSUPPORTED_LONG},apropos:{short:{...common,r:"regex",w:"wildcard",e:"exact",a:"and",l:"long"},shortExtra:{},values:{s:"sections"},long:{...commonLong,regex:"regex",wildcard:"wildcard",exact:"exact",and:"and",long:"long"},longValues:{sections:"sections"},unsupportedShort:SEARCH_UNSUPPORTED_SHORT,unsupportedLong:SEARCH_UNSUPPORTED_LONG},whatis:{short:{...common,r:"regex",w:"wildcard",e:"exact",l:"long"},shortExtra:{},values:{s:"sections"},long:{...commonLong,regex:"regex",wildcard:"wildcard",exact:"exact",long:"long"},longValues:{sections:"sections"},unsupportedShort:SEARCH_UNSUPPORTED_SHORT,unsupportedLong:SEARCH_UNSUPPORTED_LONG}};function usage(kind,name){if(kind==="man")return`usage: ${name} [options] [section] topic...
9
+ ${name} -k [options] keyword...
10
+ ${name} -f [options] topic...
11
+ ${name} -l [options] file...
12
+
13
+ -s, --sections=LIST look only in the sections in LIST, separated by : or ,
14
+ -k, --apropos search the topics and descriptions, as apropos does
15
+ -f, --whatis show the topics' one-line descriptions, as whatis does
16
+ -a, --all show every matching page
17
+ -w, --where print the page's URL instead of showing it
18
+ -P, --pager=PAGER page the output with PAGER
19
+ -l, --local-file render the operands, local Markdown files (- for stdin)
20
+ -i, --ignore-case look the topic up in lower case (the default)
21
+ -I, --match-case look the topic up as typed
22
+ --nh, --nj accepted; nothing is hyphenated or justified
23
+ -h, --help print this help and exit
24
+ -V, --version print the version and exit
25
+ --upgrade upgrade to the latest release
26
+ `;let and=kind==="apropos"?` -a, --and a page must match every keyword
27
+ `:"",what=kind==="apropos"?"keyword":"topic";return`usage: ${name} [options] ${what}...
28
+
29
+ -r, --regex each ${what} is a regular expression${kind==="apropos"?" (the default)":""}
30
+ -w, --wildcard each ${what} is a shell wildcard
31
+ -e, --exact each ${what} must match exactly${kind==="whatis"?" (the default)":""}
32
+ ${and} -l, --long don't cut lines to the terminal width
33
+ -s, --sections=LIST look only in the sections in LIST, separated by : or ,
34
+ -h, --help print this help and exit
35
+ -V, --version print the version and exit
36
+ --upgrade upgrade to the latest release
37
+ `}var pairs={match:"ignore",ignore:"match",apropos:"whatis",whatis:"apropos"};function longOption(table2,state,arg,argv){let eq=arg.indexOf("="),long=eq===-1?arg.slice(2):arg.slice(2,eq);if(table2.unsupportedLong.includes(long))return{error:`--${long}: not supported`};let value=table2.longValues[long];if(value!==void 0){if(eq!==-1)state.values[value]=arg.slice(eq+1);else if(state.i<argv.length)state.values[value]=argv[state.i++];else return{error:`--${long}: needs a value`,usage:!0};return}let flag=eq===-1?table2.long[long]:void 0;return flag===void 0?{error:`unknown option: ${arg}`,usage:!0}:state.set(flag)}function shortOptions(table2,state,arg,argv){for(let j=1;j<arg.length;j++){let ch=arg[j];if(table2.unsupportedShort.includes(ch))return{error:`-${ch}: not supported`};let value=table2.values[ch];if(value!==void 0){if(j+1<arg.length)state.values[value]=arg.slice(j+1);else if(state.i<argv.length)state.values[value]=argv[state.i++];else return{error:`-${ch}: needs a value`,usage:!0};return}let flag=table2.short[ch]??table2.shortExtra[ch];if(flag===void 0)return{error:`unknown option: -${ch}`,usage:!0};let stop=state.set(flag);if(stop)return stop}}function parse2(kind,argv){let table2=tables[kind],flags={},values={},operands=[],state={i:0,values,set(flag){if(flag==="help"||flag==="version")return{[flag]:!0};flags[flag]=!0,delete flags[pairs[flag]]}},done=extra=>({flags,values,operands,...extra});for(;state.i<argv.length;){let arg=argv[state.i++];if(arg==="--"){operands.push(...argv.slice(state.i));break}let stop;if(arg.startsWith("--")?stop=longOption(table2,state,arg,argv):arg.startsWith("-")&&arg!=="-"?stop=shortOptions(table2,state,arg,argv):operands.push(arg),stop)return done(stop)}return flags.upgrade&&argv.length>1?done({error:"--upgrade takes no other arguments",usage:!0}):done({})}var BODY=7,SUB=3;function paint(cells2,styled){let out="",current=0;for(let cell of cells2){let style=styled?cell.style:0;style!==current&&(current!==0&&(out+="\x1B[0m"),style&1&&(out+="\x1B[1m"),style&2&&(out+="\x1B[4m"),current=style),out+=cell.ch}return current!==0&&(out+="\x1B[0m"),out}function words(runs){let lines=[[]],word2=[],end=()=>{word2.length>0&&lines.at(-1).push(word2),word2=[]};for(let run3 of runs){if(run3.br){end(),lines.push([]);continue}for(let ch of run3.text)/\s/u.test(ch)&&ch!=="\xA0"?end():word2.push({ch,style:run3.style})}return end(),lines}function wrap(runs,indent,width2,styled,lead=""){let hang=" ".repeat(indent+lead.length),room=Math.max(width2-indent-lead.length,1),out=[],first=!0;for(let line2 of words(runs)){let cur=[],size=0,emit2=()=>{let text=cur.map(w=>paint(w,styled)).join(" ");out.push(`${first?" ".repeat(indent)+lead:hang}${text}`.trimEnd()),first=!1,cur=[],size=0};for(let w of line2)cur.length>0&&size+1+w.length>room&&emit2(),size+=(cur.length>0?1:0)+w.length,cur.push(w);(cur.length>0||out.length===0||line2.length===0)&&emit2()}return out}function upper(runs){return runs.map(run3=>({...run3,text:run3.text.toUpperCase()}))}function table(block,indent,styled){let rows=block.rows.map(row=>{let out=[];for(let c=0;c<block.columns;c++){let runs=inline(row[c]??""),cells2=[];for(let line3 of words(runs))for(let w of line3)cells2.length>0&&cells2.push({ch:" ",style:0}),cells2.push(...w);out.push(cells2)}return out}),widths=[];for(let c=0;c<block.columns;c++)widths.push(Math.max(3,...rows.map(row=>row[c].length)));let line2=row=>`${" ".repeat(indent)}| ${row.map((cells2,c)=>paint(cells2,styled)+" ".repeat(widths[c]-cells2.length)).join(" | ")} |`,rule2=`${" ".repeat(indent)}| ${widths.map(w=>"-".repeat(w)).join(" | ")} |`;return[line2(rows[0]),rule2,...rows.slice(1).map(line2)]}function emit(block,indent,ctx,out){let{width:width2,styled}=ctx;switch(block.type){case"paragraph":out.push(...wrap(inline(block.text),indent,width2,styled));break;case"code":out.push(...block.lines.map(line2=>`${" ".repeat(indent)}${line2}`.trimEnd()));break;case"grid":out.push(...block.lines.map(line2=>`${" ".repeat(indent)}${line2}`));break;case"table":out.push(...table(block,indent,styled));break;case"quote":sequence(block.blocks,indent+4,ctx,out);break;case"list":for(let[index,entry3]of block.items.entries()){index>0&&block.loose&&out.push("");let lead=`${entry3.marker} `,[head,...rest]=entry3.blocks;head?.type==="paragraph"?out.push(...wrap(inline(head.text),indent,width2,styled,lead)):(out.push(`${" ".repeat(indent)}${entry3.marker}`),head&&rest.unshift(head));for(let inner of rest)(block.loose||inner.type!=="list")&&out.push(""),emit(inner,indent+lead.length,ctx,out)}break;case"heading":{let runs=inline(block.text);block.level<=2?out.push(paint(cellsOf(upper(runs)),styled)):out.push(" ".repeat(SUB)+paint(cellsOf(runs),styled));break}}}function cellsOf(runs){let cells2=[];for(let line2 of words(runs))for(let w of line2)cells2.length>0&&cells2.push({ch:" ",style:1}),cells2.push(...w.map(cell=>({...cell,style:cell.style|1})));return cells2}function sequence(list2,indent,ctx,out){let after=!0;for(let block of list2)if(block.type!=="rule"){if(block.type==="heading"){out.length>0&&out.push(""),emit(block,indent,ctx,out),after=!0;continue}after||out.push(""),emit(block,indent,ctx,out),after=!1}}function metadata(block){return block?.type==="grid"?block.first.join(" ").trim()==="Metadata":block?.type==="table"?block.rows[0].join(" ").trim()==="Metadata":!1}function header(topic,width2){let side=`${topic.toUpperCase()}(1)`,middle="SLICC Manual",free=width2-side.length*2-middle.length;if(free<2)return`${side} ${middle} ${side}`;let left=Math.max(1,Math.floor((width2-middle.length)/2)-side.length),right=free-left;return right<1?`${side}${" ".repeat(free-1)}${middle} ${side}`:`${side}${" ".repeat(left)}${middle}${" ".repeat(right)}${side}`}function render(markdown,{topic,width:width2=80,styled=!1}){let list2=parse(markdown);for(;list2.length>0&&list2.at(-1).type==="rule";)list2.pop();metadata(list2.at(-1))&&list2.pop();let ctx={width:width2,styled},out=[header(topic,width2),""];out.push(paint(cellsOf([{text:"NAME",style:0}]),styled));let name=[{text:topic,style:0}];list2[0]?.type==="paragraph"&&name.push({text:" - ",style:0},...inline(list2.shift().text)),out.push(...wrap(name,BODY,width2,styled));let body=[];return sequence(list2,BODY,ctx,body),body.length>0&&(body[0]!==""&&out.push(""),out.push(...body)),`${out.join(`
38
+ `)}
39
+ `}function describe(markdown){let first=parse(markdown)[0];return first?.type==="paragraph"?plain(inline(first.text)):""}var special=/[.*+?^${}()|[\]\\/-]/g;function bracket2(pattern,i){let j=i+1;if((pattern[j]==="!"||pattern[j]==="^")&&j++,pattern[j]==="]"&&j++,j=pattern.indexOf("]",j),j===-1)return{text:"\\[",next:i+1};let body=pattern.slice(i+1,j),not=body[0]==="!"||body[0]==="^";return not&&(body=body.slice(1)),{text:`[${not?"^":""}${body.replace(/[\\\]^]/g,"\\$&")}]`,next:j+1}}function glob(pattern){let out="",i=0;for(;i<pattern.length;){let ch=pattern[i];if(ch==="["){let found=bracket2(pattern,i);out+=found.text,i=found.next;continue}ch==="*"?out+=".*":ch==="?"?out+=".":ch==="\\"&&i+1<pattern.length?out+=pattern[++i].replace(special,"\\$&"):out+=ch.replace(special,"\\$&"),i++}return new RegExp(`^(?:${out})$`,"i")}function words2(text){return text.split(/[^\p{L}\p{N}_]+/u).filter(Boolean)}function matcher2(kind,mode,keyword){if(mode==="regex"){let re=new RegExp(keyword,"i");return kind==="whatis"?entry3=>re.test(entry3.topic):entry3=>re.test(entry3.topic)||re.test(entry3.description)}if(mode==="wildcard"){let re=glob(keyword);return kind==="whatis"?entry3=>re.test(entry3.topic):entry3=>re.test(entry3.topic)||words2(entry3.description).some(w=>re.test(w))}let want=keyword.toLowerCase();return kind==="whatis"?entry3=>entry3.topic.toLowerCase()===want:entry3=>entry3.topic.toLowerCase()===want||words2(entry3.description).some(w=>w.toLowerCase()===want)}function line(entry3){return`${`${entry3.topic} (1)`.padEnd(20)} - ${entry3.description}`.trimEnd()}function cut(text,width2){let chars=Array.from(text);return chars.length<=width2?text:`${chars.slice(0,Math.max(width2-3,0)).join("")}...`}function search(entries,matchers,all){let hits=matchers.map(()=>!1),found=[];for(let entry3 of entries){let each=matchers.map(match=>match(entry3));(all?each.every(Boolean):each.some(Boolean))&&(found.push(entry3),each.forEach((hit,n)=>{hit&&(hits[n]=!0)}))}return{found,hits}}var DEFAULT_BASE="https://www.sliccy.com",FRESH=14400,TIMEOUT=3e4,PARALLEL=8;function valid(topic){return/^[A-Za-z0-9_+-][A-Za-z0-9._+-]*$/.test(topic)}function base(env){return(env.SLICC_MAN_URL||DEFAULT_BASE).replace(/\/+$/,"")}function folder(url){let parsed=new URL(url),host=parsed.hostname.replace(/[^A-Za-z0-9.-]/g,"_");return parsed.port?`${host}-${parsed.port}`:host}function key(topic){return`page-${topic.replace(/[A-Z_]/g,ch=>ch==="_"?"__":`_${ch.toLowerCase()}`)}`}function age(headers){let match=/(?:^|[,\s])max-age\s*=\s*"?(\d+)"?/i.exec(headers.get("cache-control")??"");return match?Number(match[1]):void 0}function reason(error){if(error?.name==="TimeoutError")return"timed out";let cause=error?.cause;return cause?.message||cause?.code||error?.message||String(error)}function stamp(time){return new Date(time).toISOString().replace(/\.\d+Z$/,"Z")}function topics(xml){let seen=new Set,out=[];for(let match of String(xml).matchAll(/<loc>\s*([^<]*?)\s*<\/loc>/g)){let text=match[1].replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&apos;/g,"'").replace(/&amp;/g,"&"),path;try{path=new URL(text,"https://localhost").pathname}catch{continue}let found=/^\/man\/([^/]+)$/.exec(path);if(!found)continue;let topic;try{topic=decodeURIComponent(found[1])}catch{continue}!valid(topic)||seen.has(topic)||(seen.add(topic),out.push(topic))}return out}function site({url,store:store2,fetch,now=Date.now,name,timeout=TIMEOUT}){let root=url.replace(/\/+$/,"");async function load(id){try{let text=await store2.read(id);if(text===void 0)return;let entry3=JSON.parse(text);return entry3&&typeof entry3=="object"?entry3:void 0}catch{return}}async function save(id,entry3){try{await store2.write(id,JSON.stringify(entry3))}catch{}}async function drop(id){try{await store2.remove(id)}catch{}}let fresh=entry3=>typeof entry3?.fetched=="number"&&now()-entry3.fetched<(entry3.maxAge??FRESH)*1e3;async function request(address2,id,entry3){let headers={};entry3?.lastModified&&(headers["if-modified-since"]=entry3.lastModified);let response=await fetch(address2,{headers,signal:AbortSignal.timeout(timeout)});if(response.status===304&&typeof entry3?.body=="string")return await response.body?.cancel(),await save(id,{...entry3,fetched:now(),maxAge:age(response.headers)??entry3.maxAge}),{status:"ok",body:entry3.body};if(response.status===200){let body=await response.text(),lastModified=response.headers.get("last-modified")??void 0,maxAge=age(response.headers)??FRESH;return await save(id,{fetched:now(),maxAge,lastModified,body}),{status:"ok",body}}return await response.body?.cancel(),response.status===404||response.status===410?(entry3&&await drop(id),{status:"missing"}):{why:`HTTP ${response.status}`}}async function get(address2,id,{offline=!1}={}){let entry3=await load(id),kept=typeof entry3?.body=="string"&&typeof entry3.fetched=="number";if(kept&&(fresh(entry3)||offline))return{status:"ok",body:entry3.body};if(offline)return{status:"error",message:`${name}: ${address2}: offline`};let found;try{found=await request(address2,id,entry3)}catch(error){found={why:reason(error),transport:!0}}if(found.status)return found;let transport=found.transport?{transport:!0}:{};return kept?{...transport,status:"ok",body:entry3.body,stale:!0,note:`${name}: ${address2}: ${found.why}; showing the copy cached ${stamp(entry3.fetched)}`}:{status:"error",message:`${name}: ${address2}: ${found.why}`,offline:!0,...transport}}let address=topic=>`${root}/man/${topic}.md`;async function page2(topic,options){return valid(topic)?get(address(topic),key(topic),options):{status:"missing"}}async function build(list2,offline){let entries=new Array(list2.length),next=0,down=!1,broken=offline;async function worker(){for(;next<list2.length;){let at=next++,topic=list2[at],found=await page2(topic,{offline:broken});found.transport&&(broken=!0),(found.stale||found.offline)&&(down=!0),entries[at]={topic,description:found.status==="ok"?describe(found.body):""}}}return await Promise.all(Array.from({length:Math.min(PARALLEL,list2.length)},worker)),{entries,down}}async function index(){let cached=await load("index"),usable=Array.isArray(cached?.entries);if(usable&&fresh(cached))return{entries:cached.entries};let map=`${root}/sitemap.xml`,found=await get(map,"sitemap");if(found.status!=="ok"){let message=found.message??`${name}: ${map}: HTTP 404`;if(usable&&typeof cached.fetched=="number"){let why2=message.slice(`${name}: ${map}: `.length);return{entries:cached.entries,note:`${name}: ${map}: ${why2}; showing the copy cached ${stamp(cached.fetched)}`}}return{error:message}}if(found.stale)return usable?{entries:cached.entries,note:found.note}:{entries:(await build(topics(found.body),!0)).entries,note:found.note};let sitemap=await load("sitemap"),built=await build(topics(found.body),!1);return built.down||await save("index",{fetched:now(),maxAge:sitemap?.maxAge??FRESH,entries:built.entries}),{entries:built.entries}}return{address,page:page2,index}}var OK=0,USAGE=2,NONE=16;function worse(a,b){return a===USAGE||b===USAGE?USAGE:a===NONE||b===NONE?NONE:OK}function width(env,io){let manwidth=String(env.MANWIDTH??"").trim();return/^\d+$/.test(manwidth)&&Number(manwidth)>0?Number(manwidth):io.tty&&Number.isInteger(io.columns)&&io.columns>0?io.columns:80}function sections(text){return text.split(/[:,]/).filter(Boolean)}var sectionName=/^[0-9][A-Za-z]*$/;async function show(text,ctx){let{io,env,values}=ctx;if(!io.tty)return await io.out(text),OK;let pager=values.pager??env.MANPAGER??env.PAGER;return pager===void 0&&(pager=await io.which("less")?"less -R":""),pager.trim()===""?(await io.out(text),OK):io.page(pager,text)}function choose(kind,flags){return flags.regex?"regex":flags.wildcard?"wildcard":flags.exact||kind==="whatis"?"exact":"regex"}async function compile(kind,ctx,flags,operands){let mode=choose(kind,flags),matchers=[];for(let keyword of operands)try{matchers.push(matcher2(kind,mode,keyword))}catch{await ctx.io.err(`${ctx.name}: ${keyword}: invalid regular expression
40
+ `);return}return matchers}async function keywords(kind,ctx,parsed){let{io}=ctx,{flags,values,operands}=parsed;if(operands.length===0)return await io.err(`${kind} what?
41
+ `),USAGE;let matchers=await compile(kind,ctx,flags,operands);if(!matchers)return USAGE;let all=!!flags.and,nothing=async()=>{if(all)await io.err(`${operands.join(" ")}: nothing appropriate.
42
+ `);else for(let keyword of operands)await io.err(`${keyword}: nothing appropriate.
43
+ `);return NONE};if(values.sections!==void 0&&!sections(values.sections).includes("1"))return nothing();let index=await ctx.site.index();if(index.note&&await io.err(`${index.note}
44
+ `),index.error)return await io.err(`${index.error}
45
+ `),USAGE;let{found,hits}=search(index.entries,matchers,all),columns2=width(ctx.env,io),long=flags.long||!io.tty,out="";for(let entry3 of found){let text=clean(line(entry3));out+=`${long?text:cut(text,columns2)}
46
+ `}if(out&&await io.out(out),all)return found.length===0?nothing():OK;let code=OK;for(let[n,keyword]of operands.entries())hits[n]||(await io.err(`${keyword}: nothing appropriate.
47
+ `),code=NONE);return code}function title(file){if(file==="-")return"stdin";let leaf=file.split(/[/\\]/).pop()||file;return clean(leaf.replace(/\.(md|markdown)$/i,""))||"stdin"}async function local(ctx,files){let{io,name}=ctx,code=OK;for(let file of files){let text;try{text=await io.read(file)}catch(error){await io.err(`${name}: ${clean(file)}: ${error.message}
48
+ `),code=worse(code,USAGE);continue}let page2=render(text,{topic:title(file),width:ctx.width,styled:ctx.styled}),shown=await show(page2,ctx);if(shown!==OK)return worse(code,shown)}return code}async function fallback(ctx,topic){let{io,flags}=ctx;if(!(!io.help||flags.where||!valid(topic)||!await io.which(topic)))return await io.err(`No manual entry for ${topic}; showing ${topic} --help
49
+ `),show(await io.help(topic),ctx)}async function entry2(ctx,operand,given){let{io,flags}=ctx,suffix=/^(.+)\(([0-9][A-Za-z]*)\)$/.exec(operand),[topic,section]=suffix?[suffix[1],suffix[2]]:[operand,given],none=`No manual entry for ${clean(topic)}${section===void 0?"":` in section ${clean(section)}`}
50
+ `;if(section!==void 0&&!sections(section).includes("1"))return await io.err(none),{code:NONE};let wanted=flags.match?topic:topic.toLowerCase(),found=await ctx.site.page(wanted);if(found.note&&await io.err(`${found.note}
51
+ `),found.status==="error")return await io.err(`${found.message}
52
+ `),{code:USAGE};if(found.status==="missing"){let helped=await fallback(ctx,topic);return helped===void 0&&await io.err(none),helped===void 0?{code:NONE}:{code:helped,stop:helped!==OK}}if(flags.where)return await io.out(`${ctx.site.address(wanted)}
53
+ `),{code:OK};let page2=render(found.body,{topic:wanted,width:ctx.width,styled:ctx.styled}),paged=await show(page2,ctx);return{code:paged,stop:paged!==OK}}async function pages(ctx,parsed){let operands=parsed.operands,given=ctx.values.sections;given===void 0&&operands.length>=2&&sectionName.test(operands[0])&&(given=operands[0],operands=operands.slice(1));let code=OK;for(let operand of operands){let result=await entry2(ctx,operand,given);if(code=worse(code,result.code),result.stop)break}return code}async function run2({kind="man",name,argv,env,io}){let parsed=parse2(kind,argv);if(parsed.error)return await io.err(`${name}: ${parsed.error}
54
+ ${parsed.usage?usage(kind,name):""}`),USAGE;if(parsed.help)return await io.out(usage(kind,name)),OK;if(parsed.version)return await io.out(`${io.version}
55
+ `),OK;if(parsed.flags.upgrade)return io.upgrade();let url=base(env),ctx={io,env,name,flags:parsed.flags,values:parsed.values,width:width(env,io),styled:!!io.tty&&!env.NO_COLOR,site:site({url,store:io.store(folder(url)),fetch:io.fetch,now:io.now,name,timeout:io.timeout})};return kind!=="man"?keywords(kind,ctx,parsed):parsed.flags.apropos?keywords("apropos",ctx,parsed):parsed.flags.whatis?keywords("whatis",ctx,parsed):parsed.operands.length===0?(await io.err(`What manual page do you want?
56
+ For example, try 'man man'.
57
+ `),USAGE):parsed.flags.local?local(ctx,parsed.operands):pages(ctx,parsed)}var reasons={ENOENT:"No such file or directory",EACCES:"Permission denied",EISDIR:"Is a directory",ENOTDIR:"Not a directory"};var package_default={version:"1.2.0"};var kinds={apropos:"apropos",whatis:"whatis"},decoder=new TextDecoder,counter=0;function why(error){return reasons[error?.code]??error?.message??String(error)}function store(fs,dir){let file=id=>`${dir}/${id}.json`;return{async read(id){try{return decoder.decode(await fs.readFile(file(id)))}catch{return}},async write(id,text){await fs.mkdir(dir,{recursive:!0});let temp=`${dir}/.${id}.${Date.now()}.${counter++}.tmp`;try{await fs.writeFile(temp,text),await fs.rename(temp,file(id))}catch(error){throw await fs.unlink(temp).catch(()=>{}),error}},async remove(id){await fs.unlink(file(id)).catch(()=>{})}}}async function which(ctx,file){for(let dir of String(ctx.env.PATH??"").split(":")){let path=ctx.resolve(`${dir||"."}/${file}`);try{let stat=await ctx.fs.stat(path);if(stat.isFile&&(stat.mode??493)&73)return!0}catch{}}return!1}async function input(ctx){let chunks=[];for(;;){let chunk=await ctx.read(0);if(chunk.length===0)break;chunks.push(chunk)}let all=new Uint8Array(chunks.reduce((sum,chunk)=>sum+chunk.length,0)),at=0;for(let chunk of chunks)all.set(chunk,at),at+=chunk.length;return decoder.decode(all)}async function read(ctx,file){try{return file==="-"?await input(ctx):decoder.decode(await ctx.fs.readFile(ctx.resolve(file)))}catch(error){throw new Error(why(error))}}async function page(ctx,name,command,text){let err=line2=>ctx.write(2,line2);await ctx.signals.on("SIGINT",()=>{});try{let child;try{child=await ctx.spawn({argv:["sh","-c",command],stdin:"pipe"})}catch(error){return await err(`${name}: ${command}: ${why(error)}
58
+ `),2}let writer=child.stdin.getWriter();await writer.write(new TextEncoder().encode(text)).catch(()=>{}),await writer.close().catch(()=>{});let{status}=await child.wait();return status===126||status===127?2:0}finally{await ctx.signals.reset("SIGINT")}}function help(ctx,topic){let result=ctx.sync.run({argv:[topic,"--help"],stdin:"ignore",stderr:"stdout"}),text=decoder.decode(result.stdout??new Uint8Array);return clean(text.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g,"").replace(/\u001b[@-_]/g,""))}async function columns(ctx,tty){if(tty)try{return(await ctx.tty.size(1)).columns}catch{return}}async function main(ctx){let name=String(ctx.argv[0]??"man").split("/").pop(),kind=kinds[name]??"man",tty=await ctx.isatty(1),root=`${String(ctx.env.HOME||"/root").replace(/\/+$/,"")}/.cache/slicc-man`,io={tty,columns:await columns(ctx,tty),fetch:(url,init)=>ctx.fetch(url,init),now:Date.now,version:package_default.version,out:text=>ctx.write(1,text),err:text=>ctx.write(2,text),store:dir=>store(ctx.fs,`${root}/${dir}`),which:file=>which(ctx,file),page:(command,text)=>page(ctx,name,command,text),read:file=>read(ctx,file),help:topic=>help(ctx,topic),upgrade:async()=>(await ctx.write(1,`pnpm add -g @ai-ecoverse/slicc-man@latest
59
+ `),0)};return run2({kind,name,argv:ctx.argv.slice(1),env:ctx.env,io})}export{main as default,help,page,store,which};
package/host/main.mjs CHANGED
@@ -1,19 +1,13 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { run } from '../lib/man.mjs';
4
+ import { reasons } from '../lib/reasons.mjs';
4
5
  import { cacheRoot, store } from './cache.mjs';
5
6
  import { say } from './io.mjs';
6
7
  import { page } from './pager.mjs';
7
8
  import { lookup } from './path.mjs';
8
9
  import { upgrade, version } from './upgrade.mjs';
9
10
 
10
- const reasons = {
11
- ENOENT: 'No such file or directory',
12
- EACCES: 'Permission denied',
13
- EISDIR: 'Is a directory',
14
- ENOTDIR: 'Not a directory',
15
- };
16
-
17
11
  function read(file, fds) {
18
12
  try {
19
13
  return readFileSync(file === '-' ? fds[0] : file, 'utf8');
package/lib/man.mjs CHANGED
@@ -2,7 +2,7 @@ import { clean } from './markdown.mjs';
2
2
  import { parse, usage } from './options.mjs';
3
3
  import { render } from './render.mjs';
4
4
  import { cut, line, matcher, search } from './search.mjs';
5
- import { base, folder, site } from './site.mjs';
5
+ import { base, folder, site, valid } from './site.mjs';
6
6
 
7
7
  const OK = 0;
8
8
  const USAGE = 2;
@@ -129,50 +129,55 @@ async function local(ctx, files) {
129
129
  return code;
130
130
  }
131
131
 
132
+ async function fallback(ctx, topic) {
133
+ const { io, flags } = ctx;
134
+ if (!io.help || flags.where || !valid(topic) || !(await io.which(topic))) return undefined;
135
+ await io.err(`No manual entry for ${topic}; showing ${topic} --help\n`);
136
+ return show(await io.help(topic), ctx);
137
+ }
138
+
139
+ async function entry(ctx, operand, given) {
140
+ const { io, flags } = ctx;
141
+ const suffix = /^(.+)\(([0-9][A-Za-z]*)\)$/.exec(operand);
142
+ const [topic, section] = suffix ? [suffix[1], suffix[2]] : [operand, given];
143
+ const none = `No manual entry for ${clean(topic)}${section === undefined ? '' : ` in section ${clean(section)}`}\n`;
144
+ if (section !== undefined && !sections(section).includes('1')) {
145
+ await io.err(none);
146
+ return { code: NONE };
147
+ }
148
+ const wanted = flags.match ? topic : topic.toLowerCase();
149
+ const found = await ctx.site.page(wanted);
150
+ if (found.note) await io.err(`${found.note}\n`);
151
+ if (found.status === 'error') {
152
+ await io.err(`${found.message}\n`);
153
+ return { code: USAGE };
154
+ }
155
+ if (found.status === 'missing') {
156
+ const helped = await fallback(ctx, topic);
157
+ if (helped === undefined) await io.err(none);
158
+ return helped === undefined ? { code: NONE } : { code: helped, stop: helped !== OK };
159
+ }
160
+ if (flags.where) {
161
+ await io.out(`${ctx.site.address(wanted)}\n`);
162
+ return { code: OK };
163
+ }
164
+ const page = render(found.body, { topic: wanted, width: ctx.width, styled: ctx.styled });
165
+ const paged = await show(page, ctx);
166
+ return { code: paged, stop: paged !== OK };
167
+ }
168
+
132
169
  async function pages(ctx, parsed) {
133
- const { io, flags, values } = ctx;
134
170
  let operands = parsed.operands;
135
- let given = values.sections;
171
+ let given = ctx.values.sections;
136
172
  if (given === undefined && operands.length >= 2 && sectionName.test(operands[0])) {
137
173
  given = operands[0];
138
174
  operands = operands.slice(1);
139
175
  }
140
176
  let code = OK;
141
177
  for (const operand of operands) {
142
- let topic = operand;
143
- let section = given;
144
- const suffix = /^(.+)\(([0-9][A-Za-z]*)\)$/.exec(operand);
145
- if (suffix) {
146
- topic = suffix[1];
147
- section = suffix[2];
148
- }
149
- const shown = clean(topic);
150
- const where = section === undefined ? '' : ` in section ${clean(section)}`;
151
- if (section !== undefined && !sections(section).includes('1')) {
152
- await io.err(`No manual entry for ${shown}${where}\n`);
153
- code = worse(code, NONE);
154
- continue;
155
- }
156
- const wanted = flags.match ? topic : topic.toLowerCase();
157
- const found = await ctx.site.page(wanted);
158
- if (found.note) await io.err(`${found.note}\n`);
159
- if (found.status === 'error') {
160
- await io.err(`${found.message}\n`);
161
- code = worse(code, USAGE);
162
- continue;
163
- }
164
- if (found.status === 'missing') {
165
- await io.err(`No manual entry for ${shown}${where}\n`);
166
- code = worse(code, NONE);
167
- continue;
168
- }
169
- if (flags.where) {
170
- await io.out(`${ctx.site.address(wanted)}\n`);
171
- continue;
172
- }
173
- const page = render(found.body, { topic: wanted, width: ctx.width, styled: ctx.styled });
174
- const paged = await show(page, ctx);
175
- if (paged !== OK) return worse(code, paged);
178
+ const result = await entry(ctx, operand, given);
179
+ code = worse(code, result.code);
180
+ if (result.stop) break;
176
181
  }
177
182
  return code;
178
183
  }
@@ -0,0 +1,6 @@
1
+ export const reasons = {
2
+ ENOENT: 'No such file or directory',
3
+ EACCES: 'Permission denied',
4
+ EISDIR: 'Is a directory',
5
+ ENOTDIR: 'Not a directory',
6
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-man",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "man, apropos and whatis for SLICC, and scman for SLICC, macOS, Linux and Windows, reading pages from www.sliccy.com/man",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -9,7 +9,8 @@
9
9
  "url": "git+https://github.com/ai-ecoverse/slicc-man.git"
10
10
  },
11
11
  "engines": {
12
- "node": ">=24"
12
+ "node": ">=24",
13
+ "slicc-kernel": ">=1.91.0"
13
14
  },
14
15
  "bin": {
15
16
  "scman": "host/scman.mjs"
@@ -25,18 +26,43 @@
25
26
  "provenance": true
26
27
  },
27
28
  "files": [
29
+ "dist",
28
30
  "host",
29
31
  "lib"
30
32
  ],
31
33
  "slicc": {
34
+ "commands": {
35
+ "man": {
36
+ "abi": "js",
37
+ "module": "dist/slicc.mjs",
38
+ "argv0": "man"
39
+ },
40
+ "apropos": {
41
+ "abi": "js",
42
+ "module": "dist/slicc.mjs",
43
+ "argv0": "apropos"
44
+ },
45
+ "whatis": {
46
+ "abi": "js",
47
+ "module": "dist/slicc.mjs",
48
+ "argv0": "whatis"
49
+ },
50
+ "scman": {
51
+ "abi": "js",
52
+ "module": "dist/slicc.mjs",
53
+ "argv0": "scman"
54
+ }
55
+ },
32
56
  "unitSources": [
33
57
  "host",
34
58
  "lib",
35
- "scripts"
59
+ "scripts",
60
+ "slicc"
36
61
  ]
37
62
  },
38
63
  "scripts": {
39
64
  "prepare": "slicc-install-hooks",
65
+ "prepack": "node scripts/bundle.mjs",
40
66
  "lint": "slicc-lint && shellcheck -s sh install.sh && dash -n install.sh",
41
67
  "format": "biome check --write .",
42
68
  "test": "node --test --test-timeout=120000 test/integration/*.test.mjs",
@@ -47,9 +73,13 @@
47
73
  "release": "semantic-release"
48
74
  },
49
75
  "devDependencies": {
50
- "@ai-ecoverse/slicc-shared-web": "2.11.3",
76
+ "@ai-ecoverse/slicc-kernel": "1.153.0",
77
+ "@ai-ecoverse/slicc-shared-web": "2.12.0",
78
+ "@ai-ecoverse/wasm-bash": "5.3.0-23",
79
+ "@ai-ecoverse/wasm-coreutils": "9.12.0-15",
51
80
  "@biomejs/biome": "2.5.15",
52
81
  "@semantic-release/exec": "7.1.0",
82
+ "esbuild": "0.28.2",
53
83
  "lefthook": "2.2.2",
54
84
  "semantic-release": "25.0.9"
55
85
  }