@malyuga/aether-cli 0.1.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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +119 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/check.d.ts +4 -0
  8. package/dist/commands/check.d.ts.map +1 -0
  9. package/dist/commands/check.js +45 -0
  10. package/dist/commands/check.js.map +1 -0
  11. package/dist/commands/doctor.d.ts +8 -0
  12. package/dist/commands/doctor.d.ts.map +1 -0
  13. package/dist/commands/doctor.js +62 -0
  14. package/dist/commands/doctor.js.map +1 -0
  15. package/dist/commands/generate.d.ts +15 -0
  16. package/dist/commands/generate.d.ts.map +1 -0
  17. package/dist/commands/generate.js +51 -0
  18. package/dist/commands/generate.js.map +1 -0
  19. package/dist/commands/help.d.ts +6 -0
  20. package/dist/commands/help.d.ts.map +1 -0
  21. package/dist/commands/help.js +60 -0
  22. package/dist/commands/help.js.map +1 -0
  23. package/dist/commands/init.d.ts +7 -0
  24. package/dist/commands/init.d.ts.map +1 -0
  25. package/dist/commands/init.js +24 -0
  26. package/dist/commands/init.js.map +1 -0
  27. package/dist/commands/upgrade.d.ts +5 -0
  28. package/dist/commands/upgrade.d.ts.map +1 -0
  29. package/dist/commands/upgrade.js +42 -0
  30. package/dist/commands/upgrade.js.map +1 -0
  31. package/dist/core/ast.d.ts +7 -0
  32. package/dist/core/ast.d.ts.map +1 -0
  33. package/dist/core/ast.js +90 -0
  34. package/dist/core/ast.js.map +1 -0
  35. package/dist/core/constants.d.ts +133 -0
  36. package/dist/core/constants.d.ts.map +1 -0
  37. package/dist/core/constants.js +135 -0
  38. package/dist/core/constants.js.map +1 -0
  39. package/dist/core/diff.d.ts +20 -0
  40. package/dist/core/diff.d.ts.map +1 -0
  41. package/dist/core/diff.js +39 -0
  42. package/dist/core/diff.js.map +1 -0
  43. package/dist/core/errors.d.ts +19 -0
  44. package/dist/core/errors.d.ts.map +1 -0
  45. package/dist/core/errors.js +37 -0
  46. package/dist/core/errors.js.map +1 -0
  47. package/dist/core/naming.d.ts +10 -0
  48. package/dist/core/naming.d.ts.map +1 -0
  49. package/dist/core/naming.js +50 -0
  50. package/dist/core/naming.js.map +1 -0
  51. package/dist/core/paths.d.ts +10 -0
  52. package/dist/core/paths.d.ts.map +1 -0
  53. package/dist/core/paths.js +38 -0
  54. package/dist/core/paths.js.map +1 -0
  55. package/dist/core/plan.d.ts +37 -0
  56. package/dist/core/plan.d.ts.map +1 -0
  57. package/dist/core/plan.js +126 -0
  58. package/dist/core/plan.js.map +1 -0
  59. package/dist/core/project.d.ts +16 -0
  60. package/dist/core/project.d.ts.map +1 -0
  61. package/dist/core/project.js +66 -0
  62. package/dist/core/project.js.map +1 -0
  63. package/dist/core/render.d.ts +4 -0
  64. package/dist/core/render.d.ts.map +1 -0
  65. package/dist/core/render.js +44 -0
  66. package/dist/core/render.js.map +1 -0
  67. package/dist/generators/middleware.d.ts +11 -0
  68. package/dist/generators/middleware.d.ts.map +1 -0
  69. package/dist/generators/middleware.js +41 -0
  70. package/dist/generators/middleware.js.map +1 -0
  71. package/dist/generators/openapi.d.ts +9 -0
  72. package/dist/generators/openapi.d.ts.map +1 -0
  73. package/dist/generators/openapi.js +46 -0
  74. package/dist/generators/openapi.js.map +1 -0
  75. package/dist/generators/project.d.ts +9 -0
  76. package/dist/generators/project.d.ts.map +1 -0
  77. package/dist/generators/project.js +76 -0
  78. package/dist/generators/project.js.map +1 -0
  79. package/dist/generators/resource-context.d.ts +18 -0
  80. package/dist/generators/resource-context.d.ts.map +1 -0
  81. package/dist/generators/resource-context.js +21 -0
  82. package/dist/generators/resource-context.js.map +1 -0
  83. package/dist/generators/resource-file.d.ts +8 -0
  84. package/dist/generators/resource-file.d.ts.map +1 -0
  85. package/dist/generators/resource-file.js +9 -0
  86. package/dist/generators/resource-file.js.map +1 -0
  87. package/dist/generators/resource-tests.d.ts +5 -0
  88. package/dist/generators/resource-tests.d.ts.map +1 -0
  89. package/dist/generators/resource-tests.js +17 -0
  90. package/dist/generators/resource-tests.js.map +1 -0
  91. package/dist/generators/resource.d.ts +21 -0
  92. package/dist/generators/resource.d.ts.map +1 -0
  93. package/dist/generators/resource.js +64 -0
  94. package/dist/generators/resource.js.map +1 -0
  95. package/dist/generators/types.d.ts +7 -0
  96. package/dist/generators/types.d.ts.map +1 -0
  97. package/dist/generators/types.js +2 -0
  98. package/dist/generators/types.js.map +1 -0
  99. package/dist/help/guide.txt +126 -0
  100. package/dist/openapi/extract-resources.d.ts +3 -0
  101. package/dist/openapi/extract-resources.d.ts.map +1 -0
  102. package/dist/openapi/extract-resources.js +130 -0
  103. package/dist/openapi/extract-resources.js.map +1 -0
  104. package/dist/openapi/load-spec.d.ts +3 -0
  105. package/dist/openapi/load-spec.d.ts.map +1 -0
  106. package/dist/openapi/load-spec.js +19 -0
  107. package/dist/openapi/load-spec.js.map +1 -0
  108. package/dist/openapi/types.d.ts +51 -0
  109. package/dist/openapi/types.d.ts.map +1 -0
  110. package/dist/openapi/types.js +2 -0
  111. package/dist/openapi/types.js.map +1 -0
  112. package/dist/templates/init/aether.config.json.eta +5 -0
  113. package/dist/templates/init/astraeus.config.json.eta +5 -0
  114. package/dist/templates/init/chisel.config.json.eta +5 -0
  115. package/dist/templates/init/env.eta +8 -0
  116. package/dist/templates/init/env.example.eta +8 -0
  117. package/dist/templates/init/gitignore.eta +5 -0
  118. package/dist/templates/init/package.json.eta +14 -0
  119. package/dist/templates/init/src-app-composition.ts.eta +6 -0
  120. package/dist/templates/init/src-app-config.ts.eta +105 -0
  121. package/dist/templates/init/src-app-http.ts.eta +74 -0
  122. package/dist/templates/init/src-app-list.ts.eta +35 -0
  123. package/dist/templates/init/src-app-logger.ts.eta +35 -0
  124. package/dist/templates/init/src-app-middleware-bearer-auth.ts.eta +60 -0
  125. package/dist/templates/init/src-app-middleware-body-limit.ts.eta +37 -0
  126. package/dist/templates/init/src-app-middleware-cors.ts.eta +53 -0
  127. package/dist/templates/init/src-app-middleware-error-handler.ts.eta +15 -0
  128. package/dist/templates/init/src-app-middleware-request-logger.ts.eta +8 -0
  129. package/dist/templates/init/src-app-middleware-types.ts.eta +7 -0
  130. package/dist/templates/init/src-app-router.ts.eta +72 -0
  131. package/dist/templates/init/src-app-server.ts.eta +16 -0
  132. package/dist/templates/init/src-app.ts.eta +36 -0
  133. package/dist/templates/init/src-health-health.routes.ts.eta +12 -0
  134. package/dist/templates/init/src-main.ts.eta +37 -0
  135. package/dist/templates/init/tsconfig.json.eta +14 -0
  136. package/dist/templates/middleware/middleware.ts.eta +6 -0
  137. package/dist/templates/resource/controller.ts.eta +135 -0
  138. package/dist/templates/resource/module.test.ts.eta +16 -0
  139. package/dist/templates/resource/module.ts.eta +25 -0
  140. package/dist/templates/resource/repository.fake.ts.eta +33 -0
  141. package/dist/templates/resource/repository.ts.eta +14 -0
  142. package/dist/templates/resource/routes.ts.eta +18 -0
  143. package/dist/templates/resource/service.ts.eta +45 -0
  144. package/dist/templates/resource/types.ts.eta +12 -0
  145. package/dist/templates/resource/validate.ts.eta +64 -0
  146. package/llms.txt +57 -0
  147. package/package.json +67 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "types": ["node"]
12
+ },
13
+ "include": ["src/**/*"]
14
+ }
@@ -0,0 +1,6 @@
1
+ import type { Middleware } from "./types";
2
+
3
+ export const <%= it.exportName %>: Middleware = async (_req, _res, next) => {
4
+ // TODO: implement <%= it.name %> middleware
5
+ await next();
6
+ };
@@ -0,0 +1,135 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { Params } from "../app/router";
3
+ import { JsonBodyError, readJson, sendError, sendJson, HTTP_STATUS } from "../app/http";
4
+ import type { <%= it.entityPascal %>Service } from "./<%= it.resourceKebab %>.service";
5
+ <% if (it.crud) { %>
6
+ import { parseCreateInput, parseUpdateInput } from "./<%= it.resourceKebab %>.validate";
7
+ import { parseListQuery } from "../app/list";
8
+ <% } %>
9
+
10
+ const REPOSITORY_NOT_CONFIGURED = "Repository not configured";
11
+
12
+ <% if (it.crud) { %>
13
+ async function parseJson(req: IncomingMessage): Promise<unknown> {
14
+ try {
15
+ return await readJson(req);
16
+ } catch (err) {
17
+ if (err instanceof JsonBodyError) {
18
+ throw err;
19
+ }
20
+ throw new JsonBodyError("Invalid JSON body");
21
+ }
22
+ }
23
+
24
+ export function create<%= it.entityPascal %>Controller(deps: {
25
+ service: <%= it.entityPascal %>Service | null;
26
+ repositoryConfigured: boolean;
27
+ }) {
28
+ return {
29
+ async list(req: IncomingMessage, res: ServerResponse, _params: Params): Promise<void> {
30
+ const url = new URL(req.url ?? "/", "http://localhost");
31
+ const parsed = parseListQuery(url.searchParams);
32
+ if (!parsed.ok) {
33
+ sendError(res, HTTP_STATUS.BAD_REQUEST, parsed.message);
34
+ return;
35
+ }
36
+ if (!deps.repositoryConfigured || !deps.service) {
37
+ sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
38
+ return;
39
+ }
40
+ sendJson(res, HTTP_STATUS.OK, deps.service.list(parsed.value));
41
+ },
42
+
43
+ async getById(_req: IncomingMessage, res: ServerResponse, params: Params): Promise<void> {
44
+ if (!deps.repositoryConfigured || !deps.service) {
45
+ sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
46
+ return;
47
+ }
48
+ const item = deps.service.getById(params.id!);
49
+ if (!item) {
50
+ sendError(res, HTTP_STATUS.NOT_FOUND, "Not found");
51
+ return;
52
+ }
53
+ sendJson(res, HTTP_STATUS.OK, item);
54
+ },
55
+
56
+ async create(req: IncomingMessage, res: ServerResponse, _params: Params): Promise<void> {
57
+ if (!deps.repositoryConfigured || !deps.service) {
58
+ sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
59
+ return;
60
+ }
61
+ let body: unknown;
62
+ try {
63
+ body = await parseJson(req);
64
+ } catch (err) {
65
+ if (err instanceof JsonBodyError) {
66
+ sendError(res, err.status, err.message);
67
+ return;
68
+ }
69
+ throw err;
70
+ }
71
+ const parsed = parseCreateInput(body);
72
+ if (!parsed.ok) {
73
+ sendError(res, HTTP_STATUS.BAD_REQUEST, parsed.message);
74
+ return;
75
+ }
76
+ const created = deps.service.create(parsed.value);
77
+ sendJson(res, HTTP_STATUS.CREATED, created);
78
+ },
79
+
80
+ async update(req: IncomingMessage, res: ServerResponse, params: Params): Promise<void> {
81
+ if (!deps.repositoryConfigured || !deps.service) {
82
+ sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
83
+ return;
84
+ }
85
+ let body: unknown;
86
+ try {
87
+ body = await parseJson(req);
88
+ } catch (err) {
89
+ if (err instanceof JsonBodyError) {
90
+ sendError(res, err.status, err.message);
91
+ return;
92
+ }
93
+ throw err;
94
+ }
95
+ const parsed = parseUpdateInput(body);
96
+ if (!parsed.ok) {
97
+ sendError(res, HTTP_STATUS.BAD_REQUEST, parsed.message);
98
+ return;
99
+ }
100
+ const updated = deps.service.update(params.id!, parsed.value);
101
+ if (!updated) {
102
+ sendError(res, HTTP_STATUS.NOT_FOUND, "Not found");
103
+ return;
104
+ }
105
+ sendJson(res, HTTP_STATUS.OK, updated);
106
+ },
107
+
108
+ async remove(_req: IncomingMessage, res: ServerResponse, params: Params): Promise<void> {
109
+ if (!deps.repositoryConfigured || !deps.service) {
110
+ sendError(res, HTTP_STATUS.NOT_IMPLEMENTED, REPOSITORY_NOT_CONFIGURED);
111
+ return;
112
+ }
113
+ const ok = deps.service.remove(params.id!);
114
+ if (!ok) {
115
+ sendError(res, HTTP_STATUS.NOT_FOUND, "Not found");
116
+ return;
117
+ }
118
+ res.statusCode = HTTP_STATUS.NO_CONTENT;
119
+ res.end();
120
+ },
121
+ };
122
+ }
123
+
124
+ export type <%= it.entityPascal %>Controller = ReturnType<typeof create<%= it.entityPascal %>Controller>;
125
+ <% } else { %>
126
+ export function create<%= it.entityPascal %>Controller(deps: { service: <%= it.entityPascal %>Service }) {
127
+ return {
128
+ async status(_req: IncomingMessage, res: ServerResponse, _params: Params): Promise<void> {
129
+ sendJson(res, HTTP_STATUS.OK, { resource: deps.service.status() });
130
+ },
131
+ };
132
+ }
133
+
134
+ export type <%= it.entityPascal %>Controller = ReturnType<typeof create<%= it.entityPascal %>Controller>;
135
+ <% } %>
@@ -0,0 +1,16 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { create<%= it.entityPascal %>Module } from "../src/<%= it.resourceKebab %>/<%= it.resourceKebab %>.module";
4
+ import { createFake<%= it.entityPascal %>Repository } from "./<%= it.resourceKebab %>.repository.fake";
5
+
6
+ describe("<%= it.resourceKebab %> module", () => {
7
+ it("lists a page when repository is wired", () => {
8
+ const repository = createFake<%= it.entityPascal %>Repository();
9
+ repository.create({} as never);
10
+ repository.create({} as never);
11
+ assert.equal(repository.findAll({ limit: 1, offset: 0 }).length, 1);
12
+ assert.equal(repository.findAll({ limit: 1, offset: 1 }).length, 1);
13
+ const mod = create<%= it.entityPascal %>Module({ repository });
14
+ assert.equal(mod.routes.length, 5);
15
+ });
16
+ });
@@ -0,0 +1,25 @@
1
+ <% if (it.crud) { %>
2
+ import type { <%= it.entityPascal %>Repository } from "./<%= it.resourceKebab %>.repository";
3
+ import { create<%= it.entityPascal %>Controller } from "./<%= it.resourceKebab %>.controller";
4
+ import { create<%= it.entityPascal %>Routes } from "./<%= it.resourceKebab %>.routes";
5
+ import { create<%= it.entityPascal %>Service } from "./<%= it.resourceKebab %>.service";
6
+
7
+ export function create<%= it.entityPascal %>Module(opts: { repository?: <%= it.entityPascal %>Repository }) {
8
+ const repositoryConfigured = opts.repository !== undefined;
9
+ const service = repositoryConfigured ? create<%= it.entityPascal %>Service(opts.repository!) : null;
10
+ const controller = create<%= it.entityPascal %>Controller({ service, repositoryConfigured });
11
+ const routes = create<%= it.entityPascal %>Routes(controller);
12
+ return { routes };
13
+ }
14
+ <% } else { %>
15
+ import { create<%= it.entityPascal %>Controller } from "./<%= it.resourceKebab %>.controller";
16
+ import { create<%= it.entityPascal %>Routes } from "./<%= it.resourceKebab %>.routes";
17
+ import { create<%= it.entityPascal %>Service } from "./<%= it.resourceKebab %>.service";
18
+
19
+ export function create<%= it.entityPascal %>Module() {
20
+ const service = create<%= it.entityPascal %>Service();
21
+ const controller = create<%= it.entityPascal %>Controller({ service });
22
+ const routes = create<%= it.entityPascal %>Routes(controller);
23
+ return { routes };
24
+ }
25
+ <% } %>
@@ -0,0 +1,33 @@
1
+ import type { <%= it.entityPascal %>, Create<%= it.entityPascal %>Input } from "../src/<%= it.resourceKebab %>/<%= it.resourceKebab %>.types";
2
+ import type { <%= it.entityPascal %>Repository } from "../src/<%= it.resourceKebab %>/<%= it.resourceKebab %>.repository";
3
+ import type { ListQuery } from "../src/app/list";
4
+
5
+ export function createFake<%= it.entityPascal %>Repository(): <%= it.entityPascal %>Repository {
6
+ const store = new Map<string, <%= it.entityPascal %>>();
7
+ let seq = 1;
8
+
9
+ return {
10
+ findAll(query: ListQuery) {
11
+ return [...store.values()].slice(query.offset, query.offset + query.limit);
12
+ },
13
+ findById(id: string) {
14
+ return store.get(id);
15
+ },
16
+ create(input: Create<%= it.entityPascal %>Input) {
17
+ const id = String(seq++);
18
+ const item = { id, ...input } as <%= it.entityPascal %>;
19
+ store.set(id, item);
20
+ return item;
21
+ },
22
+ update(id, input) {
23
+ const existing = store.get(id);
24
+ if (!existing) return undefined;
25
+ const next = { ...existing, ...input };
26
+ store.set(id, next);
27
+ return next;
28
+ },
29
+ delete(id) {
30
+ return store.delete(id);
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,14 @@
1
+ import type { <%= it.entityPascal %> } from "./<%= it.resourceKebab %>.types";
2
+ <% if (it.crud) { %>
3
+ import type { ListQuery } from "../app/list";
4
+ <% } %>
5
+
6
+ export interface <%= it.entityPascal %>Repository {
7
+ <% if (it.crud) { %>
8
+ findAll(query: ListQuery): <%= it.entityPascal %>[];
9
+ findById(id: string): <%= it.entityPascal %> | undefined;
10
+ create(input: Omit<<%= it.entityPascal %>, "id">): <%= it.entityPascal %>;
11
+ update(id: string, input: Partial<Omit<<%= it.entityPascal %>, "id">>): <%= it.entityPascal %> | undefined;
12
+ delete(id: string): boolean;
13
+ <% } %>
14
+ }
@@ -0,0 +1,18 @@
1
+ <% const basePath = it.routePrefix ? it.routePrefix + "/" + it.resourceKebab : "/" + it.resourceKebab; %>
2
+ import { HTTP_METHOD } from "../app/http";
3
+ import type { Route } from "../app/router";
4
+ import type { <%= it.entityPascal %>Controller } from "./<%= it.resourceKebab %>.controller";
5
+
6
+ export function create<%= it.entityPascal %>Routes(controller: <%= it.entityPascal %>Controller): Route[] {
7
+ return [
8
+ <% if (it.crud) { %>
9
+ { method: HTTP_METHOD.GET, path: "<%= basePath %>", handler: controller.list },
10
+ { method: HTTP_METHOD.GET, path: "<%= basePath %>/:id", handler: controller.getById },
11
+ { method: HTTP_METHOD.POST, path: "<%= basePath %>", handler: controller.create },
12
+ { method: HTTP_METHOD.PATCH, path: "<%= basePath %>/:id", handler: controller.update },
13
+ { method: HTTP_METHOD.DELETE, path: "<%= basePath %>/:id", handler: controller.remove },
14
+ <% } else { %>
15
+ { method: HTTP_METHOD.GET, path: "<%= basePath %>", handler: controller.status },
16
+ <% } %>
17
+ ];
18
+ }
@@ -0,0 +1,45 @@
1
+ <% if (it.crud) { %>
2
+ import type { <%= it.entityPascal %>Repository } from "./<%= it.resourceKebab %>.repository";
3
+ import type {
4
+ <%= it.entityPascal %>,
5
+ Create<%= it.entityPascal %>Input,
6
+ Update<%= it.entityPascal %>Input,
7
+ } from "./<%= it.resourceKebab %>.types";
8
+ import type { ListQuery } from "../app/list";
9
+
10
+ export function create<%= it.entityPascal %>Service(repository: <%= it.entityPascal %>Repository) {
11
+ return {
12
+ list(query: ListQuery): <%= it.entityPascal %>[] {
13
+ return repository.findAll(query);
14
+ },
15
+
16
+ getById(id: string): <%= it.entityPascal %> | undefined {
17
+ return repository.findById(id);
18
+ },
19
+
20
+ create(input: Create<%= it.entityPascal %>Input): <%= it.entityPascal %> {
21
+ return repository.create(input);
22
+ },
23
+
24
+ update(id: string, input: Update<%= it.entityPascal %>Input): <%= it.entityPascal %> | undefined {
25
+ return repository.update(id, input);
26
+ },
27
+
28
+ remove(id: string): boolean {
29
+ return repository.delete(id);
30
+ },
31
+ };
32
+ }
33
+
34
+ export type <%= it.entityPascal %>Service = ReturnType<typeof create<%= it.entityPascal %>Service>;
35
+ <% } else { %>
36
+ export function create<%= it.entityPascal %>Service() {
37
+ return {
38
+ status(): string {
39
+ return "<%= it.resourceKebab %>";
40
+ },
41
+ };
42
+ }
43
+
44
+ export type <%= it.entityPascal %>Service = ReturnType<typeof create<%= it.entityPascal %>Service>;
45
+ <% } %>
@@ -0,0 +1,12 @@
1
+ export type <%= it.entityPascal %> = {
2
+ id: string;
3
+ <% if (it.crud) { it.fields.forEach(function(field) { %>
4
+ <%= field.name %>: <%= field.tsType %>;
5
+ <% }); } %>
6
+ };
7
+
8
+ <% if (it.crud) { %>
9
+ export type Create<%= it.entityPascal %>Input = Omit<<%= it.entityPascal %>, "id">;
10
+
11
+ export type Update<%= it.entityPascal %>Input = Partial<Omit<<%= it.entityPascal %>, "id">>;
12
+ <% } %>
@@ -0,0 +1,64 @@
1
+ import type { Create<%= it.entityPascal %>Input, Update<%= it.entityPascal %>Input } from "./<%= it.resourceKebab %>.types";
2
+
3
+ /** Replace this module with Zod/Ajv/etc. when you outgrow hand-rolled checks. */
4
+ export type ValidateResult<T> = { ok: true; value: T } | { ok: false; message: string };
5
+
6
+ export function parseCreateInput(body: unknown): ValidateResult<Create<%= it.entityPascal %>Input> {
7
+ if (typeof body !== "object" || body === null) {
8
+ return { ok: false, message: "Invalid request body" };
9
+ }
10
+ const b = body as Record<string, unknown>;
11
+ <% if (it.fields.length === 0) { %>
12
+ for (const key of Object.keys(b)) {
13
+ return { ok: false, message: "Invalid request body" };
14
+ }
15
+ return { ok: true, value: {} as Create<%= it.entityPascal %>Input };
16
+ <% } else { %>
17
+ <% it.fields.filter(function(f) { return f.required; }).forEach(function(field) { %>
18
+ if (typeof b.<%= field.name %> !== "<%= field.tsType %>") {
19
+ return { ok: false, message: "Invalid request body" };
20
+ }
21
+ <% if (field.tsType === "string") { %>
22
+ if ((b.<%= field.name %> as string).length === 0) {
23
+ return { ok: false, message: "Invalid request body" };
24
+ }
25
+ <% } %>
26
+ <% }); %>
27
+ return {
28
+ ok: true,
29
+ value: b as Create<%= it.entityPascal %>Input,
30
+ };
31
+ <% } %>
32
+ }
33
+
34
+ export function parseUpdateInput(body: unknown): ValidateResult<Update<%= it.entityPascal %>Input> {
35
+ if (typeof body !== "object" || body === null) {
36
+ return { ok: false, message: "Invalid request body" };
37
+ }
38
+ <% if (it.fields.length === 0) { %>
39
+ return { ok: false, message: "No update fields configured" };
40
+ <% } else { %>
41
+ const b = body as Record<string, unknown>;
42
+ let hasField = false;
43
+ <% it.fields.forEach(function(field) { %>
44
+ if (b.<%= field.name %> !== undefined) {
45
+ hasField = true;
46
+ if (typeof b.<%= field.name %> !== "<%= field.tsType %>") {
47
+ return { ok: false, message: "Invalid request body" };
48
+ }
49
+ <% if (field.tsType === "string") { %>
50
+ if ((b.<%= field.name %> as string).length === 0) {
51
+ return { ok: false, message: "Invalid request body" };
52
+ }
53
+ <% } %>
54
+ }
55
+ <% }); %>
56
+ if (!hasField) {
57
+ return { ok: false, message: "Invalid request body" };
58
+ }
59
+ return {
60
+ ok: true,
61
+ value: b as Update<%= it.entityPascal %>Input,
62
+ };
63
+ <% } %>
64
+ }
package/llms.txt ADDED
@@ -0,0 +1,57 @@
1
+ # Aether CLI
2
+
3
+ > Source scaffolder for plain TypeScript HTTP backends on Node's built-in `http` module. Generated apps have zero runtime npm dependencies and do not require Aether at runtime.
4
+
5
+ ## When to use
6
+
7
+ - Starting a new Node.js backend with TypeScript and `node:http`, not a runtime web framework
8
+ - You want generated source you own: router, middleware, composition root, CRUD layers
9
+ - Generating CRUD modules from an OpenAPI 3.0 specification
10
+ - Production deploy should be `tsc` + `node` with no application `dependencies`
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm i -g @malyuga/aether-cli
16
+ ```
17
+
18
+ Requires Node.js 20+.
19
+
20
+ ## Quick start
21
+
22
+ ```bash
23
+ mkdir my-api && cd my-api
24
+ aether init
25
+ npm install
26
+ aether g resource users --crud
27
+ npm run dev
28
+ ```
29
+
30
+ After `init`, `GET /health` works. CRUD routes are registered in `src/app/composition.ts`, but the generated repository is an interface only. Until you pass a real implementation to `createUserModule({ repository: … })`, CRUD returns HTTP 501 with `{ "error": "Repository not configured" }`.
31
+
32
+ ## Commands
33
+
34
+ - `aether init` — scaffold project in the current directory (`-f`, `--dry-run`, `--diff`, `--json`)
35
+ - `aether g resource <name>` — resource module; `--crud` for full stack; `--tests` for fake repo + tests
36
+ - `aether g middleware <name>` — middleware stub; `--global` registers in `src/app.ts`
37
+ - `aether g openapi <spec>` — CRUD resources from OpenAPI 3.0 JSON/YAML
38
+ - `aether check` — verify composition and config layout
39
+ - `aether doctor` — Node version, project context, packaged templates
40
+ - `aether upgrade` — read-only drift report; does not rewrite code
41
+ - `aether help` — full terminal guide
42
+
43
+ Alias: `g` for `generate`. Global flags: `--json`, `--diff` (with `--dry-run`).
44
+
45
+ ## Generated app notes
46
+
47
+ - CommonJS; local imports without file extensions
48
+ - Optional Bearer auth via `BEARER_TOKEN` in `.env` (wired by `init`; health stays public)
49
+ - OpenAPI re-run without `--force` updates `*.types.ts` and `*.validate.ts` only
50
+
51
+ ## Documentation
52
+
53
+ - [README](https://github.com/MalyugaSensei/aether-cli/blob/main/README.md): user-facing overview and examples
54
+ - [npm package](https://www.npmjs.com/package/@malyuga/aether-cli): install and registry metadata
55
+ - [Repository](https://github.com/MalyugaSensei/aether-cli): source and issues
56
+
57
+ This file is for external coding agents. Repository `AGENTS.md` is for contributors developing the CLI itself, not for end-user scaffolding.
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@malyuga/aether-cli",
3
+ "version": "0.1.1",
4
+ "description": "Scaffold plain TypeScript HTTP backends on node:http with zero runtime dependencies; OpenAPI-to-CRUD and explicit composition wiring",
5
+ "keywords": [
6
+ "aether",
7
+ "cli",
8
+ "scaffold",
9
+ "generator",
10
+ "backend-scaffold",
11
+ "typescript-backend",
12
+ "node-http",
13
+ "openapi",
14
+ "crud-generator",
15
+ "zero-runtime-dependencies",
16
+ "composition-root"
17
+ ],
18
+ "license": "MIT",
19
+ "author": "MalyugaSensei",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/MalyugaSensei/aether-cli.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/MalyugaSensei/aether-cli/issues"
26
+ },
27
+ "homepage": "https://github.com/MalyugaSensei/aether-cli#readme",
28
+ "type": "module",
29
+ "bin": {
30
+ "aether": "dist/cli.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "llms.txt"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc && node scripts/copy-templates.mjs",
41
+ "pretest": "npm run build",
42
+ "dev": "tsx src/cli.ts",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "test:e2e": "vitest run tests/integration/e2e-install.test.ts",
46
+ "prepublishOnly": "npm test",
47
+ "release:patch": "npm version patch",
48
+ "release:minor": "npm version minor",
49
+ "release:major": "npm version major"
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "dependencies": {
55
+ "commander": "^15.0.0",
56
+ "eta": "^4.6.0",
57
+ "prettier": "^3.9.7",
58
+ "ts-morph": "^28.0.0",
59
+ "yaml": "^2.9.1"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^22.20.3",
63
+ "tsx": "^4.23.13",
64
+ "typescript": "^7.0.2",
65
+ "vitest": "^5.0.1"
66
+ }
67
+ }