create-skybridge 0.0.0-dev.17fa8fd → 0.0.0-dev.180fd98
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 +214 -315
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +35 -0
- package/package.json +3 -4
- package/{templates/demo → template}/README.md +15 -16
- package/{templates/demo → template}/package.json +2 -2
- package/{templates/demo → template}/src/views/onboarding.tsx +1 -1
- package/templates/blank/README.md +0 -92
- package/templates/blank/package.json +0 -28
- package/templates/blank/src/server.ts +0 -21
- package/templates/blank/tsconfig.json +0 -5
- package/templates/blank/vite.config.ts +0 -6
- package/templates/demo/.dockerignore +0 -4
- package/templates/demo/AGENTS.md +0 -1
- package/templates/demo/Dockerfile +0 -53
- package/templates/demo/_gitignore +0 -6
- package/templates/demo/alpic.json +0 -3
- package/templates/demo/node_modules/.bin/alpic +0 -21
- package/templates/demo/node_modules/.bin/sb +0 -21
- package/templates/demo/node_modules/.bin/skybridge +0 -21
- package/templates/demo/node_modules/.bin/tsc +0 -21
- package/templates/demo/node_modules/.bin/tsserver +0 -21
- package/templates/demo/node_modules/.bin/vite +0 -21
- package/templates/demo/src/helpers.ts +0 -4
- package/templates/demo/src/vite-manifest.d.ts +0 -4
- /package/{templates/blank → template}/.dockerignore +0 -0
- /package/{templates/blank → template}/AGENTS.md +0 -0
- /package/{templates/blank → template}/Dockerfile +0 -0
- /package/{templates/blank → template}/_gitignore +0 -0
- /package/{templates/blank → template}/alpic.json +0 -0
- /package/{templates/blank → template}/node_modules/.bin/alpic +0 -0
- /package/{templates/blank → template}/node_modules/.bin/sb +0 -0
- /package/{templates/blank → template}/node_modules/.bin/skybridge +0 -0
- /package/{templates/blank → template}/node_modules/.bin/tsc +0 -0
- /package/{templates/blank → template}/node_modules/.bin/tsserver +0 -0
- /package/{templates/demo → template}/node_modules/.bin/tsx +0 -0
- /package/{templates/blank → template}/node_modules/.bin/vite +0 -0
- /package/{templates/blank → template}/src/helpers.ts +0 -0
- /package/{templates/demo → template}/src/index.css +0 -0
- /package/{templates/demo → template}/src/server.ts +0 -0
- /package/{templates/demo → template}/src/views/components/doc-link.tsx +0 -0
- /package/{templates/demo → template}/src/views/components/doc.tsx +0 -0
- /package/{templates/demo → template}/src/views/components/nav.tsx +0 -0
- /package/{templates/demo → template}/src/views/components/progress.tsx +0 -0
- /package/{templates/demo → template}/src/views/components/steps/outro.tsx +0 -0
- /package/{templates/demo → template}/src/views/components/steps/state.tsx +0 -0
- /package/{templates/demo → template}/src/views/components/steps/tool-call.tsx +0 -0
- /package/{templates/demo → template}/src/views/components/steps/tool-output.tsx +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/beret.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/chapka.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/cowboy-hat.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/fez.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/jester-hat.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/mitre.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/non-la.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/original.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/propeller-beanie.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/ski-mask.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/sombrero.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/top-hat.png +0 -0
- /package/{templates/demo → template}/src/views/images/mascot/viking-helmet.png +0 -0
- /package/{templates/demo → template}/src/views/use-mascot.ts +0 -0
- /package/{templates/blank → template}/src/vite-manifest.d.ts +0 -0
- /package/{templates/demo → template}/tsconfig.json +0 -0
- /package/{templates/demo → template}/vite.config.ts +0 -0
package/dist/index.js
CHANGED
|
@@ -1,371 +1,256 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import * as prompts from "@clack/prompts";
|
|
5
|
-
import
|
|
6
|
+
import { downloadTemplate } from "giget";
|
|
6
7
|
import mri from "mri";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const pkg = JSON.parse(fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf-8"));
|
|
12
|
-
const version = pkg.version;
|
|
13
|
-
const HELP_MESSAGE = `Usage: skybridge create [path] [options]
|
|
8
|
+
const defaultProjectName = "skybridge-project";
|
|
9
|
+
// prettier-ignore
|
|
10
|
+
const helpMessage = `\
|
|
11
|
+
Usage: create-skybridge [OPTION]... [DIRECTORY]
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Arguments:
|
|
18
|
-
path Where the project will be created. Prompted when omitted.
|
|
13
|
+
Create a new Skybridge project by copying the starter template.
|
|
19
14
|
|
|
20
15
|
Options:
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--start start dev server
|
|
26
|
-
--yes skip prompts and use default values for unprovided options
|
|
27
|
-
--help display this help message
|
|
16
|
+
-h, --help show this help message
|
|
17
|
+
--repo <uri> use a git repository instead of the built-in template
|
|
18
|
+
--overwrite remove existing files in target directory
|
|
19
|
+
--immediate install dependencies and start development server
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
else {
|
|
40
|
-
_spinner.clear();
|
|
41
|
-
_spinner.start(msg);
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
stop(msg) {
|
|
45
|
-
if (!isTTY) {
|
|
46
|
-
prompts.log.success(msg);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
_spinner.stop(msg);
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
error(msg) {
|
|
53
|
-
if (!isTTY) {
|
|
54
|
-
prompts.log.error(msg);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
_spinner.error(msg);
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
};
|
|
21
|
+
Repository URI formats:
|
|
22
|
+
github:user/repo
|
|
23
|
+
gitlab:user/repo/subdirectory
|
|
24
|
+
bitbucket:user/repo#branch
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
create-skybridge my-app
|
|
28
|
+
create-skybridge my-app --repo github:alpic-ai/skybridge/examples/ecom-carousel
|
|
29
|
+
create-skybridge . --overwrite --immediate
|
|
30
|
+
`;
|
|
61
31
|
export async function init(args = process.argv.slice(2)) {
|
|
62
32
|
const argv = mri(args, {
|
|
63
|
-
boolean: ["help", "
|
|
64
|
-
string: ["
|
|
33
|
+
boolean: ["help", "overwrite", "immediate"],
|
|
34
|
+
string: ["repo"],
|
|
65
35
|
alias: { h: "help" },
|
|
66
36
|
});
|
|
67
|
-
|
|
68
|
-
|
|
37
|
+
const argTargetDir = argv._[0]
|
|
38
|
+
? sanitizeTargetDir(String(argv._[0]))
|
|
39
|
+
: undefined;
|
|
40
|
+
const argRepo = argv.repo;
|
|
41
|
+
const argOverwrite = argv.overwrite;
|
|
42
|
+
const argImmediate = argv.immediate;
|
|
43
|
+
const help = argv.help;
|
|
44
|
+
if (help) {
|
|
45
|
+
console.log(helpMessage);
|
|
69
46
|
return;
|
|
70
47
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
let pm = parsePackageManager(argv.pm || "");
|
|
77
|
-
if (argv.pm && !pm) {
|
|
78
|
-
abort(`Invalid --pm value "${argv.pm}". Expected one of: ${PACKAGE_MANAGERS.join(", ")}.`);
|
|
79
|
-
}
|
|
80
|
-
console.log(); // cosmetic line break
|
|
81
|
-
prompts.intro(`\x1b[1;36m⛰ Welcome to Skybridge v${version} \x1b[22m- the fullstack framework for building MCP Apps\x1b[0m`);
|
|
82
|
-
// 1. Target directory
|
|
48
|
+
const interactive = process.stdin.isTTY;
|
|
49
|
+
const cancel = () => prompts.cancel("Operation cancelled");
|
|
50
|
+
// 1. Get project name and target dir
|
|
51
|
+
let targetDir = argTargetDir;
|
|
83
52
|
if (!targetDir) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
targetDir = sanitizeTargetDir(choice);
|
|
96
|
-
}
|
|
97
|
-
// 2. Existing-directory handling
|
|
98
|
-
if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
|
|
99
|
-
if (argv.overwrite) {
|
|
100
|
-
emptyDir(targetDir);
|
|
101
|
-
}
|
|
102
|
-
else if (yes) {
|
|
103
|
-
prompts.log.error(`Target directory "${targetDir}" is not empty. Use --overwrite to remove existing files.`);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
const ok = await prompts.confirm({
|
|
108
|
-
message: `Target directory "${targetDir}" is not empty. Remove existing files?`,
|
|
109
|
-
initialValue: true,
|
|
53
|
+
if (interactive) {
|
|
54
|
+
const projectName = await prompts.text({
|
|
55
|
+
message: "Project name:",
|
|
56
|
+
defaultValue: defaultProjectName,
|
|
57
|
+
placeholder: defaultProjectName,
|
|
58
|
+
validate: (value) => {
|
|
59
|
+
return !value || sanitizeTargetDir(value).length > 0
|
|
60
|
+
? undefined
|
|
61
|
+
: "Invalid project name";
|
|
62
|
+
},
|
|
110
63
|
});
|
|
111
|
-
if (prompts.isCancel(
|
|
64
|
+
if (prompts.isCancel(projectName)) {
|
|
112
65
|
return cancel();
|
|
113
66
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
67
|
+
targetDir = sanitizeTargetDir(projectName);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
targetDir = defaultProjectName;
|
|
117
71
|
}
|
|
118
72
|
}
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
73
|
+
// 2. Handle directory if exist and not empty
|
|
74
|
+
if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
|
|
75
|
+
let overwrite = argOverwrite ? "yes" : undefined;
|
|
76
|
+
if (!overwrite) {
|
|
77
|
+
if (interactive) {
|
|
78
|
+
const res = await prompts.select({
|
|
79
|
+
message: (targetDir === "."
|
|
80
|
+
? "Current directory"
|
|
81
|
+
: `Target directory "${targetDir}"`) +
|
|
82
|
+
` is not empty. Please choose how to proceed:`,
|
|
83
|
+
options: [
|
|
84
|
+
{
|
|
85
|
+
label: "Cancel operation",
|
|
86
|
+
value: "no",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: "Remove existing files and continue",
|
|
90
|
+
value: "yes",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
if (prompts.isCancel(res)) {
|
|
95
|
+
return cancel();
|
|
96
|
+
}
|
|
97
|
+
overwrite = res;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
overwrite = "no";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
switch (overwrite) {
|
|
104
|
+
case "yes":
|
|
105
|
+
emptyDir(targetDir);
|
|
106
|
+
break;
|
|
107
|
+
case "no":
|
|
108
|
+
prompts.log.error("Target directory is not empty.");
|
|
109
|
+
process.exit(1);
|
|
146
110
|
}
|
|
147
|
-
template = choice;
|
|
148
111
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Spinner.start(`Copying ${template} template`);
|
|
112
|
+
const root = path.join(process.cwd(), targetDir);
|
|
113
|
+
// 3. Download from repo or copy template
|
|
152
114
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
115
|
+
if (argRepo) {
|
|
116
|
+
prompts.log.step(`Downloading ${argRepo}...`);
|
|
117
|
+
await downloadTemplate(argRepo, { dir: root });
|
|
118
|
+
prompts.log.success(`Project created in ${root}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
prompts.log.step(`Copying template...`);
|
|
122
|
+
const templateDir = fileURLToPath(new URL("../template", import.meta.url));
|
|
123
|
+
// Copy template to target directory
|
|
124
|
+
fs.cpSync(templateDir, root, {
|
|
125
|
+
recursive: true,
|
|
126
|
+
filter: (src) => [".npmrc"].every((file) => !src.endsWith(file)),
|
|
127
|
+
});
|
|
128
|
+
// Rename _gitignore to .gitignore
|
|
129
|
+
fs.renameSync(path.join(root, "_gitignore"), path.join(root, ".gitignore"));
|
|
130
|
+
prompts.log.success(`Project created in ${root}`);
|
|
161
131
|
}
|
|
162
|
-
Spinner.stop(`Copied ${template} template`);
|
|
163
132
|
}
|
|
164
133
|
catch (error) {
|
|
165
|
-
|
|
166
|
-
|
|
134
|
+
prompts.log.error("Failed to create project from template");
|
|
135
|
+
console.error(error);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
// Update project name in package.json
|
|
139
|
+
const pkgPath = path.join(root, "package.json");
|
|
140
|
+
if (!fs.existsSync(pkgPath)) {
|
|
141
|
+
prompts.log.error("No package.json found in project");
|
|
142
|
+
process.exit(1);
|
|
167
143
|
}
|
|
168
|
-
// 5. Set package.json name to the project dir basename
|
|
169
144
|
try {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
fs.writeFileSync(pkgPath, `${JSON.stringify(projectPkg, null, 2)}\n`);
|
|
145
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
146
|
+
pkg.name = path.basename(root);
|
|
147
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
174
148
|
}
|
|
175
149
|
catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// (cross-spawn.sync would block the event loop). Captures stdout/stderr to
|
|
180
|
-
// `output` when stdio is "pipe", trimmed to the last OUTPUT_TAIL_LINES lines
|
|
181
|
-
// — install errors land at the tail, so we keep that and prefix with an
|
|
182
|
-
// ellipsis when content gets dropped.
|
|
183
|
-
function spawnAsync(command, args) {
|
|
184
|
-
return new Promise((resolve) => {
|
|
185
|
-
let raw = "";
|
|
186
|
-
const child = spawn(command, args, {
|
|
187
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
188
|
-
cwd: root,
|
|
189
|
-
});
|
|
190
|
-
child.stdout?.on("data", (chunk) => {
|
|
191
|
-
raw += chunk.toString();
|
|
192
|
-
});
|
|
193
|
-
child.stderr?.on("data", (chunk) => {
|
|
194
|
-
raw += chunk.toString();
|
|
195
|
-
});
|
|
196
|
-
const done = (status) => {
|
|
197
|
-
const tail = [];
|
|
198
|
-
for (const part of raw.split("\n").reverse()) {
|
|
199
|
-
const line = part.trim();
|
|
200
|
-
if (!line) {
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (tail.length >= OUTPUT_TAIL_LINES) {
|
|
204
|
-
tail.push(`… (truncated, showing last ${OUTPUT_TAIL_LINES} lines)`);
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
tail.push(line);
|
|
208
|
-
}
|
|
209
|
-
resolve({ status, output: tail.reverse().join("\n") });
|
|
210
|
-
};
|
|
211
|
-
child.on("close", done);
|
|
212
|
-
child.on("error", () => done(1));
|
|
213
|
-
});
|
|
150
|
+
prompts.log.error("Failed to update project name in package.json");
|
|
151
|
+
console.error(error);
|
|
152
|
+
process.exit(1);
|
|
214
153
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
const choice = await prompts.confirm({
|
|
225
|
-
message: "Install coding agent skills? (recommended)",
|
|
154
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
155
|
+
const pkgManager = userAgent?.split(" ")[0]?.split("/")[0] || "npm";
|
|
156
|
+
// 4. Ask about skills installation (installed by default)
|
|
157
|
+
let skill = true;
|
|
158
|
+
if (interactive) {
|
|
159
|
+
const skillsResult = await prompts.confirm({
|
|
160
|
+
message: "Install the coding agents skills? (recommended)",
|
|
226
161
|
initialValue: true,
|
|
227
162
|
});
|
|
228
|
-
if (prompts.isCancel(
|
|
163
|
+
if (prompts.isCancel(skillsResult)) {
|
|
229
164
|
return cancel();
|
|
230
165
|
}
|
|
231
|
-
|
|
166
|
+
skill = skillsResult;
|
|
232
167
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
"--yes",
|
|
237
|
-
"skills",
|
|
168
|
+
if (skill) {
|
|
169
|
+
run([
|
|
170
|
+
...getPkgExecCmd(pkgManager, "skills"),
|
|
238
171
|
"add",
|
|
239
172
|
"alpic-ai/skybridge",
|
|
240
|
-
"
|
|
173
|
+
"-s",
|
|
241
174
|
"chatgpt-app-builder",
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"--yes",
|
|
247
|
-
]);
|
|
248
|
-
// skills cli always returns 0 so we look for the success message
|
|
249
|
-
if (status === 0 && output.includes("Done!")) {
|
|
250
|
-
Spinner.stop(`Installed coding agent skills`);
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
Spinner.error(`Failed to install coding agent skills:
|
|
254
|
-
\x1b[2m${output}\x1b[0m`);
|
|
255
|
-
prompts.log.error("Try manually: `npx skills add alpic-ai/skybridge`.");
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
// 7. Package manager — autodetect, prompt only if detection fails (interactive)
|
|
259
|
-
if (!pm) {
|
|
260
|
-
pm = detectPackageManager() || "npm";
|
|
261
|
-
}
|
|
262
|
-
if (!yes) {
|
|
263
|
-
const choice = await prompts.select({
|
|
264
|
-
message: "Choose a package manager:",
|
|
265
|
-
options: PACKAGE_MANAGERS.map((value) => ({ value })),
|
|
266
|
-
initialValue: pm,
|
|
267
|
-
});
|
|
268
|
-
if (prompts.isCancel(choice)) {
|
|
269
|
-
return cancel();
|
|
270
|
-
}
|
|
271
|
-
pm = choice;
|
|
272
|
-
}
|
|
273
|
-
// 8. Always install dependencies
|
|
274
|
-
Spinner.start(`Installing dependencies with ${pm}`);
|
|
275
|
-
const { status, output } = await spawnAsync(pm, ["install"]);
|
|
276
|
-
if (status === 0) {
|
|
277
|
-
Spinner.stop(`Installed dependencies with ${pm}`);
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
Spinner.error(`Dependency installation failed:
|
|
281
|
-
\x1b[2m${output}\x1b[0m`);
|
|
282
|
-
abort(`Try manually: cd ${targetDir} && ${pm} install`);
|
|
283
|
-
}
|
|
284
|
-
// 9. Start dev server?
|
|
285
|
-
let start = false;
|
|
286
|
-
if (argv.start) {
|
|
287
|
-
start = true;
|
|
288
|
-
}
|
|
289
|
-
else if (!yes) {
|
|
290
|
-
const choice = await prompts.confirm({
|
|
291
|
-
message: "Start dev server now?",
|
|
292
|
-
initialValue: true,
|
|
293
|
-
});
|
|
294
|
-
if (prompts.isCancel(choice)) {
|
|
295
|
-
return cancel();
|
|
296
|
-
}
|
|
297
|
-
start = choice;
|
|
298
|
-
}
|
|
299
|
-
if (start) {
|
|
300
|
-
prompts.outro(`Starting dev server in ${targetDir}…`);
|
|
301
|
-
const devResult = spawn.sync(pm, scriptArgs(pm, "dev"), {
|
|
175
|
+
...(interactive
|
|
176
|
+
? []
|
|
177
|
+
: ["--yes", "-a", "universal", "-a", "claude-code"]),
|
|
178
|
+
], {
|
|
302
179
|
stdio: "inherit",
|
|
303
|
-
cwd:
|
|
180
|
+
cwd: targetDir,
|
|
304
181
|
});
|
|
305
|
-
process.exit(devResult.status ?? 0);
|
|
306
182
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
${
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
for (const line of lines) {
|
|
323
|
-
prompts.log.error(line);
|
|
183
|
+
// 5. Ask about immediate installation
|
|
184
|
+
let immediate = argImmediate;
|
|
185
|
+
if (immediate === undefined) {
|
|
186
|
+
if (interactive) {
|
|
187
|
+
const immediateResult = await prompts.confirm({
|
|
188
|
+
message: `Install with ${pkgManager} and start now?`,
|
|
189
|
+
});
|
|
190
|
+
if (prompts.isCancel(immediateResult)) {
|
|
191
|
+
return cancel();
|
|
192
|
+
}
|
|
193
|
+
immediate = immediateResult;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
immediate = false;
|
|
197
|
+
}
|
|
324
198
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
199
|
+
const installCmd = [pkgManager, "install"];
|
|
200
|
+
const runCmd = [pkgManager];
|
|
201
|
+
switch (pkgManager) {
|
|
202
|
+
case "yarn":
|
|
203
|
+
case "pnpm":
|
|
330
204
|
case "bun":
|
|
331
|
-
|
|
205
|
+
break;
|
|
332
206
|
case "deno":
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return "npm";
|
|
336
|
-
case "pnpm":
|
|
337
|
-
return "pnpm";
|
|
338
|
-
case "yarn":
|
|
339
|
-
return "yarn";
|
|
207
|
+
runCmd.push("task");
|
|
208
|
+
break;
|
|
340
209
|
default:
|
|
341
|
-
|
|
210
|
+
runCmd.push("run");
|
|
342
211
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
212
|
+
runCmd.push("dev");
|
|
213
|
+
if (!immediate) {
|
|
214
|
+
prompts.outro(`Done! Next steps:
|
|
215
|
+
cd ${targetDir}
|
|
216
|
+
${installCmd.join(" ")}
|
|
217
|
+
${runCmd.join(" ")}
|
|
218
|
+
`);
|
|
219
|
+
return;
|
|
348
220
|
}
|
|
349
|
-
|
|
350
|
-
|
|
221
|
+
prompts.log.step(`Installing dependencies with ${pkgManager}...`);
|
|
222
|
+
run(installCmd, {
|
|
223
|
+
stdio: "inherit",
|
|
224
|
+
cwd: root,
|
|
225
|
+
});
|
|
226
|
+
prompts.log.step("Starting dev server...");
|
|
227
|
+
run(runCmd, {
|
|
228
|
+
stdio: "inherit",
|
|
229
|
+
cwd: root,
|
|
230
|
+
});
|
|
351
231
|
}
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return ["run", script];
|
|
232
|
+
function run([command, ...args], options) {
|
|
233
|
+
const { status, error } = spawnSync(command, args, options);
|
|
234
|
+
if (status != null && status > 0) {
|
|
235
|
+
process.exit(status);
|
|
236
|
+
}
|
|
237
|
+
if (error) {
|
|
238
|
+
console.error(`\n${command} ${args.join(" ")} error!`);
|
|
239
|
+
console.error(error);
|
|
240
|
+
process.exit(1);
|
|
362
241
|
}
|
|
363
|
-
}
|
|
364
|
-
function scriptCommand(pm, script) {
|
|
365
|
-
return [pm, ...scriptArgs(pm, script)].join(" ");
|
|
366
242
|
}
|
|
367
243
|
function sanitizeTargetDir(targetDir) {
|
|
368
|
-
return targetDir
|
|
244
|
+
return (targetDir
|
|
245
|
+
.trim()
|
|
246
|
+
// Only keep alphanumeric, dash, underscore, dot, @, /
|
|
247
|
+
.replace(/[^a-zA-Z0-9\-_.@/]/g, "")
|
|
248
|
+
// Prevent path traversal
|
|
249
|
+
.replace(/\.\./g, "")
|
|
250
|
+
// Collapse multiple slashes
|
|
251
|
+
.replace(/\/+/g, "/")
|
|
252
|
+
// Remove leading/trailing slashes
|
|
253
|
+
.replace(/^\/+|\/+$/g, ""));
|
|
369
254
|
}
|
|
370
255
|
// Skip user's SPEC.md and IDE/agent preferences (.idea, .claude, etc.)
|
|
371
256
|
function isSkippedEntry(entry) {
|
|
@@ -387,3 +272,17 @@ function emptyDir(dir) {
|
|
|
387
272
|
fs.rmSync(path.join(dir, entry.name), { recursive: true, force: true });
|
|
388
273
|
}
|
|
389
274
|
}
|
|
275
|
+
function getPkgExecCmd(pkgManager, cmd) {
|
|
276
|
+
switch (pkgManager) {
|
|
277
|
+
case "yarn":
|
|
278
|
+
return ["yarn", "dlx", cmd];
|
|
279
|
+
case "pnpm":
|
|
280
|
+
return ["pnpm", "dlx", cmd];
|
|
281
|
+
case "bun":
|
|
282
|
+
return ["bunx", cmd];
|
|
283
|
+
case "deno":
|
|
284
|
+
return ["deno", "run", "-A", `npm:${cmd}`];
|
|
285
|
+
default:
|
|
286
|
+
return ["npx", "--yes", cmd];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { init } from "./index.js";
|
|
6
|
+
describe("create-skybridge", () => {
|
|
7
|
+
let tempDirName;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tempDirName = `test-${randomBytes(2).toString("hex")}`;
|
|
10
|
+
});
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await fs.rm(path.join(process.cwd(), tempDirName), {
|
|
13
|
+
recursive: true,
|
|
14
|
+
force: true,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
it("should copy the template", { timeout: 10000 }, async () => {
|
|
18
|
+
const name = `../../${tempDirName}//project$`;
|
|
19
|
+
await init([name]);
|
|
20
|
+
await fs.access(path.join(process.cwd(), tempDirName, "project", ".gitignore"));
|
|
21
|
+
await fs.access(path.join(process.cwd(), tempDirName, "project", ".dockerignore"));
|
|
22
|
+
await fs.access(path.join(process.cwd(), tempDirName, "project", "Dockerfile"));
|
|
23
|
+
expect(fs.access(path.join(process.cwd(), tempDirName, "project", ".npmrc"))).rejects.toThrowError();
|
|
24
|
+
});
|
|
25
|
+
it("should download template from repo", { timeout: 10000 }, async () => {
|
|
26
|
+
const name = `../../${tempDirName}//project$`;
|
|
27
|
+
await init([
|
|
28
|
+
name,
|
|
29
|
+
"--repo",
|
|
30
|
+
"github:alpic-ai/skybridge/examples/ecom-carousel",
|
|
31
|
+
]);
|
|
32
|
+
await fs.access(path.join(process.cwd(), tempDirName, "project", ".gitignore"));
|
|
33
|
+
expect(fs.access(path.join(process.cwd(), tempDirName, "project", ".npmrc"))).rejects.toThrowError();
|
|
34
|
+
});
|
|
35
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-skybridge",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.180fd98",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alpic",
|
|
@@ -14,15 +14,14 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"index.js",
|
|
16
16
|
"dist",
|
|
17
|
-
"
|
|
17
|
+
"template"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@clack/prompts": "^1.1.0",
|
|
21
|
-
"
|
|
21
|
+
"giget": "^3.2.0",
|
|
22
22
|
"mri": "^1.2.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@types/cross-spawn": "^6.0.6",
|
|
26
25
|
"typescript": "^6.0.2",
|
|
27
26
|
"vitest": "^4.1.4"
|
|
28
27
|
},
|