tshex-cli 1.0.22 → 1.0.24

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/build/main.js CHANGED
@@ -2,18 +2,20 @@
2
2
  import { program } from 'commander';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
+ import readline from 'node:readline/promises';
6
+ import { stdin as input, stdout as output } from 'node:process';
5
7
  function readPackageJson() {
6
8
  const filePath = path.join(import.meta.dirname, '..', 'package.json');
7
9
  const fileContents = fs.readFileSync(filePath, 'utf-8');
8
10
  return JSON.parse(fileContents);
9
11
  }
10
- function executeCreateLib(templatesDir, libraryDir) {
12
+ function executeCreateProject(templatesDir, projectDir) {
11
13
  try {
12
- fs.cpSync(path.join(templatesDir, 'lib'), libraryDir, {
14
+ fs.cpSync(path.join(templatesDir, 'lib'), projectDir, {
13
15
  recursive: true,
14
16
  filter: (src) => src.endsWith('.gitkeep') === false
15
17
  });
16
- console.log('Library created successfully');
18
+ console.log('Project created successfully');
17
19
  }
18
20
  catch (err) {
19
21
  console.error(err);
@@ -43,7 +45,53 @@ function executeCreateReactContext(templatesDir, contextDir) {
43
45
  console.error(err);
44
46
  }
45
47
  }
46
- function main(program) {
48
+ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir) {
49
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
50
+ if (fs.existsSync(destinationDir) === false) {
51
+ fs.mkdirSync(destinationDir, { recursive: true });
52
+ }
53
+ for (const entry of entries) {
54
+ const sourcePath = path.join(sourceDir, entry.name);
55
+ const destinationPath = path.join(destinationDir, entry.name);
56
+ if (entry.isDirectory()) {
57
+ if (entry.name === 'shared') {
58
+ continue;
59
+ }
60
+ if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
61
+ continue;
62
+ }
63
+ if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
64
+ continue;
65
+ }
66
+ executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir);
67
+ continue;
68
+ }
69
+ if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
70
+ fs.writeFileSync(destinationPath, fileContents);
71
+ }
72
+ }
73
+ }
74
+ async function ensureTestsDirectory(testsRootDir) {
75
+ if (fs.existsSync(testsRootDir)) {
76
+ if (fs.statSync(testsRootDir).isDirectory() === false) {
77
+ throw new Error(`Tests path exists but is not a directory: ${testsRootDir}`);
78
+ }
79
+ return;
80
+ }
81
+ const rl = readline.createInterface({ input, output });
82
+ try {
83
+ const answer = await rl.question(`Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `);
84
+ if (answer.trim().toLowerCase() !== 'y') {
85
+ return false;
86
+ }
87
+ }
88
+ finally {
89
+ rl.close();
90
+ }
91
+ fs.mkdirSync(testsRootDir, { recursive: true });
92
+ return true;
93
+ }
94
+ async function main(program) {
47
95
  const templatesDir = path.join(import.meta.dirname, '..', 'templates');
48
96
  const options = program.opts();
49
97
  let targetDir = path.resolve(options.dir ?? process.cwd());
@@ -53,15 +101,15 @@ function main(program) {
53
101
  if (fs.existsSync(targetDir) === false) {
54
102
  fs.mkdirSync(targetDir, { recursive: true });
55
103
  }
56
- if (options.lib !== undefined) {
57
- targetDir = path.join(targetDir, options.lib);
58
- executeCreateLib(templatesDir, targetDir);
104
+ if (options.project !== undefined) {
105
+ targetDir = path.join(targetDir, options.project);
106
+ executeCreateProject(templatesDir, targetDir);
59
107
  }
60
- if (options.react === true && options.ctx === undefined) {
61
- program.error('Option --react requires --ctx <name>');
108
+ if (options.react === true && options.context === undefined) {
109
+ program.error('Option --react requires --context <name>');
62
110
  }
63
- if (options.ctx !== undefined) {
64
- targetDir = path.join(targetDir, options.ctx);
111
+ if (options.context !== undefined) {
112
+ targetDir = path.join(targetDir, options.context);
65
113
  if (options.react === true) {
66
114
  executeCreateReactContext(templatesDir, targetDir);
67
115
  }
@@ -69,14 +117,43 @@ function main(program) {
69
117
  executeCreateContext(templatesDir, targetDir);
70
118
  }
71
119
  }
120
+ if (options.tests !== undefined) {
121
+ const sourceDir = path.resolve(options.tests);
122
+ const testsTemplateFile = path.join(templatesDir, 'tests', 'content.ts');
123
+ if (fs.existsSync(sourceDir) === false) {
124
+ program.error(`Tests source directory does not exist: ${sourceDir}`);
125
+ }
126
+ if (fs.statSync(sourceDir).isDirectory() === false) {
127
+ program.error(`Tests source path is not a directory: ${sourceDir}`);
128
+ }
129
+ if (fs.existsSync(testsTemplateFile) === false) {
130
+ program.error(`Tests template file does not exist: ${testsTemplateFile}`);
131
+ }
132
+ const testsTemplateContents = fs.readFileSync(testsTemplateFile, 'utf-8');
133
+ const testsRootDir = path.join(targetDir, 'tests');
134
+ const testsDirectoryCreated = await ensureTestsDirectory(testsRootDir);
135
+ if (testsDirectoryCreated === false) {
136
+ program.error('Tests directory creation cancelled');
137
+ }
138
+ const testsDir = path.join(testsRootDir, path.basename(sourceDir));
139
+ if (testsDir === sourceDir) {
140
+ program.error('Tests destination directory cannot be the same as the source directory');
141
+ }
142
+ executeCreateTests(sourceDir, testsDir, testsTemplateContents, testsRootDir);
143
+ console.log('Tests structure created successfully');
144
+ }
72
145
  }
73
146
  const packageJson = readPackageJson();
74
147
  program
75
148
  .name('tshex')
76
149
  .version(packageJson.version)
77
- .option('--lib <name>', "creates a new library with it's shared directory")
78
- .option('--ctx <name>', 'creates a new context')
79
- .option('-R, --react', 'creates a React context with --ctx')
150
+ .option('-P, --project <name>', "creates a new project with it's shared directory")
151
+ .option('-C, --context <name>', 'creates a new context')
152
+ .option('-R, --react', 'creates a React context with --context')
153
+ .option('-T, --tests <path>', 'creates a .ts tests structure from an existing directory')
80
154
  .option('--dir <path>', 'sets the directory to create the new item')
81
155
  .parse(process.argv);
82
- main(program);
156
+ void main(program).catch((err) => {
157
+ const message = err instanceof Error ? err.message : String(err);
158
+ program.error(message);
159
+ });
@@ -1,12 +1,17 @@
1
1
  ### Context Ports
2
2
 
3
3
  Context ports define the communication available at a context boundary.
4
- They describe what data enters the context, what data leaves it, and which
5
- operation another system can invoke.
4
+ They describe which concrete capability a context exposes, what data enters
5
+ that capability, and what data it returns.
6
6
 
7
- Ports are used to keep communication contracts explicit. An adapter imports a
8
- port, implements that contract, and translates the external interaction into an
9
- application process.
7
+ In practice a port is usually not just an abstraction or a contract. It is a
8
+ specific boundary element of the system with identity: a command handler, a
9
+ query entry point, an event consumer, a published endpoint, or another concrete
10
+ interaction mechanism that exists because the running system exposes it.
11
+
12
+ Types and interfaces still matter, but they are secondary. Their role is to
13
+ make the port explicit. The main concern is the port as a real executable
14
+ surface that another actor can call or observe.
10
15
 
11
16
  > **Hint**
12
17
  > The generated `example-ports.ts` file is only a placeholder. Replace it when
@@ -22,12 +27,12 @@ export function example(): void {
22
27
  }
23
28
  ```
24
29
 
25
- This placeholder does not define a real contract yet. Its purpose is to mark
26
- the context root as the place where boundary-facing types and interfaces live.
30
+ This placeholder does not define a real port yet. Its purpose is to mark the
31
+ context root as the place where boundary-facing capabilities are declared.
27
32
 
28
- #### First Contract
33
+ #### First Port
29
34
 
30
- In the following example we replace the placeholder with a minimal contract for
35
+ In the following example we replace the placeholder with a concrete port for
31
36
  creating a user.
32
37
 
33
38
  ```ts title="users/example-ports.ts"
@@ -53,16 +58,17 @@ export interface CreateUserPort {
53
58
  ```
54
59
 
55
60
  `CreateUserRequest` defines the incoming payload. `CreateUserResponse` defines
56
- the outgoing payload. `CreateUserPort` exposes the operation available at the
57
- boundary.
61
+ the outgoing payload. `CreateUserPort` names the concrete capability exposed at
62
+ the boundary: creating a user.
58
63
 
59
- This contract says nothing about HTTP, queues, or databases. It only declares
60
- the shape of the communication.
64
+ This definition does not commit to HTTP, queues, or databases. It focuses on
65
+ the interaction the context makes available. The transport can vary, but the
66
+ port remains the same identifiable boundary capability.
61
67
 
62
68
  #### Adapter Implementation
63
69
 
64
- Now that the port exists, an adapter can implement it and delegate the work to
65
- an application service.
70
+ Now that the port exists, the system can materialize it through an adapter and
71
+ delegate the work to an application service.
66
72
 
67
73
  ```ts title="users/adapters/create-user.ts"
68
74
  import type {
@@ -96,22 +102,27 @@ export class CreateUserAdapter implements CreateUserPort {
96
102
  }
97
103
  ```
98
104
 
99
- The adapter implements `CreateUserPort`, so it must provide `create()`. Inside
100
- that method it translates the root-level request into the input expected by the
101
- application service.
105
+ The adapter implements `CreateUserPort`, so it materializes the boundary
106
+ capability and provides `create()`. Inside that method it translates the
107
+ root-level request into the input expected by the application service.
102
108
 
103
109
  This is the normal flow of the generated structure:
104
110
 
105
- ```text
106
- external system -> adapter -> application -> domain
111
+ ```mermaid
112
+ flowchart LR
113
+ external["External system"] --> adapter[Adapter]
114
+ adapter --> port[Port]
115
+ port --> application[Application]
116
+ application --> domain[Domain]
107
117
  ```
108
118
 
109
- The port belongs to the boundary. The adapter materializes the boundary. The
110
- application process executes the use case.
119
+ The port belongs to the boundary because it is part of what the context really
120
+ exposes. The adapter is one implementation path for that port. The application
121
+ process executes the use case and uses domain capabilities.
111
122
 
112
123
  #### Multiple Port Files
113
124
 
114
- As the context grows, you can keep several contracts at the context root.
125
+ As the context grows, you can keep several ports at the context root.
115
126
 
116
127
  ```ts title="users/list-users.ts"
117
128
  export type ListUsersRequest = {
@@ -131,35 +142,38 @@ export interface ListUsersPort {
131
142
  }
132
143
  ```
133
144
 
134
- An adapter can then import the contract from the file that owns it.
145
+ An adapter can then import the port definition from the file that owns it.
135
146
 
136
- This arrangement is useful when one context exposes several independent forms
137
- of communication. A single file works well for a small context. Separate files
138
- become easier to maintain when responsibilities start to diverge.
147
+ This arrangement is useful when one context exposes several independent
148
+ capabilities. A single file works well for a small context. Separate files
149
+ become easier to maintain when each port has its own identity and
150
+ responsibility.
139
151
 
140
152
  > **Warning**
141
- > A port should define communication, not domain behavior. Avoid moving entity
142
- > rules, repository logic, or infrastructure details into the port file.
153
+ > A port should define an exposed boundary capability, not domain internals.
154
+ > Avoid moving entity rules, repository logic, or infrastructure details into
155
+ > the port file.
143
156
 
144
157
  #### Example Layout
145
158
 
146
159
  The following structure keeps ports at the root while the implementation lives
147
160
  in the generated folders.
148
161
 
149
- ```text
150
- users/
151
- ├── example-ports.ts
152
- ├── list-users.ts
153
- ├── adapters/
154
- ├── application/
155
- └── domain/
162
+ ```mermaid
163
+ flowchart TD
164
+ users["users/"] --> examplePorts["example-ports.ts"]
165
+ users --> listUsers["list-users.ts"]
166
+ users --> adapters["adapters/"]
167
+ users --> application["application/"]
168
+ users --> domain["domain/"]
156
169
  ```
157
170
 
158
171
  This layout keeps the context boundary visible from the top level. It also
159
- reduces coupling between adapters because they all import the same contracts.
172
+ reduces coupling between adapters because they all import the same port
173
+ definitions for the capabilities the context exposes.
160
174
 
161
175
  #### Next Step
162
176
 
163
- After defining a port, implement the corresponding adapter and connect it to an
164
- application service. The surrounding structure is described in
165
- `library-structure.md`.
177
+ After defining a port, implement the corresponding executable path and connect
178
+ it to an application service. The surrounding structure is described in
179
+ `library-structure.md`.
@@ -13,11 +13,12 @@ language, rules, and operations.
13
13
 
14
14
  The root contains the entry points of the generated library.
15
15
 
16
- ```text
17
- index.d.ts
18
- main.ts
19
- shared/
20
- users/
16
+ ```mermaid
17
+ flowchart TD
18
+ root["Library root"] --> index["index.d.ts"]
19
+ root --> main["main.ts"]
20
+ root --> shared["shared/"]
21
+ root --> users["users/"]
21
22
  ```
22
23
 
23
24
  `index.d.ts` defines root-level types. `main.ts` starts as a placeholder for
@@ -29,10 +30,10 @@ or more context directories.
29
30
  The `shared` directory contains concepts that can be reused by multiple
30
31
  contexts.
31
32
 
32
- ```text
33
- shared/
34
- ├── application/
35
- └── domain/
33
+ ```mermaid
34
+ flowchart TD
35
+ shared["shared/"] --> application["application/"]
36
+ shared --> domain["domain/"]
36
37
  ```
37
38
 
38
39
  `shared/domain` contains modeling foundations such as value objects, entities,
@@ -47,11 +48,12 @@ Until then, keep it close to the context that owns the rule.
47
48
  A context groups the vocabulary, rules, and operations of one application
48
49
  capability.
49
50
 
50
- ```text
51
- users/
52
- billing/
53
- inventory/
54
- sales/
51
+ ```mermaid
52
+ flowchart TD
53
+ contexts["Contexts"] --> users["users/"]
54
+ contexts --> billing["billing/"]
55
+ contexts --> inventory["inventory/"]
56
+ contexts --> sales["sales/"]
55
57
  ```
56
58
 
57
59
  Each context can evolve independently while still reusing the abstractions from
@@ -62,12 +64,12 @@ the system.
62
64
 
63
65
  Every generated context starts with the same internal structure.
64
66
 
65
- ```text
66
- users/
67
- ├── example-ports.ts
68
- ├── adapters/
69
- ├── application/
70
- └── domain/
67
+ ```mermaid
68
+ flowchart TD
69
+ users["users/"] --> ports["example-ports.ts"]
70
+ users --> adapters["adapters/"]
71
+ users --> application["application/"]
72
+ users --> domain["domain/"]
71
73
  ```
72
74
 
73
75
  `example-ports.ts` is the root communication surface of the context.
@@ -123,14 +125,20 @@ split ports across several files. The detailed guidance for that layout lives in
123
125
 
124
126
  The normal dependency direction is the following:
125
127
 
126
- ```text
127
- adapter -> port
128
- adapter -> application
129
- application -> domain
128
+ ```mermaid
129
+ flowchart LR
130
+ adapter[Adapter] --> thirdParty["Third-party library"]
131
+ adapter --> port[Port]
132
+ port --> application[Application]
133
+ port --> domain[Domain]
134
+ application --> domain
130
135
  ```
131
136
 
132
137
  This direction keeps the core model isolated from transport and infrastructure
133
- details. The deeper a layer is, the less it should know about the outside.
138
+ details. Adapters integrate with external libraries and context ports. Ports
139
+ connect the context boundary to application processes or directly to domain
140
+ capabilities when no application orchestration is needed. The deeper a layer
141
+ is, the less it should know about the outside.
134
142
 
135
143
  > **Warning**
136
144
  > Avoid importing adapter-specific concerns into the domain layer. Once a domain
@@ -141,23 +149,23 @@ details. The deeper a layer is, the less it should know about the outside.
141
149
 
142
150
  The following diagram shows the runtime flow of a typical operation.
143
151
 
144
- ```text
145
- External system
146
- |
147
- v
148
- Port + adapter
149
- |
150
- v
151
- Application service
152
- |
153
- v
154
- Domain capability
152
+ ```mermaid
153
+ flowchart TD
154
+ main["main.ts"] --> system["Own system"]
155
+ system --> application["Application services"]
156
+ application --> domain["Domain capabilities"]
157
+ system --> adapter[Adapters]
158
+ adapter --> port[Port]
159
+ adapter --> thirdParty["Third-party libraries"]
160
+ thirdParty --> external["External systems"]
155
161
  ```
156
162
 
157
- The adapter receives the input, the application service coordinates the use
158
- case, and the domain provides the rules and behavior required by that use case.
163
+ `main.ts` is the runtime entry point into the own system. Inside that system,
164
+ application services use domain capabilities, while adapters can depend on
165
+ ports and third-party libraries. The port branch stops at the boundary because
166
+ what exists beyond that port depends on the system that implements it.
159
167
 
160
168
  #### Next Step
161
169
 
162
170
  Use this structure as the default layout for new code. When you need to inspect
163
- the purpose of a generated file, consult `generated-file-reference.md`.
171
+ the purpose of a generated file, consult `generated-file-reference.md`.
@@ -176,10 +176,15 @@ and the representation used by the context.
176
176
 
177
177
  The normal flow of the data abstractions is the following:
178
178
 
179
- ```text
180
- service -> repository -> driver -> data manager -> raw records
181
- service <- repository <- transformed records
179
+ ```mermaid
180
+ flowchart LR
181
+ service[Service] --> repository[Repository]
182
+ repository --> driver[Driver]
183
+ driver --> manager["Data manager"]
184
+ manager --> raw["Raw records"]
185
+ repository --> transformed["Transformed records"]
186
+ transformed --> service
182
187
  ```
183
188
 
184
189
  This separation keeps the application service focused on orchestration while
185
- the repository focuses on transformation.
190
+ the repository focuses on transformation.
@@ -135,10 +135,12 @@ registered or executed.
135
135
 
136
136
  The normal flow is the following:
137
137
 
138
- ```text
139
- service -> dispatch(event)
140
- dispatcher -> matching handlers
141
- handlers -> side effects
138
+ ```mermaid
139
+ flowchart LR
140
+ service[Service] --> dispatch["dispatch(event)"]
141
+ dispatch --> dispatcher[Dispatcher]
142
+ dispatcher --> handlers["Matching handlers"]
143
+ handlers --> effects["Side effects"]
142
144
  ```
143
145
 
144
- This keeps the main process separate from secondary reactions.
146
+ This keeps the main process separate from secondary reactions.
@@ -123,9 +123,12 @@ cross-cutting behavior belongs in the generated HTTP abstraction.
123
123
 
124
124
  #### Example Flow
125
125
 
126
- ```text
127
- request -> middleware -> handler -> response body
126
+ ```mermaid
127
+ flowchart LR
128
+ request[Request] --> middleware[Middleware]
129
+ middleware --> handler[Handler]
130
+ handler --> response["Response body"]
128
131
  ```
129
132
 
130
133
  This flow keeps the transport boundary explicit while leaving framework choices
131
- to the adapter layer.
134
+ to the adapter layer.
@@ -120,8 +120,11 @@ an external platform. It only depends on the application-level contract.
120
120
 
121
121
  #### Example Flow
122
122
 
123
- ```text
124
- service -> Logger contract -> adapter -> logging backend
123
+ ```mermaid
124
+ flowchart LR
125
+ service[Service] --> contract["Logger contract"]
126
+ contract --> adapter[Adapter]
127
+ adapter --> backend["Logging backend"]
125
128
  ```
126
129
 
127
- This flow keeps observability concerns outside the core process.
130
+ This flow keeps observability concerns outside the core process.
@@ -112,9 +112,13 @@ result, depending on the requirements of the use case.
112
112
 
113
113
  #### Example Flow
114
114
 
115
- ```text
116
- input -> service -> domain capabilities -> collaborators -> result
115
+ ```mermaid
116
+ flowchart LR
117
+ input[Input] --> service[Service]
118
+ service --> domain["Domain capabilities"]
119
+ domain --> collaborators[Collaborators]
120
+ collaborators --> result[Result]
117
121
  ```
118
122
 
119
123
  This flow keeps orchestration in the application layer and domain meaning in
120
- the domain layer.
124
+ the domain layer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tshex-cli",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "author": "https://github.com/virtualitems/",
5
5
  "license": "MIT",
6
6
  "description": "Typescript Hexagonal Architecture CLI",
@@ -1,8 +1,8 @@
1
1
  # Hexagonal Architecture CLI `tshex-cli`
2
2
 
3
- `tshex-cli` creates the base structure of a library organized by contexts. The structure groups shared contracts, domain concepts, use cases, and adapters into directories with defined responsibilities.
3
+ `tshex-cli` creates the base structure of a project organized by contexts. The structure groups shared contracts, domain concepts, use cases, and adapters into directories with defined responsibilities.
4
4
 
5
- In this guide, we will build a library named `core` with a context named `users`. The walkthrough starts with the CLI and then explains the purpose of the generated components.
5
+ In this guide, we will build a project named `core` with a context named `users`. The walkthrough starts with the CLI and then explains the purpose of the generated components.
6
6
 
7
7
  The examples in this guide are intentionally simple. They are designed to show the responsibility of each component, not to cover real infrastructure or production scenarios.
8
8
 
@@ -38,32 +38,44 @@ View the available commands and options with:
38
38
  npx tshex --help
39
39
  ```
40
40
 
41
- ### Create a library
41
+ ### Create a project
42
42
 
43
- The `--lib` option receives the name of the library's root directory:
43
+ The `--project` option receives the name of the project's root directory:
44
44
 
45
45
  ```bash
46
- npx tshex --lib core
46
+ npx tshex --project core
47
47
  ```
48
48
 
49
- In this example, `core` will contain the shared code, the contexts, and the library's main implementation.
49
+ You can also use the short form:
50
+
51
+ ```bash
52
+ npx tshex -P core
53
+ ```
54
+
55
+ In this example, `core` will contain the shared code, the contexts, and the project's main implementation.
50
56
 
51
57
  ### Create a context
52
58
 
53
- The `--ctx` option receives the context name:
59
+ The `--context` option receives the context name:
60
+
61
+ ```bash
62
+ npx tshex --context users
63
+ ```
64
+
65
+ You can also use the short form:
54
66
 
55
67
  ```bash
56
- npx tshex --ctx users
68
+ npx tshex -C users
57
69
  ```
58
70
 
59
71
  A context groups the rules and operations of an application capability. `users`, `sales`, `billing`, and `inventory` are examples of contexts.
60
72
 
61
- ### Create the library and the first context
73
+ ### Create the project and the first context
62
74
 
63
75
  You can generate both components in a single execution:
64
76
 
65
77
  ```bash
66
- npx tshex --lib core --ctx users
78
+ npx tshex --project core --context users
67
79
  ```
68
80
 
69
81
  The command creates this structure:
@@ -100,15 +112,15 @@ core/
100
112
  The `--dir` option specifies the directory from which the structure is created:
101
113
 
102
114
  ```bash
103
- npx tshex --dir ./src --lib core --ctx users
115
+ npx tshex --dir ./src --project core --context users
104
116
  ```
105
117
 
106
- The example library is created at `src/core`.
118
+ The example project is created at `src/core`.
107
119
 
108
- To add a context to an existing library, use the library as the destination directory:
120
+ To add a context to an existing project, use the project as the destination directory:
109
121
 
110
122
  ```bash
111
- npx tshex --dir ./core --ctx billing
123
+ npx tshex --dir ./core --context billing
112
124
  ```
113
125
 
114
126
  The context is created at `core/billing`.
@@ -122,13 +134,13 @@ A React context is a consumer by nature. It does not provide a hexagonal capabil
122
134
  Use this mode when the generated context will call APIs, validate interface data, expose hooks, compose components, and localize messages.
123
135
 
124
136
  ```bash
125
- npx tshex --ctx users --react
137
+ npx tshex --context users --react
126
138
  ```
127
139
 
128
140
  You can also use its short form:
129
141
 
130
142
  ```bash
131
- npx tshex --ctx users -R
143
+ npx tshex -C users -R
132
144
  ```
133
145
 
134
146
  The command creates this structure:
@@ -158,14 +170,61 @@ Each directory represents a consumption adapter or a resource used by those adap
158
170
 
159
171
  This layout is intentionally different from the default context template. The standard context separates `domain`, `application`, and `adapters` because it models and provides a capability. The React context generated with `--react` assumes the opposite role: it always consumes capabilities and groups the code around the adapters required by that consumption.
160
172
 
173
+ ### Create a tests structure
174
+
175
+ The `--tests` option receives a source directory and creates a matching tests structure.
176
+
177
+ ```bash
178
+ npx tshex --tests ./core
179
+ ```
180
+
181
+ You can also use the short form:
182
+
183
+ ```bash
184
+ npx tshex -T ./core
185
+ ```
186
+
187
+ The command creates the output inside a `tests/` directory. If `tests/` does not exist, the CLI asks whether it should be created.
188
+
189
+ By default, the `tests/` directory is resolved from the current execution directory:
190
+
191
+ ```bash
192
+ npx tshex -T ./core
193
+ ```
194
+
195
+ This generates a structure like this:
196
+
197
+ ```text
198
+ tests/
199
+ `-- core/
200
+ |-- users/
201
+ | |-- adapters/
202
+ | |-- application/
203
+ | | `-- create-user.ts
204
+ | `-- domain/
205
+ `-- billing/
206
+ |-- application/
207
+ `-- domain/
208
+ ```
209
+
210
+ To choose another base directory for `tests/`, combine `--tests` with `--dir`:
211
+
212
+ ```bash
213
+ npx tshex -T ./core --dir ./output
214
+ ```
215
+
216
+ That command creates or reuses `./output/tests/`.
217
+
218
+ This command helps you prepare a tests workspace that follows the shape of your source directory while fitting naturally into the place where you are working. You can use it in the current directory for a quick setup, or combine it with `--dir` when you want the tests structure to be created somewhere else. If `tests/` already contains content, the command continues working with what is already there instead of interrupting your flow.
219
+
161
220
  ## Documentation index
162
221
 
163
222
  From this point on, the guide is split into dedicated documents under `docs/`.
164
223
 
165
224
  ### General
166
225
 
167
- - [Library structure](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-structure.md)
168
- - [Library types](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-types.md)
226
+ - [Project structure](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-structure.md)
227
+ - [Project types](https://github.com/virtualitems/tshex-cli/blob/main/docs/library-types.md)
169
228
  - [Context ports](https://github.com/virtualitems/tshex-cli/blob/main/docs/context-ports.md)
170
229
  - [Generated file reference](https://github.com/virtualitems/tshex-cli/blob/main/docs/generated-file-reference.md)
171
230
 
package/source/main.ts CHANGED
@@ -8,6 +8,8 @@ import { program } from 'commander'
8
8
 
9
9
  import fs from 'node:fs'
10
10
  import path from 'node:path'
11
+ import readline from 'node:readline/promises'
12
+ import { stdin as input, stdout as output } from 'node:process'
11
13
 
12
14
  // FUNCTIONS
13
15
 
@@ -17,13 +19,13 @@ function readPackageJson() {
17
19
  return JSON.parse(fileContents)
18
20
  }
19
21
 
20
- function executeCreateLib(templatesDir: string, libraryDir: string) {
22
+ function executeCreateProject(templatesDir: string, projectDir: string) {
21
23
  try {
22
- fs.cpSync(path.join(templatesDir, 'lib'), libraryDir, {
24
+ fs.cpSync(path.join(templatesDir, 'lib'), projectDir, {
23
25
  recursive: true,
24
26
  filter: (src) => src.endsWith('.gitkeep') === false
25
27
  })
26
- console.log('Library created successfully')
28
+ console.log('Project created successfully')
27
29
  } catch (err) {
28
30
  console.error(err)
29
31
  }
@@ -53,7 +55,66 @@ function executeCreateReactContext(templatesDir: string, contextDir: string) {
53
55
  }
54
56
  }
55
57
 
56
- function main(program: typeof import('commander').program) {
58
+ function executeCreateTests(sourceDir: string, destinationDir: string, fileContents: string, ignoredSourceDir?: string) {
59
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true })
60
+
61
+ if (fs.existsSync(destinationDir) === false) {
62
+ fs.mkdirSync(destinationDir, { recursive: true })
63
+ }
64
+
65
+ for (const entry of entries) {
66
+ const sourcePath = path.join(sourceDir, entry.name)
67
+ const destinationPath = path.join(destinationDir, entry.name)
68
+
69
+ if (entry.isDirectory()) {
70
+ if (entry.name === 'shared') {
71
+ continue
72
+ }
73
+
74
+ if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
75
+ continue
76
+ }
77
+
78
+ if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
79
+ continue
80
+ }
81
+
82
+ executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir)
83
+ continue
84
+ }
85
+
86
+ if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
87
+ fs.writeFileSync(destinationPath, fileContents)
88
+ }
89
+ }
90
+ }
91
+
92
+ async function ensureTestsDirectory(testsRootDir: string) {
93
+ if (fs.existsSync(testsRootDir)) {
94
+ if (fs.statSync(testsRootDir).isDirectory() === false) {
95
+ throw new Error(`Tests path exists but is not a directory: ${testsRootDir}`)
96
+ }
97
+
98
+ return
99
+ }
100
+
101
+ const rl = readline.createInterface({ input, output })
102
+
103
+ try {
104
+ const answer = await rl.question(`Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `)
105
+
106
+ if (answer.trim().toLowerCase() !== 'y') {
107
+ return false
108
+ }
109
+ } finally {
110
+ rl.close()
111
+ }
112
+
113
+ fs.mkdirSync(testsRootDir, { recursive: true })
114
+ return true
115
+ }
116
+
117
+ async function main(program: typeof import('commander').program) {
57
118
  const templatesDir = path.join(import.meta.dirname, '..', 'templates')
58
119
 
59
120
  const options = program.opts()
@@ -68,17 +129,17 @@ function main(program: typeof import('commander').program) {
68
129
  fs.mkdirSync(targetDir, { recursive: true })
69
130
  }
70
131
 
71
- if (options.lib !== undefined) {
72
- targetDir = path.join(targetDir, options.lib)
73
- executeCreateLib(templatesDir, targetDir)
132
+ if (options.project !== undefined) {
133
+ targetDir = path.join(targetDir, options.project)
134
+ executeCreateProject(templatesDir, targetDir)
74
135
  }
75
136
 
76
- if (options.react === true && options.ctx === undefined) {
77
- program.error('Option --react requires --ctx <name>')
137
+ if (options.react === true && options.context === undefined) {
138
+ program.error('Option --react requires --context <name>')
78
139
  }
79
140
 
80
- if (options.ctx !== undefined) {
81
- targetDir = path.join(targetDir, options.ctx)
141
+ if (options.context !== undefined) {
142
+ targetDir = path.join(targetDir, options.context)
82
143
 
83
144
  if (options.react === true) {
84
145
  executeCreateReactContext(templatesDir, targetDir)
@@ -86,6 +147,41 @@ function main(program: typeof import('commander').program) {
86
147
  executeCreateContext(templatesDir, targetDir)
87
148
  }
88
149
  }
150
+
151
+ if (options.tests !== undefined) {
152
+ const sourceDir = path.resolve(options.tests)
153
+ const testsTemplateFile = path.join(templatesDir, 'tests', 'content.ts')
154
+
155
+ if (fs.existsSync(sourceDir) === false) {
156
+ program.error(`Tests source directory does not exist: ${sourceDir}`)
157
+ }
158
+
159
+ if (fs.statSync(sourceDir).isDirectory() === false) {
160
+ program.error(`Tests source path is not a directory: ${sourceDir}`)
161
+ }
162
+
163
+ if (fs.existsSync(testsTemplateFile) === false) {
164
+ program.error(`Tests template file does not exist: ${testsTemplateFile}`)
165
+ }
166
+
167
+ const testsTemplateContents = fs.readFileSync(testsTemplateFile, 'utf-8')
168
+
169
+ const testsRootDir = path.join(targetDir, 'tests')
170
+ const testsDirectoryCreated = await ensureTestsDirectory(testsRootDir)
171
+
172
+ if (testsDirectoryCreated === false) {
173
+ program.error('Tests directory creation cancelled')
174
+ }
175
+
176
+ const testsDir = path.join(testsRootDir, path.basename(sourceDir))
177
+
178
+ if (testsDir === sourceDir) {
179
+ program.error('Tests destination directory cannot be the same as the source directory')
180
+ }
181
+
182
+ executeCreateTests(sourceDir, testsDir, testsTemplateContents, testsRootDir)
183
+ console.log('Tests structure created successfully')
184
+ }
89
185
  }
90
186
 
91
187
  const packageJson = readPackageJson()
@@ -93,10 +189,14 @@ const packageJson = readPackageJson()
93
189
  program
94
190
  .name('tshex')
95
191
  .version(packageJson.version)
96
- .option('--lib <name>', "creates a new library with it's shared directory")
97
- .option('--ctx <name>', 'creates a new context')
98
- .option('-R, --react', 'creates a React context with --ctx')
192
+ .option('-P, --project <name>', "creates a new project with it's shared directory")
193
+ .option('-C, --context <name>', 'creates a new context')
194
+ .option('-R, --react', 'creates a React context with --context')
195
+ .option('-T, --tests <path>', 'creates a .ts tests structure from an existing directory')
99
196
  .option('--dir <path>', 'sets the directory to create the new item')
100
197
  .parse(process.argv)
101
198
 
102
- main(program)
199
+ void main(program).catch((err: unknown) => {
200
+ const message = err instanceof Error ? err.message : String(err)
201
+ program.error(message)
202
+ })
@@ -0,0 +1,16 @@
1
+ import { test, describe } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ describe('math', () => {
5
+ test('sum', () => {
6
+ // Arrange
7
+ const a = 2
8
+ const b = 3
9
+
10
+ // Act
11
+ const result = a * b
12
+
13
+ // Assert
14
+ assert.strictEqual(result, 6)
15
+ })
16
+ })