touch-all 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,12 +10,13 @@ It behaves like `mkdir -p` and `touch` combined, creating directories and files
10
10
 
11
11
  - Accepts tree strings in **box-drawing** (`├──`, `└──`, `│`) or **indentation** (spaces) format
12
12
  - Trailing slash `/` marks a directory; no trailing `/` marks a file
13
- - Inline comments stripped automatically (`# ...`, `// ...`, `<- ...`, `← ...`)
14
13
  - Symlink creation with `link-name -> target` syntax
15
- - `--dry-run` parses and validates without touching the file system
16
- - `--verbose` prints every created path
17
- - Path traversal protection no item can escape the target directory
14
+ - Inline comments stripped automatically (`# ...`, `// ...`, `<- ...`, `← ...`)
15
+ - `--dry-run` (`-n`) parses and validates without touching the file system
16
+ - Prints a summary line on success (`✓ Done. N items created.`); `--verbose` adds per-item detail
17
+ - Path traversal protection — no file, folder, or symlink target can escape the target directory
18
18
  - Importable as a Node.js library with full TypeScript types
19
+ - Pipable interface to run as `echo tree | touch-all` in CI or scripts
19
20
 
20
21
  ## Installation
21
22
 
@@ -60,7 +61,7 @@ touch-all "..." --dry-run
60
61
  touch-all "..." -n
61
62
  ```
62
63
 
63
- - `--verbose` , `-v` – prints every created path to the console. Useful for seeing exactly what will be created, especially with complex structures. It's an alias for `--log-level info`
64
+ - `--verbose` , `-v` – prints every created path to the console. Useful for seeing exactly what will be created, especially with complex structures. By default only a summary line is printed on success (`✓ Done. N items created.`). `--verbose` adds per-item detail.
64
65
 
65
66
  ```bash
66
67
  touch-all "..." --verbose
@@ -120,10 +121,10 @@ Both formats produce identical results.
120
121
  | `name` | file |
121
122
  | `dir/sub/` | directory at an explicit subpath |
122
123
  | `dir/sub/file.ts` | file at an explicit subpath |
123
- | `... # comment` | ignored (stripped) |
124
+ | `... # comment` | ignored (stripped) |
124
125
  | `... // comment` | ignored (stripped) |
125
126
  | `... <- comment` | ignored (stripped) |
126
- | `... ← comment` | ignored (stripped) |
127
+ | `... ← comment` | ignored (stripped) |
127
128
  | `name -> target` | symlink pointing to `target` |
128
129
 
129
130
  Items at the root level (no indentation / no parent) are created directly inside the target directory.
@@ -134,17 +135,19 @@ Use `link-name -> target` to create a symlink. The target is passed as-is to the
134
135
 
135
136
  ```
136
137
  my-project/
137
- src/
138
- index.ts
139
- utils -> ../shared/utils.ts # symlink to a sibling directory
140
- shared/
141
- utils.ts
138
+ ├─ src/
139
+ │ ├─ index.ts
140
+ │ └─ utils -> ../shared/utils.ts # symlink to a sibling directory
141
+ └─ shared/
142
+ └─ utils.ts
142
143
  ```
143
144
 
144
145
  If `target` ends with `/`, the symlink is created as a directory symlink (relevant on Windows). The link name's suffix is ignored.
145
146
 
146
147
  > [!WARNING]
147
148
  > If any symlink target resolves outside the project root (`--path`), `touch-all` will prompt for confirmation before proceeding. Use `--yes` to skip the prompt in scripts or CI.
149
+ >
150
+ > When using `fileStructureCreator` directly as a library, outside-root symlinks are rejected by default with a `PathTraversalError`. Pass `{ allowOutsideSymlinks: true }` as the third argument to allow them.
148
151
 
149
152
  ## Library API
150
153
 
@@ -157,8 +160,8 @@ import {
157
160
  parserFolderStructure,
158
161
  fileStructureCreator,
159
162
  resolveProjectPathToBase,
163
+ isSymlinkOutsideRoot,
160
164
  PathTraversalError,
161
- FsError,
162
165
  } from 'touch-all'
163
166
  import type { ParserResult, ParserResultLineItem } from 'touch-all'
164
167
  ```
@@ -189,10 +192,12 @@ const items = parserFolderStructure(`
189
192
  // ]
190
193
  ```
191
194
 
192
- ### `fileStructureCreator(items: ParserResult, basePath: string): Effect<void, FsError | PathTraversalError>`
195
+ ### `fileStructureCreator(items, basePath, options?): Effect<void, PathTraversalError>`
193
196
 
194
197
  Creates the parsed structure on disk under `basePath`. Returns an [Effect](https://effect.website/).
195
198
 
199
+ By default, symlinks whose targets resolve outside `basePath` are rejected with `PathTraversalError`. Pass `{ allowOutsideSymlinks: true }` to allow them.
200
+
196
201
  ```ts
197
202
  import { Effect } from 'effect'
198
203
  import { NodeContext, NodeRuntime } from '@effect/platform-node'
@@ -200,22 +205,45 @@ import { NodeContext, NodeRuntime } from '@effect/platform-node'
200
205
  const projectDirectory = '/absolute/target/path'
201
206
  const items = parserFolderStructure(tree)
202
207
 
208
+ // Default: outside-root symlinks are rejected
203
209
  fileStructureCreator(items, projectDirectory).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain)
210
+
211
+ // Opt out of symlink containment check
212
+ fileStructureCreator(items, projectDirectory, { allowOutsideSymlinks: true }).pipe(
213
+ Effect.provide(NodeContext.layer),
214
+ NodeRuntime.runMain
215
+ )
216
+ ```
217
+
218
+ ### `isSymlinkOutsideRoot(linkPath, target, basePath, path): boolean`
219
+
220
+ Pure function that returns `true` if a symlink target resolves outside `basePath`. Useful for pre-validating items before passing them to `fileStructureCreator`.
221
+
222
+ ```ts
223
+ import { Path } from '@effect/platform'
224
+ import { Effect } from 'effect'
225
+ import { NodeContext } from '@effect/platform-node'
226
+ import { isSymlinkOutsideRoot } from 'touch-all'
227
+
228
+ Effect.gen(function* () {
229
+ const path = yield* Path.Path
230
+ const outside = isSymlinkOutsideRoot('src/link', '../../etc/passwd', '/project', path)
231
+ // true — target escapes /project
232
+ }).pipe(Effect.provide(NodeContext.layer))
204
233
  ```
205
234
 
206
235
  ### `resolveProjectPathToBase(projectPath: string, basePath: string): Effect<string, PathTraversalError>`
207
236
 
208
237
  Resolves `projectPath` relative to `basePath` and rejects any path that would escape `basePath` (path traversal protection).
209
238
 
210
- > [!CAUTION]
239
+ > [!WARNING]
211
240
  > `projectPath` cannot traverse outside of `basePath`. If `projectPath` is absolute, it treated as relative to `basePath`. If `projectPath` is relative, it is resolved against `basePath`. In either case, if the resulting path is outside of `basePath`, a `PathTraversalError` is thrown.
212
241
 
213
242
  ### Error types
214
243
 
215
- | Class | `_tag` | When thrown |
216
- | -------------------- | ---------------------- | --------------------------------------------------------------- |
217
- | `PathTraversalError` | `'PathTraversalError'` | Resolved path escapes `basePath`, or `basePath` is not absolute |
218
- | `FsError` | `'FsError'` | `mkdirSync`, `writeFileSync`, or `symlinkSync` fails |
244
+ | Class | `_tag` | When thrown |
245
+ | -------------------- | ---------------------- | -------------------------------------------------------------------------- |
246
+ | `PathTraversalError` | `'PathTraversalError'` | A file/folder path or symlink target escapes `basePath` (unless opted out) |
219
247
 
220
248
  ## License
221
249
 
@@ -0,0 +1,2 @@
1
+ import { Effect } from 'effect';
2
+ export declare const cli: (args: ReadonlyArray<string>) => Effect.Effect<void, import("./errors").PathTraversalError | import("@effect/platform/Error").PlatformError | Error | import("@effect/cli/ValidationError").ValidationError, import("@effect/cli/CliApp").CliApp.Environment>;
@@ -7,12 +7,3 @@ export declare class PathTraversalError {
7
7
  constructor(path: string);
8
8
  toString(): string;
9
9
  }
10
- /**
11
- * Error type for file system operation failures
12
- */
13
- export declare class FsError {
14
- readonly message: string;
15
- readonly _tag = "FsError";
16
- constructor(message: string);
17
- toString(): string;
18
- }
@@ -0,0 +1,7 @@
1
+ import { FileSystem, Path } from '@effect/platform';
2
+ import { Effect } from 'effect';
3
+ import { type ParserResult } from './types';
4
+ import { PathTraversalError } from './errors';
5
+ export declare const fileStructureCreator: (items: ParserResult, basePath: string, options?: {
6
+ allowOutsideSymlinks?: boolean;
7
+ }) => Effect.Effect<void, PathTraversalError | import("@effect/platform/Error").PlatformError, Path.Path | FileSystem.FileSystem>;
@@ -0,0 +1,20 @@
1
+ import { Path } from '@effect/platform';
2
+ import { Effect } from 'effect';
3
+ import { PathTraversalError } from './errors';
4
+ /**
5
+ * Returns true if the symlink target escapes the base directory.
6
+ *
7
+ * @param linkPath string - Path of the symlink relative to project root.
8
+ * @param target string - The symlink target value.
9
+ * @param basePath string - Project root (absolute or relative).
10
+ * @param path Path.Path - Platform path service instance.
11
+ */
12
+ export declare const isSymlinkOutsideRoot: (linkPath: string, target: string, basePath: string, path: Path.Path) => boolean;
13
+ /**
14
+ * Safely normalize a user-supplied path against a base directory.
15
+ * Fails with PathTraversalError if the resolved path escapes the base.
16
+ *
17
+ * @param projectPath string - Path relative to project.
18
+ * @param basePath string - Must be absolute.
19
+ */
20
+ export declare const resolveProjectPathToBase: (projectPath: string, basePath: string) => Effect.Effect<string, PathTraversalError, Path.Path>;
@@ -0,0 +1,5 @@
1
+ export { fileStructureCreator } from './fsGenerator';
2
+ export { isSymlinkOutsideRoot, resolveProjectPathToBase } from './fsNormalizator';
3
+ export { PathTraversalError } from './errors';
4
+ export type { ParserResult, ParserResultLineItem } from './types';
5
+ export { parserFolderStructure } from './parser';
package/dist/lib/index.js CHANGED
@@ -768,12 +768,9 @@ var TypeId3 = TypeId2;
768
768
  var Path2 = Path;
769
769
  var layer2 = layer;
770
770
  // src/fsGenerator.ts
771
- import { Effect as Effect4 } from "effect";
771
+ import { Console, Effect as Effect4 } from "effect";
772
772
 
773
- // src/fsNormalizator.ts
774
- import { Effect as Effect3 } from "effect";
775
-
776
- // src/_commonErrors.ts
773
+ // src/errors.ts
777
774
  class PathTraversalError {
778
775
  path;
779
776
  _tag = "PathTraversalError";
@@ -785,18 +782,14 @@ class PathTraversalError {
785
782
  }
786
783
  }
787
784
 
788
- class FsError {
789
- message;
790
- _tag = "FsError";
791
- constructor(message) {
792
- this.message = message;
793
- }
794
- toString() {
795
- return `${this._tag}: ${this.message}`;
796
- }
797
- }
798
-
799
785
  // src/fsNormalizator.ts
786
+ import { Effect as Effect3 } from "effect";
787
+ var isSymlinkOutsideRoot = (linkPath, target, basePath, path) => {
788
+ const resolvedBase = path.resolve(basePath);
789
+ const symlinkDir = path.resolve(resolvedBase, path.dirname(linkPath));
790
+ const resolvedTarget = path.resolve(symlinkDir, target);
791
+ return path.relative(resolvedBase, resolvedTarget).startsWith("..");
792
+ };
800
793
  var resolveProjectPathToBase = (projectPath, basePath) => Effect3.gen(function* () {
801
794
  const path = yield* exports_Path.Path;
802
795
  const filePath = projectPath.startsWith(path.sep) ? `.${projectPath}` : projectPath;
@@ -809,7 +802,7 @@ var resolveProjectPathToBase = (projectPath, basePath) => Effect3.gen(function*
809
802
  });
810
803
 
811
804
  // src/fsGenerator.ts
812
- var fileStructureCreator = (items, basePath) => Effect4.gen(function* () {
805
+ var fileStructureCreator = (items, basePath, options) => Effect4.gen(function* () {
813
806
  const fs = yield* exports_FileSystem.FileSystem;
814
807
  const path = yield* exports_Path.Path;
815
808
  yield* Effect4.logInfo(`Creating structure in: ${basePath}`);
@@ -823,6 +816,9 @@ var fileStructureCreator = (items, basePath) => Effect4.gen(function* () {
823
816
  yield* Effect4.logInfo(` Created file: ${item.path}`);
824
817
  break;
825
818
  case "symlink":
819
+ if (!options?.allowOutsideSymlinks && isSymlinkOutsideRoot(item.path, item.target, basePath, path)) {
820
+ yield* Effect4.fail(new PathTraversalError(`symlink target escapes root: ${item.path} -> ${item.target} (base: ${basePath})`));
821
+ }
826
822
  yield* fs.makeDirectory(dir, { recursive: true });
827
823
  yield* fs.symlink(item.target, fullPath);
828
824
  yield* Effect4.logInfo(` Created symlink: ${item.path} -> ${item.target}`);
@@ -833,8 +829,7 @@ var fileStructureCreator = (items, basePath) => Effect4.gen(function* () {
833
829
  break;
834
830
  }
835
831
  }
836
- yield* Effect4.logInfo(`
837
- ✓ Structure created successfully!`);
832
+ yield* Console.log(`✓ Done. ${items.length} items created.`);
838
833
  });
839
834
  // src/parser.ts
840
835
  var parserFolderStructure = (treeString) => {
@@ -895,7 +890,7 @@ function countLeadingSpaces(str) {
895
890
  export {
896
891
  resolveProjectPathToBase,
897
892
  parserFolderStructure,
893
+ isSymlinkOutsideRoot,
898
894
  fileStructureCreator,
899
- PathTraversalError,
900
- FsError
895
+ PathTraversalError
901
896
  };
@@ -1,2 +1,2 @@
1
- import type { ParserResult } from './_commonTypes';
1
+ import type { ParserResult } from './types';
2
2
  export declare const parserFolderStructure: (treeString: string) => ParserResult;
@@ -0,0 +1,2 @@
1
+ import { Effect } from 'effect';
2
+ export declare const readStdin: Effect.Effect<string, Error, never>;
@@ -0,0 +1,4 @@
1
+ import { Path, Terminal } from '@effect/platform';
2
+ import { Effect } from 'effect';
3
+ import { type ParserResult } from './types';
4
+ export declare const checkOutsideSymlinks: (treeString: string, projectRoot: string, yes: boolean) => Effect.Effect<ParserResult, Error, Path.Path | Terminal.Terminal>;
@@ -15,12 +15,12 @@ var __export = (target, all) => {
15
15
  };
16
16
 
17
17
  // src/touch-all.ts
18
- import { Effect as Effect6, Logger as Logger2, LogLevel as LogLevel2 } from "effect";
18
+ import { Effect as Effect8, Logger as Logger2, LogLevel as LogLevel2 } from "effect";
19
19
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
20
20
 
21
21
  // src/cli.ts
22
- import { createInterface } from "node:readline";
23
22
  import { Args, Command as Command2, Options } from "@effect/cli";
23
+ import { Console as Console3, Effect as Effect7, Logger, LogLevel, Option as Option2 } from "effect";
24
24
  // node_modules/@effect/platform/dist/esm/FileSystem.js
25
25
  var exports_FileSystem = {};
26
26
  __export(exports_FileSystem, {
@@ -794,8 +794,73 @@ class QuitException extends (/* @__PURE__ */ TaggedError2("QuitException")) {
794
794
  }
795
795
  var isQuitException = (u) => typeof u === "object" && u != null && ("_tag" in u) && u._tag === "QuitException";
796
796
  var Terminal = tag2;
797
- // src/cli.ts
798
- import { Console, Effect as Effect5, Logger, LogLevel, Option as Option2 } from "effect";
797
+ // src/fsGenerator.ts
798
+ import { Console, Effect as Effect4 } from "effect";
799
+
800
+ // src/errors.ts
801
+ class PathTraversalError {
802
+ path;
803
+ _tag = "PathTraversalError";
804
+ constructor(path) {
805
+ this.path = path;
806
+ }
807
+ toString() {
808
+ return `${this._tag}: The path cannot be used ${this.path}`;
809
+ }
810
+ }
811
+
812
+ // src/fsNormalizator.ts
813
+ import { Effect as Effect3 } from "effect";
814
+ var isSymlinkOutsideRoot = (linkPath, target, basePath, path) => {
815
+ const resolvedBase = path.resolve(basePath);
816
+ const symlinkDir = path.resolve(resolvedBase, path.dirname(linkPath));
817
+ const resolvedTarget = path.resolve(symlinkDir, target);
818
+ return path.relative(resolvedBase, resolvedTarget).startsWith("..");
819
+ };
820
+ var resolveProjectPathToBase = (projectPath, basePath) => Effect3.gen(function* () {
821
+ const path = yield* exports_Path.Path;
822
+ const filePath = projectPath.startsWith(path.sep) ? `.${projectPath}` : projectPath;
823
+ const absolute = path.resolve(basePath, filePath);
824
+ const rel = path.relative(basePath, absolute);
825
+ if (rel.startsWith("..")) {
826
+ return yield* Effect3.fail(new PathTraversalError(["project:", projectPath, "base:", basePath].join(" ")));
827
+ }
828
+ return absolute;
829
+ });
830
+
831
+ // src/fsGenerator.ts
832
+ var fileStructureCreator = (items, basePath, options) => Effect4.gen(function* () {
833
+ const fs = yield* exports_FileSystem.FileSystem;
834
+ const path = yield* exports_Path.Path;
835
+ yield* Effect4.logInfo(`Creating structure in: ${basePath}`);
836
+ for (const item of items) {
837
+ const fullPath = yield* resolveProjectPathToBase(item.path, basePath);
838
+ const dir = path.dirname(fullPath);
839
+ switch (item.type) {
840
+ case "file":
841
+ yield* fs.makeDirectory(dir, { recursive: true });
842
+ yield* fs.writeFile(fullPath, new Uint8Array);
843
+ yield* Effect4.logInfo(` Created file: ${item.path}`);
844
+ break;
845
+ case "symlink":
846
+ if (!options?.allowOutsideSymlinks && isSymlinkOutsideRoot(item.path, item.target, basePath, path)) {
847
+ yield* Effect4.fail(new PathTraversalError(`symlink target escapes root: ${item.path} -> ${item.target} (base: ${basePath})`));
848
+ }
849
+ yield* fs.makeDirectory(dir, { recursive: true });
850
+ yield* fs.symlink(item.target, fullPath);
851
+ yield* Effect4.logInfo(` Created symlink: ${item.path} -> ${item.target}`);
852
+ break;
853
+ case "folder":
854
+ yield* fs.makeDirectory(fullPath, { recursive: true });
855
+ yield* Effect4.logInfo(` Created directory: ${item.path}`);
856
+ break;
857
+ }
858
+ }
859
+ yield* Console.log(`✓ Done. ${items.length} items created.`);
860
+ });
861
+
862
+ // src/symlinkGuard.ts
863
+ import { Console as Console2, Effect as Effect5 } from "effect";
799
864
 
800
865
  // src/parser.ts
801
866
  var parserFolderStructure = (treeString) => {
@@ -854,68 +919,48 @@ function countLeadingSpaces(str) {
854
919
  return match2 ? match2[0].length : 0;
855
920
  }
856
921
 
857
- // src/fsGenerator.ts
858
- import { Effect as Effect4 } from "effect";
859
-
860
- // src/fsNormalizator.ts
861
- import { Effect as Effect3 } from "effect";
862
-
863
- // src/_commonErrors.ts
864
- class PathTraversalError {
865
- path;
866
- _tag = "PathTraversalError";
867
- constructor(path) {
868
- this.path = path;
869
- }
870
- toString() {
871
- return `${this._tag}: The path cannot be used ${this.path}`;
872
- }
873
- }
874
-
875
- // src/fsNormalizator.ts
876
- var resolveProjectPathToBase = (projectPath, basePath) => Effect3.gen(function* () {
877
- const path = yield* exports_Path.Path;
878
- const filePath = projectPath.startsWith(path.sep) ? `.${projectPath}` : projectPath;
879
- const absolute = path.resolve(basePath, filePath);
880
- const rel = path.relative(basePath, absolute);
881
- if (rel.startsWith("..")) {
882
- return yield* Effect3.fail(new PathTraversalError(["project:", projectPath, "base:", basePath].join(" ")));
922
+ // src/symlinkGuard.ts
923
+ var checkOutsideSymlinks = (treeString, projectRoot, yes) => Effect5.gen(function* () {
924
+ const nodePath = yield* exports_Path.Path;
925
+ const items = parserFolderStructure(treeString);
926
+ const resolvedRoot = nodePath.resolve(projectRoot);
927
+ const symlinks = items.filter((item) => item.type === "symlink");
928
+ const outsideSymlinks = symlinks.filter((item) => isSymlinkOutsideRoot(item.path, item.target, resolvedRoot, nodePath));
929
+ if (outsideSymlinks.length === 0)
930
+ return items;
931
+ if (yes)
932
+ return items;
933
+ const listing = outsideSymlinks.map((item) => ` ${item.path} -> ${item.target}`).join(`
934
+ `);
935
+ yield* Console2.log(`Warning: the following symlinks point outside PROJECT_ROOT (${resolvedRoot}):
936
+ ${listing}`);
937
+ const terminal = yield* exports_Terminal.Terminal;
938
+ yield* terminal.display("Proceed? (y/N) ");
939
+ const answer = yield* terminal.readLine.pipe(Effect5.catchTag("QuitException", () => Effect5.fail(new Error(`Non-interactive mode: use --yes to allow symlinks outside project root`))));
940
+ if (answer.trim().toLowerCase() !== "y") {
941
+ yield* Console2.log("Aborted.");
942
+ return yield* Effect5.fail(new Error("Aborted by user"));
883
943
  }
884
- return absolute;
944
+ return items;
885
945
  });
886
946
 
887
- // src/fsGenerator.ts
888
- var fileStructureCreator = (items, basePath) => Effect4.gen(function* () {
889
- const fs = yield* exports_FileSystem.FileSystem;
890
- const path = yield* exports_Path.Path;
891
- yield* Effect4.logInfo(`Creating structure in: ${basePath}`);
892
- for (const item of items) {
893
- const fullPath = yield* resolveProjectPathToBase(item.path, basePath);
894
- const dir = path.dirname(fullPath);
895
- switch (item.type) {
896
- case "file":
897
- yield* fs.makeDirectory(dir, { recursive: true });
898
- yield* fs.writeFile(fullPath, new Uint8Array);
899
- yield* Effect4.logInfo(` Created file: ${item.path}`);
900
- break;
901
- case "symlink":
902
- yield* fs.makeDirectory(dir, { recursive: true });
903
- yield* fs.symlink(item.target, fullPath);
904
- yield* Effect4.logInfo(` Created symlink: ${item.path} -> ${item.target}`);
905
- break;
906
- case "folder":
907
- yield* fs.makeDirectory(fullPath, { recursive: true });
908
- yield* Effect4.logInfo(` Created directory: ${item.path}`);
909
- break;
910
- }
911
- }
912
- yield* Effect4.logInfo(`
913
- ✓ Structure created successfully!`);
947
+ // src/stdin.ts
948
+ import { createInterface } from "node:readline";
949
+ import { Effect as Effect6 } from "effect";
950
+ var readStdin = Effect6.tryPromise({
951
+ try: () => new Promise((resolve3) => {
952
+ const rl = createInterface({ input: process.stdin });
953
+ const lines = [];
954
+ rl.on("line", (line) => lines.push(line));
955
+ rl.on("close", () => resolve3(lines.join(`
956
+ `)));
957
+ }),
958
+ catch: (e) => new Error(`Failed to read stdin: ${String(e)}`)
914
959
  });
915
960
  // package.json
916
961
  var package_default = {
917
962
  name: "touch-all",
918
- version: "2.0.0",
963
+ version: "2.1.0",
919
964
  description: "CLI tool to create folder structures from markdown tree representations",
920
965
  keywords: [
921
966
  "cli",
@@ -964,7 +1009,6 @@ var package_default = {
964
1009
  effect: "^3.21.0"
965
1010
  },
966
1011
  devDependencies: {
967
- "@total-typescript/tsconfig": "1.0.3",
968
1012
  "@types/bun": "1.3.11",
969
1013
  "@types/node": "25.5.0",
970
1014
  oxfmt: "0.41.0",
@@ -980,88 +1024,43 @@ var package_default = {
980
1024
  };
981
1025
 
982
1026
  // src/cli.ts
1027
+ var { name, version } = package_default;
983
1028
  var treeArg = Args.text({ name: "tree" }).pipe(Args.withDescription("Multiline string representing the directory tree structure"), Args.optional);
984
1029
  var pathOption = Options.directory("path").pipe(Options.withAlias("p"), Options.withDefault("."), Options.withDescription("Target folder path (defaults to current directory)"));
985
1030
  var dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDefault(false), Options.withDescription("Parse and validate the tree without writing to the file system"));
986
1031
  var verboseOption = Options.boolean("verbose").pipe(Options.withAlias("v"), Options.withDefault(false), Options.withDescription("Log to console extra information about creating a directory tree"));
987
1032
  var yesOption = Options.boolean("yes").pipe(Options.withAlias("y"), Options.withDefault(false), Options.withDescription("Skip confirmation prompt when symlinks point outside the project root"));
988
- var command = Command2.make("touch-all", {
1033
+ var command = Command2.make(name, {
989
1034
  tree: treeArg,
990
1035
  path: pathOption,
991
1036
  dryRun: dryRunOption,
992
1037
  verbose: verboseOption,
993
1038
  yes: yesOption
994
1039
  }).pipe(Command2.withDescription("Create directory structure from a tree representation"), Command2.withHandler(({ tree, path: targetPath, dryRun = false, verbose, yes }) => {
995
- const readStdin = Effect5.gen(function* () {
996
- if (process.stdin.isTTY) {
997
- yield* Console.log("Paste your tree structure and press Ctrl+D when done:");
998
- }
999
- return yield* Effect5.tryPromise({
1000
- try: () => new Promise((resolve3) => {
1001
- const rl = createInterface({ input: process.stdin });
1002
- const lines = [];
1003
- rl.on("line", (line) => lines.push(line));
1004
- rl.on("close", () => resolve3(lines.join(`
1005
- `)));
1006
- }),
1007
- catch: (e) => new Error(`Failed to read stdin: ${String(e)}`)
1008
- });
1009
- });
1010
- const checkOutsideSymlinks = (treeString, projectRoot) => Effect5.gen(function* () {
1011
- const nodePath = yield* exports_Path.Path;
1012
- const items = parserFolderStructure(treeString);
1013
- const resolvedRoot = nodePath.resolve(projectRoot);
1014
- const outsideSymlinks = items.filter((item) => item.type === "symlink").filter((item) => {
1015
- const symlinkDir = nodePath.resolve(resolvedRoot, nodePath.dirname(item.path));
1016
- const resolvedTarget = nodePath.resolve(symlinkDir, item.target);
1017
- return nodePath.relative(resolvedRoot, resolvedTarget).startsWith("..");
1018
- });
1019
- if (outsideSymlinks.length === 0)
1020
- return items;
1021
- if (yes)
1022
- return items;
1023
- if (!process.stdin.isTTY) {
1024
- yield* Console.error(`Cannot prompt in non-interactive mode: tree contains symlinks outside PROJECT_ROOT (${resolvedRoot}). Use --yes to proceed.`);
1025
- return yield* Effect5.fail(new Error("Non-interactive mode with outside symlinks"));
1026
- }
1027
- const listing = outsideSymlinks.map((item) => ` ${item.path} -> ${item.type === "symlink" ? item.target : ""}`).join(`
1028
- `);
1029
- yield* Console.log(`Warning: the following symlinks point outside PROJECT_ROOT (${resolvedRoot}):
1030
- ${listing}`);
1031
- const terminal = yield* exports_Terminal.Terminal;
1032
- yield* terminal.display("Proceed? (y/N) ");
1033
- const answer = yield* terminal.readLine.pipe(Effect5.catchTag("QuitException", () => Effect5.succeed("")));
1034
- if (answer.trim().toLowerCase() !== "y") {
1035
- yield* Console.log("Aborted.");
1036
- return yield* Effect5.fail(new Error("Aborted by user"));
1037
- }
1038
- return items;
1039
- });
1040
- const program = Effect5.gen(function* () {
1041
- const treeString = Option2.isSome(tree) ? tree.value : yield* readStdin;
1040
+ const program = Effect7.gen(function* () {
1041
+ const rawTree = Option2.isSome(tree) ? tree.value : yield* readStdin;
1042
+ const treeString = Option2.isSome(tree) ? rawTree.replace(/\\(\\|n)/g, (_, c) => c === "n" ? `
1043
+ ` : "\\") : rawTree;
1042
1044
  if (dryRun) {
1043
- yield* Effect5.logInfo("Running in dry mode. No one file system node will be created.");
1045
+ yield* Effect7.logInfo("Running in dry mode. No one file system node will be created.");
1044
1046
  }
1045
- yield* Effect5.logInfo("Parsing tree structure...");
1046
- const items = yield* checkOutsideSymlinks(treeString, targetPath);
1047
+ yield* Effect7.logInfo("Parsing tree structure...");
1048
+ const items = yield* checkOutsideSymlinks(treeString, targetPath, yes);
1047
1049
  if (items.length === 0) {
1048
- yield* Console.error("No valid items found in the tree structure");
1049
- return yield* Effect5.fail(new Error("Invalid tree structure"));
1050
+ yield* Console3.error("No valid items found in the tree structure");
1051
+ return yield* Effect7.fail(new Error("Invalid tree structure"));
1050
1052
  }
1051
- yield* Effect5.logInfo(`Found ${items.length} items to create`);
1052
- yield* Effect5.logInfo(`Found:
1053
+ yield* Effect7.logInfo(`Found ${items.length} items to create`);
1054
+ yield* Effect7.logInfo(`Found:
1053
1055
  ${items.map((i) => `${i.path}
1054
1056
  `).join("")}`);
1055
1057
  if (!dryRun) {
1056
- yield* fileStructureCreator(items, targetPath);
1058
+ yield* fileStructureCreator(items, targetPath, { allowOutsideSymlinks: yes });
1057
1059
  }
1058
1060
  });
1059
1061
  return verbose ? program.pipe(Logger.withMinimumLogLevel(LogLevel.Info)) : program;
1060
1062
  }));
1061
- var cli = Command2.run(command, {
1062
- name: package_default.name,
1063
- version: package_default.version
1064
- });
1063
+ var cli = Command2.run(command, { name, version });
1065
1064
 
1066
1065
  // src/touch-all.ts
1067
- Effect6.suspend(() => cli(process.argv)).pipe(Logger2.withMinimumLogLevel(LogLevel2.Warning), Effect6.provide(NodeContext.layer), NodeRuntime.runMain);
1066
+ Effect8.suspend(() => cli(process.argv)).pipe(Logger2.withMinimumLogLevel(LogLevel2.Warning), Effect8.provide(NodeContext.layer), NodeRuntime.runMain);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "touch-all",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "CLI tool to create folder structures from markdown tree representations",
5
5
  "keywords": [
6
6
  "cli",
@@ -49,7 +49,6 @@
49
49
  "effect": "^3.21.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@total-typescript/tsconfig": "1.0.3",
53
52
  "@types/bun": "1.3.11",
54
53
  "@types/node": "25.5.0",
55
54
  "oxfmt": "0.41.0",
@@ -1,2 +0,0 @@
1
- import { Effect } from 'effect';
2
- export declare const cli: (args: ReadonlyArray<string>) => Effect.Effect<void, import("./_commonErrors").PathTraversalError | import("@effect/platform/Error").PlatformError | Error | import("@effect/cli/ValidationError").ValidationError, import("@effect/cli/CliApp").CliApp.Environment>;
@@ -1,4 +0,0 @@
1
- import { FileSystem, Path } from '@effect/platform';
2
- import { Effect } from 'effect';
3
- import { type ParserResult } from './_commonTypes';
4
- export declare const fileStructureCreator: (items: ParserResult, basePath: string) => Effect.Effect<void, import("./_commonErrors").PathTraversalError | import("@effect/platform/Error").PlatformError, Path.Path | FileSystem.FileSystem>;
@@ -1,11 +0,0 @@
1
- import { Path } from '@effect/platform';
2
- import { Effect } from 'effect';
3
- import { PathTraversalError } from './_commonErrors';
4
- /**
5
- * Safely normalize a user-supplied path against a base directory.
6
- * Fails with PathTraversalError if the resolved path escapes the base.
7
- *
8
- * @param projectPath string - Path relative to project.
9
- * @param basePath string - Must be absolute.
10
- */
11
- export declare const resolveProjectPathToBase: (projectPath: string, basePath: string) => Effect.Effect<string, PathTraversalError, Path.Path>;
@@ -1,5 +0,0 @@
1
- export { fileStructureCreator } from './fsGenerator';
2
- export { resolveProjectPathToBase } from './fsNormalizator';
3
- export { PathTraversalError, FsError } from './_commonErrors';
4
- export type { ParserResult, ParserResultLineItem } from './_commonTypes';
5
- export { parserFolderStructure } from './parser';
File without changes
File without changes