create-warlock 5.19.0 → 5.20.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 +13 -0
- package/README.md +22 -6
- package/esm/commands/create-new-app/index.mjs +16 -10
- package/esm/commands/create-new-app/index.mjs.map +1 -1
- package/esm/prompts/ask-ai-features.mjs +36 -0
- package/esm/prompts/ask-ai-features.mjs.map +1 -0
- package/esm/prompts/ask-stack.mjs +4 -6
- package/esm/prompts/ask-stack.mjs.map +1 -1
- package/llms-full.txt +17 -3
- package/package.json +3 -3
- package/skills/create-a-warlock-project/SKILL.md +17 -3
- package/templates/warlock/package.json +4 -4
- package/templates/warlock/skills/web-code-standards/SKILL.md +15 -0
- package/templates/warlock/src/web/home/index.setup.ts +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `create-warlock` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.20.0 - 2026-09-24
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Added a package-level skill index and clearer discovery descriptions for grouped agent guidance.
|
|
12
|
+
- Web starter loaders use named functions and public loader-context types while retaining inferred return types in paired setup modules.
|
|
13
|
+
|
|
14
|
+
## 5.19.1 - 2026-09-24
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The default interactive scaffold flow now asks for a database after the API-only/full-stack choice unless `--db` or `--no-db` already answered it; the same selector includes **None** for a database-free app. Customize's AI multiselect now offers **None** last, treats it alone like an empty selection, and asks again when it is combined with an AI provider or capability.
|
|
19
|
+
|
|
7
20
|
## 5.19.0 - 2026-09-23
|
|
8
21
|
|
|
9
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -28,16 +28,16 @@ Then follow the instructions, it is easy as that!
|
|
|
28
28
|
|
|
29
29
|
`create-warlock` is fully headless: every prompt has a flag that answers it,
|
|
30
30
|
`--yes` takes the default for anything you leave unset, and it works with no
|
|
31
|
-
terminal at all (CI, a script, an agent). With a terminal and neither `--yes`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Everything else below is a flag with a default; a summary of what
|
|
35
|
-
prints after scaffolding, along with how to change each one.
|
|
31
|
+
terminal at all (CI, a script, an agent). With a terminal and neither `--yes` nor `--interactive`, it asks for API-only
|
|
32
|
+
or full-stack web, then for a database unless `--db` or `--no-db` already
|
|
33
|
+
answered that choice. The stack is the only choice that changes the generated
|
|
34
|
+
app's shape. Everything else below is a flag with a default; a summary of what
|
|
35
|
+
was decided prints after scaffolding, along with how to change each one.
|
|
36
36
|
|
|
37
37
|
| Flag | Description | Default |
|
|
38
38
|
| --- | --- | --- |
|
|
39
39
|
| `--name=<name>` | Project name (or the first positional arg) | required — prompted with a TTY, otherwise fails naming this flag |
|
|
40
|
-
| `--stack=<api\|web>` |
|
|
40
|
+
| `--stack=<api\|web>` | API-only or full-stack web; the structural choice | `api` |
|
|
41
41
|
| `--db=<driver>` / `--no-db` | Database driver (`postgres`, `mongodb`, …), or skip one entirely | `mongodb` |
|
|
42
42
|
| `--features=<list>` | Comma-separated feature keys (e.g. `test,herald`) | none (or `["web"]` when `--stack=web` and `--features` is not given) |
|
|
43
43
|
| `--ai=<list>` | Comma-separated AI provider keys (e.g. `openai,anthropic`) | none |
|
|
@@ -67,6 +67,22 @@ honoured.
|
|
|
67
67
|
always works offline. An unrecognized target fails the run with the full list
|
|
68
68
|
of valid targets rather than silently falling back.
|
|
69
69
|
|
|
70
|
+
### Interactive database and AI choices
|
|
71
|
+
|
|
72
|
+
The default interactive flow offers the same database choices as Customize:
|
|
73
|
+
MongoDB and PostgreSQL, while MySQL remains shown as disabled, followed by
|
|
74
|
+
**None**. Choosing **None** is the same as
|
|
75
|
+
`--db=none` / `--no-db`: no database driver is installed and the generated
|
|
76
|
+
`src/config/database.ts` is removed. A supplied `--db` or `--no-db` skips this
|
|
77
|
+
prompt.
|
|
78
|
+
|
|
79
|
+
Customize keeps AI optional. Pressing Enter with no AI selections skips it. Its
|
|
80
|
+
last option, **None**, does the same when selected alone. **None** cannot be
|
|
81
|
+
combined with an AI provider or capability package; the wizard explains the
|
|
82
|
+
conflict and asks for the AI choices again, keeping the non-**None** choices
|
|
83
|
+
for correction. Headless `--ai` flags keep their existing provider/capability
|
|
84
|
+
key behavior.
|
|
85
|
+
|
|
70
86
|
## Developing this package locally
|
|
71
87
|
|
|
72
88
|
`bin/create-app.js` imports `../esm/index.mjs` — a build output, not the
|
|
@@ -9,6 +9,7 @@ import { seedFromFlags } from "../../flags/seed-from-flags.mjs";
|
|
|
9
9
|
import { packageRoot } from "../../helpers/paths.mjs";
|
|
10
10
|
import { App } from "../../helpers/app.mjs";
|
|
11
11
|
import { askAgentTargets } from "../../prompts/ask-agent-targets.mjs";
|
|
12
|
+
import { askAiFeatures } from "../../prompts/ask-ai-features.mjs";
|
|
12
13
|
import { askProjectName } from "../../prompts/ask-project-name.mjs";
|
|
13
14
|
import { askStack } from "../../prompts/ask-stack.mjs";
|
|
14
15
|
import { showIntroBanner } from "../../ui/banner.mjs";
|
|
@@ -92,12 +93,7 @@ async function createFullWizard(cli, pmDetectionPromise) {
|
|
|
92
93
|
cancel("Feature selection cancelled");
|
|
93
94
|
process.exit(0);
|
|
94
95
|
}
|
|
95
|
-
const selectedAiProviders = await
|
|
96
|
-
message: "Add AI packages? Providers + capabilities — the core AI package is included automatically",
|
|
97
|
-
options: [...getAiProviderOptions(), ...getAiPackageOptions()],
|
|
98
|
-
initialValues: seeds.aiProviders,
|
|
99
|
-
required: false
|
|
100
|
-
});
|
|
96
|
+
const selectedAiProviders = await askAiFeatures(seeds.aiProviders);
|
|
101
97
|
if (isCancel(selectedAiProviders)) {
|
|
102
98
|
cancel("AI provider selection cancelled");
|
|
103
99
|
process.exit(0);
|
|
@@ -139,9 +135,9 @@ async function createFullWizard(cli, pmDetectionPromise) {
|
|
|
139
135
|
* The default TTY path: neither `--yes` nor `--interactive` was given, but a
|
|
140
136
|
* terminal is available. Asks the project name when missing (a required
|
|
141
137
|
* answer with no sane default — same treatment as the non-interactive path)
|
|
142
|
-
* plus the
|
|
143
|
-
*
|
|
144
|
-
*
|
|
138
|
+
* plus the preset's database choice when `--db`/`--no-db` was not supplied.
|
|
139
|
+
* Every other choice is resolved exactly like `createNonInteractive` — flags,
|
|
140
|
+
* with defaults, never a prompt.
|
|
145
141
|
*/
|
|
146
142
|
async function createDefaultInteractive(cli, pmDetectionPromise) {
|
|
147
143
|
const appName = cli.name ?? await askProjectName();
|
|
@@ -160,11 +156,21 @@ async function createDefaultInteractive(cli, pmDetectionPromise) {
|
|
|
160
156
|
const packageManager = cli.pm ?? getPreferredPackageManager();
|
|
161
157
|
assertPackageManagerAvailable(cli.pm);
|
|
162
158
|
setPackageManager(packageManager);
|
|
159
|
+
const databaseDriver = cli.db ?? await select({
|
|
160
|
+
message: "Choose your database driver",
|
|
161
|
+
options: getDatabaseDriverOptions(),
|
|
162
|
+
initialValue: "mongodb"
|
|
163
|
+
});
|
|
164
|
+
if (isCancel(databaseDriver)) {
|
|
165
|
+
cancel("Database selection cancelled");
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
163
168
|
let options;
|
|
164
169
|
try {
|
|
165
170
|
options = await resolveAppOptions({
|
|
166
171
|
...cli,
|
|
167
|
-
stack
|
|
172
|
+
stack,
|
|
173
|
+
db: databaseDriver
|
|
168
174
|
});
|
|
169
175
|
} catch (error) {
|
|
170
176
|
cancel(error.message);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../create-warlock/src/commands/create-new-app/index.ts"],"sourcesContent":["import { cancel, confirm, isCancel, multiselect, select } from \"@clack/prompts\";\nimport { colors } from \"@mongez/copper\";\nimport { getJsonFile } from \"@warlock.js/fs\";\nimport { resolveAgentTargets } from \"../../features/agent-kit-targets\";\nimport {\n getDatabaseDriver,\n getDatabaseDriverOptions,\n isNoDatabase,\n} from \"../../features/database-drivers\";\nimport {\n getAiPackageOptions,\n getAiProviderOptions,\n getAllFeatureKeys,\n getFeatureOptions,\n} from \"../../features/features-map\";\nimport { assertFlagCombinations } from \"../../flags/assert-flag-combinations\";\nimport { assertPackageManagerAvailable } from \"../../flags/assert-package-manager\";\nimport { seedFromFlags } from \"../../flags/seed-from-flags\";\nimport { App } from \"../../helpers/app\";\nimport {\n detectPackageManagers,\n getPackageManager,\n getPreferredPackageManager,\n getSystemPackageManagers,\n setPackageManager,\n} from \"../../helpers/package-manager\";\nimport { packageRoot } from \"../../helpers/paths\";\nimport { hasInteractiveStdin } from \"../../helpers/tty\";\nimport { askAgentTargets } from \"../../prompts/ask-agent-targets\";\nimport { askProjectName } from \"../../prompts/ask-project-name\";\nimport { askStack } from \"../../prompts/ask-stack\";\nimport { showIntroBanner } from \"../../ui/banner\";\nimport { createWarlockApp } from \"../create-warlock-app\";\nimport getAppPath from \"./get-app-path\";\nimport {\n AppOptions,\n App as AppType,\n CliFlags,\n SetupChoice,\n Stack,\n} from \"./types\";\n\nexport default async function createNewApp(cli: CliFlags = {}) {\n // Start detecting package managers in the background to avoid delay later\n const pmDetectionPromise = detectPackageManagers();\n\n // Get version from package.json\n const packageJson: any = getJsonFile(packageRoot(\"package.json\"));\n const createWarlockVersion = packageJson.version;\n\n // Show the intro banner\n showIntroBanner(createWarlockVersion);\n\n console.log(colors.cyan(\"Let's create something magical! \\n\"));\n\n // Validate Node.js version (minimum v20)\n const [major] = process.versions.node.split(\".\").map(Number);\n if (major < 20) {\n cancel(\"Node.js version must be at least 20.0.0\");\n process.exit(0);\n }\n\n // Refuse contradictory flag combinations LOUDLY, before anything else looks\n // at them — `--customize --yes` used to drop `--customize` without a word.\n assertFlagCombinations(cli);\n\n // Non-interactive path: build everything from flags and skip the prompts.\n //\n // Triggered by --yes, OR automatically the moment stdin has no TTY — CI, a\n // script, an agent harness. This check MUST run before the first prompt\n // call: attempting `text()`/`select()` on a non-TTY stdin does not fail\n // cleanly, it crashes with a libuv internal (\"TTY initialization failed:\n // uv_tty_init returned EBADF\") before any app directory exists, no matter\n // how the run was invoked.\n //\n // Prompting is the fallback, not the precondition: a project name is the\n // only answer with no sane default, so when a name is already available\n // (positional arg or --name) the run proceeds exactly as --yes would, TTY\n // or not. When the name is ALSO missing and there is no TTY to ask for one,\n // createNonInteractive fails loudly — naming --yes and every non-interactive\n // flag — instead of ever reaching a prompt. Every other prompt already has a\n // default; see resolveNonInteractiveOptions below for the full table.\n if (cli.yes || !hasInteractiveStdin()) {\n await createNonInteractive(cli);\n return;\n }\n\n // `--interactive` (alias `--customize`) restores the full long-form wizard\n // below. Everyone else with a TTY falls through to the default path, which\n // asks at most ONE structural question — see createDefaultInteractive.\n if (cli.interactive) {\n await createFullWizard(cli, pmDetectionPromise);\n return;\n }\n\n await createDefaultInteractive(cli, pmDetectionPromise);\n}\n\n/**\n * The full long-form wizard (`--interactive` / `--customize`): every prompt,\n * one after another. This is the ORIGINAL default flow, preserved verbatim —\n * only its trigger moved from \"any TTY\" to an explicit opt-in flag, per the\n * rule that a reversible choice never prompts by default.\n */\nasync function createFullWizard(\n cli: CliFlags,\n pmDetectionPromise: Promise<void>,\n) {\n // Step 1: Project name — but only when it is not already known. It arrives\n // answered from `--name`/the positional arg, and from the default path when\n // the user picked \"Customize\" AFTER being asked for a name. Re-asking for an\n // answer we already hold is the double-prompt this branch exists to avoid.\n const appName = cli.name ?? (await askProjectName());\n\n const appPath = getAppPath(appName);\n if (!appPath) return;\n\n // A flag passed alongside --customize is an answer given early, not a\n // contradiction: it pre-selects the matching prompt instead of being\n // dropped. Validate the ones that CAN be wrong through the same function\n // the non-interactive path uses, so an unknown driver or feature key fails\n // here rather than showing a prompt that quietly ignores it.\n try {\n resolveNonInteractiveOptions(cli);\n } catch (error) {\n cancel((error as Error).message);\n process.exit(1);\n }\n\n const seeds = seedFromFlags(cli);\n\n // Step 2: Package Manager selection\n await pmDetectionPromise; // Ensure detection is complete\n\n // Only now can availability be judged: detection is what fills\n // getSystemPackageManagers(), and the prompt below is built from it.\n assertPackageManagerAvailable(cli.pm);\n\n const packageManager = await select({\n message: \"Which package manager do you want to use?\",\n options: getSystemPackageManagers().map(pm => ({\n value: pm,\n label: pm,\n })),\n initialValue: seeds.packageManager,\n });\n\n if (isCancel(packageManager)) {\n cancel(\"Package manager selection cancelled\");\n process.exit(0);\n }\n\n setPackageManager(packageManager as string);\n\n // Step 3: Database driver selection\n const databaseDriver = await select({\n message: \"Choose your database driver\",\n options: getDatabaseDriverOptions(),\n initialValue: seeds.databaseDriver,\n });\n\n if (isCancel(databaseDriver)) {\n cancel(\"Database selection cancelled\");\n process.exit(0);\n }\n\n const selectedDriver = getDatabaseDriver(databaseDriver as string);\n\n // Step 4: Features selection\n const selectedFeatures = await multiselect({\n message: \"Select optional features to include\",\n options: getFeatureOptions(),\n initialValues: seeds.features,\n required: false,\n });\n\n if (isCancel(selectedFeatures)) {\n cancel(\"Feature selection cancelled\");\n process.exit(0);\n }\n\n // Step 4b: AI providers + capability packages — selecting any pulls\n // @warlock.js/ai automatically. The result still flows through the\n // `selectedAiProviders` → `aiProviders` pipeline, which now also carries the\n // capability packages (ai-tools / ai-panoptic / ai-workspace).\n const selectedAiProviders = await multiselect({\n message:\n \"Add AI packages? Providers + capabilities — the core AI package is included automatically\",\n options: [...getAiProviderOptions(), ...getAiPackageOptions()],\n initialValues: seeds.aiProviders,\n required: false,\n });\n\n if (isCancel(selectedAiProviders)) {\n cancel(\"AI provider selection cancelled\");\n process.exit(0);\n }\n\n // Step 5: Git initialization\n const useGit =\n (await confirm({\n message: \"Initialize a Git repository?\",\n initialValue: seeds.useGit,\n })) === true;\n\n if (isCancel(useGit)) {\n cancel(\"Setup cancelled\");\n process.exit(0);\n }\n\n // Step 6: JWT secret generation\n const useJWT =\n (await confirm({\n message: \"Generate JWT secret keys?\",\n initialValue: seeds.useJWT,\n })) === true;\n\n if (isCancel(useJWT)) {\n cancel(\"Setup cancelled\");\n process.exit(0);\n }\n\n // Step 7: agent-kit targets. Asked here rather than left to --agents,\n // because Customize means every choice is offered (owner ruling, seq 1028).\n // --agents pre-ticks the selection, like every other flag.\n const agents = await askAgentTargets(cli.agents);\n\n // Build app details\n const appDetails: Required<AppType> = {\n appName: appName,\n appType: \"warlock\",\n appPath: appPath,\n pkgManager: getPackageManager(),\n options: {\n databaseDriver: databaseDriver as string,\n databasePort: selectedDriver?.defaultPort || 27017,\n features: selectedFeatures as string[],\n aiProviders: selectedAiProviders as string[],\n useGit,\n useJWT,\n agents,\n },\n };\n\n // Create the app\n await scaffold(appDetails);\n}\n\n/**\n * The default TTY path: neither `--yes` nor `--interactive` was given, but a\n * terminal is available. Asks the project name when missing (a required\n * answer with no sane default — same treatment as the non-interactive path)\n * plus the ONE structural question (API-only vs full-stack web) when `--stack`\n * was not already supplied. Every other choice is resolved exactly like\n * `createNonInteractive` — flags, with defaults, never a prompt.\n */\nasync function createDefaultInteractive(\n cli: CliFlags,\n pmDetectionPromise: Promise<void>,\n) {\n const appName = cli.name ?? (await askProjectName());\n\n const appPath = getAppPath(appName);\n if (!appPath) return;\n\n const choice: SetupChoice = cli.stack ?? (await askStack());\n\n // \"Customize\" is not a third stack — it is the wizard, chosen from the menu\n // instead of from a flag nobody was told about. Hand over the name we just\n // collected so the wizard starts at its SECOND question.\n if (choice === \"customize\") {\n await createFullWizard({ ...cli, name: appName }, pmDetectionPromise);\n return;\n }\n\n const stack: Stack = choice;\n\n await pmDetectionPromise;\n\n const packageManager = cli.pm ?? getPreferredPackageManager();\n\n assertPackageManagerAvailable(cli.pm);\n\n setPackageManager(packageManager);\n\n let options: AppOptions;\n\n try {\n options = await resolveAppOptions({ ...cli, stack });\n } catch (error) {\n cancel((error as Error).message);\n process.exit(1);\n }\n\n const appDetails: Required<AppType> = {\n appName,\n appType: \"warlock\",\n appPath,\n pkgManager: getPackageManager(),\n options,\n };\n\n await scaffold(appDetails);\n}\n\n/**\n * Run the scaffold and make the process's exit code tell the truth: anything\n * the user asked for that did not happen leaves the shell with a non-zero\n * status, so scripts and CI cannot mistake a half-built project for a finished\n * one. The problems themselves are printed by the scaffolder.\n */\nasync function scaffold(appDetails: Required<AppType>) {\n const outcome = await createWarlockApp(new App(appDetails));\n\n if (outcome && !outcome.ok) {\n process.exitCode = 1;\n }\n}\n\n/** Everything {@link resolveNonInteractiveOptions} can resolve without the network. */\nexport type SyncAppOptions = Omit<AppOptions, \"agents\">;\n\n/**\n * Resolve every flag-or-default `AppOptions` field EXCEPT `agents` from parsed\n * CLI flags — applying the non-interactive defaults, validating the database\n * driver and feature/provider keys, and handling the `none` (no database)\n * selection. `agents` is resolved separately by {@link resolveAppOptions}\n * because it can require a network fetch; this function stays synchronous so\n * the rest of the flag → app mapping remains trivially unit-testable.\n *\n * `--stack=web` (or the equivalent wizard answer) seeds the `web` feature by\n * default — the one structural choice — but an explicit `--features` always\n * wins, so a user who spells out the feature list is never second-guessed.\n *\n * Throws on an unknown driver, feature, or AI key so the caller can fail fast\n * before any file is written.\n */\nexport function resolveNonInteractiveOptions(cli: CliFlags): SyncAppOptions {\n const databaseDriver = cli.db ?? \"mongodb\";\n const noDatabase = isNoDatabase(databaseDriver);\n const driver = noDatabase ? undefined : getDatabaseDriver(databaseDriver);\n\n if (!noDatabase && !driver) {\n throw new Error(`Unknown database driver \"${databaseDriver}\"`);\n }\n\n const stack: Stack = cli.stack ?? \"api\";\n const features = cli.features ?? (stack === \"web\" ? [\"web\"] : []);\n const aiProviders = cli.ai ?? [];\n\n const allowedKeys = getAllFeatureKeys();\n const invalidFeatureKeys = features.filter(key => !allowedKeys.includes(key));\n const errors: string[] = [];\n\n if (invalidFeatureKeys.length > 0) {\n errors.push(`Unknown feature(s): ${invalidFeatureKeys.join(\",\")}`);\n }\n\n const aiFeatureKeys = [\n ...getAiProviderOptions(),\n ...getAiPackageOptions(),\n ].map(option => option.value);\n const invalidAiKeys = aiProviders.filter(key => !aiFeatureKeys.includes(key));\n\n if (invalidAiKeys.length > 0) {\n errors.push(\n `Unknown AI feature(s): ${invalidAiKeys.join(\",\")}. ` +\n `Valid AI choices: ${aiFeatureKeys.join(\", \")}`,\n );\n }\n\n if (errors.length > 0) {\n throw new Error(errors.join(\"\\n\"));\n }\n\n return {\n databaseDriver,\n databasePort: driver?.defaultPort ?? 27017,\n features,\n aiProviders,\n useGit: cli.git ?? false,\n useJWT: cli.jwt ?? false,\n };\n}\n\n/**\n * Resolve the FULL `AppOptions` — {@link resolveNonInteractiveOptions} plus\n * the validated `agents` list. Async because a non-default `--agents`\n * selection may need {@link resolveAgentTargets}'s network fetch; the default\n * (`claude`) never does.\n */\nexport async function resolveAppOptions(cli: CliFlags): Promise<AppOptions> {\n const options = resolveNonInteractiveOptions(cli);\n const agents = await resolveAgentTargets(cli.agents);\n\n return { ...options, agents };\n}\n\n/**\n * Non-interactive scaffold path for `--yes`. Builds the app from flags with\n * sensible defaults and skips every prompt — a single command scaffolds the\n * entire app. Validation lives in `resolveNonInteractiveOptions`; a typo fails\n * fast here instead of mid-install.\n */\nasync function createNonInteractive(cli: CliFlags) {\n const appName = (cli.name ?? \"\").trim();\n\n if (!appName) {\n // Reached either because --yes was passed with no name, or because stdin\n // has no TTY to prompt for one — either way, nothing is left to fall back\n // to. Name --yes AND every non-interactive flag so a CI log is enough to\n // fix the invocation without hunting for docs.\n cancel(\n \"A project name is required and no terminal is available to ask for one. \" +\n \"Pass it as the first argument or --name=<name>. Non-interactive flags: \" +\n \"--yes, --pm=<npm|yarn|pnpm|bun>, --db=<driver>|--no-db, --features=<list>, \" +\n \"--ai=<list>, --git|--no-git, --jwt|--no-jwt.\",\n );\n process.exit(1);\n }\n\n const appPath = getAppPath(appName);\n\n if (!appPath) return;\n\n await detectPackageManagers();\n\n const packageManager = cli.pm ?? getPreferredPackageManager();\n\n assertPackageManagerAvailable(cli.pm);\n\n setPackageManager(packageManager);\n\n let options: AppOptions;\n\n try {\n options = await resolveAppOptions(cli);\n } catch (error) {\n cancel((error as Error).message);\n process.exit(1);\n }\n\n const appDetails: Required<AppType> = {\n appName,\n appType: \"warlock\",\n appPath,\n pkgManager: getPackageManager(),\n options,\n };\n\n await scaffold(appDetails);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../create-warlock/src/commands/create-new-app/index.ts"],"sourcesContent":["import { cancel, confirm, isCancel, multiselect, select } from \"@clack/prompts\";\nimport { colors } from \"@mongez/copper\";\nimport { getJsonFile } from \"@warlock.js/fs\";\nimport { resolveAgentTargets } from \"../../features/agent-kit-targets\";\nimport {\n getDatabaseDriver,\n getDatabaseDriverOptions,\n isNoDatabase,\n} from \"../../features/database-drivers\";\nimport {\n getAiPackageOptions,\n getAiProviderOptions,\n getAllFeatureKeys,\n getFeatureOptions,\n} from \"../../features/features-map\";\nimport { assertFlagCombinations } from \"../../flags/assert-flag-combinations\";\nimport { assertPackageManagerAvailable } from \"../../flags/assert-package-manager\";\nimport { seedFromFlags } from \"../../flags/seed-from-flags\";\nimport { App } from \"../../helpers/app\";\nimport {\n detectPackageManagers,\n getPackageManager,\n getPreferredPackageManager,\n getSystemPackageManagers,\n setPackageManager,\n} from \"../../helpers/package-manager\";\nimport { packageRoot } from \"../../helpers/paths\";\nimport { hasInteractiveStdin } from \"../../helpers/tty\";\nimport { askAgentTargets } from \"../../prompts/ask-agent-targets\";\nimport { askAiFeatures } from \"../../prompts/ask-ai-features\";\nimport { askProjectName } from \"../../prompts/ask-project-name\";\nimport { askStack } from \"../../prompts/ask-stack\";\nimport { showIntroBanner } from \"../../ui/banner\";\nimport { createWarlockApp } from \"../create-warlock-app\";\nimport getAppPath from \"./get-app-path\";\nimport {\n AppOptions,\n App as AppType,\n CliFlags,\n SetupChoice,\n Stack,\n} from \"./types\";\n\nexport default async function createNewApp(cli: CliFlags = {}) {\n // Start detecting package managers in the background to avoid delay later\n const pmDetectionPromise = detectPackageManagers();\n\n // Get version from package.json\n const packageJson: any = getJsonFile(packageRoot(\"package.json\"));\n const createWarlockVersion = packageJson.version;\n\n // Show the intro banner\n showIntroBanner(createWarlockVersion);\n\n console.log(colors.cyan(\"Let's create something magical! \\n\"));\n\n // Validate Node.js version (minimum v20)\n const [major] = process.versions.node.split(\".\").map(Number);\n if (major < 20) {\n cancel(\"Node.js version must be at least 20.0.0\");\n process.exit(0);\n }\n\n // Refuse contradictory flag combinations LOUDLY, before anything else looks\n // at them — `--customize --yes` used to drop `--customize` without a word.\n assertFlagCombinations(cli);\n\n // Non-interactive path: build everything from flags and skip the prompts.\n //\n // Triggered by --yes, OR automatically the moment stdin has no TTY — CI, a\n // script, an agent harness. This check MUST run before the first prompt\n // call: attempting `text()`/`select()` on a non-TTY stdin does not fail\n // cleanly, it crashes with a libuv internal (\"TTY initialization failed:\n // uv_tty_init returned EBADF\") before any app directory exists, no matter\n // how the run was invoked.\n //\n // Prompting is the fallback, not the precondition: a project name is the\n // only answer with no sane default, so when a name is already available\n // (positional arg or --name) the run proceeds exactly as --yes would, TTY\n // or not. When the name is ALSO missing and there is no TTY to ask for one,\n // createNonInteractive fails loudly — naming --yes and every non-interactive\n // flag — instead of ever reaching a prompt. Every other prompt already has a\n // default; see resolveNonInteractiveOptions below for the full table.\n if (cli.yes || !hasInteractiveStdin()) {\n await createNonInteractive(cli);\n return;\n }\n\n // `--interactive` (alias `--customize`) restores the full long-form wizard\n // below. Everyone else with a TTY falls through to the default path, which\n // asks the preset plus database choice — see createDefaultInteractive.\n if (cli.interactive) {\n await createFullWizard(cli, pmDetectionPromise);\n return;\n }\n\n await createDefaultInteractive(cli, pmDetectionPromise);\n}\n\n/**\n * The full long-form wizard (`--interactive` / `--customize`): every prompt,\n * one after another. This is the ORIGINAL default flow, preserved verbatim —\n * only its trigger moved from \"any TTY\" to an explicit opt-in flag, per the\n * rule that a reversible choice never prompts by default.\n */\nasync function createFullWizard(\n cli: CliFlags,\n pmDetectionPromise: Promise<void>,\n) {\n // Step 1: Project name — but only when it is not already known. It arrives\n // answered from `--name`/the positional arg, and from the default path when\n // the user picked \"Customize\" AFTER being asked for a name. Re-asking for an\n // answer we already hold is the double-prompt this branch exists to avoid.\n const appName = cli.name ?? (await askProjectName());\n\n const appPath = getAppPath(appName);\n if (!appPath) return;\n\n // A flag passed alongside --customize is an answer given early, not a\n // contradiction: it pre-selects the matching prompt instead of being\n // dropped. Validate the ones that CAN be wrong through the same function\n // the non-interactive path uses, so an unknown driver or feature key fails\n // here rather than showing a prompt that quietly ignores it.\n try {\n resolveNonInteractiveOptions(cli);\n } catch (error) {\n cancel((error as Error).message);\n process.exit(1);\n }\n\n const seeds = seedFromFlags(cli);\n\n // Step 2: Package Manager selection\n await pmDetectionPromise; // Ensure detection is complete\n\n // Only now can availability be judged: detection is what fills\n // getSystemPackageManagers(), and the prompt below is built from it.\n assertPackageManagerAvailable(cli.pm);\n\n const packageManager = await select({\n message: \"Which package manager do you want to use?\",\n options: getSystemPackageManagers().map(pm => ({\n value: pm,\n label: pm,\n })),\n initialValue: seeds.packageManager,\n });\n\n if (isCancel(packageManager)) {\n cancel(\"Package manager selection cancelled\");\n process.exit(0);\n }\n\n setPackageManager(packageManager as string);\n\n // Step 3: Database driver selection\n const databaseDriver = await select({\n message: \"Choose your database driver\",\n options: getDatabaseDriverOptions(),\n initialValue: seeds.databaseDriver,\n });\n\n if (isCancel(databaseDriver)) {\n cancel(\"Database selection cancelled\");\n process.exit(0);\n }\n\n const selectedDriver = getDatabaseDriver(databaseDriver as string);\n\n // Step 4: Features selection\n const selectedFeatures = await multiselect({\n message: \"Select optional features to include\",\n options: getFeatureOptions(),\n initialValues: seeds.features,\n required: false,\n });\n\n if (isCancel(selectedFeatures)) {\n cancel(\"Feature selection cancelled\");\n process.exit(0);\n }\n\n // Step 4b: AI providers + capability packages — selecting any pulls\n // @warlock.js/ai automatically. The result still flows through the\n // `selectedAiProviders` → `aiProviders` pipeline, which now also carries the\n // capability packages (ai-tools / ai-panoptic / ai-workspace).\n const selectedAiProviders = await askAiFeatures(seeds.aiProviders);\n\n if (isCancel(selectedAiProviders)) {\n cancel(\"AI provider selection cancelled\");\n process.exit(0);\n }\n\n // Step 5: Git initialization\n const useGit =\n (await confirm({\n message: \"Initialize a Git repository?\",\n initialValue: seeds.useGit,\n })) === true;\n\n if (isCancel(useGit)) {\n cancel(\"Setup cancelled\");\n process.exit(0);\n }\n\n // Step 6: JWT secret generation\n const useJWT =\n (await confirm({\n message: \"Generate JWT secret keys?\",\n initialValue: seeds.useJWT,\n })) === true;\n\n if (isCancel(useJWT)) {\n cancel(\"Setup cancelled\");\n process.exit(0);\n }\n\n // Step 7: agent-kit targets. Asked here rather than left to --agents,\n // because Customize means every choice is offered (owner ruling, seq 1028).\n // --agents pre-ticks the selection, like every other flag.\n const agents = await askAgentTargets(cli.agents);\n\n // Build app details\n const appDetails: Required<AppType> = {\n appName: appName,\n appType: \"warlock\",\n appPath: appPath,\n pkgManager: getPackageManager(),\n options: {\n databaseDriver: databaseDriver as string,\n databasePort: selectedDriver?.defaultPort || 27017,\n features: selectedFeatures as string[],\n aiProviders: selectedAiProviders as string[],\n useGit,\n useJWT,\n agents,\n },\n };\n\n // Create the app\n await scaffold(appDetails);\n}\n\n/**\n * The default TTY path: neither `--yes` nor `--interactive` was given, but a\n * terminal is available. Asks the project name when missing (a required\n * answer with no sane default — same treatment as the non-interactive path)\n * plus the preset's database choice when `--db`/`--no-db` was not supplied.\n * Every other choice is resolved exactly like `createNonInteractive` — flags,\n * with defaults, never a prompt.\n */\nasync function createDefaultInteractive(\n cli: CliFlags,\n pmDetectionPromise: Promise<void>,\n) {\n const appName = cli.name ?? (await askProjectName());\n\n const appPath = getAppPath(appName);\n if (!appPath) return;\n\n const choice: SetupChoice = cli.stack ?? (await askStack());\n\n // \"Customize\" is not a third stack — it is the wizard, chosen from the menu\n // instead of from a flag nobody was told about. Hand over the name we just\n // collected so the wizard starts at its SECOND question.\n if (choice === \"customize\") {\n await createFullWizard({ ...cli, name: appName }, pmDetectionPromise);\n return;\n }\n\n const stack: Stack = choice;\n\n await pmDetectionPromise;\n\n const packageManager = cli.pm ?? getPreferredPackageManager();\n\n assertPackageManagerAvailable(cli.pm);\n\n setPackageManager(packageManager);\n\n const databaseDriver =\n cli.db ??\n (await select({\n message: \"Choose your database driver\",\n options: getDatabaseDriverOptions(),\n initialValue: \"mongodb\",\n }));\n\n if (isCancel(databaseDriver)) {\n cancel(\"Database selection cancelled\");\n process.exit(0);\n }\n\n let options: AppOptions;\n\n try {\n options = await resolveAppOptions({ ...cli, stack, db: databaseDriver });\n } catch (error) {\n cancel((error as Error).message);\n process.exit(1);\n }\n\n const appDetails: Required<AppType> = {\n appName,\n appType: \"warlock\",\n appPath,\n pkgManager: getPackageManager(),\n options,\n };\n\n await scaffold(appDetails);\n}\n\n/**\n * Run the scaffold and make the process's exit code tell the truth: anything\n * the user asked for that did not happen leaves the shell with a non-zero\n * status, so scripts and CI cannot mistake a half-built project for a finished\n * one. The problems themselves are printed by the scaffolder.\n */\nasync function scaffold(appDetails: Required<AppType>) {\n const outcome = await createWarlockApp(new App(appDetails));\n\n if (outcome && !outcome.ok) {\n process.exitCode = 1;\n }\n}\n\n/** Everything {@link resolveNonInteractiveOptions} can resolve without the network. */\nexport type SyncAppOptions = Omit<AppOptions, \"agents\">;\n\n/**\n * Resolve every flag-or-default `AppOptions` field EXCEPT `agents` from parsed\n * CLI flags — applying the non-interactive defaults, validating the database\n * driver and feature/provider keys, and handling the `none` (no database)\n * selection. `agents` is resolved separately by {@link resolveAppOptions}\n * because it can require a network fetch; this function stays synchronous so\n * the rest of the flag → app mapping remains trivially unit-testable.\n *\n * `--stack=web` (or the equivalent wizard answer) seeds the `web` feature by\n * default — the one structural choice — but an explicit `--features` always\n * wins, so a user who spells out the feature list is never second-guessed.\n *\n * Throws on an unknown driver, feature, or AI key so the caller can fail fast\n * before any file is written.\n */\nexport function resolveNonInteractiveOptions(cli: CliFlags): SyncAppOptions {\n const databaseDriver = cli.db ?? \"mongodb\";\n const noDatabase = isNoDatabase(databaseDriver);\n const driver = noDatabase ? undefined : getDatabaseDriver(databaseDriver);\n\n if (!noDatabase && !driver) {\n throw new Error(`Unknown database driver \"${databaseDriver}\"`);\n }\n\n const stack: Stack = cli.stack ?? \"api\";\n const features = cli.features ?? (stack === \"web\" ? [\"web\"] : []);\n const aiProviders = cli.ai ?? [];\n\n const allowedKeys = getAllFeatureKeys();\n const invalidFeatureKeys = features.filter(key => !allowedKeys.includes(key));\n const errors: string[] = [];\n\n if (invalidFeatureKeys.length > 0) {\n errors.push(`Unknown feature(s): ${invalidFeatureKeys.join(\",\")}`);\n }\n\n const aiFeatureKeys = [\n ...getAiProviderOptions(),\n ...getAiPackageOptions(),\n ].map(option => option.value);\n const invalidAiKeys = aiProviders.filter(key => !aiFeatureKeys.includes(key));\n\n if (invalidAiKeys.length > 0) {\n errors.push(\n `Unknown AI feature(s): ${invalidAiKeys.join(\",\")}. ` +\n `Valid AI choices: ${aiFeatureKeys.join(\", \")}`,\n );\n }\n\n if (errors.length > 0) {\n throw new Error(errors.join(\"\\n\"));\n }\n\n return {\n databaseDriver,\n databasePort: driver?.defaultPort ?? 27017,\n features,\n aiProviders,\n useGit: cli.git ?? false,\n useJWT: cli.jwt ?? false,\n };\n}\n\n/**\n * Resolve the FULL `AppOptions` — {@link resolveNonInteractiveOptions} plus\n * the validated `agents` list. Async because a non-default `--agents`\n * selection may need {@link resolveAgentTargets}'s network fetch; the default\n * (`claude`) never does.\n */\nexport async function resolveAppOptions(cli: CliFlags): Promise<AppOptions> {\n const options = resolveNonInteractiveOptions(cli);\n const agents = await resolveAgentTargets(cli.agents);\n\n return { ...options, agents };\n}\n\n/**\n * Non-interactive scaffold path for `--yes`. Builds the app from flags with\n * sensible defaults and skips every prompt — a single command scaffolds the\n * entire app. Validation lives in `resolveNonInteractiveOptions`; a typo fails\n * fast here instead of mid-install.\n */\nasync function createNonInteractive(cli: CliFlags) {\n const appName = (cli.name ?? \"\").trim();\n\n if (!appName) {\n // Reached either because --yes was passed with no name, or because stdin\n // has no TTY to prompt for one — either way, nothing is left to fall back\n // to. Name --yes AND every non-interactive flag so a CI log is enough to\n // fix the invocation without hunting for docs.\n cancel(\n \"A project name is required and no terminal is available to ask for one. \" +\n \"Pass it as the first argument or --name=<name>. Non-interactive flags: \" +\n \"--yes, --pm=<npm|yarn|pnpm|bun>, --db=<driver>|--no-db, --features=<list>, \" +\n \"--ai=<list>, --git|--no-git, --jwt|--no-jwt.\",\n );\n process.exit(1);\n }\n\n const appPath = getAppPath(appName);\n\n if (!appPath) return;\n\n await detectPackageManagers();\n\n const packageManager = cli.pm ?? getPreferredPackageManager();\n\n assertPackageManagerAvailable(cli.pm);\n\n setPackageManager(packageManager);\n\n let options: AppOptions;\n\n try {\n options = await resolveAppOptions(cli);\n } catch (error) {\n cancel((error as Error).message);\n process.exit(1);\n }\n\n const appDetails: Required<AppType> = {\n appName,\n appType: \"warlock\",\n appPath,\n pkgManager: getPackageManager(),\n options,\n };\n\n await scaffold(appDetails);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getAiPackageOptions, getAiProviderOptions } from "../features/features-map.mjs";
|
|
2
|
+
import { isCancel, log, multiselect } from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
//#region ../create-warlock/src/prompts/ask-ai-features.ts
|
|
5
|
+
const NO_AI = "none";
|
|
6
|
+
const aiOptions = [
|
|
7
|
+
...getAiProviderOptions(),
|
|
8
|
+
...getAiPackageOptions(),
|
|
9
|
+
{
|
|
10
|
+
value: NO_AI,
|
|
11
|
+
label: "None",
|
|
12
|
+
hint: "Do not add AI providers or capability packages"
|
|
13
|
+
}
|
|
14
|
+
];
|
|
15
|
+
/** Ask for optional AI packages without leaking the UI-only None sentinel. */
|
|
16
|
+
async function askAiFeatures(initialValues = []) {
|
|
17
|
+
let selectedValues = initialValues.filter((value) => value !== NO_AI);
|
|
18
|
+
while (true) {
|
|
19
|
+
const answer = await multiselect({
|
|
20
|
+
message: "Add AI packages? Providers + capabilities — the core AI package is included automatically",
|
|
21
|
+
options: aiOptions,
|
|
22
|
+
initialValues: selectedValues,
|
|
23
|
+
required: false
|
|
24
|
+
});
|
|
25
|
+
if (isCancel(answer)) return answer;
|
|
26
|
+
const selected = answer;
|
|
27
|
+
if (!selected.includes(NO_AI)) return selected;
|
|
28
|
+
if (selected.length === 1) return [];
|
|
29
|
+
log.warn("Choose None by itself, or select one or more AI packages.");
|
|
30
|
+
selectedValues = selected.filter((value) => value !== NO_AI);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { askAiFeatures };
|
|
36
|
+
//# sourceMappingURL=ask-ai-features.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask-ai-features.mjs","names":[],"sources":["../../../../../../create-warlock/src/prompts/ask-ai-features.ts"],"sourcesContent":["import { isCancel, log, multiselect } from \"@clack/prompts\";\nimport {\n getAiPackageOptions,\n getAiProviderOptions,\n} from \"../features/features-map\";\n\nconst NO_AI = \"none\";\n\nconst aiOptions = [\n ...getAiProviderOptions(),\n ...getAiPackageOptions(),\n {\n value: NO_AI,\n label: \"None\",\n hint: \"Do not add AI providers or capability packages\",\n },\n];\n\n/** Ask for optional AI packages without leaking the UI-only None sentinel. */\nexport async function askAiFeatures(\n initialValues: string[] = [],\n): Promise<string[] | symbol> {\n let selectedValues = initialValues.filter(value => value !== NO_AI);\n\n while (true) {\n const answer = await multiselect({\n message:\n \"Add AI packages? Providers + capabilities — the core AI package is included automatically\",\n options: aiOptions,\n initialValues: selectedValues,\n required: false,\n });\n\n if (isCancel(answer)) return answer;\n\n const selected = answer as string[];\n\n if (!selected.includes(NO_AI)) return selected;\n\n if (selected.length === 1) return [];\n\n log.warn(\"Choose None by itself, or select one or more AI packages.\");\n\n selectedValues = selected.filter(value => value !== NO_AI);\n }\n}\n"],"mappings":";;;;AAMA,MAAM,QAAQ;AAEd,MAAM,YAAY;CAChB,GAAG,qBAAqB;CACxB,GAAG,oBAAoB;CACvB;EACE,OAAO;EACP,OAAO;EACP,MAAM;CACR;AACF;;AAGA,eAAsB,cACpB,gBAA0B,CAAC,GACC;CAC5B,IAAI,iBAAiB,cAAc,QAAO,UAAS,UAAU,KAAK;CAElE,OAAO,MAAM;EACX,MAAM,SAAS,MAAM,YAAY;GAC/B,SACE;GACF,SAAS;GACT,eAAe;GACf,UAAU;EACZ,CAAC;EAED,IAAI,SAAS,MAAM,GAAG,OAAO;EAE7B,MAAM,WAAW;EAEjB,IAAI,CAAC,SAAS,SAAS,KAAK,GAAG,OAAO;EAEtC,IAAI,SAAS,WAAW,GAAG,OAAO,CAAC;EAEnC,IAAI,KAAK,2DAA2D;EAEpE,iBAAiB,SAAS,QAAO,UAAS,UAAU,KAAK;CAC3D;AACF"}
|
|
@@ -2,12 +2,10 @@ import { cancel, isCancel, select } from "@clack/prompts";
|
|
|
2
2
|
|
|
3
3
|
//#region ../create-warlock/src/prompts/ask-stack.ts
|
|
4
4
|
/**
|
|
5
|
-
* The
|
|
6
|
-
* path
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* package manager, agent-kit targets, …) is reversible within minutes and
|
|
10
|
-
* ships as a flag with a default instead.
|
|
5
|
+
* The structural question in the default (non-`--interactive`) interactive
|
|
6
|
+
* path: does the generated app need server-rendered web pages, or is it an
|
|
7
|
+
* API only? The following database prompt is shared by both presets; the
|
|
8
|
+
* remaining choices stay behind flags or the Customize wizard.
|
|
11
9
|
*
|
|
12
10
|
* The third entry is not a third stack: `customize` selects the FLOW, handing
|
|
13
11
|
* the run to the full wizard (`--customize`) so every one of those reversible
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask-stack.mjs","names":[],"sources":["../../../../../../create-warlock/src/prompts/ask-stack.ts"],"sourcesContent":["import { cancel, isCancel, select } from \"@clack/prompts\";\nimport type { SetupChoice } from \"../commands/create-new-app/types\";\n\n/**\n * The
|
|
1
|
+
{"version":3,"file":"ask-stack.mjs","names":[],"sources":["../../../../../../create-warlock/src/prompts/ask-stack.ts"],"sourcesContent":["import { cancel, isCancel, select } from \"@clack/prompts\";\nimport type { SetupChoice } from \"../commands/create-new-app/types\";\n\n/**\n * The structural question in the default (non-`--interactive`) interactive\n * path: does the generated app need server-rendered web pages, or is it an\n * API only? The following database prompt is shared by both presets; the\n * remaining choices stay behind flags or the Customize wizard.\n *\n * The third entry is not a third stack: `customize` selects the FLOW, handing\n * the run to the full wizard (`--customize`) so every one of those reversible\n * choices gets asked instead of defaulted. It sits in this menu because a flag\n * nobody is told about is a feature nobody has.\n */\nexport async function askStack(): Promise<SetupChoice> {\n const answer = await select({\n message: \"What are we building?\",\n options: [\n {\n value: \"api\",\n label: \"API only\",\n hint: \"A REST API, no server-rendered pages\",\n },\n {\n value: \"web\",\n label: \"Full-stack web\",\n hint: \"API + server-rendered React pages (SSR)\",\n },\n {\n value: \"customize\",\n label: \"Customize\",\n hint: \"Pick package manager, database, features, AI providers and coding agents one by one\",\n },\n ],\n initialValue: \"api\",\n });\n\n if (isCancel(answer)) {\n cancel(\"Setup cancelled\");\n process.exit(0);\n }\n\n return answer as SetupChoice;\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,eAAsB,WAAiC;CACrD,MAAM,SAAS,MAAM,OAAO;EAC1B,SAAS;EACT,SAAS;GACP;IACE,OAAO;IACP,OAAO;IACP,MAAM;GACR;GACA;IACE,OAAO;IACP,OAAO;IACP,MAAM;GACR;GACA;IACE,OAAO;IACP,OAAO;IACP,MAAM;GACR;EACF;EACA,cAAc;CAChB,CAAC;CAED,IAAI,SAAS,MAAM,GAAG;EACpB,OAAO,iBAAiB;EACxB,QAAQ,KAAK,CAAC;CAChB;CAEA,OAAO;AACT"}
|
package/llms-full.txt
CHANGED
|
@@ -18,7 +18,7 @@ description: "Create a Warlock project with the canonical scaffold, package sele
|
|
|
18
18
|
## The shape
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
# Default:
|
|
21
|
+
# Default: choose API-only or full-stack web, then a database unless a DB flag already answered it
|
|
22
22
|
npx create-warlock my-app
|
|
23
23
|
|
|
24
24
|
# Fully headless — one command scaffolds the whole app, no prompts at all
|
|
@@ -38,7 +38,7 @@ The first positional argument is the project name; it also becomes the directory
|
|
|
38
38
|
|
|
39
39
|
## Three modes
|
|
40
40
|
|
|
41
|
-
1. **Default (a TTY, no `--yes`, no `--interactive`)** — asks the project name (if not already given)
|
|
41
|
+
1. **Default (a TTY, no `--yes`, no `--interactive`)** — asks the project name (if not already given), API-only or full-stack web (`--stack`), then a database unless `--db` or `--no-db` already supplied it. The stack is the only choice that changes the generated app's shape.
|
|
42
42
|
2. **`--yes` (or no TTY at all — CI, a script, an agent harness)** — fully headless. Every answer comes from a flag or its default; nothing is ever prompted, and stdin is never read.
|
|
43
43
|
3. **`--interactive` (alias `--customize`)** — the full long-form wizard: project name, package manager, database driver, features, AI providers, git, JWT — one prompt after another, exactly like earlier `create-warlock` releases.
|
|
44
44
|
|
|
@@ -74,7 +74,7 @@ Using npm · database: none · agent-kit targets: claude
|
|
|
74
74
|
| Flag | Takes value | Default | Purpose |
|
|
75
75
|
| --------------------------- | ----------- | -------------- | ------------------------------------------------------------------- |
|
|
76
76
|
| `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
|
|
77
|
-
| `--stack` | yes | `api` | `api` or `web` — the
|
|
77
|
+
| `--stack` | yes | `api` | `api` or `web` — the structural choice; `web` seeds the `web` feature by default. |
|
|
78
78
|
| `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
|
|
79
79
|
| `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
|
|
80
80
|
| `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — must be one of `npm` / `yarn` / `pnpm` / `bun`; anything else is rejected before scaffolding starts. |
|
|
@@ -124,6 +124,20 @@ guesses.
|
|
|
124
124
|
|
|
125
125
|
**AI providers / packages** (`--ai`): `ai-openai`, `ai-google`, `ai-anthropic`, `ai-bedrock`, `ai-ollama`, plus the capability packages `ai-tools`, `ai-panoptic`, `ai-workspace`. Any pick auto-pulls the core `@warlock.js/ai` package.
|
|
126
126
|
|
|
127
|
+
## Interactive selectors
|
|
128
|
+
|
|
129
|
+
The default flow and Customize both use the same database selector: MongoDB
|
|
130
|
+
and PostgreSQL, with MySQL shown as disabled, then **None**. **None** is
|
|
131
|
+
`--db=none` / `--no-db`: no driver
|
|
132
|
+
feature is added and `src/config/database.ts` is removed. A supplied database
|
|
133
|
+
flag skips the default-flow selector.
|
|
134
|
+
|
|
135
|
+
In Customize's AI multiselect, pressing Enter with no selection skips AI.
|
|
136
|
+
**None** is the last option and also skips AI when it is the sole choice.
|
|
137
|
+
Selecting **None** alongside a provider or capability is invalid: the wizard
|
|
138
|
+
shows a warning and returns to that selector with the non-**None** choices
|
|
139
|
+
retained for correction. Noninteractive `--ai` flag semantics are unchanged.
|
|
140
|
+
|
|
127
141
|
> The scaffolder holds **no dependency versions** for these — the keys map to `warlock add <key>`, whose feature map in `@warlock.js/core` is the single source of truth. A feature that resolves there will install; one that does not is rejected.
|
|
128
142
|
|
|
129
143
|
## What gets generated
|
package/package.json
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@clack/prompts": "^0.7.0",
|
|
14
14
|
"@mongez/copper": "^2.1.2",
|
|
15
|
-
"@warlock.js/fs": "5.
|
|
16
|
-
"@mongez/reinforcements": "^4.0.
|
|
15
|
+
"@warlock.js/fs": "5.20.0",
|
|
16
|
+
"@mongez/reinforcements": "^4.0.2",
|
|
17
17
|
"cross-spawn": "^7.0.3",
|
|
18
18
|
"rimraf": "^6.0.1",
|
|
19
19
|
"which-pm-runs": "^1.1.0"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=22.13"
|
|
23
23
|
},
|
|
24
|
-
"version": "5.
|
|
24
|
+
"version": "5.20.0",
|
|
25
25
|
"type": "module",
|
|
26
26
|
"main": "./esm/index.mjs",
|
|
27
27
|
"module": "./esm/index.mjs",
|
|
@@ -10,7 +10,7 @@ description: "Create a Warlock project with the canonical scaffold, package sele
|
|
|
10
10
|
## The shape
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
# Default:
|
|
13
|
+
# Default: choose API-only or full-stack web, then a database unless a DB flag already answered it
|
|
14
14
|
npx create-warlock my-app
|
|
15
15
|
|
|
16
16
|
# Fully headless — one command scaffolds the whole app, no prompts at all
|
|
@@ -30,7 +30,7 @@ The first positional argument is the project name; it also becomes the directory
|
|
|
30
30
|
|
|
31
31
|
## Three modes
|
|
32
32
|
|
|
33
|
-
1. **Default (a TTY, no `--yes`, no `--interactive`)** — asks the project name (if not already given)
|
|
33
|
+
1. **Default (a TTY, no `--yes`, no `--interactive`)** — asks the project name (if not already given), API-only or full-stack web (`--stack`), then a database unless `--db` or `--no-db` already supplied it. The stack is the only choice that changes the generated app's shape.
|
|
34
34
|
2. **`--yes` (or no TTY at all — CI, a script, an agent harness)** — fully headless. Every answer comes from a flag or its default; nothing is ever prompted, and stdin is never read.
|
|
35
35
|
3. **`--interactive` (alias `--customize`)** — the full long-form wizard: project name, package manager, database driver, features, AI providers, git, JWT — one prompt after another, exactly like earlier `create-warlock` releases.
|
|
36
36
|
|
|
@@ -66,7 +66,7 @@ Using npm · database: none · agent-kit targets: claude
|
|
|
66
66
|
| Flag | Takes value | Default | Purpose |
|
|
67
67
|
| --------------------------- | ----------- | -------------- | ------------------------------------------------------------------- |
|
|
68
68
|
| `<positional>` / `--name` | yes | — (required) | Project name + target directory. |
|
|
69
|
-
| `--stack` | yes | `api` | `api` or `web` — the
|
|
69
|
+
| `--stack` | yes | `api` | `api` or `web` — the structural choice; `web` seeds the `web` feature by default. |
|
|
70
70
|
| `--db` | yes | `mongodb` | Database driver key (`mongodb`, `postgres`, or `none`). |
|
|
71
71
|
| `--no-db` | no | — | Opt out of a database — shorthand for `--db=none`. |
|
|
72
72
|
| `--pm` | yes | inferred (`npm_config_user_agent` / system) | Package manager — must be one of `npm` / `yarn` / `pnpm` / `bun`; anything else is rejected before scaffolding starts. |
|
|
@@ -116,6 +116,20 @@ guesses.
|
|
|
116
116
|
|
|
117
117
|
**AI providers / packages** (`--ai`): `ai-openai`, `ai-google`, `ai-anthropic`, `ai-bedrock`, `ai-ollama`, plus the capability packages `ai-tools`, `ai-panoptic`, `ai-workspace`. Any pick auto-pulls the core `@warlock.js/ai` package.
|
|
118
118
|
|
|
119
|
+
## Interactive selectors
|
|
120
|
+
|
|
121
|
+
The default flow and Customize both use the same database selector: MongoDB
|
|
122
|
+
and PostgreSQL, with MySQL shown as disabled, then **None**. **None** is
|
|
123
|
+
`--db=none` / `--no-db`: no driver
|
|
124
|
+
feature is added and `src/config/database.ts` is removed. A supplied database
|
|
125
|
+
flag skips the default-flow selector.
|
|
126
|
+
|
|
127
|
+
In Customize's AI multiselect, pressing Enter with no selection skips AI.
|
|
128
|
+
**None** is the last option and also skips AI when it is the sole choice.
|
|
129
|
+
Selecting **None** alongside a provider or capability is invalid: the wizard
|
|
130
|
+
shows a warning and returns to that selector with the non-**None** choices
|
|
131
|
+
retained for correction. Noninteractive `--ai` flag semantics are unchanged.
|
|
132
|
+
|
|
119
133
|
> The scaffolder holds **no dependency versions** for these — the keys map to `warlock add <key>`, whose feature map in `@warlock.js/core` is the single source of truth. A feature that resolves there will install; one that does not is rejected.
|
|
120
134
|
|
|
121
135
|
## What gets generated
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"lf": "find . -type d ( -name 'node_modules' -o -name '.git' ) -prune -o -type f -exec dos2unix {} ;"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@mongez/reinforcements": "^4.0.
|
|
35
|
-
"@mongez/localization": "^3.5.
|
|
36
|
-
"@mongez/supportive-is": "^2.1.
|
|
34
|
+
"@mongez/reinforcements": "^4.0.2",
|
|
35
|
+
"@mongez/localization": "^3.5.1",
|
|
36
|
+
"@mongez/supportive-is": "^2.1.5",
|
|
37
37
|
"@warlock.js/auth": "0.0.0-set-by-scaffolder",
|
|
38
38
|
"@warlock.js/cache": "0.0.0-set-by-scaffolder",
|
|
39
39
|
"@warlock.js/cascade": "0.0.0-set-by-scaffolder",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dayjs": "^1.11.19"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@mongez/agent-kit": "^1.
|
|
48
|
+
"@mongez/agent-kit": "^1.3.1",
|
|
49
49
|
"@types/node": "^25.0.3",
|
|
50
50
|
"@types/prettier": "^3.0.0",
|
|
51
51
|
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
@@ -113,6 +113,12 @@ export const config = {
|
|
|
113
113
|
} satisfies RootConfig;
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
Every loader is an exported named `async function` with a public typed context;
|
|
117
|
+
let TypeScript infer its return. Use
|
|
118
|
+
`PageLoaderContext<typeof config.validation, typeof config.route>` for a page,
|
|
119
|
+
`LayoutLoaderContext` for a layout, and `AppLoaderContext` for the root. Do not
|
|
120
|
+
annotate the whole function as `PageLoader`, use `satisfies` on a loader,
|
|
121
|
+
duplicate its payload shape, or cast an unknown return.
|
|
116
122
|
A page file declares its route contract, coordinates a controller-like loader,
|
|
117
123
|
and composes presentational components. The loader validates input, calls
|
|
118
124
|
named services under `src/app/<domain>/services/`, converts results through
|
|
@@ -173,6 +179,15 @@ pure and has no React or browser lifecycle dependency. Keep feature CSS beside
|
|
|
173
179
|
the feature and import it from the owning presentation boundary; keep loading,
|
|
174
180
|
error, empty, focus, reduced-motion, and locale behavior with that boundary.
|
|
175
181
|
|
|
182
|
+
## Declaration spacing and local types
|
|
183
|
+
|
|
184
|
+
Put one blank line after the final import. Keep a short related declaration run
|
|
185
|
+
together, then put a blank line before a substantial multiline expression such
|
|
186
|
+
as a `useSubmitForm()` configuration. Declare a schema alias immediately after
|
|
187
|
+
its `const` schema instead of repeating `typeof schema` through the file. When
|
|
188
|
+
state starts unset, write `useState<ErrorText>()`; do not add a redundant
|
|
189
|
+
`| undefined` union or pass `undefined` explicitly.
|
|
190
|
+
|
|
176
191
|
## 2. Make the server-to-browser data contract explicit
|
|
177
192
|
|
|
178
193
|
- Validate URL parameters and query input at the page boundary. Use the
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { groupedTranslations } from "@mongez/localization";
|
|
2
|
-
import type { PageConfig,
|
|
2
|
+
import type { PageConfig, PageLoaderContext } from "@warlock.js/web";
|
|
3
3
|
import { getHomeService } from "app/home/services/home.service";
|
|
4
4
|
import { isLocaleCode } from "../../shared/locales";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export async function loader({ request, response }: HomeLoaderOptions) {
|
|
6
|
+
export async function loader({ request, response }: PageLoaderContext<undefined, undefined>) {
|
|
9
7
|
const locale = request.locale;
|
|
10
8
|
|
|
11
9
|
if (!isLocaleCode(locale)) {
|