create-qauri 0.2.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/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/index.mjs +744 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
- package/templates/base/README.md.tmpl +22 -0
- package/templates/base/gitignore.tmpl +33 -0
- package/templates/config/qauri.config.json.tmpl +31 -0
- package/templates/frontend/react/index.html +12 -0
- package/templates/frontend/react/package.json.tmpl +20 -0
- package/templates/frontend/react/src/App.css +32 -0
- package/templates/frontend/react/src/App.jsx +17 -0
- package/templates/frontend/react/src/main.jsx +10 -0
- package/templates/frontend/react/vite.config.js +10 -0
- package/templates/frontend/react-ts/index.html +12 -0
- package/templates/frontend/react-ts/package.json.tmpl +23 -0
- package/templates/frontend/react-ts/src/App.css +32 -0
- package/templates/frontend/react-ts/src/App.tsx +17 -0
- package/templates/frontend/react-ts/src/main.tsx +10 -0
- package/templates/frontend/react-ts/tsconfig.json +15 -0
- package/templates/frontend/react-ts/vite.config.ts +10 -0
- package/templates/frontend/svelte/index.html +12 -0
- package/templates/frontend/svelte/package.json.tmpl +19 -0
- package/templates/frontend/svelte/src/App.svelte +46 -0
- package/templates/frontend/svelte/src/main.js +7 -0
- package/templates/frontend/svelte/vite.config.js +10 -0
- package/templates/frontend/svelte-ts/index.html +12 -0
- package/templates/frontend/svelte-ts/package.json.tmpl +22 -0
- package/templates/frontend/svelte-ts/src/App.svelte +46 -0
- package/templates/frontend/svelte-ts/src/main.ts +7 -0
- package/templates/frontend/svelte-ts/svelte.config.js +5 -0
- package/templates/frontend/svelte-ts/tsconfig.json +14 -0
- package/templates/frontend/svelte-ts/vite.config.ts +10 -0
- package/templates/frontend/vanilla/index.html +16 -0
- package/templates/frontend/vanilla/main.js +6 -0
- package/templates/frontend/vanilla/package.json.tmpl +17 -0
- package/templates/frontend/vanilla/style.css +25 -0
- package/templates/frontend/vanilla-ts/index.html +13 -0
- package/templates/frontend/vanilla-ts/main.ts +8 -0
- package/templates/frontend/vanilla-ts/package.json.tmpl +18 -0
- package/templates/frontend/vanilla-ts/style.css +25 -0
- package/templates/frontend/vanilla-ts/tsconfig.json +14 -0
- package/templates/frontend/vanilla-ts/vite.config.ts +8 -0
- package/templates/frontend/vue/index.html +12 -0
- package/templates/frontend/vue/package.json.tmpl +19 -0
- package/templates/frontend/vue/src/App.vue +48 -0
- package/templates/frontend/vue/src/main.js +4 -0
- package/templates/frontend/vue/vite.config.js +10 -0
- package/templates/frontend/vue-ts/index.html +12 -0
- package/templates/frontend/vue-ts/package.json.tmpl +21 -0
- package/templates/frontend/vue-ts/src/App.vue +48 -0
- package/templates/frontend/vue-ts/src/main.ts +4 -0
- package/templates/frontend/vue-ts/tsconfig.json +15 -0
- package/templates/frontend/vue-ts/vite.config.ts +10 -0
- package/templates/native/cpp/CMakeLists.txt.tmpl +16 -0
- package/templates/native/cpp/main.cpp.tmpl +24 -0
- package/templates/native/python/main.py.tmpl +19 -0
- package/templates/native/python/pyproject.toml.tmpl +8 -0
- package/templates/native/python/requirements.txt.tmpl +2 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/create.ts
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { resolve as resolve4 } from "path";
|
|
7
|
+
import { existsSync as existsSync5 } from "fs";
|
|
8
|
+
|
|
9
|
+
// src/validator.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
var PROJECT_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
13
|
+
function validateProjectName(name) {
|
|
14
|
+
if (!name) {
|
|
15
|
+
return { valid: false, message: "Project name cannot be empty" };
|
|
16
|
+
}
|
|
17
|
+
if (!PROJECT_NAME_RE.test(name)) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
message: "Project name must start with a letter and contain only letters, numbers, hyphens, and underscores"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { valid: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/utils/config.ts
|
|
27
|
+
import { readFile } from "fs/promises";
|
|
28
|
+
import { resolve as resolve2 } from "path";
|
|
29
|
+
function buildConfig(selections) {
|
|
30
|
+
const features = selections.features ?? [];
|
|
31
|
+
const hasFeature = (f) => features.includes(f);
|
|
32
|
+
const config = {
|
|
33
|
+
project: {
|
|
34
|
+
name: selections.projectName,
|
|
35
|
+
version: "0.1.0",
|
|
36
|
+
type: selections.language
|
|
37
|
+
},
|
|
38
|
+
window: {
|
|
39
|
+
title: selections.projectName,
|
|
40
|
+
width: 800,
|
|
41
|
+
height: 600,
|
|
42
|
+
resizable: true,
|
|
43
|
+
center: true
|
|
44
|
+
},
|
|
45
|
+
dev: {
|
|
46
|
+
serverUrl: `http://localhost:${selections.devPort}`,
|
|
47
|
+
autoStart: true
|
|
48
|
+
},
|
|
49
|
+
prod: {
|
|
50
|
+
distDir: "../dist",
|
|
51
|
+
entry: "index.html"
|
|
52
|
+
},
|
|
53
|
+
debug: {
|
|
54
|
+
enableF12: hasFeature("devtools"),
|
|
55
|
+
enableF5: hasFeature("reload")
|
|
56
|
+
},
|
|
57
|
+
newWindow: {
|
|
58
|
+
behavior: selections.newWindowBehavior ?? "block"
|
|
59
|
+
},
|
|
60
|
+
ipc: {
|
|
61
|
+
enableWhitelist: hasFeature("whitelist"),
|
|
62
|
+
...hasFeature("examples") ? { allowedCommands: ["greet", "getSystemInfo", "readFile"] } : {}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
if (selections.language === "python") {
|
|
66
|
+
config.python = {
|
|
67
|
+
...selections.pythonVersion ? { version: selections.pythonVersion } : {},
|
|
68
|
+
...selections.pythonPath ? { path: selections.pythonPath } : {},
|
|
69
|
+
...selections.pythonEnvTool ? { packageManager: selections.pythonEnvTool } : {}
|
|
70
|
+
};
|
|
71
|
+
} else {
|
|
72
|
+
config.cpp = {};
|
|
73
|
+
}
|
|
74
|
+
if (selections.qtVersion || selections.qtPath) {
|
|
75
|
+
config.qt = {
|
|
76
|
+
...selections.qtVersion ? { version: selections.qtVersion } : {},
|
|
77
|
+
...selections.qtPath ? { path: selections.qtPath } : {}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
function serializeConfig(config) {
|
|
83
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/config.ts
|
|
87
|
+
var FRAMEWORK_META = {
|
|
88
|
+
vanilla: { displayName: "Vanilla", devServerUrl: "http://localhost:5173", devServerPort: 5173, distDir: "../dist" },
|
|
89
|
+
"vanilla-ts": { displayName: "Vanilla (TypeScript)", devServerUrl: "http://localhost:5173", devServerPort: 5173, distDir: "../dist" },
|
|
90
|
+
react: { displayName: "React", devServerUrl: "http://localhost:5173", devServerPort: 5173, distDir: "../dist" },
|
|
91
|
+
"react-ts": { displayName: "React (TypeScript)", devServerUrl: "http://localhost:5173", devServerPort: 5173, distDir: "../dist" },
|
|
92
|
+
vue: { displayName: "Vue", devServerUrl: "http://localhost:5173", devServerPort: 5173, distDir: "../dist" },
|
|
93
|
+
"vue-ts": { displayName: "Vue (TypeScript)", devServerUrl: "http://localhost:5173", devServerPort: 5173, distDir: "../dist" },
|
|
94
|
+
svelte: { displayName: "Svelte", devServerUrl: "http://localhost:5173", devServerPort: 5173, distDir: "../dist" },
|
|
95
|
+
"svelte-ts": { displayName: "Svelte (TypeScript)", devServerUrl: "http://localhost:5173", devServerPort: 5173, distDir: "../dist" }
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/generator.ts
|
|
99
|
+
import { join, resolve as resolve3 } from "path";
|
|
100
|
+
import { fileURLToPath } from "url";
|
|
101
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
102
|
+
import { readdirSync, statSync } from "fs";
|
|
103
|
+
|
|
104
|
+
// src/template.ts
|
|
105
|
+
import { readFileSync } from "fs";
|
|
106
|
+
var VAR_RE = /\{\{(\w+)\}\}/g;
|
|
107
|
+
var TemplateError = class extends Error {
|
|
108
|
+
constructor(variable, templateFile) {
|
|
109
|
+
const loc = templateFile ? ` in ${templateFile}` : "";
|
|
110
|
+
super(`Undefined template variable "{{${variable}}}"${loc}`);
|
|
111
|
+
this.variable = variable;
|
|
112
|
+
this.templateFile = templateFile;
|
|
113
|
+
this.name = "TemplateError";
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
function renderTemplate(content, context) {
|
|
117
|
+
return content.replace(VAR_RE, (_match, key) => {
|
|
118
|
+
if (!(key in context)) {
|
|
119
|
+
throw new TemplateError(key);
|
|
120
|
+
}
|
|
121
|
+
return String(context[key]);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function findUnresolvedVariables(content) {
|
|
125
|
+
const vars = [];
|
|
126
|
+
let m;
|
|
127
|
+
const re = new RegExp(VAR_RE.source, "g");
|
|
128
|
+
while ((m = re.exec(content)) !== null) {
|
|
129
|
+
vars.push(m[1]);
|
|
130
|
+
}
|
|
131
|
+
return vars;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/writer.ts
|
|
135
|
+
import { mkdir, writeFile as fsWriteFile, copyFile as fsCopyFile, rm } from "fs/promises";
|
|
136
|
+
import { dirname } from "path";
|
|
137
|
+
async function ensureDir(dir) {
|
|
138
|
+
await mkdir(dir, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
async function writeFile(path, content) {
|
|
141
|
+
try {
|
|
142
|
+
await ensureDir(dirname(path));
|
|
143
|
+
await fsWriteFile(path, content, "utf-8");
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
+
throw new Error(`Failed to write ${path}: ${msg}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function copyFile(src, dest) {
|
|
150
|
+
try {
|
|
151
|
+
await ensureDir(dirname(dest));
|
|
152
|
+
await fsCopyFile(src, dest);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
throw new Error(`Failed to copy ${src} \u2192 ${dest}: ${msg}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function removeDir(dir) {
|
|
159
|
+
await rm(dir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/generator.ts
|
|
163
|
+
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
164
|
+
function templatesRoot() {
|
|
165
|
+
const fromDist = resolve3(__dirname, "..", "templates");
|
|
166
|
+
if (existsSync2(fromDist)) return fromDist;
|
|
167
|
+
const fromSrc = resolve3(__dirname, "..", "..", "templates");
|
|
168
|
+
if (existsSync2(fromSrc)) return fromSrc;
|
|
169
|
+
throw new Error("Templates directory not found");
|
|
170
|
+
}
|
|
171
|
+
function buildTemplateContext(config) {
|
|
172
|
+
const backendMap = { webview2: "webview2", cef: "cef", both: "auto" };
|
|
173
|
+
return {
|
|
174
|
+
projectName: config.projectName,
|
|
175
|
+
language: config.language,
|
|
176
|
+
backend: config.backend,
|
|
177
|
+
backendType: backendMap[config.backend],
|
|
178
|
+
frontendFramework: config.frontendFramework,
|
|
179
|
+
devServerUrl: config.devServerUrl,
|
|
180
|
+
devServerPort: config.devServerPort,
|
|
181
|
+
devPort: config.devServerPort,
|
|
182
|
+
distDir: config.distDir,
|
|
183
|
+
pythonEnvTool: config.pythonEnvTool ?? "",
|
|
184
|
+
buildSystem: config.buildSystem ?? "",
|
|
185
|
+
features: (config.features ?? []).join(","),
|
|
186
|
+
newWindowBehavior: config.newWindowBehavior ?? "block",
|
|
187
|
+
pythonVersion: config.pythonVersion ?? "",
|
|
188
|
+
pythonPath: config.pythonPath ?? "",
|
|
189
|
+
qtVersion: config.qtVersion ?? "",
|
|
190
|
+
qtPath: config.qtPath ?? ""
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function listFiles(dir, base = dir) {
|
|
194
|
+
const results = [];
|
|
195
|
+
if (!existsSync2(dir)) return results;
|
|
196
|
+
for (const entry of readdirSync(dir)) {
|
|
197
|
+
const full = join(dir, entry);
|
|
198
|
+
if (statSync(full).isDirectory()) {
|
|
199
|
+
results.push(...listFiles(full, base));
|
|
200
|
+
} else {
|
|
201
|
+
results.push(full.slice(base.length + 1).replace(/\\/g, "/"));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
function resolveFileList(config) {
|
|
207
|
+
const root = templatesRoot();
|
|
208
|
+
const entries = [];
|
|
209
|
+
const baseDir = join(root, "base");
|
|
210
|
+
for (const rel of listFiles(baseDir)) {
|
|
211
|
+
const isTmpl = rel.endsWith(".tmpl");
|
|
212
|
+
const targetName = isTmpl ? rel.slice(0, -5) : rel;
|
|
213
|
+
const target = targetName === "gitignore" ? ".gitignore" : targetName;
|
|
214
|
+
entries.push({ targetPath: target, templatePath: join(baseDir, rel), isTemplate: isTmpl });
|
|
215
|
+
}
|
|
216
|
+
const configTmpl = join(root, "config", "qauri.config.json.tmpl");
|
|
217
|
+
if (existsSync2(configTmpl)) {
|
|
218
|
+
entries.push({ targetPath: "src-qauri/qauri.config.json", templatePath: configTmpl, isTemplate: true });
|
|
219
|
+
}
|
|
220
|
+
const nativeDir = join(root, "native", config.language);
|
|
221
|
+
for (const rel of listFiles(nativeDir)) {
|
|
222
|
+
const isTmpl = rel.endsWith(".tmpl");
|
|
223
|
+
const targetName = isTmpl ? rel.slice(0, -5) : rel;
|
|
224
|
+
entries.push({ targetPath: `src-qauri/${targetName}`, templatePath: join(nativeDir, rel), isTemplate: isTmpl });
|
|
225
|
+
}
|
|
226
|
+
const fwDir = join(root, "frontend", config.frontendFramework);
|
|
227
|
+
for (const rel of listFiles(fwDir)) {
|
|
228
|
+
const isTmpl = rel.endsWith(".tmpl");
|
|
229
|
+
const targetName = isTmpl ? rel.slice(0, -5) : rel;
|
|
230
|
+
entries.push({ targetPath: targetName, templatePath: join(fwDir, rel), isTemplate: isTmpl });
|
|
231
|
+
}
|
|
232
|
+
return entries;
|
|
233
|
+
}
|
|
234
|
+
async function generateProject(targetDir, config) {
|
|
235
|
+
const ctx = buildTemplateContext(config);
|
|
236
|
+
const files = resolveFileList(config);
|
|
237
|
+
const selections = {
|
|
238
|
+
projectName: config.projectName,
|
|
239
|
+
language: config.language,
|
|
240
|
+
pythonEnvTool: config.pythonEnvTool,
|
|
241
|
+
backend: config.backend,
|
|
242
|
+
frontendFramework: config.frontendFramework,
|
|
243
|
+
features: config.features ?? [],
|
|
244
|
+
newWindowBehavior: config.newWindowBehavior ?? "block",
|
|
245
|
+
installDeps: false,
|
|
246
|
+
pythonVersion: config.pythonVersion,
|
|
247
|
+
pythonPath: config.pythonPath,
|
|
248
|
+
qtVersion: config.qtVersion,
|
|
249
|
+
qtPath: config.qtPath,
|
|
250
|
+
devPort: config.devServerPort
|
|
251
|
+
};
|
|
252
|
+
const qauriConfig = buildConfig(selections);
|
|
253
|
+
const qauriConfigContent = serializeConfig(qauriConfig);
|
|
254
|
+
try {
|
|
255
|
+
await ensureDir(targetDir);
|
|
256
|
+
for (const entry of files) {
|
|
257
|
+
const dest = join(targetDir, entry.targetPath);
|
|
258
|
+
if (entry.targetPath === "src-qauri/qauri.config.json") {
|
|
259
|
+
await writeFile(dest, qauriConfigContent);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (entry.isTemplate) {
|
|
263
|
+
const raw = readFileSync2(entry.templatePath, "utf-8");
|
|
264
|
+
const rendered = renderTemplate(raw, ctx);
|
|
265
|
+
const unresolved = findUnresolvedVariables(rendered);
|
|
266
|
+
if (unresolved.length > 0) {
|
|
267
|
+
throw new TemplateError(unresolved[0], entry.templatePath);
|
|
268
|
+
}
|
|
269
|
+
await writeFile(dest, rendered);
|
|
270
|
+
} else {
|
|
271
|
+
await copyFile(entry.templatePath, dest);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
await removeDir(targetDir).catch(() => {
|
|
276
|
+
});
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/utils/npm.ts
|
|
282
|
+
import { spawn } from "child_process";
|
|
283
|
+
import { existsSync as existsSync3 } from "fs";
|
|
284
|
+
import { join as join2 } from "path";
|
|
285
|
+
async function installDependencies(projectPath) {
|
|
286
|
+
if (!existsSync3(join2(projectPath, "package.json"))) {
|
|
287
|
+
throw new Error("package.json not found in project directory");
|
|
288
|
+
}
|
|
289
|
+
return new Promise((resolve5, reject) => {
|
|
290
|
+
const proc = spawn("npm", ["install"], {
|
|
291
|
+
cwd: projectPath,
|
|
292
|
+
stdio: "inherit",
|
|
293
|
+
shell: true
|
|
294
|
+
});
|
|
295
|
+
proc.on("close", (code) => {
|
|
296
|
+
if (code === 0) {
|
|
297
|
+
resolve5();
|
|
298
|
+
} else {
|
|
299
|
+
reject(new Error(`npm install failed with code ${code}`));
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
proc.on("error", (err) => {
|
|
303
|
+
reject(new Error(`Failed to start npm: ${err.message}`));
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/utils/port.ts
|
|
309
|
+
import net from "net";
|
|
310
|
+
function isPortInUse(port) {
|
|
311
|
+
return new Promise((resolve5) => {
|
|
312
|
+
const socket = new net.Socket();
|
|
313
|
+
socket.setTimeout(1e3);
|
|
314
|
+
socket.once("connect", () => {
|
|
315
|
+
socket.destroy();
|
|
316
|
+
resolve5(true);
|
|
317
|
+
});
|
|
318
|
+
socket.once("timeout", () => {
|
|
319
|
+
socket.destroy();
|
|
320
|
+
resolve5(false);
|
|
321
|
+
});
|
|
322
|
+
socket.once("error", () => {
|
|
323
|
+
socket.destroy();
|
|
324
|
+
resolve5(false);
|
|
325
|
+
});
|
|
326
|
+
socket.connect(port, "127.0.0.1");
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async function isPortAvailable(port) {
|
|
330
|
+
if (port < 1 || port > 65535) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
const inUse = await isPortInUse(port);
|
|
334
|
+
if (inUse) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
return new Promise((resolve5) => {
|
|
338
|
+
const server = net.createServer();
|
|
339
|
+
server.once("error", () => {
|
|
340
|
+
resolve5(false);
|
|
341
|
+
});
|
|
342
|
+
server.once("listening", () => {
|
|
343
|
+
server.close();
|
|
344
|
+
resolve5(true);
|
|
345
|
+
});
|
|
346
|
+
server.listen(port, "127.0.0.1");
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async function findAvailablePort(startPort = 5173, maxAttempts = 100) {
|
|
350
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
351
|
+
const port = startPort + i;
|
|
352
|
+
if (port > 65535) break;
|
|
353
|
+
if (await isPortAvailable(port)) {
|
|
354
|
+
return port;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return startPort;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/utils/environment.ts
|
|
361
|
+
import { execSync } from "child_process";
|
|
362
|
+
import { existsSync as existsSync4 } from "fs";
|
|
363
|
+
import { join as join3 } from "path";
|
|
364
|
+
var SUPPORTED_VERSIONS = {
|
|
365
|
+
python: ["3.11"],
|
|
366
|
+
qt: ["6.5.3", "6.9.2"]
|
|
367
|
+
};
|
|
368
|
+
function detectPython(version) {
|
|
369
|
+
const versionNum = version.replace(".", "");
|
|
370
|
+
const possiblePaths = [
|
|
371
|
+
`C:/Application/Python${versionNum}/python.exe`,
|
|
372
|
+
`C:/Python${versionNum}/python.exe`,
|
|
373
|
+
`C:/Program Files/Python${versionNum}/python.exe`,
|
|
374
|
+
`C:/Program Files (x86)/Python${versionNum}/python.exe`,
|
|
375
|
+
`${process.env.LOCALAPPDATA}/Programs/Python/Python${versionNum}/python.exe`,
|
|
376
|
+
`${process.env.USERPROFILE}/Python${versionNum}/python.exe`,
|
|
377
|
+
`${process.env.USERPROFILE}/AppData/Local/Programs/Python/Python${versionNum}/python.exe`,
|
|
378
|
+
`${process.env.USERPROFILE}/anaconda3/python.exe`,
|
|
379
|
+
`${process.env.USERPROFILE}/miniconda3/python.exe`,
|
|
380
|
+
`C:/ProgramData/anaconda3/python.exe`,
|
|
381
|
+
`C:/ProgramData/miniconda3/python.exe`
|
|
382
|
+
];
|
|
383
|
+
for (const p2 of possiblePaths) {
|
|
384
|
+
if (p2 && existsSync4(p2)) {
|
|
385
|
+
try {
|
|
386
|
+
const output = execSync(`"${p2}" --version`, { encoding: "utf-8" });
|
|
387
|
+
if (output.includes(version)) {
|
|
388
|
+
return p2;
|
|
389
|
+
}
|
|
390
|
+
} catch {
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const result = execSync("where python", { encoding: "utf-8" });
|
|
396
|
+
const paths = result.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
397
|
+
for (const pythonPath of paths) {
|
|
398
|
+
if (existsSync4(pythonPath)) {
|
|
399
|
+
try {
|
|
400
|
+
const output = execSync(`"${pythonPath}" --version`, { encoding: "utf-8" });
|
|
401
|
+
if (output.includes(version)) {
|
|
402
|
+
return pythonPath;
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
function detectQt(version) {
|
|
413
|
+
const msvcSuffix = version.startsWith("6.9") ? "msvc2022_64" : "msvc2019_64";
|
|
414
|
+
const possiblePaths = [
|
|
415
|
+
`D:/Tools/Qt/${version}/${msvcSuffix}`,
|
|
416
|
+
`D:/Qt/${version}/${msvcSuffix}`,
|
|
417
|
+
`C:/Qt/${version}/${msvcSuffix}`,
|
|
418
|
+
`C:/Tools/Qt/${version}/${msvcSuffix}`,
|
|
419
|
+
`C:/Program Files/Qt/${version}/${msvcSuffix}`,
|
|
420
|
+
`${process.env.USERPROFILE}/Qt/${version}/${msvcSuffix}`,
|
|
421
|
+
`E:/Qt/${version}/${msvcSuffix}`,
|
|
422
|
+
`F:/Qt/${version}/${msvcSuffix}`
|
|
423
|
+
];
|
|
424
|
+
for (const p2 of possiblePaths) {
|
|
425
|
+
if (p2 && existsSync4(join3(p2, "bin", "Qt6Core.dll"))) {
|
|
426
|
+
return p2;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (process.env.QTDIR) {
|
|
430
|
+
const qtDir = process.env.QTDIR;
|
|
431
|
+
if (existsSync4(join3(qtDir, "bin", "Qt6Core.dll"))) {
|
|
432
|
+
return qtDir;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const qtRoots = [
|
|
436
|
+
"C:/Qt",
|
|
437
|
+
"D:/Qt",
|
|
438
|
+
"D:/Tools/Qt",
|
|
439
|
+
`${process.env.USERPROFILE}/Qt`
|
|
440
|
+
];
|
|
441
|
+
for (const root of qtRoots) {
|
|
442
|
+
if (root && existsSync4(root)) {
|
|
443
|
+
const versionDir = join3(root, version, msvcSuffix);
|
|
444
|
+
if (existsSync4(join3(versionDir, "bin", "Qt6Core.dll"))) {
|
|
445
|
+
return versionDir;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
function detectUv() {
|
|
452
|
+
try {
|
|
453
|
+
execSync("uv --version", { encoding: "utf-8", stdio: "pipe" });
|
|
454
|
+
return true;
|
|
455
|
+
} catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function validatePythonPath(path) {
|
|
460
|
+
return /python\.exe$/i.test(path);
|
|
461
|
+
}
|
|
462
|
+
function validateQtPath(path) {
|
|
463
|
+
return existsSync4(path) && existsSync4(join3(path, "bin", "Qt6Core.dll"));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/commands/create.ts
|
|
467
|
+
function selectionsToProjectConfig(selections) {
|
|
468
|
+
const meta = FRAMEWORK_META[selections.frontendFramework];
|
|
469
|
+
return {
|
|
470
|
+
projectName: selections.projectName,
|
|
471
|
+
language: selections.language,
|
|
472
|
+
backend: selections.backend,
|
|
473
|
+
frontendFramework: selections.frontendFramework,
|
|
474
|
+
pythonEnvTool: selections.pythonEnvTool,
|
|
475
|
+
buildSystem: "cmake",
|
|
476
|
+
devServerUrl: `http://localhost:${selections.devPort}`,
|
|
477
|
+
devServerPort: selections.devPort,
|
|
478
|
+
distDir: meta.distDir,
|
|
479
|
+
features: selections.features,
|
|
480
|
+
newWindowBehavior: selections.newWindowBehavior,
|
|
481
|
+
pythonVersion: selections.pythonVersion,
|
|
482
|
+
pythonPath: selections.pythonPath,
|
|
483
|
+
qtVersion: selections.qtVersion,
|
|
484
|
+
qtPath: selections.qtPath
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
async function createCommand(name) {
|
|
488
|
+
p.intro(pc.bgCyan(pc.black(" create-qauri-app ")));
|
|
489
|
+
const projectName2 = await p.text({
|
|
490
|
+
message: "What is your project name?",
|
|
491
|
+
placeholder: "my-qauri-app",
|
|
492
|
+
initialValue: name,
|
|
493
|
+
defaultValue: "my-qauri-app",
|
|
494
|
+
validate: (value) => {
|
|
495
|
+
const result = validateProjectName(value);
|
|
496
|
+
if (!result.valid) return result.message;
|
|
497
|
+
return void 0;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
if (p.isCancel(projectName2)) {
|
|
501
|
+
p.cancel("Operation cancelled.");
|
|
502
|
+
process.exit(0);
|
|
503
|
+
}
|
|
504
|
+
const language = await p.select({
|
|
505
|
+
message: "Select native language.",
|
|
506
|
+
options: [
|
|
507
|
+
{ value: "cpp", label: "C++", hint: "Pure C++ backend" },
|
|
508
|
+
{ value: "python", label: "Python", hint: "Python backend with PySide6" }
|
|
509
|
+
]
|
|
510
|
+
});
|
|
511
|
+
if (p.isCancel(language)) {
|
|
512
|
+
p.cancel("Operation cancelled.");
|
|
513
|
+
process.exit(0);
|
|
514
|
+
}
|
|
515
|
+
let pythonEnvTool;
|
|
516
|
+
let pythonVersion = "3.11";
|
|
517
|
+
let pythonPath = "";
|
|
518
|
+
if (language === "python") {
|
|
519
|
+
const pmChoice = await p.select({
|
|
520
|
+
message: "Select Python package manager.",
|
|
521
|
+
options: [
|
|
522
|
+
{ value: "pip", label: "pip", hint: "Standard package manager" },
|
|
523
|
+
{ value: "uv", label: "uv", hint: "Fast package manager (recommended)" }
|
|
524
|
+
]
|
|
525
|
+
});
|
|
526
|
+
if (p.isCancel(pmChoice)) {
|
|
527
|
+
p.cancel("Operation cancelled.");
|
|
528
|
+
process.exit(0);
|
|
529
|
+
}
|
|
530
|
+
pythonEnvTool = pmChoice;
|
|
531
|
+
if (pmChoice === "uv" && !detectUv()) {
|
|
532
|
+
p.cancel(
|
|
533
|
+
"uv is not installed. Please install it first:\n pip install uv\n or visit: https://docs.astral.sh/uv/"
|
|
534
|
+
);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
pythonVersion = await p.select({
|
|
538
|
+
message: "Select Python version.",
|
|
539
|
+
options: SUPPORTED_VERSIONS.python.map((v) => ({
|
|
540
|
+
value: v,
|
|
541
|
+
label: `Python ${v}`,
|
|
542
|
+
hint: v === "3.11" ? "recommended" : void 0
|
|
543
|
+
}))
|
|
544
|
+
});
|
|
545
|
+
if (p.isCancel(pythonVersion)) {
|
|
546
|
+
p.cancel("Operation cancelled.");
|
|
547
|
+
process.exit(0);
|
|
548
|
+
}
|
|
549
|
+
pythonPath = detectPython(pythonVersion) || "";
|
|
550
|
+
if (pythonPath) {
|
|
551
|
+
p.log.info(`Python detected: ${pythonPath}`);
|
|
552
|
+
} else {
|
|
553
|
+
const manualPython = await p.text({
|
|
554
|
+
message: `Python ${pythonVersion} not found. Please enter the path to python.exe:`,
|
|
555
|
+
placeholder: "C:/Python311/python.exe",
|
|
556
|
+
validate: (value) => {
|
|
557
|
+
if (!value) return "Please enter a path.";
|
|
558
|
+
if (!validatePythonPath(value)) return "Path must end with python.exe";
|
|
559
|
+
if (!existsSync5(value)) return "File not found.";
|
|
560
|
+
return void 0;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
if (p.isCancel(manualPython)) {
|
|
564
|
+
p.cancel("Operation cancelled.");
|
|
565
|
+
process.exit(0);
|
|
566
|
+
}
|
|
567
|
+
pythonPath = manualPython;
|
|
568
|
+
p.log.info(`Using Python: ${pythonPath}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const qtVersion = await p.select({
|
|
572
|
+
message: "Select Qt version.",
|
|
573
|
+
options: SUPPORTED_VERSIONS.qt.map((v) => ({
|
|
574
|
+
value: v,
|
|
575
|
+
label: `Qt ${v}`,
|
|
576
|
+
hint: v === "6.5.3" ? "recommended" : void 0
|
|
577
|
+
}))
|
|
578
|
+
});
|
|
579
|
+
if (p.isCancel(qtVersion)) {
|
|
580
|
+
p.cancel("Operation cancelled.");
|
|
581
|
+
process.exit(0);
|
|
582
|
+
}
|
|
583
|
+
let qtPath = detectQt(qtVersion);
|
|
584
|
+
if (qtPath) {
|
|
585
|
+
p.log.info(`Qt detected: ${qtPath}`);
|
|
586
|
+
} else {
|
|
587
|
+
const manualQt = await p.text({
|
|
588
|
+
message: `Qt ${qtVersion} not found. Please enter the Qt MSVC directory:`,
|
|
589
|
+
placeholder: "C:/Qt/6.5.3/msvc2019_64",
|
|
590
|
+
validate: (value) => {
|
|
591
|
+
if (!value) return "Please enter a path.";
|
|
592
|
+
if (!validateQtPath(value)) return "Qt6Core.dll not found in bin directory.";
|
|
593
|
+
return void 0;
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
if (p.isCancel(manualQt)) {
|
|
597
|
+
p.cancel("Operation cancelled.");
|
|
598
|
+
process.exit(0);
|
|
599
|
+
}
|
|
600
|
+
qtPath = manualQt;
|
|
601
|
+
p.log.info(`Using Qt: ${qtPath}`);
|
|
602
|
+
}
|
|
603
|
+
const backend = await p.select({
|
|
604
|
+
message: "Select web backend.",
|
|
605
|
+
options: [
|
|
606
|
+
{ value: "webview2", label: "WebView2", hint: "Windows native (recommended)" },
|
|
607
|
+
{ value: "cef", label: "CEF", hint: "Chromium Embedded Framework" },
|
|
608
|
+
{ value: "both", label: "Both", hint: "Dual backend support" }
|
|
609
|
+
]
|
|
610
|
+
});
|
|
611
|
+
if (p.isCancel(backend)) {
|
|
612
|
+
p.cancel("Operation cancelled.");
|
|
613
|
+
process.exit(0);
|
|
614
|
+
}
|
|
615
|
+
const frameworkOptions = Object.entries(FRAMEWORK_META).map(([value, meta]) => ({ value, label: meta.displayName }));
|
|
616
|
+
const frontendFramework = await p.select({
|
|
617
|
+
message: "Select a frontend framework.",
|
|
618
|
+
options: frameworkOptions
|
|
619
|
+
});
|
|
620
|
+
if (p.isCancel(frontendFramework)) {
|
|
621
|
+
p.cancel("Operation cancelled.");
|
|
622
|
+
process.exit(0);
|
|
623
|
+
}
|
|
624
|
+
const features = await p.multiselect({
|
|
625
|
+
message: "Select features.",
|
|
626
|
+
options: [
|
|
627
|
+
{ value: "devtools", label: "DevTools (F12)", hint: "recommended" },
|
|
628
|
+
{ value: "reload", label: "Reload (F5)", hint: "recommended" },
|
|
629
|
+
{ value: "whitelist", label: "Command Whitelist" },
|
|
630
|
+
{ value: "examples", label: "Include Examples" }
|
|
631
|
+
],
|
|
632
|
+
initialValues: ["devtools", "reload", "examples"],
|
|
633
|
+
required: false
|
|
634
|
+
});
|
|
635
|
+
if (p.isCancel(features)) {
|
|
636
|
+
p.cancel("Operation cancelled.");
|
|
637
|
+
process.exit(0);
|
|
638
|
+
}
|
|
639
|
+
const newWindowBehavior = await p.select({
|
|
640
|
+
message: "New window behavior?",
|
|
641
|
+
options: [
|
|
642
|
+
{ value: "block", label: "Block", hint: "Prevent new windows" },
|
|
643
|
+
{ value: "browser", label: "Browser", hint: "Open in default browser" },
|
|
644
|
+
{ value: "new-window", label: "New Window", hint: "Create new Qauri window" }
|
|
645
|
+
]
|
|
646
|
+
});
|
|
647
|
+
if (p.isCancel(newWindowBehavior)) {
|
|
648
|
+
p.cancel("Operation cancelled.");
|
|
649
|
+
process.exit(0);
|
|
650
|
+
}
|
|
651
|
+
const installDeps = await p.confirm({
|
|
652
|
+
message: "Install npm dependencies?",
|
|
653
|
+
initialValue: true
|
|
654
|
+
});
|
|
655
|
+
if (p.isCancel(installDeps)) {
|
|
656
|
+
p.cancel("Operation cancelled.");
|
|
657
|
+
process.exit(0);
|
|
658
|
+
}
|
|
659
|
+
const projectPath = resolve4(process.cwd(), projectName2);
|
|
660
|
+
if (existsSync5(projectPath)) {
|
|
661
|
+
const overwrite = await p.confirm({
|
|
662
|
+
message: `Directory "${projectName2}" already exists. Overwrite?`,
|
|
663
|
+
initialValue: false
|
|
664
|
+
});
|
|
665
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
666
|
+
p.cancel("Operation cancelled.");
|
|
667
|
+
process.exit(0);
|
|
668
|
+
}
|
|
669
|
+
await removeDir(projectPath);
|
|
670
|
+
}
|
|
671
|
+
const s = p.spinner();
|
|
672
|
+
try {
|
|
673
|
+
s.start("Finding available port...");
|
|
674
|
+
const devPort = await findAvailablePort(5173);
|
|
675
|
+
s.stop(`Using port ${devPort}`);
|
|
676
|
+
s.start("Creating project structure...");
|
|
677
|
+
const selections = {
|
|
678
|
+
projectName: projectName2,
|
|
679
|
+
language,
|
|
680
|
+
pythonEnvTool,
|
|
681
|
+
backend,
|
|
682
|
+
frontendFramework,
|
|
683
|
+
features,
|
|
684
|
+
newWindowBehavior,
|
|
685
|
+
installDeps,
|
|
686
|
+
pythonVersion,
|
|
687
|
+
pythonPath,
|
|
688
|
+
qtVersion,
|
|
689
|
+
qtPath,
|
|
690
|
+
devPort
|
|
691
|
+
};
|
|
692
|
+
const config = selectionsToProjectConfig(selections);
|
|
693
|
+
await generateProject(projectPath, config);
|
|
694
|
+
s.stop("Project structure created");
|
|
695
|
+
if (installDeps) {
|
|
696
|
+
s.start("Installing npm dependencies...");
|
|
697
|
+
await installDependencies(projectPath);
|
|
698
|
+
s.stop("Dependencies installed");
|
|
699
|
+
}
|
|
700
|
+
let nextSteps = `cd ${projectName2}`;
|
|
701
|
+
if (!installDeps) {
|
|
702
|
+
nextSteps += "\nnpm install";
|
|
703
|
+
}
|
|
704
|
+
nextSteps += "\nqauri dev";
|
|
705
|
+
p.note(nextSteps, "Next steps");
|
|
706
|
+
p.outro(
|
|
707
|
+
pc.green("\u2713") + " Project created successfully!\n" + pc.dim(' Run "qauri dev" to start development')
|
|
708
|
+
);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
s.stop("Failed");
|
|
711
|
+
p.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/index.ts
|
|
717
|
+
var args = process.argv.slice(2);
|
|
718
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
719
|
+
console.log(`
|
|
720
|
+
Usage: create-qauri [project-name]
|
|
721
|
+
|
|
722
|
+
Create a new Qauri project with interactive prompts.
|
|
723
|
+
|
|
724
|
+
Arguments:
|
|
725
|
+
project-name Optional project name (will prompt if not provided)
|
|
726
|
+
|
|
727
|
+
Options:
|
|
728
|
+
-h, --help Display this help message
|
|
729
|
+
-v, --version Display version number
|
|
730
|
+
|
|
731
|
+
Examples:
|
|
732
|
+
$ npm create qauri@latest
|
|
733
|
+
$ npm create qauri@latest my-app
|
|
734
|
+
$ pnpm create qauri
|
|
735
|
+
`);
|
|
736
|
+
process.exit(0);
|
|
737
|
+
}
|
|
738
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
739
|
+
console.log("0.2.0");
|
|
740
|
+
process.exit(0);
|
|
741
|
+
}
|
|
742
|
+
var projectName = args.find((arg) => !arg.startsWith("-"));
|
|
743
|
+
await createCommand(projectName);
|
|
744
|
+
//# sourceMappingURL=index.mjs.map
|