tshex-cli 1.0.24 → 1.0.25

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
@@ -45,7 +45,7 @@ function executeCreateReactContext(templatesDir, contextDir) {
45
45
  console.error(err);
46
46
  }
47
47
  }
48
- function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir) {
48
+ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir, rootSourceDir = sourceDir) {
49
49
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
50
50
  if (fs.existsSync(destinationDir) === false) {
51
51
  fs.mkdirSync(destinationDir, { recursive: true });
@@ -54,7 +54,7 @@ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSour
54
54
  const sourcePath = path.join(sourceDir, entry.name);
55
55
  const destinationPath = path.join(destinationDir, entry.name);
56
56
  if (entry.isDirectory()) {
57
- if (entry.name === 'shared') {
57
+ if (entry.name === 'shared' && sourceDir === rootSourceDir) {
58
58
  continue;
59
59
  }
60
60
  if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
@@ -63,7 +63,7 @@ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSour
63
63
  if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
64
64
  continue;
65
65
  }
66
- executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir);
66
+ executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir);
67
67
  continue;
68
68
  }
69
69
  if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
@@ -4,10 +4,10 @@ Context ports define the communication available at a context boundary.
4
4
  They describe which concrete capability a context exposes, what data enters
5
5
  that capability, and what data it returns.
6
6
 
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.
7
+ In practice a port is a specific boundary element of the system with identity:
8
+ a command handler, a query entry point, an event consumer, a published
9
+ endpoint, or another concrete interaction mechanism that exists because the
10
+ running system exposes it.
11
11
 
12
12
  Types and interfaces still matter, but they are secondary. Their role is to
13
13
  make the port explicit. The main concern is the port as a real executable
@@ -22,13 +22,19 @@ surface that another actor can call or observe.
22
22
  The generated context starts with a single root file for ports.
23
23
 
24
24
  ```ts title="users/example-ports.ts"
25
- export function example(): void {
26
- // ...
25
+ export class ExamplePort {
26
+ public doSomething(): void {
27
+ // ...
28
+ }
27
29
  }
28
30
  ```
29
31
 
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.
32
+ This placeholder marks the context root as the place where boundary-facing
33
+ capabilities are declared. Replace it with the first module that defines a real
34
+ port of the context when that capability becomes clear.
35
+
36
+ Context root `.ts` files belong to this same boundary surface. Each one is
37
+ expected to be a module that defines one or more context ports.
32
38
 
33
39
  #### First Port
34
40
 
@@ -36,113 +42,56 @@ In the following example we replace the placeholder with a concrete port for
36
42
  creating a user.
37
43
 
38
44
  ```ts title="users/example-ports.ts"
39
- export type CreateUserRequest = {
40
- user: {
41
- id: string
42
- email: string
43
- active: boolean | null
44
- }
45
+ export interface User {
46
+ id: string
47
+ email: string
48
+ active: boolean | null
45
49
  }
46
50
 
47
- export type CreateUserResponse = {
48
- user: {
49
- id: string
50
- email: string
51
- active: boolean | null
51
+ export class UsersRegistry {
52
+ public async createUser(user: User): Promise<User> {
53
+ // ...
52
54
  }
53
55
  }
54
-
55
- export interface CreateUserPort {
56
- create(request: CreateUserRequest): Promise<CreateUserResponse>
57
- }
58
56
  ```
59
57
 
60
- `CreateUserRequest` defines the incoming payload. `CreateUserResponse` defines
61
- the outgoing payload. `CreateUserPort` names the concrete capability exposed at
62
- the boundary: creating a user.
58
+ `User` expresses the user data that crosses the boundary. `UsersRegistry` is a
59
+ boundary object of the context and `createUser()` is one concrete capability
60
+ that it exposes.
63
61
 
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.
62
+ This definition focuses on the interaction the context makes available. The
63
+ port keeps its identity as one boundary capability while making its data and
64
+ action explicit.
67
65
 
68
- #### Adapter Implementation
66
+ The port is the executable object that this context exposes. Types help
67
+ describe it and make its boundary explicit.
69
68
 
70
- Now that the port exists, the system can materialize it through an adapter and
71
- delegate the work to an application service.
72
-
73
- ```ts title="users/adapters/create-user.ts"
74
- import type {
75
- CreateUserPort,
76
- CreateUserRequest,
77
- CreateUserResponse,
78
- } from '../example-ports.js'
79
-
80
- type CreateUserService = {
81
- execute(data: {
82
- id: string
83
- email: string
84
- active: boolean | null
85
- }): Promise<CreateUserResponse>
86
- }
87
-
88
- export class CreateUserAdapter implements CreateUserPort {
89
- public constructor(
90
- private readonly service: CreateUserService,
91
- ) {}
92
-
93
- public async create(
94
- request: CreateUserRequest,
95
- ): Promise<CreateUserResponse> {
96
- return this.service.execute({
97
- id: request.user.id,
98
- email: request.user.email,
99
- active: request.user.active,
100
- })
101
- }
102
- }
103
- ```
104
-
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.
108
-
109
- This is the normal flow of the generated structure:
69
+ This is the normal flow inside the context boundary:
110
70
 
111
71
  ```mermaid
112
72
  flowchart LR
113
- external["External system"] --> adapter[Adapter]
114
- adapter --> port[Port]
73
+ caller["Caller"] --> port[Concrete port]
115
74
  port --> application[Application]
116
75
  application --> domain[Domain]
117
76
  ```
118
77
 
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.
78
+ The port belongs to the boundary because it is part of what the context
79
+ exposes. The application process executes the use case behind the exposed
80
+ capability and uses domain capabilities.
122
81
 
123
82
  #### Multiple Port Files
124
83
 
125
- As the context grows, you can keep several ports at the context root.
126
-
127
- ```ts title="users/list-users.ts"
128
- export type ListUsersRequest = {
129
- active: boolean | null
130
- }
84
+ As the context grows, you can keep several port modules at the context root.
131
85
 
132
- export type ListUsersResponse = {
133
- users: Array<{
134
- id: string
135
- email: string
136
- active: boolean | null
137
- }>
138
- }
139
-
140
- export interface ListUsersPort {
141
- list(request: ListUsersRequest): Promise<ListUsersResponse>
86
+ ```ts title="users/registry.ts"
87
+ export class UsersRegistry {
88
+ public async listUsers(): Promise<User[]> {
89
+ // ...
90
+ }
142
91
  }
143
92
  ```
144
93
 
145
- An adapter can then import the port definition from the file that owns it.
94
+ Another caller can then import the port from the file that owns it.
146
95
 
147
96
  This arrangement is useful when one context exposes several independent
148
97
  capabilities. A single file works well for a small context. Separate files
@@ -150,9 +99,9 @@ become easier to maintain when each port has its own identity and
150
99
  responsibility.
151
100
 
152
101
  > **Warning**
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.
102
+ > A port should define an exposed boundary capability.
103
+ > Keep business rules, repository logic, and infrastructure details in their
104
+ > corresponding layers.
156
105
 
157
106
  #### Example Layout
158
107
 
@@ -161,16 +110,14 @@ in the generated folders.
161
110
 
162
111
  ```mermaid
163
112
  flowchart TD
164
- users["users/"] --> examplePorts["example-ports.ts"]
165
- users --> listUsers["list-users.ts"]
166
- users --> adapters["adapters/"]
113
+ users["users/"] --> registry["registry.ts"]
167
114
  users --> application["application/"]
168
115
  users --> domain["domain/"]
169
116
  ```
170
117
 
171
118
  This layout keeps the context boundary visible from the top level. It also
172
- reduces coupling between adapters because they all import the same port
173
- definitions for the capabilities the context exposes.
119
+ makes each exposed capability easy to locate because the port modules stay at
120
+ the root of the context.
174
121
 
175
122
  #### Next Step
176
123
 
@@ -61,14 +61,14 @@ application coordinates data access without forcing a specific driver.
61
61
 
62
62
  #### Context Files
63
63
 
64
- Each generated context starts with a root port file and three directories.
64
+ Each generated context starts with an example port file and three directories.
65
65
 
66
66
  | Path | Responsibility |
67
67
  | --- | --- |
68
- | `users/example-ports.ts` | Placeholder root file for context ports. |
68
+ | `users/example-ports.ts` | Example module for context ports. All context root `.ts` files are expected to define context ports. |
69
69
  | `users/domain/` | Domain capabilities and rules for the context. |
70
70
  | `users/application/` | Processes that use domain capabilities to fulfill system purposes. |
71
- | `users/adapters/` | Integrations that implement ports and connect the context to external systems. |
71
+ | `users/adapters/` | Integrations that wraps third-party libraries or context ports. |
72
72
 
73
73
  The `users/` path is an example context name. Your project can generate one or
74
74
  more contexts with the same internal layout.
@@ -81,8 +81,8 @@ Use the following sequence when deciding where new code belongs.
81
81
  2. Put reusable application contracts in `shared/application`.
82
82
  3. Put context-specific rules in `<context>/domain`.
83
83
  4. Put use cases in `<context>/application`.
84
- 5. Put boundary implementations in `<context>/adapters`.
85
- 6. Put boundary contracts at the context root.
84
+ 5. Put transport and infrastructure integrations in `<context>/adapters`.
85
+ 6. Put concrete boundary ports in context root `.ts` files.
86
86
 
87
87
  This reference explains placement. The architectural rationale is described in
88
- `library-structure.md`.
88
+ `library-structure.md`.
@@ -72,9 +72,14 @@ flowchart TD
72
72
  users --> domain["domain/"]
73
73
  ```
74
74
 
75
- `example-ports.ts` is the root communication surface of the context.
75
+ `example-ports.ts` is an example module in the root communication surface of
76
+ the context.
76
77
  `domain/` contains capabilities and rules. `application/` contains processes.
77
- `adapters/` contains boundary implementations.
78
+ `adapters/` contains integrations that wrap third-party libraries or context
79
+ ports.
80
+
81
+ Context root `.ts` files belong to the boundary surface of the context. Each
82
+ one is expected to be a module that defines one or more context ports.
78
83
 
79
84
  #### Domain
80
85
 
@@ -105,21 +110,23 @@ business rules themselves.
105
110
  The adapters layer contains the integrations that connect a context to other
106
111
  systems.
107
112
 
108
- An adapter can expose an HTTP handler, consume a message, call a remote API,
109
- implement a data driver, or connect to an event bus. Its role is to translate
110
- external input or output into the contracts expected by the application layer.
113
+ An adapter wraps a third-party library or a context port so that the context
114
+ can interact with a concrete transport or infrastructure path. An adapter can
115
+ expose an HTTP handler, consume a message, call a remote API, implement a data
116
+ driver, or connect to an event bus.
111
117
 
112
118
  #### Ports
113
119
 
114
120
  Ports define the communication available at the context boundary.
115
121
 
116
122
  They live at the context root because they describe how the context is used
117
- from the outside. An adapter imports a port, implements it, and delegates the
118
- work to an application process.
123
+ from the outside. A port is the concrete object the context exposes. Adapters
124
+ or other callers can use that port object and route work into an application
125
+ process.
119
126
 
120
- The generated template starts with `example-ports.ts`, but a larger context may
121
- split ports across several files. The detailed guidance for that layout lives in
122
- `context-ports.md`.
127
+ The generated template starts with `example-ports.ts`. As the context grows,
128
+ additional context root `.ts` modules can define more ports. The detailed
129
+ guidance for that layout lives in `context-ports.md`.
123
130
 
124
131
  #### Dependency Direction
125
132
 
@@ -130,15 +137,13 @@ flowchart LR
130
137
  adapter[Adapter] --> thirdParty["Third-party library"]
131
138
  adapter --> port[Port]
132
139
  port --> application[Application]
133
- port --> domain[Domain]
134
140
  application --> domain
135
141
  ```
136
142
 
137
143
  This direction keeps the core model isolated from transport and infrastructure
138
144
  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.
145
+ connect the context boundary to application processes. The deeper a layer is,
146
+ the less it should know about the outside.
142
147
 
143
148
  > **Warning**
144
149
  > Avoid importing adapter-specific concerns into the domain layer. Once a domain
@@ -1,9 +1,8 @@
1
1
  ### Aggregates
2
2
 
3
- An aggregate is responsible for grouping multiple entities into one logical
4
- domain unit.
5
- It is used when the behavior of the concept depends on the collaboration of
6
- several entities instead of a single entity in isolation.
3
+ An aggregate groups multiple entities into one logical domain unit.
4
+ It is useful when the behavior of a concept depends on the collaboration of
5
+ several entities working together.
7
6
 
8
7
  The generated template provides `Aggregate` as a semantic base class.
9
8
 
@@ -15,45 +14,145 @@ The generated template provides `Aggregate` as a semantic base class.
15
14
  export abstract class Aggregate {}
16
15
  ```
17
16
 
18
- This means the generated base class does not impose persistence rules, event
19
- publication, or identity behavior. Those concerns remain in the concrete domain
20
- model of the project.
17
+ The class is intentionally empty. Its role is to provide the semantic base for
18
+ an aggregate whose concrete implementation gathers the involved entities as a
19
+ key-value structure through its own properties, such as `buyer`, `seller`,
20
+ `order`, or `invoice`. Persistence rules, event publication, and identity
21
+ behavior live in the concrete domain model of the project.
21
22
 
22
23
  #### First Aggregate
23
24
 
24
- In the following example we model a shopping cart as a group of line items.
25
+ In the following example we model a sale as one domain unit composed of a
26
+ buyer, a seller, an order, and an invoice generated by the aggregate itself.
25
27
 
26
- ```ts title="users/domain/cart.ts"
28
+ ```ts title="sales/domain/sale.ts"
27
29
  import { Aggregate } from '../../shared/domain/aggregates.js'
28
30
  import { Entity } from '../../shared/domain/entities.js'
29
31
 
30
- class LineItem extends Entity {
32
+ class Person extends Entity {
33
+ public readonly id: string
34
+ public readonly name: string
35
+
36
+ public constructor(id: string, name: string) {
37
+ super()
38
+ this.id = id
39
+ this.name = name
40
+ }
41
+
42
+ public equals(other: Entity): boolean {
43
+ return other instanceof Person && this.id === other.id
44
+ }
45
+ }
46
+
47
+ class Product extends Entity {
48
+ public readonly id: string
49
+ public readonly name: string
50
+ public readonly price: number
51
+
52
+ public constructor(id: string, name: string, price: number) {
53
+ super()
54
+ this.id = id
55
+ this.name = name
56
+ this.price = price
57
+ }
58
+
59
+ public equals(other: Entity): boolean {
60
+ return other instanceof Product && this.id === other.id
61
+ }
62
+ }
63
+
64
+ class Order extends Entity {
65
+ public readonly id: string
66
+ public readonly products: Product[]
67
+
68
+ public constructor(id: string, products: Product[]) {
69
+ super()
70
+ this.id = id
71
+ this.products = products
72
+ }
73
+
74
+ public equals(other: Entity): boolean {
75
+ return other instanceof Order && this.id === other.id
76
+ }
77
+ }
78
+
79
+ class Invoice extends Entity {
80
+ public readonly id: string
81
+ public readonly order: Order
82
+ public readonly buyer: Person
83
+ public readonly seller: Person
84
+ public readonly total: number
85
+
31
86
  public constructor(
32
- public readonly id: string,
33
- public readonly quantity: number,
87
+ id: string,
88
+ order: Order,
89
+ buyer: Person,
90
+ seller: Person,
34
91
  ) {
35
92
  super()
93
+ this.id = id
94
+ this.order = order
95
+ this.buyer = buyer
96
+ this.seller = seller
97
+ this.total = this.order.products.reduce(
98
+ (sum, product) => sum + product.price,
99
+ 0,
100
+ )
36
101
  }
37
102
 
38
103
  public equals(other: Entity): boolean {
39
- return other instanceof LineItem && this.id === other.id
104
+ return other instanceof Invoice && this.id === other.id
40
105
  }
41
106
  }
42
107
 
43
- export class Cart extends Aggregate {
44
- public constructor(public readonly items: Array<LineItem>) {
108
+ export class Sale extends Aggregate {
109
+ public readonly buyer: Person
110
+ public readonly seller: Person
111
+ public readonly order: Order
112
+ protected invoice: Invoice | null
113
+
114
+ public constructor(
115
+ buyer: Person,
116
+ seller: Person,
117
+ order: Order,
118
+ ) {
45
119
  super()
120
+ this.buyer = buyer
121
+ this.seller = seller
122
+ this.order = order
123
+ this.invoice = null
124
+ }
125
+
126
+ public generateInvoice(): { invoice: Invoice } {
127
+ const id = Math.random().toString()
128
+ const invoice = new Invoice(
129
+ id,
130
+ this.order,
131
+ this.buyer,
132
+ this.seller,
133
+ )
134
+
135
+ this.invoice = invoice
136
+
137
+ return { invoice }
46
138
  }
47
139
 
48
- public totalItems(): number {
49
- return this.items.reduce((sum, item) => sum + item.quantity, 0)
140
+ public getInvoice(): Invoice | null {
141
+ return this.invoice
50
142
  }
51
143
  }
52
144
  ```
53
145
 
54
- `Cart` is an aggregate because its behavior depends on the collection of
55
- entities that compose it. `LineItem` remains an entity because its identity is
56
- independent inside the aggregate.
146
+ `Sale` is an aggregate because the rule for generating the invoice depends on
147
+ the collaboration between buyer, seller, and order. In this example, both
148
+ buyer and seller are modeled with the same `Person` entity, while `Order`
149
+ stores the list of purchased products and `Invoice` stores the computed
150
+ `total`. `Invoice` is created inside the aggregate, and its constructor
151
+ calculates the total from the order products when the invoice is generated. The
152
+ `invoice` field stays inside the aggregate lifecycle and `getInvoice()` offers
153
+ controlled access to the generated invoice. When a domain object already
154
+ exists, the example uses that object directly and keeps the collaboration
155
+ between domain parts explicit.
57
156
 
58
157
  #### Responsibility Boundary
59
158
 
@@ -66,14 +165,15 @@ Examples include:
66
165
  2. keeping related entities in a consistent state;
67
166
  3. exposing operations that depend on the collaboration of those entities.
68
167
 
69
- The generated `Aggregate` base class does not implement these rules for you. It
70
- only provides the semantic place where those rules belong.
168
+ The generated `Aggregate` base class provides the semantic place where those
169
+ rules belong. The concrete aggregate defines and coordinates the rules that
170
+ keep the domain unit consistent.
71
171
 
72
172
  > **Warning**
73
- > Do not extend `Aggregate` only to create a container for unrelated values. Use
74
- > it when the concept represents one domain unit composed of several parts.
173
+ > Extend `Aggregate` when the concept represents one domain unit composed of
174
+ > several related parts and shared rules.
75
175
 
76
176
  #### Next Step
77
177
 
78
178
  Aggregates usually collaborate with entities and value objects. The base entity
79
- behavior is documented in `entities.md`.
179
+ behavior is documented in `entities.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tshex-cli",
3
- "version": "1.0.24",
3
+ "version": "1.0.25",
4
4
  "author": "https://github.com/virtualitems/",
5
5
  "license": "MIT",
6
6
  "description": "Typescript Hexagonal Architecture CLI",
@@ -45,6 +45,7 @@
45
45
  "commander": "^12.1.0"
46
46
  },
47
47
  "devDependencies": {
48
+ "@fission-ai/openspec": "^1.6.0",
48
49
  "@types/node": "^22.5.4",
49
50
  "typescript": "^5.4.5"
50
51
  },
package/source/main.ts CHANGED
@@ -55,7 +55,13 @@ function executeCreateReactContext(templatesDir: string, contextDir: string) {
55
55
  }
56
56
  }
57
57
 
58
- function executeCreateTests(sourceDir: string, destinationDir: string, fileContents: string, ignoredSourceDir?: string) {
58
+ function executeCreateTests(
59
+ sourceDir: string,
60
+ destinationDir: string,
61
+ fileContents: string,
62
+ ignoredSourceDir?: string,
63
+ rootSourceDir: string = sourceDir
64
+ ) {
59
65
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true })
60
66
 
61
67
  if (fs.existsSync(destinationDir) === false) {
@@ -67,7 +73,7 @@ function executeCreateTests(sourceDir: string, destinationDir: string, fileConte
67
73
  const destinationPath = path.join(destinationDir, entry.name)
68
74
 
69
75
  if (entry.isDirectory()) {
70
- if (entry.name === 'shared') {
76
+ if (entry.name === 'shared' && sourceDir === rootSourceDir) {
71
77
  continue
72
78
  }
73
79
 
@@ -79,7 +85,7 @@ function executeCreateTests(sourceDir: string, destinationDir: string, fileConte
79
85
  continue
80
86
  }
81
87
 
82
- executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir)
88
+ executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir)
83
89
  continue
84
90
  }
85
91