@qodalis/cli-files 2.0.0-beta.12 → 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.
package/public-api.js CHANGED
@@ -6,7 +6,7 @@ var cliCore = require('@qodalis/cli-core');
6
6
  var IFileSystemService_TOKEN = "cli-file-system-service";
7
7
 
8
8
  // src/lib/version.ts
9
- var LIBRARY_VERSION = "2.0.0-beta.12";
9
+ var LIBRARY_VERSION = "2.0.0-beta.14";
10
10
  var API_VERSION = 2;
11
11
 
12
12
  // src/lib/processors/cli-ls-command-processor.ts
@@ -36,7 +36,7 @@ var CliLsCommandProcessor = class {
36
36
  ];
37
37
  }
38
38
  async processCommand(command, context) {
39
- const fs = context.services.get(
39
+ const fs = context.services.getRequired(
40
40
  IFileSystemService_TOKEN
41
41
  );
42
42
  const targetPath = command.value || fs.getCurrentDirectory();
@@ -107,7 +107,7 @@ var CliCdCommandProcessor = class {
107
107
  this.metadata = { icon: "\u{1F4C1}", module: "file management" };
108
108
  }
109
109
  async processCommand(command, context) {
110
- const fs = context.services.get(
110
+ const fs = context.services.getRequired(
111
111
  IFileSystemService_TOKEN
112
112
  );
113
113
  const target = command.value || "~";
@@ -128,7 +128,7 @@ var CliPwdCommandProcessor = class {
128
128
  this.metadata = { icon: "\u{1F4C2}", module: "file management" };
129
129
  }
130
130
  async processCommand(command, context) {
131
- const fs = context.services.get(
131
+ const fs = context.services.getRequired(
132
132
  IFileSystemService_TOKEN
133
133
  );
134
134
  context.writer.writeln(fs.getCurrentDirectory());
@@ -154,7 +154,7 @@ var CliMkdirCommandProcessor = class {
154
154
  ];
155
155
  }
156
156
  async processCommand(command, context) {
157
- const fs = context.services.get(
157
+ const fs = context.services.getRequired(
158
158
  IFileSystemService_TOKEN
159
159
  );
160
160
  const path = command.value;
@@ -182,7 +182,7 @@ var CliRmdirCommandProcessor = class {
182
182
  this.metadata = { icon: "\u{1F5D1}", module: "file management" };
183
183
  }
184
184
  async processCommand(command, context) {
185
- const fs = context.services.get(
185
+ const fs = context.services.getRequired(
186
186
  IFileSystemService_TOKEN
187
187
  );
188
188
  const path = command.value;
@@ -226,7 +226,7 @@ var CliTouchCommandProcessor = class {
226
226
  this.metadata = { icon: "\u{1F4C4}", module: "file management" };
227
227
  }
228
228
  async processCommand(command, context) {
229
- const fs = context.services.get(
229
+ const fs = context.services.getRequired(
230
230
  IFileSystemService_TOKEN
231
231
  );
232
232
  const path = command.value;
@@ -253,7 +253,7 @@ var CliCatCommandProcessor = class {
253
253
  this.metadata = { icon: "\u{1F4D6}", module: "file management" };
254
254
  }
255
255
  async processCommand(command, context) {
256
- const fs = context.services.get(
256
+ const fs = context.services.getRequired(
257
257
  IFileSystemService_TOKEN
258
258
  );
259
259
  const path = command.value;
@@ -292,7 +292,7 @@ var CliEchoCommandProcessor = class {
292
292
  }
293
293
  return;
294
294
  }
295
- const fs = context.services.get(
295
+ const fs = context.services.getRequired(
296
296
  IFileSystemService_TOKEN
297
297
  );
298
298
  let text;
@@ -364,7 +364,7 @@ var CliRmCommandProcessor = class {
364
364
  ];
365
365
  }
366
366
  async processCommand(command, context) {
367
- const fs = context.services.get(
367
+ const fs = context.services.getRequired(
368
368
  IFileSystemService_TOKEN
369
369
  );
370
370
  const path = command.value;
@@ -413,7 +413,7 @@ var CliCpCommandProcessor = class {
413
413
  ];
414
414
  }
415
415
  async processCommand(command, context) {
416
- const fs = context.services.get(
416
+ const fs = context.services.getRequired(
417
417
  IFileSystemService_TOKEN
418
418
  );
419
419
  const recursive = command.args["recursive"] || command.args["r"] || command.args["R"];
@@ -449,7 +449,7 @@ var CliMvCommandProcessor = class {
449
449
  this.metadata = { icon: "\u{1F4E6}", module: "file management" };
450
450
  }
451
451
  async processCommand(command, context) {
452
- const fs = context.services.get(
452
+ const fs = context.services.getRequired(
453
453
  IFileSystemService_TOKEN
454
454
  );
455
455
  const parts = this.parseArgs(command);
@@ -472,6 +472,14 @@ var CliMvCommandProcessor = class {
472
472
  return raw.split(/\s+/).filter((t) => t && !t.startsWith("--") && !t.startsWith("-"));
473
473
  }
474
474
  };
475
+ function readableChildren(fs, path) {
476
+ try {
477
+ return fs.listDirectory(path);
478
+ } catch (e) {
479
+ if (cliCore.isCliAccessDeniedError(e)) return null;
480
+ throw e;
481
+ }
482
+ }
475
483
  var CliTreeCommandProcessor = class {
476
484
  constructor() {
477
485
  this.command = "tree";
@@ -491,7 +499,7 @@ var CliTreeCommandProcessor = class {
491
499
  ];
492
500
  }
493
501
  async processCommand(command, context) {
494
- const fs = context.services.get(
502
+ const fs = context.services.getRequired(
495
503
  IFileSystemService_TOKEN
496
504
  );
497
505
  const targetPath = command.value || fs.getCurrentDirectory();
@@ -518,7 +526,8 @@ var CliTreeCommandProcessor = class {
518
526
  )
519
527
  );
520
528
  const counts = { dirs: 0, files: 0 };
521
- this.printTree(node, "", true, 0, maxDepth, context, counts);
529
+ this.fs = fs;
530
+ this.printTree(node, "", true, 0, maxDepth, context, counts, resolvedPath);
522
531
  context.writer.writeln();
523
532
  context.writer.writeln(
524
533
  `${counts.dirs} directories, ${counts.files} files`
@@ -527,11 +536,15 @@ var CliTreeCommandProcessor = class {
527
536
  context.writer.writeError(e.message);
528
537
  }
529
538
  }
530
- printTree(node, prefix, isRoot, depth, maxDepth, context, counts) {
531
- if (!node.children || depth >= maxDepth) {
539
+ printTree(node, prefix, isRoot, depth, maxDepth, context, counts, path = "/") {
540
+ if (depth >= maxDepth) {
532
541
  return;
533
542
  }
534
- const children = [...node.children].sort((a, b) => {
543
+ const visible = this.fs ? readableChildren(this.fs, path) : node.children;
544
+ if (!visible) {
545
+ return;
546
+ }
547
+ const children = [...visible].sort((a, b) => {
535
548
  if (a.type !== b.type) {
536
549
  return a.type === "directory" ? -1 : 1;
537
550
  }
@@ -556,7 +569,8 @@ var CliTreeCommandProcessor = class {
556
569
  depth + 1,
557
570
  maxDepth,
558
571
  context,
559
- counts
572
+ counts,
573
+ path === "/" ? `/${child.name}` : `${path}/${child.name}`
560
574
  );
561
575
  } else {
562
576
  counts.files++;
@@ -584,7 +598,7 @@ var CliHeadCommandProcessor = class {
584
598
  ];
585
599
  }
586
600
  async processCommand(command, context) {
587
- const fs = context.services.get(
601
+ const fs = context.services.getRequired(
588
602
  IFileSystemService_TOKEN
589
603
  );
590
604
  const count = parseInt(
@@ -649,7 +663,7 @@ var CliTailCommandProcessor = class {
649
663
  ];
650
664
  }
651
665
  async processCommand(command, context) {
652
- const fs = context.services.get(
666
+ const fs = context.services.getRequired(
653
667
  IFileSystemService_TOKEN
654
668
  );
655
669
  const count = parseInt(
@@ -728,7 +742,7 @@ var CliWcCommandProcessor = class {
728
742
  ];
729
743
  }
730
744
  async processCommand(command, context) {
731
- const fs = context.services.get(
745
+ const fs = context.services.getRequired(
732
746
  IFileSystemService_TOKEN
733
747
  );
734
748
  const showLines = command.args["lines"] || command.args["l"] || false;
@@ -822,7 +836,7 @@ var CliFindCommandProcessor = class {
822
836
  ];
823
837
  }
824
838
  async processCommand(command, context) {
825
- const fs = context.services.get(
839
+ const fs = context.services.getRequired(
826
840
  IFileSystemService_TOKEN
827
841
  );
828
842
  const searchPath = this.parseSearchPath(command) || fs.getCurrentDirectory();
@@ -846,6 +860,7 @@ var CliFindCommandProcessor = class {
846
860
  const resolvedBase = fs.resolvePath(searchPath);
847
861
  const nameRegex = namePattern ? this.globToRegex(namePattern) : null;
848
862
  const results = [];
863
+ this.fs = fs;
849
864
  this.walk(
850
865
  node,
851
866
  resolvedBase,
@@ -873,8 +888,9 @@ var CliFindCommandProcessor = class {
873
888
  return new RegExp(`^${regexStr}$`, "i");
874
889
  }
875
890
  walk(node, currentPath, depth, maxDepth, nameRegex, typeFilter, results) {
876
- if (!node.children) return;
877
- for (const child of node.children) {
891
+ const children = this.fs ? readableChildren(this.fs, currentPath) : node.children;
892
+ if (!children) return;
893
+ for (const child of children) {
878
894
  const childPath = currentPath === "/" ? `/${child.name}` : `${currentPath}/${child.name}`;
879
895
  let matches = true;
880
896
  if (nameRegex && !nameRegex.test(child.name)) {
@@ -957,7 +973,7 @@ var CliGrepCommandProcessor = class {
957
973
  ];
958
974
  }
959
975
  async processCommand(command, context) {
960
- const fs = context.services.get(
976
+ const fs = context.services.getRequired(
961
977
  IFileSystemService_TOKEN
962
978
  );
963
979
  const ignoreCase = command.args["ignore-case"] || command.args["i"] || false;
@@ -1105,8 +1121,9 @@ var CliGrepCommandProcessor = class {
1105
1121
  };
1106
1122
  }
1107
1123
  collectFiles(fs, basePath, node, result) {
1108
- if (!node.children) return;
1109
- for (const child of node.children) {
1124
+ const children = readableChildren(fs, basePath);
1125
+ if (!children) return;
1126
+ for (const child of children) {
1110
1127
  const childPath = basePath === "/" ? `/${child.name}` : `${basePath}/${child.name}`;
1111
1128
  if (child.type === "file") {
1112
1129
  result.push(childPath);
@@ -1127,7 +1144,7 @@ var CliTacCommandProcessor = class {
1127
1144
  this.metadata = { icon: "\u{1F504}", module: "file management" };
1128
1145
  }
1129
1146
  async processCommand(command, context) {
1130
- const fs = context.services.get(
1147
+ const fs = context.services.getRequired(
1131
1148
  IFileSystemService_TOKEN
1132
1149
  );
1133
1150
  const paths = this.parsePaths(command);
@@ -1299,7 +1316,7 @@ var CliSortCommandProcessor = class {
1299
1316
  ];
1300
1317
  }
1301
1318
  async processCommand(command, context) {
1302
- const fs = context.services.get(
1319
+ const fs = context.services.getRequired(
1303
1320
  IFileSystemService_TOKEN
1304
1321
  );
1305
1322
  const reverse = !!command.args["r"] || !!command.args["reverse"];
@@ -1452,7 +1469,7 @@ var CliUniqCommandProcessor = class {
1452
1469
  ];
1453
1470
  }
1454
1471
  async processCommand(command, context) {
1455
- const fs = context.services.get(
1472
+ const fs = context.services.getRequired(
1456
1473
  IFileSystemService_TOKEN
1457
1474
  );
1458
1475
  const showCount = !!command.args["c"] || !!command.args["count"];
@@ -1583,7 +1600,7 @@ var CliCutCommandProcessor = class {
1583
1600
  ];
1584
1601
  }
1585
1602
  async processCommand(command, context) {
1586
- const fs = context.services.get(
1603
+ const fs = context.services.getRequired(
1587
1604
  IFileSystemService_TOKEN
1588
1605
  );
1589
1606
  const delimiter = command.args["d"] || command.args["delimiter"] || " ";
@@ -1733,7 +1750,7 @@ var CliPasteCommandProcessor = class {
1733
1750
  ];
1734
1751
  }
1735
1752
  async processCommand(command, context) {
1736
- const fs = context.services.get(
1753
+ const fs = context.services.getRequired(
1737
1754
  IFileSystemService_TOKEN
1738
1755
  );
1739
1756
  const delimiter = command.args["d"] || command.args["delimiters"] || " ";
@@ -1900,7 +1917,7 @@ var CliTrCommandProcessor = class {
1900
1917
  ];
1901
1918
  }
1902
1919
  async processCommand(command, context) {
1903
- const fs = context.services.get(
1920
+ const fs = context.services.getRequired(
1904
1921
  IFileSystemService_TOKEN
1905
1922
  );
1906
1923
  const raw = command.value || "";
@@ -2055,7 +2072,7 @@ var CliStatCommandProcessor = class {
2055
2072
  this.parameters = [];
2056
2073
  }
2057
2074
  async processCommand(command, context) {
2058
- const fs = context.services.get(
2075
+ const fs = context.services.getRequired(
2059
2076
  IFileSystemService_TOKEN
2060
2077
  );
2061
2078
  const paths = this.parsePaths(command);
@@ -2065,6 +2082,10 @@ var CliStatCommandProcessor = class {
2065
2082
  }
2066
2083
  for (const path of paths) {
2067
2084
  const resolved = fs.resolvePath(path);
2085
+ if (fs.canTraverse && !fs.canTraverse(resolved)) {
2086
+ context.writer.writeError(`stat: cannot stat '${path}': Permission denied`);
2087
+ continue;
2088
+ }
2068
2089
  const node = fs.getNode(resolved);
2069
2090
  if (!node) {
2070
2091
  context.writer.writeError(
@@ -2108,7 +2129,7 @@ var CliChmodCommandProcessor = class {
2108
2129
  ];
2109
2130
  }
2110
2131
  async processCommand(command, context) {
2111
- const fs = context.services.get(
2132
+ const fs = context.services.getRequired(
2112
2133
  IFileSystemService_TOKEN
2113
2134
  );
2114
2135
  const { recursive, mode, paths } = this.parseArgs(command);
@@ -2228,7 +2249,7 @@ var CliChownCommandProcessor = class {
2228
2249
  ];
2229
2250
  }
2230
2251
  async processCommand(command, context) {
2231
- const fs = context.services.get(
2252
+ const fs = context.services.getRequired(
2232
2253
  IFileSystemService_TOKEN
2233
2254
  );
2234
2255
  const { recursive, ownerSpec, paths } = this.parseArgs(command);
@@ -2323,7 +2344,7 @@ var CliDuCommandProcessor = class {
2323
2344
  ];
2324
2345
  }
2325
2346
  async processCommand(command, context) {
2326
- const fs = context.services.get(
2347
+ const fs = context.services.getRequired(
2327
2348
  IFileSystemService_TOKEN
2328
2349
  );
2329
2350
  const { humanReadable, summaryOnly, maxDepth, paths } = this.parseArgs(command);
@@ -2333,6 +2354,10 @@ var CliDuCommandProcessor = class {
2333
2354
  }
2334
2355
  for (const path of paths) {
2335
2356
  const resolved = fs.resolvePath(path);
2357
+ if (fs.canTraverse && !fs.canTraverse(resolved)) {
2358
+ context.writer.writeError(`du: cannot access '${path}': Permission denied`);
2359
+ continue;
2360
+ }
2336
2361
  const node = fs.getNode(resolved);
2337
2362
  if (!node) {
2338
2363
  context.writer.writeError(
@@ -2346,6 +2371,7 @@ var CliDuCommandProcessor = class {
2346
2371
  );
2347
2372
  } else {
2348
2373
  const entries = [];
2374
+ this.fs = fs;
2349
2375
  this.collectSizes(node, resolved, 0, maxDepth, summaryOnly, entries);
2350
2376
  for (const entry of entries) {
2351
2377
  context.writer.writeln(
@@ -2368,8 +2394,9 @@ var CliDuCommandProcessor = class {
2368
2394
  return node.size;
2369
2395
  }
2370
2396
  let total = 0;
2371
- if (node.children) {
2372
- for (const child of node.children) {
2397
+ const children = this.fs ? readableChildren(this.fs, path) : node.children;
2398
+ if (children) {
2399
+ for (const child of children) {
2373
2400
  const childPath = path === "/" ? `/${child.name}` : `${path}/${child.name}`;
2374
2401
  total += this.collectSizes(
2375
2402
  child,
@@ -2420,7 +2447,7 @@ var CliLnCommandProcessor = class {
2420
2447
  ];
2421
2448
  }
2422
2449
  async processCommand(command, context) {
2423
- const fs = context.services.get(
2450
+ const fs = context.services.getRequired(
2424
2451
  IFileSystemService_TOKEN
2425
2452
  );
2426
2453
  const { symbolic, target, linkName } = this.parseArgs(command);
@@ -2494,7 +2521,7 @@ var CliSedCommandProcessor = class {
2494
2521
  ];
2495
2522
  }
2496
2523
  async processCommand(command, context) {
2497
- const fs = context.services.get(
2524
+ const fs = context.services.getRequired(
2498
2525
  IFileSystemService_TOKEN
2499
2526
  );
2500
2527
  const { inPlace, suppress, expressions, filePath } = this.parseRawCommand(command);
@@ -2837,7 +2864,7 @@ var CliAwkCommandProcessor = class {
2837
2864
  ];
2838
2865
  }
2839
2866
  async processCommand(command, context) {
2840
- const fs = context.services.get(
2867
+ const fs = context.services.getRequired(
2841
2868
  IFileSystemService_TOKEN
2842
2869
  );
2843
2870
  const { program, filePath, fieldSep } = this.parseArgs(command);
@@ -3313,7 +3340,7 @@ var CliDiffCommandProcessor = class {
3313
3340
  ];
3314
3341
  }
3315
3342
  async processCommand(command, context) {
3316
- const fs = context.services.get(
3343
+ const fs = context.services.getRequired(
3317
3344
  IFileSystemService_TOKEN
3318
3345
  );
3319
3346
  const unified = command.args["unified"] || command.args["u"] || false;
@@ -3499,7 +3526,7 @@ var CliTeeCommandProcessor = class {
3499
3526
  ];
3500
3527
  }
3501
3528
  async processCommand(command, context) {
3502
- const fs = context.services.get(
3529
+ const fs = context.services.getRequired(
3503
3530
  IFileSystemService_TOKEN
3504
3531
  );
3505
3532
  const append = command.args["append"] || command.args["a"] || false;
@@ -3593,7 +3620,7 @@ var CliXargsCommandProcessor = class {
3593
3620
  ];
3594
3621
  }
3595
3622
  async processCommand(command, context) {
3596
- const fs = context.services.get(
3623
+ const fs = context.services.getRequired(
3597
3624
  IFileSystemService_TOKEN
3598
3625
  );
3599
3626
  const replaceStr = command.args["replace"] || command.args["I"] || null;
@@ -3706,7 +3733,7 @@ var CliShCommandProcessor = class {
3706
3733
  ];
3707
3734
  }
3708
3735
  async processCommand(command, context) {
3709
- const fs = context.services.get(
3736
+ const fs = context.services.getRequired(
3710
3737
  IFileSystemService_TOKEN
3711
3738
  );
3712
3739
  let filePath = (command.value || "").trim();
@@ -3832,14 +3859,14 @@ function substituteVariables(input, variables) {
3832
3859
  );
3833
3860
  return result;
3834
3861
  }
3835
-
3836
- // src/lib/services/indexed-db-file-system.service.ts
3837
3862
  var DB_NAME = "qodalis-cli-filesystem";
3838
3863
  var STORE_NAME = "filesystem";
3839
3864
  var ROOT_KEY = "root";
3840
3865
  var CWD_KEY = "cwd";
3841
3866
  var HOME_KEY = "home";
3842
3867
  var DEFAULT_HOME = "/";
3868
+ var SYSTEM_OWNERSHIP = { uid: "root", gid: "admin" };
3869
+ var ANONYMOUS_OWNERSHIP = { uid: "nobody", gid: "nogroup" };
3843
3870
  function createSeedFileSystem() {
3844
3871
  const now = Date.now();
3845
3872
  return {
@@ -3887,6 +3914,88 @@ var IndexedDbFileSystemService = class {
3887
3914
  this.homePath = DEFAULT_HOME;
3888
3915
  this.currentUid = null;
3889
3916
  this.currentGroups = [];
3917
+ this.systemDepth = 0;
3918
+ }
3919
+ // --- Access control ---
3920
+ setAccessPolicy(policy) {
3921
+ this.accessPolicy = policy;
3922
+ }
3923
+ runAsSystem(fn) {
3924
+ this.systemDepth++;
3925
+ try {
3926
+ return fn();
3927
+ } finally {
3928
+ this.systemDepth--;
3929
+ }
3930
+ }
3931
+ reassignOwner(fromUid, toUid) {
3932
+ const from = fromUid.toLowerCase();
3933
+ if (!from || !toUid) return 0;
3934
+ let changed = 0;
3935
+ const visit = (node) => {
3936
+ if (node.ownership && node.ownership.uid.toLowerCase() === from) {
3937
+ node.ownership = { ...node.ownership, uid: toUid };
3938
+ changed++;
3939
+ }
3940
+ node.children?.forEach(visit);
3941
+ };
3942
+ visit(this.root);
3943
+ return changed;
3944
+ }
3945
+ canTraverse(path) {
3946
+ try {
3947
+ this.assertTraverse(this.resolvePath(path), "stat", path);
3948
+ return true;
3949
+ } catch {
3950
+ return false;
3951
+ }
3952
+ }
3953
+ get enforcing() {
3954
+ return !!this.accessPolicy && this.systemDepth === 0;
3955
+ }
3956
+ deny(op, path, reason = "Permission denied") {
3957
+ throw new cliCore.CliAccessDeniedError(`${op}: ${path}: ${reason}`);
3958
+ }
3959
+ /** `x` on every existing directory on the way to `resolved` (excluding the target). */
3960
+ assertTraverse(resolved, op, display) {
3961
+ if (!this.enforcing) return;
3962
+ const parts = resolved.split("/").filter(Boolean);
3963
+ let current = this.root;
3964
+ for (let i = 0; i < parts.length; i++) {
3965
+ if (!current || current.type !== "directory") return;
3966
+ if (!this.accessPolicy.canAccess(current, "execute")) this.deny(op, display);
3967
+ current = current.children?.find((c) => c.name === parts[i]);
3968
+ }
3969
+ }
3970
+ assertAccess(node, action, op, display) {
3971
+ if (this.enforcing && !this.accessPolicy.canAccess(node, action)) this.deny(op, display);
3972
+ }
3973
+ /** Recursive delete: every directory inside must allow removing its entries (`w` + `x`, readable). */
3974
+ assertRemovableTree(dir, dirPath, display) {
3975
+ if (!this.enforcing) return;
3976
+ this.assertAccess(dir, "read", "rm", display);
3977
+ this.assertAccess(dir, "execute", "rm", display);
3978
+ if ((dir.children?.length ?? 0) > 0) this.assertAccess(dir, "write", "rm", display);
3979
+ for (const child of dir.children ?? []) {
3980
+ if (child.type === "directory") this.assertRemovableTree(child, `${dirPath}/${child.name}`, display);
3981
+ }
3982
+ }
3983
+ /** Copying reads everything it copies: `r` on every file, `r` + `x` on every directory. */
3984
+ assertReadableTree(node, display) {
3985
+ if (!this.enforcing) return;
3986
+ this.assertAccess(node, "read", "cp", display);
3987
+ if (node.type !== "directory") return;
3988
+ this.assertAccess(node, "execute", "cp", display);
3989
+ for (const child of node.children ?? []) {
3990
+ this.assertReadableTree(child, `${display.replace(/\/+$/, "")}/${child.name}`);
3991
+ }
3992
+ }
3993
+ /** Creating/removing entries needs `w` and `x` on the containing directory. */
3994
+ assertModifyDir(dir, dirPath, op, display) {
3995
+ if (!this.enforcing) return;
3996
+ this.assertTraverse(dirPath, op, display);
3997
+ this.assertAccess(dir, "execute", op, display);
3998
+ this.assertAccess(dir, "write", op, display);
3890
3999
  }
3891
4000
  // --- User context ---
3892
4001
  setCurrentUser(uid, groups) {
@@ -3894,7 +4003,9 @@ var IndexedDbFileSystemService = class {
3894
4003
  this.currentGroups = groups;
3895
4004
  }
3896
4005
  getDefaultOwnership() {
3897
- if (!this.currentUid) return void 0;
4006
+ if (!this.currentUid) {
4007
+ return this.accessPolicy ? { ...ANONYMOUS_OWNERSHIP } : void 0;
4008
+ }
3898
4009
  return {
3899
4010
  uid: this.currentUid,
3900
4011
  gid: this.currentGroups[0] || "users"
@@ -3913,6 +4024,8 @@ var IndexedDbFileSystemService = class {
3913
4024
  if (node.type !== "directory") {
3914
4025
  throw new Error(`cd: ${path}: Not a directory`);
3915
4026
  }
4027
+ this.assertTraverse(resolved, "cd", path);
4028
+ this.assertAccess(node, "execute", "cd", path);
3916
4029
  this.cwd = resolved;
3917
4030
  }
3918
4031
  getHomePath() {
@@ -3969,6 +4082,8 @@ var IndexedDbFileSystemService = class {
3969
4082
  if (node.type !== "directory") {
3970
4083
  throw new Error(`ls: ${path}: Not a directory`);
3971
4084
  }
4085
+ this.assertTraverse(this.resolvePath(path), "ls", path);
4086
+ this.assertAccess(node, "read", "ls", path);
3972
4087
  return node.children || [];
3973
4088
  }
3974
4089
  readFile(path) {
@@ -3979,6 +4094,8 @@ var IndexedDbFileSystemService = class {
3979
4094
  if (node.type === "directory") {
3980
4095
  throw new Error(`cat: ${path}: Is a directory`);
3981
4096
  }
4097
+ this.assertTraverse(this.resolvePath(path), "cat", path);
4098
+ this.assertAccess(node, "read", "cat", path);
3982
4099
  return node.content ?? "";
3983
4100
  }
3984
4101
  exists(path) {
@@ -4003,6 +4120,10 @@ var IndexedDbFileSystemService = class {
4003
4120
  }
4004
4121
  let child = current.children.find((c) => c.name === part);
4005
4122
  if (!child) {
4123
+ if (this.enforcing) {
4124
+ this.assertAccess(current, "execute", "mkdir", path);
4125
+ this.assertAccess(current, "write", "mkdir", path);
4126
+ }
4006
4127
  const now = Date.now();
4007
4128
  child = {
4008
4129
  name: part,
@@ -4018,6 +4139,8 @@ var IndexedDbFileSystemService = class {
4018
4139
  current.modifiedAt = now;
4019
4140
  } else if (child.type !== "directory") {
4020
4141
  throw new Error(`mkdir: ${part}: Not a directory`);
4142
+ } else {
4143
+ this.assertAccess(child, "execute", "mkdir", path);
4021
4144
  }
4022
4145
  current = child;
4023
4146
  }
@@ -4036,6 +4159,7 @@ var IndexedDbFileSystemService = class {
4036
4159
  if (parent.children?.find((c) => c.name === dirName)) {
4037
4160
  throw new Error(`mkdir: ${path}: File exists`);
4038
4161
  }
4162
+ this.assertModifyDir(parent, parentPath, "mkdir", path);
4039
4163
  const now = Date.now();
4040
4164
  if (!parent.children) {
4041
4165
  parent.children = [];
@@ -4070,6 +4194,12 @@ var IndexedDbFileSystemService = class {
4070
4194
  }
4071
4195
  const existing = parent.children?.find((c) => c.name === fileName);
4072
4196
  const now = Date.now();
4197
+ if (existing) {
4198
+ this.assertTraverse(resolved, "touch", path);
4199
+ this.assertAccess(existing, "write", "touch", path);
4200
+ } else {
4201
+ this.assertModifyDir(parent, parentPath, "touch", path);
4202
+ }
4073
4203
  if (existing) {
4074
4204
  existing.modifiedAt = now;
4075
4205
  } else {
@@ -4096,6 +4226,8 @@ var IndexedDbFileSystemService = class {
4096
4226
  if (node.type === "directory") {
4097
4227
  throw new Error(`write: ${path}: Is a directory`);
4098
4228
  }
4229
+ this.assertTraverse(resolved, "write", path);
4230
+ this.assertAccess(node, "write", "write", path);
4099
4231
  const now = Date.now();
4100
4232
  node.content = append ? (node.content ?? "") + content : content;
4101
4233
  node.size = new Blob([node.content]).size;
@@ -4124,6 +4256,8 @@ var IndexedDbFileSystemService = class {
4124
4256
  if (target.type === "directory" && !recursive) {
4125
4257
  throw new Error(`rm: ${path}: Is a directory`);
4126
4258
  }
4259
+ this.assertModifyDir(parent, parentPath, "rm", path);
4260
+ if (target.type === "directory") this.assertRemovableTree(target, resolved, path);
4127
4261
  parent.children.splice(index, 1);
4128
4262
  parent.modifiedAt = Date.now();
4129
4263
  }
@@ -4138,10 +4272,13 @@ var IndexedDbFileSystemService = class {
4138
4272
  `cp: -r not specified; omitting directory '${src}'`
4139
4273
  );
4140
4274
  }
4275
+ this.assertTraverse(srcResolved, "cp", src);
4276
+ this.assertReadableTree(srcNode, src);
4141
4277
  const destResolved = this.resolvePath(dest);
4142
4278
  const destNode = this.getNode(destResolved);
4143
4279
  if (destNode && destNode.type === "directory") {
4144
- const clone = this.cloneNode(srcNode);
4280
+ this.assertModifyDir(destNode, destResolved, "cp", dest);
4281
+ const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
4145
4282
  if (!destNode.children) {
4146
4283
  destNode.children = [];
4147
4284
  }
@@ -4161,7 +4298,8 @@ var IndexedDbFileSystemService = class {
4161
4298
  if (!parent || parent.type !== "directory") {
4162
4299
  throw new Error(`cp: ${parentPath}: No such file or directory`);
4163
4300
  }
4164
- const clone = this.cloneNode(srcNode);
4301
+ this.assertModifyDir(parent, parentPath, "cp", dest);
4302
+ const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
4165
4303
  clone.name = newName;
4166
4304
  if (!parent.children) {
4167
4305
  parent.children = [];
@@ -4193,10 +4331,12 @@ var IndexedDbFileSystemService = class {
4193
4331
  if (!srcParent || !srcParent.children) {
4194
4332
  throw new Error(`mv: ${src}: No such file or directory`);
4195
4333
  }
4334
+ this.assertModifyDir(srcParent, srcParentPath, "mv", src);
4196
4335
  const srcIndex = srcParent.children.findIndex(
4197
4336
  (c) => c.name === srcNode.name
4198
4337
  );
4199
4338
  if (destNode && destNode.type === "directory") {
4339
+ this.assertModifyDir(destNode, destResolved, "mv", dest);
4200
4340
  if (!destNode.children) {
4201
4341
  destNode.children = [];
4202
4342
  }
@@ -4219,6 +4359,7 @@ var IndexedDbFileSystemService = class {
4219
4359
  `mv: ${destParentPath}: No such file or directory`
4220
4360
  );
4221
4361
  }
4362
+ this.assertModifyDir(destParent, destParentPath, "mv", dest);
4222
4363
  srcParent.children.splice(srcIndex, 1);
4223
4364
  srcNode.name = newName;
4224
4365
  if (!destParent.children) {
@@ -4242,6 +4383,9 @@ var IndexedDbFileSystemService = class {
4242
4383
  if (!node) {
4243
4384
  throw new Error(`chmod: ${path}: No such file or directory`);
4244
4385
  }
4386
+ if (this.enforcing && !this.accessPolicy.canChangeOwnership(node)) {
4387
+ this.deny("chmod", path, "Operation not permitted");
4388
+ }
4245
4389
  node.permissions = permissions;
4246
4390
  }
4247
4391
  chown(path, ownership) {
@@ -4250,6 +4394,9 @@ var IndexedDbFileSystemService = class {
4250
4394
  if (!node) {
4251
4395
  throw new Error(`chown: ${path}: No such file or directory`);
4252
4396
  }
4397
+ if (this.enforcing && !this.accessPolicy.canChangeOwnership(node, ownership)) {
4398
+ this.deny("chown", path, "Operation not permitted");
4399
+ }
4253
4400
  node.ownership = ownership;
4254
4401
  }
4255
4402
  // --- Persistence ---
@@ -4291,6 +4438,18 @@ var IndexedDbFileSystemService = class {
4291
4438
  }
4292
4439
  }
4293
4440
  // --- Private helpers ---
4441
+ /** A copy belongs to whoever made it (like `cp` without `-p`) */
4442
+ ownedByCurrentUser(node) {
4443
+ const owner = this.getDefaultOwnership();
4444
+ if (owner) {
4445
+ const apply = (n) => {
4446
+ n.ownership = { ...owner };
4447
+ n.children?.forEach(apply);
4448
+ };
4449
+ apply(node);
4450
+ }
4451
+ return node;
4452
+ }
4294
4453
  cloneNode(node) {
4295
4454
  const now = Date.now();
4296
4455
  const clone = {
@@ -4332,8 +4491,6 @@ var IndexedDbFileSystemService = class {
4332
4491
  });
4333
4492
  }
4334
4493
  };
4335
-
4336
- // src/lib/services/server-file-system.service.ts
4337
4494
  var DEFAULT_HOME2 = "/home/user";
4338
4495
  function createSeedFileSystem2() {
4339
4496
  const now = Date.now();
@@ -4383,6 +4540,7 @@ var ServerFileSystemService = class {
4383
4540
  this.currentUid = null;
4384
4541
  this.currentGroups = [];
4385
4542
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
4543
+ this.headersProvider = options.headers;
4386
4544
  }
4387
4545
  // --- User context ---
4388
4546
  setCurrentUser(uid, groups) {
@@ -4790,7 +4948,14 @@ var ServerFileSystemService = class {
4790
4948
  // --- Private: server communication ---
4791
4949
  async serverFetch(endpoint, options) {
4792
4950
  const url = `${this.baseUrl}/api/qcli/fs${endpoint}`;
4793
- const response = await fetch(url, options);
4951
+ const mergedHeaders = {
4952
+ ...cliCore.resolveHeaders(this.headersProvider),
4953
+ ...options?.headers ?? {}
4954
+ };
4955
+ const response = await fetch(url, {
4956
+ ...options,
4957
+ headers: mergedHeaders
4958
+ });
4794
4959
  if (!response.ok) {
4795
4960
  const body = await response.text();
4796
4961
  throw new Error(
@@ -4959,6 +5124,115 @@ var VirtualFsFileTransferService = class {
4959
5124
  return this._browserService.uploadFromBrowser(accept);
4960
5125
  }
4961
5126
  };
5127
+ var CliFilesPermissions = {
5128
+ /** Bypass all file permission checks */
5129
+ Admin: "files:admin",
5130
+ /** chmod/chown files you do not own */
5131
+ Chown: "files:chown"
5132
+ };
5133
+ var HOME_DIR_PERMISSIONS = "rwxr-x---";
5134
+ function authorization(context) {
5135
+ try {
5136
+ return context.services.get(cliCore.ICliAuthorizationService_TOKEN);
5137
+ } catch {
5138
+ return void 0;
5139
+ }
5140
+ }
5141
+ function createFileAccessPolicy(context) {
5142
+ return {
5143
+ // Nodes without an owner (system seed, legacy data) belong to root:admin
5144
+ canAccess: (node, action) => cliCore.cliCanAccess(
5145
+ context,
5146
+ node.ownership ? node : { ...node, ownership: SYSTEM_OWNERSHIP },
5147
+ action,
5148
+ [CliFilesPermissions.Admin]
5149
+ ),
5150
+ canChangeOwnership: (node, newOwnership) => {
5151
+ const auth = authorization(context);
5152
+ if (!auth) return true;
5153
+ if (auth.can(CliFilesPermissions.Admin) || auth.can(CliFilesPermissions.Chown)) return true;
5154
+ const principal = auth.getPrincipal();
5155
+ if (!principal) return false;
5156
+ const owns = !!node.ownership && (node.ownership.uid === principal.name || node.ownership.uid === principal.id);
5157
+ const keepsOwner = !newOwnership || newOwnership.uid === node.ownership?.uid || newOwnership.uid === principal.name;
5158
+ return owns && keepsOwner;
5159
+ }
5160
+ };
5161
+ }
5162
+ function followUserSession(context, fs) {
5163
+ let sessions;
5164
+ try {
5165
+ sessions = context.services.get(cliCore.ICliUserSessionService_TOKEN);
5166
+ } catch {
5167
+ sessions = void 0;
5168
+ }
5169
+ if (!sessions) return void 0;
5170
+ let previousUser = null;
5171
+ return sessions.getUserSession().subscribe((session) => {
5172
+ if (!session?.user) {
5173
+ fs.setCurrentUser("", []);
5174
+ if (previousUser !== null || !canEnter(fs, fs.getCurrentDirectory())) {
5175
+ try {
5176
+ fs.setCurrentDirectory("/");
5177
+ void fs.persist();
5178
+ } catch {
5179
+ }
5180
+ }
5181
+ previousUser = null;
5182
+ return;
5183
+ }
5184
+ const user = session.user;
5185
+ fs.setCurrentUser(user.name, user.groups ?? []);
5186
+ if (user.homeDir) {
5187
+ const home = user.homeDir;
5188
+ const system = (fn) => fs.runAsSystem ? fs.runAsSystem(fn) : fn();
5189
+ if (!fs.exists(home)) {
5190
+ system(() => {
5191
+ fs.createDirectory(home, true);
5192
+ if (home !== "/") {
5193
+ fs.chown(home, { uid: user.name, gid: user.groups?.[0] ?? "users" });
5194
+ fs.chmod(home, HOME_DIR_PERMISSIONS);
5195
+ }
5196
+ });
5197
+ void fs.persist();
5198
+ }
5199
+ fs.setHomePath(home);
5200
+ if (previousUser !== user.id) {
5201
+ try {
5202
+ fs.setCurrentDirectory(home);
5203
+ } catch {
5204
+ }
5205
+ }
5206
+ }
5207
+ previousUser = user.id;
5208
+ });
5209
+ }
5210
+ function canEnter(fs, path) {
5211
+ try {
5212
+ fs.setCurrentDirectory(path);
5213
+ return true;
5214
+ } catch {
5215
+ return false;
5216
+ }
5217
+ }
5218
+ function followUserAccounts(context, fs) {
5219
+ let events;
5220
+ try {
5221
+ events = context.services.get(cliCore.ICliUserEventService_TOKEN);
5222
+ } catch {
5223
+ events = void 0;
5224
+ }
5225
+ if (!events || !fs.reassignOwner) return void 0;
5226
+ return events.events$.subscribe((event) => {
5227
+ let changed = 0;
5228
+ if (event.type === "user_updated" && event.previousName && event.previousName !== event.user?.name) {
5229
+ changed = fs.reassignOwner(event.previousName, event.user.name);
5230
+ } else if (event.type === "user_deleted" && event.user?.name) {
5231
+ changed = fs.reassignOwner(event.user.name, SYSTEM_OWNERSHIP.uid);
5232
+ }
5233
+ if (changed > 0) void fs.persist().catch(() => void 0);
5234
+ });
5235
+ }
4962
5236
 
4963
5237
  // src/lib/completion/file-path-completion-provider.ts
4964
5238
  var FILE_COMMANDS = /* @__PURE__ */ new Set([
@@ -5059,10 +5333,10 @@ var CliUploadCommandProcessor = class {
5059
5333
  ];
5060
5334
  }
5061
5335
  async processCommand(command, context) {
5062
- const fs = context.services.get(
5336
+ const fs = context.services.getRequired(
5063
5337
  IFileSystemService_TOKEN
5064
5338
  );
5065
- const fileService = context.services.get(
5339
+ const fileService = context.services.getRequired(
5066
5340
  cliCore.ICliFileTransferService_TOKEN
5067
5341
  );
5068
5342
  if (!fileService) {
@@ -5072,7 +5346,7 @@ var CliUploadCommandProcessor = class {
5072
5346
  const accept = command.args?.["accept"];
5073
5347
  const destPath = command.value?.trim() || void 0;
5074
5348
  context.spinner?.show("Waiting for file selection...");
5075
- context.setStatusText("Waiting for file selection");
5349
+ context.notifier.info("Waiting for file selection");
5076
5350
  const picked = await fileService.uploadFromBrowser(accept);
5077
5351
  if (!picked) {
5078
5352
  context.spinner?.hide();
@@ -5085,7 +5359,7 @@ var CliUploadCommandProcessor = class {
5085
5359
  filename.startsWith("/") ? filename : cwd + "/" + filename
5086
5360
  );
5087
5361
  context.spinner?.show(`Saving "${picked.name}" (${picked.content.length} bytes)...`);
5088
- context.setStatusText(`Saving ${picked.name}`);
5362
+ context.notifier.info(`Saving ${picked.name}`);
5089
5363
  try {
5090
5364
  fs.writeFile(resolved, picked.content);
5091
5365
  await fs.persist();
@@ -5153,6 +5427,10 @@ var filesModule = {
5153
5427
  new CliShCommandProcessor(),
5154
5428
  new CliUploadCommandProcessor()
5155
5429
  ],
5430
+ permissions: [
5431
+ { name: CliFilesPermissions.Admin, description: "Bypass file permission checks" },
5432
+ { name: CliFilesPermissions.Chown, description: "chmod/chown files owned by others" }
5433
+ ],
5156
5434
  services: [
5157
5435
  {
5158
5436
  provide: IFileSystemService_TOKEN,
@@ -5184,7 +5462,7 @@ var filesModule = {
5184
5462
  return { ...this, config };
5185
5463
  },
5186
5464
  async onInit(context) {
5187
- const fs = context.services.get(
5465
+ const fs = context.services.getRequired(
5188
5466
  IFileSystemService_TOKEN
5189
5467
  );
5190
5468
  await fs.initialize();
@@ -5195,7 +5473,7 @@ var filesModule = {
5195
5473
  }
5196
5474
  let dragDrop;
5197
5475
  try {
5198
- dragDrop = context.services.get(cliCore.ICliDragDropService_TOKEN);
5476
+ dragDrop = context.services.getRequired(cliCore.ICliDragDropService_TOKEN);
5199
5477
  } catch {
5200
5478
  }
5201
5479
  if (dragDrop) {
@@ -5224,13 +5502,26 @@ var filesModule = {
5224
5502
  });
5225
5503
  }
5226
5504
  },
5505
+ async onAfterBoot(context) {
5506
+ const fs = context.services.getRequired(IFileSystemService_TOKEN);
5507
+ fs.setAccessPolicy?.(createFileAccessPolicy(context));
5508
+ this._sessionSubscription?.unsubscribe();
5509
+ this._sessionSubscription = followUserSession(context, fs);
5510
+ this._accountsSubscription?.unsubscribe();
5511
+ this._accountsSubscription = followUserAccounts(context, fs);
5512
+ },
5227
5513
  async onDestroy(_context) {
5228
5514
  const sub = this._dragDropSubscription;
5229
5515
  sub?.unsubscribe();
5230
5516
  this._dragDropSubscription = void 0;
5517
+ this._sessionSubscription?.unsubscribe();
5518
+ this._sessionSubscription = void 0;
5519
+ this._accountsSubscription?.unsubscribe();
5520
+ this._accountsSubscription = void 0;
5231
5521
  }
5232
5522
  };
5233
5523
 
5524
+ exports.ANONYMOUS_OWNERSHIP = ANONYMOUS_OWNERSHIP;
5234
5525
  exports.CliAwkCommandProcessor = CliAwkCommandProcessor;
5235
5526
  exports.CliBasenameCommandProcessor = CliBasenameCommandProcessor;
5236
5527
  exports.CliCatCommandProcessor = CliCatCommandProcessor;
@@ -5243,6 +5534,7 @@ exports.CliDiffCommandProcessor = CliDiffCommandProcessor;
5243
5534
  exports.CliDirnameCommandProcessor = CliDirnameCommandProcessor;
5244
5535
  exports.CliDuCommandProcessor = CliDuCommandProcessor;
5245
5536
  exports.CliEchoCommandProcessor = CliEchoCommandProcessor;
5537
+ exports.CliFilesPermissions = CliFilesPermissions;
5246
5538
  exports.CliFindCommandProcessor = CliFindCommandProcessor;
5247
5539
  exports.CliGrepCommandProcessor = CliGrepCommandProcessor;
5248
5540
  exports.CliHeadCommandProcessor = CliHeadCommandProcessor;
@@ -5268,11 +5560,16 @@ exports.CliUniqCommandProcessor = CliUniqCommandProcessor;
5268
5560
  exports.CliWcCommandProcessor = CliWcCommandProcessor;
5269
5561
  exports.CliXargsCommandProcessor = CliXargsCommandProcessor;
5270
5562
  exports.FilePathCompletionProvider = FilePathCompletionProvider;
5563
+ exports.HOME_DIR_PERMISSIONS = HOME_DIR_PERMISSIONS;
5271
5564
  exports.IFileSystemService_TOKEN = IFileSystemService_TOKEN;
5272
5565
  exports.IndexedDbFileSystemService = IndexedDbFileSystemService;
5566
+ exports.SYSTEM_OWNERSHIP = SYSTEM_OWNERSHIP;
5273
5567
  exports.ServerFileSystemService = ServerFileSystemService;
5274
5568
  exports.VirtualFsFileTransferService = VirtualFsFileTransferService;
5569
+ exports.createFileAccessPolicy = createFileAccessPolicy;
5275
5570
  exports.executeScript = executeScript;
5276
5571
  exports.filesModule = filesModule;
5572
+ exports.followUserAccounts = followUserAccounts;
5573
+ exports.followUserSession = followUserSession;
5277
5574
  //# sourceMappingURL=public-api.js.map
5278
5575
  //# sourceMappingURL=public-api.js.map