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 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,3 @@
1
+ # Destack App
2
+
3
+ Destack application starter.
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ // @ts-ignore
2
+ export * from "./main.ds";
@@ -0,0 +1,7 @@
1
+ /// Application entry point.
2
+
3
+ function main() {
4
+ console.log("Hello, Destack!");
5
+ }
6
+
7
+ main();
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "noImplicitAny": true,
9
+ "esModuleInterop": true
10
+ },
11
+ "include": ["**/*"]
12
+ }
@@ -0,0 +1,3 @@
1
+ # Destack App
2
+
3
+ Minimal Destack project.
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "__DESTACK_PROJECT__",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "echo 'Destack app ready'",
8
+ "build": "echo 'Build command pending'"
9
+ }
10
+ }
File without changes