create-muten 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present, Muten
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # create-muten
2
+
3
+ Scaffold a new [Muten](https://www.npmjs.com/package/muten) app.
4
+
5
+ ```sh
6
+ npm create muten@latest # or: npx create-muten
7
+ ```
8
+
9
+ It prompts for the **project name**, the **stylesheet** (CSS/SCSS) and the **package manager**
10
+ (default = the one you invoked it with), then scaffolds and — unless you decline — runs
11
+ `<pm> install` + `<pm> run dev`. CI flags: `--css|--scss`, `--pm <npm|pnpm|yarn|bun>`, `--no-install`.
12
+
13
+ The engine (`@muten/core`) is a separate package the app installs as a dependency — the same split as
14
+ `create-vue` ↔ `vue`. It's a Node CLI (not a shell script), so it behaves the same on Windows and macOS.
package/index.js ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // create-muten — scaffold a new Muten app. Zero runtime deps (Node built-ins only).
3
+ //
4
+ // npm create muten@latest [name] (when published to npm)
5
+ // npx github:karttofer/create-muten [name] (from GitHub, not yet public)
6
+ // create-muten [name] [--css|--scss] [--pm npm|pnpm|yarn|bun] [--no-install]
7
+ //
8
+ // Interactive in a TTY (prompts for name, stylesheet, package manager); flags / non-TTY make it
9
+ // scriptable. The package manager defaults to the one that invoked us. Then it scaffolds and,
10
+ // unless --no-install, runs `<pm> install` + `<pm> run dev`.
11
+ import { cpSync, existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
12
+ import { join, dirname, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { createInterface } from 'node:readline/promises';
15
+ import { spawnSync } from 'node:child_process';
16
+
17
+ const SELF = dirname(fileURLToPath(import.meta.url));
18
+ const TEMPLATE = join(SELF, 'template');
19
+ const PKG = JSON.parse(readFileSync(join(SELF, 'package.json'), 'utf8'));
20
+ const PMS = ['npm', 'pnpm', 'yarn', 'bun'];
21
+
22
+ const BANNER = [
23
+ '',
24
+ ' _',
25
+ ' _ __ ___ _ _| |_ ___ _ __',
26
+ " | '_ ` _ \\| | | | __/ _ \\ '_ \\",
27
+ ' | | | | | | |_| | || __/ | | |',
28
+ ' |_| |_| |_|\\__,_|\\__\\___|_| |_|',
29
+ '',
30
+ ' AI-first frontend framework',
31
+ '',
32
+ ].join('\n');
33
+
34
+ // Which PM launched us? npm/pnpm/yarn/bun all set npm_config_user_agent (e.g. "pnpm/8.6 ...").
35
+ // Detecting it is what create-vue/vite do — the idiomatic default, no guessing.
36
+ const detectPM = () => {
37
+ const ua = process.env.npm_config_user_agent || '';
38
+ return PMS.find((p) => ua.startsWith(p + '/')) || 'npm';
39
+ };
40
+
41
+ // Safe folder name: starts alphanumeric, then [A-Za-z0-9._-]. Blocks path traversal / odd input.
42
+ const validName = (n) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(n);
43
+ const die = (msg) => { console.error(msg); process.exit(1); };
44
+
45
+ async function main() {
46
+ // args: one positional name + optional flags (so the CLI is also scriptable / CI-friendly)
47
+ const argv = process.argv.slice(2);
48
+ const has = (f) => argv.includes(f);
49
+ const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
50
+ if (has('-v') || has('--version')) { console.log(PKG.version); return; }
51
+ console.log(BANNER);
52
+ if (has('-h') || has('--help')) { console.log(' Usage: create-muten [name] [--css|--scss] [--pm npm|pnpm|yarn|bun] [--no-install]\n'); return; }
53
+
54
+ let name = argv.filter((a, i) => !a.startsWith('-') && argv[i - 1] !== '--pm')[0];
55
+ let style = has('--scss') ? 'scss' : has('--css') ? 'css' : undefined;
56
+ let pm = val('--pm');
57
+ let install = has('--no-install') ? false : undefined;
58
+
59
+ if (name && !validName(name)) die(`Invalid name: "${name}" (letters, digits, . _ -)`);
60
+ if (pm && !PMS.includes(pm)) die(`Unknown package manager: "${pm}" (${PMS.join(', ')})`);
61
+
62
+ const dpm = detectPM();
63
+
64
+ // Prompt only with a real TTY — piped/CI input drops lines through readline, so there we use
65
+ // flags + defaults instead of hanging on a prompt.
66
+ if (process.stdin.isTTY) {
67
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
68
+ const ask = async (q, def) => (await rl.question(q)).trim() || def;
69
+ const pick = async (q, opts, def) => {
70
+ for (;;) {
71
+ const v = (await ask(q, def)).toLowerCase();
72
+ if (opts.includes(v)) return v;
73
+ console.log(` Choose: ${opts.join(', ')}.`);
74
+ }
75
+ };
76
+ while (!name) {
77
+ const a = await ask('Project name: (muten-app) ', 'muten-app');
78
+ if (validName(a)) name = a; else console.log(' Letters, digits, . _ - (start alphanumeric).');
79
+ }
80
+ if (!style) style = await pick('Stylesheet? [css/scss] (css) ', ['css', 'scss'], 'css');
81
+ if (!pm) pm = await pick(`Package manager? [${PMS.join('/')}] (${dpm}) `, PMS, dpm);
82
+ if (install === undefined) install = (await ask('Install deps and start the dev server now? [Y/n] ', 'y')).toLowerCase() !== 'n';
83
+ rl.close();
84
+ }
85
+ name = name || 'muten-app';
86
+ style = style || 'css';
87
+ pm = pm || dpm;
88
+ if (install === undefined) install = false;
89
+
90
+ const target = resolve(name);
91
+ if (existsSync(target)) die(`"${name}" already exists.`);
92
+
93
+ // scaffold from ./template
94
+ cpSync(TEMPLATE, target, { recursive: true });
95
+ const ignore = join(target, '_gitignore'); // npm strips a real .gitignore on publish
96
+ if (existsSync(ignore)) renameSync(ignore, join(target, '.gitignore'));
97
+ if (style === 'scss') renameSync(join(target, 'src', 'styles.css'), join(target, 'src', 'styles.scss')); // plugin auto-detects
98
+
99
+ const pkgPath = join(target, 'package.json');
100
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
101
+ pkg.name = name;
102
+ if (style === 'scss') pkg.devDependencies = { ...(pkg.devDependencies || {}), sass: '^1.101.0' };
103
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
104
+
105
+ console.log(`\n Created ${name} (${style}, ${pm})\n`);
106
+
107
+ if (!install) { console.log(` cd ${name}\n ${pm} install\n ${pm} run dev\n`); return; }
108
+
109
+ // PMs are .cmd shims on Windows → spawn needs shell:true to find them.
110
+ const run = (a) => spawnSync(pm, a, { cwd: target, stdio: 'inherit', shell: process.platform === 'win32' });
111
+ if (run(['install']).status === 0) run(['run', 'dev']);
112
+ }
113
+
114
+ main();
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "create-muten",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a new Muten app.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/karttofer/create-muten.git"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "bin": {
15
+ "create-muten": "index.js"
16
+ },
17
+ "keywords": ["muten", "create-muten", "scaffold", "starter", "cli", "frontend"],
18
+ "files": ["index.js", "template"]
19
+ }
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ dist/
@@ -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">
6
+ <title>Muten app</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/app.muten"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "muten-app",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "lint": "muten lint"
9
+ },
10
+ "dependencies": {
11
+ "@muten/core": "^0.0.1",
12
+ "vite": "^8.0.16"
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ # The app ROOT — the single source of truth the AI reads first.
2
+ # `routes` maps a URL to a page (the folder under src/pages/). Add a persistent navbar/footer by
3
+ # wrapping a `shell { … slot … }` around it; a `slot`-only shell (the default) just mounts the page.
4
+ routes {
5
+ / -> home
6
+ }
@@ -0,0 +1,9 @@
1
+ # A page = one .muten file; its folder name (home) is the route target from app.muten.
2
+ # `Page` is the <main> landmark; lay it out with style() tokens, skin it with class("…").
3
+ screen home
4
+
5
+ Page style(padding.xl, gap.md) {
6
+ Title "Hello, Muten"
7
+ Text "Edit src/pages/home/home.muten to build your first page."
8
+ Text "Add routes in src/app.muten; define your token scale in theme.muten."
9
+ }
@@ -0,0 +1,9 @@
1
+ /* Your look. Muten ships STRUCTURE (semantic tags) + LAYOUT (style tokens); the LOOK lives here,
2
+ referenced from .muten via class("…"). This base reset is all a fresh app needs to start. */
3
+ * { box-sizing: border-box; }
4
+ body { margin: 0; font: 15px/1.55 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; color: #111; }
5
+ h1, h2, h3, h4, h5, h6, p { margin: 0; }
6
+ h1 { font-size: 32px; font-weight: 700; letter-spacing: -.02em; }
7
+ .stack { display: flex; flex-direction: column; } /* the class Stack and column layouts emit */
8
+ img { max-width: 100%; display: block; }
9
+ a { color: inherit; text-decoration: none; }
@@ -0,0 +1,8 @@
1
+ # The project's token SCALE — the values style() resolves against (the engine ships none).
2
+ # Add/rename steps freely; the linter validates tokens against this.
3
+ theme {
4
+ space { xs "4px" sm "8px" md "16px" lg "24px" xl "32px" }
5
+ font { sm "13px" md "15px" lg "20px" xl "28px" }
6
+ weight { medium "500" bold "700" }
7
+ breakpoints { sm "640px" md "768px" lg "1024px" }
8
+ }
@@ -0,0 +1,5 @@
1
+ import muten from '@muten/core/vite-plugin-muten.js';
2
+
3
+ export default {
4
+ plugins: [muten()], // theme.muten is auto-loaded; everything else stays .muten
5
+ };