create-tether-app 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.d.ts +1 -0
- package/dist/index.js +729 -0
- package/package.json +59 -0
- package/template/.env.example +18 -0
- package/template/README.md.template +123 -0
- package/template/backend/app/__init__.py.template +5 -0
- package/template/backend/app/main.py +66 -0
- package/template/backend/app/routes/__init__.py +3 -0
- package/template/backend/app/routes/chat.py +151 -0
- package/template/backend/app/routes/health.py +28 -0
- package/template/backend/app/routes/models.py +126 -0
- package/template/backend/app/services/__init__.py +3 -0
- package/template/backend/app/services/llm.py +526 -0
- package/template/backend/pyproject.toml.template +34 -0
- package/template/backend/scripts/build.py +112 -0
- package/template/frontend/App.css +58 -0
- package/template/frontend/App.tsx +62 -0
- package/template/frontend/components/Chat.css +220 -0
- package/template/frontend/components/Chat.tsx +284 -0
- package/template/frontend/components/ChatMessage.css +206 -0
- package/template/frontend/components/ChatMessage.tsx +62 -0
- package/template/frontend/components/ModelStatus.css +62 -0
- package/template/frontend/components/ModelStatus.tsx +103 -0
- package/template/frontend/hooks/useApi.ts +334 -0
- package/template/frontend/index.css +92 -0
- package/template/frontend/main.tsx +10 -0
- package/template/frontend/vite-env.d.ts +1 -0
- package/template/index.html.template +13 -0
- package/template/package.json.template +33 -0
- package/template/postcss.config.js.template +6 -0
- package/template/public/tether.svg +15 -0
- package/template/src-tauri/.cargo/config.toml +66 -0
- package/template/src-tauri/Cargo.lock +4764 -0
- package/template/src-tauri/Cargo.toml +24 -0
- package/template/src-tauri/build.rs +3 -0
- package/template/src-tauri/capabilities/default.json +40 -0
- package/template/src-tauri/icons/128x128.png +0 -0
- package/template/src-tauri/icons/128x128@2x.png +0 -0
- package/template/src-tauri/icons/32x32.png +0 -0
- package/template/src-tauri/icons/icon.icns +0 -0
- package/template/src-tauri/icons/icon.ico +0 -0
- package/template/src-tauri/src/main.rs +65 -0
- package/template/src-tauri/src/sidecar.rs +110 -0
- package/template/src-tauri/tauri.conf.json.template +44 -0
- package/template/tailwind.config.js.template +19 -0
- package/template/tsconfig.json +21 -0
- package/template/tsconfig.node.json +11 -0
- package/template/vite.config.ts +27 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/scaffold.ts
|
|
7
|
+
import fs2 from "fs-extra";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
|
|
12
|
+
// src/utils.ts
|
|
13
|
+
import fs from "fs-extra";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { execSync } from "child_process";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import validateNpmPackageName from "validate-npm-package-name";
|
|
18
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
var __dirname = path.dirname(__filename);
|
|
20
|
+
function validateProjectName(name) {
|
|
21
|
+
const result = validateNpmPackageName(name);
|
|
22
|
+
if (!result.validForNewPackages) {
|
|
23
|
+
const errors = [...result.errors || [], ...result.warnings || []];
|
|
24
|
+
return {
|
|
25
|
+
valid: false,
|
|
26
|
+
error: errors[0] || "Invalid package name"
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { valid: true };
|
|
30
|
+
}
|
|
31
|
+
function getPackageVersion() {
|
|
32
|
+
try {
|
|
33
|
+
const packageJsonPath = path.resolve(__dirname, "..", "package.json");
|
|
34
|
+
const packageJson = fs.readJsonSync(packageJsonPath);
|
|
35
|
+
return packageJson.version || "0.0.0";
|
|
36
|
+
} catch {
|
|
37
|
+
return "0.0.0";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getTemplateDir() {
|
|
41
|
+
const devPath = path.resolve(__dirname, "..", "..", "..", "template");
|
|
42
|
+
const prodPath = path.resolve(__dirname, "..", "template");
|
|
43
|
+
if (fs.existsSync(devPath)) {
|
|
44
|
+
return devPath;
|
|
45
|
+
}
|
|
46
|
+
return prodPath;
|
|
47
|
+
}
|
|
48
|
+
async function copyTemplate(templateDir, targetDir, replacements) {
|
|
49
|
+
await fs.copy(templateDir, targetDir);
|
|
50
|
+
const files = await getTemplateFiles(targetDir);
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
if (file.endsWith(".template")) {
|
|
53
|
+
const content = await fs.readFile(file, "utf-8");
|
|
54
|
+
let processed = content;
|
|
55
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
56
|
+
processed = processed.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
57
|
+
}
|
|
58
|
+
const newPath = file.replace(".template", "");
|
|
59
|
+
await fs.writeFile(newPath, processed);
|
|
60
|
+
await fs.remove(file);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function getTemplateFiles(dir) {
|
|
65
|
+
const files = [];
|
|
66
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const fullPath = path.join(dir, entry.name);
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
files.push(...await getTemplateFiles(fullPath));
|
|
71
|
+
} else {
|
|
72
|
+
files.push(fullPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return files;
|
|
76
|
+
}
|
|
77
|
+
function initGit(dir) {
|
|
78
|
+
try {
|
|
79
|
+
execSync("git init", { cwd: dir, stdio: "ignore" });
|
|
80
|
+
execSync("git add -A", { cwd: dir, stdio: "ignore" });
|
|
81
|
+
execSync('git commit -m "Initial commit from create-tether-app"', {
|
|
82
|
+
cwd: dir,
|
|
83
|
+
stdio: "ignore"
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function installDependencies(dir, packageManager) {
|
|
89
|
+
const commands = {
|
|
90
|
+
pnpm: "pnpm install",
|
|
91
|
+
npm: "npm install",
|
|
92
|
+
yarn: "yarn"
|
|
93
|
+
};
|
|
94
|
+
execSync(commands[packageManager], {
|
|
95
|
+
cwd: dir,
|
|
96
|
+
stdio: "inherit"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function checkCommandExists(command) {
|
|
100
|
+
try {
|
|
101
|
+
execSync(`which ${command}`, { stdio: "ignore" });
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function getCommandVersion(command, versionFlag = "--version") {
|
|
108
|
+
try {
|
|
109
|
+
const output = execSync(`${command} ${versionFlag}`, {
|
|
110
|
+
encoding: "utf-8",
|
|
111
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
112
|
+
});
|
|
113
|
+
const match = output.match(/(\d+\.\d+(\.\d+)?)/);
|
|
114
|
+
return match ? match[1] : output.trim().split("\n")[0];
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function checkEnvironment() {
|
|
120
|
+
const checks = [
|
|
121
|
+
{
|
|
122
|
+
name: "Node.js",
|
|
123
|
+
command: "node",
|
|
124
|
+
installed: checkCommandExists("node"),
|
|
125
|
+
version: getCommandVersion("node", "-v"),
|
|
126
|
+
required: "18+",
|
|
127
|
+
installUrl: "https://nodejs.org/"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "pnpm",
|
|
131
|
+
command: "pnpm",
|
|
132
|
+
installed: checkCommandExists("pnpm"),
|
|
133
|
+
version: getCommandVersion("pnpm", "-v"),
|
|
134
|
+
required: "8+",
|
|
135
|
+
installUrl: "https://pnpm.io/installation"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "Python",
|
|
139
|
+
command: "python3",
|
|
140
|
+
installed: checkCommandExists("python3"),
|
|
141
|
+
version: getCommandVersion("python3", "--version"),
|
|
142
|
+
required: "3.11+",
|
|
143
|
+
installUrl: "https://www.python.org/"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "uv",
|
|
147
|
+
command: "uv",
|
|
148
|
+
installed: checkCommandExists("uv"),
|
|
149
|
+
version: getCommandVersion("uv", "--version"),
|
|
150
|
+
required: "latest",
|
|
151
|
+
installUrl: "https://docs.astral.sh/uv/"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "Rust (cargo)",
|
|
155
|
+
command: "cargo",
|
|
156
|
+
installed: checkCommandExists("cargo"),
|
|
157
|
+
version: getCommandVersion("cargo", "--version"),
|
|
158
|
+
required: "latest",
|
|
159
|
+
installUrl: "https://rustup.rs/"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "Ollama",
|
|
163
|
+
command: "ollama",
|
|
164
|
+
installed: checkCommandExists("ollama"),
|
|
165
|
+
version: getCommandVersion("ollama", "--version"),
|
|
166
|
+
required: "optional",
|
|
167
|
+
installUrl: "https://ollama.com/"
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
return checks;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/scaffold.ts
|
|
174
|
+
function log(message, verbose) {
|
|
175
|
+
if (verbose) {
|
|
176
|
+
console.log(chalk.dim(` ${message}`));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function scaffoldProject(options) {
|
|
180
|
+
const targetDir = path2.resolve(process.cwd(), options.projectName);
|
|
181
|
+
const { verbose } = options;
|
|
182
|
+
if (await fs2.pathExists(targetDir)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Directory "${options.projectName}" already exists. Please choose a different name.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const spinner = ora("Checking prerequisites...").start();
|
|
188
|
+
if (!checkCommandExists("uv")) {
|
|
189
|
+
spinner.warn(
|
|
190
|
+
chalk.yellow("uv not found. Install it from https://docs.astral.sh/uv/")
|
|
191
|
+
);
|
|
192
|
+
console.log(
|
|
193
|
+
chalk.dim(
|
|
194
|
+
" You can still create the project, but Python setup may fail."
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
console.log();
|
|
198
|
+
}
|
|
199
|
+
if (options.packageManager === "pnpm" && !checkCommandExists("pnpm")) {
|
|
200
|
+
spinner.warn(chalk.yellow("pnpm not found. Falling back to npm."));
|
|
201
|
+
options.packageManager = "npm";
|
|
202
|
+
}
|
|
203
|
+
spinner.succeed("Prerequisites checked");
|
|
204
|
+
spinner.start("Creating project structure...");
|
|
205
|
+
log(`Target directory: ${targetDir}`, verbose);
|
|
206
|
+
const templateDir = getTemplateDir();
|
|
207
|
+
log(`Template directory: ${templateDir}`, verbose);
|
|
208
|
+
if (!await fs2.pathExists(templateDir)) {
|
|
209
|
+
spinner.fail("Template directory not found");
|
|
210
|
+
throw new Error(
|
|
211
|
+
"Template directory not found. This may be a bug in create-tether-app."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await fs2.ensureDir(targetDir);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error instanceof Error && error.message.includes("EACCES")) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Permission denied creating directory "${options.projectName}". Check your permissions.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
await copyTemplate(templateDir, targetDir, {
|
|
225
|
+
PROJECT_NAME: options.projectName,
|
|
226
|
+
TEMPLATE: options.template
|
|
227
|
+
});
|
|
228
|
+
log("Template files copied", verbose);
|
|
229
|
+
await customizeForTemplate(targetDir, options);
|
|
230
|
+
log(`Configured for ${options.template} backend`, verbose);
|
|
231
|
+
spinner.succeed("Project structure created");
|
|
232
|
+
if (options.useTailwind) {
|
|
233
|
+
spinner.start("Setting up Tailwind CSS...");
|
|
234
|
+
await setupTailwind(targetDir, verbose);
|
|
235
|
+
spinner.succeed("Tailwind CSS configured");
|
|
236
|
+
} else {
|
|
237
|
+
await removeTailwindTemplateFiles(targetDir, verbose);
|
|
238
|
+
}
|
|
239
|
+
if (!options.includeExample) {
|
|
240
|
+
spinner.start("Removing example components...");
|
|
241
|
+
await removeExampleComponents(targetDir);
|
|
242
|
+
spinner.succeed("Example components removed");
|
|
243
|
+
}
|
|
244
|
+
spinner.start("Initializing git repository...");
|
|
245
|
+
initGit(targetDir);
|
|
246
|
+
spinner.succeed("Git repository initialized");
|
|
247
|
+
if (!options.skipInstall) {
|
|
248
|
+
spinner.start(`Installing dependencies with ${options.packageManager}...`);
|
|
249
|
+
try {
|
|
250
|
+
installDependencies(targetDir, options.packageManager);
|
|
251
|
+
spinner.succeed("Dependencies installed");
|
|
252
|
+
} catch (error) {
|
|
253
|
+
spinner.fail("Failed to install dependencies");
|
|
254
|
+
console.log(chalk.yellow(" You can install them manually later."));
|
|
255
|
+
if (verbose && error instanceof Error) {
|
|
256
|
+
console.log(chalk.dim(` Error: ${error.message}`));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
console.log();
|
|
261
|
+
console.log(chalk.green.bold(" Success!"), `Created ${options.projectName}`);
|
|
262
|
+
console.log();
|
|
263
|
+
console.log(" Next steps:");
|
|
264
|
+
console.log();
|
|
265
|
+
console.log(chalk.cyan(` cd ${options.projectName}`));
|
|
266
|
+
if (options.skipInstall) {
|
|
267
|
+
console.log(chalk.cyan(` ${options.packageManager} install`));
|
|
268
|
+
}
|
|
269
|
+
console.log(chalk.cyan(" pnpm dev # Start frontend"));
|
|
270
|
+
console.log(chalk.cyan(" pnpm dev:py # Start Python backend"));
|
|
271
|
+
console.log(chalk.cyan(" pnpm dev:all # Start both"));
|
|
272
|
+
console.log();
|
|
273
|
+
console.log(" To build the desktop app:");
|
|
274
|
+
console.log();
|
|
275
|
+
console.log(chalk.cyan(" pnpm build:app"));
|
|
276
|
+
console.log();
|
|
277
|
+
if (options.template === "ollama") {
|
|
278
|
+
console.log(
|
|
279
|
+
chalk.dim(" Note: Make sure Ollama is running (ollama serve).")
|
|
280
|
+
);
|
|
281
|
+
console.log(chalk.dim(" Pull a model with: ollama pull llama3.2"));
|
|
282
|
+
console.log();
|
|
283
|
+
} else if (options.template === "local-llm") {
|
|
284
|
+
console.log(
|
|
285
|
+
chalk.dim(
|
|
286
|
+
" Note: For local LLM support, you'll need to download a model."
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
console.log(
|
|
290
|
+
chalk.dim(" See the README.md in your project for instructions.")
|
|
291
|
+
);
|
|
292
|
+
console.log();
|
|
293
|
+
} else if (options.template === "openai") {
|
|
294
|
+
console.log(
|
|
295
|
+
chalk.dim(
|
|
296
|
+
" Note: Set your OPENAI_API_KEY in .env to use the OpenAI API."
|
|
297
|
+
)
|
|
298
|
+
);
|
|
299
|
+
console.log();
|
|
300
|
+
}
|
|
301
|
+
if (options.useTailwind) {
|
|
302
|
+
console.log(
|
|
303
|
+
chalk.dim(
|
|
304
|
+
" Tailwind CSS is configured. Use utility classes in your components."
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
console.log();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function setupTailwind(targetDir, verbose) {
|
|
311
|
+
const templateDir = getTemplateDir();
|
|
312
|
+
const tailwindTemplatePath = path2.join(
|
|
313
|
+
templateDir,
|
|
314
|
+
"tailwind.config.js.template"
|
|
315
|
+
);
|
|
316
|
+
const tailwindTargetPath = path2.join(targetDir, "tailwind.config.js");
|
|
317
|
+
if (await fs2.pathExists(tailwindTemplatePath)) {
|
|
318
|
+
await fs2.copy(tailwindTemplatePath, tailwindTargetPath);
|
|
319
|
+
log("Created tailwind.config.js", verbose);
|
|
320
|
+
}
|
|
321
|
+
const postcssTemplatePath = path2.join(
|
|
322
|
+
templateDir,
|
|
323
|
+
"postcss.config.js.template"
|
|
324
|
+
);
|
|
325
|
+
const postcssTargetPath = path2.join(targetDir, "postcss.config.js");
|
|
326
|
+
if (await fs2.pathExists(postcssTemplatePath)) {
|
|
327
|
+
await fs2.copy(postcssTemplatePath, postcssTargetPath);
|
|
328
|
+
log("Created postcss.config.js", verbose);
|
|
329
|
+
}
|
|
330
|
+
const indexCssPath = path2.join(targetDir, "frontend", "index.css");
|
|
331
|
+
if (await fs2.pathExists(indexCssPath)) {
|
|
332
|
+
const existingCss = await fs2.readFile(indexCssPath, "utf-8");
|
|
333
|
+
const tailwindDirectives = `@tailwind base;
|
|
334
|
+
@tailwind components;
|
|
335
|
+
@tailwind utilities;
|
|
336
|
+
|
|
337
|
+
`;
|
|
338
|
+
await fs2.writeFile(indexCssPath, tailwindDirectives + existingCss);
|
|
339
|
+
log("Added Tailwind directives to index.css", verbose);
|
|
340
|
+
}
|
|
341
|
+
const packageJsonPath = path2.join(targetDir, "package.json");
|
|
342
|
+
if (await fs2.pathExists(packageJsonPath)) {
|
|
343
|
+
const packageJson = await fs2.readJson(packageJsonPath);
|
|
344
|
+
packageJson.devDependencies = packageJson.devDependencies || {};
|
|
345
|
+
packageJson.devDependencies["tailwindcss"] = "^3.4.0";
|
|
346
|
+
packageJson.devDependencies["postcss"] = "^8.4.0";
|
|
347
|
+
packageJson.devDependencies["autoprefixer"] = "^10.4.0";
|
|
348
|
+
await fs2.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
349
|
+
log("Added Tailwind dependencies to package.json", verbose);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function customizeForTemplate(targetDir, options) {
|
|
353
|
+
const pythonServicePath = path2.join(
|
|
354
|
+
targetDir,
|
|
355
|
+
"backend",
|
|
356
|
+
"app",
|
|
357
|
+
"services",
|
|
358
|
+
"llm.py"
|
|
359
|
+
);
|
|
360
|
+
const pyprojectPath = path2.join(targetDir, "backend", "pyproject.toml");
|
|
361
|
+
if (await fs2.pathExists(pyprojectPath)) {
|
|
362
|
+
let content = await fs2.readFile(pyprojectPath, "utf-8");
|
|
363
|
+
if (options.template === "ollama") {
|
|
364
|
+
content = content.replace(/^\s*"llama-cpp-python[^"]*",?\n/gm, "");
|
|
365
|
+
content = content.replace(/^\s*"openai[^"]*",?\n/gm, "");
|
|
366
|
+
} else if (options.template === "openai") {
|
|
367
|
+
content = content.replace(/^\s*"llama-cpp-python[^"]*",?\n/gm, "");
|
|
368
|
+
} else if (options.template === "custom") {
|
|
369
|
+
content = content.replace(/^\s*"llama-cpp-python[^"]*",?\n/gm, "");
|
|
370
|
+
content = content.replace(/^\s*"openai[^"]*",?\n/gm, "");
|
|
371
|
+
}
|
|
372
|
+
await fs2.writeFile(pyprojectPath, content);
|
|
373
|
+
}
|
|
374
|
+
if (await fs2.pathExists(pythonServicePath)) {
|
|
375
|
+
let content = await fs2.readFile(pythonServicePath, "utf-8");
|
|
376
|
+
const backendRegex = /(tether_llm_backend:\s*Literal\[[^\]]+\]\s*=\s*)["'][^"']+["']/;
|
|
377
|
+
if (options.template === "local-llm") {
|
|
378
|
+
content = content.replace(backendRegex, '$1"local"');
|
|
379
|
+
} else if (options.template === "ollama") {
|
|
380
|
+
content = content.replace(backendRegex, '$1"ollama"');
|
|
381
|
+
} else if (options.template === "openai") {
|
|
382
|
+
content = content.replace(backendRegex, '$1"openai"');
|
|
383
|
+
} else if (options.template === "custom") {
|
|
384
|
+
content = content.replace(backendRegex, '$1"mock"');
|
|
385
|
+
}
|
|
386
|
+
await fs2.writeFile(pythonServicePath, content);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function removeTailwindTemplateFiles(targetDir, verbose) {
|
|
390
|
+
const tailwindConfigPath = path2.join(targetDir, "tailwind.config.js");
|
|
391
|
+
const postcssConfigPath = path2.join(targetDir, "postcss.config.js");
|
|
392
|
+
if (await fs2.pathExists(tailwindConfigPath)) {
|
|
393
|
+
await fs2.remove(tailwindConfigPath);
|
|
394
|
+
log("Removed tailwind.config.js (not requested)", verbose);
|
|
395
|
+
}
|
|
396
|
+
if (await fs2.pathExists(postcssConfigPath)) {
|
|
397
|
+
await fs2.remove(postcssConfigPath);
|
|
398
|
+
log("Removed postcss.config.js (not requested)", verbose);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function removeExampleComponents(targetDir) {
|
|
402
|
+
const exampleFiles = [
|
|
403
|
+
path2.join(targetDir, "frontend", "components", "Chat.tsx"),
|
|
404
|
+
path2.join(targetDir, "frontend", "components", "ChatMessage.tsx")
|
|
405
|
+
];
|
|
406
|
+
for (const file of exampleFiles) {
|
|
407
|
+
if (await fs2.pathExists(file)) {
|
|
408
|
+
await fs2.remove(file);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const appPath = path2.join(targetDir, "frontend", "App.tsx");
|
|
412
|
+
if (await fs2.pathExists(appPath)) {
|
|
413
|
+
let content = await fs2.readFile(appPath, "utf-8");
|
|
414
|
+
content = content.replace(/import.*Chat.*from.*\n?/g, "");
|
|
415
|
+
content = content.replace(/<Chat\s*\/>/g, "");
|
|
416
|
+
await fs2.writeFile(appPath, content);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/prompts.ts
|
|
421
|
+
import inquirer from "inquirer";
|
|
422
|
+
async function promptForOptions(options) {
|
|
423
|
+
const questions = [];
|
|
424
|
+
if (options.needsName) {
|
|
425
|
+
questions.push({
|
|
426
|
+
type: "input",
|
|
427
|
+
name: "projectName",
|
|
428
|
+
message: "Project name:",
|
|
429
|
+
default: "my-tether-app",
|
|
430
|
+
validate: (input) => {
|
|
431
|
+
if (!input.trim()) {
|
|
432
|
+
return "Project name is required";
|
|
433
|
+
}
|
|
434
|
+
if (!/^[a-z0-9-_]+$/i.test(input)) {
|
|
435
|
+
return "Project name can only contain letters, numbers, hyphens, and underscores";
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
questions.push(
|
|
442
|
+
{
|
|
443
|
+
type: "list",
|
|
444
|
+
name: "template",
|
|
445
|
+
message: "Select ML backend:",
|
|
446
|
+
choices: [
|
|
447
|
+
{
|
|
448
|
+
name: "Ollama - Run models locally via Ollama (Recommended)",
|
|
449
|
+
value: "ollama"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "Local LLM (llama-cpp-python) - Embed models directly",
|
|
453
|
+
value: "local-llm"
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: "OpenAI API - Use GPT models via API",
|
|
457
|
+
value: "openai"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "Custom - Bare FastAPI setup",
|
|
461
|
+
value: "custom"
|
|
462
|
+
}
|
|
463
|
+
],
|
|
464
|
+
default: "ollama"
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
type: "confirm",
|
|
468
|
+
name: "includeExample",
|
|
469
|
+
message: "Include example chat component?",
|
|
470
|
+
default: true
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
type: "confirm",
|
|
474
|
+
name: "useTailwind",
|
|
475
|
+
message: "Add Tailwind CSS for styling?",
|
|
476
|
+
default: false
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
const answers = await inquirer.prompt(questions);
|
|
480
|
+
return {
|
|
481
|
+
projectName: answers.projectName || "",
|
|
482
|
+
template: answers.template || "ollama",
|
|
483
|
+
includeExample: answers.includeExample ?? true,
|
|
484
|
+
useTailwind: answers.useTailwind ?? false
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/cli.ts
|
|
489
|
+
import chalk2 from "chalk";
|
|
490
|
+
var LLM_TEMPLATES = [
|
|
491
|
+
{
|
|
492
|
+
name: "ollama",
|
|
493
|
+
description: "Run models locally via Ollama (Recommended)",
|
|
494
|
+
details: "Requires Ollama to be installed and running. Pull models with 'ollama pull'."
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "local-llm",
|
|
498
|
+
description: "Embed models directly with llama-cpp-python",
|
|
499
|
+
details: "Models are bundled with the app. Good for offline distribution."
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: "openai",
|
|
503
|
+
description: "Use OpenAI API (requires API key)",
|
|
504
|
+
details: "Uses GPT models via the OpenAI API. Requires OPENAI_API_KEY env var."
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "custom",
|
|
508
|
+
description: "Bare FastAPI setup, no LLM integration",
|
|
509
|
+
details: "Clean slate for custom ML/AI implementations."
|
|
510
|
+
}
|
|
511
|
+
];
|
|
512
|
+
function createCli() {
|
|
513
|
+
const program = new Command();
|
|
514
|
+
program.name("create-tether-app").description("Create a new Tether AI/ML desktop application").version(getPackageVersion()).argument("[project-name]", "Name of the project to create").option(
|
|
515
|
+
"--llm <provider>",
|
|
516
|
+
"LLM backend: ollama (default), local-llm, openai, custom"
|
|
517
|
+
).option("-t, --template <template>", "Alias for --llm").option("-y, --yes", "Skip prompts and use defaults (ollama, with example)").option("--skip-prompts", "Alias for --yes").option("--skip-install", "Skip dependency installation").option("--use-npm", "Use npm instead of pnpm").option("--use-yarn", "Use yarn instead of pnpm").option("--dry-run", "Show what would be created without making changes").option("--no-example", "Skip example chat component").option("--tailwind", "Include Tailwind CSS setup").option("--no-tailwind", "Skip Tailwind CSS setup").option("-v, --verbose", "Show detailed output").option("--list-templates", "List available LLM templates").option("--check", "Check if all required dependencies are installed").addHelpText(
|
|
518
|
+
"after",
|
|
519
|
+
`
|
|
520
|
+
Examples:
|
|
521
|
+
${chalk2.cyan("npx create-tether-app my-app")}
|
|
522
|
+
Create a new app with interactive prompts
|
|
523
|
+
|
|
524
|
+
${chalk2.cyan("npx create-tether-app my-app -y")}
|
|
525
|
+
Create with defaults (ollama, includes example)
|
|
526
|
+
|
|
527
|
+
${chalk2.cyan("npx create-tether-app my-app --llm openai")}
|
|
528
|
+
Create with OpenAI backend
|
|
529
|
+
|
|
530
|
+
${chalk2.cyan("npx create-tether-app my-app --tailwind")}
|
|
531
|
+
Create with Tailwind CSS support
|
|
532
|
+
|
|
533
|
+
${chalk2.cyan("npx create-tether-app my-app --dry-run")}
|
|
534
|
+
Preview what would be created
|
|
535
|
+
|
|
536
|
+
${chalk2.cyan("npx create-tether-app --list-templates")}
|
|
537
|
+
Show available LLM backends
|
|
538
|
+
|
|
539
|
+
${chalk2.cyan("npx create-tether-app --check")}
|
|
540
|
+
Check if all dependencies are installed
|
|
541
|
+
|
|
542
|
+
LLM Backends:
|
|
543
|
+
ollama Run models locally via Ollama (recommended)
|
|
544
|
+
local-llm Embed models directly with llama-cpp-python
|
|
545
|
+
openai Use OpenAI API (requires API key)
|
|
546
|
+
custom Bare FastAPI setup, no LLM integration
|
|
547
|
+
`
|
|
548
|
+
).action(async (projectName, options) => {
|
|
549
|
+
if (options.listTemplates) {
|
|
550
|
+
console.log();
|
|
551
|
+
console.log(chalk2.bold("Available LLM Templates:"));
|
|
552
|
+
console.log();
|
|
553
|
+
for (const template of LLM_TEMPLATES) {
|
|
554
|
+
console.log(chalk2.cyan(` ${template.name}`));
|
|
555
|
+
console.log(chalk2.white(` ${template.description}`));
|
|
556
|
+
console.log(chalk2.dim(` ${template.details}`));
|
|
557
|
+
console.log();
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (options.check) {
|
|
562
|
+
console.log();
|
|
563
|
+
console.log(chalk2.bold("Checking dependencies..."));
|
|
564
|
+
console.log();
|
|
565
|
+
const checks2 = checkEnvironment();
|
|
566
|
+
let allGood = true;
|
|
567
|
+
for (const dep of checks2) {
|
|
568
|
+
const status = dep.installed ? chalk2.green("\u2713") : dep.required === "optional" ? chalk2.yellow("\u25CB") : chalk2.red("\u2717");
|
|
569
|
+
const version = dep.version ? chalk2.dim(` (${dep.version})`) : "";
|
|
570
|
+
const required = dep.required === "optional" ? chalk2.dim(" [optional]") : chalk2.dim(` [${dep.required}]`);
|
|
571
|
+
console.log(` ${status} ${dep.name}${version}${required}`);
|
|
572
|
+
if (!dep.installed && dep.required !== "optional") {
|
|
573
|
+
allGood = false;
|
|
574
|
+
console.log(chalk2.dim(` Install: ${dep.installUrl}`));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
console.log();
|
|
578
|
+
if (allGood) {
|
|
579
|
+
console.log(chalk2.green("All required dependencies are installed!"));
|
|
580
|
+
} else {
|
|
581
|
+
console.log(
|
|
582
|
+
chalk2.yellow(
|
|
583
|
+
"Some dependencies are missing. Install them before creating a project."
|
|
584
|
+
)
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
console.log();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
console.log();
|
|
591
|
+
console.log(chalk2.bold.cyan(" Tether"));
|
|
592
|
+
console.log(chalk2.dim(" Create AI/ML desktop applications"));
|
|
593
|
+
console.log();
|
|
594
|
+
const checks = checkEnvironment();
|
|
595
|
+
const missing = checks.filter(
|
|
596
|
+
(d) => !d.installed && d.required !== "optional"
|
|
597
|
+
);
|
|
598
|
+
if (missing.length > 0) {
|
|
599
|
+
console.log(chalk2.yellow("Warning: Some dependencies are missing:"));
|
|
600
|
+
for (const dep of missing) {
|
|
601
|
+
console.log(chalk2.yellow(` - ${dep.name}: ${dep.installUrl}`));
|
|
602
|
+
}
|
|
603
|
+
console.log();
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const llmProvider = options.llm || options.template;
|
|
607
|
+
const skipPrompts = options.yes || options.skipPrompts;
|
|
608
|
+
const verbose = options.verbose || false;
|
|
609
|
+
let name = projectName;
|
|
610
|
+
if (!name) {
|
|
611
|
+
if (skipPrompts || options.dryRun) {
|
|
612
|
+
console.error(chalk2.red("Error: Project name is required"));
|
|
613
|
+
console.log(
|
|
614
|
+
chalk2.dim(" Usage: npx create-tether-app my-app [options]")
|
|
615
|
+
);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
const answers = await promptForOptions({ needsName: true });
|
|
619
|
+
name = answers.projectName;
|
|
620
|
+
}
|
|
621
|
+
const validation = validateProjectName(name);
|
|
622
|
+
if (!validation.valid) {
|
|
623
|
+
console.error(chalk2.red(`Error: Invalid project name "${name}"`));
|
|
624
|
+
console.log(chalk2.dim(` ${validation.error}`));
|
|
625
|
+
console.log(
|
|
626
|
+
chalk2.dim(
|
|
627
|
+
" Project names must be lowercase and can contain letters, numbers, and hyphens."
|
|
628
|
+
)
|
|
629
|
+
);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
let template = llmProvider;
|
|
633
|
+
let includeExample = options.example !== false;
|
|
634
|
+
let useTailwind = options.tailwind;
|
|
635
|
+
if (!skipPrompts && !options.dryRun && !template) {
|
|
636
|
+
const answers = await promptForOptions({ needsName: false });
|
|
637
|
+
template = answers.template;
|
|
638
|
+
includeExample = answers.includeExample;
|
|
639
|
+
if (useTailwind === void 0) {
|
|
640
|
+
useTailwind = answers.useTailwind;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
template = template || "ollama";
|
|
644
|
+
useTailwind = useTailwind ?? false;
|
|
645
|
+
if (options.dryRun) {
|
|
646
|
+
console.log(chalk2.yellow("Dry run mode - no changes will be made"));
|
|
647
|
+
console.log();
|
|
648
|
+
console.log("Would create project:");
|
|
649
|
+
console.log(chalk2.cyan(` Name: ${name}`));
|
|
650
|
+
console.log(chalk2.cyan(` LLM Backend: ${template}`));
|
|
651
|
+
console.log(
|
|
652
|
+
chalk2.cyan(` Include Example: ${includeExample ? "yes" : "no"}`)
|
|
653
|
+
);
|
|
654
|
+
console.log(
|
|
655
|
+
chalk2.cyan(` Tailwind CSS: ${useTailwind ? "yes" : "no"}`)
|
|
656
|
+
);
|
|
657
|
+
console.log(
|
|
658
|
+
chalk2.cyan(
|
|
659
|
+
` Package Manager: ${options.useNpm ? "npm" : options.useYarn ? "yarn" : "pnpm"}`
|
|
660
|
+
)
|
|
661
|
+
);
|
|
662
|
+
console.log();
|
|
663
|
+
console.log("Would create directory structure:");
|
|
664
|
+
console.log(chalk2.dim(` ${name}/`));
|
|
665
|
+
console.log(
|
|
666
|
+
chalk2.dim(` \u251C\u2500\u2500 frontend/ # React + TypeScript + Vite`)
|
|
667
|
+
);
|
|
668
|
+
console.log(chalk2.dim(` \u251C\u2500\u2500 backend/ # Python + FastAPI`));
|
|
669
|
+
console.log(chalk2.dim(` \u2514\u2500\u2500 src-tauri/ # Tauri (Rust)`));
|
|
670
|
+
if (useTailwind) {
|
|
671
|
+
console.log();
|
|
672
|
+
console.log("Tailwind files:");
|
|
673
|
+
console.log(chalk2.dim(` \u251C\u2500\u2500 tailwind.config.js`));
|
|
674
|
+
console.log(chalk2.dim(` \u2514\u2500\u2500 postcss.config.js`));
|
|
675
|
+
}
|
|
676
|
+
console.log();
|
|
677
|
+
console.log("Would install dependencies:");
|
|
678
|
+
console.log(
|
|
679
|
+
chalk2.dim(
|
|
680
|
+
` react, react-dom, vite, typescript, @tauri-apps/cli, ...`
|
|
681
|
+
)
|
|
682
|
+
);
|
|
683
|
+
if (useTailwind) {
|
|
684
|
+
console.log(chalk2.dim(` tailwindcss, postcss, autoprefixer`));
|
|
685
|
+
}
|
|
686
|
+
console.log();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
await scaffoldProject({
|
|
690
|
+
projectName: name,
|
|
691
|
+
template,
|
|
692
|
+
includeExample,
|
|
693
|
+
skipInstall: options.skipInstall || false,
|
|
694
|
+
packageManager: options.useNpm ? "npm" : options.useYarn ? "yarn" : "pnpm",
|
|
695
|
+
useTailwind,
|
|
696
|
+
verbose
|
|
697
|
+
});
|
|
698
|
+
} catch (error) {
|
|
699
|
+
if (error instanceof Error) {
|
|
700
|
+
console.error(chalk2.red(`Error: ${error.message}`));
|
|
701
|
+
if (error.message.includes("already exists")) {
|
|
702
|
+
console.log(
|
|
703
|
+
chalk2.dim(
|
|
704
|
+
" Try a different project name or delete the existing directory."
|
|
705
|
+
)
|
|
706
|
+
);
|
|
707
|
+
} else if (error.message.includes("EACCES") || error.message.includes("permission")) {
|
|
708
|
+
console.log(
|
|
709
|
+
chalk2.dim(
|
|
710
|
+
" Permission denied. Try running with sudo or check directory permissions."
|
|
711
|
+
)
|
|
712
|
+
);
|
|
713
|
+
} else if (error.message.includes("uv not found") || error.message.includes("uv")) {
|
|
714
|
+
console.log(
|
|
715
|
+
chalk2.dim(
|
|
716
|
+
" Install uv from: https://docs.astral.sh/uv/getting-started/installation/"
|
|
717
|
+
)
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
return program;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/index.ts
|
|
728
|
+
var cli = createCli();
|
|
729
|
+
cli.parse(process.argv);
|