touch-all 1.2.1 → 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,11 +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 (`# ...` and `// ...`)
14
- - `--dry-run` parses and validates without touching the file system
15
- - `--verbose` prints every created path
16
- - Path traversal protection no item can escape the target directory
13
+ - Symlink creation with `link-name -> target` syntax
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
17
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
18
20
 
19
21
  ## Installation
20
22
 
@@ -59,13 +61,20 @@ touch-all "..." --dry-run
59
61
  touch-all "..." -n
60
62
  ```
61
63
 
62
- - `--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.
63
65
 
64
66
  ```bash
65
67
  touch-all "..." --verbose
66
68
  touch-all "..." -v
67
69
  ```
68
70
 
71
+ - `--yes` , `-y` – skips the confirmation prompt when symlinks point outside the project root. Required in non-interactive environments (scripts, CI).
72
+
73
+ ```bash
74
+ touch-all "..." --yes
75
+ touch-all "..." -y
76
+ ```
77
+
69
78
  - `--completions` – generates a completion script for a specific shell. Supported shells: `sh`, `bash`, `fish`, `zsh`.
70
79
  - `--log-level` – sets the minimum log level for a command. Supported levels: `all`, `trace`, `debug`, `info`, `warning`, `error`, `fatal`, `none`. The default log level is `warning`.
71
80
  - `--help` , `-h` – shows the help documentation for a command.
@@ -112,11 +121,34 @@ Both formats produce identical results.
112
121
  | `name` | file |
113
122
  | `dir/sub/` | directory at an explicit subpath |
114
123
  | `dir/sub/file.ts` | file at an explicit subpath |
115
- | `... # comment` | ignored (stripped) |
124
+ | `... # comment` | ignored (stripped) |
116
125
  | `... // comment` | ignored (stripped) |
126
+ | `... <- comment` | ignored (stripped) |
127
+ | `... ← comment` | ignored (stripped) |
128
+ | `name -> target` | symlink pointing to `target` |
117
129
 
118
130
  Items at the root level (no indentation / no parent) are created directly inside the target directory.
119
131
 
132
+ ### Symlinks
133
+
134
+ Use `link-name -> target` to create a symlink. The target is passed as-is to the OS — use paths relative to the symlink's location, just as you would in a shell.
135
+
136
+ ```
137
+ my-project/
138
+ ├─ src/
139
+ │ ├─ index.ts
140
+ │ └─ utils -> ../shared/utils.ts # symlink to a sibling directory
141
+ └─ shared/
142
+ └─ utils.ts
143
+ ```
144
+
145
+ If `target` ends with `/`, the symlink is created as a directory symlink (relevant on Windows). The link name's suffix is ignored.
146
+
147
+ > [!WARNING]
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.
151
+
120
152
  ## Library API
121
153
 
122
154
  ```bash
@@ -128,36 +160,44 @@ import {
128
160
  parserFolderStructure,
129
161
  fileStructureCreator,
130
162
  resolveProjectPathToBase,
163
+ isSymlinkOutsideRoot,
131
164
  PathTraversalError,
132
- FsError,
133
165
  } from 'touch-all'
134
166
  import type { ParserResult, ParserResultLineItem } from 'touch-all'
135
167
  ```
136
168
 
137
169
  ### `parserFolderStructure(tree: string): ParserResult`
138
170
 
139
- Parses a tree string into a flat list of `type ParserResult = { path: string, isFile: boolean }` items. Pure function, no I/O.
171
+ Parses a tree string into a flat list of items. Pure function, no I/O.
172
+
173
+ Each item is one of:
174
+
175
+ ```ts
176
+ type ParserResultLineItem =
177
+ | { type: 'file'; path: string }
178
+ | { type: 'folder'; path: string }
179
+ | { type: 'symlink'; path: string; target: string }
180
+ ```
140
181
 
141
182
  ```ts
142
183
  const items = parserFolderStructure(`
143
184
  src/
144
185
  index.ts
186
+ link -> ../shared.ts
145
187
  `)
146
188
  // [
147
- // {
148
- // path: 'src',
149
- // isFile: false
150
- // }, {
151
- // path: 'src/index.ts',
152
- // isFile: true
153
- // }
189
+ // { type: 'folder', path: 'src' },
190
+ // { type: 'file', path: 'src/index.ts' },
191
+ // { type: 'symlink', path: 'src/link', target: '../shared.ts' },
154
192
  // ]
155
193
  ```
156
194
 
157
- ### `fileStructureCreator(items: ParserResult, basePath: string): Effect<void, FsError | PathTraversalError>`
195
+ ### `fileStructureCreator(items, basePath, options?): Effect<void, PathTraversalError>`
158
196
 
159
197
  Creates the parsed structure on disk under `basePath`. Returns an [Effect](https://effect.website/).
160
198
 
199
+ By default, symlinks whose targets resolve outside `basePath` are rejected with `PathTraversalError`. Pass `{ allowOutsideSymlinks: true }` to allow them.
200
+
161
201
  ```ts
162
202
  import { Effect } from 'effect'
163
203
  import { NodeContext, NodeRuntime } from '@effect/platform-node'
@@ -165,22 +205,45 @@ import { NodeContext, NodeRuntime } from '@effect/platform-node'
165
205
  const projectDirectory = '/absolute/target/path'
166
206
  const items = parserFolderStructure(tree)
167
207
 
208
+ // Default: outside-root symlinks are rejected
168
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))
169
233
  ```
170
234
 
171
235
  ### `resolveProjectPathToBase(projectPath: string, basePath: string): Effect<string, PathTraversalError>`
172
236
 
173
237
  Resolves `projectPath` relative to `basePath` and rejects any path that would escape `basePath` (path traversal protection).
174
238
 
175
- > [!CAUTION]
239
+ > [!WARNING]
176
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.
177
241
 
178
242
  ### Error types
179
243
 
180
- | Class | `_tag` | When thrown |
181
- | -------------------- | ---------------------- | --------------------------------------------------------------- |
182
- | `PathTraversalError` | `'PathTraversalError'` | Resolved path escapes `basePath`, or `basePath` is not absolute |
183
- | `FsError` | `'FsError'` | `mkdirSync` or `writeFileSync` fails |
244
+ | Class | `_tag` | When thrown |
245
+ | -------------------- | ---------------------- | -------------------------------------------------------------------------- |
246
+ | `PathTraversalError` | `'PathTraversalError'` | A file/folder path or symlink target escapes `basePath` (unless opted out) |
184
247
 
185
248
  ## License
186
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';