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 +491 -105
- 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 -10
- package/package.json +22 -5
- package/README.md +0 -34
- package/dist/templates/default/README.md +0 -12
- package/dist/templates/default/docs/index.html +0 -8
- package/dist/templates/default/eslint.config.js +0 -18
- package/dist/templates/default/index.ts +0 -14
package/dist/index.js
CHANGED
|
@@ -1,127 +1,513 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
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 {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
//
|
|
21
|
-
var
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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.
|
|
@@ -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,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 --
|
|
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
|
-
"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"
|
|
@@ -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
|
-
"
|
|
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,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}`);
|