@lousy-agents/cli 5.9.5 → 5.10.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/dist/index.js CHANGED
@@ -10229,11 +10229,14 @@ var promises_ = __webpack_require__(1455);
10229
10229
 
10230
10230
  // EXTERNAL MODULE: external "node:path"
10231
10231
  var external_node_path_ = __webpack_require__(6760);
10232
+ // EXTERNAL MODULE: external "node:fs"
10233
+ var external_node_fs_ = __webpack_require__(3024);
10232
10234
  ;// CONCATENATED MODULE: ../core/src/gateways/file-system-utils.ts
10233
10235
  /**
10234
10236
  * Shared file system utilities for gateways.
10235
10237
  */
10236
10238
 
10239
+
10237
10240
  /**
10238
10241
  * Checks if a file or directory exists.
10239
10242
  */ async function file_system_utils_fileExists(path) {
@@ -10303,6 +10306,44 @@ function isPathWithinRoot(rootPath, candidatePath) {
10303
10306
  throw new Error(`${context} exceeds size limit (${fileStats.size} bytes > ${maxBytes} bytes)`);
10304
10307
  }
10305
10308
  }
10309
+ /**
10310
+ * Reads a file atomically with symlink and size protection.
10311
+ *
10312
+ * Uses `O_NOFOLLOW` (where available) to atomically reject symlinks at
10313
+ * the kernel level, eliminating the TOCTOU window between `lstat()` and
10314
+ * `readFile()`. Falls back to `lstat()` on platforms without `O_NOFOLLOW`.
10315
+ * Validates file size via `fstat()` on the opened file descriptor so the
10316
+ * size check and the read operate on the same inode.
10317
+ */ async function readFileNoFollow(filePath, maxBytes) {
10318
+ const hasNoFollow = typeof external_node_fs_.constants.O_NOFOLLOW === "number" && external_node_fs_.constants.O_NOFOLLOW !== 0;
10319
+ const safePath = JSON.stringify(filePath);
10320
+ let fh;
10321
+ if (hasNoFollow) {
10322
+ try {
10323
+ fh = await (0,promises_.open)(filePath, external_node_fs_.constants.O_RDONLY | external_node_fs_.constants.O_NOFOLLOW);
10324
+ } catch (error) {
10325
+ if (error instanceof Error && "code" in error && error.code === "ELOOP") {
10326
+ throw new Error(`Symlinks are not allowed: ${safePath}`);
10327
+ }
10328
+ throw error;
10329
+ }
10330
+ } else {
10331
+ const stats = await (0,promises_.lstat)(filePath);
10332
+ if (stats.isSymbolicLink()) {
10333
+ throw new Error(`Symlinks are not allowed: ${safePath}`);
10334
+ }
10335
+ fh = await (0,promises_.open)(filePath, external_node_fs_.constants.O_RDONLY);
10336
+ }
10337
+ try {
10338
+ const fdStats = await fh.stat();
10339
+ if (fdStats.size > maxBytes) {
10340
+ throw new Error(`File ${safePath} exceeds size limit (${fdStats.size} bytes > ${maxBytes} bytes)`);
10341
+ }
10342
+ return await fh.readFile("utf-8");
10343
+ } finally{
10344
+ await fh.close();
10345
+ }
10346
+ }
10306
10347
 
10307
10348
  ;// CONCATENATED MODULE: ../core/src/gateways/agent-file-gateway.ts
10308
10349
  /**
@@ -10414,8 +10455,6 @@ function isPathWithinRoot(rootPath, candidatePath) {
10414
10455
  "pip"
10415
10456
  ];
10416
10457
 
10417
- // EXTERNAL MODULE: external "node:fs"
10418
- var external_node_fs_ = __webpack_require__(3024);
10419
10458
  ;// CONCATENATED MODULE: ../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
10420
10459
  let _lazyMatch = () => { var __lib__=(()=>{var m=Object.defineProperty,V=Object.getOwnPropertyDescriptor,G=Object.getOwnPropertyNames,T=Object.prototype.hasOwnProperty,q=(r,e)=>{for(var n in e)m(r,n,{get:e[n],enumerable:true});},H=(r,e,n,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of G(e))!T.call(r,t)&&t!==n&&m(r,t,{get:()=>e[t],enumerable:!(a=V(e,t))||a.enumerable});return r},J=r=>H(m({},"__esModule",{value:true}),r),w={};q(w,{default:()=>re});var A=r=>Array.isArray(r),d=r=>typeof r=="function",Q=r=>r.length===0,W=r=>typeof r=="number",K=r=>typeof r=="object"&&r!==null,X=r=>r instanceof RegExp,b=r=>typeof r=="string",h=r=>r===void 0,Y=r=>{const e=new Map;return n=>{const a=e.get(n);if(a)return a;const t=r(n);return e.set(n,t),t}},rr=(r,e,n={})=>{const a={cache:{},input:r,index:0,indexMax:0,options:n,output:[]};if(v(e)(a)&&a.index===r.length)return a.output;throw new Error(`Failed to parse at index ${a.indexMax}`)},i=(r,e)=>A(r)?er(r,e):b(r)?ar(r,e):nr(r,e),er=(r,e)=>{const n={};for(const a of r){if(a.length!==1)throw new Error(`Invalid character: "${a}"`);const t=a.charCodeAt(0);n[t]=true;}return a=>{const t=a.index,o=a.input;for(;a.index<o.length&&o.charCodeAt(a.index)in n;)a.index+=1;const u=a.index;if(u>t){if(!h(e)&&!a.options.silent){const s=a.input.slice(t,u),c=d(e)?e(s,o,String(t)):e;h(c)||a.output.push(c);}a.indexMax=Math.max(a.indexMax,a.index);}return true}},nr=(r,e)=>{const n=r.source,a=r.flags.replace(/y|$/,"y"),t=new RegExp(n,a);return g(o=>{t.lastIndex=o.index;const u=t.exec(o.input);if(u){if(!h(e)&&!o.options.silent){const s=d(e)?e(...u,o.input,String(o.index)):e;h(s)||o.output.push(s);}return o.index+=u[0].length,o.indexMax=Math.max(o.indexMax,o.index),true}else return false})},ar=(r,e)=>n=>{if(n.input.startsWith(r,n.index)){if(!h(e)&&!n.options.silent){const t=d(e)?e(r,n.input,String(n.index)):e;h(t)||n.output.push(t);}return n.index+=r.length,n.indexMax=Math.max(n.indexMax,n.index),true}else return false},C=(r,e,n,a)=>{const t=v(r);return g(_(M(o=>{let u=0;for(;u<n;){const s=o.index;if(!t(o)||(u+=1,o.index===s))break}return u>=e})))},tr=(r,e)=>C(r,0,1),f=(r,e)=>C(r,0,1/0),x=(r,e)=>{const n=r.map(v);return g(_(M(a=>{for(let t=0,o=n.length;t<o;t++)if(!n[t](a))return false;return true})))},l=(r,e)=>{const n=r.map(v);return g(_(a=>{for(let t=0,o=n.length;t<o;t++)if(n[t](a))return true;return false}))},M=(r,e=false)=>{const n=v(r);return a=>{const t=a.index,o=a.output.length,u=n(a);return (!u||e)&&(a.index=t,a.output.length!==o&&(a.output.length=o)),u}},_=(r,e)=>{const n=v(r);return n},g=(()=>{let r=0;return e=>{const n=v(e),a=r+=1;return t=>{var o;if(t.options.memoization===false)return n(t);const u=t.index,s=(o=t.cache)[a]||(o[a]=new Map),c=s.get(u);if(c===false)return false;if(W(c))return t.index=c,true;if(c)return t.index=c.index,c.output?.length&&t.output.push(...c.output),true;{const Z=t.output.length;if(n(t)){const D=t.index,U=t.output.length;if(U>Z){const ee=t.output.slice(Z,U);s.set(u,{index:D,output:ee});}else s.set(u,D);return true}else return s.set(u,false),false}}}})(),E=r=>{let e;return n=>(e||(e=v(r())),e(n))},v=Y(r=>{if(d(r))return Q(r)?E(r):r;if(b(r)||X(r))return i(r);if(A(r))return x(r);if(K(r))return l(Object.values(r));throw new Error("Invalid rule")}),P="abcdefghijklmnopqrstuvwxyz",ir=r=>{let e="";for(;r>0;){const n=(r-1)%26;e=P[n]+e,r=Math.floor((r-1)/26);}return e},O=r=>{let e=0;for(let n=0,a=r.length;n<a;n++)e=e*26+P.indexOf(r[n])+1;return e},S=(r,e)=>{if(e<r)return S(e,r);const n=[];for(;r<=e;)n.push(r++);return n},or=(r,e,n)=>S(r,e).map(a=>String(a).padStart(n,"0")),R=(r,e)=>S(O(r),O(e)).map(ir),p=r=>r,z=r=>ur(e=>rr(e,r,{memoization:false}).join("")),ur=r=>{const e={};return n=>e[n]??(e[n]=r(n))},sr=i(/^\*\*\/\*$/,".*"),cr=i(/^\*\*\/(\*)?([ a-zA-Z0-9._-]+)$/,(r,e,n)=>`.*${e?"":"(?:^|/)"}${n.replaceAll(".","\\.")}`),lr=i(/^\*\*\/(\*)?([ a-zA-Z0-9._-]*)\{([ a-zA-Z0-9._-]+(?:,[ a-zA-Z0-9._-]+)*)\}$/,(r,e,n,a)=>`.*${e?"":"(?:^|/)"}${n.replaceAll(".","\\.")}(?:${a.replaceAll(",","|").replaceAll(".","\\.")})`),y=i(/\\./,p),pr=i(/[$.*+?^(){}[\]\|]/,r=>`\\${r}`),vr=i(/./,p),hr=i(/^(?:!!)*!(.*)$/,(r,e)=>`(?!^${L(e)}$).*?`),dr=i(/^(!!)+/,""),fr=l([hr,dr]),xr=i(/\/(\*\*\/)+/,"(?:/.+/|/)"),gr=i(/^(\*\*\/)+/,"(?:^|.*/)"),mr=i(/\/(\*\*)$/,"(?:/.*|$)"),_r=i(/\*\*/,".*"),j=l([xr,gr,mr,_r]),Sr=i(/\*\/(?!\*\*\/)/,"[^/]*/"),yr=i(/\*/,"[^/]*"),N=l([Sr,yr]),k=i("?","[^/]"),$r=i("[",p),wr=i("]",p),Ar=i(/[!^]/,"^/"),br=i(/[a-z]-[a-z]|[0-9]-[0-9]/i,p),Cr=i(/[$.*+?^(){}[\|]/,r=>`\\${r}`),Mr=i(/[^\]]/,p),Er=l([y,Cr,br,Mr]),B=x([$r,tr(Ar),f(Er),wr]),Pr=i("{","(?:"),Or=i("}",")"),Rr=i(/(\d+)\.\.(\d+)/,(r,e,n)=>or(+e,+n,Math.min(e.length,n.length)).join("|")),zr=i(/([a-z]+)\.\.([a-z]+)/,(r,e,n)=>R(e,n).join("|")),jr=i(/([A-Z]+)\.\.([A-Z]+)/,(r,e,n)=>R(e.toLowerCase(),n.toLowerCase()).join("|").toUpperCase()),Nr=l([Rr,zr,jr]),I=x([Pr,Nr,Or]),kr=i("{","(?:"),Br=i("}",")"),Ir=i(",","|"),Fr=i(/[$.*+?^(){[\]\|]/,r=>`\\${r}`),Lr=i(/[^}]/,p),Zr=E(()=>F),Dr=l([j,N,k,B,I,Zr,y,Fr,Ir,Lr]),F=x([kr,f(Dr),Br]),Ur=f(l([sr,cr,lr,fr,j,N,k,B,I,F,y,pr,vr])),Vr=Ur,Gr=z(Vr),L=Gr,Tr=i(/\\./,p),qr=i(/./,p),Hr=i(/\*\*\*+/,"*"),Jr=i(/([^/{[(!])\*\*/,(r,e)=>`${e}*`),Qr=i(/(^|.)\*\*(?=[^*/)\]}])/,(r,e)=>`${e}*`),Wr=f(l([Tr,Hr,Jr,Qr,qr])),Kr=Wr,Xr=z(Kr),Yr=Xr,$=(r,e)=>{const n=Array.isArray(r)?r:[r];if(!n.length)return false;const a=n.map($.compile),t=n.every(s=>/(\/(?:\*\*)?|\[\/\])$/.test(s)),o=e.replace(/[\\\/]+/g,"/").replace(/\/$/,t?"/":"");return a.some(s=>s.test(o))};$.compile=r=>new RegExp(`^${L(Yr(r))}$`,"s");var re=$;return J(w)})();
10421
10460
  return __lib__.default || __lib__; };
@@ -33436,6 +33475,7 @@ var dist = __webpack_require__(3519);
33436
33475
 
33437
33476
 
33438
33477
 
33478
+ /** Maximum skill file size: 1 MB */ const MAX_SKILL_FILE_BYTES = 1_048_576;
33439
33479
  /**
33440
33480
  * Skill directory locations to search for SKILL.md files.
33441
33481
  */ const SKILL_DIRECTORIES = [
@@ -33455,12 +33495,29 @@ var dist = __webpack_require__(3519);
33455
33495
  return skills;
33456
33496
  }
33457
33497
  async discoverSkillsInDir(skillsDir) {
33458
- if (!await file_system_utils_fileExists(skillsDir)) {
33498
+ let dirStats;
33499
+ try {
33500
+ dirStats = await (0,promises_.lstat)(skillsDir);
33501
+ } catch (error) {
33502
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
33503
+ return [];
33504
+ }
33505
+ throw error;
33506
+ }
33507
+ if (dirStats.isSymbolicLink() || !dirStats.isDirectory()) {
33459
33508
  return [];
33460
33509
  }
33461
- const entries = await (0,promises_.readdir)(skillsDir, {
33462
- withFileTypes: true
33463
- });
33510
+ let entries;
33511
+ try {
33512
+ entries = await (0,promises_.readdir)(skillsDir, {
33513
+ withFileTypes: true
33514
+ });
33515
+ } catch (error) {
33516
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
33517
+ return [];
33518
+ }
33519
+ throw error;
33520
+ }
33464
33521
  const skills = [];
33465
33522
  for (const entry of entries){
33466
33523
  if (!entry.isDirectory()) {
@@ -33470,7 +33527,17 @@ var dist = __webpack_require__(3519);
33470
33527
  continue;
33471
33528
  }
33472
33529
  const skillFilePath = (0,external_node_path_.join)(skillsDir, entry.name, "SKILL.md");
33473
- if (await file_system_utils_fileExists(skillFilePath)) {
33530
+ let skillStat;
33531
+ try {
33532
+ skillStat = await (0,promises_.lstat)(skillFilePath);
33533
+ } catch (error) {
33534
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
33535
+ skillStat = null;
33536
+ } else {
33537
+ throw error;
33538
+ }
33539
+ }
33540
+ if (skillStat && !skillStat.isSymbolicLink() && skillStat.isFile()) {
33474
33541
  skills.push({
33475
33542
  filePath: skillFilePath,
33476
33543
  skillName: entry.name
@@ -33480,7 +33547,7 @@ var dist = __webpack_require__(3519);
33480
33547
  return skills;
33481
33548
  }
33482
33549
  async readSkillFileContent(filePath) {
33483
- return (0,promises_.readFile)(filePath, "utf-8");
33550
+ return readFileNoFollow(filePath, MAX_SKILL_FILE_BYTES);
33484
33551
  }
33485
33552
  parseFrontmatter(content) {
33486
33553
  const lines = content.split("\n");
@@ -36577,12 +36644,22 @@ const initCommand = defineCommand({
36577
36644
 
36578
36645
 
36579
36646
 
36647
+ /** Maximum agent file size: 1 MB */ const MAX_AGENT_FILE_BYTES = 1_048_576;
36580
36648
  /**
36581
36649
  * File system implementation of the agent lint gateway.
36582
36650
  */ class FileSystemAgentLintGateway {
36583
36651
  async discoverAgents(targetDir) {
36584
36652
  const agentsDir = (0,external_node_path_.join)(targetDir, ".github", "agents");
36585
- if (!await file_system_utils_fileExists(agentsDir)) {
36653
+ let agentsDirStats;
36654
+ try {
36655
+ agentsDirStats = await (0,promises_.lstat)(agentsDir);
36656
+ } catch (error) {
36657
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
36658
+ return [];
36659
+ }
36660
+ throw error;
36661
+ }
36662
+ if (agentsDirStats.isSymbolicLink() || !agentsDirStats.isDirectory()) {
36586
36663
  return [];
36587
36664
  }
36588
36665
  const resolvedAgentsDir = (0,external_node_path_.resolve)(agentsDir);
@@ -36626,11 +36703,7 @@ const initCommand = defineCommand({
36626
36703
  return agents;
36627
36704
  }
36628
36705
  async readAgentFileContent(filePath) {
36629
- const stats = await (0,promises_.lstat)(filePath);
36630
- if (stats.isSymbolicLink()) {
36631
- throw new Error(`Symlinks are not allowed: ${filePath}`);
36632
- }
36633
- return (0,promises_.readFile)(filePath, "utf-8");
36706
+ return readFileNoFollow(filePath, MAX_AGENT_FILE_BYTES);
36634
36707
  }
36635
36708
  parseFrontmatter(content) {
36636
36709
  const lines = content.split("\n");
@@ -36681,7 +36754,6 @@ const initCommand = defineCommand({
36681
36754
  * Discovers hook config files for GitHub Copilot and Claude Code.
36682
36755
  */
36683
36756
 
36684
-
36685
36757
  /** Maximum hook config file size: 1 MB */ const MAX_CONFIG_FILE_BYTES = 1_048_576;
36686
36758
  /** Matches the Copilot hook key `"preToolUse":` to detect hook section presence */ const COPILOT_HOOK_PATTERN = /"preToolUse"\s*:/;
36687
36759
  /** Matches the Claude hook key `"PreToolUse":` to detect hook section presence */ const CLAUDE_HOOK_PATTERN = /"PreToolUse"\s*:/;
@@ -36710,22 +36782,30 @@ const initCommand = defineCommand({
36710
36782
  let safePath;
36711
36783
  try {
36712
36784
  safePath = await file_system_utils_resolveSafePath(targetDir, config.relativePath);
36713
- } catch {
36714
- continue;
36715
- }
36716
- if (!await file_system_utils_fileExists(safePath)) {
36717
- continue;
36718
- }
36719
- const stats = await (0,promises_.lstat)(safePath);
36720
- if (stats.isSymbolicLink()) {
36721
- continue;
36785
+ } catch (error) {
36786
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
36787
+ continue;
36788
+ }
36789
+ if (error instanceof Error && (error.message.startsWith("Resolved path is outside target directory:") || error.message.startsWith("Path contains symbolic link:"))) {
36790
+ continue;
36791
+ }
36792
+ throw error;
36722
36793
  }
36794
+ let content;
36723
36795
  try {
36724
- await assertFileSizeWithinLimit(safePath, MAX_CONFIG_FILE_BYTES, `Hook config ${config.relativePath}`);
36725
- } catch {
36726
- continue;
36796
+ content = await readFileNoFollow(safePath, MAX_CONFIG_FILE_BYTES);
36797
+ } catch (error) {
36798
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
36799
+ continue;
36800
+ }
36801
+ if (error instanceof Error && error.message.startsWith("Symlinks are not allowed")) {
36802
+ continue;
36803
+ }
36804
+ if (error instanceof Error && error.message.includes("exceeds size limit")) {
36805
+ continue;
36806
+ }
36807
+ throw error;
36727
36808
  }
36728
- const content = await (0,promises_.readFile)(safePath, "utf-8");
36729
36809
  if (this.mayContainHookSection(content, config.platform)) {
36730
36810
  discovered.push({
36731
36811
  filePath: safePath,
@@ -36736,12 +36816,7 @@ const initCommand = defineCommand({
36736
36816
  return discovered;
36737
36817
  }
36738
36818
  async readFileContent(filePath) {
36739
- const stats = await (0,promises_.lstat)(filePath);
36740
- if (stats.isSymbolicLink()) {
36741
- throw new Error(`Symlinks are not allowed: ${filePath}`);
36742
- }
36743
- await assertFileSizeWithinLimit(filePath, MAX_CONFIG_FILE_BYTES, `Hook config ${filePath}`);
36744
- return (0,promises_.readFile)(filePath, "utf-8");
36819
+ return readFileNoFollow(filePath, MAX_CONFIG_FILE_BYTES);
36745
36820
  }
36746
36821
  /**
36747
36822
  * Lightweight heuristic to check if a file may contain a pre-tool-use hooks section.
@@ -55689,11 +55764,12 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55689
55764
  */
55690
55765
 
55691
55766
 
55767
+ /** Maximum instruction file size: 1 MB */ const MAX_INSTRUCTION_FILE_BYTES = 1_048_576;
55692
55768
  /**
55693
55769
  * Remark-based implementation of the Markdown AST gateway.
55694
55770
  */ class RemarkMarkdownAstGateway {
55695
55771
  async parseFile(filePath) {
55696
- const content = await (0,promises_.readFile)(filePath, "utf-8");
55772
+ const content = await readFileNoFollow(filePath, MAX_INSTRUCTION_FILE_BYTES);
55697
55773
  return this.parseContent(content);
55698
55774
  }
55699
55775
  parseContent(content) {
@@ -56839,13 +56915,109 @@ function hasFrontmatterDelimiters(content) {
56839
56915
  return false;
56840
56916
  }
56841
56917
 
56842
- ;// CONCATENATED MODULE: ./src/commands/lint.ts
56918
+ ;// CONCATENATED MODULE: ../lint/src/validate-directory.ts
56843
56919
  /**
56844
- * CLI command for linting agent skills, custom agents, and instruction files.
56845
- * Discovers targets, validates frontmatter/quality, and reports diagnostics.
56920
+ * Directory path validation for the public lint API.
56921
+ *
56922
+ * Validates user-supplied directory paths by rejecting unsafe inputs
56923
+ * (control characters, path traversal, null bytes) and canonicalizing
56924
+ * the path via `realpath()` for cross-platform compatibility.
56846
56925
  */
56847
56926
 
56927
+ /**
56928
+ * Thrown when user-supplied directory input fails validation.
56929
+ *
56930
+ * Consumers can catch this type to distinguish user-input errors
56931
+ * (bad path, missing directory) from system-level errors (EACCES, EMFILE).
56932
+ */ class LintValidationError extends Error {
56933
+ constructor(message){
56934
+ super(message);
56935
+ this.name = "LintValidationError";
56936
+ }
56937
+ }
56938
+ function isControlCharacter(code) {
56939
+ if (code >= 0x00 && code <= 0x1f) return true;
56940
+ if (code === 0x7f) return true;
56941
+ if (code >= 0x80 && code <= 0x9f) return true;
56942
+ if (code === 0x2028 || code === 0x2029) return true;
56943
+ if (code >= 0x202a && code <= 0x202e) return true;
56944
+ if (code >= 0x2066 && code <= 0x2069) return true;
56945
+ return false;
56946
+ }
56947
+ function containsControlCharacters(value) {
56948
+ for(let i = 0; i < value.length; i++){
56949
+ if (isControlCharacter(value.charCodeAt(i))) {
56950
+ return true;
56951
+ }
56952
+ }
56953
+ return false;
56954
+ }
56955
+ function sanitizeForErrorMessage(value) {
56956
+ return JSON.stringify(value);
56957
+ }
56958
+ function hasPathTraversalSegment(directory) {
56959
+ return directory.split(/[\\/]/).includes("..");
56960
+ }
56961
+ /**
56962
+ * Validates a user-supplied directory path for safety and existence.
56963
+ *
56964
+ * Rejects paths that contain control characters (ASCII C0/C1, Unicode
56965
+ * bidirectional overrides, line/paragraph separators), path traversal
56966
+ * segments, or that do not resolve to an existing directory. Returns
56967
+ * the canonicalized absolute path (all symlinks resolved) so downstream
56968
+ * code always operates on real paths.
56969
+ *
56970
+ * @throws {LintValidationError} If the path is empty, contains control
56971
+ * characters, traversal segments, does not exist, or is not a directory.
56972
+ */ async function validateDirectory(directory) {
56973
+ if (directory.length === 0) {
56974
+ throw new LintValidationError("directory must not be empty");
56975
+ }
56976
+ const safeDir = sanitizeForErrorMessage(directory);
56977
+ if (containsControlCharacters(directory)) {
56978
+ throw new LintValidationError(`Invalid directory path (contains control characters): ${safeDir}`);
56979
+ }
56980
+ if (hasPathTraversalSegment(directory)) {
56981
+ throw new LintValidationError(`Invalid directory path (path traversal detected): ${safeDir}`);
56982
+ }
56983
+ const resolved = (0,external_node_path_.resolve)(directory);
56984
+ let canonical;
56985
+ try {
56986
+ canonical = await (0,promises_.realpath)(resolved);
56987
+ } catch (error) {
56988
+ if (error instanceof Error && "code" in error) {
56989
+ if (error.code === "ENOENT") {
56990
+ throw new LintValidationError(`Directory does not exist: ${safeDir}`);
56991
+ }
56992
+ if (error.code === "ENOTDIR") {
56993
+ throw new LintValidationError(`Path is not a directory: ${safeDir}`);
56994
+ }
56995
+ }
56996
+ throw error;
56997
+ }
56998
+ let stats;
56999
+ try {
57000
+ stats = await (0,promises_.lstat)(canonical);
57001
+ } catch (error) {
57002
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
57003
+ throw new LintValidationError(`Directory does not exist: ${safeDir}`);
57004
+ }
57005
+ throw error;
57006
+ }
57007
+ if (!stats.isDirectory()) {
57008
+ throw new LintValidationError(`Path is not a directory: ${safeDir}`);
57009
+ }
57010
+ return canonical;
57011
+ }
56848
57012
 
57013
+ ;// CONCATENATED MODULE: ../lint/src/lint.ts
57014
+ /**
57015
+ * Lint composition root.
57016
+ *
57017
+ * Wires internal gateways, use cases, and severity filtering into a
57018
+ * single `runLint` entry point. This is a Layer 4 (Infrastructure)
57019
+ * module — the only place that instantiates concrete implementations.
57020
+ */
56849
57021
 
56850
57022
 
56851
57023
 
@@ -56859,51 +57031,17 @@ function hasFrontmatterDelimiters(content) {
56859
57031
 
56860
57032
 
56861
57033
 
56862
-
56863
- /** Schema for validating target directory */ const TargetDirSchema = schemas_string().min(1, "Target directory is required");
56864
- /**
56865
- * Validates the target directory.
56866
- * Checks for path traversal attempts before resolution.
56867
- */ function validateTargetDir(targetDir) {
56868
- const parsed = TargetDirSchema.parse(targetDir);
56869
- // Check for path traversal attempts in raw input before resolution
56870
- if (parsed.includes("..")) {
56871
- throw new Error(`Invalid target directory (path traversal detected): ${targetDir}`);
56872
- }
56873
- return (0,external_node_path_.resolve)(parsed);
56874
- }
56875
- /**
56876
- * Converts skill lint output to unified LintOutput.
56877
- */ function skillOutputToLintOutput(output) {
56878
- const diagnostics = [];
56879
- for (const result of output.results){
56880
- for (const d of result.diagnostics){
56881
- diagnostics.push({
56882
- filePath: result.filePath,
56883
- line: d.line,
56884
- severity: d.severity,
56885
- message: d.message,
56886
- field: d.field,
56887
- ruleId: d.ruleId,
56888
- target: "skill"
56889
- });
56890
- }
56891
- }
56892
- return {
56893
- diagnostics,
56894
- target: "skill",
56895
- filesAnalyzed: output.results.map((r)=>r.filePath),
56896
- summary: {
56897
- totalFiles: output.totalSkills,
56898
- totalErrors: output.totalErrors,
56899
- totalWarnings: output.totalWarnings,
56900
- totalInfos: 0
56901
- }
56902
- };
56903
- }
56904
- /**
56905
- * Converts agent lint output to unified LintOutput.
56906
- */ function agentOutputToLintOutput(output) {
57034
+ const LintTargetsSchema = schemas_object({
57035
+ skills: schemas_boolean().optional(),
57036
+ agents: schemas_boolean().optional(),
57037
+ hooks: schemas_boolean().optional(),
57038
+ instructions: schemas_boolean().optional()
57039
+ }).strict().optional();
57040
+ const LintOptionsSchema = schemas_object({
57041
+ directory: schemas_string().min(1, "directory must not be empty"),
57042
+ targets: LintTargetsSchema
57043
+ }).strict();
57044
+ /** Converts a use-case lint output to the unified LintOutput shape. */ function toLintOutput(output, target, totalFiles) {
56907
57045
  const diagnostics = [];
56908
57046
  for (const result of output.results){
56909
57047
  for (const d of result.diagnostics){
@@ -56914,114 +57052,47 @@ function hasFrontmatterDelimiters(content) {
56914
57052
  message: d.message,
56915
57053
  field: d.field,
56916
57054
  ruleId: d.ruleId,
56917
- target: "agent"
57055
+ target
56918
57056
  });
56919
57057
  }
56920
57058
  }
56921
57059
  return {
56922
57060
  diagnostics,
56923
- target: "agent",
57061
+ target,
56924
57062
  filesAnalyzed: output.results.map((r)=>r.filePath),
56925
57063
  summary: {
56926
- totalFiles: output.totalAgents,
57064
+ totalFiles,
56927
57065
  totalErrors: output.totalErrors,
56928
57066
  totalWarnings: output.totalWarnings,
56929
- totalInfos: 0
57067
+ totalInfos: diagnostics.filter((d)=>d.severity === "info").length
56930
57068
  }
56931
57069
  };
56932
57070
  }
56933
- /**
56934
- * Formats and displays a LintOutput using consola.
56935
- */ function displayLintOutput(output, label) {
56936
- if (output.summary.totalFiles === 0) {
56937
- consola.info(`No ${label} found`);
56938
- return;
56939
- }
56940
- consola.info(`Discovered ${output.summary.totalFiles} ${label}`);
56941
- // Group diagnostics by file
56942
- const filesWithDiagnostics = new Set();
56943
- for (const d of output.diagnostics){
56944
- filesWithDiagnostics.add(d.filePath);
56945
- }
56946
- for (const file of output.filesAnalyzed){
56947
- if (!filesWithDiagnostics.has(file)) {
56948
- consola.success(`${file}: OK`);
56949
- }
56950
- }
56951
- for (const d of output.diagnostics){
56952
- const prefix = `${d.filePath}:${d.line}`;
56953
- const fieldInfo = d.field ? ` [${d.field}]` : "";
56954
- if (d.severity === "error") {
56955
- consola.error(`${prefix}${fieldInfo}: ${d.message}`);
56956
- } else if (d.severity === "warning") {
56957
- consola.warn(`${prefix}${fieldInfo}: ${d.message}`);
56958
- } else {
56959
- consola.info(`${prefix}${fieldInfo}: ${d.message}`);
56960
- }
56961
- }
56962
- }
56963
- /**
56964
- * Runs skill linting.
56965
- */ async function lintSkills(targetDir) {
57071
+ async function lintSkills(targetDir) {
56966
57072
  const gateway = createSkillLintGateway();
56967
57073
  const useCase = new LintSkillFrontmatterUseCase(gateway);
56968
57074
  const output = await useCase.execute({
56969
57075
  targetDir
56970
57076
  });
56971
- return skillOutputToLintOutput(output);
57077
+ return toLintOutput(output, "skill", output.totalSkills);
56972
57078
  }
56973
- /**
56974
- * Runs agent linting.
56975
- */ async function lintAgents(targetDir) {
57079
+ async function lintAgents(targetDir) {
56976
57080
  const gateway = createAgentLintGateway();
56977
57081
  const useCase = new LintAgentFrontmatterUseCase(gateway);
56978
57082
  const output = await useCase.execute({
56979
57083
  targetDir
56980
57084
  });
56981
- return agentOutputToLintOutput(output);
57085
+ return toLintOutput(output, "agent", output.totalAgents);
56982
57086
  }
56983
- /**
56984
- * Converts hook lint output to unified LintOutput.
56985
- */ function hookOutputToLintOutput(output) {
56986
- const diagnostics = [];
56987
- for (const result of output.results){
56988
- for (const d of result.diagnostics){
56989
- diagnostics.push({
56990
- filePath: result.filePath,
56991
- line: d.line,
56992
- severity: d.severity,
56993
- message: d.message,
56994
- field: d.field,
56995
- ruleId: d.ruleId,
56996
- target: "hook"
56997
- });
56998
- }
56999
- }
57000
- return {
57001
- diagnostics,
57002
- target: "hook",
57003
- filesAnalyzed: output.results.map((r)=>r.filePath),
57004
- summary: {
57005
- totalFiles: output.totalFiles,
57006
- totalErrors: output.totalErrors,
57007
- totalWarnings: output.totalWarnings,
57008
- totalInfos: 0
57009
- }
57010
- };
57011
- }
57012
- /**
57013
- * Runs hook configuration linting.
57014
- */ async function lintHooks(targetDir) {
57087
+ async function lintHooks(targetDir) {
57015
57088
  const gateway = createHookConfigGateway();
57016
57089
  const useCase = new LintHookConfigUseCase(gateway);
57017
57090
  const output = await useCase.execute({
57018
57091
  targetDir
57019
57092
  });
57020
- return hookOutputToLintOutput(output);
57093
+ return toLintOutput(output, "hook", output.totalFiles);
57021
57094
  }
57022
- /**
57023
- * Runs instruction quality analysis.
57024
- */ async function lintInstructions(targetDir) {
57095
+ async function lintInstructions(targetDir) {
57025
57096
  const discoveryGateway = createInstructionFileDiscoveryGateway();
57026
57097
  const astGateway = createMarkdownAstGateway();
57027
57098
  const commandsGateway = createFeedbackLoopCommandsGateway();
@@ -57043,6 +57114,141 @@ function hasFrontmatterDelimiters(content) {
57043
57114
  }
57044
57115
  };
57045
57116
  }
57117
+ const LINT_TARGETS = [
57118
+ {
57119
+ key: "skills",
57120
+ execute: lintSkills
57121
+ },
57122
+ {
57123
+ key: "agents",
57124
+ execute: lintAgents
57125
+ },
57126
+ {
57127
+ key: "hooks",
57128
+ execute: lintHooks
57129
+ },
57130
+ {
57131
+ key: "instructions",
57132
+ execute: lintInstructions
57133
+ }
57134
+ ];
57135
+ function isTargetEnabled(key, targets) {
57136
+ if (!targets) return true;
57137
+ const hasAnyEnabled = Object.values(targets).some(Boolean);
57138
+ return !hasAnyEnabled || targets[key] === true;
57139
+ }
57140
+ /**
57141
+ * Run lint checks on a project directory.
57142
+ *
57143
+ * Orchestrates all lint targets (skills, agents, hooks, instructions),
57144
+ * applies lint rule configuration, and returns structured results.
57145
+ *
57146
+ * When no targets are specified (or all are false), all targets are run.
57147
+ *
57148
+ * @example
57149
+ * ```typescript
57150
+ * import { runLint } from '@lousy-agents/lint';
57151
+ *
57152
+ * const result = await runLint({ directory: '/path/to/project' });
57153
+ * console.log(result.hasErrors);
57154
+ * console.log(result.outputs);
57155
+ * ```
57156
+ *
57157
+ * @throws {LintValidationError} If directory validation, schema validation, or lint configuration validation fails.
57158
+ */ async function runLint(options) {
57159
+ let parsed;
57160
+ try {
57161
+ parsed = LintOptionsSchema.parse(options);
57162
+ } catch (error) {
57163
+ if (error instanceof ZodError) {
57164
+ throw new LintValidationError(error.issues.map((e)=>e.message).join("; "));
57165
+ }
57166
+ throw error;
57167
+ }
57168
+ const targetDir = await validateDirectory(parsed.directory);
57169
+ let rulesConfig;
57170
+ try {
57171
+ rulesConfig = await loadLintConfig(targetDir);
57172
+ } catch (error) {
57173
+ if (error instanceof ZodError) {
57174
+ throw new LintValidationError(`Invalid lint configuration: ${error.issues.map((e)=>e.message).join("; ")}`);
57175
+ }
57176
+ throw error;
57177
+ }
57178
+ const enabledTargets = LINT_TARGETS.filter((t)=>isTargetEnabled(t.key, parsed.targets));
57179
+ const outputs = [];
57180
+ for (const target of enabledTargets){
57181
+ const rawOutput = await target.execute(targetDir);
57182
+ outputs.push(applySeverityFilter(rawOutput, rulesConfig));
57183
+ }
57184
+ const totalErrors = outputs.reduce((sum, output)=>sum + output.summary.totalErrors, 0);
57185
+ return {
57186
+ outputs,
57187
+ hasErrors: totalErrors > 0
57188
+ };
57189
+ }
57190
+
57191
+ ;// CONCATENATED MODULE: ../lint/src/index.ts
57192
+ /**
57193
+ * @lousy-agents/lint — Public Lint API
57194
+ *
57195
+ * Provides a programmatic lint interface for validating agent skills,
57196
+ * custom agents, hook configurations, and instruction files.
57197
+ *
57198
+ * @example
57199
+ * ```typescript
57200
+ * import { runLint, createFormatter } from '@lousy-agents/lint';
57201
+ *
57202
+ * const result = await runLint({ directory: '/path/to/project' });
57203
+ * if (result.hasErrors) {
57204
+ * const formatter = createFormatter('json');
57205
+ * console.error(formatter.format(result.outputs));
57206
+ * }
57207
+ * ```
57208
+ */ // ── Instruction quality result types (returned in LintOutput) ────────
57209
+
57210
+ // ── Output formatters ────────────────────────────────────────────────
57211
+
57212
+
57213
+
57214
+
57215
+ ;// CONCATENATED MODULE: ./src/commands/lint.ts
57216
+ /**
57217
+ * CLI command for linting agent skills, custom agents, and instruction files.
57218
+ * Delegates to the lint package facade and handles CLI display concerns.
57219
+ */
57220
+
57221
+
57222
+ /**
57223
+ * Formats and displays a LintOutput using consola.
57224
+ */ function displayLintOutput(output, label) {
57225
+ if (output.summary.totalFiles === 0) {
57226
+ consola.info(`No ${label} found`);
57227
+ return;
57228
+ }
57229
+ consola.info(`Discovered ${output.summary.totalFiles} ${label}`);
57230
+ // Group diagnostics by file
57231
+ const filesWithDiagnostics = new Set();
57232
+ for (const d of output.diagnostics){
57233
+ filesWithDiagnostics.add(d.filePath);
57234
+ }
57235
+ for (const file of output.filesAnalyzed){
57236
+ if (!filesWithDiagnostics.has(file)) {
57237
+ consola.success(`${file}: OK`);
57238
+ }
57239
+ }
57240
+ for (const d of output.diagnostics){
57241
+ const prefix = `${d.filePath}:${d.line}`;
57242
+ const fieldInfo = d.field ? ` [${d.field}]` : "";
57243
+ if (d.severity === "error") {
57244
+ consola.error(`${prefix}${fieldInfo}: ${d.message}`);
57245
+ } else if (d.severity === "warning") {
57246
+ consola.warn(`${prefix}${fieldInfo}: ${d.message}`);
57247
+ } else {
57248
+ consola.info(`${prefix}${fieldInfo}: ${d.message}`);
57249
+ }
57250
+ }
57251
+ }
57046
57252
  /**
57047
57253
  * Displays instruction quality analysis results using consola.
57048
57254
  */ function displayInstructionQuality(output) {
@@ -57099,57 +57305,43 @@ function hasFrontmatterDelimiters(content) {
57099
57305
  },
57100
57306
  run: async (context)=>{
57101
57307
  const rawTargetDir = typeof context.data?.targetDir === "string" ? context.data.targetDir : process.cwd();
57102
- const targetDir = validateTargetDir(rawTargetDir);
57103
- let rulesConfig;
57104
- try {
57105
- rulesConfig = await loadLintConfig(targetDir);
57106
- } catch (error) {
57107
- const message = error instanceof Error ? error.message : String(error);
57108
- consola.error(`Failed to load lint configuration: ${message}`);
57109
- process.exitCode = 1;
57110
- return;
57111
- }
57112
57308
  const lintSkillsFlag = context.args?.skills === true || context.data?.skills === true;
57113
57309
  const lintAgentsFlag = context.args?.agents === true || context.data?.agents === true;
57114
57310
  const lintHooksFlag = context.args?.hooks === true || context.data?.hooks === true;
57115
57311
  const lintInstructionsFlag = context.args?.instructions === true || context.data?.instructions === true;
57116
- const noFlagProvided = !lintSkillsFlag && !lintAgentsFlag && !lintHooksFlag && !lintInstructionsFlag;
57117
- const formatValue = context.args?.format ?? context.data?.format ?? "human";
57118
- const format = [
57312
+ const rawFormat = typeof context.args?.format === "string" ? context.args.format : typeof context.data?.format === "string" ? context.data.format : "human";
57313
+ const validFormats = new Set([
57119
57314
  "human",
57120
57315
  "json",
57121
57316
  "rdjsonl"
57122
- ].includes(formatValue) ? formatValue : "human";
57123
- let totalErrors = 0;
57317
+ ]);
57318
+ function isLintFormatType(value) {
57319
+ return validFormats.has(value);
57320
+ }
57321
+ const format = isLintFormatType(rawFormat) ? rawFormat : "human";
57322
+ let result;
57323
+ try {
57324
+ result = await runLint({
57325
+ directory: rawTargetDir,
57326
+ targets: {
57327
+ skills: lintSkillsFlag,
57328
+ agents: lintAgentsFlag,
57329
+ hooks: lintHooksFlag,
57330
+ instructions: lintInstructionsFlag
57331
+ }
57332
+ });
57333
+ } catch (error) {
57334
+ if (error instanceof LintValidationError) {
57335
+ consola.error(`Lint failed: ${error.message}`);
57336
+ process.exitCode = 1;
57337
+ return;
57338
+ }
57339
+ throw error;
57340
+ }
57341
+ const { outputs, hasErrors } = result;
57124
57342
  let totalWarnings = 0;
57125
- const allOutputs = [];
57126
- if (noFlagProvided || lintSkillsFlag) {
57127
- const rawOutput = await lintSkills(targetDir);
57128
- const skillOutput = applySeverityFilter(rawOutput, rulesConfig);
57129
- allOutputs.push(skillOutput);
57130
- totalErrors += skillOutput.summary.totalErrors;
57131
- totalWarnings += skillOutput.summary.totalWarnings;
57132
- }
57133
- if (noFlagProvided || lintAgentsFlag) {
57134
- const rawOutput = await lintAgents(targetDir);
57135
- const agentOutput = applySeverityFilter(rawOutput, rulesConfig);
57136
- allOutputs.push(agentOutput);
57137
- totalErrors += agentOutput.summary.totalErrors;
57138
- totalWarnings += agentOutput.summary.totalWarnings;
57139
- }
57140
- if (noFlagProvided || lintHooksFlag) {
57141
- const rawOutput = await lintHooks(targetDir);
57142
- const hookOutput = applySeverityFilter(rawOutput, rulesConfig);
57143
- allOutputs.push(hookOutput);
57144
- totalErrors += hookOutput.summary.totalErrors;
57145
- totalWarnings += hookOutput.summary.totalWarnings;
57146
- }
57147
- if (noFlagProvided || lintInstructionsFlag) {
57148
- const rawOutput = await lintInstructions(targetDir);
57149
- const instructionOutput = applySeverityFilter(rawOutput, rulesConfig);
57150
- allOutputs.push(instructionOutput);
57151
- totalErrors += instructionOutput.summary.totalErrors;
57152
- totalWarnings += instructionOutput.summary.totalWarnings;
57343
+ for (const output of outputs){
57344
+ totalWarnings += output.summary.totalWarnings;
57153
57345
  }
57154
57346
  const targetLabels = {
57155
57347
  skill: "skill(s)",
@@ -57159,12 +57351,12 @@ function hasFrontmatterDelimiters(content) {
57159
57351
  };
57160
57352
  if (format !== "human") {
57161
57353
  const formatter = createFormatter(format);
57162
- const formatted = formatter.format(allOutputs);
57354
+ const formatted = formatter.format(outputs);
57163
57355
  if (formatted) {
57164
57356
  process.stdout.write(`${formatted}\n`);
57165
57357
  }
57166
57358
  } else {
57167
- for (const output of allOutputs){
57359
+ for (const output of outputs){
57168
57360
  const label = targetLabels[output.target] ?? output.target;
57169
57361
  if (output.target === "instruction") {
57170
57362
  displayInstructionQuality(output);
@@ -57173,9 +57365,10 @@ function hasFrontmatterDelimiters(content) {
57173
57365
  }
57174
57366
  }
57175
57367
  }
57176
- if (totalErrors > 0) {
57368
+ if (hasErrors) {
57177
57369
  process.exitCode = 1;
57178
57370
  if (format === "human") {
57371
+ const totalErrors = outputs.reduce((sum, o)=>sum + o.summary.totalErrors, 0);
57179
57372
  consola.error(`lint failed: ${totalErrors} error(s), ${totalWarnings} warning(s)`);
57180
57373
  }
57181
57374
  return;
@@ -57184,7 +57377,7 @@ function hasFrontmatterDelimiters(content) {
57184
57377
  if (totalWarnings > 0) {
57185
57378
  consola.warn(`Lint passed with ${totalWarnings} warning(s)`);
57186
57379
  } else {
57187
- const targets = allOutputs.map((o)=>targetLabels[o.target] ?? o.target).join(", ");
57380
+ const targets = outputs.map((o)=>targetLabels[o.target] ?? o.target).join(", ");
57188
57381
  consola.success(`All ${targets} passed lint checks`);
57189
57382
  }
57190
57383
  }