@warlock.js/core 5.0.1 → 5.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/CHANGELOG.md +47 -0
- package/esm/cli/cli-commands.manager.mjs +36 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +1 -1
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs +207 -0
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/config.check.mjs +5 -2
- package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/connectors.check.mjs +38 -12
- package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +8 -4
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs +77 -11
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/index.mjs +5 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +116 -53
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +27 -9
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/routes.check.mjs +51 -12
- package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/doctor-command.action.mjs +16 -6
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor/run-checks.mjs +15 -6
- package/esm/cli/commands/doctor/run-checks.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +17 -9
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/connectors/describe-server-address.mjs +89 -6
- package/esm/connectors/describe-server-address.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts +18 -0
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +36 -3
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +1 -4
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +3 -7
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +1 -1
- package/esm/dev-server/ready-block.mjs +62 -0
- package/esm/dev-server/ready-block.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs +2 -575
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +76 -0
- package/esm/generations/features/access.feature.mjs.map +1 -0
- package/esm/generations/features/ai-anthropic.feature.mjs +10 -0
- package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -0
- package/esm/generations/features/ai-bedrock.feature.mjs +10 -0
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -0
- package/esm/generations/features/ai-google.feature.mjs +10 -0
- package/esm/generations/features/ai-google.feature.mjs.map +1 -0
- package/esm/generations/features/ai-ollama.feature.mjs +10 -0
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -0
- package/esm/generations/features/ai-openai.feature.mjs +10 -0
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -0
- package/esm/generations/features/ai-panoptic.feature.mjs +13 -0
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -0
- package/esm/generations/features/ai-tools.feature.mjs +13 -0
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -0
- package/esm/generations/features/ai-workspace.feature.mjs +13 -0
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -0
- package/esm/generations/features/ai.feature.mjs +15 -0
- package/esm/generations/features/ai.feature.mjs.map +1 -0
- package/esm/generations/features/herald.feature.mjs +19 -0
- package/esm/generations/features/herald.feature.mjs.map +1 -0
- package/esm/generations/features/image.feature.mjs +9 -0
- package/esm/generations/features/image.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +72 -0
- package/esm/generations/features/index.mjs.map +1 -0
- package/esm/generations/features/mail.feature.mjs +10 -0
- package/esm/generations/features/mail.feature.mjs.map +1 -0
- package/esm/generations/features/mongodb.feature.mjs +9 -0
- package/esm/generations/features/mongodb.feature.mjs.map +1 -0
- package/esm/generations/features/mysql.feature.mjs +9 -0
- package/esm/generations/features/mysql.feature.mjs.map +1 -0
- package/esm/generations/features/notifications.feature.mjs +43 -0
- package/esm/generations/features/notifications.feature.mjs.map +1 -0
- package/esm/generations/features/postgres.feature.mjs +9 -0
- package/esm/generations/features/postgres.feature.mjs.map +1 -0
- package/esm/generations/features/react-email.feature.mjs +70 -0
- package/esm/generations/features/react-email.feature.mjs.map +1 -0
- package/esm/generations/features/react.feature.mjs +16 -0
- package/esm/generations/features/react.feature.mjs.map +1 -0
- package/esm/generations/features/redis.feature.mjs +9 -0
- package/esm/generations/features/redis.feature.mjs.map +1 -0
- package/esm/generations/features/s3.feature.mjs +13 -0
- package/esm/generations/features/s3.feature.mjs.map +1 -0
- package/esm/generations/features/scheduler.feature.mjs +9 -0
- package/esm/generations/features/scheduler.feature.mjs.map +1 -0
- package/esm/generations/features/ses.feature.mjs +9 -0
- package/esm/generations/features/ses.feature.mjs.map +1 -0
- package/esm/generations/features/shadcn.feature.mjs +397 -0
- package/esm/generations/features/shadcn.feature.mjs.map +1 -0
- package/esm/generations/features/shared/link-ai-package-import.mjs +37 -0
- package/esm/generations/features/shared/link-ai-package-import.mjs.map +1 -0
- package/esm/generations/features/shared/migration-timestamp.mjs +17 -0
- package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -0
- package/esm/generations/features/socket.feature.mjs +15 -0
- package/esm/generations/features/socket.feature.mjs.map +1 -0
- package/esm/generations/features/tailwind.feature.mjs +174 -0
- package/esm/generations/features/tailwind.feature.mjs.map +1 -0
- package/esm/generations/features/test.feature.mjs +96 -0
- package/esm/generations/features/test.feature.mjs.map +1 -0
- package/esm/generations/features/web.feature.mjs +179 -0
- package/esm/generations/features/web.feature.mjs.map +1 -0
- package/esm/generations/stubs.mjs +95 -8
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/index.d.mts +1 -0
- package/esm/http/index.mjs +1 -0
- package/esm/http/ready-report.d.mts +56 -0
- package/esm/http/ready-report.d.mts.map +1 -0
- package/esm/http/ready-report.mjs +24 -0
- package/esm/http/ready-report.mjs.map +1 -0
- package/esm/index.d.mts +2 -1
- package/esm/index.mjs +2 -1
- package/llms-full.txt +322 -1
- package/llms.txt +2 -0
- package/package.json +12 -12
- package/skills/use-request-locals/SKILL.md +24 -1
- package/skills/warlock-conventions/SKILL.md +5 -1
|
@@ -2,585 +2,12 @@ import { rootPath, srcPath } from "../utils/paths.mjs";
|
|
|
2
2
|
import "../utils/index.mjs";
|
|
3
3
|
import { getWarlockVersion } from "../utils/framework-vesion.mjs";
|
|
4
4
|
import { detectPackageManager, getAddCommand } from "../updater/package-manager.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { featuresMap } from "./features/index.mjs";
|
|
6
6
|
import { colors } from "@mongez/copper";
|
|
7
|
-
import {
|
|
7
|
+
import { fileExistsAsync, getJsonFileAsync, putFileAsync, putJsonFileAsync } from "@warlock.js/fs";
|
|
8
8
|
import { execSync } from "node:child_process";
|
|
9
9
|
|
|
10
10
|
//#region ../core/src/generations/add-command.action.ts
|
|
11
|
-
/**
|
|
12
|
-
* Build a migration filename timestamp prefix in the framework's
|
|
13
|
-
* MM-DD-YYYY_HH-MM-SS form. Cascade infers a migration's createdAt from this
|
|
14
|
-
* prefix and orders migrations deterministically by it. Pass `offsetSeconds` to
|
|
15
|
-
* stamp sibling migrations created in the same scaffold a second apart so they
|
|
16
|
-
* never collide and keep a stable relative order.
|
|
17
|
-
*/
|
|
18
|
-
function migrationTimestamp(offsetSeconds = 0) {
|
|
19
|
-
const now = new Date(Date.now() + offsetSeconds * 1e3);
|
|
20
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
21
|
-
return `${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${now.getFullYear()}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
|
22
|
-
}
|
|
23
|
-
async function completeTestInstallation(options) {
|
|
24
|
-
const testGlobalSetupPath = srcPath("test-global-setup.ts");
|
|
25
|
-
if (!await fileExistsAsync(testGlobalSetupPath)) {
|
|
26
|
-
await putFileAsync(testGlobalSetupPath, `/**
|
|
27
|
-
* Global Test Setup
|
|
28
|
-
*
|
|
29
|
-
* Runs ONCE before all test workers.
|
|
30
|
-
* Starts the HTTP server for integration tests.
|
|
31
|
-
*/
|
|
32
|
-
import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests";
|
|
33
|
-
|
|
34
|
-
export async function setup() {
|
|
35
|
-
await startHttpTestServer();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function teardown() {
|
|
39
|
-
await stopHttpTestServer();
|
|
40
|
-
}
|
|
41
|
-
`);
|
|
42
|
-
console.log(`${colors.green("✓")} Created src/test-global-setup.ts`);
|
|
43
|
-
}
|
|
44
|
-
const testSetupPath = srcPath("test-setup.ts");
|
|
45
|
-
if (!await fileExistsAsync(testSetupPath)) {
|
|
46
|
-
await putFileAsync(testSetupPath, `/**
|
|
47
|
-
* Test Setup - runs before EVERY test file
|
|
48
|
-
*
|
|
49
|
-
* Vitest runs setupFiles before each test file and rebuilds the module
|
|
50
|
-
* registry with it, so this pair boots and closes the test runtime once per
|
|
51
|
-
* test file.
|
|
52
|
-
*
|
|
53
|
-
* setupTest() is called with no options on purpose: an explicit connectors
|
|
54
|
-
* value outranks tests.connectors from src/config/tests.ts, so passing one
|
|
55
|
-
* here would erase your project config. Omitting it leaves the config in
|
|
56
|
-
* charge.
|
|
57
|
-
*
|
|
58
|
-
* afterAll(teardownTest) is the other half of the pair: whoever calls
|
|
59
|
-
* setupTest() owns closing it in the same runtime context.
|
|
60
|
-
*/
|
|
61
|
-
import { setupTest, teardownTest } from "@warlock.js/core/tests";
|
|
62
|
-
import { afterAll } from "vitest";
|
|
63
|
-
|
|
64
|
-
await setupTest();
|
|
65
|
-
|
|
66
|
-
afterAll(teardownTest);
|
|
67
|
-
`);
|
|
68
|
-
console.log(`${colors.green("✓")} Created src/test-setup.ts`);
|
|
69
|
-
}
|
|
70
|
-
const viteConfigPath = rootPath("vite.config.ts");
|
|
71
|
-
if (!await fileExistsAsync(viteConfigPath)) {
|
|
72
|
-
await putFileAsync(viteConfigPath, `import { lowerStage3Decorators } from "@warlock.js/core/vite";
|
|
73
|
-
import mongezVite from "@mongez/vite";
|
|
74
|
-
import { defineConfig } from "vitest/config";
|
|
75
|
-
|
|
76
|
-
export default defineConfig({
|
|
77
|
-
// lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)
|
|
78
|
-
// decorators with esbuild before oxc / the SSR rewrite can mangle them, so
|
|
79
|
-
// decorated Cascade models load under Vitest.
|
|
80
|
-
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
81
|
-
test: {
|
|
82
|
-
globalSetup: "./src/test-global-setup.ts", // HTTP server - runs once
|
|
83
|
-
setupFiles: ["./src/test-setup.ts"], // DB/cache - runs per test file
|
|
84
|
-
environment: "node",
|
|
85
|
-
globals: false,
|
|
86
|
-
include: ["src/app/**/*.test.ts"],
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
`);
|
|
90
|
-
console.log(`${colors.green("✓")} Created vite.config.ts`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
async function completeReactEmailInstallation(_options) {
|
|
94
|
-
const emailsFolderPath = rootPath("emails");
|
|
95
|
-
const sampleEmailPath = rootPath("emails/welcome-email.tsx");
|
|
96
|
-
if (!await fileExistsAsync(sampleEmailPath)) {
|
|
97
|
-
await ensureDirectoryAsync(emailsFolderPath);
|
|
98
|
-
await putFileAsync(sampleEmailPath, `import { Body, Container, Head, Html, Text } from "@react-email/components";
|
|
99
|
-
import { Tailwind } from "@react-email/tailwind";
|
|
100
|
-
|
|
101
|
-
interface WelcomeEmailProps {
|
|
102
|
-
name: string;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Sample welcome email component.
|
|
107
|
-
* Preview with: yarn email:preview
|
|
108
|
-
*/
|
|
109
|
-
export default function WelcomeEmail({ name }: WelcomeEmailProps) {
|
|
110
|
-
return (
|
|
111
|
-
<Html>
|
|
112
|
-
<Head />
|
|
113
|
-
<Tailwind>
|
|
114
|
-
<Body className="bg-gray-100 font-sans">
|
|
115
|
-
<Container className="mx-auto max-w-xl py-8 px-4">
|
|
116
|
-
<Text className="text-2xl font-bold text-gray-900">
|
|
117
|
-
Welcome, {name}!
|
|
118
|
-
</Text>
|
|
119
|
-
<Text className="text-gray-600 mt-2">
|
|
120
|
-
You're all set. We're glad to have you on board.
|
|
121
|
-
</Text>
|
|
122
|
-
</Container>
|
|
123
|
-
</Body>
|
|
124
|
-
</Tailwind>
|
|
125
|
-
</Html>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
`);
|
|
129
|
-
console.log(`${colors.green("✓")} Created emails/welcome-email.tsx`);
|
|
130
|
-
}
|
|
131
|
-
const tsconfigPath = rootPath("tsconfig.json");
|
|
132
|
-
const tsconfig = await getJsonFileAsync(tsconfigPath);
|
|
133
|
-
if (!tsconfig.include) tsconfig.include = [];
|
|
134
|
-
if (!tsconfig.include.includes("emails")) {
|
|
135
|
-
tsconfig.include.push("emails");
|
|
136
|
-
await putJsonFileAsync(tsconfigPath, tsconfig);
|
|
137
|
-
console.log(`${colors.green("✓")} Added "emails" to tsconfig.json include`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
async function completeNotificationsInstallation(_options) {
|
|
141
|
-
const modelPath = srcPath("app/notifications/notification.model.ts");
|
|
142
|
-
if (await fileExistsAsync(modelPath)) {
|
|
143
|
-
console.log(`${colors.yellowBright("src/app/notifications")} already scaffolded, skipping model + migration...`);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
await ensureDirectoryAsync(srcPath("app/notifications"));
|
|
147
|
-
await putFileAsync(modelPath, notificationModelStub);
|
|
148
|
-
console.log(`${colors.green("✓")} Created src/app/notifications/notification.model.ts`);
|
|
149
|
-
await ensureDirectoryAsync(srcPath("app/notifications/migrations"));
|
|
150
|
-
const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;
|
|
151
|
-
await putFileAsync(srcPath("app/notifications/migrations", migrationFile), notificationMigrationStub);
|
|
152
|
-
console.log(`${colors.green("✓")} Created src/app/notifications/migrations/${migrationFile}`);
|
|
153
|
-
await ensureDirectoryAsync(srcPath("app/notifications/controllers"));
|
|
154
|
-
await putFileAsync(srcPath("app/notifications/controllers/notifications.controller.ts"), notificationControllersStub);
|
|
155
|
-
await putFileAsync(srcPath("app/notifications/routes.ts"), notificationRoutesStub);
|
|
156
|
-
console.log(`${colors.green("✓")} Created src/app/notifications/routes.ts + controllers`);
|
|
157
|
-
}
|
|
158
|
-
async function registerAccessLocale() {
|
|
159
|
-
const localesPath = srcPath("app/shared/utils/locales.ts");
|
|
160
|
-
const accessLocale = `groupedTranslations("access", {
|
|
161
|
-
errors: {
|
|
162
|
-
forbidden: {
|
|
163
|
-
en: "You do not have permission to perform this action.",
|
|
164
|
-
ar: "ليس لديك صلاحية لتنفيذ هذا الإجراء.",
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
`;
|
|
169
|
-
if (await fileExistsAsync(localesPath)) {
|
|
170
|
-
const current = await getFileAsync(localesPath);
|
|
171
|
-
if (current.includes(`groupedTranslations("access"`)) {
|
|
172
|
-
console.log(`${colors.yellowBright("access")} locale already registered, skipping...`);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
await putFileAsync(localesPath, `${current.includes("groupedTranslations(") ? "" : `import { groupedTranslations } from "@warlock.js/core";\n\n`}${current.trimEnd()}\n\n${accessLocale}`);
|
|
176
|
-
console.log(`${colors.green("✓")} Registered the access locale in src/app/shared/utils/locales.ts`);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
await ensureDirectoryAsync(srcPath("app/shared/utils"));
|
|
180
|
-
await putFileAsync(localesPath, `import { groupedTranslations } from "@warlock.js/core";\n\n${accessLocale}`);
|
|
181
|
-
console.log(`${colors.green("✓")} Created src/app/shared/utils/locales.ts with the access locale`);
|
|
182
|
-
}
|
|
183
|
-
async function scaffoldAccessFiles() {
|
|
184
|
-
const resolverPath = srcPath("app/access/services/access-resolver.ts");
|
|
185
|
-
if (await fileExistsAsync(resolverPath)) {
|
|
186
|
-
console.log(`${colors.yellowBright("src/app/access")} already scaffolded, skipping resolver + role tables...`);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
await ensureDirectoryAsync(srcPath("app/access/models/role"));
|
|
190
|
-
await putFileAsync(srcPath("app/access/models/role/role.model.ts"), accessRoleModelStub);
|
|
191
|
-
await putFileAsync(srcPath("app/access/models/role/index.ts"), accessRoleModelIndexStub);
|
|
192
|
-
console.log(`${colors.green("✓")} Created src/app/access/models/role`);
|
|
193
|
-
await ensureDirectoryAsync(srcPath("app/access/models/role/migrations"));
|
|
194
|
-
const roleMigrationFile = `${migrationTimestamp()}-role.migration.ts`;
|
|
195
|
-
await putFileAsync(srcPath("app/access/models/role/migrations", roleMigrationFile), accessRoleMigrationStub);
|
|
196
|
-
console.log(`${colors.green("✓")} Created src/app/access/models/role/migrations/${roleMigrationFile}`);
|
|
197
|
-
await ensureDirectoryAsync(srcPath("app/access/models/user-role"));
|
|
198
|
-
await putFileAsync(srcPath("app/access/models/user-role/user-role.model.ts"), accessUserRoleModelStub);
|
|
199
|
-
await putFileAsync(srcPath("app/access/models/user-role/index.ts"), accessUserRoleModelIndexStub);
|
|
200
|
-
console.log(`${colors.green("✓")} Created src/app/access/models/user-role`);
|
|
201
|
-
await ensureDirectoryAsync(srcPath("app/access/models/user-role/migrations"));
|
|
202
|
-
const userRoleMigrationFile = `${migrationTimestamp(1)}-user-role.migration.ts`;
|
|
203
|
-
await putFileAsync(srcPath("app/access/models/user-role/migrations", userRoleMigrationFile), accessUserRoleMigrationStub);
|
|
204
|
-
console.log(`${colors.green("✓")} Created src/app/access/models/user-role/migrations/${userRoleMigrationFile}`);
|
|
205
|
-
await ensureDirectoryAsync(srcPath("app/access/services"));
|
|
206
|
-
await putFileAsync(resolverPath, accessResolverStub);
|
|
207
|
-
console.log(`${colors.green("✓")} Created src/app/access/services/access-resolver.ts`);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Register the WebConnector in `warlock.config.ts`, and ONLY there.
|
|
211
|
-
*
|
|
212
|
-
* It belongs to the config array or to app code, never both. Both halves are
|
|
213
|
-
* registered before app code loads — the CLI preloader in dev, the generated
|
|
214
|
-
* entry in production — so also calling `connectorsManager.register(...)` in
|
|
215
|
-
* `src/app/main.ts` boots the connector twice and installs every page route
|
|
216
|
-
* twice. That surfaces at PRODUCTION boot as `Route name "..." is already
|
|
217
|
-
* taken`, because pages and API routes share one route-name namespace.
|
|
218
|
-
*
|
|
219
|
-
* The config array is the half to prefer: `warlock build` reads the same array
|
|
220
|
-
* to drain each connector's build contribution, so "built for" and "boots with"
|
|
221
|
-
* cannot drift.
|
|
222
|
-
*
|
|
223
|
-
* String surgery rather than a TypeScript parse: `warlock.config.ts` is an
|
|
224
|
-
* app-owned file that may carry any formatting, and a parse-and-print would
|
|
225
|
-
* reformat the parts we did not come to change.
|
|
226
|
-
*/
|
|
227
|
-
async function registerWebConnector() {
|
|
228
|
-
const configPath = rootPath("warlock.config.ts");
|
|
229
|
-
if (!await fileExistsAsync(configPath)) {
|
|
230
|
-
console.log(`${colors.yellowBright("warlock.config.ts")} not found — add this yourself:\n import { webConnector } from "@warlock.js/web/connector";\n export default defineConfig({ connectors: [webConnector()] });`);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const current = await getFileAsync(configPath);
|
|
234
|
-
if (current.includes("webConnector")) {
|
|
235
|
-
console.log(`${colors.yellowBright("webConnector")} already registered, skipping...`);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
const importLine = "import { webConnector } from \"@warlock.js/web/connector\";";
|
|
239
|
-
let next = current.includes(importLine) ? current : `${importLine}\n${current}`;
|
|
240
|
-
if (/connectors:\s*\[/.test(next)) next = next.replace(/connectors:\s*\[/, "connectors: [webConnector(),");
|
|
241
|
-
else if (next.includes("defineConfig({")) next = next.replace("defineConfig({", "defineConfig({\n connectors: [webConnector()],");
|
|
242
|
-
else {
|
|
243
|
-
console.log(`${colors.yellowBright("warlock.config.ts")} has no recognisable defineConfig({...}) — add \`connectors: [webConnector()]\` yourself.`);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
await putFileAsync(configPath, next);
|
|
247
|
-
console.log(`${colors.green("✓")} Registered webConnector in warlock.config.ts`);
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* The app routes file the project template registers `GET /` in. Only this one
|
|
251
|
-
* path is inspected: `warlock add web` is not a codebase-wide route auditor, and
|
|
252
|
-
* a project that keeps its routes elsewhere lands on the `absent` outcome below,
|
|
253
|
-
* which writes the page exactly as before.
|
|
254
|
-
*/
|
|
255
|
-
const APP_ROUTES_FILE = "app/shared/routes.ts";
|
|
256
|
-
/**
|
|
257
|
-
* A TOP-LEVEL `router.get("/", ...)` — anchored at column 0 on purpose.
|
|
258
|
-
*
|
|
259
|
-
* Routes nested in a `router.group({ prefix: "/x" }, ...)` are indented by every
|
|
260
|
-
* formatter this codebase runs, and their real path is `/x`, not `/`. Anchoring
|
|
261
|
-
* is what keeps the notifications feature's own `router.get("/", ...)` (inside
|
|
262
|
-
* the `/notifications` group) from reading as a homepage collision.
|
|
263
|
-
*
|
|
264
|
-
* Only the path literal is captured. The handler — a bare identifier in the
|
|
265
|
-
* template, but possibly an inline arrow spanning lines — is never matched, so
|
|
266
|
-
* the rewrite below cannot damage it.
|
|
267
|
-
*/
|
|
268
|
-
const TOP_LEVEL_ROOT_GET = /^router\s*\.\s*get\(\s*(["'`])\/\1/gm;
|
|
269
|
-
/**
|
|
270
|
-
* Whether `/welcome` is already spoken for, so relocating onto it would trade
|
|
271
|
-
* one duplicate-route 500 for another.
|
|
272
|
-
*/
|
|
273
|
-
const TOP_LEVEL_WELCOME_GET = /^router\s*\.\s*get\(\s*(["'`])\/welcome\1/m;
|
|
274
|
-
/**
|
|
275
|
-
* Make room for a page that declares `route = "/"`.
|
|
276
|
-
*
|
|
277
|
-
* The project template registers `router.get("/", homePageController)` and the
|
|
278
|
-
* page stub declares `route = "/"`. Fastify rejects the second registration
|
|
279
|
-
* (`Method 'GET' already declared for route '/'`) and the homepage 500s at
|
|
280
|
-
* request time — so `warlock add web` cannot just write the page and hope.
|
|
281
|
-
*
|
|
282
|
-
* Of the three ways out, this RELOCATES the JSON route to `/welcome` rather than
|
|
283
|
-
* deleting it or refusing to scaffold:
|
|
284
|
-
*
|
|
285
|
-
* - Deleting the controller is what the scaffolder's own `react` feature does,
|
|
286
|
-
* but it may do that: it owns the file it is deleting, seconds after writing
|
|
287
|
-
* it. `warlock add web` runs against a project a human has been living in, and
|
|
288
|
-
* silently unlinking their code is not a thing an `add` command gets to do.
|
|
289
|
-
* - Writing the page anyway and printing a warning ships a project whose
|
|
290
|
-
* homepage 500s. A warning above a broken app is still a broken app.
|
|
291
|
-
* - Relocating keeps BOTH surfaces working: the React homepage takes `/`, the
|
|
292
|
-
* JSON welcome answers at `/welcome`, and no line of user code disappears.
|
|
293
|
-
*
|
|
294
|
-
* Only the exact top-level shape is rewritten, and only the path literal inside
|
|
295
|
-
* it. Anything else that claims `/` is reported and left completely alone — we
|
|
296
|
-
* do not guess at code we cannot recognise.
|
|
297
|
-
*/
|
|
298
|
-
async function relocateConflictingHomeRoute() {
|
|
299
|
-
const routesPath = srcPath(APP_ROUTES_FILE);
|
|
300
|
-
if (!await fileExistsAsync(routesPath)) return { outcome: "absent" };
|
|
301
|
-
let current;
|
|
302
|
-
try {
|
|
303
|
-
current = await getFileAsync(routesPath);
|
|
304
|
-
} catch (error) {
|
|
305
|
-
return {
|
|
306
|
-
outcome: "failed",
|
|
307
|
-
reason: `could not be read (${error.message})`
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
const matches = current.match(TOP_LEVEL_ROOT_GET) ?? [];
|
|
311
|
-
if (matches.length === 0) return { outcome: "absent" };
|
|
312
|
-
if (matches.length > 1) return {
|
|
313
|
-
outcome: "conflict",
|
|
314
|
-
reason: `declares ${matches.length} top-level GET "/" routes`
|
|
315
|
-
};
|
|
316
|
-
if (TOP_LEVEL_WELCOME_GET.test(current)) return {
|
|
317
|
-
outcome: "conflict",
|
|
318
|
-
reason: "already declares GET \"/welcome\", so the usual relocation target is taken"
|
|
319
|
-
};
|
|
320
|
-
const next = current.replace(TOP_LEVEL_ROOT_GET, (match, quote) => match.replace(`${quote}/${quote}`, `${quote}/welcome${quote}`));
|
|
321
|
-
if (next === current) return {
|
|
322
|
-
outcome: "conflict",
|
|
323
|
-
reason: "its GET \"/\" route could not be rewritten"
|
|
324
|
-
};
|
|
325
|
-
try {
|
|
326
|
-
await putFileAsync(routesPath, next);
|
|
327
|
-
} catch (error) {
|
|
328
|
-
return {
|
|
329
|
-
outcome: "failed",
|
|
330
|
-
reason: `could not be written (${error.message})`
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
return { outcome: "relocated" };
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Scaffold the smallest page layer that renders, and register the connector.
|
|
337
|
-
*
|
|
338
|
-
* `src/web/root.tsx` is the sentinel for "already scaffolded" — the framework
|
|
339
|
-
* ships a default root, so its presence means a human has been here.
|
|
340
|
-
*/
|
|
341
|
-
async function completeWebInstallation(_options) {
|
|
342
|
-
const rootFile = srcPath("web/root.tsx");
|
|
343
|
-
if (await fileExistsAsync(rootFile)) console.log(`${colors.yellowBright("src/web")} already scaffolded, skipping...`);
|
|
344
|
-
else {
|
|
345
|
-
await ensureDirectoryAsync(srcPath("web"));
|
|
346
|
-
await putFileAsync(rootFile, webRootStub);
|
|
347
|
-
console.log(`${colors.green("✓")} Created src/web/root.tsx`);
|
|
348
|
-
const collision = await relocateConflictingHomeRoute();
|
|
349
|
-
if (collision.outcome === "relocated") console.log(`${colors.green("✓")} Moved the existing ${colors.yellowBright("GET \"/\"")} route to ${colors.yellowBright("\"/welcome\"")} in ${colors.yellowBright(`src/${APP_ROUTES_FILE}`)} — the new page owns \`/\` now, and the JSON welcome route still answers at /welcome.`);
|
|
350
|
-
if (collision.outcome === "conflict" || collision.outcome === "failed") {
|
|
351
|
-
const verb = collision.outcome === "failed" ? colors.redBright("✗") : colors.yellowBright("!");
|
|
352
|
-
console.log(`${verb} Did not create src/web/home.page.tsx: ${colors.yellowBright(`src/${APP_ROUTES_FILE}`)} ${collision.reason}.\n The page stub declares ${colors.yellowBright("route = \"/\"")}, and two handlers on one path is a 500 at request time, not a startup error.
|
|
353
|
-
Free up ${colors.yellowBright("GET \"/\"")} in that file — move it to a path of its own, or remove it — then create src/web/home.page.tsx yourself. Giving the page a \`route\` other than \`/\` works too.`);
|
|
354
|
-
process.exitCode = 1;
|
|
355
|
-
} else {
|
|
356
|
-
await putFileAsync(srcPath("web/home.page.tsx"), webHomePageStub);
|
|
357
|
-
console.log(`${colors.green("✓")} Created src/web/home.page.tsx`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
await registerWebConnector();
|
|
361
|
-
}
|
|
362
|
-
async function completeAccessInstallation(_options) {
|
|
363
|
-
await registerAccessLocale();
|
|
364
|
-
await scaffoldAccessFiles();
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Link a satellite AI package into src/config/ai.ts via a side-effect import.
|
|
368
|
-
*
|
|
369
|
-
* The satellites (@warlock.js/ai-tools, ai-panoptic, ai-workspace) augment the
|
|
370
|
-
* `ai` object on import — they register their runtime surface (ai.tools / ai.mcp,
|
|
371
|
-
* ai.workspace, panoptic's ai.config({ panoptic }) wiring) AND the matching TS
|
|
372
|
-
* declaration-merging so ai.* members resolve. config-loader runs config/ai.ts at
|
|
373
|
-
* boot, so dropping a bare `import "<specifier>";` at the top of that file is what
|
|
374
|
-
* actually loads the augmentation before the ai connector applies the config.
|
|
375
|
-
*
|
|
376
|
-
* No-op if config/ai.ts is missing (the `ai` feature ejects it) or the import is
|
|
377
|
-
* already present. Inserts right after the `warlock:ai-packages` marker when it
|
|
378
|
-
* exists so the satellite imports stay grouped; otherwise prepends to the top.
|
|
379
|
-
*/
|
|
380
|
-
async function linkAiPackageImport(specifier) {
|
|
381
|
-
const aiConfigPath = srcPath("config/ai.ts");
|
|
382
|
-
if (!await fileExistsAsync(aiConfigPath)) return;
|
|
383
|
-
const current = await getFileAsync(aiConfigPath);
|
|
384
|
-
const importLine = `import "${specifier}";`;
|
|
385
|
-
if (current.includes(importLine)) return;
|
|
386
|
-
const marker = "// >>> warlock:ai-packages (auto-managed) >>>";
|
|
387
|
-
let next;
|
|
388
|
-
if (current.includes(marker)) next = current.replace(marker, `${marker}\n${importLine}`);
|
|
389
|
-
else next = `${importLine}\n${current}`;
|
|
390
|
-
await putFileAsync(aiConfigPath, next);
|
|
391
|
-
console.log(`${colors.green("✓")} Linked ${specifier} in src/config/ai.ts`);
|
|
392
|
-
}
|
|
393
|
-
const featuresMap = {
|
|
394
|
-
"react-email": {
|
|
395
|
-
description: "Installs react-email for building email templates with React and Tailwind",
|
|
396
|
-
requires: ["mail", "react"],
|
|
397
|
-
dependencies: {
|
|
398
|
-
"react-email": "^5.2.10",
|
|
399
|
-
"@react-email/components": "^1.0.11",
|
|
400
|
-
"@react-email/render": "^2.0.5",
|
|
401
|
-
"@react-email/tailwind": "^2.0.7"
|
|
402
|
-
},
|
|
403
|
-
devDependencies: { "@react-email/preview-server": "5.2.10" },
|
|
404
|
-
script: { "email:preview": "npx react-email dev" },
|
|
405
|
-
onExecuting: completeReactEmailInstallation
|
|
406
|
-
},
|
|
407
|
-
react: {
|
|
408
|
-
description: "Installs React and React dom for rendering React components (non-interactive), useful for sending mails and generating HTML",
|
|
409
|
-
dependencies: {
|
|
410
|
-
react: "^19.2.3",
|
|
411
|
-
"react-dom": "^19.2.3"
|
|
412
|
-
},
|
|
413
|
-
devDependencies: {
|
|
414
|
-
"@types/react": "^19.2.7",
|
|
415
|
-
"@types/react-dom": "^19.2.3"
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
image: {
|
|
419
|
-
description: "Installs sharp for image processing",
|
|
420
|
-
dependencies: { sharp: "^0.34.5" }
|
|
421
|
-
},
|
|
422
|
-
mail: {
|
|
423
|
-
description: "Installs nodemailer for sending emails",
|
|
424
|
-
dependencies: { nodemailer: "^8.0.5" },
|
|
425
|
-
devDependencies: { "@types/nodemailer": "^8.0.0" }
|
|
426
|
-
},
|
|
427
|
-
ses: {
|
|
428
|
-
description: "Installs AWS SES SDK for sending emails via Amazon SES",
|
|
429
|
-
dependencies: { "@aws-sdk/client-sesv2": "^3.1025.0" }
|
|
430
|
-
},
|
|
431
|
-
mongodb: {
|
|
432
|
-
description: "Installs mongodb driver for database driver (Cascade Package)",
|
|
433
|
-
dependencies: { mongodb: "^7.0.0" }
|
|
434
|
-
},
|
|
435
|
-
scheduler: {
|
|
436
|
-
description: "Installs warlock scheduler for scheduling tasks",
|
|
437
|
-
dependencies: { "@warlock.js/scheduler": "~4.0.0" }
|
|
438
|
-
},
|
|
439
|
-
postgres: {
|
|
440
|
-
description: "Installs pg for Postgres database (Cascade Package)",
|
|
441
|
-
dependencies: { pg: "^8.11.0" }
|
|
442
|
-
},
|
|
443
|
-
mysql: {
|
|
444
|
-
description: "Installs mysql2 for MySQL database driver (Cascade Package)",
|
|
445
|
-
dependencies: { mysql2: "^3.5.0" }
|
|
446
|
-
},
|
|
447
|
-
redis: {
|
|
448
|
-
description: "Installs redis for Redis cache driver (Cache Package)",
|
|
449
|
-
dependencies: { redis: "^4.6.13" }
|
|
450
|
-
},
|
|
451
|
-
s3: {
|
|
452
|
-
description: "Installs AWS SDK for Cloud storage (Storage Package)",
|
|
453
|
-
dependencies: {
|
|
454
|
-
"@aws-sdk/client-s3": "^3.955.0",
|
|
455
|
-
"@aws-sdk/lib-storage": "^3.955.0",
|
|
456
|
-
"@aws-sdk/s3-request-presigner": "^3.955.0"
|
|
457
|
-
}
|
|
458
|
-
},
|
|
459
|
-
test: {
|
|
460
|
-
description: "Installs warlock test for testing",
|
|
461
|
-
onExecuting: completeTestInstallation,
|
|
462
|
-
script: {
|
|
463
|
-
test: "vitest run",
|
|
464
|
-
"test:coverage": "vitest run --coverage",
|
|
465
|
-
"test:ui": "vitest --ui",
|
|
466
|
-
"test:watch": "vitest --watch"
|
|
467
|
-
},
|
|
468
|
-
devDependencies: {
|
|
469
|
-
"@mongez/vite": "^2.0.4",
|
|
470
|
-
vite: "^8.0.16",
|
|
471
|
-
vitest: "^4.1.8",
|
|
472
|
-
"@vitest/coverage-v8": "^4.1.8"
|
|
473
|
-
}
|
|
474
|
-
},
|
|
475
|
-
web: {
|
|
476
|
-
description: "Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.",
|
|
477
|
-
dependencies: {
|
|
478
|
-
"@warlock.js/web": "~4.0.0",
|
|
479
|
-
react: "^19.2.3",
|
|
480
|
-
"react-dom": "^19.2.3"
|
|
481
|
-
},
|
|
482
|
-
devDependencies: {
|
|
483
|
-
"@types/react": "^19.2.7",
|
|
484
|
-
"@types/react-dom": "^19.2.3",
|
|
485
|
-
vite: "^7.3.5",
|
|
486
|
-
"@vitejs/plugin-react": "^5.2.0"
|
|
487
|
-
},
|
|
488
|
-
onExecuting: completeWebInstallation
|
|
489
|
-
},
|
|
490
|
-
herald: {
|
|
491
|
-
description: "Installs herald for message broker (Herald Package)",
|
|
492
|
-
dependencies: {
|
|
493
|
-
"@warlock.js/herald": "~4.0.0",
|
|
494
|
-
amqplib: "^0.10.0"
|
|
495
|
-
},
|
|
496
|
-
devDependencies: { "@types/amqplib": "^0.10.0" },
|
|
497
|
-
ejectConfig: {
|
|
498
|
-
content: communicatorsConfigStub,
|
|
499
|
-
name: "herald"
|
|
500
|
-
}
|
|
501
|
-
},
|
|
502
|
-
socket: {
|
|
503
|
-
description: "Installs socket.io for the realtime socket server (Socket Connector)",
|
|
504
|
-
dependencies: { "socket.io": "^4.8.3" },
|
|
505
|
-
ejectConfig: {
|
|
506
|
-
content: socketConfigStub,
|
|
507
|
-
name: "socket"
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
notifications: {
|
|
511
|
-
description: "Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications",
|
|
512
|
-
requires: ["mail"],
|
|
513
|
-
dependencies: {
|
|
514
|
-
"@warlock.js/notifications": "~4.0.0",
|
|
515
|
-
"@warlock.js/auth": "~4.0.0"
|
|
516
|
-
},
|
|
517
|
-
ejectConfig: {
|
|
518
|
-
content: notificationsConfigStub,
|
|
519
|
-
name: "notifications"
|
|
520
|
-
},
|
|
521
|
-
onExecuting: completeNotificationsInstallation
|
|
522
|
-
},
|
|
523
|
-
access: {
|
|
524
|
-
description: "Installs @warlock.js/access — authorization (RBAC + ABAC): permission checks, ABAC policies, and roles. Ejects config/access.ts, the DatabaseAccessResolver + Role/UserRole models and migrations into src/app/access, and registers the access locale in src/app/shared/utils/locales.ts",
|
|
525
|
-
dependencies: { "@warlock.js/access": "~4.0.0" },
|
|
526
|
-
ejectConfig: {
|
|
527
|
-
content: accessConfigStub,
|
|
528
|
-
name: "access"
|
|
529
|
-
},
|
|
530
|
-
onExecuting: completeAccessInstallation
|
|
531
|
-
},
|
|
532
|
-
ai: {
|
|
533
|
-
description: "Installs @warlock.js/ai — the core AI toolkit (agents, tools, workflows)",
|
|
534
|
-
dependencies: { "@warlock.js/ai": "~4.0.0" },
|
|
535
|
-
ejectConfig: {
|
|
536
|
-
content: aiConfigStub,
|
|
537
|
-
name: "ai"
|
|
538
|
-
}
|
|
539
|
-
},
|
|
540
|
-
"ai-openai": {
|
|
541
|
-
description: "OpenAI provider for @warlock.js/ai (pulls the core ai package)",
|
|
542
|
-
requires: ["ai"],
|
|
543
|
-
dependencies: { "@warlock.js/ai-openai": "~4.0.0" }
|
|
544
|
-
},
|
|
545
|
-
"ai-google": {
|
|
546
|
-
description: "Google (Gemini) provider for @warlock.js/ai (pulls the core ai package)",
|
|
547
|
-
requires: ["ai"],
|
|
548
|
-
dependencies: { "@warlock.js/ai-google": "~4.0.0" }
|
|
549
|
-
},
|
|
550
|
-
"ai-anthropic": {
|
|
551
|
-
description: "Anthropic (Claude) provider for @warlock.js/ai (pulls the core ai package)",
|
|
552
|
-
requires: ["ai"],
|
|
553
|
-
dependencies: { "@warlock.js/ai-anthropic": "~4.0.0" }
|
|
554
|
-
},
|
|
555
|
-
"ai-bedrock": {
|
|
556
|
-
description: "AWS Bedrock provider for @warlock.js/ai (pulls the core ai package)",
|
|
557
|
-
requires: ["ai"],
|
|
558
|
-
dependencies: { "@warlock.js/ai-bedrock": "~4.0.0" }
|
|
559
|
-
},
|
|
560
|
-
"ai-ollama": {
|
|
561
|
-
description: "Ollama provider for @warlock.js/ai (pulls the core ai package)",
|
|
562
|
-
requires: ["ai"],
|
|
563
|
-
dependencies: { "@warlock.js/ai-ollama": "~4.0.0" }
|
|
564
|
-
},
|
|
565
|
-
"ai-tools": {
|
|
566
|
-
description: "Installs @warlock.js/ai-tools — ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + an MCP client/server, under ai.tools.* / ai.mcp (pulls the core ai package)",
|
|
567
|
-
requires: ["ai"],
|
|
568
|
-
dependencies: { "@warlock.js/ai-tools": "~4.0.0" },
|
|
569
|
-
onExecuting: () => linkAiPackageImport("@warlock.js/ai-tools")
|
|
570
|
-
},
|
|
571
|
-
"ai-panoptic": {
|
|
572
|
-
description: "Installs @warlock.js/ai-panoptic — observability for @warlock.js/ai (collector, exporters, zero-setup local dashboard) via ai.config({ panoptic }) (pulls the core ai package)",
|
|
573
|
-
requires: ["ai"],
|
|
574
|
-
dependencies: { "@warlock.js/ai-panoptic": "~4.0.0" },
|
|
575
|
-
onExecuting: () => linkAiPackageImport("@warlock.js/ai-panoptic")
|
|
576
|
-
},
|
|
577
|
-
"ai-workspace": {
|
|
578
|
-
description: "Installs @warlock.js/ai-workspace — a policy-jailed filesystem + shell workspace for coding agents, as ai.workspace (pulls the core ai package)",
|
|
579
|
-
requires: ["ai"],
|
|
580
|
-
dependencies: { "@warlock.js/ai-workspace": "~4.0.0" },
|
|
581
|
-
onExecuting: () => linkAiPackageImport("@warlock.js/ai-workspace")
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
11
|
const allowedFeatures = Object.keys(featuresMap);
|
|
585
12
|
function resolveFeatures(features, visited = /* @__PURE__ */ new Set()) {
|
|
586
13
|
const resolved = [];
|