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