@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.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { ICliCompletionProvider_TOKEN, ICliFileTransferService_TOKEN, BrowserFileTransferService, DefaultLibraryAuthor, CliForegroundColor, ICliDragDropService_TOKEN } from '@qodalis/cli-core';
1
+ import { ICliCompletionProvider_TOKEN, ICliFileTransferService_TOKEN, CliAccessDeniedError, BrowserFileTransferService, DefaultLibraryAuthor, CliForegroundColor, resolveHeaders, cliCanAccess, ICliUserSessionService_TOKEN, ICliUserEventService_TOKEN, ICliDragDropService_TOKEN, isCliAccessDeniedError, ICliAuthorizationService_TOKEN } from '@qodalis/cli-core';
2
2
 
3
3
  // src/lib/interfaces/i-file-system-service.ts
4
4
  var IFileSystemService_TOKEN = "cli-file-system-service";
5
5
 
6
6
  // src/lib/version.ts
7
- var LIBRARY_VERSION = "2.0.0-beta.12";
7
+ var LIBRARY_VERSION = "2.0.0-beta.14";
8
8
  var API_VERSION = 2;
9
9
 
10
10
  // src/lib/processors/cli-ls-command-processor.ts
@@ -34,7 +34,7 @@ var CliLsCommandProcessor = class {
34
34
  ];
35
35
  }
36
36
  async processCommand(command, context) {
37
- const fs = context.services.get(
37
+ const fs = context.services.getRequired(
38
38
  IFileSystemService_TOKEN
39
39
  );
40
40
  const targetPath = command.value || fs.getCurrentDirectory();
@@ -105,7 +105,7 @@ var CliCdCommandProcessor = class {
105
105
  this.metadata = { icon: "\u{1F4C1}", module: "file management" };
106
106
  }
107
107
  async processCommand(command, context) {
108
- const fs = context.services.get(
108
+ const fs = context.services.getRequired(
109
109
  IFileSystemService_TOKEN
110
110
  );
111
111
  const target = command.value || "~";
@@ -126,7 +126,7 @@ var CliPwdCommandProcessor = class {
126
126
  this.metadata = { icon: "\u{1F4C2}", module: "file management" };
127
127
  }
128
128
  async processCommand(command, context) {
129
- const fs = context.services.get(
129
+ const fs = context.services.getRequired(
130
130
  IFileSystemService_TOKEN
131
131
  );
132
132
  context.writer.writeln(fs.getCurrentDirectory());
@@ -152,7 +152,7 @@ var CliMkdirCommandProcessor = class {
152
152
  ];
153
153
  }
154
154
  async processCommand(command, context) {
155
- const fs = context.services.get(
155
+ const fs = context.services.getRequired(
156
156
  IFileSystemService_TOKEN
157
157
  );
158
158
  const path = command.value;
@@ -180,7 +180,7 @@ var CliRmdirCommandProcessor = class {
180
180
  this.metadata = { icon: "\u{1F5D1}", module: "file management" };
181
181
  }
182
182
  async processCommand(command, context) {
183
- const fs = context.services.get(
183
+ const fs = context.services.getRequired(
184
184
  IFileSystemService_TOKEN
185
185
  );
186
186
  const path = command.value;
@@ -224,7 +224,7 @@ var CliTouchCommandProcessor = class {
224
224
  this.metadata = { icon: "\u{1F4C4}", module: "file management" };
225
225
  }
226
226
  async processCommand(command, context) {
227
- const fs = context.services.get(
227
+ const fs = context.services.getRequired(
228
228
  IFileSystemService_TOKEN
229
229
  );
230
230
  const path = command.value;
@@ -251,7 +251,7 @@ var CliCatCommandProcessor = class {
251
251
  this.metadata = { icon: "\u{1F4D6}", module: "file management" };
252
252
  }
253
253
  async processCommand(command, context) {
254
- const fs = context.services.get(
254
+ const fs = context.services.getRequired(
255
255
  IFileSystemService_TOKEN
256
256
  );
257
257
  const path = command.value;
@@ -290,7 +290,7 @@ var CliEchoCommandProcessor = class {
290
290
  }
291
291
  return;
292
292
  }
293
- const fs = context.services.get(
293
+ const fs = context.services.getRequired(
294
294
  IFileSystemService_TOKEN
295
295
  );
296
296
  let text;
@@ -362,7 +362,7 @@ var CliRmCommandProcessor = class {
362
362
  ];
363
363
  }
364
364
  async processCommand(command, context) {
365
- const fs = context.services.get(
365
+ const fs = context.services.getRequired(
366
366
  IFileSystemService_TOKEN
367
367
  );
368
368
  const path = command.value;
@@ -411,7 +411,7 @@ var CliCpCommandProcessor = class {
411
411
  ];
412
412
  }
413
413
  async processCommand(command, context) {
414
- const fs = context.services.get(
414
+ const fs = context.services.getRequired(
415
415
  IFileSystemService_TOKEN
416
416
  );
417
417
  const recursive = command.args["recursive"] || command.args["r"] || command.args["R"];
@@ -447,7 +447,7 @@ var CliMvCommandProcessor = class {
447
447
  this.metadata = { icon: "\u{1F4E6}", module: "file management" };
448
448
  }
449
449
  async processCommand(command, context) {
450
- const fs = context.services.get(
450
+ const fs = context.services.getRequired(
451
451
  IFileSystemService_TOKEN
452
452
  );
453
453
  const parts = this.parseArgs(command);
@@ -470,6 +470,14 @@ var CliMvCommandProcessor = class {
470
470
  return raw.split(/\s+/).filter((t) => t && !t.startsWith("--") && !t.startsWith("-"));
471
471
  }
472
472
  };
473
+ function readableChildren(fs, path) {
474
+ try {
475
+ return fs.listDirectory(path);
476
+ } catch (e) {
477
+ if (isCliAccessDeniedError(e)) return null;
478
+ throw e;
479
+ }
480
+ }
473
481
  var CliTreeCommandProcessor = class {
474
482
  constructor() {
475
483
  this.command = "tree";
@@ -489,7 +497,7 @@ var CliTreeCommandProcessor = class {
489
497
  ];
490
498
  }
491
499
  async processCommand(command, context) {
492
- const fs = context.services.get(
500
+ const fs = context.services.getRequired(
493
501
  IFileSystemService_TOKEN
494
502
  );
495
503
  const targetPath = command.value || fs.getCurrentDirectory();
@@ -516,7 +524,8 @@ var CliTreeCommandProcessor = class {
516
524
  )
517
525
  );
518
526
  const counts = { dirs: 0, files: 0 };
519
- this.printTree(node, "", true, 0, maxDepth, context, counts);
527
+ this.fs = fs;
528
+ this.printTree(node, "", true, 0, maxDepth, context, counts, resolvedPath);
520
529
  context.writer.writeln();
521
530
  context.writer.writeln(
522
531
  `${counts.dirs} directories, ${counts.files} files`
@@ -525,11 +534,15 @@ var CliTreeCommandProcessor = class {
525
534
  context.writer.writeError(e.message);
526
535
  }
527
536
  }
528
- printTree(node, prefix, isRoot, depth, maxDepth, context, counts) {
529
- if (!node.children || depth >= maxDepth) {
537
+ printTree(node, prefix, isRoot, depth, maxDepth, context, counts, path = "/") {
538
+ if (depth >= maxDepth) {
530
539
  return;
531
540
  }
532
- const children = [...node.children].sort((a, b) => {
541
+ const visible = this.fs ? readableChildren(this.fs, path) : node.children;
542
+ if (!visible) {
543
+ return;
544
+ }
545
+ const children = [...visible].sort((a, b) => {
533
546
  if (a.type !== b.type) {
534
547
  return a.type === "directory" ? -1 : 1;
535
548
  }
@@ -554,7 +567,8 @@ var CliTreeCommandProcessor = class {
554
567
  depth + 1,
555
568
  maxDepth,
556
569
  context,
557
- counts
570
+ counts,
571
+ path === "/" ? `/${child.name}` : `${path}/${child.name}`
558
572
  );
559
573
  } else {
560
574
  counts.files++;
@@ -582,7 +596,7 @@ var CliHeadCommandProcessor = class {
582
596
  ];
583
597
  }
584
598
  async processCommand(command, context) {
585
- const fs = context.services.get(
599
+ const fs = context.services.getRequired(
586
600
  IFileSystemService_TOKEN
587
601
  );
588
602
  const count = parseInt(
@@ -647,7 +661,7 @@ var CliTailCommandProcessor = class {
647
661
  ];
648
662
  }
649
663
  async processCommand(command, context) {
650
- const fs = context.services.get(
664
+ const fs = context.services.getRequired(
651
665
  IFileSystemService_TOKEN
652
666
  );
653
667
  const count = parseInt(
@@ -726,7 +740,7 @@ var CliWcCommandProcessor = class {
726
740
  ];
727
741
  }
728
742
  async processCommand(command, context) {
729
- const fs = context.services.get(
743
+ const fs = context.services.getRequired(
730
744
  IFileSystemService_TOKEN
731
745
  );
732
746
  const showLines = command.args["lines"] || command.args["l"] || false;
@@ -820,7 +834,7 @@ var CliFindCommandProcessor = class {
820
834
  ];
821
835
  }
822
836
  async processCommand(command, context) {
823
- const fs = context.services.get(
837
+ const fs = context.services.getRequired(
824
838
  IFileSystemService_TOKEN
825
839
  );
826
840
  const searchPath = this.parseSearchPath(command) || fs.getCurrentDirectory();
@@ -844,6 +858,7 @@ var CliFindCommandProcessor = class {
844
858
  const resolvedBase = fs.resolvePath(searchPath);
845
859
  const nameRegex = namePattern ? this.globToRegex(namePattern) : null;
846
860
  const results = [];
861
+ this.fs = fs;
847
862
  this.walk(
848
863
  node,
849
864
  resolvedBase,
@@ -871,8 +886,9 @@ var CliFindCommandProcessor = class {
871
886
  return new RegExp(`^${regexStr}$`, "i");
872
887
  }
873
888
  walk(node, currentPath, depth, maxDepth, nameRegex, typeFilter, results) {
874
- if (!node.children) return;
875
- for (const child of node.children) {
889
+ const children = this.fs ? readableChildren(this.fs, currentPath) : node.children;
890
+ if (!children) return;
891
+ for (const child of children) {
876
892
  const childPath = currentPath === "/" ? `/${child.name}` : `${currentPath}/${child.name}`;
877
893
  let matches = true;
878
894
  if (nameRegex && !nameRegex.test(child.name)) {
@@ -955,7 +971,7 @@ var CliGrepCommandProcessor = class {
955
971
  ];
956
972
  }
957
973
  async processCommand(command, context) {
958
- const fs = context.services.get(
974
+ const fs = context.services.getRequired(
959
975
  IFileSystemService_TOKEN
960
976
  );
961
977
  const ignoreCase = command.args["ignore-case"] || command.args["i"] || false;
@@ -1103,8 +1119,9 @@ var CliGrepCommandProcessor = class {
1103
1119
  };
1104
1120
  }
1105
1121
  collectFiles(fs, basePath, node, result) {
1106
- if (!node.children) return;
1107
- for (const child of node.children) {
1122
+ const children = readableChildren(fs, basePath);
1123
+ if (!children) return;
1124
+ for (const child of children) {
1108
1125
  const childPath = basePath === "/" ? `/${child.name}` : `${basePath}/${child.name}`;
1109
1126
  if (child.type === "file") {
1110
1127
  result.push(childPath);
@@ -1125,7 +1142,7 @@ var CliTacCommandProcessor = class {
1125
1142
  this.metadata = { icon: "\u{1F504}", module: "file management" };
1126
1143
  }
1127
1144
  async processCommand(command, context) {
1128
- const fs = context.services.get(
1145
+ const fs = context.services.getRequired(
1129
1146
  IFileSystemService_TOKEN
1130
1147
  );
1131
1148
  const paths = this.parsePaths(command);
@@ -1297,7 +1314,7 @@ var CliSortCommandProcessor = class {
1297
1314
  ];
1298
1315
  }
1299
1316
  async processCommand(command, context) {
1300
- const fs = context.services.get(
1317
+ const fs = context.services.getRequired(
1301
1318
  IFileSystemService_TOKEN
1302
1319
  );
1303
1320
  const reverse = !!command.args["r"] || !!command.args["reverse"];
@@ -1450,7 +1467,7 @@ var CliUniqCommandProcessor = class {
1450
1467
  ];
1451
1468
  }
1452
1469
  async processCommand(command, context) {
1453
- const fs = context.services.get(
1470
+ const fs = context.services.getRequired(
1454
1471
  IFileSystemService_TOKEN
1455
1472
  );
1456
1473
  const showCount = !!command.args["c"] || !!command.args["count"];
@@ -1581,7 +1598,7 @@ var CliCutCommandProcessor = class {
1581
1598
  ];
1582
1599
  }
1583
1600
  async processCommand(command, context) {
1584
- const fs = context.services.get(
1601
+ const fs = context.services.getRequired(
1585
1602
  IFileSystemService_TOKEN
1586
1603
  );
1587
1604
  const delimiter = command.args["d"] || command.args["delimiter"] || " ";
@@ -1731,7 +1748,7 @@ var CliPasteCommandProcessor = class {
1731
1748
  ];
1732
1749
  }
1733
1750
  async processCommand(command, context) {
1734
- const fs = context.services.get(
1751
+ const fs = context.services.getRequired(
1735
1752
  IFileSystemService_TOKEN
1736
1753
  );
1737
1754
  const delimiter = command.args["d"] || command.args["delimiters"] || " ";
@@ -1898,7 +1915,7 @@ var CliTrCommandProcessor = class {
1898
1915
  ];
1899
1916
  }
1900
1917
  async processCommand(command, context) {
1901
- const fs = context.services.get(
1918
+ const fs = context.services.getRequired(
1902
1919
  IFileSystemService_TOKEN
1903
1920
  );
1904
1921
  const raw = command.value || "";
@@ -2053,7 +2070,7 @@ var CliStatCommandProcessor = class {
2053
2070
  this.parameters = [];
2054
2071
  }
2055
2072
  async processCommand(command, context) {
2056
- const fs = context.services.get(
2073
+ const fs = context.services.getRequired(
2057
2074
  IFileSystemService_TOKEN
2058
2075
  );
2059
2076
  const paths = this.parsePaths(command);
@@ -2063,6 +2080,10 @@ var CliStatCommandProcessor = class {
2063
2080
  }
2064
2081
  for (const path of paths) {
2065
2082
  const resolved = fs.resolvePath(path);
2083
+ if (fs.canTraverse && !fs.canTraverse(resolved)) {
2084
+ context.writer.writeError(`stat: cannot stat '${path}': Permission denied`);
2085
+ continue;
2086
+ }
2066
2087
  const node = fs.getNode(resolved);
2067
2088
  if (!node) {
2068
2089
  context.writer.writeError(
@@ -2106,7 +2127,7 @@ var CliChmodCommandProcessor = class {
2106
2127
  ];
2107
2128
  }
2108
2129
  async processCommand(command, context) {
2109
- const fs = context.services.get(
2130
+ const fs = context.services.getRequired(
2110
2131
  IFileSystemService_TOKEN
2111
2132
  );
2112
2133
  const { recursive, mode, paths } = this.parseArgs(command);
@@ -2226,7 +2247,7 @@ var CliChownCommandProcessor = class {
2226
2247
  ];
2227
2248
  }
2228
2249
  async processCommand(command, context) {
2229
- const fs = context.services.get(
2250
+ const fs = context.services.getRequired(
2230
2251
  IFileSystemService_TOKEN
2231
2252
  );
2232
2253
  const { recursive, ownerSpec, paths } = this.parseArgs(command);
@@ -2321,7 +2342,7 @@ var CliDuCommandProcessor = class {
2321
2342
  ];
2322
2343
  }
2323
2344
  async processCommand(command, context) {
2324
- const fs = context.services.get(
2345
+ const fs = context.services.getRequired(
2325
2346
  IFileSystemService_TOKEN
2326
2347
  );
2327
2348
  const { humanReadable, summaryOnly, maxDepth, paths } = this.parseArgs(command);
@@ -2331,6 +2352,10 @@ var CliDuCommandProcessor = class {
2331
2352
  }
2332
2353
  for (const path of paths) {
2333
2354
  const resolved = fs.resolvePath(path);
2355
+ if (fs.canTraverse && !fs.canTraverse(resolved)) {
2356
+ context.writer.writeError(`du: cannot access '${path}': Permission denied`);
2357
+ continue;
2358
+ }
2334
2359
  const node = fs.getNode(resolved);
2335
2360
  if (!node) {
2336
2361
  context.writer.writeError(
@@ -2344,6 +2369,7 @@ var CliDuCommandProcessor = class {
2344
2369
  );
2345
2370
  } else {
2346
2371
  const entries = [];
2372
+ this.fs = fs;
2347
2373
  this.collectSizes(node, resolved, 0, maxDepth, summaryOnly, entries);
2348
2374
  for (const entry of entries) {
2349
2375
  context.writer.writeln(
@@ -2366,8 +2392,9 @@ var CliDuCommandProcessor = class {
2366
2392
  return node.size;
2367
2393
  }
2368
2394
  let total = 0;
2369
- if (node.children) {
2370
- for (const child of node.children) {
2395
+ const children = this.fs ? readableChildren(this.fs, path) : node.children;
2396
+ if (children) {
2397
+ for (const child of children) {
2371
2398
  const childPath = path === "/" ? `/${child.name}` : `${path}/${child.name}`;
2372
2399
  total += this.collectSizes(
2373
2400
  child,
@@ -2418,7 +2445,7 @@ var CliLnCommandProcessor = class {
2418
2445
  ];
2419
2446
  }
2420
2447
  async processCommand(command, context) {
2421
- const fs = context.services.get(
2448
+ const fs = context.services.getRequired(
2422
2449
  IFileSystemService_TOKEN
2423
2450
  );
2424
2451
  const { symbolic, target, linkName } = this.parseArgs(command);
@@ -2492,7 +2519,7 @@ var CliSedCommandProcessor = class {
2492
2519
  ];
2493
2520
  }
2494
2521
  async processCommand(command, context) {
2495
- const fs = context.services.get(
2522
+ const fs = context.services.getRequired(
2496
2523
  IFileSystemService_TOKEN
2497
2524
  );
2498
2525
  const { inPlace, suppress, expressions, filePath } = this.parseRawCommand(command);
@@ -2835,7 +2862,7 @@ var CliAwkCommandProcessor = class {
2835
2862
  ];
2836
2863
  }
2837
2864
  async processCommand(command, context) {
2838
- const fs = context.services.get(
2865
+ const fs = context.services.getRequired(
2839
2866
  IFileSystemService_TOKEN
2840
2867
  );
2841
2868
  const { program, filePath, fieldSep } = this.parseArgs(command);
@@ -3311,7 +3338,7 @@ var CliDiffCommandProcessor = class {
3311
3338
  ];
3312
3339
  }
3313
3340
  async processCommand(command, context) {
3314
- const fs = context.services.get(
3341
+ const fs = context.services.getRequired(
3315
3342
  IFileSystemService_TOKEN
3316
3343
  );
3317
3344
  const unified = command.args["unified"] || command.args["u"] || false;
@@ -3497,7 +3524,7 @@ var CliTeeCommandProcessor = class {
3497
3524
  ];
3498
3525
  }
3499
3526
  async processCommand(command, context) {
3500
- const fs = context.services.get(
3527
+ const fs = context.services.getRequired(
3501
3528
  IFileSystemService_TOKEN
3502
3529
  );
3503
3530
  const append = command.args["append"] || command.args["a"] || false;
@@ -3591,7 +3618,7 @@ var CliXargsCommandProcessor = class {
3591
3618
  ];
3592
3619
  }
3593
3620
  async processCommand(command, context) {
3594
- const fs = context.services.get(
3621
+ const fs = context.services.getRequired(
3595
3622
  IFileSystemService_TOKEN
3596
3623
  );
3597
3624
  const replaceStr = command.args["replace"] || command.args["I"] || null;
@@ -3704,7 +3731,7 @@ var CliShCommandProcessor = class {
3704
3731
  ];
3705
3732
  }
3706
3733
  async processCommand(command, context) {
3707
- const fs = context.services.get(
3734
+ const fs = context.services.getRequired(
3708
3735
  IFileSystemService_TOKEN
3709
3736
  );
3710
3737
  let filePath = (command.value || "").trim();
@@ -3830,14 +3857,14 @@ function substituteVariables(input, variables) {
3830
3857
  );
3831
3858
  return result;
3832
3859
  }
3833
-
3834
- // src/lib/services/indexed-db-file-system.service.ts
3835
3860
  var DB_NAME = "qodalis-cli-filesystem";
3836
3861
  var STORE_NAME = "filesystem";
3837
3862
  var ROOT_KEY = "root";
3838
3863
  var CWD_KEY = "cwd";
3839
3864
  var HOME_KEY = "home";
3840
3865
  var DEFAULT_HOME = "/";
3866
+ var SYSTEM_OWNERSHIP = { uid: "root", gid: "admin" };
3867
+ var ANONYMOUS_OWNERSHIP = { uid: "nobody", gid: "nogroup" };
3841
3868
  function createSeedFileSystem() {
3842
3869
  const now = Date.now();
3843
3870
  return {
@@ -3885,6 +3912,88 @@ var IndexedDbFileSystemService = class {
3885
3912
  this.homePath = DEFAULT_HOME;
3886
3913
  this.currentUid = null;
3887
3914
  this.currentGroups = [];
3915
+ this.systemDepth = 0;
3916
+ }
3917
+ // --- Access control ---
3918
+ setAccessPolicy(policy) {
3919
+ this.accessPolicy = policy;
3920
+ }
3921
+ runAsSystem(fn) {
3922
+ this.systemDepth++;
3923
+ try {
3924
+ return fn();
3925
+ } finally {
3926
+ this.systemDepth--;
3927
+ }
3928
+ }
3929
+ reassignOwner(fromUid, toUid) {
3930
+ const from = fromUid.toLowerCase();
3931
+ if (!from || !toUid) return 0;
3932
+ let changed = 0;
3933
+ const visit = (node) => {
3934
+ if (node.ownership && node.ownership.uid.toLowerCase() === from) {
3935
+ node.ownership = { ...node.ownership, uid: toUid };
3936
+ changed++;
3937
+ }
3938
+ node.children?.forEach(visit);
3939
+ };
3940
+ visit(this.root);
3941
+ return changed;
3942
+ }
3943
+ canTraverse(path) {
3944
+ try {
3945
+ this.assertTraverse(this.resolvePath(path), "stat", path);
3946
+ return true;
3947
+ } catch {
3948
+ return false;
3949
+ }
3950
+ }
3951
+ get enforcing() {
3952
+ return !!this.accessPolicy && this.systemDepth === 0;
3953
+ }
3954
+ deny(op, path, reason = "Permission denied") {
3955
+ throw new CliAccessDeniedError(`${op}: ${path}: ${reason}`);
3956
+ }
3957
+ /** `x` on every existing directory on the way to `resolved` (excluding the target). */
3958
+ assertTraverse(resolved, op, display) {
3959
+ if (!this.enforcing) return;
3960
+ const parts = resolved.split("/").filter(Boolean);
3961
+ let current = this.root;
3962
+ for (let i = 0; i < parts.length; i++) {
3963
+ if (!current || current.type !== "directory") return;
3964
+ if (!this.accessPolicy.canAccess(current, "execute")) this.deny(op, display);
3965
+ current = current.children?.find((c) => c.name === parts[i]);
3966
+ }
3967
+ }
3968
+ assertAccess(node, action, op, display) {
3969
+ if (this.enforcing && !this.accessPolicy.canAccess(node, action)) this.deny(op, display);
3970
+ }
3971
+ /** Recursive delete: every directory inside must allow removing its entries (`w` + `x`, readable). */
3972
+ assertRemovableTree(dir, dirPath, display) {
3973
+ if (!this.enforcing) return;
3974
+ this.assertAccess(dir, "read", "rm", display);
3975
+ this.assertAccess(dir, "execute", "rm", display);
3976
+ if ((dir.children?.length ?? 0) > 0) this.assertAccess(dir, "write", "rm", display);
3977
+ for (const child of dir.children ?? []) {
3978
+ if (child.type === "directory") this.assertRemovableTree(child, `${dirPath}/${child.name}`, display);
3979
+ }
3980
+ }
3981
+ /** Copying reads everything it copies: `r` on every file, `r` + `x` on every directory. */
3982
+ assertReadableTree(node, display) {
3983
+ if (!this.enforcing) return;
3984
+ this.assertAccess(node, "read", "cp", display);
3985
+ if (node.type !== "directory") return;
3986
+ this.assertAccess(node, "execute", "cp", display);
3987
+ for (const child of node.children ?? []) {
3988
+ this.assertReadableTree(child, `${display.replace(/\/+$/, "")}/${child.name}`);
3989
+ }
3990
+ }
3991
+ /** Creating/removing entries needs `w` and `x` on the containing directory. */
3992
+ assertModifyDir(dir, dirPath, op, display) {
3993
+ if (!this.enforcing) return;
3994
+ this.assertTraverse(dirPath, op, display);
3995
+ this.assertAccess(dir, "execute", op, display);
3996
+ this.assertAccess(dir, "write", op, display);
3888
3997
  }
3889
3998
  // --- User context ---
3890
3999
  setCurrentUser(uid, groups) {
@@ -3892,7 +4001,9 @@ var IndexedDbFileSystemService = class {
3892
4001
  this.currentGroups = groups;
3893
4002
  }
3894
4003
  getDefaultOwnership() {
3895
- if (!this.currentUid) return void 0;
4004
+ if (!this.currentUid) {
4005
+ return this.accessPolicy ? { ...ANONYMOUS_OWNERSHIP } : void 0;
4006
+ }
3896
4007
  return {
3897
4008
  uid: this.currentUid,
3898
4009
  gid: this.currentGroups[0] || "users"
@@ -3911,6 +4022,8 @@ var IndexedDbFileSystemService = class {
3911
4022
  if (node.type !== "directory") {
3912
4023
  throw new Error(`cd: ${path}: Not a directory`);
3913
4024
  }
4025
+ this.assertTraverse(resolved, "cd", path);
4026
+ this.assertAccess(node, "execute", "cd", path);
3914
4027
  this.cwd = resolved;
3915
4028
  }
3916
4029
  getHomePath() {
@@ -3967,6 +4080,8 @@ var IndexedDbFileSystemService = class {
3967
4080
  if (node.type !== "directory") {
3968
4081
  throw new Error(`ls: ${path}: Not a directory`);
3969
4082
  }
4083
+ this.assertTraverse(this.resolvePath(path), "ls", path);
4084
+ this.assertAccess(node, "read", "ls", path);
3970
4085
  return node.children || [];
3971
4086
  }
3972
4087
  readFile(path) {
@@ -3977,6 +4092,8 @@ var IndexedDbFileSystemService = class {
3977
4092
  if (node.type === "directory") {
3978
4093
  throw new Error(`cat: ${path}: Is a directory`);
3979
4094
  }
4095
+ this.assertTraverse(this.resolvePath(path), "cat", path);
4096
+ this.assertAccess(node, "read", "cat", path);
3980
4097
  return node.content ?? "";
3981
4098
  }
3982
4099
  exists(path) {
@@ -4001,6 +4118,10 @@ var IndexedDbFileSystemService = class {
4001
4118
  }
4002
4119
  let child = current.children.find((c) => c.name === part);
4003
4120
  if (!child) {
4121
+ if (this.enforcing) {
4122
+ this.assertAccess(current, "execute", "mkdir", path);
4123
+ this.assertAccess(current, "write", "mkdir", path);
4124
+ }
4004
4125
  const now = Date.now();
4005
4126
  child = {
4006
4127
  name: part,
@@ -4016,6 +4137,8 @@ var IndexedDbFileSystemService = class {
4016
4137
  current.modifiedAt = now;
4017
4138
  } else if (child.type !== "directory") {
4018
4139
  throw new Error(`mkdir: ${part}: Not a directory`);
4140
+ } else {
4141
+ this.assertAccess(child, "execute", "mkdir", path);
4019
4142
  }
4020
4143
  current = child;
4021
4144
  }
@@ -4034,6 +4157,7 @@ var IndexedDbFileSystemService = class {
4034
4157
  if (parent.children?.find((c) => c.name === dirName)) {
4035
4158
  throw new Error(`mkdir: ${path}: File exists`);
4036
4159
  }
4160
+ this.assertModifyDir(parent, parentPath, "mkdir", path);
4037
4161
  const now = Date.now();
4038
4162
  if (!parent.children) {
4039
4163
  parent.children = [];
@@ -4068,6 +4192,12 @@ var IndexedDbFileSystemService = class {
4068
4192
  }
4069
4193
  const existing = parent.children?.find((c) => c.name === fileName);
4070
4194
  const now = Date.now();
4195
+ if (existing) {
4196
+ this.assertTraverse(resolved, "touch", path);
4197
+ this.assertAccess(existing, "write", "touch", path);
4198
+ } else {
4199
+ this.assertModifyDir(parent, parentPath, "touch", path);
4200
+ }
4071
4201
  if (existing) {
4072
4202
  existing.modifiedAt = now;
4073
4203
  } else {
@@ -4094,6 +4224,8 @@ var IndexedDbFileSystemService = class {
4094
4224
  if (node.type === "directory") {
4095
4225
  throw new Error(`write: ${path}: Is a directory`);
4096
4226
  }
4227
+ this.assertTraverse(resolved, "write", path);
4228
+ this.assertAccess(node, "write", "write", path);
4097
4229
  const now = Date.now();
4098
4230
  node.content = append ? (node.content ?? "") + content : content;
4099
4231
  node.size = new Blob([node.content]).size;
@@ -4122,6 +4254,8 @@ var IndexedDbFileSystemService = class {
4122
4254
  if (target.type === "directory" && !recursive) {
4123
4255
  throw new Error(`rm: ${path}: Is a directory`);
4124
4256
  }
4257
+ this.assertModifyDir(parent, parentPath, "rm", path);
4258
+ if (target.type === "directory") this.assertRemovableTree(target, resolved, path);
4125
4259
  parent.children.splice(index, 1);
4126
4260
  parent.modifiedAt = Date.now();
4127
4261
  }
@@ -4136,10 +4270,13 @@ var IndexedDbFileSystemService = class {
4136
4270
  `cp: -r not specified; omitting directory '${src}'`
4137
4271
  );
4138
4272
  }
4273
+ this.assertTraverse(srcResolved, "cp", src);
4274
+ this.assertReadableTree(srcNode, src);
4139
4275
  const destResolved = this.resolvePath(dest);
4140
4276
  const destNode = this.getNode(destResolved);
4141
4277
  if (destNode && destNode.type === "directory") {
4142
- const clone = this.cloneNode(srcNode);
4278
+ this.assertModifyDir(destNode, destResolved, "cp", dest);
4279
+ const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
4143
4280
  if (!destNode.children) {
4144
4281
  destNode.children = [];
4145
4282
  }
@@ -4159,7 +4296,8 @@ var IndexedDbFileSystemService = class {
4159
4296
  if (!parent || parent.type !== "directory") {
4160
4297
  throw new Error(`cp: ${parentPath}: No such file or directory`);
4161
4298
  }
4162
- const clone = this.cloneNode(srcNode);
4299
+ this.assertModifyDir(parent, parentPath, "cp", dest);
4300
+ const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
4163
4301
  clone.name = newName;
4164
4302
  if (!parent.children) {
4165
4303
  parent.children = [];
@@ -4191,10 +4329,12 @@ var IndexedDbFileSystemService = class {
4191
4329
  if (!srcParent || !srcParent.children) {
4192
4330
  throw new Error(`mv: ${src}: No such file or directory`);
4193
4331
  }
4332
+ this.assertModifyDir(srcParent, srcParentPath, "mv", src);
4194
4333
  const srcIndex = srcParent.children.findIndex(
4195
4334
  (c) => c.name === srcNode.name
4196
4335
  );
4197
4336
  if (destNode && destNode.type === "directory") {
4337
+ this.assertModifyDir(destNode, destResolved, "mv", dest);
4198
4338
  if (!destNode.children) {
4199
4339
  destNode.children = [];
4200
4340
  }
@@ -4217,6 +4357,7 @@ var IndexedDbFileSystemService = class {
4217
4357
  `mv: ${destParentPath}: No such file or directory`
4218
4358
  );
4219
4359
  }
4360
+ this.assertModifyDir(destParent, destParentPath, "mv", dest);
4220
4361
  srcParent.children.splice(srcIndex, 1);
4221
4362
  srcNode.name = newName;
4222
4363
  if (!destParent.children) {
@@ -4240,6 +4381,9 @@ var IndexedDbFileSystemService = class {
4240
4381
  if (!node) {
4241
4382
  throw new Error(`chmod: ${path}: No such file or directory`);
4242
4383
  }
4384
+ if (this.enforcing && !this.accessPolicy.canChangeOwnership(node)) {
4385
+ this.deny("chmod", path, "Operation not permitted");
4386
+ }
4243
4387
  node.permissions = permissions;
4244
4388
  }
4245
4389
  chown(path, ownership) {
@@ -4248,6 +4392,9 @@ var IndexedDbFileSystemService = class {
4248
4392
  if (!node) {
4249
4393
  throw new Error(`chown: ${path}: No such file or directory`);
4250
4394
  }
4395
+ if (this.enforcing && !this.accessPolicy.canChangeOwnership(node, ownership)) {
4396
+ this.deny("chown", path, "Operation not permitted");
4397
+ }
4251
4398
  node.ownership = ownership;
4252
4399
  }
4253
4400
  // --- Persistence ---
@@ -4289,6 +4436,18 @@ var IndexedDbFileSystemService = class {
4289
4436
  }
4290
4437
  }
4291
4438
  // --- Private helpers ---
4439
+ /** A copy belongs to whoever made it (like `cp` without `-p`) */
4440
+ ownedByCurrentUser(node) {
4441
+ const owner = this.getDefaultOwnership();
4442
+ if (owner) {
4443
+ const apply = (n) => {
4444
+ n.ownership = { ...owner };
4445
+ n.children?.forEach(apply);
4446
+ };
4447
+ apply(node);
4448
+ }
4449
+ return node;
4450
+ }
4292
4451
  cloneNode(node) {
4293
4452
  const now = Date.now();
4294
4453
  const clone = {
@@ -4330,8 +4489,6 @@ var IndexedDbFileSystemService = class {
4330
4489
  });
4331
4490
  }
4332
4491
  };
4333
-
4334
- // src/lib/services/server-file-system.service.ts
4335
4492
  var DEFAULT_HOME2 = "/home/user";
4336
4493
  function createSeedFileSystem2() {
4337
4494
  const now = Date.now();
@@ -4381,6 +4538,7 @@ var ServerFileSystemService = class {
4381
4538
  this.currentUid = null;
4382
4539
  this.currentGroups = [];
4383
4540
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
4541
+ this.headersProvider = options.headers;
4384
4542
  }
4385
4543
  // --- User context ---
4386
4544
  setCurrentUser(uid, groups) {
@@ -4788,7 +4946,14 @@ var ServerFileSystemService = class {
4788
4946
  // --- Private: server communication ---
4789
4947
  async serverFetch(endpoint, options) {
4790
4948
  const url = `${this.baseUrl}/api/qcli/fs${endpoint}`;
4791
- const response = await fetch(url, options);
4949
+ const mergedHeaders = {
4950
+ ...resolveHeaders(this.headersProvider),
4951
+ ...options?.headers ?? {}
4952
+ };
4953
+ const response = await fetch(url, {
4954
+ ...options,
4955
+ headers: mergedHeaders
4956
+ });
4792
4957
  if (!response.ok) {
4793
4958
  const body = await response.text();
4794
4959
  throw new Error(
@@ -4957,6 +5122,115 @@ var VirtualFsFileTransferService = class {
4957
5122
  return this._browserService.uploadFromBrowser(accept);
4958
5123
  }
4959
5124
  };
5125
+ var CliFilesPermissions = {
5126
+ /** Bypass all file permission checks */
5127
+ Admin: "files:admin",
5128
+ /** chmod/chown files you do not own */
5129
+ Chown: "files:chown"
5130
+ };
5131
+ var HOME_DIR_PERMISSIONS = "rwxr-x---";
5132
+ function authorization(context) {
5133
+ try {
5134
+ return context.services.get(ICliAuthorizationService_TOKEN);
5135
+ } catch {
5136
+ return void 0;
5137
+ }
5138
+ }
5139
+ function createFileAccessPolicy(context) {
5140
+ return {
5141
+ // Nodes without an owner (system seed, legacy data) belong to root:admin
5142
+ canAccess: (node, action) => cliCanAccess(
5143
+ context,
5144
+ node.ownership ? node : { ...node, ownership: SYSTEM_OWNERSHIP },
5145
+ action,
5146
+ [CliFilesPermissions.Admin]
5147
+ ),
5148
+ canChangeOwnership: (node, newOwnership) => {
5149
+ const auth = authorization(context);
5150
+ if (!auth) return true;
5151
+ if (auth.can(CliFilesPermissions.Admin) || auth.can(CliFilesPermissions.Chown)) return true;
5152
+ const principal = auth.getPrincipal();
5153
+ if (!principal) return false;
5154
+ const owns = !!node.ownership && (node.ownership.uid === principal.name || node.ownership.uid === principal.id);
5155
+ const keepsOwner = !newOwnership || newOwnership.uid === node.ownership?.uid || newOwnership.uid === principal.name;
5156
+ return owns && keepsOwner;
5157
+ }
5158
+ };
5159
+ }
5160
+ function followUserSession(context, fs) {
5161
+ let sessions;
5162
+ try {
5163
+ sessions = context.services.get(ICliUserSessionService_TOKEN);
5164
+ } catch {
5165
+ sessions = void 0;
5166
+ }
5167
+ if (!sessions) return void 0;
5168
+ let previousUser = null;
5169
+ return sessions.getUserSession().subscribe((session) => {
5170
+ if (!session?.user) {
5171
+ fs.setCurrentUser("", []);
5172
+ if (previousUser !== null || !canEnter(fs, fs.getCurrentDirectory())) {
5173
+ try {
5174
+ fs.setCurrentDirectory("/");
5175
+ void fs.persist();
5176
+ } catch {
5177
+ }
5178
+ }
5179
+ previousUser = null;
5180
+ return;
5181
+ }
5182
+ const user = session.user;
5183
+ fs.setCurrentUser(user.name, user.groups ?? []);
5184
+ if (user.homeDir) {
5185
+ const home = user.homeDir;
5186
+ const system = (fn) => fs.runAsSystem ? fs.runAsSystem(fn) : fn();
5187
+ if (!fs.exists(home)) {
5188
+ system(() => {
5189
+ fs.createDirectory(home, true);
5190
+ if (home !== "/") {
5191
+ fs.chown(home, { uid: user.name, gid: user.groups?.[0] ?? "users" });
5192
+ fs.chmod(home, HOME_DIR_PERMISSIONS);
5193
+ }
5194
+ });
5195
+ void fs.persist();
5196
+ }
5197
+ fs.setHomePath(home);
5198
+ if (previousUser !== user.id) {
5199
+ try {
5200
+ fs.setCurrentDirectory(home);
5201
+ } catch {
5202
+ }
5203
+ }
5204
+ }
5205
+ previousUser = user.id;
5206
+ });
5207
+ }
5208
+ function canEnter(fs, path) {
5209
+ try {
5210
+ fs.setCurrentDirectory(path);
5211
+ return true;
5212
+ } catch {
5213
+ return false;
5214
+ }
5215
+ }
5216
+ function followUserAccounts(context, fs) {
5217
+ let events;
5218
+ try {
5219
+ events = context.services.get(ICliUserEventService_TOKEN);
5220
+ } catch {
5221
+ events = void 0;
5222
+ }
5223
+ if (!events || !fs.reassignOwner) return void 0;
5224
+ return events.events$.subscribe((event) => {
5225
+ let changed = 0;
5226
+ if (event.type === "user_updated" && event.previousName && event.previousName !== event.user?.name) {
5227
+ changed = fs.reassignOwner(event.previousName, event.user.name);
5228
+ } else if (event.type === "user_deleted" && event.user?.name) {
5229
+ changed = fs.reassignOwner(event.user.name, SYSTEM_OWNERSHIP.uid);
5230
+ }
5231
+ if (changed > 0) void fs.persist().catch(() => void 0);
5232
+ });
5233
+ }
4960
5234
 
4961
5235
  // src/lib/completion/file-path-completion-provider.ts
4962
5236
  var FILE_COMMANDS = /* @__PURE__ */ new Set([
@@ -5057,10 +5331,10 @@ var CliUploadCommandProcessor = class {
5057
5331
  ];
5058
5332
  }
5059
5333
  async processCommand(command, context) {
5060
- const fs = context.services.get(
5334
+ const fs = context.services.getRequired(
5061
5335
  IFileSystemService_TOKEN
5062
5336
  );
5063
- const fileService = context.services.get(
5337
+ const fileService = context.services.getRequired(
5064
5338
  ICliFileTransferService_TOKEN
5065
5339
  );
5066
5340
  if (!fileService) {
@@ -5070,7 +5344,7 @@ var CliUploadCommandProcessor = class {
5070
5344
  const accept = command.args?.["accept"];
5071
5345
  const destPath = command.value?.trim() || void 0;
5072
5346
  context.spinner?.show("Waiting for file selection...");
5073
- context.setStatusText("Waiting for file selection");
5347
+ context.notifier.info("Waiting for file selection");
5074
5348
  const picked = await fileService.uploadFromBrowser(accept);
5075
5349
  if (!picked) {
5076
5350
  context.spinner?.hide();
@@ -5083,7 +5357,7 @@ var CliUploadCommandProcessor = class {
5083
5357
  filename.startsWith("/") ? filename : cwd + "/" + filename
5084
5358
  );
5085
5359
  context.spinner?.show(`Saving "${picked.name}" (${picked.content.length} bytes)...`);
5086
- context.setStatusText(`Saving ${picked.name}`);
5360
+ context.notifier.info(`Saving ${picked.name}`);
5087
5361
  try {
5088
5362
  fs.writeFile(resolved, picked.content);
5089
5363
  await fs.persist();
@@ -5151,6 +5425,10 @@ var filesModule = {
5151
5425
  new CliShCommandProcessor(),
5152
5426
  new CliUploadCommandProcessor()
5153
5427
  ],
5428
+ permissions: [
5429
+ { name: CliFilesPermissions.Admin, description: "Bypass file permission checks" },
5430
+ { name: CliFilesPermissions.Chown, description: "chmod/chown files owned by others" }
5431
+ ],
5154
5432
  services: [
5155
5433
  {
5156
5434
  provide: IFileSystemService_TOKEN,
@@ -5182,7 +5460,7 @@ var filesModule = {
5182
5460
  return { ...this, config };
5183
5461
  },
5184
5462
  async onInit(context) {
5185
- const fs = context.services.get(
5463
+ const fs = context.services.getRequired(
5186
5464
  IFileSystemService_TOKEN
5187
5465
  );
5188
5466
  await fs.initialize();
@@ -5193,7 +5471,7 @@ var filesModule = {
5193
5471
  }
5194
5472
  let dragDrop;
5195
5473
  try {
5196
- dragDrop = context.services.get(ICliDragDropService_TOKEN);
5474
+ dragDrop = context.services.getRequired(ICliDragDropService_TOKEN);
5197
5475
  } catch {
5198
5476
  }
5199
5477
  if (dragDrop) {
@@ -5222,13 +5500,25 @@ var filesModule = {
5222
5500
  });
5223
5501
  }
5224
5502
  },
5503
+ async onAfterBoot(context) {
5504
+ const fs = context.services.getRequired(IFileSystemService_TOKEN);
5505
+ fs.setAccessPolicy?.(createFileAccessPolicy(context));
5506
+ this._sessionSubscription?.unsubscribe();
5507
+ this._sessionSubscription = followUserSession(context, fs);
5508
+ this._accountsSubscription?.unsubscribe();
5509
+ this._accountsSubscription = followUserAccounts(context, fs);
5510
+ },
5225
5511
  async onDestroy(_context) {
5226
5512
  const sub = this._dragDropSubscription;
5227
5513
  sub?.unsubscribe();
5228
5514
  this._dragDropSubscription = void 0;
5515
+ this._sessionSubscription?.unsubscribe();
5516
+ this._sessionSubscription = void 0;
5517
+ this._accountsSubscription?.unsubscribe();
5518
+ this._accountsSubscription = void 0;
5229
5519
  }
5230
5520
  };
5231
5521
 
5232
- export { CliAwkCommandProcessor, CliBasenameCommandProcessor, CliCatCommandProcessor, CliCdCommandProcessor, CliChmodCommandProcessor, CliChownCommandProcessor, CliCpCommandProcessor, CliCutCommandProcessor, CliDiffCommandProcessor, CliDirnameCommandProcessor, CliDuCommandProcessor, CliEchoCommandProcessor, CliFindCommandProcessor, CliGrepCommandProcessor, CliHeadCommandProcessor, CliLnCommandProcessor, CliLsCommandProcessor, CliMkdirCommandProcessor, CliMvCommandProcessor, CliPasteCommandProcessor, CliPwdCommandProcessor, CliRmCommandProcessor, CliRmdirCommandProcessor, CliSedCommandProcessor, CliShCommandProcessor, CliSortCommandProcessor, CliStatCommandProcessor, CliTacCommandProcessor, CliTailCommandProcessor, CliTeeCommandProcessor, CliTouchCommandProcessor, CliTrCommandProcessor, CliTreeCommandProcessor, CliUniqCommandProcessor, CliWcCommandProcessor, CliXargsCommandProcessor, FilePathCompletionProvider, IFileSystemService_TOKEN, IndexedDbFileSystemService, ServerFileSystemService, VirtualFsFileTransferService, executeScript, filesModule };
5522
+ export { ANONYMOUS_OWNERSHIP, CliAwkCommandProcessor, CliBasenameCommandProcessor, CliCatCommandProcessor, CliCdCommandProcessor, CliChmodCommandProcessor, CliChownCommandProcessor, CliCpCommandProcessor, CliCutCommandProcessor, CliDiffCommandProcessor, CliDirnameCommandProcessor, CliDuCommandProcessor, CliEchoCommandProcessor, CliFilesPermissions, CliFindCommandProcessor, CliGrepCommandProcessor, CliHeadCommandProcessor, CliLnCommandProcessor, CliLsCommandProcessor, CliMkdirCommandProcessor, CliMvCommandProcessor, CliPasteCommandProcessor, CliPwdCommandProcessor, CliRmCommandProcessor, CliRmdirCommandProcessor, CliSedCommandProcessor, CliShCommandProcessor, CliSortCommandProcessor, CliStatCommandProcessor, CliTacCommandProcessor, CliTailCommandProcessor, CliTeeCommandProcessor, CliTouchCommandProcessor, CliTrCommandProcessor, CliTreeCommandProcessor, CliUniqCommandProcessor, CliWcCommandProcessor, CliXargsCommandProcessor, FilePathCompletionProvider, HOME_DIR_PERMISSIONS, IFileSystemService_TOKEN, IndexedDbFileSystemService, SYSTEM_OWNERSHIP, ServerFileSystemService, VirtualFsFileTransferService, createFileAccessPolicy, executeScript, filesModule, followUserAccounts, followUserSession };
5233
5523
  //# sourceMappingURL=public-api.mjs.map
5234
5524
  //# sourceMappingURL=public-api.mjs.map