@katajs/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/index.js +782 -0
- package/dist/templates/module/errors.ts +15 -0
- package/dist/templates/module/index.ts +24 -0
- package/dist/templates/module/routes.ts +11 -0
- package/dist/templates/module/schema.ts +14 -0
- package/dist/templates/module/service.ts +13 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yaseer A. Okino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @katajs/cli
|
|
2
|
+
|
|
3
|
+
Project commands for [katajs](https://github.com/ookino/katajs) apps. Install as a devDependency in your scaffolded project (it's already there if you used `pnpm create katajs`).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add -D @katajs/cli
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
### `katajs add module <name>`
|
|
12
|
+
|
|
13
|
+
Scaffolds a new module: `src/modules/<name>/` with the standard 5 files, plus mutations to wire it into the app.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm katajs add module comments
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Generates:
|
|
20
|
+
- `src/modules/comments/index.ts` — `defineModule` call + `CommentsRegistry` type export
|
|
21
|
+
- `src/modules/comments/comments.service.ts` — service factory with a `ping()` example method
|
|
22
|
+
- `src/modules/comments/comments.routes.ts` — Hono routes with one example `GET /`
|
|
23
|
+
- `src/modules/comments/comments.schema.ts` — Zod schema starter
|
|
24
|
+
- `src/modules/comments/comments.errors.ts` — `AppError` subclass starter
|
|
25
|
+
|
|
26
|
+
Mutates:
|
|
27
|
+
- `src/types.d.ts` — adds `CommentsRegistry` import + extends entry
|
|
28
|
+
- `src/app.ts` — adds `commentsModule` import, modules-array entry, and route mount
|
|
29
|
+
- `scripts/graph.ts` — adds `commentsModule` to the graph script (if present)
|
|
30
|
+
|
|
31
|
+
The mutations rely on anchor comments (`// katajs:registry`, `// katajs:modules`, `// katajs:routes`, etc.) that ship in the scaffolder templates. If you've removed them, the command prints the snippet for manual paste instead of failing.
|
|
32
|
+
|
|
33
|
+
#### Naming
|
|
34
|
+
|
|
35
|
+
Names are normalized into kebab-case (directory), camelCase (variable), PascalCase (type), and snake_case (error code):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
katajs add module user-profile
|
|
39
|
+
# directory: src/modules/user-profile/
|
|
40
|
+
# variable: userProfileModule, userProfileService
|
|
41
|
+
# type: UserProfileService, UserProfileRegistry, UserProfileNotFoundError
|
|
42
|
+
# code: 'user_profile_not_found'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Names must start with a letter and contain only letters, digits, and hyphens.
|
|
46
|
+
|
|
47
|
+
### `katajs add service <name> --in <module>`
|
|
48
|
+
|
|
49
|
+
Adds a single service to an existing module.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm katajs add service featured --in posts
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Generates `src/modules/posts/featured.service.ts` and wires it into the module's `index.ts`:
|
|
56
|
+
|
|
57
|
+
- New `import { makeFeaturedService, type FeaturedService } from './featured.service';` line
|
|
58
|
+
- New `featuredService: (c) => makeFeaturedService(c),` entry in `provides`
|
|
59
|
+
- New `featuredService: FeaturedService;` entry in the `PostsRegistry` slice
|
|
60
|
+
|
|
61
|
+
The mutations target the `// katajs:module-service-imports`, `// katajs:module-provides`, and `// katajs:module-registry` anchors that ship in scaffolded modules.
|
|
62
|
+
|
|
63
|
+
### `katajs add route <method> <path> --in <module>`
|
|
64
|
+
|
|
65
|
+
Appends a new route handler to a module's `<module>.routes.ts` chain.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pnpm katajs add route post /comments --in posts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Methods: `get`, `post`, `put`, `patch`, `delete`, `options`, `head`. Path must start with `/`. The handler is inserted before the `// katajs:module-routes` anchor with a `// TODO: implement <METHOD> <path>` body.
|
|
72
|
+
|
|
73
|
+
### `katajs add queue <name> --in <module>`
|
|
74
|
+
|
|
75
|
+
Adds a queue consumer to an existing module — generates the consumer file, wires it into the module via anchors, and prints wrangler.jsonc + Bindings type snippets for manual paste.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpm katajs add queue orders --in orders
|
|
79
|
+
pnpm katajs add queue order-events --in posts --dlq ORDER_EVENTS_DLQ
|
|
80
|
+
pnpm katajs add queue analytics --in analytics --batch
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Flags:
|
|
84
|
+
|
|
85
|
+
- `--in <module>` *(required)* — target module
|
|
86
|
+
- `--binding <BINDING>` *(optional)* — wrangler binding name. Default: `<NAME>_QUEUE` (kebab → SCREAMING_SNAKE)
|
|
87
|
+
- `--dlq <DLQ_BINDING>` *(optional)* — dead-letter queue binding. When set, the generated consumer adds `dlq:` and `maxRetries: 5`
|
|
88
|
+
- `--batch` *(optional flag)* — generate `handleBatch` instead of `handle`
|
|
89
|
+
|
|
90
|
+
Generates `src/modules/<module>/<name>.consumer.ts` using `defineConsumer` from `@katajs/core` (drives full contextual typing on the handler — `message.body` is inferred from the schema, no `any`). Mutates `<module>/index.ts` to import the consumer and add it to `defineModule({ ..., consumer: <name>Consumer })` via the `// katajs:module-service-imports` and `// katajs:module-consumer` anchors.
|
|
91
|
+
|
|
92
|
+
Two manual paste steps the CLI doesn't auto-mutate (because both touch user-customizable territory):
|
|
93
|
+
|
|
94
|
+
1. **wrangler.jsonc** — add the producer, consumer, and (if `--dlq`) DLQ producer entries to the `queues` block.
|
|
95
|
+
2. **`Bindings` type** in `src/app.ts` — add `<BINDING>: Queue<XxxEvent>` so `c.env.<BINDING>.send(...)` typechecks.
|
|
96
|
+
|
|
97
|
+
Both snippets are printed by the command, ready to paste.
|
|
98
|
+
|
|
99
|
+
### Coming in later versions
|
|
100
|
+
|
|
101
|
+
- `katajs add migration <name>`
|
|
102
|
+
- `katajs add cron <name>`
|
|
103
|
+
- `katajs add do <name>`
|
|
104
|
+
- `katajs upgrade`
|
|
105
|
+
|
|
106
|
+
## How it works
|
|
107
|
+
|
|
108
|
+
The CLI walks up from your current working directory looking for a `package.json` that declares `@katajs/core`. If found, that's your project root and `src/` is where files land. Run `katajs add module foo` from anywhere inside your project — the CLI finds the right place.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
[MIT](./LICENSE) © Yaseer A. Okino
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import cac from "cac";
|
|
5
|
+
import * as p5 from "@clack/prompts";
|
|
6
|
+
import { red as red2 } from "kolorist";
|
|
7
|
+
|
|
8
|
+
// src/commands/add-module.ts
|
|
9
|
+
import {
|
|
10
|
+
existsSync as existsSync2,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync as readFileSync2,
|
|
13
|
+
readdirSync,
|
|
14
|
+
writeFileSync
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import * as p from "@clack/prompts";
|
|
19
|
+
import { cyan, dim, green, red, yellow } from "kolorist";
|
|
20
|
+
|
|
21
|
+
// src/codemod.ts
|
|
22
|
+
var AnchorMissingError = class extends Error {
|
|
23
|
+
constructor(anchor) {
|
|
24
|
+
super(`Anchor "// katajs:${anchor}" not found`);
|
|
25
|
+
this.anchor = anchor;
|
|
26
|
+
this.name = "AnchorMissingError";
|
|
27
|
+
}
|
|
28
|
+
anchor;
|
|
29
|
+
};
|
|
30
|
+
function insertBeforeAnchor(content, anchor, lines) {
|
|
31
|
+
const anchorMatch = new RegExp(`^(\\s*)// katajs:${escapeRe(anchor)}\\s*$`, "m");
|
|
32
|
+
const m = content.match(anchorMatch);
|
|
33
|
+
if (!m) throw new AnchorMissingError(anchor);
|
|
34
|
+
const indent = m[1] ?? "";
|
|
35
|
+
const arr = Array.isArray(lines) ? lines : [lines];
|
|
36
|
+
const toInsert = arr.map((l) => indent + l).join("\n");
|
|
37
|
+
const allPresent = arr.every((l) => content.includes(l.trim()));
|
|
38
|
+
if (allPresent) return content;
|
|
39
|
+
return content.replace(anchorMatch, `${toInsert}
|
|
40
|
+
${m[0]}`);
|
|
41
|
+
}
|
|
42
|
+
function escapeRe(s) {
|
|
43
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44
|
+
}
|
|
45
|
+
function appendRouteToChain(content, moduleVar) {
|
|
46
|
+
const newCallSig = `.route(${moduleVar}.prefix, ${moduleVar}.routes)`;
|
|
47
|
+
if (content.includes(newCallSig)) return content;
|
|
48
|
+
const lines = content.split("\n");
|
|
49
|
+
const anchorIdx = lines.findIndex(
|
|
50
|
+
(l) => /^\s*\/\/ katajs:routes\s*$/.test(l)
|
|
51
|
+
);
|
|
52
|
+
if (anchorIdx === -1) throw new AnchorMissingError("routes");
|
|
53
|
+
let lastChainIdx = -1;
|
|
54
|
+
const chainLineRe = /^\s*\.route\(\w+\.prefix,\s*\w+\.routes\)/;
|
|
55
|
+
for (let i = anchorIdx - 1; i >= Math.max(0, anchorIdx - 25); i--) {
|
|
56
|
+
if (chainLineRe.test(lines[i] ?? "")) {
|
|
57
|
+
lastChainIdx = i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (lastChainIdx === -1) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"appendRouteToChain: could not locate a `.route(<X>.prefix, <X>.routes)` line above // katajs:routes"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const lastLine = lines[lastChainIdx];
|
|
67
|
+
const indent = lastLine.match(/^(\s*)/)?.[1] ?? " ";
|
|
68
|
+
lines[lastChainIdx] = lastLine.replace(/,(\s*)$/, "$1");
|
|
69
|
+
lines.splice(
|
|
70
|
+
lastChainIdx + 1,
|
|
71
|
+
0,
|
|
72
|
+
`${indent}.route(${moduleVar}.prefix, ${moduleVar}.routes),`
|
|
73
|
+
);
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/utils/project.ts
|
|
78
|
+
import { existsSync, readFileSync } from "fs";
|
|
79
|
+
import { dirname, join, resolve } from "path";
|
|
80
|
+
function findKatajsProject(cwd = process.cwd()) {
|
|
81
|
+
let dir = resolve(cwd);
|
|
82
|
+
while (true) {
|
|
83
|
+
const pkgPath = join(dir, "package.json");
|
|
84
|
+
if (existsSync(pkgPath)) {
|
|
85
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
86
|
+
if (declaresKatajs(pkg)) {
|
|
87
|
+
const srcDir = join(dir, "src");
|
|
88
|
+
if (!existsSync(srcDir)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Found @katajs/core in ${pkgPath}, but no src/ directory. Run from a katajs project root.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return { root: dir, srcDir, pkg };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const parent = dirname(dir);
|
|
97
|
+
if (parent === dir) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Not in a katajs project \u2014 couldn't find a package.json with @katajs/core in this directory or any parent.
|
|
100
|
+
Run this command from inside a project created with \`pnpm create katajs\`.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
dir = parent;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function declaresKatajs(pkg) {
|
|
107
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
108
|
+
const deps = pkg[key];
|
|
109
|
+
if (deps && typeof deps === "object" && "@katajs/core" in deps) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/utils/text.ts
|
|
117
|
+
function normalizeName(input) {
|
|
118
|
+
const cleaned = input.trim().replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "").toLowerCase();
|
|
119
|
+
if (!cleaned) {
|
|
120
|
+
throw new Error(`Invalid name: "${input}". Use lowercase letters, digits, and hyphens.`);
|
|
121
|
+
}
|
|
122
|
+
if (!/^[a-z]/.test(cleaned)) {
|
|
123
|
+
throw new Error(`Name must start with a letter: "${input}".`);
|
|
124
|
+
}
|
|
125
|
+
const parts = cleaned.split(/[-_]+/).filter(Boolean);
|
|
126
|
+
const camel = parts[0] + parts.slice(1).map(capitalize).join("");
|
|
127
|
+
const pascal = parts.map(capitalize).join("");
|
|
128
|
+
return { kebab: parts.join("-"), pascal, camel };
|
|
129
|
+
}
|
|
130
|
+
function capitalize(s) {
|
|
131
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/commands/add-module.ts
|
|
135
|
+
async function addModule(opts) {
|
|
136
|
+
const project = findKatajsProject(opts.cwd);
|
|
137
|
+
const casings = normalizeName(opts.name);
|
|
138
|
+
const moduleDir = join2(project.srcDir, "modules", casings.kebab);
|
|
139
|
+
if (existsSync2(moduleDir)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Module directory already exists: ${moduleDir}
|
|
142
|
+
Refusing to overwrite. Delete the directory first or pick a different name.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
generateModuleFiles(moduleDir, casings);
|
|
146
|
+
const fallbacks = [];
|
|
147
|
+
applyMutation({
|
|
148
|
+
file: join2(project.srcDir, "types.d.ts"),
|
|
149
|
+
fn: (content) => {
|
|
150
|
+
const c1 = insertBeforeAnchor(
|
|
151
|
+
content,
|
|
152
|
+
"registry-imports",
|
|
153
|
+
`import type { ${casings.pascal}Registry } from './modules/${casings.kebab}/index';`
|
|
154
|
+
);
|
|
155
|
+
return insertBeforeAnchor(c1, "registry", `, ${casings.pascal}Registry`);
|
|
156
|
+
},
|
|
157
|
+
onFallback: (msg) => fallbacks.push(msg),
|
|
158
|
+
snippet: () => [
|
|
159
|
+
`// In ${join2(project.srcDir, "types.d.ts")}:`,
|
|
160
|
+
`import type { ${casings.pascal}Registry } from './modules/${casings.kebab}/index';`,
|
|
161
|
+
`// Add to the Registry interface extends list:`,
|
|
162
|
+
`, ${casings.pascal}Registry`
|
|
163
|
+
].join("\n")
|
|
164
|
+
});
|
|
165
|
+
applyMutation({
|
|
166
|
+
file: join2(project.srcDir, "app.ts"),
|
|
167
|
+
fn: (content) => {
|
|
168
|
+
const c1 = insertBeforeAnchor(
|
|
169
|
+
content,
|
|
170
|
+
"module-imports",
|
|
171
|
+
`import { ${casings.camel}Module } from './modules/${casings.kebab}/index';`
|
|
172
|
+
);
|
|
173
|
+
const c2 = insertBeforeAnchor(c1, "modules", `${casings.camel}Module,`);
|
|
174
|
+
return appendRouteToChain(c2, `${casings.camel}Module`);
|
|
175
|
+
},
|
|
176
|
+
onFallback: (msg) => fallbacks.push(msg),
|
|
177
|
+
snippet: () => [
|
|
178
|
+
`// In ${join2(project.srcDir, "app.ts")}:`,
|
|
179
|
+
`import { ${casings.camel}Module } from './modules/${casings.kebab}/index';`,
|
|
180
|
+
`// Add to the modules array:`,
|
|
181
|
+
`${casings.camel}Module,`,
|
|
182
|
+
`// And in the routes callback chain:`,
|
|
183
|
+
`.route(${casings.camel}Module.prefix, ${casings.camel}Module.routes)`
|
|
184
|
+
].join("\n")
|
|
185
|
+
});
|
|
186
|
+
const modulesScript = join2(project.root, "scripts", "modules.ts");
|
|
187
|
+
const graphScript = join2(project.root, "scripts", "graph.ts");
|
|
188
|
+
const registryFile = existsSync2(modulesScript) ? modulesScript : existsSync2(graphScript) ? graphScript : null;
|
|
189
|
+
if (registryFile) {
|
|
190
|
+
applyMutation({
|
|
191
|
+
file: registryFile,
|
|
192
|
+
fn: (content) => {
|
|
193
|
+
const c1 = insertBeforeAnchor(
|
|
194
|
+
content,
|
|
195
|
+
"graph-imports",
|
|
196
|
+
`import { ${casings.camel}Module } from '../src/modules/${casings.kebab}/index';`
|
|
197
|
+
);
|
|
198
|
+
return insertBeforeAnchor(c1, "graph-modules", `${casings.camel}Module,`);
|
|
199
|
+
},
|
|
200
|
+
onFallback: (msg) => fallbacks.push(msg),
|
|
201
|
+
snippet: () => [
|
|
202
|
+
`// In ${registryFile}:`,
|
|
203
|
+
`import { ${casings.camel}Module } from '../src/modules/${casings.kebab}/index';`,
|
|
204
|
+
`// Add to the modules array:`,
|
|
205
|
+
`${casings.camel}Module,`
|
|
206
|
+
].join("\n")
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
p.outro(`${green("\u2713")} Created module ${cyan(casings.kebab)}`);
|
|
210
|
+
console.log(dim(" Files:"));
|
|
211
|
+
for (const f of [
|
|
212
|
+
"index.ts",
|
|
213
|
+
`${casings.kebab}.service.ts`,
|
|
214
|
+
`${casings.kebab}.routes.ts`,
|
|
215
|
+
`${casings.kebab}.schema.ts`,
|
|
216
|
+
`${casings.kebab}.errors.ts`
|
|
217
|
+
]) {
|
|
218
|
+
console.log(dim(` src/modules/${casings.kebab}/${f}`));
|
|
219
|
+
}
|
|
220
|
+
if (fallbacks.length > 0) {
|
|
221
|
+
console.log("\n" + yellow(" Some files couldn\u2019t be auto-edited."));
|
|
222
|
+
console.log(yellow(" Paste these snippets manually:\n"));
|
|
223
|
+
for (const f of fallbacks) {
|
|
224
|
+
console.log(f + "\n");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function generateModuleFiles(moduleDir, casings) {
|
|
229
|
+
mkdirSync(moduleDir, { recursive: true });
|
|
230
|
+
const templatesDir = getTemplatesDir();
|
|
231
|
+
const files = [
|
|
232
|
+
["index.ts", "index.ts"],
|
|
233
|
+
["service.ts", `${casings.kebab}.service.ts`],
|
|
234
|
+
["routes.ts", `${casings.kebab}.routes.ts`],
|
|
235
|
+
["schema.ts", `${casings.kebab}.schema.ts`],
|
|
236
|
+
["errors.ts", `${casings.kebab}.errors.ts`]
|
|
237
|
+
];
|
|
238
|
+
for (const [srcName, dstName] of files) {
|
|
239
|
+
const srcPath = join2(templatesDir, "module", srcName);
|
|
240
|
+
if (!existsSync2(srcPath)) {
|
|
241
|
+
throw new Error(`Template file missing: ${srcPath}`);
|
|
242
|
+
}
|
|
243
|
+
const content = readFileSync2(srcPath, "utf8");
|
|
244
|
+
const replaced = applyCasings(content, casings);
|
|
245
|
+
writeFileSync(join2(moduleDir, dstName), replaced);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function applyCasings(content, casings) {
|
|
249
|
+
return content.replace(/\{\{kebab\}\}/g, casings.kebab).replace(/\{\{Pascal\}\}/g, casings.pascal).replace(/\{\{camel\}\}/g, casings.camel).replace(/\{\{snake\}\}/g, casings.kebab.replace(/-/g, "_"));
|
|
250
|
+
}
|
|
251
|
+
function applyMutation(step) {
|
|
252
|
+
if (!existsSync2(step.file)) {
|
|
253
|
+
step.onFallback(red(`Skipped ${step.file} (not found)
|
|
254
|
+
${step.snippet()}`));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const before = readFileSync2(step.file, "utf8");
|
|
258
|
+
try {
|
|
259
|
+
const after = step.fn(before);
|
|
260
|
+
if (after !== before) {
|
|
261
|
+
writeFileSync(step.file, after);
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (err instanceof AnchorMissingError) {
|
|
265
|
+
step.onFallback(
|
|
266
|
+
yellow(`Anchor "${err.anchor}" missing in ${step.file}.
|
|
267
|
+
${step.snippet()}`)
|
|
268
|
+
);
|
|
269
|
+
} else {
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function getTemplatesDir() {
|
|
275
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
276
|
+
const candidates = [
|
|
277
|
+
join2(here, "templates"),
|
|
278
|
+
join2(here, "..", "src", "templates"),
|
|
279
|
+
join2(here, "..", "..", "src", "templates")
|
|
280
|
+
];
|
|
281
|
+
for (const c of candidates) {
|
|
282
|
+
if (existsSync2(c)) {
|
|
283
|
+
try {
|
|
284
|
+
const sub = readdirSync(c);
|
|
285
|
+
if (sub.includes("module")) return c;
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Could not locate templates directory. Tried: ${candidates.join(", ")}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/commands/add-service.ts
|
|
296
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
297
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
298
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
299
|
+
import * as p2 from "@clack/prompts";
|
|
300
|
+
import { cyan as cyan2, dim as dim2, green as green2, yellow as yellow2 } from "kolorist";
|
|
301
|
+
async function addService(opts) {
|
|
302
|
+
const project = findKatajsProject(opts.cwd);
|
|
303
|
+
const moduleCasings = normalizeName(opts.inModule);
|
|
304
|
+
const serviceCasings = normalizeName(opts.name);
|
|
305
|
+
const moduleDir = join3(project.srcDir, "modules", moduleCasings.kebab);
|
|
306
|
+
if (!existsSync3(moduleDir)) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Module "${moduleCasings.kebab}" not found at ${moduleDir}. Run \`katajs add module ${moduleCasings.kebab}\` first.`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const servicePath = join3(moduleDir, `${serviceCasings.kebab}.service.ts`);
|
|
312
|
+
if (existsSync3(servicePath)) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Service file already exists: ${servicePath}
|
|
315
|
+
Refusing to overwrite.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
writeServiceFile(servicePath, serviceCasings);
|
|
319
|
+
const indexPath = join3(moduleDir, "index.ts");
|
|
320
|
+
const fallbacks = [];
|
|
321
|
+
if (!existsSync3(indexPath)) {
|
|
322
|
+
fallbacks.push(
|
|
323
|
+
yellow2(`Module index not found: ${indexPath}
|
|
324
|
+
Wire up the new service manually.`)
|
|
325
|
+
);
|
|
326
|
+
} else {
|
|
327
|
+
let content = readFileSync3(indexPath, "utf8");
|
|
328
|
+
let snippet = "";
|
|
329
|
+
try {
|
|
330
|
+
content = insertBeforeAnchor(
|
|
331
|
+
content,
|
|
332
|
+
"module-service-imports",
|
|
333
|
+
`import { make${serviceCasings.pascal}Service, type ${serviceCasings.pascal}Service } from './${serviceCasings.kebab}.service';`
|
|
334
|
+
);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
if (err instanceof AnchorMissingError) {
|
|
337
|
+
snippet += `import { make${serviceCasings.pascal}Service, type ${serviceCasings.pascal}Service } from './${serviceCasings.kebab}.service';
|
|
338
|
+
`;
|
|
339
|
+
} else throw err;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
content = insertBeforeAnchor(
|
|
343
|
+
content,
|
|
344
|
+
"module-provides",
|
|
345
|
+
`${serviceCasings.camel}Service: (c): ${serviceCasings.pascal}Service => make${serviceCasings.pascal}Service(c),`
|
|
346
|
+
);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
if (err instanceof AnchorMissingError) {
|
|
349
|
+
snippet += `// Add to provides:
|
|
350
|
+
${serviceCasings.camel}Service: (c): ${serviceCasings.pascal}Service => make${serviceCasings.pascal}Service(c),
|
|
351
|
+
`;
|
|
352
|
+
} else throw err;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
content = insertBeforeAnchor(
|
|
356
|
+
content,
|
|
357
|
+
"module-registry",
|
|
358
|
+
`${serviceCasings.camel}Service: ${serviceCasings.pascal}Service;`
|
|
359
|
+
);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
if (err instanceof AnchorMissingError) {
|
|
362
|
+
snippet += `// Add to ${moduleCasings.pascal}Registry:
|
|
363
|
+
${serviceCasings.camel}Service: ${serviceCasings.pascal}Service;
|
|
364
|
+
`;
|
|
365
|
+
} else throw err;
|
|
366
|
+
}
|
|
367
|
+
writeFileSync2(indexPath, content);
|
|
368
|
+
if (snippet) {
|
|
369
|
+
fallbacks.push(
|
|
370
|
+
yellow2(`Some anchors missing in ${indexPath}.
|
|
371
|
+
Paste manually:
|
|
372
|
+
`) + snippet
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
p2.outro(
|
|
377
|
+
`${green2("\u2713")} Created service ${cyan2(serviceCasings.camel + "Service")} in module ${cyan2(moduleCasings.kebab)}`
|
|
378
|
+
);
|
|
379
|
+
console.log(dim2(" File:"));
|
|
380
|
+
console.log(
|
|
381
|
+
dim2(` src/modules/${moduleCasings.kebab}/${serviceCasings.kebab}.service.ts`)
|
|
382
|
+
);
|
|
383
|
+
if (fallbacks.length > 0) {
|
|
384
|
+
console.log("\n" + yellow2(" Some snippets need manual paste:\n"));
|
|
385
|
+
for (const f of fallbacks) {
|
|
386
|
+
console.log(f + "\n");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function writeServiceFile(dst, casings) {
|
|
391
|
+
const tmpl = `import type { RequestContainer } from '@katajs/core';
|
|
392
|
+
|
|
393
|
+
export type ${casings.pascal}Service = {
|
|
394
|
+
ping(): Promise<{ ok: true; service: string }>;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
export function make${casings.pascal}Service(_c: RequestContainer): ${casings.pascal}Service {
|
|
398
|
+
return {
|
|
399
|
+
async ping() {
|
|
400
|
+
return { ok: true as const, service: '${casings.kebab}' };
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
405
|
+
writeFileSync2(dst, tmpl);
|
|
406
|
+
}
|
|
407
|
+
var __dirname = dirname3(fileURLToPath2(import.meta.url));
|
|
408
|
+
|
|
409
|
+
// src/commands/add-route.ts
|
|
410
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
411
|
+
import { join as join4 } from "path";
|
|
412
|
+
import * as p3 from "@clack/prompts";
|
|
413
|
+
import { cyan as cyan3, dim as dim3, green as green3, yellow as yellow3 } from "kolorist";
|
|
414
|
+
var VALID_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head"]);
|
|
415
|
+
async function addRoute(opts) {
|
|
416
|
+
const project = findKatajsProject(opts.cwd);
|
|
417
|
+
const method = opts.method.toLowerCase();
|
|
418
|
+
if (!VALID_METHODS.has(method)) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Invalid HTTP method: "${opts.method}". Supported: ${[...VALID_METHODS].join(", ")}.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (!opts.path.startsWith("/")) {
|
|
424
|
+
throw new Error(`Path must start with "/": got "${opts.path}".`);
|
|
425
|
+
}
|
|
426
|
+
const moduleCasings = normalizeName(opts.inModule);
|
|
427
|
+
const moduleDir = join4(project.srcDir, "modules", moduleCasings.kebab);
|
|
428
|
+
const routesPath = join4(moduleDir, `${moduleCasings.kebab}.routes.ts`);
|
|
429
|
+
if (!existsSync4(routesPath)) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
`Routes file not found: ${routesPath}
|
|
432
|
+
Is "${moduleCasings.kebab}" a routed module? Service-only modules don't have routes.`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const content = readFileSync4(routesPath, "utf8");
|
|
436
|
+
const snippet = buildRouteSnippet(method, opts.path);
|
|
437
|
+
let updated;
|
|
438
|
+
try {
|
|
439
|
+
updated = insertBeforeAnchor(content, "module-routes", snippet.split("\n"));
|
|
440
|
+
} catch (err) {
|
|
441
|
+
if (err instanceof AnchorMissingError) {
|
|
442
|
+
p3.outro(
|
|
443
|
+
yellow3(
|
|
444
|
+
`Anchor "// katajs:module-routes" missing in ${routesPath}.
|
|
445
|
+
Paste this snippet into the chain manually:
|
|
446
|
+
|
|
447
|
+
${snippet}`
|
|
448
|
+
)
|
|
449
|
+
);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
throw err;
|
|
453
|
+
}
|
|
454
|
+
if (updated === content) {
|
|
455
|
+
p3.outro(yellow3(`No change \u2014 ${method.toUpperCase()} ${opts.path} appears to already exist.`));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
writeFileSync3(routesPath, updated);
|
|
459
|
+
p3.outro(
|
|
460
|
+
`${green3("\u2713")} Added ${cyan3(method.toUpperCase() + " " + opts.path)} to module ${cyan3(moduleCasings.kebab)}`
|
|
461
|
+
);
|
|
462
|
+
console.log(dim3(` Edited src/modules/${moduleCasings.kebab}/${moduleCasings.kebab}.routes.ts`));
|
|
463
|
+
}
|
|
464
|
+
function buildRouteSnippet(method, path) {
|
|
465
|
+
return [
|
|
466
|
+
`.${method}('${path}', async (c) => {`,
|
|
467
|
+
` // TODO: implement ${method.toUpperCase()} ${path}`,
|
|
468
|
+
` return c.json({});`,
|
|
469
|
+
`})`
|
|
470
|
+
].join("\n");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/commands/add-queue.ts
|
|
474
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
475
|
+
import { join as join5 } from "path";
|
|
476
|
+
import * as p4 from "@clack/prompts";
|
|
477
|
+
import { cyan as cyan4, dim as dim4, green as green4, yellow as yellow4 } from "kolorist";
|
|
478
|
+
async function addQueue(opts) {
|
|
479
|
+
const project = findKatajsProject(opts.cwd);
|
|
480
|
+
const moduleCasings = normalizeName(opts.inModule);
|
|
481
|
+
const queueCasings = normalizeName(opts.name);
|
|
482
|
+
const bindingName = opts.binding ?? deriveBindingName(queueCasings.kebab);
|
|
483
|
+
const dlqBinding = opts.dlq;
|
|
484
|
+
const moduleDir = join5(project.srcDir, "modules", moduleCasings.kebab);
|
|
485
|
+
if (!existsSync5(moduleDir)) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`Module "${moduleCasings.kebab}" not found at ${moduleDir}. Run \`katajs add module ${moduleCasings.kebab}\` first.`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
const consumerPath = join5(moduleDir, `${queueCasings.kebab}.consumer.ts`);
|
|
491
|
+
if (existsSync5(consumerPath)) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`Consumer file already exists: ${consumerPath}
|
|
494
|
+
Refusing to overwrite.`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
writeConsumerFile({
|
|
498
|
+
dst: consumerPath,
|
|
499
|
+
queue: queueCasings,
|
|
500
|
+
binding: bindingName,
|
|
501
|
+
dlq: dlqBinding,
|
|
502
|
+
batch: opts.batch ?? false
|
|
503
|
+
});
|
|
504
|
+
const indexPath = join5(moduleDir, "index.ts");
|
|
505
|
+
const fallbacks = [];
|
|
506
|
+
if (!existsSync5(indexPath)) {
|
|
507
|
+
fallbacks.push(
|
|
508
|
+
yellow4(`Module index not found: ${indexPath}
|
|
509
|
+
Wire up the consumer manually.`)
|
|
510
|
+
);
|
|
511
|
+
} else {
|
|
512
|
+
let content = readFileSync5(indexPath, "utf8");
|
|
513
|
+
let snippet = "";
|
|
514
|
+
try {
|
|
515
|
+
content = insertBeforeAnchor(
|
|
516
|
+
content,
|
|
517
|
+
"module-service-imports",
|
|
518
|
+
`import { ${queueCasings.camel}Consumer } from './${queueCasings.kebab}.consumer';`
|
|
519
|
+
);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
if (err instanceof AnchorMissingError) {
|
|
522
|
+
snippet += `import { ${queueCasings.camel}Consumer } from './${queueCasings.kebab}.consumer';
|
|
523
|
+
`;
|
|
524
|
+
} else throw err;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
content = insertBeforeAnchor(
|
|
528
|
+
content,
|
|
529
|
+
"module-consumer",
|
|
530
|
+
`consumer: ${queueCasings.camel}Consumer,`
|
|
531
|
+
);
|
|
532
|
+
} catch (err) {
|
|
533
|
+
if (err instanceof AnchorMissingError) {
|
|
534
|
+
snippet += `// Add to defineModule call:
|
|
535
|
+
consumer: ${queueCasings.camel}Consumer,
|
|
536
|
+
`;
|
|
537
|
+
} else throw err;
|
|
538
|
+
}
|
|
539
|
+
writeFileSync4(indexPath, content);
|
|
540
|
+
if (snippet) {
|
|
541
|
+
fallbacks.push(
|
|
542
|
+
yellow4(`Some anchors missing in ${indexPath}.
|
|
543
|
+
Paste manually:
|
|
544
|
+
`) + snippet
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (!opts.noProducer) {
|
|
549
|
+
const appPath = join5(project.srcDir, "app.ts");
|
|
550
|
+
if (existsSync5(appPath)) {
|
|
551
|
+
let appSrc = readFileSync5(appPath, "utf8");
|
|
552
|
+
const importLine = `import { ${queueCasings.pascal}EventSchema } from './modules/${moduleCasings.kebab}/${queueCasings.kebab}.consumer';`;
|
|
553
|
+
const queuesEntry = [
|
|
554
|
+
`${queueCasings.camel}: {`,
|
|
555
|
+
` binding: '${bindingName}',`,
|
|
556
|
+
` schema: ${queueCasings.pascal}EventSchema,`,
|
|
557
|
+
`},`
|
|
558
|
+
];
|
|
559
|
+
let appFallback = "";
|
|
560
|
+
if (!appSrc.includes(importLine)) {
|
|
561
|
+
try {
|
|
562
|
+
appSrc = insertBeforeAnchor(appSrc, "module-imports", importLine);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
if (err instanceof AnchorMissingError) {
|
|
565
|
+
appFallback += importLine + "\n";
|
|
566
|
+
} else throw err;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
appSrc = insertBeforeAnchor(appSrc, "queues", queuesEntry);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
if (err instanceof AnchorMissingError) {
|
|
573
|
+
appFallback += `// Add to createApp's queues:
|
|
574
|
+
${queueCasings.camel}: { binding: '${bindingName}', schema: ${queueCasings.pascal}EventSchema },
|
|
575
|
+
`;
|
|
576
|
+
} else throw err;
|
|
577
|
+
}
|
|
578
|
+
writeFileSync4(appPath, appSrc);
|
|
579
|
+
if (appFallback) {
|
|
580
|
+
fallbacks.push(
|
|
581
|
+
yellow4(`Some anchors missing in ${appPath}.
|
|
582
|
+
Paste manually:
|
|
583
|
+
`) + appFallback
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const typesPath = join5(project.srcDir, "types.d.ts");
|
|
588
|
+
if (existsSync5(typesPath)) {
|
|
589
|
+
let typesSrc = readFileSync5(typesPath, "utf8");
|
|
590
|
+
let typesFallback = "";
|
|
591
|
+
const typeImportLine = `import type { ${queueCasings.pascal}Event } from './modules/${moduleCasings.kebab}/${queueCasings.kebab}.consumer';`;
|
|
592
|
+
const typedQueueImport = `import type { TypedQueue } from '@katajs/core';`;
|
|
593
|
+
const registryEntry = `${queueCasings.camel}: TypedQueue<${queueCasings.pascal}Event>;`;
|
|
594
|
+
if (!typesSrc.includes(typedQueueImport)) {
|
|
595
|
+
typesSrc = typesSrc.replace(
|
|
596
|
+
/^import type \{ DrizzleClient \} from '@katajs\/drizzle';/m,
|
|
597
|
+
`${typedQueueImport}
|
|
598
|
+
$&`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
if (!typesSrc.includes(typeImportLine)) {
|
|
602
|
+
try {
|
|
603
|
+
typesSrc = insertBeforeAnchor(typesSrc, "registry-imports", typeImportLine);
|
|
604
|
+
} catch (err) {
|
|
605
|
+
if (err instanceof AnchorMissingError) {
|
|
606
|
+
typesFallback += typeImportLine + "\n";
|
|
607
|
+
} else throw err;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
typesSrc = insertBeforeAnchor(typesSrc, "queues-registry", registryEntry);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
if (err instanceof AnchorMissingError) {
|
|
614
|
+
typesFallback += `// Add to QueuesRegistry interface:
|
|
615
|
+
${registryEntry}
|
|
616
|
+
`;
|
|
617
|
+
} else throw err;
|
|
618
|
+
}
|
|
619
|
+
writeFileSync4(typesPath, typesSrc);
|
|
620
|
+
if (typesFallback) {
|
|
621
|
+
fallbacks.push(
|
|
622
|
+
yellow4(`Some anchors missing in ${typesPath}.
|
|
623
|
+
Paste manually:
|
|
624
|
+
`) + typesFallback
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
p4.outro(
|
|
630
|
+
`${green4("\u2713")} Added queue consumer ${cyan4(queueCasings.camel + "Consumer")} to module ${cyan4(moduleCasings.kebab)}` + (opts.noProducer ? "" : `
|
|
631
|
+
Wired ${cyan4(`c.var.queues.${queueCasings.camel}.send(...)`)} producer`)
|
|
632
|
+
);
|
|
633
|
+
console.log(dim4(" File:"));
|
|
634
|
+
console.log(
|
|
635
|
+
dim4(` src/modules/${moduleCasings.kebab}/${queueCasings.kebab}.consumer.ts`)
|
|
636
|
+
);
|
|
637
|
+
if (fallbacks.length > 0) {
|
|
638
|
+
console.log("\n" + yellow4(" Some snippets need manual paste:\n"));
|
|
639
|
+
for (const f of fallbacks) {
|
|
640
|
+
console.log(f + "\n");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
console.log("\n" + cyan4(" Next: add the wrangler bindings"));
|
|
644
|
+
console.log(
|
|
645
|
+
dim4(" In wrangler.jsonc, add (or extend) the queues section:\n")
|
|
646
|
+
);
|
|
647
|
+
console.log(buildWranglerSnippet(queueCasings.kebab, bindingName, dlqBinding));
|
|
648
|
+
if (!opts.noProducer) {
|
|
649
|
+
console.log(
|
|
650
|
+
"\n" + dim4(
|
|
651
|
+
` Producer wired automatically \u2014 call ${cyan4(`c.var.queues.${queueCasings.camel}.send(...)`)} from any service or route.`
|
|
652
|
+
)
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function deriveBindingName(kebab) {
|
|
657
|
+
return kebab.replace(/-/g, "_").toUpperCase() + "_QUEUE";
|
|
658
|
+
}
|
|
659
|
+
function writeConsumerFile(args) {
|
|
660
|
+
const { queue, binding, dlq, batch } = args;
|
|
661
|
+
const optionalDlq = dlq ? `
|
|
662
|
+
dlq: '${dlq}',
|
|
663
|
+
maxRetries: 5,` : "";
|
|
664
|
+
const handlerBlock = batch ? ` async handleBatch(batch, c) {
|
|
665
|
+
// TODO: implement batch processing.
|
|
666
|
+
// The framework has already validated each message body against ${queue.pascal}EventSchema.
|
|
667
|
+
for (const message of batch.messages) {
|
|
668
|
+
const event = message.body;
|
|
669
|
+
void event;
|
|
670
|
+
void c;
|
|
671
|
+
message.ack();
|
|
672
|
+
}
|
|
673
|
+
},` : ` async handle(message, c) {
|
|
674
|
+
// TODO: implement ${queue.kebab} message processing.
|
|
675
|
+
const event = message.body;
|
|
676
|
+
void event;
|
|
677
|
+
void c;
|
|
678
|
+
},`;
|
|
679
|
+
const tmpl = `import { z } from 'zod';
|
|
680
|
+
import { defineConsumer } from '@katajs/core';
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Message body schema for the ${queue.kebab} queue. Update this to match
|
|
684
|
+
* the actual messages your producers send.
|
|
685
|
+
*/
|
|
686
|
+
export const ${queue.pascal}EventSchema = z.object({
|
|
687
|
+
// TODO: define your message shape
|
|
688
|
+
id: z.string().uuid(),
|
|
689
|
+
});
|
|
690
|
+
export type ${queue.pascal}Event = z.infer<typeof ${queue.pascal}EventSchema>;
|
|
691
|
+
|
|
692
|
+
export const ${queue.camel}Consumer = defineConsumer({
|
|
693
|
+
queue: '${binding}',
|
|
694
|
+
schema: ${queue.pascal}EventSchema,${optionalDlq}
|
|
695
|
+
${handlerBlock}
|
|
696
|
+
});
|
|
697
|
+
`;
|
|
698
|
+
writeFileSync4(args.dst, tmpl);
|
|
699
|
+
}
|
|
700
|
+
function buildWranglerSnippet(queueName, binding, dlq) {
|
|
701
|
+
const producers = [` { "binding": "${binding}", "queue": "${queueName}" }`];
|
|
702
|
+
const dlqProducer = dlq ? ` ,
|
|
703
|
+
{ "binding": "${dlq}", "queue": "${queueName}-dlq" }` : "";
|
|
704
|
+
const consumerEntry = dlq ? ` {
|
|
705
|
+
"queue": "${queueName}",
|
|
706
|
+
"max_batch_size": 100,
|
|
707
|
+
"max_batch_timeout": 30,
|
|
708
|
+
"max_retries": 5,
|
|
709
|
+
"dead_letter_queue": "${queueName}-dlq"
|
|
710
|
+
}` : ` {
|
|
711
|
+
"queue": "${queueName}",
|
|
712
|
+
"max_batch_size": 100,
|
|
713
|
+
"max_batch_timeout": 30,
|
|
714
|
+
"max_retries": 3
|
|
715
|
+
}`;
|
|
716
|
+
return ` "queues": {
|
|
717
|
+
"producers": [
|
|
718
|
+
${producers.join(",\n")}${dlqProducer}
|
|
719
|
+
],
|
|
720
|
+
"consumers": [
|
|
721
|
+
${consumerEntry}
|
|
722
|
+
]
|
|
723
|
+
}`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/index.ts
|
|
727
|
+
var cli = cac("katajs");
|
|
728
|
+
cli.command("add <kind> <name> [path]", "Add something to your katajs project").option("--in <module>", "Target module (for `add service`, `add route`, `add queue`)").option("--binding <BINDING>", "wrangler binding name (for `add queue`; defaults to <NAME>_QUEUE)").option("--dlq <DLQ_BINDING>", "wrangler binding name for the dead-letter queue (for `add queue`)").option("--batch", "Generate `handleBatch` instead of `handle` (for `add queue`)").option("--no-producer", "Skip the producer manifest entry (for consumer-only Workers, e.g. apps/worker)").example(" katajs add module comments").example(" katajs add service featured --in posts").example(" katajs add route post /comments --in posts").example(" katajs add queue orders --in orders").example(" katajs add queue orders --in orders --dlq ORDERS_DLQ --batch").example(" katajs add queue orders --in orders --no-producer # consumer-only (apps/worker)").action(
|
|
729
|
+
async (kind, name, path, opts) => {
|
|
730
|
+
try {
|
|
731
|
+
switch (kind) {
|
|
732
|
+
case "module":
|
|
733
|
+
case "m":
|
|
734
|
+
p5.intro("katajs add module");
|
|
735
|
+
await addModule({ name });
|
|
736
|
+
return;
|
|
737
|
+
case "service":
|
|
738
|
+
case "s":
|
|
739
|
+
if (!opts.in)
|
|
740
|
+
throw new Error("Missing --in <module>. Example: katajs add service featured --in posts");
|
|
741
|
+
p5.intro("katajs add service");
|
|
742
|
+
await addService({ name, inModule: opts.in });
|
|
743
|
+
return;
|
|
744
|
+
case "route":
|
|
745
|
+
case "r": {
|
|
746
|
+
if (!path)
|
|
747
|
+
throw new Error("Missing path. Example: katajs add route post /comments --in posts");
|
|
748
|
+
if (!opts.in)
|
|
749
|
+
throw new Error("Missing --in <module>. Example: katajs add route post /comments --in posts");
|
|
750
|
+
p5.intro("katajs add route");
|
|
751
|
+
await addRoute({ method: name, path, inModule: opts.in });
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
case "queue":
|
|
755
|
+
case "q":
|
|
756
|
+
if (!opts.in)
|
|
757
|
+
throw new Error("Missing --in <module>. Example: katajs add queue orders --in orders");
|
|
758
|
+
p5.intro("katajs add queue");
|
|
759
|
+
await addQueue({
|
|
760
|
+
name,
|
|
761
|
+
inModule: opts.in,
|
|
762
|
+
binding: opts.binding,
|
|
763
|
+
dlq: opts.dlq,
|
|
764
|
+
batch: opts.batch ?? false,
|
|
765
|
+
// cac maps `--no-producer` to producer=false
|
|
766
|
+
noProducer: opts.producer === false
|
|
767
|
+
});
|
|
768
|
+
return;
|
|
769
|
+
default:
|
|
770
|
+
console.error(red2(`Unknown target: '${kind}'.`));
|
|
771
|
+
console.error("Supported: module, service, route, queue");
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
} catch (err) {
|
|
775
|
+
p5.cancel(red2(err.message));
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
);
|
|
780
|
+
cli.help();
|
|
781
|
+
cli.version("0.1.0");
|
|
782
|
+
cli.parse();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AppError } from '@katajs/core';
|
|
2
|
+
|
|
3
|
+
export class {{Pascal}}NotFoundError extends AppError {
|
|
4
|
+
override readonly status = 404;
|
|
5
|
+
override readonly code = '{{snake}}_not_found';
|
|
6
|
+
override readonly publicMessage = '{{Pascal}} not found';
|
|
7
|
+
|
|
8
|
+
constructor(public readonly id: string) {
|
|
9
|
+
super(`{{Pascal}} ${id} not found`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override get publicPayload() {
|
|
13
|
+
return { id: this.id };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineModule } from '@katajs/core';
|
|
2
|
+
import { make{{Pascal}}Service, type {{Pascal}}Service } from './{{kebab}}.service';
|
|
3
|
+
// katajs:module-service-imports
|
|
4
|
+
import { {{camel}}Routes } from './{{kebab}}.routes';
|
|
5
|
+
|
|
6
|
+
export const {{camel}}Module = defineModule({
|
|
7
|
+
name: '{{kebab}}',
|
|
8
|
+
provides: {
|
|
9
|
+
{{camel}}Service: (c): {{Pascal}}Service => make{{Pascal}}Service(c),
|
|
10
|
+
// katajs:module-provides
|
|
11
|
+
},
|
|
12
|
+
requires: [] as const,
|
|
13
|
+
routes: {{camel}}Routes,
|
|
14
|
+
prefix: '/{{kebab}}',
|
|
15
|
+
// katajs:module-consumer
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Services this module contributes to the container's `Registry`. */
|
|
19
|
+
export type {{Pascal}}Registry = {
|
|
20
|
+
{{camel}}Service: {{Pascal}}Service;
|
|
21
|
+
// katajs:module-registry
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type { {{Pascal}}Service };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { AppEnv } from '../../app';
|
|
3
|
+
|
|
4
|
+
export const {{camel}}Routes = new Hono<AppEnv>()
|
|
5
|
+
.get('/', async (c) => {
|
|
6
|
+
const service = c.var.resolve('{{camel}}Service');
|
|
7
|
+
const result = await service.ping();
|
|
8
|
+
return c.json(result);
|
|
9
|
+
})
|
|
10
|
+
// katajs:module-routes
|
|
11
|
+
;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// Add Zod schemas for this module's request bodies, query params, and route
|
|
4
|
+
// params. Example:
|
|
5
|
+
//
|
|
6
|
+
// export const Create{{Pascal}}Schema = z.object({
|
|
7
|
+
// name: z.string().min(1),
|
|
8
|
+
// });
|
|
9
|
+
// export type Create{{Pascal}}Input = z.infer<typeof Create{{Pascal}}Schema>;
|
|
10
|
+
|
|
11
|
+
export const {{Pascal}}IdParam = z.object({
|
|
12
|
+
id: z.string().uuid(),
|
|
13
|
+
});
|
|
14
|
+
export type {{Pascal}}IdParamInput = z.infer<typeof {{Pascal}}IdParam>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestContainer } from '@katajs/core';
|
|
2
|
+
|
|
3
|
+
export type {{Pascal}}Service = {
|
|
4
|
+
ping(): Promise<{ ok: true; module: string }>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function make{{Pascal}}Service(_c: RequestContainer): {{Pascal}}Service {
|
|
8
|
+
return {
|
|
9
|
+
async ping() {
|
|
10
|
+
return { ok: true as const, module: '{{kebab}}' };
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@katajs/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Project commands for katajs apps — `katajs add module/service/route`, codemods, and upgrades.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"katajs",
|
|
7
|
+
"cli",
|
|
8
|
+
"codemod",
|
|
9
|
+
"scaffold",
|
|
10
|
+
"hono",
|
|
11
|
+
"cloudflare",
|
|
12
|
+
"workers"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Yaseer A. Okino <yaseerokino@gmail.com>",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ookino/katajs.git",
|
|
19
|
+
"directory": "packages/katajs-cli"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/ookino/katajs/tree/main/packages/katajs-cli#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/ookino/katajs/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"katajs": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@clack/prompts": "^0.8.2",
|
|
36
|
+
"cac": "^6.7.14",
|
|
37
|
+
"kolorist": "^1.8.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.9.0",
|
|
41
|
+
"tsup": "^8.3.5",
|
|
42
|
+
"typescript": "^5.6.3",
|
|
43
|
+
"vitest": "^2.1.5"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"test": "vitest run --passWithNoTests",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
53
|
+
}
|
|
54
|
+
}
|