create-disposabl-app 0.0.3 → 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 +490 -85
- package/dist/templates/default/LICENSE +21 -0
- package/dist/templates/default/apps/blog/package.json +1 -0
- package/dist/templates/default/apps/changelog/package.json +1 -0
- package/dist/templates/default/apps/desktop/package.json +1 -0
- package/dist/templates/default/apps/docs/package.json +1 -0
- package/dist/templates/default/apps/mobile/package.json +1 -0
- package/dist/templates/default/apps/web/package.json +1 -0
- package/dist/templates/default/gitignore +2 -0
- package/dist/templates/default/package.json +6 -2
- package/package.json +26 -5
- package/README.md +0 -23
- package/dist/templates/default/README.md +0 -9
- package/dist/templates/default/index.ts +0 -8
package/dist/index.js
CHANGED
|
@@ -1,108 +1,513 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
import { parseArgs } from "node:util";
|
|
5
|
-
|
|
6
|
-
// cli/create-app.ts
|
|
3
|
+
// index.ts
|
|
7
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
|
|
8
265
|
import {
|
|
9
266
|
cp,
|
|
10
267
|
mkdir,
|
|
11
|
-
readdir,
|
|
12
268
|
readFile,
|
|
269
|
+
readdir,
|
|
13
270
|
rename,
|
|
271
|
+
rm,
|
|
14
272
|
writeFile
|
|
15
273
|
} from "node:fs/promises";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
async function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
const dir = resolve(cwd, name);
|
|
26
|
-
await mkdir(dir);
|
|
27
|
-
console.log(`Creating ${name}...`);
|
|
28
|
-
const template = fileURLToPath(new URL("./templates/default", import.meta.url));
|
|
29
|
-
await Promise.all((await readdir(template)).map((file) => cp(resolve(template, file), resolve(dir, file), {
|
|
30
|
-
recursive: true,
|
|
31
|
-
force: false,
|
|
32
|
-
errorOnExist: true
|
|
33
|
-
})));
|
|
34
|
-
await rename(resolve(dir, "gitignore"), resolve(dir, ".gitignore"));
|
|
35
|
-
const manifestPath = resolve(dir, "package.json");
|
|
36
|
-
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
37
|
-
manifest.name = name;
|
|
38
|
-
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
39
|
-
`);
|
|
40
|
-
if (!skipInstall) {
|
|
41
|
-
await new Promise((resolveInstall, reject) => {
|
|
42
|
-
const install = spawn("bun", ["install"], {
|
|
43
|
-
cwd: dir,
|
|
44
|
-
stdio: "inherit",
|
|
45
|
-
env: { ...process.env, NODE_ENV: "development" }
|
|
46
|
-
});
|
|
47
|
-
install.once("error", reject);
|
|
48
|
-
install.once("close", (code) => {
|
|
49
|
-
if (code === 0)
|
|
50
|
-
resolveInstall();
|
|
51
|
-
else {
|
|
52
|
-
reject(new Error(`Dependencies could not be installed. Run bun install inside ${name} to retry.`));
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
});
|
|
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
|
+
}
|
|
56
282
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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) + `
|
|
63
308
|
`);
|
|
64
|
-
|
|
309
|
+
} catch (error) {
|
|
310
|
+
await rm(destination, { recursive: true, force: true });
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
return folders;
|
|
65
314
|
}
|
|
66
|
-
// cli/package.json
|
|
67
|
-
var version = "0.0.3";
|
|
68
|
-
|
|
69
|
-
// cli/index.ts
|
|
70
|
-
var help = `Usage: create-disposabl-app <appname> [options]
|
|
71
315
|
|
|
72
|
-
|
|
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
|
|
73
324
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
--skip-install Copy the starter without running bun install
|
|
325
|
+
Features:
|
|
326
|
+
docs, blog, changelog, authentication
|
|
327
|
+
Authentication currently records a selection only.
|
|
78
328
|
|
|
79
329
|
Examples:
|
|
80
|
-
bun create disposabl-app@latest
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
83
335
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
383
|
+
`);
|
|
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" }
|
|
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())));
|
|
94
407
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
}));
|
|
426
|
+
}
|
|
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"
|
|
474
|
+
});
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
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));
|
|
102
488
|
} else {
|
|
103
|
-
|
|
489
|
+
console.log(`
|
|
490
|
+
Next steps
|
|
491
|
+
|
|
492
|
+
cd ` + name + `
|
|
493
|
+
bun run dev
|
|
494
|
+
`);
|
|
104
495
|
}
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
await main();
|
|
105
499
|
} catch (error) {
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -1,9 +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 --
|
|
11
|
+
"dev": "bun run --parallel --if-present --filter './apps/*' dev"
|
|
8
12
|
}
|
|
9
13
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-disposabl-app",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Create a Disposabl app
|
|
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": "
|
|
10
|
+
"directory": "packages/create-disposabl-app"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
13
|
"create-disposabl-app": "dist/index.js"
|
|
@@ -17,12 +17,33 @@
|
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=22"
|
|
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"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@clack/core": "1.5.1",
|
|
35
|
+
"@clack/prompts": "1.8.1",
|
|
36
|
+
"commander": "15.0.0",
|
|
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"
|
|
27
48
|
}
|
|
28
49
|
}
|
package/README.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Create Disposabl app
|
|
2
|
-
|
|
3
|
-
Create an app with [Bun](https://bun.com):
|
|
4
|
-
|
|
5
|
-
```sh
|
|
6
|
-
bun create disposabl-app@latest my-app
|
|
7
|
-
cd my-app
|
|
8
|
-
bun run dev
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Or use Node.js 22 or later:
|
|
12
|
-
|
|
13
|
-
```sh
|
|
14
|
-
npx create-disposabl-app@latest my-app
|
|
15
|
-
npx create-disposabl-app@latest -h
|
|
16
|
-
npx create-disposabl-app@latest -v
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Options: `-h` / `--help`, `-v` / `--version`, and `--skip-install`.
|
|
20
|
-
|
|
21
|
-
The starter is bundled in the package. The CLI copies only its files, installs dependencies with Bun, and prints the next commands. Existing folders are never overwritten. Installing dependencies and running the app require Bun; help, version, and `--skip-install` work without it. The starter is an `index.ts` using `Bun.serve`.
|
|
22
|
-
|
|
23
|
-
The standard CLI uses Node.js 22 or later. In a Bun-only environment, run `bunx --bun create-disposabl-app@latest my-app`.
|