@qodalis/cli-files 2.0.0-beta.13 → 2.0.0-beta.14

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.
@@ -628,14 +628,14 @@ var files = (() => {
628
628
  var require_sort = __commonJS({
629
629
  "../../node_modules/.pnpm/semver@7.7.4/node_modules/semver/functions/sort.js"(exports$1, module2) {
630
630
  var compareBuild = require_compare_build();
631
- var sort = (list, loose) => list.sort((a, b) => compareBuild(a, b, loose));
631
+ var sort = (list2, loose) => list2.sort((a, b) => compareBuild(a, b, loose));
632
632
  module2.exports = sort;
633
633
  }
634
634
  });
635
635
  var require_rsort = __commonJS({
636
636
  "../../node_modules/.pnpm/semver@7.7.4/node_modules/semver/functions/rsort.js"(exports$1, module2) {
637
637
  var compareBuild = require_compare_build();
638
- var rsort = (list, loose) => list.sort((a, b) => compareBuild(b, a, loose));
638
+ var rsort = (list2, loose) => list2.sort((a, b) => compareBuild(b, a, loose));
639
639
  module2.exports = rsort;
640
640
  }
641
641
  });
@@ -1838,6 +1838,17 @@ var files = (() => {
1838
1838
  if (!provider) return {};
1839
1839
  return typeof provider === "function" ? provider() : provider;
1840
1840
  }
1841
+ var CliAccessDeniedError = class _CliAccessDeniedError extends Error {
1842
+ constructor(message = "permission denied") {
1843
+ super(message);
1844
+ this.code = "EACCES";
1845
+ this.name = "CliAccessDeniedError";
1846
+ Object.setPrototypeOf(this, _CliAccessDeniedError.prototype);
1847
+ }
1848
+ };
1849
+ function isCliAccessDeniedError(error) {
1850
+ return error instanceof CliAccessDeniedError || !!error && typeof error === "object" && error.name === "CliAccessDeniedError";
1851
+ }
1841
1852
  var DefaultLibraryAuthor = {
1842
1853
  name: "Nicolae Lupei",
1843
1854
  email: "nicolae.lupei@qodalis.com"
@@ -2701,7 +2712,7 @@ var files = (() => {
2701
2712
  };
2702
2713
 
2703
2714
  // src/lib/version.ts
2704
- var LIBRARY_VERSION = "2.0.0-beta.13";
2715
+ var LIBRARY_VERSION = "2.0.0-beta.14";
2705
2716
  var API_VERSION2 = 2;
2706
2717
 
2707
2718
  // src/lib/interfaces/i-file-system-service.ts
@@ -2714,6 +2725,7 @@ var files = (() => {
2714
2725
  var CWD_KEY = "cwd";
2715
2726
  var HOME_KEY = "home";
2716
2727
  var DEFAULT_HOME = "/";
2728
+ var ANONYMOUS_OWNERSHIP = { uid: "nobody", gid: "nogroup" };
2717
2729
  function createSeedFileSystem() {
2718
2730
  const now = Date.now();
2719
2731
  return {
@@ -2761,6 +2773,88 @@ var files = (() => {
2761
2773
  this.homePath = DEFAULT_HOME;
2762
2774
  this.currentUid = null;
2763
2775
  this.currentGroups = [];
2776
+ this.systemDepth = 0;
2777
+ }
2778
+ // --- Access control ---
2779
+ setAccessPolicy(policy) {
2780
+ this.accessPolicy = policy;
2781
+ }
2782
+ runAsSystem(fn) {
2783
+ this.systemDepth++;
2784
+ try {
2785
+ return fn();
2786
+ } finally {
2787
+ this.systemDepth--;
2788
+ }
2789
+ }
2790
+ reassignOwner(fromUid, toUid) {
2791
+ const from = fromUid.toLowerCase();
2792
+ if (!from || !toUid) return 0;
2793
+ let changed = 0;
2794
+ const visit = (node) => {
2795
+ if (node.ownership && node.ownership.uid.toLowerCase() === from) {
2796
+ node.ownership = { ...node.ownership, uid: toUid };
2797
+ changed++;
2798
+ }
2799
+ node.children?.forEach(visit);
2800
+ };
2801
+ visit(this.root);
2802
+ return changed;
2803
+ }
2804
+ canTraverse(path) {
2805
+ try {
2806
+ this.assertTraverse(this.resolvePath(path), "stat", path);
2807
+ return true;
2808
+ } catch {
2809
+ return false;
2810
+ }
2811
+ }
2812
+ get enforcing() {
2813
+ return !!this.accessPolicy && this.systemDepth === 0;
2814
+ }
2815
+ deny(op, path, reason = "Permission denied") {
2816
+ throw new CliAccessDeniedError(`${op}: ${path}: ${reason}`);
2817
+ }
2818
+ /** `x` on every existing directory on the way to `resolved` (excluding the target). */
2819
+ assertTraverse(resolved, op, display) {
2820
+ if (!this.enforcing) return;
2821
+ const parts = resolved.split("/").filter(Boolean);
2822
+ let current = this.root;
2823
+ for (let i = 0; i < parts.length; i++) {
2824
+ if (!current || current.type !== "directory") return;
2825
+ if (!this.accessPolicy.canAccess(current, "execute")) this.deny(op, display);
2826
+ current = current.children?.find((c) => c.name === parts[i]);
2827
+ }
2828
+ }
2829
+ assertAccess(node, action, op, display) {
2830
+ if (this.enforcing && !this.accessPolicy.canAccess(node, action)) this.deny(op, display);
2831
+ }
2832
+ /** Recursive delete: every directory inside must allow removing its entries (`w` + `x`, readable). */
2833
+ assertRemovableTree(dir, dirPath, display) {
2834
+ if (!this.enforcing) return;
2835
+ this.assertAccess(dir, "read", "rm", display);
2836
+ this.assertAccess(dir, "execute", "rm", display);
2837
+ if ((dir.children?.length ?? 0) > 0) this.assertAccess(dir, "write", "rm", display);
2838
+ for (const child of dir.children ?? []) {
2839
+ if (child.type === "directory") this.assertRemovableTree(child, `${dirPath}/${child.name}`, display);
2840
+ }
2841
+ }
2842
+ /** Copying reads everything it copies: `r` on every file, `r` + `x` on every directory. */
2843
+ assertReadableTree(node, display) {
2844
+ if (!this.enforcing) return;
2845
+ this.assertAccess(node, "read", "cp", display);
2846
+ if (node.type !== "directory") return;
2847
+ this.assertAccess(node, "execute", "cp", display);
2848
+ for (const child of node.children ?? []) {
2849
+ this.assertReadableTree(child, `${display.replace(/\/+$/, "")}/${child.name}`);
2850
+ }
2851
+ }
2852
+ /** Creating/removing entries needs `w` and `x` on the containing directory. */
2853
+ assertModifyDir(dir, dirPath, op, display) {
2854
+ if (!this.enforcing) return;
2855
+ this.assertTraverse(dirPath, op, display);
2856
+ this.assertAccess(dir, "execute", op, display);
2857
+ this.assertAccess(dir, "write", op, display);
2764
2858
  }
2765
2859
  // --- User context ---
2766
2860
  setCurrentUser(uid, groups) {
@@ -2768,7 +2862,9 @@ var files = (() => {
2768
2862
  this.currentGroups = groups;
2769
2863
  }
2770
2864
  getDefaultOwnership() {
2771
- if (!this.currentUid) return void 0;
2865
+ if (!this.currentUid) {
2866
+ return this.accessPolicy ? { ...ANONYMOUS_OWNERSHIP } : void 0;
2867
+ }
2772
2868
  return {
2773
2869
  uid: this.currentUid,
2774
2870
  gid: this.currentGroups[0] || "users"
@@ -2787,6 +2883,8 @@ var files = (() => {
2787
2883
  if (node.type !== "directory") {
2788
2884
  throw new Error(`cd: ${path}: Not a directory`);
2789
2885
  }
2886
+ this.assertTraverse(resolved, "cd", path);
2887
+ this.assertAccess(node, "execute", "cd", path);
2790
2888
  this.cwd = resolved;
2791
2889
  }
2792
2890
  getHomePath() {
@@ -2843,6 +2941,8 @@ var files = (() => {
2843
2941
  if (node.type !== "directory") {
2844
2942
  throw new Error(`ls: ${path}: Not a directory`);
2845
2943
  }
2944
+ this.assertTraverse(this.resolvePath(path), "ls", path);
2945
+ this.assertAccess(node, "read", "ls", path);
2846
2946
  return node.children || [];
2847
2947
  }
2848
2948
  readFile(path) {
@@ -2853,6 +2953,8 @@ var files = (() => {
2853
2953
  if (node.type === "directory") {
2854
2954
  throw new Error(`cat: ${path}: Is a directory`);
2855
2955
  }
2956
+ this.assertTraverse(this.resolvePath(path), "cat", path);
2957
+ this.assertAccess(node, "read", "cat", path);
2856
2958
  return node.content ?? "";
2857
2959
  }
2858
2960
  exists(path) {
@@ -2877,6 +2979,10 @@ var files = (() => {
2877
2979
  }
2878
2980
  let child = current.children.find((c) => c.name === part);
2879
2981
  if (!child) {
2982
+ if (this.enforcing) {
2983
+ this.assertAccess(current, "execute", "mkdir", path);
2984
+ this.assertAccess(current, "write", "mkdir", path);
2985
+ }
2880
2986
  const now = Date.now();
2881
2987
  child = {
2882
2988
  name: part,
@@ -2892,6 +2998,8 @@ var files = (() => {
2892
2998
  current.modifiedAt = now;
2893
2999
  } else if (child.type !== "directory") {
2894
3000
  throw new Error(`mkdir: ${part}: Not a directory`);
3001
+ } else {
3002
+ this.assertAccess(child, "execute", "mkdir", path);
2895
3003
  }
2896
3004
  current = child;
2897
3005
  }
@@ -2910,6 +3018,7 @@ var files = (() => {
2910
3018
  if (parent.children?.find((c) => c.name === dirName)) {
2911
3019
  throw new Error(`mkdir: ${path}: File exists`);
2912
3020
  }
3021
+ this.assertModifyDir(parent, parentPath, "mkdir", path);
2913
3022
  const now = Date.now();
2914
3023
  if (!parent.children) {
2915
3024
  parent.children = [];
@@ -2944,6 +3053,12 @@ var files = (() => {
2944
3053
  }
2945
3054
  const existing = parent.children?.find((c) => c.name === fileName);
2946
3055
  const now = Date.now();
3056
+ if (existing) {
3057
+ this.assertTraverse(resolved, "touch", path);
3058
+ this.assertAccess(existing, "write", "touch", path);
3059
+ } else {
3060
+ this.assertModifyDir(parent, parentPath, "touch", path);
3061
+ }
2947
3062
  if (existing) {
2948
3063
  existing.modifiedAt = now;
2949
3064
  } else {
@@ -2970,6 +3085,8 @@ var files = (() => {
2970
3085
  if (node.type === "directory") {
2971
3086
  throw new Error(`write: ${path}: Is a directory`);
2972
3087
  }
3088
+ this.assertTraverse(resolved, "write", path);
3089
+ this.assertAccess(node, "write", "write", path);
2973
3090
  const now = Date.now();
2974
3091
  node.content = append ? (node.content ?? "") + content : content;
2975
3092
  node.size = new Blob([node.content]).size;
@@ -2998,6 +3115,8 @@ var files = (() => {
2998
3115
  if (target.type === "directory" && !recursive) {
2999
3116
  throw new Error(`rm: ${path}: Is a directory`);
3000
3117
  }
3118
+ this.assertModifyDir(parent, parentPath, "rm", path);
3119
+ if (target.type === "directory") this.assertRemovableTree(target, resolved, path);
3001
3120
  parent.children.splice(index, 1);
3002
3121
  parent.modifiedAt = Date.now();
3003
3122
  }
@@ -3012,10 +3131,13 @@ var files = (() => {
3012
3131
  `cp: -r not specified; omitting directory '${src}'`
3013
3132
  );
3014
3133
  }
3134
+ this.assertTraverse(srcResolved, "cp", src);
3135
+ this.assertReadableTree(srcNode, src);
3015
3136
  const destResolved = this.resolvePath(dest);
3016
3137
  const destNode = this.getNode(destResolved);
3017
3138
  if (destNode && destNode.type === "directory") {
3018
- const clone = this.cloneNode(srcNode);
3139
+ this.assertModifyDir(destNode, destResolved, "cp", dest);
3140
+ const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
3019
3141
  if (!destNode.children) {
3020
3142
  destNode.children = [];
3021
3143
  }
@@ -3035,7 +3157,8 @@ var files = (() => {
3035
3157
  if (!parent || parent.type !== "directory") {
3036
3158
  throw new Error(`cp: ${parentPath}: No such file or directory`);
3037
3159
  }
3038
- const clone = this.cloneNode(srcNode);
3160
+ this.assertModifyDir(parent, parentPath, "cp", dest);
3161
+ const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
3039
3162
  clone.name = newName;
3040
3163
  if (!parent.children) {
3041
3164
  parent.children = [];
@@ -3067,10 +3190,12 @@ var files = (() => {
3067
3190
  if (!srcParent || !srcParent.children) {
3068
3191
  throw new Error(`mv: ${src}: No such file or directory`);
3069
3192
  }
3193
+ this.assertModifyDir(srcParent, srcParentPath, "mv", src);
3070
3194
  const srcIndex = srcParent.children.findIndex(
3071
3195
  (c) => c.name === srcNode.name
3072
3196
  );
3073
3197
  if (destNode && destNode.type === "directory") {
3198
+ this.assertModifyDir(destNode, destResolved, "mv", dest);
3074
3199
  if (!destNode.children) {
3075
3200
  destNode.children = [];
3076
3201
  }
@@ -3093,6 +3218,7 @@ var files = (() => {
3093
3218
  `mv: ${destParentPath}: No such file or directory`
3094
3219
  );
3095
3220
  }
3221
+ this.assertModifyDir(destParent, destParentPath, "mv", dest);
3096
3222
  srcParent.children.splice(srcIndex, 1);
3097
3223
  srcNode.name = newName;
3098
3224
  if (!destParent.children) {
@@ -3116,6 +3242,9 @@ var files = (() => {
3116
3242
  if (!node) {
3117
3243
  throw new Error(`chmod: ${path}: No such file or directory`);
3118
3244
  }
3245
+ if (this.enforcing && !this.accessPolicy.canChangeOwnership(node)) {
3246
+ this.deny("chmod", path, "Operation not permitted");
3247
+ }
3119
3248
  node.permissions = permissions;
3120
3249
  }
3121
3250
  chown(path, ownership) {
@@ -3124,6 +3253,9 @@ var files = (() => {
3124
3253
  if (!node) {
3125
3254
  throw new Error(`chown: ${path}: No such file or directory`);
3126
3255
  }
3256
+ if (this.enforcing && !this.accessPolicy.canChangeOwnership(node, ownership)) {
3257
+ this.deny("chown", path, "Operation not permitted");
3258
+ }
3127
3259
  node.ownership = ownership;
3128
3260
  }
3129
3261
  // --- Persistence ---
@@ -3165,6 +3297,18 @@ var files = (() => {
3165
3297
  }
3166
3298
  }
3167
3299
  // --- Private helpers ---
3300
+ /** A copy belongs to whoever made it (like `cp` without `-p`) */
3301
+ ownedByCurrentUser(node) {
3302
+ const owner = this.getDefaultOwnership();
3303
+ if (owner) {
3304
+ const apply = (n) => {
3305
+ n.ownership = { ...owner };
3306
+ n.children?.forEach(apply);
3307
+ };
3308
+ apply(node);
3309
+ }
3310
+ return node;
3311
+ }
3168
3312
  cloneNode(node) {
3169
3313
  const now = Date.now();
3170
3314
  const clone = {
@@ -3773,6 +3917,16 @@ var files = (() => {
3773
3917
  }
3774
3918
  };
3775
3919
 
3920
+ // src/lib/utils/readable-children.ts
3921
+ function readableChildren(fs, path) {
3922
+ try {
3923
+ return fs.listDirectory(path);
3924
+ } catch (e) {
3925
+ if (isCliAccessDeniedError(e)) return null;
3926
+ throw e;
3927
+ }
3928
+ }
3929
+
3776
3930
  // src/lib/processors/cli-tree-command-processor.ts
3777
3931
  var CliTreeCommandProcessor = class {
3778
3932
  constructor() {
@@ -3820,7 +3974,8 @@ var files = (() => {
3820
3974
  )
3821
3975
  );
3822
3976
  const counts = { dirs: 0, files: 0 };
3823
- this.printTree(node, "", true, 0, maxDepth, context, counts);
3977
+ this.fs = fs;
3978
+ this.printTree(node, "", true, 0, maxDepth, context, counts, resolvedPath);
3824
3979
  context.writer.writeln();
3825
3980
  context.writer.writeln(
3826
3981
  `${counts.dirs} directories, ${counts.files} files`
@@ -3829,11 +3984,15 @@ var files = (() => {
3829
3984
  context.writer.writeError(e.message);
3830
3985
  }
3831
3986
  }
3832
- printTree(node, prefix, isRoot, depth, maxDepth, context, counts) {
3833
- if (!node.children || depth >= maxDepth) {
3987
+ printTree(node, prefix, isRoot, depth, maxDepth, context, counts, path = "/") {
3988
+ if (depth >= maxDepth) {
3989
+ return;
3990
+ }
3991
+ const visible = this.fs ? readableChildren(this.fs, path) : node.children;
3992
+ if (!visible) {
3834
3993
  return;
3835
3994
  }
3836
- const children = [...node.children].sort((a, b) => {
3995
+ const children = [...visible].sort((a, b) => {
3837
3996
  if (a.type !== b.type) {
3838
3997
  return a.type === "directory" ? -1 : 1;
3839
3998
  }
@@ -3858,7 +4017,8 @@ var files = (() => {
3858
4017
  depth + 1,
3859
4018
  maxDepth,
3860
4019
  context,
3861
- counts
4020
+ counts,
4021
+ path === "/" ? `/${child.name}` : `${path}/${child.name}`
3862
4022
  );
3863
4023
  } else {
3864
4024
  counts.files++;
@@ -4156,6 +4316,7 @@ var files = (() => {
4156
4316
  const resolvedBase = fs.resolvePath(searchPath);
4157
4317
  const nameRegex = namePattern ? this.globToRegex(namePattern) : null;
4158
4318
  const results = [];
4319
+ this.fs = fs;
4159
4320
  this.walk(
4160
4321
  node,
4161
4322
  resolvedBase,
@@ -4183,8 +4344,9 @@ var files = (() => {
4183
4344
  return new RegExp(`^${regexStr}$`, "i");
4184
4345
  }
4185
4346
  walk(node, currentPath, depth, maxDepth, nameRegex, typeFilter, results) {
4186
- if (!node.children) return;
4187
- for (const child of node.children) {
4347
+ const children = this.fs ? readableChildren(this.fs, currentPath) : node.children;
4348
+ if (!children) return;
4349
+ for (const child of children) {
4188
4350
  const childPath = currentPath === "/" ? `/${child.name}` : `${currentPath}/${child.name}`;
4189
4351
  let matches = true;
4190
4352
  if (nameRegex && !nameRegex.test(child.name)) {
@@ -4417,8 +4579,9 @@ var files = (() => {
4417
4579
  };
4418
4580
  }
4419
4581
  collectFiles(fs, basePath, node, result) {
4420
- if (!node.children) return;
4421
- for (const child of node.children) {
4582
+ const children = readableChildren(fs, basePath);
4583
+ if (!children) return;
4584
+ for (const child of children) {
4422
4585
  const childPath = basePath === "/" ? `/${child.name}` : `${basePath}/${child.name}`;
4423
4586
  if (child.type === "file") {
4424
4587
  result.push(childPath);