create-katajs 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 +59 -0
- package/dist/index.js +568 -0
- package/dist/templates/api/.github/workflows/deploy.yml +26 -0
- package/dist/templates/api/README.md +95 -0
- package/dist/templates/api/_dev.vars.example +3 -0
- package/dist/templates/api/_gitignore +11 -0
- package/dist/templates/api/drizzle.config.ts +10 -0
- package/dist/templates/api/package.json +32 -0
- package/dist/templates/api/scripts/graph.ts +19 -0
- package/dist/templates/api/scripts/modules.ts +7 -0
- package/dist/templates/api/src/app.ts +39 -0
- package/dist/templates/api/src/db/schema.ts +11 -0
- package/dist/templates/api/src/index.ts +6 -0
- package/dist/templates/api/src/modules/posts/index.ts +31 -0
- package/dist/templates/api/src/modules/posts/posts.errors.ts +15 -0
- package/dist/templates/api/src/modules/posts/posts.repository.ts +22 -0
- package/dist/templates/api/src/modules/posts/posts.routes.ts +20 -0
- package/dist/templates/api/src/modules/posts/posts.schema.ts +12 -0
- package/dist/templates/api/src/modules/posts/posts.service.ts +26 -0
- package/dist/templates/api/src/types.d.ts +31 -0
- package/dist/templates/api/tsconfig.json +22 -0
- package/dist/templates/api/wrangler.jsonc +35 -0
- package/dist/templates/auth-snippets/api/src/db/auth-schema.ts +53 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/auth.config.ts +30 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/auth.errors.ts +10 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/auth.middleware.ts +37 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/auth.routes.ts +7 -0
- package/dist/templates/auth-snippets/api/src/modules/auth/index.ts +28 -0
- package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.errors.ts +10 -0
- package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.middleware.ts +37 -0
- package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.routes.ts +7 -0
- package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/index.ts +29 -0
- package/dist/templates/auth-snippets/monorepo/packages/auth/package.json +22 -0
- package/dist/templates/auth-snippets/monorepo/packages/auth/src/auth.ts +40 -0
- package/dist/templates/auth-snippets/monorepo/packages/auth/src/index.ts +2 -0
- package/dist/templates/auth-snippets/monorepo/packages/auth/tsconfig.json +4 -0
- package/dist/templates/auth-snippets/monorepo/packages/db/src/auth-schema.ts +53 -0
- package/dist/templates/monorepo/.github/workflows/deploy.yml +39 -0
- package/dist/templates/monorepo/README.md +91 -0
- package/dist/templates/monorepo/_gitignore +12 -0
- package/dist/templates/monorepo/apps/api/_dev.vars.example +7 -0
- package/dist/templates/monorepo/apps/api/_gitignore +7 -0
- package/dist/templates/monorepo/apps/api/package.json +36 -0
- package/dist/templates/monorepo/apps/api/scripts/graph.ts +19 -0
- package/dist/templates/monorepo/apps/api/scripts/modules.ts +7 -0
- package/dist/templates/monorepo/apps/api/src/app.ts +42 -0
- package/dist/templates/monorepo/apps/api/src/index.ts +7 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/index.ts +31 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.errors.ts +15 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.repository.ts +22 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.routes.ts +20 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.schema.ts +13 -0
- package/dist/templates/monorepo/apps/api/src/modules/posts/posts.service.ts +26 -0
- package/dist/templates/monorepo/apps/api/src/types.d.ts +29 -0
- package/dist/templates/monorepo/apps/api/tsconfig.json +7 -0
- package/dist/templates/monorepo/apps/api/wrangler.jsonc +34 -0
- package/dist/templates/monorepo/package.json +24 -0
- package/dist/templates/monorepo/packages/api-client/package.json +20 -0
- package/dist/templates/monorepo/packages/api-client/src/index.ts +43 -0
- package/dist/templates/monorepo/packages/api-client/tsconfig.json +4 -0
- package/dist/templates/monorepo/packages/db/drizzle.config.ts +12 -0
- package/dist/templates/monorepo/packages/db/package.json +26 -0
- package/dist/templates/monorepo/packages/db/src/index.ts +13 -0
- package/dist/templates/monorepo/packages/db/src/schema.ts +12 -0
- package/dist/templates/monorepo/packages/db/tsconfig.json +4 -0
- package/dist/templates/monorepo/pnpm-workspace.yaml +3 -0
- package/dist/templates/monorepo/tsconfig.base.json +18 -0
- package/dist/templates/monorepo/turbo.json +19 -0
- package/dist/templates/monorepo-worker/apps/worker/_dev.vars.example +4 -0
- package/dist/templates/monorepo-worker/apps/worker/_gitignore +6 -0
- package/dist/templates/monorepo-worker/apps/worker/package.json +29 -0
- package/dist/templates/monorepo-worker/apps/worker/src/app.ts +27 -0
- package/dist/templates/monorepo-worker/apps/worker/src/index.ts +6 -0
- package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/example.consumer.ts +23 -0
- package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/example.service.ts +18 -0
- package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/index.ts +24 -0
- package/dist/templates/monorepo-worker/apps/worker/src/types.d.ts +19 -0
- package/dist/templates/monorepo-worker/apps/worker/tsconfig.json +7 -0
- package/dist/templates/monorepo-worker/apps/worker/wrangler.jsonc +28 -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,59 @@
|
|
|
1
|
+
# create-katajs
|
|
2
|
+
|
|
3
|
+
Scaffolding CLI for [katajs](https://github.com/ookino/katajs) projects.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm create katajs my-app
|
|
7
|
+
# or
|
|
8
|
+
npm create katajs@latest my-app
|
|
9
|
+
# or
|
|
10
|
+
yarn create katajs my-app
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Flags
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm create katajs my-app [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--auth Add Better Auth scaffolding
|
|
20
|
+
--pm <pm> Package manager (pnpm | npm | yarn | bun)
|
|
21
|
+
--no-install Skip install step
|
|
22
|
+
--no-git Skip git init
|
|
23
|
+
-h, --help Show help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If you omit a name or skip flags, the CLI prompts interactively.
|
|
27
|
+
|
|
28
|
+
## What you get
|
|
29
|
+
|
|
30
|
+
A single-API Cloudflare Workers project with:
|
|
31
|
+
|
|
32
|
+
- `@katajs/core` runtime + `@katajs/drizzle` adapter wired up
|
|
33
|
+
- One example `posts` module (schema, repository, service, routes, errors)
|
|
34
|
+
- Drizzle config + migration setup
|
|
35
|
+
- `wrangler.jsonc` with Hyperdrive binding placeholder
|
|
36
|
+
- `.dev.vars.example`
|
|
37
|
+
- `.github/workflows/deploy.yml` stub
|
|
38
|
+
- README with next-step instructions
|
|
39
|
+
|
|
40
|
+
With `--auth`, you also get a `src/modules/auth/` integrating [Better Auth](https://better-auth.com), Better Auth tables added to your Drizzle schema, and a protected route on the example module.
|
|
41
|
+
|
|
42
|
+
## After scaffolding
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd my-app
|
|
46
|
+
|
|
47
|
+
# 1. Configure your Hyperdrive binding in wrangler.jsonc
|
|
48
|
+
# (see https://developers.cloudflare.com/hyperdrive/ to create one)
|
|
49
|
+
|
|
50
|
+
# 2. Generate migrations from the schema
|
|
51
|
+
pnpm db:generate
|
|
52
|
+
|
|
53
|
+
# 3. Run dev server
|
|
54
|
+
pnpm dev
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
[MIT](./LICENSE) © Yaseer A. Okino
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { cac } from "cac";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { cyan, dim, green, red, yellow } from "kolorist";
|
|
7
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
8
|
+
import { resolve as pathResolve } from "path";
|
|
9
|
+
|
|
10
|
+
// src/scaffold.ts
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
import {
|
|
13
|
+
cpSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
renameSync,
|
|
19
|
+
statSync,
|
|
20
|
+
writeFileSync
|
|
21
|
+
} from "fs";
|
|
22
|
+
import { dirname, join, relative, resolve } from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
|
|
25
|
+
// src/codemod.ts
|
|
26
|
+
var AnchorMissingError = class extends Error {
|
|
27
|
+
constructor(anchor) {
|
|
28
|
+
super(`Anchor "// katajs:${anchor}" not found`);
|
|
29
|
+
this.anchor = anchor;
|
|
30
|
+
this.name = "AnchorMissingError";
|
|
31
|
+
}
|
|
32
|
+
anchor;
|
|
33
|
+
};
|
|
34
|
+
function insertBeforeAnchor(content, anchor, lines) {
|
|
35
|
+
const anchorMatch = new RegExp(`^(\\s*)// katajs:${escapeRe(anchor)}\\s*$`, "m");
|
|
36
|
+
const m = content.match(anchorMatch);
|
|
37
|
+
if (!m) throw new AnchorMissingError(anchor);
|
|
38
|
+
const indent = m[1] ?? "";
|
|
39
|
+
const toInsert = (Array.isArray(lines) ? lines : [lines]).map((l) => indent + l).join("\n");
|
|
40
|
+
const allPresent = (Array.isArray(lines) ? lines : [lines]).every(
|
|
41
|
+
(l) => content.includes(l.trim())
|
|
42
|
+
);
|
|
43
|
+
if (allPresent) return content;
|
|
44
|
+
return content.replace(anchorMatch, `${toInsert}
|
|
45
|
+
${m[0]}`);
|
|
46
|
+
}
|
|
47
|
+
function escapeRe(s) {
|
|
48
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
49
|
+
}
|
|
50
|
+
function appendRouteToChain(content, moduleVar) {
|
|
51
|
+
const newCallSig = `.route(${moduleVar}.prefix, ${moduleVar}.routes)`;
|
|
52
|
+
if (content.includes(newCallSig)) return content;
|
|
53
|
+
const lines = content.split("\n");
|
|
54
|
+
const anchorIdx = lines.findIndex(
|
|
55
|
+
(l) => /^\s*\/\/ katajs:routes\s*$/.test(l)
|
|
56
|
+
);
|
|
57
|
+
if (anchorIdx === -1) throw new AnchorMissingError("routes");
|
|
58
|
+
let lastChainIdx = -1;
|
|
59
|
+
const chainLineRe = /^\s*\.route\(\w+\.prefix,\s*\w+\.routes\)/;
|
|
60
|
+
for (let i = anchorIdx - 1; i >= Math.max(0, anchorIdx - 25); i--) {
|
|
61
|
+
if (chainLineRe.test(lines[i] ?? "")) {
|
|
62
|
+
lastChainIdx = i;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (lastChainIdx === -1) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"appendRouteToChain: could not locate a `.route(<X>.prefix, <X>.routes)` line above // katajs:routes"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const lastLine = lines[lastChainIdx];
|
|
72
|
+
const indent = lastLine.match(/^(\s*)/)?.[1] ?? " ";
|
|
73
|
+
lines[lastChainIdx] = lastLine.replace(/,(\s*)$/, "$1");
|
|
74
|
+
lines.splice(
|
|
75
|
+
lastChainIdx + 1,
|
|
76
|
+
0,
|
|
77
|
+
`${indent}.route(${moduleVar}.prefix, ${moduleVar}.routes),`
|
|
78
|
+
);
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/scaffold.ts
|
|
83
|
+
var PROJECT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
84
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
85
|
+
".ts",
|
|
86
|
+
".tsx",
|
|
87
|
+
".js",
|
|
88
|
+
".cjs",
|
|
89
|
+
".mjs",
|
|
90
|
+
".json",
|
|
91
|
+
".jsonc",
|
|
92
|
+
".md",
|
|
93
|
+
".yml",
|
|
94
|
+
".yaml",
|
|
95
|
+
".example",
|
|
96
|
+
".toml",
|
|
97
|
+
""
|
|
98
|
+
]);
|
|
99
|
+
async function runScaffold(opts) {
|
|
100
|
+
const workerName = opts.workerName ?? "worker";
|
|
101
|
+
if (opts.worker && !opts.monorepo) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`--worker requires --monorepo. apps/${workerName} only makes sense in a monorepo layout.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (opts.worker && !PROJECT_NAME_RE.test(workerName)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid worker name '${workerName}'. Use lowercase letters, digits, and hyphens (must start with a letter or digit).`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const templateName = opts.monorepo ? "monorepo" : "api";
|
|
112
|
+
const templateRoot = resolve(getTemplatesDir(), templateName);
|
|
113
|
+
if (!existsSync(templateRoot)) {
|
|
114
|
+
throw new Error(`Template directory not found: ${templateRoot}`);
|
|
115
|
+
}
|
|
116
|
+
ensureDir(opts.targetDir);
|
|
117
|
+
const tokens = {
|
|
118
|
+
PROJECT_NAME: opts.projectName,
|
|
119
|
+
TODAY_ISO: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
120
|
+
WORKER_NAME: workerName
|
|
121
|
+
};
|
|
122
|
+
copyTemplate(templateRoot, opts.targetDir, tokens);
|
|
123
|
+
if (opts.worker) {
|
|
124
|
+
const workerRoot = resolve(getTemplatesDir(), "monorepo-worker");
|
|
125
|
+
if (!existsSync(workerRoot)) {
|
|
126
|
+
throw new Error(`Worker template not found: ${workerRoot}`);
|
|
127
|
+
}
|
|
128
|
+
copyTemplate(workerRoot, opts.targetDir, tokens);
|
|
129
|
+
if (workerName !== "worker") {
|
|
130
|
+
const oldDir = join(opts.targetDir, "apps", "worker");
|
|
131
|
+
const newDir = join(opts.targetDir, "apps", workerName);
|
|
132
|
+
if (existsSync(oldDir)) {
|
|
133
|
+
renameSync(oldDir, newDir);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
augmentForWorker(opts.targetDir, tokens);
|
|
137
|
+
}
|
|
138
|
+
renameUnderscorePrefixed(opts.targetDir);
|
|
139
|
+
if (opts.auth) {
|
|
140
|
+
const authVariant = opts.monorepo ? "monorepo" : "api";
|
|
141
|
+
const authRoot = resolve(getTemplatesDir(), "auth-snippets", authVariant);
|
|
142
|
+
if (!existsSync(authRoot)) {
|
|
143
|
+
throw new Error(`Auth snippet template not found: ${authRoot}`);
|
|
144
|
+
}
|
|
145
|
+
copyTemplate(authRoot, opts.targetDir, tokens);
|
|
146
|
+
if (opts.monorepo) {
|
|
147
|
+
augmentForAuthMonorepo(opts.targetDir, tokens);
|
|
148
|
+
} else {
|
|
149
|
+
augmentForAuth(opts.targetDir, tokens);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (opts.install) {
|
|
153
|
+
runCommand(opts.packageManager, ["install"], opts.targetDir);
|
|
154
|
+
}
|
|
155
|
+
if (opts.initGit) {
|
|
156
|
+
runCommand("git", ["init", "-q"], opts.targetDir);
|
|
157
|
+
runCommand("git", ["add", "."], opts.targetDir);
|
|
158
|
+
runCommand(
|
|
159
|
+
"git",
|
|
160
|
+
["commit", "-q", "-m", "Initial commit from create-katajs"],
|
|
161
|
+
opts.targetDir,
|
|
162
|
+
true
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function renameUnderscorePrefixed(root) {
|
|
167
|
+
const renamableNames = /* @__PURE__ */ new Map([
|
|
168
|
+
["_gitignore", ".gitignore"],
|
|
169
|
+
["_dev.vars.example", ".dev.vars.example"]
|
|
170
|
+
]);
|
|
171
|
+
const stack = [root];
|
|
172
|
+
while (stack.length) {
|
|
173
|
+
const dir = stack.pop();
|
|
174
|
+
let entries;
|
|
175
|
+
try {
|
|
176
|
+
entries = readdirSync(dir);
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const full = join(dir, entry);
|
|
182
|
+
let st;
|
|
183
|
+
try {
|
|
184
|
+
st = statSync(full);
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (st.isDirectory()) {
|
|
189
|
+
if (entry === "node_modules") continue;
|
|
190
|
+
stack.push(full);
|
|
191
|
+
} else if (st.isFile()) {
|
|
192
|
+
const target = renamableNames.get(entry);
|
|
193
|
+
if (target) {
|
|
194
|
+
renameSync(full, join(dir, target));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function getTemplatesDir() {
|
|
201
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
202
|
+
const candidates = [join(here, "templates"), join(here, "..", "src", "templates")];
|
|
203
|
+
for (const c of candidates) {
|
|
204
|
+
if (existsSync(c)) return c;
|
|
205
|
+
}
|
|
206
|
+
return candidates[0];
|
|
207
|
+
}
|
|
208
|
+
function copyTemplate(srcRoot, dstRoot, tokens) {
|
|
209
|
+
const stack = [
|
|
210
|
+
{ from: srcRoot, to: dstRoot }
|
|
211
|
+
];
|
|
212
|
+
while (stack.length) {
|
|
213
|
+
const { from, to } = stack.pop();
|
|
214
|
+
ensureDir(to);
|
|
215
|
+
for (const entry of readdirSync(from)) {
|
|
216
|
+
const fromPath = join(from, entry);
|
|
217
|
+
const toPath = join(to, entry);
|
|
218
|
+
const st = statSync(fromPath);
|
|
219
|
+
if (st.isDirectory()) {
|
|
220
|
+
stack.push({ from: fromPath, to: toPath });
|
|
221
|
+
} else if (st.isFile()) {
|
|
222
|
+
copyFileWithTokens(fromPath, toPath, tokens);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function copyFileWithTokens(from, to, tokens) {
|
|
228
|
+
const ext = extOf(from);
|
|
229
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
230
|
+
const content = readFileSync(from, "utf8");
|
|
231
|
+
const replaced = applyTokens(content, tokens);
|
|
232
|
+
writeFileSync(to, replaced);
|
|
233
|
+
} else {
|
|
234
|
+
cpSync(from, to);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function applyTokens(content, tokens) {
|
|
238
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => tokens[key] ?? `{{${key}}}`);
|
|
239
|
+
}
|
|
240
|
+
function ensureDir(dir) {
|
|
241
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
242
|
+
}
|
|
243
|
+
function extOf(path) {
|
|
244
|
+
const base = path.split("/").pop() ?? "";
|
|
245
|
+
const idx = base.lastIndexOf(".");
|
|
246
|
+
if (idx <= 0) return "";
|
|
247
|
+
return base.slice(idx);
|
|
248
|
+
}
|
|
249
|
+
function runCommand(cmd, args, cwd, allowFailure = false) {
|
|
250
|
+
const res = spawnSync(cmd, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
251
|
+
if (res.status !== 0 && !allowFailure) {
|
|
252
|
+
throw new Error(`Command failed: ${cmd} ${args.join(" ")} (in ${relative(process.cwd(), cwd) || "."})`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function augmentForAuth(targetDir, _tokens) {
|
|
256
|
+
const appPath = join(targetDir, "src", "app.ts");
|
|
257
|
+
if (existsSync(appPath)) {
|
|
258
|
+
let app = readFileSync(appPath, "utf8");
|
|
259
|
+
if (!app.includes("from './modules/auth/index'")) {
|
|
260
|
+
app = insertBeforeAnchor(
|
|
261
|
+
app,
|
|
262
|
+
"module-imports",
|
|
263
|
+
"import { authModule } from './modules/auth/index';"
|
|
264
|
+
);
|
|
265
|
+
app = insertBeforeAnchor(app, "modules", "authModule,");
|
|
266
|
+
app = appendRouteToChain(app, "authModule");
|
|
267
|
+
writeFileSync(appPath, app);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const typesPath = join(targetDir, "src", "types.d.ts");
|
|
271
|
+
if (existsSync(typesPath)) {
|
|
272
|
+
let types = readFileSync(typesPath, "utf8");
|
|
273
|
+
if (!types.includes("'./modules/auth/index'")) {
|
|
274
|
+
types = insertBeforeAnchor(
|
|
275
|
+
types,
|
|
276
|
+
"registry-imports",
|
|
277
|
+
"import type { AuthRegistry } from './modules/auth/index';"
|
|
278
|
+
);
|
|
279
|
+
types = insertBeforeAnchor(types, "registry", ", AuthRegistry");
|
|
280
|
+
writeFileSync(typesPath, types);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const modulesScript = join(targetDir, "scripts", "modules.ts");
|
|
284
|
+
if (existsSync(modulesScript)) {
|
|
285
|
+
let g = readFileSync(modulesScript, "utf8");
|
|
286
|
+
if (!g.includes("'../src/modules/auth/index'")) {
|
|
287
|
+
g = insertBeforeAnchor(
|
|
288
|
+
g,
|
|
289
|
+
"graph-imports",
|
|
290
|
+
"import { authModule } from '../src/modules/auth/index';"
|
|
291
|
+
);
|
|
292
|
+
g = insertBeforeAnchor(g, "graph-modules", "authModule,");
|
|
293
|
+
writeFileSync(modulesScript, g);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const schemaPath = join(targetDir, "src", "db", "schema.ts");
|
|
297
|
+
const authSchemaPath = join(targetDir, "src", "db", "auth-schema.ts");
|
|
298
|
+
if (existsSync(authSchemaPath) && existsSync(schemaPath)) {
|
|
299
|
+
const schemaContent = readFileSync(schemaPath, "utf8");
|
|
300
|
+
if (!schemaContent.includes("export * from './auth-schema';")) {
|
|
301
|
+
writeFileSync(
|
|
302
|
+
schemaPath,
|
|
303
|
+
schemaContent + "\nexport * from './auth-schema';\n"
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const pkgPath = join(targetDir, "package.json");
|
|
308
|
+
if (existsSync(pkgPath)) {
|
|
309
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
310
|
+
pkg.dependencies = { ...pkg.dependencies, "better-auth": "^1.0.0" };
|
|
311
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
312
|
+
}
|
|
313
|
+
const dvPath = join(targetDir, ".dev.vars.example");
|
|
314
|
+
if (existsSync(dvPath)) {
|
|
315
|
+
const content = readFileSync(dvPath, "utf8");
|
|
316
|
+
if (!content.includes("BETTER_AUTH_SECRET")) {
|
|
317
|
+
writeFileSync(
|
|
318
|
+
dvPath,
|
|
319
|
+
content.trimEnd() + '\nBETTER_AUTH_SECRET="replace-me-with-a-32-byte-secret"\nBETTER_AUTH_URL="http://localhost:8787"\n'
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function augmentForAuthMonorepo(targetDir, tokens) {
|
|
325
|
+
const projectName = tokens.PROJECT_NAME ?? "";
|
|
326
|
+
const appPath = join(targetDir, "apps", "api", "src", "app.ts");
|
|
327
|
+
if (existsSync(appPath)) {
|
|
328
|
+
let app = readFileSync(appPath, "utf8");
|
|
329
|
+
if (!app.includes("from './modules/auth/index'")) {
|
|
330
|
+
app = insertBeforeAnchor(
|
|
331
|
+
app,
|
|
332
|
+
"module-imports",
|
|
333
|
+
"import { authModule } from './modules/auth/index';"
|
|
334
|
+
);
|
|
335
|
+
app = insertBeforeAnchor(app, "modules", "authModule,");
|
|
336
|
+
app = appendRouteToChain(app, "authModule");
|
|
337
|
+
writeFileSync(appPath, app);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const typesPath = join(targetDir, "apps", "api", "src", "types.d.ts");
|
|
341
|
+
if (existsSync(typesPath)) {
|
|
342
|
+
let types = readFileSync(typesPath, "utf8");
|
|
343
|
+
if (!types.includes("'./modules/auth/index'")) {
|
|
344
|
+
types = insertBeforeAnchor(
|
|
345
|
+
types,
|
|
346
|
+
"registry-imports",
|
|
347
|
+
"import type { AuthRegistry } from './modules/auth/index';"
|
|
348
|
+
);
|
|
349
|
+
types = insertBeforeAnchor(types, "registry", ", AuthRegistry");
|
|
350
|
+
writeFileSync(typesPath, types);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const modulesScript = join(targetDir, "apps", "api", "scripts", "modules.ts");
|
|
354
|
+
if (existsSync(modulesScript)) {
|
|
355
|
+
let g = readFileSync(modulesScript, "utf8");
|
|
356
|
+
if (!g.includes("'../src/modules/auth/index'")) {
|
|
357
|
+
g = insertBeforeAnchor(
|
|
358
|
+
g,
|
|
359
|
+
"graph-imports",
|
|
360
|
+
"import { authModule } from '../src/modules/auth/index';"
|
|
361
|
+
);
|
|
362
|
+
g = insertBeforeAnchor(g, "graph-modules", "authModule,");
|
|
363
|
+
writeFileSync(modulesScript, g);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const dbIndexPath = join(targetDir, "packages", "db", "src", "index.ts");
|
|
367
|
+
if (existsSync(dbIndexPath)) {
|
|
368
|
+
const dbIndex = readFileSync(dbIndexPath, "utf8");
|
|
369
|
+
if (!dbIndex.includes("export * from './auth-schema';")) {
|
|
370
|
+
writeFileSync(
|
|
371
|
+
dbIndexPath,
|
|
372
|
+
dbIndex.trimEnd() + "\nexport * from './auth-schema';\n"
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const apiPkgPath = join(targetDir, "apps", "api", "package.json");
|
|
377
|
+
if (existsSync(apiPkgPath)) {
|
|
378
|
+
const pkg = JSON.parse(readFileSync(apiPkgPath, "utf8"));
|
|
379
|
+
pkg.dependencies = {
|
|
380
|
+
...pkg.dependencies,
|
|
381
|
+
[`@${projectName}/auth`]: "workspace:*",
|
|
382
|
+
"better-auth": "^1.0.0"
|
|
383
|
+
};
|
|
384
|
+
writeFileSync(apiPkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
385
|
+
}
|
|
386
|
+
const dvPath = join(targetDir, "apps", "api", ".dev.vars.example");
|
|
387
|
+
if (existsSync(dvPath)) {
|
|
388
|
+
const content = readFileSync(dvPath, "utf8");
|
|
389
|
+
if (!content.includes("BETTER_AUTH_SECRET")) {
|
|
390
|
+
writeFileSync(
|
|
391
|
+
dvPath,
|
|
392
|
+
content.trimEnd() + '\nBETTER_AUTH_SECRET="replace-me-with-a-32-byte-secret"\nBETTER_AUTH_URL="http://localhost:8787"\n'
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function augmentForWorker(targetDir, tokens) {
|
|
398
|
+
const projectName = tokens.PROJECT_NAME ?? "";
|
|
399
|
+
const workerName = tokens.WORKER_NAME ?? "worker";
|
|
400
|
+
const rootPkgPath = join(targetDir, "package.json");
|
|
401
|
+
if (!existsSync(rootPkgPath)) return;
|
|
402
|
+
const pkg = JSON.parse(readFileSync(rootPkgPath, "utf8"));
|
|
403
|
+
pkg.scripts = {
|
|
404
|
+
...pkg.scripts,
|
|
405
|
+
deploy: `pnpm --filter @${projectName}/api deploy && pnpm --filter @${projectName}/${workerName} deploy`,
|
|
406
|
+
"deploy:api": `pnpm --filter @${projectName}/api deploy`,
|
|
407
|
+
[`deploy:${workerName}`]: `pnpm --filter @${projectName}/${workerName} deploy`
|
|
408
|
+
};
|
|
409
|
+
writeFileSync(rootPkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/prompts.ts
|
|
413
|
+
function detectPackageManager() {
|
|
414
|
+
const ua = process.env.npm_config_user_agent;
|
|
415
|
+
if (!ua) return void 0;
|
|
416
|
+
if (ua.startsWith("pnpm/")) return "pnpm";
|
|
417
|
+
if (ua.startsWith("bun/")) return "bun";
|
|
418
|
+
if (ua.startsWith("npm/")) return "npm";
|
|
419
|
+
return void 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/index.ts
|
|
423
|
+
var PROJECT_NAME_RE2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
424
|
+
async function main() {
|
|
425
|
+
const cli = cac("create-katajs");
|
|
426
|
+
cli.command("[name]", "Scaffold a katajs project").option("--auth", "Include Better Auth").option("--monorepo", "Scaffold as a pnpm + Turbo monorepo (apps/api + packages/db + packages/api-client)").option("--worker [name]", 'Add a queue-consumer Worker app. Optional name (e.g. "payout-worker"); defaults to "worker". Requires --monorepo.').option("--no-install", "Skip pnpm/npm/bun install").option("--pm <pm>", "Force package manager (pnpm | npm | bun)").option("--no-git", "Skip git init + initial commit").action(async (name, flags) => {
|
|
427
|
+
try {
|
|
428
|
+
await run(name, flags);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
p.log.error(red(err.message));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
cli.help();
|
|
435
|
+
cli.version("0.1.0");
|
|
436
|
+
cli.parse();
|
|
437
|
+
}
|
|
438
|
+
async function run(rawName, flags) {
|
|
439
|
+
p.intro(cyan("create-katajs"));
|
|
440
|
+
const projectName = await resolveProjectName(rawName);
|
|
441
|
+
if (p.isCancel(projectName)) return p.cancel("Aborted.");
|
|
442
|
+
const targetDir = pathResolve(process.cwd(), projectName);
|
|
443
|
+
if (existsSync2(targetDir) && readdirSync2(targetDir).length > 0) {
|
|
444
|
+
throw new Error(`Target directory '${projectName}' is not empty.`);
|
|
445
|
+
}
|
|
446
|
+
const interactive = process.stdin.isTTY === true;
|
|
447
|
+
const monorepo = flags.monorepo ?? (interactive ? await askYesNo("Scaffold as a monorepo (apps/api + shared packages)?", false) : false);
|
|
448
|
+
if (p.isCancel(monorepo)) return p.cancel("Aborted.");
|
|
449
|
+
let worker = flags.worker !== void 0 && flags.worker !== false;
|
|
450
|
+
let workerName;
|
|
451
|
+
if (typeof flags.worker === "string" && flags.worker.length > 0) {
|
|
452
|
+
workerName = flags.worker;
|
|
453
|
+
} else if (worker) {
|
|
454
|
+
workerName = "worker";
|
|
455
|
+
} else {
|
|
456
|
+
workerName = "worker";
|
|
457
|
+
}
|
|
458
|
+
if (monorepo && flags.worker === void 0 && interactive) {
|
|
459
|
+
const answer = await askYesNo("Include a queue worker app?", false);
|
|
460
|
+
if (p.isCancel(answer)) return p.cancel("Aborted.");
|
|
461
|
+
worker = !!answer;
|
|
462
|
+
if (worker) {
|
|
463
|
+
const nameAnswer = await p.text({
|
|
464
|
+
message: "Worker name?",
|
|
465
|
+
placeholder: "worker",
|
|
466
|
+
initialValue: "worker",
|
|
467
|
+
validate: (v) => {
|
|
468
|
+
if (!v) return "Required";
|
|
469
|
+
if (!PROJECT_NAME_RE2.test(v))
|
|
470
|
+
return "Use lowercase letters, digits, and hyphens.";
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
if (p.isCancel(nameAnswer)) return p.cancel("Aborted.");
|
|
475
|
+
workerName = nameAnswer;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (worker && !monorepo) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
"--worker requires --monorepo. A worker app only makes sense in a monorepo layout."
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
if (worker && !PROJECT_NAME_RE2.test(workerName)) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`Invalid worker name '${workerName}'. Use lowercase letters, digits, and hyphens.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const auth = flags.auth ?? (interactive ? await askYesNo("Include Better Auth?", false) : false);
|
|
489
|
+
if (p.isCancel(auth)) return p.cancel("Aborted.");
|
|
490
|
+
const detectedPm = detectPackageManager();
|
|
491
|
+
const pm = flags.pm ?? detectedPm ?? (interactive ? await askPm() : "pnpm");
|
|
492
|
+
if (p.isCancel(pm)) return p.cancel("Aborted.");
|
|
493
|
+
if (pm !== "pnpm" && pm !== "npm" && pm !== "bun") {
|
|
494
|
+
throw new Error(`Unsupported package manager '${pm}'. Use one of: pnpm, npm, bun.`);
|
|
495
|
+
}
|
|
496
|
+
const install = flags.install !== false;
|
|
497
|
+
const initGit = flags.git !== false;
|
|
498
|
+
p.log.step(
|
|
499
|
+
`Scaffolding ${green(projectName)} (${monorepo ? "monorepo" : "single-api"}, worker=${worker ? workerName : "no"}, auth=${auth ? "yes" : "no"}, pm=${pm}, install=${install ? "yes" : "no"}, git=${initGit ? "yes" : "no"})`
|
|
500
|
+
);
|
|
501
|
+
const opts = {
|
|
502
|
+
targetDir,
|
|
503
|
+
projectName,
|
|
504
|
+
auth: !!auth,
|
|
505
|
+
monorepo: !!monorepo,
|
|
506
|
+
worker,
|
|
507
|
+
workerName,
|
|
508
|
+
packageManager: pm,
|
|
509
|
+
install,
|
|
510
|
+
initGit
|
|
511
|
+
};
|
|
512
|
+
await runScaffold(opts);
|
|
513
|
+
p.outro(green(`\u2713 Created ${projectName} in ./${projectName}`));
|
|
514
|
+
console.log();
|
|
515
|
+
console.log("Next steps:");
|
|
516
|
+
console.log(` ${cyan("cd")} ${projectName}`);
|
|
517
|
+
if (!install) console.log(` ${cyan(pm)} install`);
|
|
518
|
+
console.log(` ${cyan(pm)} dev`);
|
|
519
|
+
console.log();
|
|
520
|
+
console.log(dim("Resources:"));
|
|
521
|
+
console.log(dim(" Docs: https://github.com/<owner>/katajs"));
|
|
522
|
+
console.log(dim(" Hono: https://hono.dev"));
|
|
523
|
+
console.log(dim(" Hyperdrive: https://developers.cloudflare.com/hyperdrive"));
|
|
524
|
+
}
|
|
525
|
+
async function resolveProjectName(raw) {
|
|
526
|
+
if (raw) {
|
|
527
|
+
if (!PROJECT_NAME_RE2.test(raw)) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`Invalid project name '${raw}'. Use lowercase letters, digits, and hyphens (must start with a letter or digit).`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return raw;
|
|
533
|
+
}
|
|
534
|
+
if (process.stdin.isTTY !== true) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
"Project name is required when running non-interactively. Pass it as the first positional argument."
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const answer = await p.text({
|
|
540
|
+
message: "Project name?",
|
|
541
|
+
placeholder: "my-api",
|
|
542
|
+
validate: (v) => {
|
|
543
|
+
if (!v) return "Required";
|
|
544
|
+
if (!PROJECT_NAME_RE2.test(v)) return "Use lowercase letters, digits, and hyphens.";
|
|
545
|
+
return void 0;
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
return answer;
|
|
549
|
+
}
|
|
550
|
+
async function askYesNo(message, initial) {
|
|
551
|
+
return p.confirm({ message, initialValue: initial });
|
|
552
|
+
}
|
|
553
|
+
async function askPm() {
|
|
554
|
+
return p.select({
|
|
555
|
+
message: "Package manager?",
|
|
556
|
+
initialValue: "pnpm",
|
|
557
|
+
options: [
|
|
558
|
+
{ value: "pnpm", label: "pnpm" },
|
|
559
|
+
{ value: "npm", label: "npm" },
|
|
560
|
+
{ value: "bun", label: "bun" }
|
|
561
|
+
]
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
void main().catch((err) => {
|
|
565
|
+
console.error(yellow("Unexpected error:"));
|
|
566
|
+
console.error(err);
|
|
567
|
+
process.exit(1);
|
|
568
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: pnpm/action-setup@v4
|
|
16
|
+
with:
|
|
17
|
+
version: 9
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 20
|
|
21
|
+
cache: pnpm
|
|
22
|
+
- run: pnpm install --frozen-lockfile
|
|
23
|
+
- run: pnpm typecheck
|
|
24
|
+
- uses: cloudflare/wrangler-action@v3
|
|
25
|
+
with:
|
|
26
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|