create-dowel-app 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +465 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
- package/templates/ai/src/app/app/agents/page.tsx +66 -0
- package/templates/ai/src/app/app/page.tsx +42 -0
- package/templates/ai/src/app/app/usage/page.tsx +58 -0
- package/templates/ai/src/app/page.tsx +25 -0
- package/templates/app-shell/src/app/app/layout.tsx +48 -0
- package/templates/app-shell/src/components/app-nav.tsx +41 -0
- package/templates/base/gitignore +16 -0
- package/templates/base/next.config.ts +5 -0
- package/templates/base/package.json +25 -0
- package/templates/base/postcss.config.mjs +5 -0
- package/templates/base/src/app/globals.css +1 -0
- package/templates/base/src/app/layout.tsx +22 -0
- package/templates/base/tsconfig.json +23 -0
- package/templates/saas/src/app/app/analytics/page.tsx +62 -0
- package/templates/saas/src/app/app/billing/page.tsx +36 -0
- package/templates/saas/src/app/app/page.tsx +69 -0
- package/templates/saas/src/app/app/settings/page.tsx +51 -0
- package/templates/saas/src/app/page.tsx +24 -0
- package/templates/starter/src/app/page.tsx +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dowel contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/create.d.ts
|
|
2
|
+
interface CreateOptions {
|
|
3
|
+
/** Directory to create, relative to cwd or absolute. */
|
|
4
|
+
directory?: string;
|
|
5
|
+
template?: string;
|
|
6
|
+
theme?: string;
|
|
7
|
+
packageManager?: string;
|
|
8
|
+
/** Accept every default and never prompt. */
|
|
9
|
+
yes: boolean;
|
|
10
|
+
skipInstall: boolean;
|
|
11
|
+
/** Write files but do not fetch components. Mostly for tests. */
|
|
12
|
+
skipComponents: boolean;
|
|
13
|
+
cwd: string;
|
|
14
|
+
}
|
|
15
|
+
declare function create(options: CreateOptions): Promise<void>;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { create };
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/create.ts"],"mappings":";UAoBiB;;EAEf;EACA;EACA;EACA;;EAEA;EACA;;EAEA;EACA;;iBAiDoB,OAAO,SAAS,gBAAgB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import * as prompts from "@clack/prompts";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
//#region src/branding.ts
|
|
10
|
+
/**
|
|
11
|
+
* Branding, mirrored from the repository root config.
|
|
12
|
+
*
|
|
13
|
+
* Duplicated deliberately: the published scaffolder cannot import from the
|
|
14
|
+
* monorepo root, and `pnpm rebrand` rewrites every copy in the same pass.
|
|
15
|
+
*/
|
|
16
|
+
const branding = {
|
|
17
|
+
libraryName: "Dowel",
|
|
18
|
+
cliPackage: "@dowel-ui/cli",
|
|
19
|
+
packageScope: "@dowel-ui",
|
|
20
|
+
registryUrl: "https://dowel-eight.vercel.app/r",
|
|
21
|
+
docsUrl: "https://dowel-eight.vercel.app"
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/lib/errors.ts
|
|
25
|
+
/**
|
|
26
|
+
* An error whose message is written for the person running the command.
|
|
27
|
+
*
|
|
28
|
+
* Anything thrown as a CreateError is printed as a clean message with no stack
|
|
29
|
+
* trace; everything else is treated as a bug, where the stack is the useful
|
|
30
|
+
* part.
|
|
31
|
+
*/
|
|
32
|
+
var CreateError = class extends Error {
|
|
33
|
+
hint;
|
|
34
|
+
constructor(message, hint) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "CreateError";
|
|
37
|
+
this.hint = hint;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/lib/files.ts
|
|
42
|
+
/** Files npm will not publish under their real name. */
|
|
43
|
+
const RENAME_ON_COPY = {
|
|
44
|
+
gitignore: ".gitignore",
|
|
45
|
+
npmrc: ".npmrc",
|
|
46
|
+
"env.example": ".env.example"
|
|
47
|
+
};
|
|
48
|
+
/** Extensions worth substituting into. Anything else is copied byte for byte. */
|
|
49
|
+
const TEXT_EXTENSIONS = [
|
|
50
|
+
".ts",
|
|
51
|
+
".tsx",
|
|
52
|
+
".js",
|
|
53
|
+
".mjs",
|
|
54
|
+
".json",
|
|
55
|
+
".css",
|
|
56
|
+
".md",
|
|
57
|
+
".txt"
|
|
58
|
+
];
|
|
59
|
+
function isText(path) {
|
|
60
|
+
return TEXT_EXTENSIONS.some((extension) => path.endsWith(extension)) || !path.includes(".");
|
|
61
|
+
}
|
|
62
|
+
function substitute(content, replacements) {
|
|
63
|
+
let result = content;
|
|
64
|
+
for (const [key, value] of Object.entries(replacements)) result = result.replaceAll(`__${key}__`, value);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Copies one template layer over a destination, substituting as it goes.
|
|
69
|
+
*
|
|
70
|
+
* Layers are applied in order and a later one overwrites an earlier one, which
|
|
71
|
+
* is how `saas` replaces the base landing page without the base having to know
|
|
72
|
+
* that anything might.
|
|
73
|
+
*/
|
|
74
|
+
function copyLayer(from, to, replacements) {
|
|
75
|
+
const written = [];
|
|
76
|
+
const walk = (source, target, prefix) => {
|
|
77
|
+
mkdirSync(target, { recursive: true });
|
|
78
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
79
|
+
const name = RENAME_ON_COPY[entry.name] ?? entry.name;
|
|
80
|
+
const sourcePath = join(source, entry.name);
|
|
81
|
+
const targetPath = join(target, name);
|
|
82
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
walk(sourcePath, targetPath, relative);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isText(sourcePath)) writeFileSync(targetPath, substitute(readFileSync(sourcePath, "utf8"), replacements));
|
|
88
|
+
else cpSync(sourcePath, targetPath);
|
|
89
|
+
written.push(relative);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
walk(from, to, "");
|
|
93
|
+
return written;
|
|
94
|
+
}
|
|
95
|
+
/** True when the directory does not exist, or exists and holds nothing. */
|
|
96
|
+
function isEmptyDirectory(path) {
|
|
97
|
+
if (!existsSync(path)) return true;
|
|
98
|
+
if (!statSync(path).isDirectory()) return false;
|
|
99
|
+
return readdirSync(path).length === 0;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/lib/pm.ts
|
|
103
|
+
const PACKAGE_MANAGERS = [
|
|
104
|
+
"pnpm",
|
|
105
|
+
"npm",
|
|
106
|
+
"yarn",
|
|
107
|
+
"bun"
|
|
108
|
+
];
|
|
109
|
+
function isPackageManager(value) {
|
|
110
|
+
return PACKAGE_MANAGERS.includes(value);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Which package manager invoked this process.
|
|
114
|
+
*
|
|
115
|
+
* `npm_config_user_agent` is set by every one of them, and it is the only
|
|
116
|
+
* reliable signal: someone running `pnpm create dowel-app` wants pnpm, and
|
|
117
|
+
* asking them again is asking a question the environment already answered.
|
|
118
|
+
*/
|
|
119
|
+
function detectPackageManager() {
|
|
120
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
121
|
+
for (const candidate of PACKAGE_MANAGERS) if (agent.startsWith(`${candidate}/`)) return candidate;
|
|
122
|
+
return "npm";
|
|
123
|
+
}
|
|
124
|
+
function installCommand(manager) {
|
|
125
|
+
return manager === "npm" ? "npm install" : `${manager} install`;
|
|
126
|
+
}
|
|
127
|
+
function runCommand(manager, script) {
|
|
128
|
+
return manager === "npm" ? `npm run ${script}` : `${manager} ${script}`;
|
|
129
|
+
}
|
|
130
|
+
/** The runner that executes a package's binary without installing it globally. */
|
|
131
|
+
function dlx(manager) {
|
|
132
|
+
switch (manager) {
|
|
133
|
+
case "pnpm": return ["pnpm", "dlx"];
|
|
134
|
+
case "yarn": return ["yarn", "dlx"];
|
|
135
|
+
case "bun": return ["bunx"];
|
|
136
|
+
default: return ["npx", "-y"];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function install(manager, cwd) {
|
|
140
|
+
const [command, ...args] = installCommand(manager).split(" ");
|
|
141
|
+
execFileSync(command ?? "npm", args, {
|
|
142
|
+
cwd,
|
|
143
|
+
stdio: "inherit"
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Runs the component CLI in the new project. */
|
|
147
|
+
function runDowel(manager, cwd, cliPackage, args) {
|
|
148
|
+
const [command, ...runner] = dlx(manager);
|
|
149
|
+
execFileSync(command ?? "npx", [
|
|
150
|
+
...runner,
|
|
151
|
+
cliPackage,
|
|
152
|
+
...args
|
|
153
|
+
], {
|
|
154
|
+
cwd,
|
|
155
|
+
stdio: "inherit"
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/lib/logger.ts
|
|
160
|
+
/**
|
|
161
|
+
* All CLI output goes through here.
|
|
162
|
+
*
|
|
163
|
+
* A single place to route messages means the format stays consistent, and
|
|
164
|
+
* anything that needs to change later — quiet mode, JSON output, writing to
|
|
165
|
+
* stderr — changes in one file rather than in every command.
|
|
166
|
+
*/
|
|
167
|
+
const logger = {
|
|
168
|
+
info(message) {
|
|
169
|
+
console.log(message);
|
|
170
|
+
},
|
|
171
|
+
success(message) {
|
|
172
|
+
console.log(`${pc.green("✓")} ${message}`);
|
|
173
|
+
},
|
|
174
|
+
warn(message) {
|
|
175
|
+
console.warn(`${pc.yellow("!")} ${message}`);
|
|
176
|
+
},
|
|
177
|
+
error(message) {
|
|
178
|
+
console.error(`${pc.red("✕")} ${message}`);
|
|
179
|
+
},
|
|
180
|
+
step(message) {
|
|
181
|
+
console.log(`${pc.dim("·")} ${message}`);
|
|
182
|
+
},
|
|
183
|
+
blank() {
|
|
184
|
+
console.log("");
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/templates.ts
|
|
189
|
+
const TEMPLATES = [
|
|
190
|
+
{
|
|
191
|
+
id: "starter",
|
|
192
|
+
title: "Starter",
|
|
193
|
+
description: `A Next.js app wired to ${branding.libraryName}: tokens, aliases and a landing page.`,
|
|
194
|
+
layers: ["base", "starter"],
|
|
195
|
+
items: [
|
|
196
|
+
"button",
|
|
197
|
+
"card",
|
|
198
|
+
"badge"
|
|
199
|
+
],
|
|
200
|
+
routes: ["/"]
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "saas",
|
|
204
|
+
title: "SaaS",
|
|
205
|
+
description: "Adds an application shell with dashboard, analytics, billing, settings and onboarding.",
|
|
206
|
+
layers: [
|
|
207
|
+
"base",
|
|
208
|
+
"app-shell",
|
|
209
|
+
"saas"
|
|
210
|
+
],
|
|
211
|
+
items: [
|
|
212
|
+
"sidebar",
|
|
213
|
+
"dashboard",
|
|
214
|
+
"analytics",
|
|
215
|
+
"billing",
|
|
216
|
+
"settings",
|
|
217
|
+
"onboarding"
|
|
218
|
+
],
|
|
219
|
+
routes: [
|
|
220
|
+
"/",
|
|
221
|
+
"/app",
|
|
222
|
+
"/app/analytics",
|
|
223
|
+
"/app/billing",
|
|
224
|
+
"/app/settings"
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "ai",
|
|
229
|
+
title: "AI product",
|
|
230
|
+
description: "Adds a chat surface, an agent console and a usage dashboard.",
|
|
231
|
+
layers: [
|
|
232
|
+
"base",
|
|
233
|
+
"app-shell",
|
|
234
|
+
"ai"
|
|
235
|
+
],
|
|
236
|
+
items: [
|
|
237
|
+
"sidebar",
|
|
238
|
+
"ai-chat",
|
|
239
|
+
"agent-console",
|
|
240
|
+
"ai-dashboard"
|
|
241
|
+
],
|
|
242
|
+
routes: [
|
|
243
|
+
"/",
|
|
244
|
+
"/app",
|
|
245
|
+
"/app/agents",
|
|
246
|
+
"/app/usage"
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
];
|
|
250
|
+
function findTemplate(id) {
|
|
251
|
+
return TEMPLATES.find((template) => template.id === id);
|
|
252
|
+
}
|
|
253
|
+
/** Presets the scaffolder offers, mirroring what the theme layer ships. */
|
|
254
|
+
const THEMES = [
|
|
255
|
+
"default",
|
|
256
|
+
"ocean",
|
|
257
|
+
"emerald",
|
|
258
|
+
"violet",
|
|
259
|
+
"rose",
|
|
260
|
+
"amber",
|
|
261
|
+
"monochrome"
|
|
262
|
+
];
|
|
263
|
+
function isTheme(value) {
|
|
264
|
+
return THEMES.includes(value);
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/create.ts
|
|
268
|
+
/** Where the shipped templates live, relative to the built entry point. */
|
|
269
|
+
const templatesRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
|
|
270
|
+
/**
|
|
271
|
+
* npm's rules for a package name, which is what this becomes.
|
|
272
|
+
*
|
|
273
|
+
* Checked before anything is written rather than after: a directory created and
|
|
274
|
+
* then abandoned because the name was rejected is worse than a question asked
|
|
275
|
+
* twice.
|
|
276
|
+
*/
|
|
277
|
+
function validateProjectName(name) {
|
|
278
|
+
if (name.length === 0) return "Give the project a name.";
|
|
279
|
+
if (name.length > 214) return "That is longer than npm allows for a package name.";
|
|
280
|
+
if (name.startsWith(".") || name.startsWith("_")) return "A package name cannot start with a dot or an underscore.";
|
|
281
|
+
if (name !== name.toLowerCase()) return "A package name has to be lowercase.";
|
|
282
|
+
if (!/^[a-z0-9._-]+$/.test(name)) return "Use lowercase letters, digits, dots, hyphens and underscores only.";
|
|
283
|
+
}
|
|
284
|
+
/** The last segment of a path, as a package name. */
|
|
285
|
+
function projectNameFrom(directory) {
|
|
286
|
+
return directory.split("/").filter(Boolean).pop() ?? "app";
|
|
287
|
+
}
|
|
288
|
+
/** The nav for the app shell, written into its layout. */
|
|
289
|
+
function appLinks(template) {
|
|
290
|
+
const labels = {
|
|
291
|
+
"/app": template.id === "ai" ? "Chat" : "Dashboard",
|
|
292
|
+
"/app/analytics": "Analytics",
|
|
293
|
+
"/app/billing": "Billing",
|
|
294
|
+
"/app/settings": "Settings",
|
|
295
|
+
"/app/agents": "Agents",
|
|
296
|
+
"/app/usage": "Usage"
|
|
297
|
+
};
|
|
298
|
+
return `[\n${template.routes.filter((route) => route !== "/").map((route) => ` { href: "${route}", label: "${labels[route] ?? route}" },`).join("\n")}\n]`;
|
|
299
|
+
}
|
|
300
|
+
async function create(options) {
|
|
301
|
+
const interactive = !options.yes;
|
|
302
|
+
if (interactive) prompts.intro(`${branding.libraryName} — create an app`);
|
|
303
|
+
const directory = await resolveDirectory(options, interactive);
|
|
304
|
+
const target = isAbsolute(directory) ? directory : resolve(options.cwd, directory);
|
|
305
|
+
const name = projectNameFrom(directory);
|
|
306
|
+
const invalid = validateProjectName(name);
|
|
307
|
+
if (invalid) throw new CreateError(invalid);
|
|
308
|
+
if (!isEmptyDirectory(target)) throw new CreateError(`${directory} already exists and is not empty.`, "Choose another name, or empty the directory first.");
|
|
309
|
+
const template = await resolveTemplate(options, interactive);
|
|
310
|
+
const theme = await resolveTheme(options, interactive);
|
|
311
|
+
const manager = resolvePackageManager(options);
|
|
312
|
+
const replacements = {
|
|
313
|
+
PROJECT_NAME: name,
|
|
314
|
+
LIBRARY_NAME: branding.libraryName,
|
|
315
|
+
CLI_PACKAGE: branding.cliPackage,
|
|
316
|
+
DOCS_URL: branding.docsUrl,
|
|
317
|
+
THEME: theme,
|
|
318
|
+
APP_LINKS: appLinks(template)
|
|
319
|
+
};
|
|
320
|
+
mkdirSync(target, { recursive: true });
|
|
321
|
+
const written = [];
|
|
322
|
+
for (const layer of template.layers) {
|
|
323
|
+
const from = join(templatesRoot, layer);
|
|
324
|
+
if (!existsSync(from)) throw new CreateError(`The ${layer} template is missing from this installation.`, "Reinstall create-dowel-app, or report this if it persists.");
|
|
325
|
+
written.push(...copyLayer(from, target, replacements));
|
|
326
|
+
}
|
|
327
|
+
logger.blank();
|
|
328
|
+
logger.success(`Created ${pc.bold(name)} from the ${pc.bold(template.title)} template.`);
|
|
329
|
+
logger.info(pc.dim(` ${String(new Set(written).size)} files in ${directory}`));
|
|
330
|
+
if (!options.skipInstall) {
|
|
331
|
+
logger.blank();
|
|
332
|
+
logger.step(`Installing dependencies with ${manager}`);
|
|
333
|
+
install(manager, target);
|
|
334
|
+
}
|
|
335
|
+
if (!options.skipComponents) {
|
|
336
|
+
logger.blank();
|
|
337
|
+
logger.step("Fetching components from the registry");
|
|
338
|
+
runDowel(manager, target, branding.cliPackage, [
|
|
339
|
+
"init",
|
|
340
|
+
"--yes",
|
|
341
|
+
"--skip-install"
|
|
342
|
+
]);
|
|
343
|
+
runDowel(manager, target, branding.cliPackage, [
|
|
344
|
+
"add",
|
|
345
|
+
...template.items,
|
|
346
|
+
"--yes",
|
|
347
|
+
...options.skipInstall ? ["--skip-install"] : []
|
|
348
|
+
]);
|
|
349
|
+
}
|
|
350
|
+
summarise({
|
|
351
|
+
directory,
|
|
352
|
+
template,
|
|
353
|
+
theme,
|
|
354
|
+
manager,
|
|
355
|
+
options
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function summarise({ directory, template, theme, manager, options }) {
|
|
359
|
+
logger.blank();
|
|
360
|
+
logger.success("Done.");
|
|
361
|
+
logger.blank();
|
|
362
|
+
logger.info(pc.dim("Next:"));
|
|
363
|
+
logger.info(` cd ${directory}`);
|
|
364
|
+
if (options.skipInstall) logger.info(` ${installCommand(manager)}`);
|
|
365
|
+
logger.info(` ${runCommand(manager, "dev")}`);
|
|
366
|
+
logger.blank();
|
|
367
|
+
logger.info(pc.dim("Routes:"));
|
|
368
|
+
for (const route of template.routes) logger.info(` ${route}`);
|
|
369
|
+
logger.blank();
|
|
370
|
+
logger.info(pc.dim(`Theme: ${theme}. Change it on <html data-theme> in src/app/layout.tsx — no component file changes.`));
|
|
371
|
+
logger.info(pc.dim(`Teach your coding agent what is installed: npx ${branding.cliPackage} agents`));
|
|
372
|
+
}
|
|
373
|
+
async function resolveDirectory(options, interactive) {
|
|
374
|
+
if (options.directory) return options.directory;
|
|
375
|
+
if (!interactive) throw new CreateError("No directory given.", "Pass one, e.g. `create-dowel-app my-app`, or drop --yes to be asked.");
|
|
376
|
+
const answer = await prompts.text({
|
|
377
|
+
message: "Where should it go?",
|
|
378
|
+
placeholder: "my-app",
|
|
379
|
+
defaultValue: "my-app",
|
|
380
|
+
validate: (value) => validateProjectName(projectNameFrom(value || "my-app"))
|
|
381
|
+
});
|
|
382
|
+
if (prompts.isCancel(answer)) throw new CreateError("Cancelled — nothing was written.");
|
|
383
|
+
return answer || "my-app";
|
|
384
|
+
}
|
|
385
|
+
async function resolveTemplate(options, interactive) {
|
|
386
|
+
if (options.template) {
|
|
387
|
+
const found = findTemplate(options.template);
|
|
388
|
+
if (!found) throw new CreateError(`Unknown template "${options.template}".`, `Choose from: ${TEMPLATES.map((entry) => entry.id).join(", ")}.`);
|
|
389
|
+
return found;
|
|
390
|
+
}
|
|
391
|
+
if (!interactive) return TEMPLATES[0];
|
|
392
|
+
const answer = await prompts.select({
|
|
393
|
+
message: "What are you building?",
|
|
394
|
+
options: TEMPLATES.map((entry) => ({
|
|
395
|
+
value: entry.id,
|
|
396
|
+
label: entry.title,
|
|
397
|
+
hint: entry.description
|
|
398
|
+
}))
|
|
399
|
+
});
|
|
400
|
+
if (prompts.isCancel(answer)) throw new CreateError("Cancelled — nothing was written.");
|
|
401
|
+
return findTemplate(answer) ?? TEMPLATES[0];
|
|
402
|
+
}
|
|
403
|
+
async function resolveTheme(options, interactive) {
|
|
404
|
+
if (options.theme) {
|
|
405
|
+
if (!isTheme(options.theme)) throw new CreateError(`Unknown theme "${options.theme}".`, `Choose from: ${THEMES.join(", ")}.`);
|
|
406
|
+
return options.theme;
|
|
407
|
+
}
|
|
408
|
+
if (!interactive) return "default";
|
|
409
|
+
const answer = await prompts.select({
|
|
410
|
+
message: "Which theme?",
|
|
411
|
+
options: THEMES.map((entry) => ({
|
|
412
|
+
value: entry,
|
|
413
|
+
label: entry,
|
|
414
|
+
hint: entry === "monochrome" ? "No colour at all — a standing check that nothing relies on it" : void 0
|
|
415
|
+
}))
|
|
416
|
+
});
|
|
417
|
+
if (prompts.isCancel(answer)) throw new CreateError("Cancelled — nothing was written.");
|
|
418
|
+
return answer;
|
|
419
|
+
}
|
|
420
|
+
function resolvePackageManager(options) {
|
|
421
|
+
if (!options.packageManager) return detectPackageManager();
|
|
422
|
+
if (!isPackageManager(options.packageManager)) throw new CreateError(`Unknown package manager "${options.packageManager}".`, "Choose from: pnpm, npm, yarn, bun.");
|
|
423
|
+
return options.packageManager;
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/index.ts
|
|
427
|
+
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
428
|
+
const program = new Command();
|
|
429
|
+
program.name("create-dowel-app").description(`Creates a Next.js application wired to ${branding.libraryName}, with the components fetched from the registry rather than copied out of a template.`).version(version).argument("[directory]", "where to create it").option("-t, --template <name>", `one of: ${TEMPLATES.map((entry) => entry.id).join(", ")}`).option("--theme <name>", `one of: ${THEMES.join(", ")}`).option("--pm <manager>", "pnpm, npm, yarn or bun; detected from how this was run").option("-y, --yes", "accept every default and never prompt", false).option("--skip-install", "write files but do not install dependencies", false).option("--skip-components", "write files but do not fetch components", false).action(async (directory, options) => {
|
|
430
|
+
await create({
|
|
431
|
+
directory,
|
|
432
|
+
template: options.template,
|
|
433
|
+
theme: options.theme,
|
|
434
|
+
packageManager: options.pm,
|
|
435
|
+
yes: options.yes,
|
|
436
|
+
skipInstall: options.skipInstall,
|
|
437
|
+
skipComponents: options.skipComponents,
|
|
438
|
+
cwd: process.cwd()
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
/**
|
|
442
|
+
* A CreateError is a message for the person running the command; anything else
|
|
443
|
+
* is a bug, and its stack trace is the useful part.
|
|
444
|
+
*/
|
|
445
|
+
async function main() {
|
|
446
|
+
try {
|
|
447
|
+
await program.parseAsync(process.argv);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
logger.blank();
|
|
450
|
+
if (error instanceof CreateError) {
|
|
451
|
+
logger.error(error.message);
|
|
452
|
+
if (error.hint) logger.info(pc.dim(` ${error.hint}`));
|
|
453
|
+
} else {
|
|
454
|
+
logger.error("Something went wrong.");
|
|
455
|
+
logger.info(String(error instanceof Error ? error.stack ?? error.message : error));
|
|
456
|
+
}
|
|
457
|
+
logger.blank();
|
|
458
|
+
process.exitCode = 1;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
main();
|
|
462
|
+
//#endregion
|
|
463
|
+
export { create };
|
|
464
|
+
|
|
465
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/branding.ts","../src/lib/errors.ts","../src/lib/files.ts","../src/lib/pm.ts","../src/lib/logger.ts","../src/templates.ts","../src/create.ts","../src/index.ts"],"sourcesContent":["/**\n * Branding, mirrored from the repository root config.\n *\n * Duplicated deliberately: the published scaffolder cannot import from the\n * monorepo root, and `pnpm rebrand` rewrites every copy in the same pass.\n */\nexport const branding = {\n libraryName: \"Dowel\",\n cliPackage: \"@dowel-ui/cli\",\n packageScope: \"@dowel-ui\",\n registryUrl: \"https://dowel-eight.vercel.app/r\",\n docsUrl: \"https://dowel-eight.vercel.app\",\n} as const;\n","/**\n * An error whose message is written for the person running the command.\n *\n * Anything thrown as a CreateError is printed as a clean message with no stack\n * trace; everything else is treated as a bug, where the stack is the useful\n * part.\n */\nexport class CreateError extends Error {\n readonly hint: string | undefined;\n\n constructor(message: string, hint?: string) {\n super(message);\n this.name = \"CreateError\";\n this.hint = hint;\n }\n}\n","import {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n renameSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Placeholders substituted into template files.\n *\n * Written as `__NAME__` rather than as a template syntax so every template file\n * stays valid TypeScript, valid JSON and valid CSS. A template you cannot\n * typecheck is a template that ships broken, and the only way to find out is to\n * generate from it.\n */\nexport type Replacements = Record<string, string>;\n\n/** Files npm will not publish under their real name. */\nconst RENAME_ON_COPY: Record<string, string> = {\n gitignore: \".gitignore\",\n npmrc: \".npmrc\",\n \"env.example\": \".env.example\",\n};\n\n/** Extensions worth substituting into. Anything else is copied byte for byte. */\nconst TEXT_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".mjs\", \".json\", \".css\", \".md\", \".txt\"];\n\nfunction isText(path: string): boolean {\n return TEXT_EXTENSIONS.some((extension) => path.endsWith(extension)) || !path.includes(\".\");\n}\n\nexport function substitute(content: string, replacements: Replacements): string {\n let result = content;\n for (const [key, value] of Object.entries(replacements)) {\n result = result.replaceAll(`__${key}__`, value);\n }\n return result;\n}\n\n/**\n * Copies one template layer over a destination, substituting as it goes.\n *\n * Layers are applied in order and a later one overwrites an earlier one, which\n * is how `saas` replaces the base landing page without the base having to know\n * that anything might.\n */\nexport function copyLayer(from: string, to: string, replacements: Replacements): string[] {\n const written: string[] = [];\n\n const walk = (source: string, target: string, prefix: string): void => {\n mkdirSync(target, { recursive: true });\n\n for (const entry of readdirSync(source, { withFileTypes: true })) {\n const name = RENAME_ON_COPY[entry.name] ?? entry.name;\n const sourcePath = join(source, entry.name);\n const targetPath = join(target, name);\n const relative = prefix ? `${prefix}/${name}` : name;\n\n if (entry.isDirectory()) {\n walk(sourcePath, targetPath, relative);\n continue;\n }\n\n if (isText(sourcePath)) {\n writeFileSync(targetPath, substitute(readFileSync(sourcePath, \"utf8\"), replacements));\n } else {\n cpSync(sourcePath, targetPath);\n }\n\n written.push(relative);\n }\n };\n\n walk(from, to, \"\");\n return written;\n}\n\n/** True when the directory does not exist, or exists and holds nothing. */\nexport function isEmptyDirectory(path: string): boolean {\n if (!existsSync(path)) return true;\n if (!statSync(path).isDirectory()) return false;\n return readdirSync(path).length === 0;\n}\n\nexport { renameSync };\n","import { execFileSync } from \"node:child_process\";\n\nexport type PackageManager = \"pnpm\" | \"npm\" | \"yarn\" | \"bun\";\n\nexport const PACKAGE_MANAGERS: PackageManager[] = [\"pnpm\", \"npm\", \"yarn\", \"bun\"];\n\nexport function isPackageManager(value: string): value is PackageManager {\n return (PACKAGE_MANAGERS as string[]).includes(value);\n}\n\n/**\n * Which package manager invoked this process.\n *\n * `npm_config_user_agent` is set by every one of them, and it is the only\n * reliable signal: someone running `pnpm create dowel-app` wants pnpm, and\n * asking them again is asking a question the environment already answered.\n */\nexport function detectPackageManager(): PackageManager {\n const agent = process.env.npm_config_user_agent ?? \"\";\n\n for (const candidate of PACKAGE_MANAGERS) {\n if (agent.startsWith(`${candidate}/`)) return candidate;\n }\n\n return \"npm\";\n}\n\nexport function installCommand(manager: PackageManager): string {\n return manager === \"npm\" ? \"npm install\" : `${manager} install`;\n}\n\nexport function runCommand(manager: PackageManager, script: string): string {\n return manager === \"npm\" ? `npm run ${script}` : `${manager} ${script}`;\n}\n\n/** The runner that executes a package's binary without installing it globally. */\nexport function dlx(manager: PackageManager): string[] {\n switch (manager) {\n case \"pnpm\":\n return [\"pnpm\", \"dlx\"];\n case \"yarn\":\n return [\"yarn\", \"dlx\"];\n case \"bun\":\n return [\"bunx\"];\n default:\n return [\"npx\", \"-y\"];\n }\n}\n\nexport function install(manager: PackageManager, cwd: string): void {\n const [command, ...args] = installCommand(manager).split(\" \");\n execFileSync(command ?? \"npm\", args, { cwd, stdio: \"inherit\" });\n}\n\n/** Runs the component CLI in the new project. */\nexport function runDowel(\n manager: PackageManager,\n cwd: string,\n cliPackage: string,\n args: string[],\n): void {\n const [command, ...runner] = dlx(manager);\n execFileSync(command ?? \"npx\", [...runner, cliPackage, ...args], { cwd, stdio: \"inherit\" });\n}\n","import pc from \"picocolors\";\n\n/**\n * All CLI output goes through here.\n *\n * A single place to route messages means the format stays consistent, and\n * anything that needs to change later — quiet mode, JSON output, writing to\n * stderr — changes in one file rather than in every command.\n */\nexport const logger = {\n info(message: string) {\n console.log(message);\n },\n success(message: string) {\n console.log(`${pc.green(\"✓\")} ${message}`);\n },\n warn(message: string) {\n console.warn(`${pc.yellow(\"!\")} ${message}`);\n },\n error(message: string) {\n console.error(`${pc.red(\"✕\")} ${message}`);\n },\n step(message: string) {\n console.log(`${pc.dim(\"·\")} ${message}`);\n },\n blank() {\n console.log(\"\");\n },\n};\n\nexport { pc };\n","import { branding } from \"./branding\";\n\n/**\n * What a template is, here.\n *\n * A directory of application files, plus a list of registry items to install\n * into it. The components are *not* in the template — they are fetched from the\n * registry at creation time by the same CLI a user would run themselves.\n *\n * That is the whole design. A template that carries its own copy of Button is a\n * copy that is wrong by the next release, and the person who generated from it\n * has no way to know. Fetching means a project created today is built from\n * today's registry, and means a template is a dozen files rather than a hundred.\n */\n\nexport interface Template {\n id: string;\n title: string;\n /** One line, shown in the picker. */\n description: string;\n /**\n * Template directories layered in order, so shared files are written once.\n * Later directories overwrite earlier ones.\n */\n layers: string[];\n /** Registry items installed with `add` after the files are written. */\n items: string[];\n /** Routes the template ships, for the \"what next\" summary. */\n routes: string[];\n}\n\nexport const TEMPLATES: Template[] = [\n {\n id: \"starter\",\n title: \"Starter\",\n description: `A Next.js app wired to ${branding.libraryName}: tokens, aliases and a landing page.`,\n layers: [\"base\", \"starter\"],\n items: [\"button\", \"card\", \"badge\"],\n routes: [\"/\"],\n },\n {\n id: \"saas\",\n title: \"SaaS\",\n description:\n \"Adds an application shell with dashboard, analytics, billing, settings and onboarding.\",\n layers: [\"base\", \"app-shell\", \"saas\"],\n items: [\"sidebar\", \"dashboard\", \"analytics\", \"billing\", \"settings\", \"onboarding\"],\n routes: [\"/\", \"/app\", \"/app/analytics\", \"/app/billing\", \"/app/settings\"],\n },\n {\n id: \"ai\",\n title: \"AI product\",\n description: \"Adds a chat surface, an agent console and a usage dashboard.\",\n layers: [\"base\", \"app-shell\", \"ai\"],\n items: [\"sidebar\", \"ai-chat\", \"agent-console\", \"ai-dashboard\"],\n routes: [\"/\", \"/app\", \"/app/agents\", \"/app/usage\"],\n },\n];\n\nexport function findTemplate(id: string): Template | undefined {\n return TEMPLATES.find((template) => template.id === id);\n}\n\n/** Presets the scaffolder offers, mirroring what the theme layer ships. */\nexport const THEMES = [\n \"default\",\n \"ocean\",\n \"emerald\",\n \"violet\",\n \"rose\",\n \"amber\",\n \"monochrome\",\n] as const;\n\nexport type Theme = (typeof THEMES)[number];\n\nexport function isTheme(value: string): value is Theme {\n return (THEMES as readonly string[]).includes(value);\n}\n","import * as prompts from \"@clack/prompts\";\nimport { existsSync, mkdirSync, readdirSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { branding } from \"./branding\";\nimport { CreateError } from \"./lib/errors\";\nimport { copyLayer, isEmptyDirectory, type Replacements } from \"./lib/files\";\nimport {\n detectPackageManager,\n install,\n installCommand,\n isPackageManager,\n runCommand,\n runDowel,\n type PackageManager,\n} from \"./lib/pm\";\nimport { logger, pc } from \"./lib/logger\";\nimport { findTemplate, isTheme, TEMPLATES, THEMES, type Template } from \"./templates\";\n\nexport interface CreateOptions {\n /** Directory to create, relative to cwd or absolute. */\n directory?: string;\n template?: string;\n theme?: string;\n packageManager?: string;\n /** Accept every default and never prompt. */\n yes: boolean;\n skipInstall: boolean;\n /** Write files but do not fetch components. Mostly for tests. */\n skipComponents: boolean;\n cwd: string;\n}\n\n/** Where the shipped templates live, relative to the built entry point. */\nconst templatesRoot = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"templates\");\n\n/**\n * npm's rules for a package name, which is what this becomes.\n *\n * Checked before anything is written rather than after: a directory created and\n * then abandoned because the name was rejected is worse than a question asked\n * twice.\n */\nexport function validateProjectName(name: string): string | undefined {\n if (name.length === 0) return \"Give the project a name.\";\n if (name.length > 214) return \"That is longer than npm allows for a package name.\";\n if (name.startsWith(\".\") || name.startsWith(\"_\")) {\n return \"A package name cannot start with a dot or an underscore.\";\n }\n if (name !== name.toLowerCase()) return \"A package name has to be lowercase.\";\n if (!/^[a-z0-9._-]+$/.test(name)) {\n return \"Use lowercase letters, digits, dots, hyphens and underscores only.\";\n }\n return undefined;\n}\n\n/** The last segment of a path, as a package name. */\nexport function projectNameFrom(directory: string): string {\n return directory.split(\"/\").filter(Boolean).pop() ?? \"app\";\n}\n\n/** The nav for the app shell, written into its layout. */\nfunction appLinks(template: Template): string {\n const labels: Record<string, string> = {\n \"/app\": template.id === \"ai\" ? \"Chat\" : \"Dashboard\",\n \"/app/analytics\": \"Analytics\",\n \"/app/billing\": \"Billing\",\n \"/app/settings\": \"Settings\",\n \"/app/agents\": \"Agents\",\n \"/app/usage\": \"Usage\",\n };\n\n const links = template.routes\n .filter((route) => route !== \"/\")\n .map((route) => ` { href: \"${route}\", label: \"${labels[route] ?? route}\" },`);\n\n return `[\\n${links.join(\"\\n\")}\\n]`;\n}\n\nexport async function create(options: CreateOptions): Promise<void> {\n const interactive = !options.yes;\n\n if (interactive) {\n prompts.intro(`${branding.libraryName} — create an app`);\n }\n\n const directory = await resolveDirectory(options, interactive);\n const target = isAbsolute(directory) ? directory : resolve(options.cwd, directory);\n const name = projectNameFrom(directory);\n\n const invalid = validateProjectName(name);\n if (invalid) throw new CreateError(invalid);\n\n if (!isEmptyDirectory(target)) {\n throw new CreateError(\n `${directory} already exists and is not empty.`,\n \"Choose another name, or empty the directory first.\",\n );\n }\n\n const template = await resolveTemplate(options, interactive);\n const theme = await resolveTheme(options, interactive);\n const manager = resolvePackageManager(options);\n\n const replacements: Replacements = {\n PROJECT_NAME: name,\n LIBRARY_NAME: branding.libraryName,\n CLI_PACKAGE: branding.cliPackage,\n DOCS_URL: branding.docsUrl,\n THEME: theme,\n APP_LINKS: appLinks(template),\n };\n\n mkdirSync(target, { recursive: true });\n\n const written: string[] = [];\n for (const layer of template.layers) {\n const from = join(templatesRoot, layer);\n if (!existsSync(from)) {\n throw new CreateError(\n `The ${layer} template is missing from this installation.`,\n \"Reinstall create-dowel-app, or report this if it persists.\",\n );\n }\n written.push(...copyLayer(from, target, replacements));\n }\n\n logger.blank();\n logger.success(`Created ${pc.bold(name)} from the ${pc.bold(template.title)} template.`);\n logger.info(pc.dim(` ${String(new Set(written).size)} files in ${directory}`));\n\n if (!options.skipInstall) {\n logger.blank();\n logger.step(`Installing dependencies with ${manager}`);\n install(manager, target);\n }\n\n if (!options.skipComponents) {\n logger.blank();\n logger.step(\"Fetching components from the registry\");\n\n // Through the real CLI, not a bundled copy. A template that carried its own\n // Button would be carrying whichever Button was current the day it was\n // written, and nothing would ever say so.\n runDowel(manager, target, branding.cliPackage, [\"init\", \"--yes\", \"--skip-install\"]);\n runDowel(manager, target, branding.cliPackage, [\n \"add\",\n ...template.items,\n \"--yes\",\n ...(options.skipInstall ? [\"--skip-install\"] : []),\n ]);\n }\n\n summarise({ directory, template, theme, manager, options });\n}\n\ninterface SummaryContext {\n directory: string;\n template: Template;\n theme: string;\n manager: PackageManager;\n options: CreateOptions;\n}\n\nfunction summarise({ directory, template, theme, manager, options }: SummaryContext): void {\n logger.blank();\n logger.success(\"Done.\");\n logger.blank();\n\n logger.info(pc.dim(\"Next:\"));\n logger.info(` cd ${directory}`);\n if (options.skipInstall) logger.info(` ${installCommand(manager)}`);\n logger.info(` ${runCommand(manager, \"dev\")}`);\n\n logger.blank();\n logger.info(pc.dim(\"Routes:\"));\n for (const route of template.routes) logger.info(` ${route}`);\n\n logger.blank();\n logger.info(\n pc.dim(\n `Theme: ${theme}. Change it on <html data-theme> in src/app/layout.tsx — no component file changes.`,\n ),\n );\n logger.info(\n pc.dim(`Teach your coding agent what is installed: npx ${branding.cliPackage} agents`),\n );\n}\n\nasync function resolveDirectory(options: CreateOptions, interactive: boolean): Promise<string> {\n if (options.directory) return options.directory;\n if (!interactive) {\n throw new CreateError(\n \"No directory given.\",\n \"Pass one, e.g. `create-dowel-app my-app`, or drop --yes to be asked.\",\n );\n }\n\n const answer = await prompts.text({\n message: \"Where should it go?\",\n placeholder: \"my-app\",\n defaultValue: \"my-app\",\n validate: (value) => validateProjectName(projectNameFrom(value || \"my-app\")),\n });\n\n if (prompts.isCancel(answer)) throw new CreateError(\"Cancelled — nothing was written.\");\n return answer || \"my-app\";\n}\n\nasync function resolveTemplate(\n options: CreateOptions,\n interactive: boolean,\n): Promise<Template> {\n if (options.template) {\n const found = findTemplate(options.template);\n if (!found) {\n throw new CreateError(\n `Unknown template \"${options.template}\".`,\n `Choose from: ${TEMPLATES.map((entry) => entry.id).join(\", \")}.`,\n );\n }\n return found;\n }\n\n if (!interactive) return TEMPLATES[0]!;\n\n const answer = await prompts.select({\n message: \"What are you building?\",\n options: TEMPLATES.map((entry) => ({\n value: entry.id,\n label: entry.title,\n hint: entry.description,\n })),\n });\n\n if (prompts.isCancel(answer)) throw new CreateError(\"Cancelled — nothing was written.\");\n return findTemplate(answer) ?? TEMPLATES[0]!;\n}\n\nasync function resolveTheme(options: CreateOptions, interactive: boolean): Promise<string> {\n if (options.theme) {\n if (!isTheme(options.theme)) {\n throw new CreateError(\n `Unknown theme \"${options.theme}\".`,\n `Choose from: ${THEMES.join(\", \")}.`,\n );\n }\n return options.theme;\n }\n\n if (!interactive) return \"default\";\n\n const answer = await prompts.select({\n message: \"Which theme?\",\n options: THEMES.map((entry) => ({\n value: entry,\n label: entry,\n hint:\n entry === \"monochrome\"\n ? \"No colour at all — a standing check that nothing relies on it\"\n : undefined,\n })),\n });\n\n if (prompts.isCancel(answer)) throw new CreateError(\"Cancelled — nothing was written.\");\n return answer;\n}\n\nfunction resolvePackageManager(options: CreateOptions): PackageManager {\n if (!options.packageManager) return detectPackageManager();\n\n if (!isPackageManager(options.packageManager)) {\n throw new CreateError(\n `Unknown package manager \"${options.packageManager}\".`,\n \"Choose from: pnpm, npm, yarn, bun.\",\n );\n }\n\n return options.packageManager;\n}\n\nexport { readdirSync };\n","#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\n\nimport { Command } from \"commander\";\n\nimport { branding } from \"./branding\";\nimport { create } from \"./create\";\nimport { CreateError } from \"./lib/errors\";\nimport { logger, pc } from \"./lib/logger\";\nimport { TEMPLATES, THEMES } from \"./templates\";\n\nconst { version } = JSON.parse(\n readFileSync(new URL(\"../package.json\", import.meta.url), \"utf8\"),\n) as { version: string };\n\nconst program = new Command();\n\nprogram\n .name(\"create-dowel-app\")\n .description(\n `Creates a Next.js application wired to ${branding.libraryName}, with the components ` +\n `fetched from the registry rather than copied out of a template.`,\n )\n .version(version)\n .argument(\"[directory]\", \"where to create it\")\n .option(\"-t, --template <name>\", `one of: ${TEMPLATES.map((entry) => entry.id).join(\", \")}`)\n .option(\"--theme <name>\", `one of: ${THEMES.join(\", \")}`)\n .option(\"--pm <manager>\", \"pnpm, npm, yarn or bun; detected from how this was run\")\n .option(\"-y, --yes\", \"accept every default and never prompt\", false)\n .option(\"--skip-install\", \"write files but do not install dependencies\", false)\n .option(\"--skip-components\", \"write files but do not fetch components\", false)\n .action(\n async (\n directory: string | undefined,\n options: {\n template?: string;\n theme?: string;\n pm?: string;\n yes: boolean;\n skipInstall: boolean;\n skipComponents: boolean;\n },\n ) => {\n await create({\n directory,\n template: options.template,\n theme: options.theme,\n packageManager: options.pm,\n yes: options.yes,\n skipInstall: options.skipInstall,\n skipComponents: options.skipComponents,\n cwd: process.cwd(),\n });\n },\n );\n\n/**\n * A CreateError is a message for the person running the command; anything else\n * is a bug, and its stack trace is the useful part.\n */\nasync function main(): Promise<void> {\n try {\n await program.parseAsync(process.argv);\n } catch (error) {\n logger.blank();\n if (error instanceof CreateError) {\n logger.error(error.message);\n if (error.hint) logger.info(pc.dim(` ${error.hint}`));\n } else {\n logger.error(\"Something went wrong.\");\n logger.info(String(error instanceof Error ? (error.stack ?? error.message) : error));\n }\n logger.blank();\n process.exitCode = 1;\n }\n}\n\nvoid main();\n\nexport { create };\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-dowel-app",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Creates a Next.js application wired to Dowel: design tokens, path aliases and the blocks for the kind of product you are building, installed as source you own.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"create",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"starter",
|
|
10
|
+
"template",
|
|
11
|
+
"next",
|
|
12
|
+
"nextjs",
|
|
13
|
+
"react",
|
|
14
|
+
"ui",
|
|
15
|
+
"components",
|
|
16
|
+
"design-system",
|
|
17
|
+
"tailwind",
|
|
18
|
+
"saas",
|
|
19
|
+
"ai",
|
|
20
|
+
"dowel"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"homepage": "https://dowel-eight.vercel.app",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/aqkprogrammer/dowel-ui.git",
|
|
27
|
+
"directory": "packages/create-dowel-app"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/aqkprogrammer/dowel-ui/issues"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"templates"
|
|
35
|
+
],
|
|
36
|
+
"bin": {
|
|
37
|
+
"create-dowel-app": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"default": "./dist/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@clack/prompts": "1.7.0",
|
|
50
|
+
"commander": "15.0.0",
|
|
51
|
+
"picocolors": "1.1.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "26.4.0",
|
|
55
|
+
"tsdown": "0.22.14",
|
|
56
|
+
"typescript": "6.0.3",
|
|
57
|
+
"vitest": "4.1.10",
|
|
58
|
+
"@dowel-ui/config": "0.7.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsdown",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"clean": "rm -rf dist .turbo"
|
|
65
|
+
}
|
|
66
|
+
}
|