@vylos/cli 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/bin/vylos.mjs ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { createRequire } from 'node:module';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const require = createRequire(import.meta.url);
9
+ const tsxLoader = pathToFileURL(require.resolve('tsx')).href;
10
+ const cli = resolve(__dirname, '..', 'src', 'index.ts');
11
+
12
+ const child = spawn(process.execPath, ['--import', tsxLoader, cli, ...process.argv.slice(2)], {
13
+ stdio: 'inherit',
14
+ cwd: process.cwd(),
15
+ });
16
+ child.on('exit', (code) => process.exit(code ?? 0));
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@vylos/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "vylos": "./bin/vylos.mjs"
7
+ },
8
+ "dependencies": {
9
+ "@vitejs/plugin-vue": "^5.2.1",
10
+ "vue": "^3.5.13",
11
+ "vite": "^6.0.7",
12
+ "vite-plugin-singlefile": "^2.0.3",
13
+ "tailwindcss": "^4.0.0",
14
+ "@tailwindcss/vite": "^4.0.0",
15
+ "tsx": "^4.19.2",
16
+ "@vylos/core": "0.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.7.2"
20
+ },
21
+ "scripts": {
22
+ "dev": "tsx src/index.ts dev",
23
+ "build": "tsx src/index.ts build"
24
+ }
25
+ }
@@ -0,0 +1,38 @@
1
+ import { build as viteBuild } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+ import { viteSingleFile } from 'vite-plugin-singlefile';
5
+ import { resolve } from 'path';
6
+ import { vylosProjectPlugin } from '../vite/projectPlugin';
7
+ import { vylosI18nPlugin } from '../vite/i18nPlugin';
8
+
9
+ export async function build(projectRoot: string) {
10
+ console.log(`\n Vylos building...\n Project: ${projectRoot}\n`);
11
+
12
+ const outDir = resolve(projectRoot, 'dist');
13
+
14
+ await viteBuild({
15
+ root: projectRoot,
16
+ plugins: [
17
+ vue(),
18
+ tailwindcss(),
19
+ vylosProjectPlugin(projectRoot),
20
+ vylosI18nPlugin(projectRoot),
21
+ viteSingleFile(),
22
+ ],
23
+ resolve: {
24
+ alias: {
25
+ '@project': projectRoot,
26
+ },
27
+ },
28
+ build: {
29
+ outDir,
30
+ emptyOutDir: true,
31
+ rollupOptions: {
32
+ input: resolve(projectRoot, 'index.html'),
33
+ },
34
+ },
35
+ });
36
+
37
+ console.log(`\n Build complete: ${outDir}\n`);
38
+ }
@@ -0,0 +1,32 @@
1
+ import { resolve, dirname } from 'path';
2
+ import { existsSync, mkdirSync, cpSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ export async function create(name: string, targetDir?: string) {
6
+ const dest = resolve(targetDir ?? process.cwd(), name);
7
+
8
+ if (existsSync(dest)) {
9
+ console.error(` Directory already exists: ${dest}`);
10
+ process.exit(1);
11
+ }
12
+
13
+ // Templates are shipped with @vylos/cli
14
+ const cliRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
15
+ const templateDir = resolve(cliRoot, 'templates');
16
+
17
+ if (!existsSync(templateDir)) {
18
+ console.error(` Template directory not found: ${templateDir}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log(`\n Creating Vylos project: ${name}\n`);
23
+
24
+ mkdirSync(dest, { recursive: true });
25
+ cpSync(templateDir, dest, { recursive: true });
26
+
27
+ console.log(` Project created at: ${dest}`);
28
+ console.log(`\n Next steps:`);
29
+ console.log(` cd ${name}`);
30
+ console.log(` pnpm install`);
31
+ console.log(` pnpm dev\n`);
32
+ }
@@ -0,0 +1,38 @@
1
+ import { createServer } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+ import { resolve } from 'path';
5
+ import { vylosProjectPlugin } from '../vite/projectPlugin';
6
+ import { vylosI18nPlugin } from '../vite/i18nPlugin';
7
+ import { vylosEditorApiPlugin } from '../vite/editorApiPlugin';
8
+
9
+ export async function dev(projectRoot: string) {
10
+ console.log(`\n Vylos dev server starting...\n Project: ${projectRoot}\n`);
11
+
12
+ const server = await createServer({
13
+ root: projectRoot,
14
+ plugins: [
15
+ vue(),
16
+ tailwindcss(),
17
+ vylosProjectPlugin(projectRoot),
18
+ vylosI18nPlugin(projectRoot),
19
+ vylosEditorApiPlugin(),
20
+ ],
21
+ resolve: {
22
+ alias: {
23
+ '@project': projectRoot,
24
+ },
25
+ dedupe: ['vue', 'pinia'],
26
+ },
27
+ optimizeDeps: {
28
+ include: ['vue', 'pinia', 'reflect-metadata'],
29
+ },
30
+ server: {
31
+ port: 5173,
32
+ open: true,
33
+ },
34
+ });
35
+
36
+ await server.listen();
37
+ server.printUrls();
38
+ }
@@ -0,0 +1,28 @@
1
+ import { createServer } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+ import { dirname, resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { vylosEditorApiPlugin } from '../vite/editorApiPlugin';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ export async function editor(projectRoot: string) {
10
+ console.log(`\n Vylos editor starting...\n Project: ${projectRoot}\n`);
11
+
12
+ const editorRoot = resolve(__dirname, '..', 'editor');
13
+
14
+ const server = await createServer({
15
+ root: editorRoot,
16
+ plugins: [
17
+ vue(),
18
+ vylosEditorApiPlugin(),
19
+ ],
20
+ server: {
21
+ port: 5174,
22
+ open: true,
23
+ },
24
+ });
25
+
26
+ await server.listen();
27
+ server.printUrls();
28
+ }
@@ -0,0 +1,24 @@
1
+ import { execSync } from 'child_process';
2
+ import { resolve } from 'path';
3
+ import { existsSync } from 'fs';
4
+
5
+ export async function verify(projectRoot: string) {
6
+ console.log(`\n Vylos verify\n Project: ${projectRoot}\n`);
7
+
8
+ const tsconfig = resolve(projectRoot, 'tsconfig.json');
9
+ if (!existsSync(tsconfig)) {
10
+ console.error(` Error: No tsconfig.json found at ${projectRoot}`);
11
+ process.exit(1);
12
+ }
13
+
14
+ try {
15
+ execSync('vue-tsc --noEmit', {
16
+ cwd: projectRoot,
17
+ stdio: 'inherit',
18
+ });
19
+ console.log('\n Verification passed — no type errors found.\n');
20
+ } catch {
21
+ console.error('\n Verification failed — type errors found above.\n');
22
+ process.exit(1);
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:system-ui,sans-serif;background:#1a1a2e;color:#e0e0e0;">
3
+ <div style="text-align:center;">
4
+ <h1 style="font-size:2rem;margin-bottom:0.5rem;">Vylos Editor</h1>
5
+ <p style="opacity:0.6;">Coming Soon</p>
6
+ </div>
7
+ </div>
8
+ </template>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Vylos Editor</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="./main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,4 @@
1
+ import { createApp } from 'vue';
2
+ import App from './App.vue';
3
+
4
+ createApp(App).mount('#app');
package/src/index.ts ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'path';
3
+
4
+ const args = process.argv.slice(2);
5
+ const command = args[0];
6
+
7
+ async function main() {
8
+ switch (command) {
9
+ case 'dev': {
10
+ const projectRoot = resolve(args[1] ?? process.cwd());
11
+ const { dev } = await import('./commands/dev');
12
+ await dev(projectRoot);
13
+ break;
14
+ }
15
+
16
+ case 'build': {
17
+ const projectRoot = resolve(args[1] ?? process.cwd());
18
+ const { build } = await import('./commands/build');
19
+ await build(projectRoot);
20
+ break;
21
+ }
22
+
23
+ case 'editor': {
24
+ const projectRoot = resolve(args[1] ?? process.cwd());
25
+ const { editor } = await import('./commands/editor');
26
+ await editor(projectRoot);
27
+ break;
28
+ }
29
+
30
+ case 'verify': {
31
+ const projectRoot = resolve(args[1] ?? process.cwd());
32
+ const { verify } = await import('./commands/verify');
33
+ await verify(projectRoot);
34
+ break;
35
+ }
36
+
37
+ case 'create': {
38
+ const name = args[1];
39
+ if (!name) {
40
+ console.error(' Usage: vylos create <project-name>');
41
+ process.exit(1);
42
+ }
43
+ const { create } = await import('./commands/create');
44
+ await create(name, args[2]);
45
+ break;
46
+ }
47
+
48
+ default:
49
+ console.log(`
50
+ Vylos — Visual Novel Engine
51
+
52
+ Usage:
53
+ vylos dev [project-dir] Start dev server
54
+ vylos build [project-dir] Build for production
55
+ vylos editor [project-dir] Open visual editor
56
+ vylos verify [project-dir] Type-check project
57
+ vylos create <name> Create new project
58
+ `);
59
+ }
60
+ }
61
+
62
+ main().catch((error) => {
63
+ console.error(error);
64
+ process.exit(1);
65
+ });
@@ -0,0 +1,37 @@
1
+ import { defineConfig, type UserConfig } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+ import { resolve } from 'path';
5
+
6
+ export interface VylosViteOptions {
7
+ projectRoot: string;
8
+ mode: 'development' | 'production';
9
+ }
10
+
11
+ /** Create shared Vite config for Vylos projects */
12
+ export function createViteConfig(options: VylosViteOptions): UserConfig {
13
+ const { projectRoot, mode } = options;
14
+
15
+ return defineConfig({
16
+ root: projectRoot,
17
+ mode,
18
+ plugins: [
19
+ vue(),
20
+ tailwindcss(),
21
+ ],
22
+ resolve: {
23
+ alias: {
24
+ '@project': projectRoot,
25
+ '@vylos': resolve(projectRoot, 'node_modules/@vylos/core/src'),
26
+ },
27
+ },
28
+ server: {
29
+ port: 5173,
30
+ open: true,
31
+ },
32
+ build: {
33
+ outDir: resolve(projectRoot, 'dist'),
34
+ emptyOutDir: true,
35
+ },
36
+ });
37
+ }
@@ -0,0 +1,32 @@
1
+ import type { Plugin } from 'vite';
2
+
3
+ /**
4
+ * REST API stubs for a future visual editor.
5
+ * Currently only sets up the middleware — no UI.
6
+ */
7
+ export function vylosEditorApiPlugin(): Plugin {
8
+ return {
9
+ name: 'vylos-editor-api',
10
+ apply: 'serve', // dev only
11
+
12
+ configureServer(server) {
13
+ // GET /api/state — current engine state (placeholder)
14
+ server.middlewares.use('/api/state', (_req, res) => {
15
+ res.setHeader('Content-Type', 'application/json');
16
+ res.end(JSON.stringify({ status: 'editor-api-stub', message: 'Not implemented yet' }));
17
+ });
18
+
19
+ // GET /api/files — list project files (placeholder)
20
+ server.middlewares.use('/api/files', (_req, res) => {
21
+ res.setHeader('Content-Type', 'application/json');
22
+ res.end(JSON.stringify({ status: 'editor-api-stub', files: [] }));
23
+ });
24
+
25
+ // GET /api/assets — list project assets (placeholder)
26
+ server.middlewares.use('/api/assets', (_req, res) => {
27
+ res.setHeader('Content-Type', 'application/json');
28
+ res.end(JSON.stringify({ status: 'editor-api-stub', assets: [] }));
29
+ });
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,126 @@
1
+ import type { Plugin } from 'vite';
2
+ import { resolve, relative, dirname, basename } from 'path';
3
+ import { existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs';
4
+
5
+ const VIRTUAL_PREFIX = 'vylos:texts/';
6
+ const RESOLVED_PREFIX = '\0vylos:texts/';
7
+
8
+ /**
9
+ * Vite plugin that merges per-language text files into virtual modules.
10
+ *
11
+ * Given: locations/cafe/texts/talk_barista/en.ts + fr.ts
12
+ * Produces virtual module: vylos:texts/cafe/talk_barista
13
+ * Which exports: { greeting: { en: "Hello!", fr: "Salut!" }, ... }
14
+ *
15
+ * Also generates .d.ts files for IntelliSense.
16
+ */
17
+ export function vylosI18nPlugin(projectRoot: string): Plugin {
18
+ const textModules = new Map<string, string[]>(); // moduleId → list of lang file paths
19
+
20
+ function scanTextDirs() {
21
+ textModules.clear();
22
+ const dirs = [
23
+ { base: resolve(projectRoot, 'locations'), prefix: '' },
24
+ { base: resolve(projectRoot, 'global', 'texts'), prefix: 'global/' },
25
+ ];
26
+
27
+ for (const { base, prefix } of dirs) {
28
+ if (!existsSync(base)) continue;
29
+
30
+ if (prefix === 'global/') {
31
+ // global/texts/<scope>/<lang>.ts
32
+ for (const scope of safeDirRead(base)) {
33
+ const scopeDir = resolve(base, scope);
34
+ const langs = safeDirRead(scopeDir).filter(f => f.endsWith('.ts'));
35
+ if (langs.length > 0) {
36
+ textModules.set(`global/${scope}`, langs.map(l => resolve(scopeDir, l)));
37
+ }
38
+ }
39
+ } else {
40
+ // locations/<loc>/texts/<scope>/<lang>.ts
41
+ for (const loc of safeDirRead(base)) {
42
+ const textsDir = resolve(base, loc, 'texts');
43
+ if (!existsSync(textsDir)) continue;
44
+ for (const scope of safeDirRead(textsDir)) {
45
+ const scopeDir = resolve(textsDir, scope);
46
+ const langs = safeDirRead(scopeDir).filter(f => f.endsWith('.ts'));
47
+ if (langs.length > 0) {
48
+ textModules.set(`${loc}/${scope}`, langs.map(l => resolve(scopeDir, l)));
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ return {
57
+ name: 'vylos-i18n',
58
+
59
+ buildStart() {
60
+ scanTextDirs();
61
+ },
62
+
63
+ resolveId(id) {
64
+ if (id.startsWith(VIRTUAL_PREFIX)) {
65
+ return RESOLVED_PREFIX + id.slice(VIRTUAL_PREFIX.length);
66
+ }
67
+ },
68
+
69
+ load(id) {
70
+ if (!id.startsWith(RESOLVED_PREFIX)) return;
71
+
72
+ const moduleId = id.slice(RESOLVED_PREFIX.length);
73
+ const langFiles = textModules.get(moduleId);
74
+
75
+ if (!langFiles) {
76
+ return `export default {};`;
77
+ }
78
+
79
+ // Generate code that imports all lang files and merges them
80
+ const imports: string[] = [];
81
+ const langs: string[] = [];
82
+
83
+ for (const filePath of langFiles) {
84
+ const lang = basename(filePath, '.ts');
85
+ const varName = `_${lang}`;
86
+ imports.push(`import ${varName} from '${filePath.replace(/\\/g, '/')}';`);
87
+ langs.push(lang);
88
+ }
89
+
90
+ const mergeCode = `
91
+ ${imports.join('\n')}
92
+
93
+ const _texts = {};
94
+ const _langs = { ${langs.map(l => `${l}: _${l}`).join(', ')} };
95
+
96
+ for (const [lang, entries] of Object.entries(_langs)) {
97
+ for (const [key, value] of Object.entries(entries)) {
98
+ if (!_texts[key]) _texts[key] = {};
99
+ _texts[key][lang] = value;
100
+ }
101
+ }
102
+
103
+ export default _texts;
104
+ `;
105
+ return mergeCode;
106
+ },
107
+
108
+ handleHotUpdate({ file }) {
109
+ // If a text file changed, trigger rescan
110
+ const rel = relative(projectRoot, file).replace(/\\/g, '/');
111
+ if (rel.includes('/texts/') && file.endsWith('.ts')) {
112
+ scanTextDirs();
113
+ }
114
+ },
115
+ };
116
+ }
117
+
118
+ function safeDirRead(dir: string): string[] {
119
+ try {
120
+ return readdirSync(dir, { withFileTypes: true })
121
+ .filter(d => d.isDirectory() || d.isFile())
122
+ .map(d => d.name);
123
+ } catch {
124
+ return [];
125
+ }
126
+ }
@@ -0,0 +1,67 @@
1
+ import type { Plugin } from 'vite';
2
+ import { resolve } from 'path';
3
+ import { existsSync } from 'fs';
4
+
5
+ /**
6
+ * Vite plugin that resolves virtual module `vylos:project` to the project's config.
7
+ * Also serves project assets.
8
+ */
9
+ export function vylosProjectPlugin(projectRoot: string): Plugin {
10
+ const virtualModuleId = 'vylos:project';
11
+ const resolvedVirtualModuleId = '\0' + virtualModuleId;
12
+
13
+ return {
14
+ name: 'vylos-project',
15
+
16
+ resolveId(id) {
17
+ if (id === virtualModuleId) {
18
+ return resolvedVirtualModuleId;
19
+ }
20
+ },
21
+
22
+ load(id) {
23
+ if (id === resolvedVirtualModuleId) {
24
+ const configPath = resolve(projectRoot, 'vylos.config.ts');
25
+ const setupPath = resolve(projectRoot, 'setup.ts');
26
+
27
+ let code = `export { default as config } from '${configPath.replace(/\\/g, '/')}';\n`;
28
+
29
+ if (existsSync(setupPath)) {
30
+ code += `export { default as plugin } from '${setupPath.replace(/\\/g, '/')}';\n`;
31
+ } else {
32
+ code += `export const plugin = undefined;\n`;
33
+ }
34
+
35
+ return code;
36
+ }
37
+ },
38
+
39
+ configureServer(server) {
40
+ // Serve location assets
41
+ const locationsDir = resolve(projectRoot, 'locations');
42
+ const globalDir = resolve(projectRoot, 'global');
43
+
44
+ server.middlewares.use((req, _res, next) => {
45
+ if (!req.url) return next();
46
+
47
+ // Rewrite /locations/... to project locations dir
48
+ if (req.url.startsWith('/locations/')) {
49
+ const assetPath = resolve(locationsDir, req.url.slice('/locations/'.length));
50
+ if (existsSync(assetPath)) {
51
+ req.url = '/@fs/' + assetPath.replace(/\\/g, '/');
52
+ }
53
+ }
54
+
55
+ // Rewrite /global/... to project global dir
56
+ if (req.url.startsWith('/global/')) {
57
+ const assetPath = resolve(globalDir, req.url.slice('/global/'.length));
58
+ if (existsSync(assetPath)) {
59
+ req.url = '/@fs/' + assetPath.replace(/\\/g, '/');
60
+ }
61
+ }
62
+
63
+ next();
64
+ });
65
+ },
66
+ };
67
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": ["node_modules", "dist"]
10
+ }