touch-all 0.0.1

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.
@@ -0,0 +1,45 @@
1
+ import { Effect } from 'effect';
2
+ import * as _effect_cli_CliApp from '@effect/cli/CliApp';
3
+ import * as _effect_cli_ValidationError from '@effect/cli/ValidationError';
4
+
5
+ interface ParserResultLineItem {
6
+ path: string;
7
+ isFile: boolean;
8
+ }
9
+ type ParserResult = Array<ParserResultLineItem>;
10
+
11
+ /**
12
+ * Error type for path traversal detection
13
+ */
14
+ declare class PathTraversalError {
15
+ readonly path: string;
16
+ readonly _tag = "PathTraversalError";
17
+ constructor(path: string);
18
+ toString(): string;
19
+ }
20
+ /**
21
+ * Error type for file system operation failures
22
+ */
23
+ declare class FsError {
24
+ readonly message: string;
25
+ readonly _tag = "FsError";
26
+ constructor(message: string);
27
+ toString(): string;
28
+ }
29
+
30
+ declare const fileStructureCreator: (items: ParserResult, basePath: string) => Effect.Effect<void, FsError | PathTraversalError>;
31
+
32
+ /**
33
+ * Safely normalize a user-supplied path against a base directory.
34
+ * Fails with PathTraversalError if the resolved path escapes the base.
35
+ *
36
+ * @param projectPath string - Path relative to project.
37
+ * @param basePath string - Must be absolute.
38
+ */
39
+ declare const resolveProjectPathToBase: (projectPath: string, basePath: string) => Effect.Effect<string, PathTraversalError>;
40
+
41
+ declare const parserFolderStructure: (treeString: string) => ParserResult;
42
+
43
+ declare const cli: (args: ReadonlyArray<string>) => Effect.Effect<void, PathTraversalError | FsError | Error | _effect_cli_ValidationError.ValidationError, _effect_cli_CliApp.CliApp.Environment>;
44
+
45
+ export { FsError, type ParserResult, type ParserResultLineItem, PathTraversalError, cli, fileStructureCreator, parserFolderStructure, resolveProjectPathToBase };
@@ -0,0 +1,179 @@
1
+ // src/fsGenerator.ts
2
+ import { Effect as Effect2 } from "effect";
3
+ import path from "path";
4
+ import fs from "fs";
5
+
6
+ // src/fsNormalizator.ts
7
+ import { resolve, relative, sep } from "path";
8
+ import { Effect } from "effect";
9
+
10
+ // src/_commonErrors.ts
11
+ var PathTraversalError = class {
12
+ constructor(path2) {
13
+ this.path = path2;
14
+ }
15
+ _tag = "PathTraversalError";
16
+ toString() {
17
+ return `${this._tag}: The path cannot be used ${this.path}`;
18
+ }
19
+ };
20
+ var FsError = class {
21
+ constructor(message) {
22
+ this.message = message;
23
+ }
24
+ _tag = "FsError";
25
+ toString() {
26
+ return `${this._tag}: ${this.message}`;
27
+ }
28
+ };
29
+
30
+ // src/fsNormalizator.ts
31
+ var resolveProjectPathToBase = (projectPath, basePath) => {
32
+ const filePath = projectPath.startsWith(sep) ? `.${projectPath}` : projectPath;
33
+ const absolute = resolve(basePath, filePath);
34
+ const rel = relative(basePath, absolute);
35
+ if (rel.startsWith("..")) {
36
+ return Effect.fail(new PathTraversalError(["project:", projectPath, "base:", basePath].join(" ")));
37
+ }
38
+ return Effect.succeed(absolute);
39
+ };
40
+
41
+ // src/fsGenerator.ts
42
+ var fileStructureCreator = (items, basePath) => Effect2.gen(function* (_) {
43
+ yield* _(Effect2.logInfo(`Creating structure in: ${basePath}`));
44
+ for (const item of items) {
45
+ const fullPath = yield* _(resolveProjectPathToBase(item.path, basePath));
46
+ if (item.isFile) {
47
+ const dir = path.dirname(fullPath);
48
+ yield* _(
49
+ Effect2.try({
50
+ try: () => fs.mkdirSync(dir, { recursive: true }),
51
+ catch: (error) => new FsError(`Failed to create directory ${dir}: ${String(error)}`)
52
+ })
53
+ );
54
+ yield* _(
55
+ Effect2.try({
56
+ try: () => fs.writeFileSync(fullPath, ""),
57
+ catch: (error) => new FsError(`Failed to create file ${fullPath}: ${String(error)}`)
58
+ })
59
+ );
60
+ yield* _(Effect2.logInfo(` Created file: ${item.path}`));
61
+ } else {
62
+ yield* _(
63
+ Effect2.try({
64
+ try: () => fs.mkdirSync(fullPath, { recursive: true }),
65
+ catch: (error) => new FsError(`Failed to create directory ${fullPath}: ${String(error)}`)
66
+ })
67
+ );
68
+ yield* _(Effect2.logInfo(` Created directory: ${item.path}`));
69
+ }
70
+ }
71
+ yield* _(Effect2.logInfo("\n\u2713 Structure created successfully!"));
72
+ });
73
+
74
+ // src/parser.ts
75
+ var parserFolderStructure = (treeString) => {
76
+ const lines = treeString.split("\n");
77
+ const result = [];
78
+ const pathStack = [];
79
+ let indentSize = 0;
80
+ let previousIndentLevel = 0;
81
+ for (const line of lines) {
82
+ const [p01 = "", _comment01] = line.split("#");
83
+ const [p02 = "", _comment02] = p01.split("//");
84
+ const p03 = p02.replace(/[│├└─\s]/g, " ");
85
+ const cleanLine = p03.trim();
86
+ if (!cleanLine) continue;
87
+ if (cleanLine === "/") continue;
88
+ if (cleanLine.startsWith("./")) continue;
89
+ if (cleanLine.endsWith("../")) continue;
90
+ const indent = countLeadingSpaces(p03);
91
+ indentSize = indentSize === 0 && indent > 0 ? indent : indentSize;
92
+ const level = indentSize === 0 ? 0 : indent / indentSize;
93
+ if (previousIndentLevel > level) {
94
+ pathStack.splice(level, previousIndentLevel - level);
95
+ }
96
+ previousIndentLevel = level;
97
+ const isFile = !cleanLine.endsWith("/");
98
+ const name = cleanLine.split("/").filter(Boolean);
99
+ pathStack.push(name);
100
+ const path2 = pathStack.flat().join("/");
101
+ result.push({
102
+ path: path2,
103
+ isFile
104
+ });
105
+ if (isFile) {
106
+ pathStack.pop();
107
+ }
108
+ }
109
+ return result;
110
+ };
111
+ function countLeadingSpaces(str) {
112
+ const match = str.match(/^[\s]*/);
113
+ return match ? match[0].length : 0;
114
+ }
115
+
116
+ // src/cli.ts
117
+ import { Args, Command, Options } from "@effect/cli";
118
+ import { Console, Effect as Effect3, Logger, LogLevel } from "effect";
119
+ var treeArg = Args.text({ name: "tree" }).pipe(
120
+ Args.withDescription("Multiline string representing the directory tree structure")
121
+ );
122
+ var pathOption = Options.directory("path").pipe(
123
+ Options.withAlias("p"),
124
+ Options.withDefault("."),
125
+ Options.withDescription("Target folder path (defaults to current directory)")
126
+ );
127
+ var dryRunOption = Options.boolean("dry-run").pipe(
128
+ Options.withAlias("n"),
129
+ Options.withDefault(false),
130
+ Options.withDescription("Skip the top-level directory if there is only one")
131
+ );
132
+ var verboseOption = Options.boolean("verbose").pipe(
133
+ Options.withAlias("v"),
134
+ Options.withDefault(false),
135
+ Options.withDescription("Log to console extra information about creating a directory tree")
136
+ );
137
+ var command = Command.make("touch-all", {
138
+ tree: treeArg,
139
+ path: pathOption,
140
+ dryRun: dryRunOption,
141
+ verbose: verboseOption
142
+ }).pipe(
143
+ Command.withDescription("Create directory structure from a tree representation"),
144
+ Command.withHandler(({ tree, path: targetPath, dryRun = false, verbose }) => {
145
+ const program = Effect3.gen(function* (_) {
146
+ if (dryRun) {
147
+ yield* _(Effect3.logInfo("Running in dry mode. No one file system node will be created."));
148
+ }
149
+ yield* _(Effect3.logInfo("Parsing tree structure..."));
150
+ const items = parserFolderStructure(tree);
151
+ if (items.length === 0) {
152
+ yield* _(Console.error("No valid items found in the tree structure"));
153
+ yield* _(Console.error(items));
154
+ yield* _(Console.error(tree));
155
+ return yield* _(Effect3.fail(new Error("Invalid tree structure")));
156
+ }
157
+ yield* _(Effect3.logInfo(`Found ${items.length} items to create`));
158
+ yield* _(Effect3.logInfo(`Found:
159
+ ${items.map((i) => `${i.path}
160
+ `).join("")}`));
161
+ if (!dryRun) {
162
+ yield* _(fileStructureCreator(items, targetPath));
163
+ }
164
+ });
165
+ return verbose ? program.pipe(Logger.withMinimumLogLevel(LogLevel.Info)) : program;
166
+ })
167
+ );
168
+ var cli = Command.run(command, {
169
+ name: "Touch All",
170
+ version: "0.0.1"
171
+ });
172
+ export {
173
+ FsError,
174
+ PathTraversalError,
175
+ cli,
176
+ fileStructureCreator,
177
+ parserFolderStructure,
178
+ resolveProjectPathToBase
179
+ };
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/touch-all.ts
4
+ import { Effect as Effect4, Logger as Logger2, LogLevel as LogLevel2 } from "effect";
5
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
6
+
7
+ // src/cli.ts
8
+ import { Args, Command, Options } from "@effect/cli";
9
+ import { Console, Effect as Effect3, Logger, LogLevel } from "effect";
10
+
11
+ // src/parser.ts
12
+ var parserFolderStructure = (treeString) => {
13
+ const lines = treeString.split("\n");
14
+ const result = [];
15
+ const pathStack = [];
16
+ let indentSize = 0;
17
+ let previousIndentLevel = 0;
18
+ for (const line of lines) {
19
+ const [p01 = "", _comment01] = line.split("#");
20
+ const [p02 = "", _comment02] = p01.split("//");
21
+ const p03 = p02.replace(/[│├└─\s]/g, " ");
22
+ const cleanLine = p03.trim();
23
+ if (!cleanLine) continue;
24
+ if (cleanLine === "/") continue;
25
+ if (cleanLine.startsWith("./")) continue;
26
+ if (cleanLine.endsWith("../")) continue;
27
+ const indent = countLeadingSpaces(p03);
28
+ indentSize = indentSize === 0 && indent > 0 ? indent : indentSize;
29
+ const level = indentSize === 0 ? 0 : indent / indentSize;
30
+ if (previousIndentLevel > level) {
31
+ pathStack.splice(level, previousIndentLevel - level);
32
+ }
33
+ previousIndentLevel = level;
34
+ const isFile = !cleanLine.endsWith("/");
35
+ const name = cleanLine.split("/").filter(Boolean);
36
+ pathStack.push(name);
37
+ const path2 = pathStack.flat().join("/");
38
+ result.push({
39
+ path: path2,
40
+ isFile
41
+ });
42
+ if (isFile) {
43
+ pathStack.pop();
44
+ }
45
+ }
46
+ return result;
47
+ };
48
+ function countLeadingSpaces(str) {
49
+ const match = str.match(/^[\s]*/);
50
+ return match ? match[0].length : 0;
51
+ }
52
+
53
+ // src/fsGenerator.ts
54
+ import { Effect as Effect2 } from "effect";
55
+ import path from "path";
56
+ import fs from "fs";
57
+
58
+ // src/fsNormalizator.ts
59
+ import { resolve, relative, sep } from "path";
60
+ import { Effect } from "effect";
61
+
62
+ // src/_commonErrors.ts
63
+ var PathTraversalError = class {
64
+ constructor(path2) {
65
+ this.path = path2;
66
+ }
67
+ _tag = "PathTraversalError";
68
+ toString() {
69
+ return `${this._tag}: The path cannot be used ${this.path}`;
70
+ }
71
+ };
72
+ var FsError = class {
73
+ constructor(message) {
74
+ this.message = message;
75
+ }
76
+ _tag = "FsError";
77
+ toString() {
78
+ return `${this._tag}: ${this.message}`;
79
+ }
80
+ };
81
+
82
+ // src/fsNormalizator.ts
83
+ var resolveProjectPathToBase = (projectPath, basePath) => {
84
+ const filePath = projectPath.startsWith(sep) ? `.${projectPath}` : projectPath;
85
+ const absolute = resolve(basePath, filePath);
86
+ const rel = relative(basePath, absolute);
87
+ if (rel.startsWith("..")) {
88
+ return Effect.fail(new PathTraversalError(["project:", projectPath, "base:", basePath].join(" ")));
89
+ }
90
+ return Effect.succeed(absolute);
91
+ };
92
+
93
+ // src/fsGenerator.ts
94
+ var fileStructureCreator = (items, basePath) => Effect2.gen(function* (_) {
95
+ yield* _(Effect2.logInfo(`Creating structure in: ${basePath}`));
96
+ for (const item of items) {
97
+ const fullPath = yield* _(resolveProjectPathToBase(item.path, basePath));
98
+ if (item.isFile) {
99
+ const dir = path.dirname(fullPath);
100
+ yield* _(
101
+ Effect2.try({
102
+ try: () => fs.mkdirSync(dir, { recursive: true }),
103
+ catch: (error) => new FsError(`Failed to create directory ${dir}: ${String(error)}`)
104
+ })
105
+ );
106
+ yield* _(
107
+ Effect2.try({
108
+ try: () => fs.writeFileSync(fullPath, ""),
109
+ catch: (error) => new FsError(`Failed to create file ${fullPath}: ${String(error)}`)
110
+ })
111
+ );
112
+ yield* _(Effect2.logInfo(` Created file: ${item.path}`));
113
+ } else {
114
+ yield* _(
115
+ Effect2.try({
116
+ try: () => fs.mkdirSync(fullPath, { recursive: true }),
117
+ catch: (error) => new FsError(`Failed to create directory ${fullPath}: ${String(error)}`)
118
+ })
119
+ );
120
+ yield* _(Effect2.logInfo(` Created directory: ${item.path}`));
121
+ }
122
+ }
123
+ yield* _(Effect2.logInfo("\n\u2713 Structure created successfully!"));
124
+ });
125
+
126
+ // src/cli.ts
127
+ var treeArg = Args.text({ name: "tree" }).pipe(
128
+ Args.withDescription("Multiline string representing the directory tree structure")
129
+ );
130
+ var pathOption = Options.directory("path").pipe(
131
+ Options.withAlias("p"),
132
+ Options.withDefault("."),
133
+ Options.withDescription("Target folder path (defaults to current directory)")
134
+ );
135
+ var dryRunOption = Options.boolean("dry-run").pipe(
136
+ Options.withAlias("n"),
137
+ Options.withDefault(false),
138
+ Options.withDescription("Skip the top-level directory if there is only one")
139
+ );
140
+ var verboseOption = Options.boolean("verbose").pipe(
141
+ Options.withAlias("v"),
142
+ Options.withDefault(false),
143
+ Options.withDescription("Log to console extra information about creating a directory tree")
144
+ );
145
+ var command = Command.make("touch-all", {
146
+ tree: treeArg,
147
+ path: pathOption,
148
+ dryRun: dryRunOption,
149
+ verbose: verboseOption
150
+ }).pipe(
151
+ Command.withDescription("Create directory structure from a tree representation"),
152
+ Command.withHandler(({ tree, path: targetPath, dryRun = false, verbose }) => {
153
+ const program = Effect3.gen(function* (_) {
154
+ if (dryRun) {
155
+ yield* _(Effect3.logInfo("Running in dry mode. No one file system node will be created."));
156
+ }
157
+ yield* _(Effect3.logInfo("Parsing tree structure..."));
158
+ const items = parserFolderStructure(tree);
159
+ if (items.length === 0) {
160
+ yield* _(Console.error("No valid items found in the tree structure"));
161
+ yield* _(Console.error(items));
162
+ yield* _(Console.error(tree));
163
+ return yield* _(Effect3.fail(new Error("Invalid tree structure")));
164
+ }
165
+ yield* _(Effect3.logInfo(`Found ${items.length} items to create`));
166
+ yield* _(Effect3.logInfo(`Found:
167
+ ${items.map((i) => `${i.path}
168
+ `).join("")}`));
169
+ if (!dryRun) {
170
+ yield* _(fileStructureCreator(items, targetPath));
171
+ }
172
+ });
173
+ return verbose ? program.pipe(Logger.withMinimumLogLevel(LogLevel.Info)) : program;
174
+ })
175
+ );
176
+ var cli = Command.run(command, {
177
+ name: "Touch All",
178
+ version: "0.0.1"
179
+ });
180
+
181
+ // src/touch-all.ts
182
+ Effect4.suspend(() => cli(process.argv)).pipe(
183
+ Logger2.withMinimumLogLevel(LogLevel2.Warning),
184
+ Effect4.provide(NodeContext.layer),
185
+ NodeRuntime.runMain
186
+ );
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "touch-all",
3
+ "version": "0.0.1",
4
+ "description": "CLI tool to create folder structures from markdown tree representations",
5
+ "keywords": [
6
+ "cli",
7
+ "folders",
8
+ "generator",
9
+ "markdown",
10
+ "structure"
11
+ ],
12
+ "license": "GPL-3.0-or-later",
13
+ "author": "Anton Huz <anton@ahuz.dev>",
14
+ "bin": {
15
+ "touch-all": "./dist/slim/touch-all.js"
16
+ },
17
+ "files": [
18
+ "dist/bundled",
19
+ "dist/lib"
20
+ ],
21
+ "type": "module",
22
+ "main": "dist/lib/index.js",
23
+ "types": "dist/lib/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/lib/index.d.ts",
27
+ "import": "./dist/lib/index.js"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "tsup --config .config/tsup.config.ts",
32
+ "check:lint": "oxlint . -c .config/.oxlintrc.json",
33
+ "check:format": "oxfmt . -c .config/.oxfmtrc.json --check",
34
+ "format": "oxfmt . -c .config/.oxfmtrc.json --write",
35
+ "test": "vitest --config .config/vitest.config.ts run"
36
+ },
37
+ "dependencies": {
38
+ "@effect/cli": "^0.73.2",
39
+ "@effect/platform-node": "^0.104.1",
40
+ "effect": "^3.19.16"
41
+ },
42
+ "devDependencies": {
43
+ "@total-typescript/tsconfig": "1.0.4",
44
+ "@types/node": "22.10.2",
45
+ "oxfmt": "0.28.0",
46
+ "oxlint": "1.43.0",
47
+ "tsup": "8.5.1",
48
+ "typescript": "5.9.3",
49
+ "vitest": "4.0.18"
50
+ },
51
+ "engines": {
52
+ "node": ">=18"
53
+ }
54
+ }