create-disposabl-app 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,127 +1,513 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // cli/index.ts
3
+ // index.ts
4
4
  import { spawn, spawnSync } from "node:child_process";
5
+ import { existsSync as existsSync2 } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { Command, CommanderError } from "commander";
9
+ import * as p2 from "@clack/prompts";
10
+ // package.json
11
+ var version = "0.1.0";
12
+
13
+ // options.ts
14
+ var platforms = [
15
+ { id: "web", label: "Web", group: "Required", status: "required" },
16
+ { id: "android", label: "Android", group: "Mobile", status: "available" },
17
+ { id: "ios", label: "iOS", group: "Mobile", status: "available" },
18
+ { id: "windows", label: "Windows", group: "Desktop", status: "available" },
19
+ { id: "linux", label: "Linux", group: "Desktop", status: "available" },
20
+ { id: "macos", label: "macOS", group: "Desktop", status: "available" },
21
+ {
22
+ id: "mcp",
23
+ label: "MCP",
24
+ group: "MCP & plugins",
25
+ status: "available",
26
+ description: "Tools for AI assistants"
27
+ },
28
+ {
29
+ id: "openai-plugin",
30
+ label: "OpenAI Plugin",
31
+ description: "Tools and skills for OpenAI",
32
+ group: "MCP & plugins",
33
+ status: "available"
34
+ },
35
+ {
36
+ id: "claude-code-plugin",
37
+ label: "Claude Code Plugin",
38
+ description: "Tools and skills for Claude Code",
39
+ group: "MCP & plugins",
40
+ status: "available"
41
+ },
42
+ { id: "api", label: "API", group: "Coming soon", status: "disabled" },
43
+ { id: "cli", label: "CLI", group: "Coming soon", status: "disabled" },
44
+ {
45
+ id: "python-sdk",
46
+ label: "Python SDK",
47
+ group: "Coming soon",
48
+ status: "disabled"
49
+ },
50
+ {
51
+ id: "typescript-sdk",
52
+ label: "TypeScript SDK",
53
+ group: "Coming soon",
54
+ status: "disabled"
55
+ }
56
+ ];
57
+ var platformDependencies = {
58
+ "openai-plugin": ["mcp"],
59
+ "claude-code-plugin": ["mcp"]
60
+ };
61
+ var features = [
62
+ { id: "docs", label: "Docs", hint: "apps/docs" },
63
+ { id: "blog", label: "Blog", hint: "apps/blog" },
64
+ { id: "changelog", label: "Changelog", hint: "apps/changelog" },
65
+ {
66
+ id: "authentication",
67
+ label: "Authentication",
68
+ hint: "Record selection only"
69
+ }
70
+ ];
71
+ var parseList = (input) => input.split(",").map((value) => value.trim());
72
+ function selectPlatforms(input = "web") {
73
+ const requested = parseList(input);
74
+ for (const id of requested) {
75
+ const option = platforms.find((platform) => platform.id === id);
76
+ if (!option)
77
+ throw new Error("Unknown platform " + JSON.stringify(id) + ". Use --list-options to see valid IDs.");
78
+ if (option.status === "disabled")
79
+ throw new Error(option.label + " is not available yet and cannot be selected.");
80
+ }
81
+ for (const [plugin, dependencies] of Object.entries(platformDependencies)) {
82
+ if (requested.includes(plugin))
83
+ requested.push(...dependencies);
84
+ }
85
+ return platforms.filter((option) => option.id === "web" || requested.includes(option.id)).map((option) => option.id);
86
+ }
87
+ function selectFeatures(input = "none") {
88
+ if (input === "none")
89
+ return [];
90
+ const requested = parseList(input);
91
+ for (const id of requested) {
92
+ if (!features.some((feature) => feature.id === id)) {
93
+ throw new Error("Unknown feature " + JSON.stringify(id) + ". Use --features none or --list-options to see valid IDs.");
94
+ }
95
+ }
96
+ return features.filter((option) => requested.includes(option.id)).map((option) => option.id);
97
+ }
98
+ function selectedFolders(config) {
99
+ const result = ["apps/web"];
100
+ if (config.platforms.some((platform) => platform === "android" || platform === "ios"))
101
+ result.push("apps/mobile");
102
+ if (config.platforms.some((platform) => ["windows", "linux", "macos"].includes(platform)))
103
+ result.push("apps/desktop");
104
+ for (const feature of config.features) {
105
+ if (feature !== "authentication")
106
+ result.push("apps/" + feature);
107
+ }
108
+ return result;
109
+ }
110
+ function validateName(name) {
111
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name) || name.length > 214 || ["node_modules", "favicon.ico"].includes(name)) {
112
+ return "Use a lowercase project name with letters, numbers, dots, hyphens, or underscores.";
113
+ }
114
+ }
115
+
116
+ // platform-prompt.ts
117
+ import { MultiSelectPrompt } from "@clack/core";
118
+ import * as p from "@clack/prompts";
119
+ import color from "picocolors";
120
+ function createPlatformPrompt(streams = {}) {
121
+ return new MultiSelectPrompt({
122
+ ...streams,
123
+ options: platforms.map((option) => ({
124
+ value: option.id,
125
+ label: option.label,
126
+ disabled: option.status !== "available",
127
+ description: "description" in option ? option.description : ""
128
+ })),
129
+ required: false,
130
+ render() {
131
+ const explicit = this.value ?? [];
132
+ const selected = selectPlatforms(explicit.length ? explicit.join(",") : "web");
133
+ const title = `${p.symbol(this.state)} Choose platforms`;
134
+ const bar = color.dim(p.S_BAR);
135
+ if (this.state === "submit") {
136
+ const labels = this.options.filter((option) => selected.includes(option.value)).map((option) => option.label.trim());
137
+ return `${title}
138
+ ${bar} ${color.dim(labels.join(", ") || "Web only")}`;
139
+ }
140
+ if (this.state === "cancel")
141
+ return `${title}
142
+ ${bar} ${color.dim("Cancelled")}`;
143
+ const style = (option, active) => {
144
+ const checked = selected.includes(option.value);
145
+ const hint = option.value === "web" ? "Required" : option.disabled ? "Unavailable" : option.description + (option.value.endsWith("-plugin") ? " · Requires MCP" : "");
146
+ const checkbox = checked ? p.S_CHECKBOX_SELECTED : active ? p.S_CHECKBOX_ACTIVE : p.S_CHECKBOX_INACTIVE;
147
+ const label = `${checkbox} ${option.label}`;
148
+ return (option.value === "web" ? color.green(label) : option.disabled ? color.dim(label) : active ? color.cyan(label) : label) + (hint ? color.dim(` (${hint})`) : "");
149
+ };
150
+ const unavailable = this.options.filter((option) => option.disabled && option.value !== "web");
151
+ const rows = p.limitOptions({
152
+ options: this.options.filter((option) => !unavailable.includes(option)),
153
+ cursor: this.cursor,
154
+ output: streams.output,
155
+ columnPadding: 3,
156
+ rowPadding: 6 + unavailable.length,
157
+ style
158
+ });
159
+ return [
160
+ title,
161
+ ...rows.map((row) => `${bar} ${row}`),
162
+ ...unavailable.map((option) => `${bar} ${style(option, false)}`),
163
+ `${bar} ${color.dim("↑/↓ move · Space select · Enter confirm")}`,
164
+ color.dim(p.S_BAR_END)
165
+ ].join(`
166
+ `);
167
+ }
168
+ });
169
+ }
170
+
171
+ // project-setup.ts
172
+ import { clearScreenDown, cursorTo, moveCursor } from "node:readline";
173
+ import color2 from "picocolors";
174
+ import { performance } from "node:perf_hooks";
175
+ function setupTable(name, config, status, columns = 80) {
176
+ const installation = status.installation + (status.elapsedSeconds === undefined ? "" : ` (${status.elapsedSeconds.toFixed(2)}s)`);
177
+ const rows = [
178
+ ["App", name],
179
+ ["Disposabl Version", config.version],
180
+ [
181
+ "Platforms",
182
+ config.platforms.map((id) => platforms.find((p) => p.id === id).label).join(", ")
183
+ ],
184
+ [
185
+ "Features",
186
+ config.features.map((id) => features.find((f) => f.id === id).label).join(", ") || "None"
187
+ ],
188
+ ["Installation", installation]
189
+ ];
190
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
191
+ const padding = labelWidth + 7;
192
+ const contentWidth = Math.max(46, ...rows.map(([, value]) => value.length + padding));
193
+ const width = Math.max(24, Math.min(columns - 1, 80, contentWidth));
194
+ const valueWidth = width - padding;
195
+ const border = color2.dim("─".repeat(width - 2));
196
+ const result = [color2.bold("App Setup"), `┌${border}┐`];
197
+ for (const [label, value] of rows) {
198
+ let remaining = value;
199
+ let first = true;
200
+ do {
201
+ let end = valueWidth;
202
+ if (remaining.length > end) {
203
+ const space = remaining.lastIndexOf(" ", end);
204
+ if (space > 0)
205
+ end = space;
206
+ }
207
+ const part = remaining.slice(0, end);
208
+ remaining = remaining.slice(end).trimStart();
209
+ const cell = part.padEnd(valueWidth);
210
+ const isStatus = label === "Installation";
211
+ const styled = isStatus && status.installation === "Complete" ? color2.green(cell) : isStatus && status.installation === "Failed" ? color2.red(cell) : label === "App" ? color2.bold(cell) : cell;
212
+ result.push(`│ ${(first ? label : "").padEnd(labelWidth)} │ ${styled} │`);
213
+ first = false;
214
+ } while (remaining);
215
+ }
216
+ result.push(`└${border}┘`);
217
+ return result.join(`
218
+ `);
219
+ }
220
+ function projectSetup(name, config) {
221
+ const live = Boolean(process.stdout.isTTY);
222
+ const status = { installation: "Pending" };
223
+ let lines = 0;
224
+ let started;
225
+ let timer;
226
+ function render() {
227
+ if (live && lines) {
228
+ moveCursor(process.stdout, 0, -lines);
229
+ cursorTo(process.stdout, 0);
230
+ clearScreenDown(process.stdout);
231
+ }
232
+ const frame = setupTable(name, config, status, process.stdout.columns);
233
+ process.stdout.write(frame + `
234
+ `);
235
+ lines = frame.split(`
236
+ `).length;
237
+ }
238
+ if (live)
239
+ render();
240
+ return {
241
+ live,
242
+ installing() {
243
+ started = performance.now();
244
+ status.installation = "Installing";
245
+ const tick = () => {
246
+ status.elapsedSeconds = (performance.now() - started) / 1000;
247
+ if (live)
248
+ render();
249
+ };
250
+ tick();
251
+ if (live)
252
+ timer = setInterval(tick, 100);
253
+ },
254
+ finish(next) {
255
+ clearInterval(timer);
256
+ Object.assign(status, next);
257
+ if (started !== undefined)
258
+ status.elapsedSeconds = (performance.now() - started) / 1000;
259
+ render();
260
+ }
261
+ };
262
+ }
263
+
264
+ // scaffold.ts
5
265
  import {
6
266
  cp,
7
267
  mkdir,
8
- readdir,
9
268
  readFile,
269
+ readdir,
10
270
  rename,
271
+ rm,
11
272
  writeFile
12
273
  } from "node:fs/promises";
13
- import { resolve } from "node:path";
14
- import { fileURLToPath } from "node:url";
15
- import { Command } from "commander";
16
- import prompts from "prompts";
17
- // cli/package.json
18
- var version = "0.0.4";
274
+ import { existsSync } from "node:fs";
275
+ import { join } from "node:path";
276
+ async function createProject(template, destination, name, config) {
277
+ const folders = selectedFolders(config);
278
+ for (const folder of folders) {
279
+ if (!existsSync(join(template, folder, "package.json"))) {
280
+ throw new Error("The bundled starter is missing " + folder + "/package.json. Rebuild the CLI.");
281
+ }
282
+ }
283
+ await mkdir(destination);
284
+ try {
285
+ for (const entry of await readdir(template)) {
286
+ await cp(join(template, entry), join(destination, entry), {
287
+ recursive: true,
288
+ force: false,
289
+ errorOnExist: true
290
+ });
291
+ }
292
+ for (const entry of await readdir(join(destination, "apps"))) {
293
+ if (!folders.includes("apps/" + entry))
294
+ await rm(join(destination, "apps", entry), {
295
+ recursive: true,
296
+ force: true
297
+ });
298
+ }
299
+ if (existsSync(join(destination, "gitignore"))) {
300
+ await rename(join(destination, "gitignore"), join(destination, ".gitignore"));
301
+ }
302
+ const manifestPath = join(destination, "package.json");
303
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
304
+ manifest.name = name;
305
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + `
306
+ `);
307
+ await writeFile(join(destination, "disposabl.json"), JSON.stringify(config, null, 2) + `
308
+ `);
309
+ } catch (error) {
310
+ await rm(destination, { recursive: true, force: true });
311
+ throw error;
312
+ }
313
+ return folders;
314
+ }
19
315
 
20
- // cli/index.ts
21
- var program = new Command("create-disposabl-app").description("Create a Disposabl app from the bundled starter.").version(version, "-v, --version").argument("[appname]", "Name of the new app").option("-y, --yes", "Use defaults without asking questions").option("--docs", "Include Docs").option("--skip-install", "Skip bun install").helpOption("-h, --help").parse();
22
- var validName = (name) => /^[a-z0-9][a-z0-9._-]*$/.test(name) && name.length <= 214 && !["node_modules", "favicon.ico"].includes(name);
23
- try {
24
- const options = program.opts();
25
- let name = program.args[0];
26
- let docs = Boolean(options.docs);
27
- if (process.stdin.isTTY && !options.yes) {
28
- const answers = await prompts([
29
- {
30
- type: name ? null : "text",
31
- name: "name",
32
- message: "What is your project named?",
33
- initial: "my-app",
34
- validate: (value) => validName(value) || "Use a lowercase app name without spaces."
35
- },
36
- {
37
- type: "multiselect",
38
- name: "platforms",
39
- message: "Which platforms?",
40
- choices: [
41
- {
42
- title: "Web (required)",
43
- value: "web",
44
- selected: true,
45
- disabled: true
46
- }
47
- ],
48
- min: 1,
49
- warn: "Web is required.",
50
- onRender() {
51
- for (const choice of this.value) {
52
- if (choice.value === "web")
53
- choice.selected = true;
54
- }
55
- },
56
- hint: "Web is required. Press Enter to continue."
57
- },
58
- {
59
- type: options.docs ? null : "multiselect",
60
- name: "features",
61
- message: "Which features?",
62
- choices: [{ title: "Docs", value: "docs" }],
63
- hint: "Space to select. Enter to continue without optional features."
64
- }
65
- ], {
66
- onCancel: () => {
67
- console.log("Cancelled.");
68
- process.exit(0);
69
- }
70
- });
71
- name ??= answers.name;
72
- docs ||= answers.features?.includes("docs") ?? false;
73
- }
74
- if (!name || !validName(name))
75
- throw new Error("Provide a lowercase app name, or run with --help.");
76
- if (!options.skipInstall && spawnSync("bun", ["--version"]).status !== 0)
77
- throw new Error("Install Bun from https://bun.com first.");
78
- const dir = resolve(name);
79
- await mkdir(dir);
80
- console.log(`
81
- Creating a new Disposabl app in ${dir}.
316
+ // index.ts
317
+ var jsonOutput = process.argv.slice(2).includes("--json");
318
+ var program = new Command("create-disposabl-app").description("Create an empty Disposabl starter. Web is always included.").version(version, "-v, --version").argument("[appname]", "App name and new directory").option("--platforms <ids>", "Comma-separated platform IDs; Web is always included").option("--features <ids>", "Comma-separated feature IDs, or none (default: none)").option("-y, --yes", "Skip prompts; default to Web only and no features").option("--skip-install", "Create files without running bun install").option("--list-options", "List every platform and feature, including disabled choices").option("--json", "Skip prompts and emit a JSON result or error; install logs go to stderr").helpOption("-h, --help").addHelpText("after", `
319
+ Platforms:
320
+ web (required), android, ios, windows, linux, macos, mcp,
321
+ openai-plugin, claude-code-plugin
322
+ Either plugin automatically includes MCP.
323
+ Disabled: api, cli, python-sdk, typescript-sdk
324
+
325
+ Features:
326
+ docs, blog, changelog, authentication
327
+ Authentication currently records a selection only.
82
328
 
83
- Using bun.
329
+ Examples:
330
+ bun create disposabl-app@latest
331
+ bun create disposabl-app@latest my-app --yes
332
+ bun create disposabl-app@latest my-app --platforms web,android,ios,mcp --features docs,authentication --yes
333
+ bun create disposabl-app@latest my-app --platforms web --features none --skip-install --json
334
+ bun create disposabl-app@latest --list-options --json
84
335
 
85
- Platform: Web
86
- Features: ${docs ? "Docs" : "None"}
336
+ Without a TTY, the CLI never prompts. Supply an app name.
337
+ Platform and feature choices are saved in disposabl.json.
338
+ App folders contain empty package.json files. No appId or Convex deployment is created.
339
+ `).configureOutput({ writeErr: () => {} }).exitOverride();
340
+
341
+ class Cancelled extends Error {
342
+ }
343
+ function answer(value) {
344
+ if (p2.isCancel(value))
345
+ throw new Cancelled("Cancelled. No files were created.");
346
+ return value;
347
+ }
348
+ function showOptions(json) {
349
+ if (json) {
350
+ console.log(JSON.stringify({
351
+ platforms,
352
+ platformDependencies,
353
+ features,
354
+ defaults: { platforms: ["web"], features: [] },
355
+ folders: {
356
+ web: "apps/web",
357
+ android: "apps/mobile",
358
+ ios: "apps/mobile",
359
+ windows: "apps/desktop",
360
+ linux: "apps/desktop",
361
+ macos: "apps/desktop",
362
+ docs: "apps/docs",
363
+ blog: "apps/blog",
364
+ changelog: "apps/changelog"
365
+ },
366
+ configOnly: [
367
+ "mcp",
368
+ "openai-plugin",
369
+ "claude-code-plugin",
370
+ "authentication"
371
+ ]
372
+ }, null, 2));
373
+ return;
374
+ }
375
+ console.log(`Platforms
376
+ `);
377
+ for (const option of platforms)
378
+ console.log(" " + option.id.padEnd(21) + option.label.padEnd(20) + option.status);
379
+ console.log(`
380
+ OpenAI Plugin and Claude Code Plugin automatically include MCP.`);
381
+ console.log(`
382
+ Features
87
383
  `);
88
- const template = fileURLToPath(new URL("./templates/default", import.meta.url));
89
- for (const file of await readdir(template)) {
90
- if (file === "docs" && !docs)
91
- continue;
92
- await cp(resolve(template, file), resolve(dir, file), {
93
- recursive: true,
94
- force: false,
95
- errorOnExist: true
384
+ for (const option of features)
385
+ console.log(" " + option.id.padEnd(21) + option.hint);
386
+ console.log(`
387
+ Defaults: Web only; no features. Use --platforms and --features with the IDs above.`);
388
+ }
389
+ async function install(directory, interactive) {
390
+ await new Promise((done, reject) => {
391
+ const child = spawn("bun", ["install"], {
392
+ cwd: directory,
393
+ stdio: ["ignore", "pipe", "pipe"],
394
+ env: { ...process.env, NODE_ENV: "development" }
96
395
  });
396
+ let detail = "";
397
+ const onData = (chunk) => {
398
+ detail = (detail + chunk.toString()).slice(-4000);
399
+ if (!interactive)
400
+ process.stderr.write(chunk);
401
+ };
402
+ child.stdout.on("data", onData);
403
+ child.stderr.on("data", onData);
404
+ child.once("error", reject);
405
+ child.once("close", (code) => code === 0 ? done() : reject(new Error("bun install failed. The project was created at " + directory + `. Run bun install there to retry.
406
+ ` + detail.trim())));
407
+ });
408
+ }
409
+ async function main() {
410
+ program.parse();
411
+ const options = program.opts();
412
+ if (options.listOptions)
413
+ return showOptions(Boolean(options.json));
414
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !p2.isCI() && !options.yes && !options.json);
415
+ let chosenPlatforms = selectPlatforms(options.platforms);
416
+ let chosenFeatures = selectFeatures(options.features);
417
+ let name = program.args[0];
418
+ if (interactive) {
419
+ if (!name)
420
+ name = answer(await p2.text({
421
+ message: "App name",
422
+ placeholder: "my-app",
423
+ defaultValue: "my-app",
424
+ validate: (value) => validateName(value ?? "") || (existsSync2(resolve(value ?? "")) ? "That directory already exists. Choose a new name." : undefined)
425
+ }));
97
426
  }
98
- await rename(resolve(dir, "gitignore"), resolve(dir, ".gitignore"));
99
- const manifestPath = resolve(dir, "package.json");
100
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
101
- manifest.name = name;
102
- await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
103
- `);
104
- if (!options.skipInstall) {
105
- await new Promise((done, reject) => {
106
- const child = spawn("bun", ["install"], {
107
- cwd: dir,
108
- stdio: "inherit",
109
- env: { ...process.env, NODE_ENV: "development" }
110
- });
111
- child.once("error", reject);
112
- child.once("close", (code) => code === 0 ? done() : reject(new Error(`Run bun install inside ${name} to retry.`)));
427
+ if (!name)
428
+ throw new Error("Provide an app name in non-interactive mode. Example: bun create disposabl-app@latest my-app --platforms web --features none --yes");
429
+ const invalidName = validateName(name);
430
+ if (invalidName)
431
+ throw new Error(invalidName);
432
+ const directory = resolve(name);
433
+ if (existsSync2(directory))
434
+ throw new Error("Directory already exists: " + directory + ". Choose a new name; existing files are never overwritten.");
435
+ if (interactive && options.platforms === undefined) {
436
+ const selected = answer(await createPlatformPrompt().prompt() ?? []);
437
+ chosenPlatforms = selectPlatforms(selected.length ? selected.join(",") : "web");
438
+ }
439
+ if (interactive && options.features === undefined) {
440
+ const selected = answer(await p2.multiselect({
441
+ message: "Choose features (optional)",
442
+ options: features.map((feature) => ({
443
+ value: feature.id,
444
+ label: feature.label,
445
+ hint: feature.hint
446
+ })),
447
+ required: false
448
+ }));
449
+ chosenFeatures = selectFeatures(selected.length ? selected.join(",") : "none");
450
+ }
451
+ const config = {
452
+ version,
453
+ platforms: chosenPlatforms,
454
+ features: chosenFeatures
455
+ };
456
+ const setup = options.json ? undefined : projectSetup(name, config);
457
+ let installationStarted = false;
458
+ try {
459
+ if (!options.skipInstall && spawnSync("bun", ["--version"], { stdio: "ignore" }).status !== 0)
460
+ throw new Error("Install Bun first, or pass --skip-install to create the files only.");
461
+ const template = fileURLToPath(new URL("./templates/default", import.meta.url));
462
+ await createProject(template, directory, name, config);
463
+ if (!options.skipInstall) {
464
+ installationStarted = true;
465
+ setup?.installing();
466
+ await install(directory, Boolean(setup?.live));
467
+ }
468
+ setup?.finish({
469
+ installation: options.skipInstall ? "Skipped" : "Complete"
470
+ });
471
+ } catch (error) {
472
+ setup?.finish({
473
+ installation: installationStarted ? "Failed" : "Not started"
113
474
  });
475
+ throw error;
114
476
  }
115
- console.log(`
116
- Success! Created ${name} at ${dir}
117
-
118
- Get started:
477
+ const folders = selectedFolders(config);
478
+ if (options.json) {
479
+ console.log(JSON.stringify({
480
+ status: "created",
481
+ name,
482
+ directory,
483
+ configFile: "disposabl.json",
484
+ ...config,
485
+ folders,
486
+ installed: !options.skipInstall
487
+ }, null, 2));
488
+ } else {
489
+ console.log(`
490
+ Next steps
119
491
 
120
- cd ${name}
121
- ${options.skipInstall ? ` bun install
122
- ` : ""} bun run dev
492
+ cd ` + name + `
493
+ bun run dev
123
494
  `);
495
+ }
496
+ }
497
+ try {
498
+ await main();
124
499
  } catch (error) {
125
- console.error(error instanceof Error ? error.message : String(error));
126
- process.exitCode = 1;
500
+ if (error instanceof CommanderError && error.exitCode === 0) {
501
+ process.exitCode = 0;
502
+ } else if (error instanceof Cancelled) {
503
+ p2.cancel(error.message);
504
+ process.exitCode = 130;
505
+ } else {
506
+ const message = error instanceof Error ? error.message : String(error);
507
+ if (jsonOutput)
508
+ console.log(JSON.stringify({ status: "error", message }));
509
+ else
510
+ console.error("Error: " + message);
511
+ process.exitCode = 1;
512
+ }
127
513
  }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Disposabl
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.
@@ -2,3 +2,5 @@ node_modules/
2
2
  .env
3
3
  .env.*
4
4
  !.env.example
5
+ **/dist/
6
+ .turbo/
@@ -1,17 +1,13 @@
1
1
  {
2
2
  "name": "disposabl-app",
3
- "version": "0.0.1",
4
3
  "private": true,
5
4
  "type": "module",
5
+ "packageManager": "bun@1.4.4",
6
+ "workspaces": [
7
+ "apps/*",
8
+ "packages/*"
9
+ ],
6
10
  "scripts": {
7
- "dev": "bun --hot index.ts",
8
- "lint": "eslint ."
9
- },
10
- "devDependencies": {
11
- "eslint": "10.13.0",
12
- "@eslint/js": "10.0.1",
13
- "typescript-eslint": "8.72.1",
14
- "typescript": "6.0.3",
15
- "@types/bun": "1.4.4"
11
+ "dev": "bun run --parallel --if-present --filter './apps/*' dev"
16
12
  }
17
13
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "create-disposabl-app",
3
- "version": "0.0.4",
4
- "description": "Create a Disposabl app with Bun",
3
+ "version": "0.1.0",
4
+ "description": "Create a Disposabl app",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/disposabl-dev/create-disposabl-app.git",
10
- "directory": "cli"
10
+ "directory": "packages/create-disposabl-app"
11
11
  },
12
12
  "bin": {
13
13
  "create-disposabl-app": "dist/index.js"
@@ -19,14 +19,31 @@
19
19
  "engines": {
20
20
  "node": ">=22.12.0"
21
21
  },
22
+ "packageManager": "bun@1.4.4",
22
23
  "scripts": {
23
- "build": "bun build.ts"
24
+ "build": "bun build.ts",
25
+ "typecheck": "bun check",
26
+ "lint": "eslint .",
27
+ "test": "bun test tests",
28
+ "format": "bun fmt"
24
29
  },
25
30
  "publishConfig": {
26
31
  "access": "public"
27
32
  },
28
33
  "dependencies": {
34
+ "@clack/core": "1.5.1",
35
+ "@clack/prompts": "1.8.1",
29
36
  "commander": "15.0.0",
30
- "prompts": "2.4.2"
37
+ "picocolors": "1.1.1"
38
+ },
39
+ "devDependencies": {
40
+ "@eslint/js": "10.0.1",
41
+ "@types/bun": "1.4.4",
42
+ "@types/semver": "7.8.0",
43
+ "eslint": "10.13.0",
44
+ "jiti": "2.7.0",
45
+ "semver": "7.8.5",
46
+ "typescript": "6.0.3",
47
+ "typescript-eslint": "8.72.1"
31
48
  }
32
49
  }
package/README.md DELETED
@@ -1,34 +0,0 @@
1
- # Create Disposabl app
2
-
3
- ```sh
4
- bun create disposabl-app@latest
5
- # or
6
- npx create-disposabl-app@latest my-app
7
- ```
8
-
9
- Choose platforms and optional features. Web is required; Docs is optional. The CLI installs dependencies with Bun, then prints the commands to start your app.
10
-
11
- Options: `-h` / `--help`, `-v` / `--version`, `-y` / `--yes` for defaults, `--docs`, and `--skip-install`.
12
-
13
- The CLI uses Node.js 22.12 or later. Bun-only environments can use `bunx --bun create-disposabl-app@latest`. The generated app uses Bun, ESLint, and `bun fmt`. Docs, when selected, is available at `/docs`.
14
-
15
- ## Development
16
-
17
- The app lives in `cli/templates/default/`. Everything outside that directory is CLI or repository tooling. `bun run build` bundles the template; generated apps receive no publishing scripts or workflows.
18
-
19
- ```sh
20
- bun install
21
- bun fmt
22
- bun run lint
23
- bun run typecheck
24
- bun run build
25
- bun run dev
26
- ```
27
-
28
- ## Releases
29
-
30
- Push or merge to `main` to publish. Stable versions increment the patch automatically. Set a higher stable version in `cli/package.json` for a minor or major release. Set an explicit future version such as `0.1.0-beta.1` for a beta, incrementing it manually for each beta publication. Remove the suffix and set the intended stable version when ready.
31
-
32
- The workflow records the actual version in a release commit, publishes to npm `latest` or `beta`, confirms availability, then creates the matching Git tag and GitHub Release. Betas become GitHub prereleases. Reruns reuse the recorded version and repair incomplete releases. No PR builds are published.
33
-
34
- First publication only: build, run `npm login`, then `npm publish` inside `cli/` with version `0.0.1`. Later releases use npm trusted publishing without an npm token in GitHub.
@@ -1,12 +0,0 @@
1
- # Disposabl app
2
-
3
- Start the development server with [Bun](https://bun.com):
4
-
5
- ```sh
6
- bun run dev
7
- ```
8
-
9
- Open http://localhost:3000. Edit `index.ts` to build your app.
10
-
11
- Run `bun run lint` for ESLint and `bun fmt` for formatting.
12
- If you selected Docs, open http://localhost:3000/docs and edit `docs/index.html`.
@@ -1,8 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <meta charset="utf-8" />
4
- <title>Documentation</title>
5
- <h1>Documentation</h1>
6
- <p>Edit docs/index.html to write your app's documentation.</p>
7
- <a href="/">Back to your app</a>
8
- </html>
@@ -1,18 +0,0 @@
1
- import js from "@eslint/js";
2
- import ts from "typescript-eslint";
3
-
4
- export default ts.config(
5
- { ignores: ["**/dist/**", "**/node_modules/**"] },
6
- js.configs.recommended,
7
- ts.configs.recommended,
8
- {
9
- languageOptions: {
10
- globals: {
11
- Bun: "readonly",
12
- process: "readonly",
13
- console: "readonly",
14
- URL: "readonly",
15
- },
16
- },
17
- },
18
- );
@@ -1,14 +0,0 @@
1
- const server = Bun.serve({
2
- port: process.env.PORT ?? 3000,
3
- async fetch(request) {
4
- if (new URL(request.url).pathname === "/docs") {
5
- const page = Bun.file(new URL("./docs/index.html", import.meta.url));
6
- return (await page.exists())
7
- ? new Response(page)
8
- : new Response("Not found", { status: 404 });
9
- }
10
- return new Response("Your Disposabl app is ready.\n");
11
- },
12
- });
13
-
14
- console.log(`Your app is running at ${server.url}`);