create-destack 0.55.2
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/README.md +38 -0
- package/bin/create-destack.mjs +538 -0
- package/package.json +43 -0
- package/templates/app/README.md +3 -0
- package/templates/app/dsconfig.json +12 -0
- package/templates/app/package.json +16 -0
- package/templates/app/src/index.ts +2 -0
- package/templates/app/src/main.ds +7 -0
- package/templates/app/tsconfig.json +12 -0
- package/templates/empty/README.md +3 -0
- package/templates/empty/package.json +10 -0
- package/templates/empty/src/.gitkeep +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# create-destack
|
|
2
|
+
|
|
3
|
+
Create a new Destack app.
|
|
4
|
+
This package powers `npm create destack`.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm create destack@latest my-app
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm create destack@latest my-app
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
yarn create destack my-app
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bun create destack my-app
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The default template is `app`.
|
|
25
|
+
Use `--template empty` for a minimal starter.
|
|
26
|
+
|
|
27
|
+
## Options
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm create destack@latest my-app -- --template app --yes
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `-t, --template <name>`: Select `app` or `empty`.
|
|
34
|
+
- `-p, --package-manager <name>`: Select `npm`, `pnpm`, `yarn`, or `bun`.
|
|
35
|
+
- `--overwrite`: Allow writing into a non-empty target directory.
|
|
36
|
+
- `--dry-run`: Print the planned actions without writing files.
|
|
37
|
+
- `-y, --yes`: Skip prompts.
|
|
38
|
+
- `-h, --help`: Show CLI help.
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { access, copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { constants as fsConstants, readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TEMPLATE = "app";
|
|
11
|
+
const DEFAULT_TARGET_DIRECTORY = "destack-app";
|
|
12
|
+
const TEMPLATES = new Map([
|
|
13
|
+
["app", "Application starter with src/main.ds and dsconfig.json"],
|
|
14
|
+
["empty", "Minimal starter with package and source folder"],
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Read the package version from the local package manifest.
|
|
19
|
+
*/
|
|
20
|
+
function readPackageVersion() {
|
|
21
|
+
// load package metadata from this package directory
|
|
22
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const packageJsonPath = path.resolve(scriptDirectory, "../package.json");
|
|
24
|
+
const packageJson = readFileSync(packageJsonPath, "utf8");
|
|
25
|
+
const packageData = JSON.parse(packageJson);
|
|
26
|
+
const packageVersion = packageData.version;
|
|
27
|
+
|
|
28
|
+
// require a valid version for deterministic cli output
|
|
29
|
+
if (typeof packageVersion !== "string" || packageVersion.trim().length === 0) {
|
|
30
|
+
throw new Error(`missing version in ${packageJsonPath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return packageVersion;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const VERSION = readPackageVersion();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Print usage help for the initializer command.
|
|
40
|
+
*/
|
|
41
|
+
function printHelp() {
|
|
42
|
+
// render template names for the usage line
|
|
43
|
+
const templateNames = [...TEMPLATES.keys()].join("|");
|
|
44
|
+
|
|
45
|
+
// print help content
|
|
46
|
+
console.log(`create-destack v${VERSION}`);
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log("Usage:");
|
|
49
|
+
console.log(` npm create destack@latest [target-directory] [-- --template <${templateNames}>] [--yes]`);
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log("Options:");
|
|
52
|
+
console.log(` -t, --template <name> Template to use (default: ${DEFAULT_TEMPLATE})`);
|
|
53
|
+
console.log(" -p, --package-manager Package manager for next steps (npm|pnpm|yarn|bun)");
|
|
54
|
+
console.log(" --overwrite Overwrite files in a non-empty target directory");
|
|
55
|
+
console.log(" --dry-run Print planned actions without writing files");
|
|
56
|
+
console.log(" -y, --yes Skip prompts");
|
|
57
|
+
console.log(" -h, --help Show help");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse cli arguments into a normalized options object.
|
|
62
|
+
*/
|
|
63
|
+
function parseArgs(argv) {
|
|
64
|
+
// defaults
|
|
65
|
+
let targetDirectory = "";
|
|
66
|
+
let template = DEFAULT_TEMPLATE;
|
|
67
|
+
let is_yes = false;
|
|
68
|
+
let is_dry_run = false;
|
|
69
|
+
let is_overwrite = false;
|
|
70
|
+
let is_template_explicit = false;
|
|
71
|
+
let packageManager = "";
|
|
72
|
+
|
|
73
|
+
// scan args once and capture flags and positional values
|
|
74
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
75
|
+
const argument = argv[index];
|
|
76
|
+
|
|
77
|
+
// exit early when help was requested
|
|
78
|
+
if (argument === "-h" || argument === "--help") {
|
|
79
|
+
return { is_help: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// enable non interactive mode
|
|
83
|
+
if (argument === "-y" || argument === "--yes") {
|
|
84
|
+
is_yes = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// enable dry run output only mode
|
|
89
|
+
if (argument === "--dry-run") {
|
|
90
|
+
is_dry_run = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// allow overwriting non empty target directories
|
|
95
|
+
if (argument === "--overwrite") {
|
|
96
|
+
is_overwrite = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// capture explicit template choice
|
|
101
|
+
if (argument === "-t" || argument === "--template") {
|
|
102
|
+
template = argv[index + 1] ?? DEFAULT_TEMPLATE;
|
|
103
|
+
is_template_explicit = true;
|
|
104
|
+
index += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// capture explicit package manager choice
|
|
109
|
+
if (argument === "--package-manager" || argument === "-p") {
|
|
110
|
+
packageManager = (argv[index + 1] ?? "").trim();
|
|
111
|
+
index += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// use the first positional arg as target directory
|
|
116
|
+
if (!argument.startsWith("-") && targetDirectory.length === 0) {
|
|
117
|
+
targetDirectory = argument;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// return normalized parse result
|
|
122
|
+
return {
|
|
123
|
+
is_help: false,
|
|
124
|
+
is_dry_run,
|
|
125
|
+
is_overwrite,
|
|
126
|
+
is_template_explicit,
|
|
127
|
+
is_yes,
|
|
128
|
+
packageManager,
|
|
129
|
+
targetDirectory,
|
|
130
|
+
template,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Normalize a target directory string for filesystem operations.
|
|
136
|
+
*/
|
|
137
|
+
function formatTargetDirectory(directory) {
|
|
138
|
+
return directory.trim().replace(/\/+$/g, "") || ".";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Assert that a template name is supported by this initializer.
|
|
143
|
+
*/
|
|
144
|
+
function assertTemplate(templateName) {
|
|
145
|
+
if (!TEMPLATES.has(templateName)) {
|
|
146
|
+
const supported = [...TEMPLATES.keys()].join(", ");
|
|
147
|
+
throw new Error(`unknown template: ${templateName}. Supported templates: ${supported}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve package manager from explicit args or npm user agent.
|
|
153
|
+
*/
|
|
154
|
+
function detectPackageManager(explicitPackageManager) {
|
|
155
|
+
// prefer explicitly passed value
|
|
156
|
+
const normalizedExplicitPackageManager = explicitPackageManager.toLowerCase();
|
|
157
|
+
if (normalizedExplicitPackageManager.length > 0) {
|
|
158
|
+
return normalizedExplicitPackageManager;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// otherwise infer from npm user agent prefix
|
|
162
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
163
|
+
if (userAgent.startsWith("pnpm/")) {
|
|
164
|
+
return "pnpm";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (userAgent.startsWith("yarn/")) {
|
|
168
|
+
return "yarn";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (userAgent.startsWith("bun/")) {
|
|
172
|
+
return "bun";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (userAgent.startsWith("npm/")) {
|
|
176
|
+
return "npm";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// default to npm for unknown agents
|
|
180
|
+
return "npm";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve install and dev commands for a package manager.
|
|
185
|
+
*/
|
|
186
|
+
function resolveNextStepCommands(packageManager) {
|
|
187
|
+
switch (packageManager) {
|
|
188
|
+
case "pnpm":
|
|
189
|
+
return { install: "pnpm install", dev: "pnpm dev" };
|
|
190
|
+
case "yarn":
|
|
191
|
+
return { install: "yarn", dev: "yarn dev" };
|
|
192
|
+
case "bun":
|
|
193
|
+
return { install: "bun install", dev: "bun run dev" };
|
|
194
|
+
case "npm":
|
|
195
|
+
return { install: "npm install", dev: "npm run dev" };
|
|
196
|
+
default:
|
|
197
|
+
throw new Error(`unsupported package manager: ${packageManager}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate whether a string is a legal npm package name.
|
|
203
|
+
*/
|
|
204
|
+
function isValidPackageName(packageName) {
|
|
205
|
+
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/[a-z0-9-~][a-z0-9-._~]*|[a-z0-9-~][a-z0-9-._~]*)$/.test(
|
|
206
|
+
packageName,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Convert an arbitrary string into a safer npm package name.
|
|
212
|
+
*/
|
|
213
|
+
function toValidPackageName(packageName) {
|
|
214
|
+
return packageName
|
|
215
|
+
.toLowerCase()
|
|
216
|
+
.trim()
|
|
217
|
+
.replace(/\s+/g, "-")
|
|
218
|
+
.replace(/^[._]+/, "")
|
|
219
|
+
.replace(/[^a-z0-9-~]+/g, "-")
|
|
220
|
+
.replace(/^-+/, "")
|
|
221
|
+
.replace(/-+$/, "");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Derive a package name from the target directory.
|
|
226
|
+
*/
|
|
227
|
+
function derivePackageName(targetDirectory) {
|
|
228
|
+
// use cwd basename when writing into current directory
|
|
229
|
+
if (targetDirectory === ".") {
|
|
230
|
+
return path.basename(process.cwd());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// otherwise use the target directory basename
|
|
234
|
+
return path.basename(targetDirectory);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Assert that a template directory can be read.
|
|
239
|
+
*/
|
|
240
|
+
async function ensureTemplateExists(templateDirectory) {
|
|
241
|
+
try {
|
|
242
|
+
await access(templateDirectory, fsConstants.R_OK);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
throw new Error(`template directory does not exist: ${templateDirectory}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Return whether a directory does not contain any entries.
|
|
251
|
+
*/
|
|
252
|
+
async function isDirectoryEmpty(directory) {
|
|
253
|
+
try {
|
|
254
|
+
const entries = await readdir(directory);
|
|
255
|
+
return entries.length === 0;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Delete all entries inside a directory.
|
|
264
|
+
*/
|
|
265
|
+
async function emptyDirectory(directory) {
|
|
266
|
+
const entries = await readdir(directory);
|
|
267
|
+
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
await rm(path.join(directory, entry), { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Copy one directory tree recursively to a destination.
|
|
275
|
+
*/
|
|
276
|
+
async function copyDirectory(sourceDirectory, destinationDirectory) {
|
|
277
|
+
// create destination path first
|
|
278
|
+
await mkdir(destinationDirectory, { recursive: true });
|
|
279
|
+
|
|
280
|
+
// walk source directory entries
|
|
281
|
+
const entries = await readdir(sourceDirectory, { withFileTypes: true });
|
|
282
|
+
for (const entry of entries) {
|
|
283
|
+
const sourcePath = path.join(sourceDirectory, entry.name);
|
|
284
|
+
const destinationPath = path.join(destinationDirectory, entry.name);
|
|
285
|
+
|
|
286
|
+
// recurse into subdirectories
|
|
287
|
+
if (entry.isDirectory()) {
|
|
288
|
+
await copyDirectory(sourcePath, destinationPath);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// copy regular files
|
|
293
|
+
await copyFile(sourcePath, destinationPath);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Write the resolved package name into the generated package file.
|
|
299
|
+
*/
|
|
300
|
+
async function writePackageName(projectDirectory, packageName) {
|
|
301
|
+
// skip templates that do not ship a package manifest
|
|
302
|
+
const packageJsonPath = path.join(projectDirectory, "package.json");
|
|
303
|
+
try {
|
|
304
|
+
await access(packageJsonPath, fsConstants.F_OK);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// load package json and rewrite the name field
|
|
311
|
+
const packageJson = await readFile(packageJsonPath, "utf8");
|
|
312
|
+
const packageData = JSON.parse(packageJson);
|
|
313
|
+
packageData.name = packageName;
|
|
314
|
+
|
|
315
|
+
// persist normalized package json with trailing newline
|
|
316
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageData, null, 2)}\n`, "utf8");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Print post init commands for the selected package manager.
|
|
321
|
+
*/
|
|
322
|
+
function printNextSteps(targetDirectory, nextSteps) {
|
|
323
|
+
// print section header
|
|
324
|
+
console.log("\nNext steps:");
|
|
325
|
+
|
|
326
|
+
// print directory change only for non dot targets
|
|
327
|
+
if (targetDirectory !== ".") {
|
|
328
|
+
console.log(` cd ${targetDirectory}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// print install and run commands
|
|
332
|
+
console.log(` ${nextSteps.install}`);
|
|
333
|
+
console.log(` ${nextSteps.dev}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Prompt for target and template when interactive mode is enabled.
|
|
338
|
+
*/
|
|
339
|
+
async function promptForInitialization(initialTargetDirectory, initialTemplate, is_template_prompt_enabled) {
|
|
340
|
+
// open prompt reader
|
|
341
|
+
const reader = createInterface({
|
|
342
|
+
input: process.stdin,
|
|
343
|
+
output: process.stdout,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
// seed with incoming defaults
|
|
348
|
+
let targetDirectory = initialTargetDirectory;
|
|
349
|
+
let templateName = initialTemplate;
|
|
350
|
+
|
|
351
|
+
// prompt for target directory when not provided
|
|
352
|
+
if (targetDirectory.length === 0) {
|
|
353
|
+
const answer = await reader.question(`Target directory (${DEFAULT_TARGET_DIRECTORY}): `);
|
|
354
|
+
targetDirectory = formatTargetDirectory(answer.length > 0 ? answer : DEFAULT_TARGET_DIRECTORY);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// prompt for template when no explicit template flag was passed
|
|
358
|
+
if (is_template_prompt_enabled) {
|
|
359
|
+
const supported = [...TEMPLATES.keys()].join(", ");
|
|
360
|
+
const answer = await reader.question(`Template (${templateName}) [${supported}]: `);
|
|
361
|
+
const candidate = answer.trim();
|
|
362
|
+
|
|
363
|
+
if (candidate.length > 0) {
|
|
364
|
+
templateName = candidate;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { targetDirectory, templateName };
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
// always close prompt reader
|
|
372
|
+
reader.close();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Prompt whether a non empty directory can be overwritten.
|
|
378
|
+
*/
|
|
379
|
+
async function promptForOverwrite(targetDirectory) {
|
|
380
|
+
// open prompt reader
|
|
381
|
+
const reader = createInterface({
|
|
382
|
+
input: process.stdin,
|
|
383
|
+
output: process.stdout,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
// ask overwrite confirmation and map to bool
|
|
388
|
+
const answer = await reader.question(
|
|
389
|
+
`Target directory "${targetDirectory}" is not empty. Overwrite existing files? (y/N): `,
|
|
390
|
+
);
|
|
391
|
+
return answer.trim().toLowerCase() === "y";
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
// always close prompt reader
|
|
395
|
+
reader.close();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Prompt for a valid package name when derived name is invalid.
|
|
401
|
+
*/
|
|
402
|
+
async function promptForPackageName(initialPackageName) {
|
|
403
|
+
// open prompt reader
|
|
404
|
+
const reader = createInterface({
|
|
405
|
+
input: process.stdin,
|
|
406
|
+
output: process.stdout,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
// use sanitized name as default suggestion
|
|
411
|
+
const suggestedName = toValidPackageName(initialPackageName) || "destack-app";
|
|
412
|
+
const answer = await reader.question(`Package name (${suggestedName}): `);
|
|
413
|
+
const candidateName = answer.trim().length > 0 ? answer.trim() : suggestedName;
|
|
414
|
+
|
|
415
|
+
// enforce npm package naming rules
|
|
416
|
+
if (!isValidPackageName(candidateName)) {
|
|
417
|
+
throw new Error(`invalid package name: ${candidateName}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return candidateName;
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
// always close prompt reader
|
|
424
|
+
reader.close();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Execute the initializer command.
|
|
430
|
+
*/
|
|
431
|
+
async function main() {
|
|
432
|
+
// parse cli arguments
|
|
433
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
434
|
+
|
|
435
|
+
// return early for help mode
|
|
436
|
+
if (parsed.is_help) {
|
|
437
|
+
printHelp();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// resolve target and runtime configuration
|
|
442
|
+
let targetDirectory = formatTargetDirectory(parsed.targetDirectory || DEFAULT_TARGET_DIRECTORY);
|
|
443
|
+
let templateName = parsed.template;
|
|
444
|
+
const packageManager = detectPackageManager(parsed.packageManager);
|
|
445
|
+
const nextSteps = resolveNextStepCommands(packageManager);
|
|
446
|
+
|
|
447
|
+
// prompt for missing values when interactive mode is enabled
|
|
448
|
+
if (!parsed.is_yes) {
|
|
449
|
+
const prompted = await promptForInitialization(
|
|
450
|
+
parsed.targetDirectory,
|
|
451
|
+
parsed.is_template_explicit ? templateName : DEFAULT_TEMPLATE,
|
|
452
|
+
!parsed.is_template_explicit,
|
|
453
|
+
);
|
|
454
|
+
targetDirectory = formatTargetDirectory(prompted.targetDirectory);
|
|
455
|
+
templateName = prompted.templateName;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// validate template selection
|
|
459
|
+
assertTemplate(templateName);
|
|
460
|
+
|
|
461
|
+
// derive and validate package name
|
|
462
|
+
let packageName = derivePackageName(targetDirectory);
|
|
463
|
+
if (!isValidPackageName(packageName)) {
|
|
464
|
+
// in non interactive mode: sanitize and validate
|
|
465
|
+
if (parsed.is_yes) {
|
|
466
|
+
packageName = toValidPackageName(packageName) || "destack-app";
|
|
467
|
+
|
|
468
|
+
if (!isValidPackageName(packageName)) {
|
|
469
|
+
throw new Error(`invalid package name derived from target directory: ${packageName}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// in interactive mode: ask user for a valid package name
|
|
473
|
+
else {
|
|
474
|
+
packageName = await promptForPackageName(packageName);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// resolve and validate template directory
|
|
479
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
480
|
+
const templateDirectory = path.resolve(scriptDirectory, "../templates", templateName);
|
|
481
|
+
await ensureTemplateExists(templateDirectory);
|
|
482
|
+
|
|
483
|
+
// inspect target directory state
|
|
484
|
+
const projectDirectory = path.resolve(process.cwd(), targetDirectory);
|
|
485
|
+
const isEmpty = await isDirectoryEmpty(projectDirectory);
|
|
486
|
+
|
|
487
|
+
// enforce overwrite rules for non empty directories
|
|
488
|
+
let shouldOverwrite = parsed.is_overwrite;
|
|
489
|
+
if (!isEmpty && !shouldOverwrite) {
|
|
490
|
+
// fail fast in non interactive mode
|
|
491
|
+
if (parsed.is_yes) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`target directory is not empty: ${projectDirectory}. Use --overwrite or run without --yes to confirm`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ask for overwrite confirmation in interactive mode
|
|
498
|
+
shouldOverwrite = await promptForOverwrite(targetDirectory);
|
|
499
|
+
if (!shouldOverwrite) {
|
|
500
|
+
throw new Error("operation cancelled");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// print execution plan and exit during dry run mode
|
|
505
|
+
if (parsed.is_dry_run) {
|
|
506
|
+
console.log(`\n[dry-run] Would create ${targetDirectory} using template ${templateName}.`);
|
|
507
|
+
console.log(`[dry-run] Package name: ${packageName}`);
|
|
508
|
+
|
|
509
|
+
if (!isEmpty && shouldOverwrite) {
|
|
510
|
+
console.log(`[dry-run] Would overwrite existing files in ${targetDirectory}.`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
printNextSteps(targetDirectory, nextSteps);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// create target directory before copying template content
|
|
518
|
+
await mkdir(projectDirectory, { recursive: true });
|
|
519
|
+
|
|
520
|
+
// clear target directory contents only for non dot targets
|
|
521
|
+
if (targetDirectory !== "." && !isEmpty && shouldOverwrite) {
|
|
522
|
+
await emptyDirectory(projectDirectory);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// copy template and patch package name
|
|
526
|
+
await copyDirectory(templateDirectory, projectDirectory);
|
|
527
|
+
await writePackageName(projectDirectory, packageName);
|
|
528
|
+
|
|
529
|
+
// print success summary
|
|
530
|
+
console.log(`\nCreated ${targetDirectory} using template ${templateName}.`);
|
|
531
|
+
printNextSteps(targetDirectory, nextSteps);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
main().catch((error) => {
|
|
535
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
536
|
+
console.error(`create-destack: ${message}`);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-destack",
|
|
3
|
+
"version": "0.55.2",
|
|
4
|
+
"description": "Create Destack apps",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/destack-sh/destack.git",
|
|
10
|
+
"directory": "templates/create-destack"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/destack-sh/destack/tree/main/templates/create-destack",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/destack-sh/destack/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"destack",
|
|
18
|
+
"create",
|
|
19
|
+
"initializer",
|
|
20
|
+
"template"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"create-destack": "bin/create-destack.mjs"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"bin",
|
|
27
|
+
"templates",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node ./bin/create-destack.mjs --help",
|
|
32
|
+
"test": "node ./bin/create-destack.mjs --help",
|
|
33
|
+
"prepublishOnly": "npm run build",
|
|
34
|
+
"publish:dry": "npm publish --dry-run --access public",
|
|
35
|
+
"publish:live": "npm publish --access public"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://destack.sh/schemas/dsconfig.schema.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "esnext",
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"lib": ["esnext"],
|
|
8
|
+
"rootDir": "src"
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*"],
|
|
11
|
+
"exclude": ["node_modules", "dist"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__DESTACK_PROJECT__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "bun --hot src/main.ds",
|
|
8
|
+
"start": "NODE_ENV=production bun src/main.ds"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@destack/bun": "^0.55.2"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/bun": "latest"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
File without changes
|