touch-all 1.1.7 → 1.2.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
@@ -1,13 +1,15 @@
1
1
  # touch-all
2
2
 
3
- CLI tool to create folder structures from markdown tree representations.
3
+ CLI tool to create folder structures from Markdown tree representations.
4
4
 
5
- Pass a tree string — drawn with box-drawing characters or plain indentation — and `touch-all` creates every directory and file on disk.
5
+ ![](.media/terminal-sceen-cast.svg)
6
+
7
+ It behaves like `mkdir -p` and `touch` combined, creating directories and files as needed. It can be used to quickly scaffold a project structure or generate placeholder files.
6
8
 
7
9
  ## Features
8
10
 
9
11
  - Accepts tree strings in **box-drawing** (`├──`, `└──`, `│`) or **indentation** (spaces) format
10
- - Trailing `/` marks a directory; no trailing `/` marks a file
12
+ - Trailing slash `/` marks a directory; no trailing `/` marks a file
11
13
  - Inline comments stripped automatically (`# ...` and `// ...`)
12
14
  - `--dry-run` parses and validates without touching the file system
13
15
  - `--verbose` prints every created path
@@ -20,10 +22,10 @@ Pass a tree string — drawn with box-drawing characters or plain indentation
20
22
  npm install -g touch-all
21
23
  ```
22
24
 
23
- Or run without installing:
25
+ or with `npx` without installing:
24
26
 
25
27
  ```bash
26
- npx touch-all "..."
28
+ npx touch-all@latest "..."
27
29
  ```
28
30
 
29
31
  ## Usage
@@ -41,37 +43,37 @@ my-project/
41
43
  "
42
44
  ```
43
45
 
44
- ### Specify target directory
46
+ ## Arguments
47
+
48
+ - `--path` , `-p` – specifies target directory. By default, the current working directory is used. Can be an absolute path or a path relative to the current working directory.
45
49
 
46
50
  ```bash
47
- touch-all "..." --path ./my-project
48
- touch-all "..." -p ./my-project
51
+ touch-all "..." --path=./my-project
52
+ touch-all "..." -p ~/Documents/my-project
49
53
  ```
50
54
 
51
- ### Dry run parse and validate, no files created
55
+ - `--dry-run` , `-n` – parses and validates the tree string without creating any files or directories. Useful for testing and debugging.
52
56
 
53
57
  ```bash
54
58
  touch-all "..." --dry-run
55
59
  touch-all "..." -n
56
60
  ```
57
61
 
58
- ### Verbose output
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`
59
63
 
60
64
  ```bash
61
65
  touch-all "..." --verbose
62
66
  touch-all "..." -v
63
67
  ```
64
68
 
65
- ### Help
66
-
67
- ```bash
68
- touch-all --help
69
- ```
69
+ - `--completions` – generates a completion script for a specific shell. Supported shells: `sh`, `bash`, `fish`, `zsh`.
70
+ - `--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
+ - `--help` , `-h` – shows the help documentation for a command.
72
+ - `--wizard` – starts wizard mode for a command, providing an interactive step-by-step interface.
73
+ - `--version` – shows the version of the application.
70
74
 
71
75
  ## Tree Format
72
76
 
73
- Both formats produce identical results.
74
-
75
77
  ### Box-drawing characters
76
78
 
77
79
  ```
@@ -100,16 +102,19 @@ my-project/
100
102
  README.md
101
103
  ```
102
104
 
105
+ Both formats produce identical results.
106
+
107
+
103
108
  ### Rules
104
109
 
105
110
  | Syntax | Meaning |
106
- | ----------------- | -------------------------------- |
111
+ |-------------------| -------------------------------- |
107
112
  | `name/` | directory |
108
113
  | `name` | file |
109
114
  | `dir/sub/` | directory at an explicit subpath |
110
115
  | `dir/sub/file.ts` | file at an explicit subpath |
111
- | `# comment` | ignored (stripped) |
112
- | `// comment` | ignored (stripped) |
116
+ | `... # comment` | ignored (stripped) |
117
+ | `... // comment` | ignored (stripped) |
113
118
 
114
119
  Items at the root level (no indentation / no parent) are created directly inside the target directory.
115
120
 
@@ -126,21 +131,28 @@ import {
126
131
  resolveProjectPathToBase,
127
132
  PathTraversalError,
128
133
  FsError,
129
- cli,
130
134
  } from 'touch-all'
131
135
  import type { ParserResult, ParserResultLineItem } from 'touch-all'
132
136
  ```
133
137
 
134
138
  ### `parserFolderStructure(tree: string): ParserResult`
135
139
 
136
- Parses a tree string into a flat list of `{ path, isFile }` items. Pure function, no I/O.
140
+ Parses a tree string into a flat list of `type ParserResult = { path: string, isFile: boolean }` items. Pure function, no I/O.
137
141
 
138
142
  ```ts
139
143
  const items = parserFolderStructure(`
140
144
  src/
141
145
  index.ts
142
146
  `)
143
- // [{ path: 'src', isFile: false }, { path: 'src/index.ts', isFile: true }]
147
+ // [
148
+ // {
149
+ // path: 'src',
150
+ // isFile: false
151
+ // }, {
152
+ // path: 'src/index.ts',
153
+ // isFile: true
154
+ // }
155
+ // ]
144
156
  ```
145
157
 
146
158
  ### `fileStructureCreator(items: ParserResult, basePath: string): Effect<void, FsError | PathTraversalError>`
@@ -151,15 +163,24 @@ Creates the parsed structure on disk under `basePath`. Returns an [Effect](https
151
163
  import { Effect } from 'effect'
152
164
  import { NodeContext, NodeRuntime } from '@effect/platform-node'
153
165
 
166
+ const projectDirectory = '/absolute/target/path'
154
167
  const items = parserFolderStructure(tree)
155
168
 
156
- fileStructureCreator(items, '/absolute/target/path').pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain)
169
+ fileStructureCreator(items, projectDirectory)
170
+ .pipe(
171
+ Effect.provide(NodeContext.layer),
172
+ NodeRuntime.runMain,
173
+ )
157
174
  ```
158
175
 
159
176
  ### `resolveProjectPathToBase(projectPath: string, basePath: string): Effect<string, PathTraversalError>`
160
177
 
161
178
  Resolves `projectPath` relative to `basePath` and rejects any path that would escape `basePath` (path traversal protection).
162
179
 
180
+ > [!CAUTION]
181
+ > `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.
182
+
183
+
163
184
  ### Error types
164
185
 
165
186
  | Class | `_tag` | When thrown |
@@ -3,4 +3,3 @@ export { resolveProjectPathToBase } from './fsNormalizator';
3
3
  export { PathTraversalError, FsError } from './_commonErrors';
4
4
  export type { ParserResult, ParserResultLineItem } from './_commonTypes';
5
5
  export { parserFolderStructure } from './parser';
6
- export { cli } from './cli';
package/dist/lib/index.js CHANGED
@@ -79,21 +79,19 @@ var parserFolderStructure = (treeString) => {
79
79
  for (const line of lines) {
80
80
  const [p01 = "", _comment01] = line.split("#");
81
81
  const [p02 = "", _comment02] = p01.split("//");
82
- const p03 = p02.replace(/[│├└─\s]/g, " ");
83
- const cleanLine = p03.trim();
82
+ const p03 = p02.replace(/[│├└─\t]/g, " ");
83
+ const cleanLine = p03.trim().replace(/^\.\//, "");
84
84
  if (!cleanLine)
85
85
  continue;
86
86
  if (cleanLine === "/")
87
87
  continue;
88
- if (cleanLine.startsWith("./"))
89
- continue;
90
88
  if (cleanLine.endsWith("../"))
91
89
  continue;
92
90
  const indent = countLeadingSpaces(p03);
93
91
  indentSize = indentSize === 0 && indent > 0 ? indent : indentSize;
94
92
  const level = indentSize === 0 ? 0 : indent / indentSize;
95
- if (previousIndentLevel > level) {
96
- pathStack.splice(level, previousIndentLevel - level);
93
+ if (previousIndentLevel >= level) {
94
+ pathStack.splice(level, pathStack.length - level);
97
95
  }
98
96
  previousIndentLevel = level;
99
97
  const isFile = !cleanLine.endsWith("/");
@@ -114,62 +112,10 @@ function countLeadingSpaces(str) {
114
112
  const match = str.match(/^[\s]*/);
115
113
  return match ? match[0].length : 0;
116
114
  }
117
- // src/cli.ts
118
- import { Args, Command, Options } from "@effect/cli";
119
- import { Console, Effect as Effect3, Logger, LogLevel, Option } from "effect";
120
- import { createInterface } from "node:readline";
121
- var treeArg = Args.text({ name: "tree" }).pipe(Args.withDescription("Multiline string representing the directory tree structure"), Args.optional);
122
- var pathOption = Options.directory("path").pipe(Options.withAlias("p"), Options.withDefault("."), Options.withDescription("Target folder path (defaults to current directory)"));
123
- var dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDefault(false), Options.withDescription("Skip the top-level directory if there is only one"));
124
- var verboseOption = Options.boolean("verbose").pipe(Options.withAlias("v"), Options.withDefault(false), Options.withDescription("Log to console extra information about creating a directory tree"));
125
- var command = Command.make("touch-all", {
126
- tree: treeArg,
127
- path: pathOption,
128
- dryRun: dryRunOption,
129
- verbose: verboseOption
130
- }).pipe(Command.withDescription("Create directory structure from a tree representation"), Command.withHandler(({ tree, path: targetPath, dryRun = false, verbose }) => {
131
- const readStdin = Effect3.tryPromise({
132
- try: () => new Promise((resolve2) => {
133
- const rl = createInterface({ input: process.stdin });
134
- const lines = [];
135
- rl.on("line", (line) => lines.push(line));
136
- rl.on("close", () => resolve2(lines.join(`
137
- `)));
138
- }),
139
- catch: (e) => new Error(`Failed to read stdin: ${String(e)}`)
140
- });
141
- const program = Effect3.gen(function* (_) {
142
- const treeString = Option.isSome(tree) ? tree.value : yield* _(readStdin);
143
- if (dryRun) {
144
- yield* _(Effect3.logInfo("Running in dry mode. No one file system node will be created."));
145
- }
146
- yield* _(Effect3.logInfo("Parsing tree structure..."));
147
- const items = parserFolderStructure(treeString);
148
- if (items.length === 0) {
149
- yield* _(Console.error("No valid items found in the tree structure"));
150
- yield* _(Console.error(items));
151
- yield* _(Console.error(treeString));
152
- return yield* _(Effect3.fail(new Error("Invalid tree structure")));
153
- }
154
- yield* _(Effect3.logInfo(`Found ${items.length} items to create`));
155
- yield* _(Effect3.logInfo(`Found:
156
- ${items.map((i) => `${i.path}
157
- `).join("")}`));
158
- if (!dryRun) {
159
- yield* _(fileStructureCreator(items, targetPath));
160
- }
161
- });
162
- return verbose ? program.pipe(Logger.withMinimumLogLevel(LogLevel.Info)) : program;
163
- }));
164
- var cli = Command.run(command, {
165
- name: "Touch All",
166
- version: "0.0.1"
167
- });
168
115
  export {
169
116
  resolveProjectPathToBase,
170
117
  parserFolderStructure,
171
118
  fileStructureCreator,
172
- cli,
173
119
  PathTraversalError,
174
120
  FsError
175
121
  };
@@ -4,9 +4,9 @@ import { Effect as Effect4, Logger as Logger2, LogLevel as LogLevel2 } from "eff
4
4
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
5
5
 
6
6
  // src/cli.ts
7
+ import { createInterface } from "node:readline";
7
8
  import { Args, Command, Options } from "@effect/cli";
8
9
  import { Console, Effect as Effect3, Logger, LogLevel, Option } from "effect";
9
- import { createInterface } from "node:readline";
10
10
 
11
11
  // src/parser.ts
12
12
  var parserFolderStructure = (treeString) => {
@@ -19,21 +19,19 @@ var parserFolderStructure = (treeString) => {
19
19
  for (const line of lines) {
20
20
  const [p01 = "", _comment01] = line.split("#");
21
21
  const [p02 = "", _comment02] = p01.split("//");
22
- const p03 = p02.replace(/[│├└─\s]/g, " ");
23
- const cleanLine = p03.trim();
22
+ const p03 = p02.replace(/[│├└─\t]/g, " ");
23
+ const cleanLine = p03.trim().replace(/^\.\//, "");
24
24
  if (!cleanLine)
25
25
  continue;
26
26
  if (cleanLine === "/")
27
27
  continue;
28
- if (cleanLine.startsWith("./"))
29
- continue;
30
28
  if (cleanLine.endsWith("../"))
31
29
  continue;
32
30
  const indent = countLeadingSpaces(p03);
33
31
  indentSize = indentSize === 0 && indent > 0 ? indent : indentSize;
34
32
  const level = indentSize === 0 ? 0 : indent / indentSize;
35
- if (previousIndentLevel > level) {
36
- pathStack.splice(level, previousIndentLevel - level);
33
+ if (previousIndentLevel >= level) {
34
+ pathStack.splice(level, pathStack.length - level);
37
35
  }
38
36
  previousIndentLevel = level;
39
37
  const isFile = !cleanLine.endsWith("/");
@@ -125,11 +123,76 @@ var fileStructureCreator = (items, basePath) => Effect2.gen(function* (_) {
125
123
  yield* _(Effect2.logInfo(`
126
124
  ✓ Structure created successfully!`));
127
125
  });
126
+ // package.json
127
+ var package_default = {
128
+ name: "touch-all",
129
+ version: "1.2.0",
130
+ description: "CLI tool to create folder structures from markdown tree representations",
131
+ keywords: [
132
+ "cli",
133
+ "folders",
134
+ "generator",
135
+ "markdown",
136
+ "structure"
137
+ ],
138
+ license: "GPL-3.0-or-later",
139
+ author: "Anton Huz <anton@ahuz.dev>",
140
+ repository: {
141
+ type: "git",
142
+ url: "https://github.com/anton-huz/touch-all"
143
+ },
144
+ bin: {
145
+ "touch-all": "dist/slim/touch-all.js"
146
+ },
147
+ files: [
148
+ "dist/slim",
149
+ "dist/lib"
150
+ ],
151
+ type: "module",
152
+ main: "dist/lib/index.js",
153
+ types: "dist/lib/index.d.ts",
154
+ exports: {
155
+ ".": {
156
+ types: "./dist/lib/index.d.ts",
157
+ import: "./dist/lib/index.js"
158
+ }
159
+ },
160
+ publishConfig: {
161
+ access: "public",
162
+ provenance: true
163
+ },
164
+ scripts: {
165
+ build: "tsup --config .config/tsup.config.ts",
166
+ "check:lint": "oxlint . -c .config/.oxlintrc.json",
167
+ "check:format": "oxfmt . -c .config/.oxfmtrc.json --check",
168
+ format: "oxfmt . -c .config/.oxfmtrc.json --write",
169
+ test: "vitest --config .config/vitest.config.ts run"
170
+ },
171
+ dependencies: {
172
+ "@effect/cli": "^0.73.2",
173
+ "@effect/platform-node": "^0.104.1",
174
+ effect: "^3.19.16"
175
+ },
176
+ devDependencies: {
177
+ "@total-typescript/tsconfig": "1.0.4",
178
+ "@types/bun": "1.3.9",
179
+ "@types/node": "25.3.2",
180
+ oxfmt: "0.28.0",
181
+ oxlint: "1.43.0",
182
+ tsup: "8.5.1",
183
+ typescript: "5.9.3",
184
+ vitest: "4.0.18"
185
+ },
186
+ engines: {
187
+ node: ">=18"
188
+ },
189
+ url: "https://github.com/anton-huz/touch-all"
190
+ };
128
191
 
129
192
  // src/cli.ts
130
193
  var treeArg = Args.text({ name: "tree" }).pipe(Args.withDescription("Multiline string representing the directory tree structure"), Args.optional);
131
194
  var pathOption = Options.directory("path").pipe(Options.withAlias("p"), Options.withDefault("."), Options.withDescription("Target folder path (defaults to current directory)"));
132
- var dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDefault(false), Options.withDescription("Skip the top-level directory if there is only one"));
195
+ 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"));
133
196
  var verboseOption = Options.boolean("verbose").pipe(Options.withAlias("v"), Options.withDefault(false), Options.withDescription("Log to console extra information about creating a directory tree"));
134
197
  var command = Command.make("touch-all", {
135
198
  tree: treeArg,
@@ -137,15 +200,20 @@ var command = Command.make("touch-all", {
137
200
  dryRun: dryRunOption,
138
201
  verbose: verboseOption
139
202
  }).pipe(Command.withDescription("Create directory structure from a tree representation"), Command.withHandler(({ tree, path: targetPath, dryRun = false, verbose }) => {
140
- const readStdin = Effect3.tryPromise({
141
- try: () => new Promise((resolve2) => {
142
- const rl = createInterface({ input: process.stdin });
143
- const lines = [];
144
- rl.on("line", (line) => lines.push(line));
145
- rl.on("close", () => resolve2(lines.join(`
203
+ const readStdin = Effect3.gen(function* (_) {
204
+ if (process.stdin.isTTY) {
205
+ yield* _(Console.log("Paste your tree structure and press Ctrl+D when done:"));
206
+ }
207
+ return yield* _(Effect3.tryPromise({
208
+ try: () => new Promise((resolve2) => {
209
+ const rl = createInterface({ input: process.stdin });
210
+ const lines = [];
211
+ rl.on("line", (line) => lines.push(line));
212
+ rl.on("close", () => resolve2(lines.join(`
146
213
  `)));
147
- }),
148
- catch: (e) => new Error(`Failed to read stdin: ${String(e)}`)
214
+ }),
215
+ catch: (e) => new Error(`Failed to read stdin: ${String(e)}`)
216
+ }));
149
217
  });
150
218
  const program = Effect3.gen(function* (_) {
151
219
  const treeString = Option.isSome(tree) ? tree.value : yield* _(readStdin);
@@ -156,8 +224,6 @@ var command = Command.make("touch-all", {
156
224
  const items = parserFolderStructure(treeString);
157
225
  if (items.length === 0) {
158
226
  yield* _(Console.error("No valid items found in the tree structure"));
159
- yield* _(Console.error(items));
160
- yield* _(Console.error(treeString));
161
227
  return yield* _(Effect3.fail(new Error("Invalid tree structure")));
162
228
  }
163
229
  yield* _(Effect3.logInfo(`Found ${items.length} items to create`));
@@ -171,8 +237,8 @@ ${items.map((i) => `${i.path}
171
237
  return verbose ? program.pipe(Logger.withMinimumLogLevel(LogLevel.Info)) : program;
172
238
  }));
173
239
  var cli = Command.run(command, {
174
- name: "Touch All",
175
- version: "0.0.1"
240
+ name: package_default.name,
241
+ version: package_default.version
176
242
  });
177
243
 
178
244
  // src/touch-all.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "touch-all",
3
- "version": "1.1.7",
3
+ "version": "1.2.0",
4
4
  "description": "CLI tool to create folder structures from markdown tree representations",
5
5
  "keywords": [
6
6
  "cli",
@@ -11,11 +11,15 @@
11
11
  ],
12
12
  "license": "GPL-3.0-or-later",
13
13
  "author": "Anton Huz <anton@ahuz.dev>",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/anton-huz/touch-all"
17
+ },
14
18
  "bin": {
15
19
  "touch-all": "dist/slim/touch-all.js"
16
20
  },
17
21
  "files": [
18
- "dist/bundled",
22
+ "dist/slim",
19
23
  "dist/lib"
20
24
  ],
21
25
  "type": "module",
@@ -28,7 +32,8 @@
28
32
  }
29
33
  },
30
34
  "publishConfig": {
31
- "access": "public"
35
+ "access": "public",
36
+ "provenance": true
32
37
  },
33
38
  "scripts": {
34
39
  "build": "tsup --config .config/tsup.config.ts",