decantr 0.1.0 → 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.
Potentially problematic release.
This version of decantr might be problematic. Click here for more details.
- package/cli/art.js +59 -0
- package/cli/commands/init.js +97 -207
- package/cli/index.js +7 -4
- package/cli/templates/dashboard.js +300 -0
- package/cli/templates/demo.js +336 -0
- package/cli/templates/landing.js +219 -0
- package/cli/templates/shared.js +85 -0
- package/package.json +14 -2
- package/src/components/_base.js +71 -0
- package/src/components/badge.js +60 -0
- package/src/components/button.js +67 -0
- package/src/components/card.js +73 -0
- package/src/components/icon.js +46 -0
- package/src/components/index.js +6 -0
- package/src/components/input.js +91 -0
- package/src/components/modal.js +85 -0
- package/src/css/index.js +2 -0
- package/src/css/styles/brutalist.js +57 -0
- package/src/css/styles/clay.js +57 -0
- package/src/css/styles/flat.js +58 -0
- package/src/css/styles/glass.js +58 -0
- package/src/css/styles/mono.js +58 -0
- package/src/css/styles/sketchy.js +58 -0
- package/src/css/styles/skeuo.js +58 -0
- package/src/css/styles.js +77 -0
- package/src/css/themes.js +113 -0
- package/src/test/dom.js +5 -0
- package/tools/builder.js +14 -2
- package/tools/dev-server.js +7 -0
package/cli/art.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const WINE = '\x1b[35m';
|
|
2
|
+
const DIM = '\x1b[2m';
|
|
3
|
+
const BOLD = '\x1b[1m';
|
|
4
|
+
const RESET = '\x1b[0m';
|
|
5
|
+
const CYAN = '\x1b[36m';
|
|
6
|
+
|
|
7
|
+
const DECANTER = `
|
|
8
|
+
${WINE} ╭─────╮
|
|
9
|
+
│ │
|
|
10
|
+
╰──┬──╯
|
|
11
|
+
│
|
|
12
|
+
╭──┴──╮
|
|
13
|
+
╱ ╲
|
|
14
|
+
╱ ╲
|
|
15
|
+
│ ${RESET}${BOLD}decantr${RESET}${WINE} │
|
|
16
|
+
│ │
|
|
17
|
+
╲ ╱
|
|
18
|
+
╰───────╯${RESET}`;
|
|
19
|
+
|
|
20
|
+
const messages = [
|
|
21
|
+
'Letting the code breathe...',
|
|
22
|
+
'Decanting your project...',
|
|
23
|
+
'Swirling the dependencies...',
|
|
24
|
+
'Pouring the components...',
|
|
25
|
+
'A fine vintage of JavaScript...',
|
|
26
|
+
'Uncorking fresh signals...',
|
|
27
|
+
'Aerating the DOM...',
|
|
28
|
+
'Notes of CSS on the palate...',
|
|
29
|
+
'Bottle-aged to perfection...',
|
|
30
|
+
'Full-bodied, zero dependencies...'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export function art() {
|
|
34
|
+
return DECANTER;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function tagline() {
|
|
38
|
+
return messages[Math.floor(Math.random() * messages.length)];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function welcome(version) {
|
|
42
|
+
return `${art()}
|
|
43
|
+
|
|
44
|
+
${BOLD}decantr${RESET} ${DIM}v${version}${RESET} ${DIM}— AI-first web framework${RESET}
|
|
45
|
+
${CYAN}${tagline()}${RESET}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function success(msg) {
|
|
50
|
+
return `\x1b[32m✓${RESET} ${msg}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function info(msg) {
|
|
54
|
+
return `${CYAN}→${RESET} ${msg}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function heading(msg) {
|
|
58
|
+
return `\n ${BOLD}${msg}${RESET}\n`;
|
|
59
|
+
}
|
package/cli/commands/init.js
CHANGED
|
@@ -2,244 +2,134 @@ import { createInterface } from 'node:readline/promises';
|
|
|
2
2
|
import { stdin, stdout } from 'node:process';
|
|
3
3
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join, basename } from 'node:path';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type: 'module',
|
|
11
|
-
scripts: {
|
|
12
|
-
dev: 'decantr dev',
|
|
13
|
-
build: 'decantr build',
|
|
14
|
-
test: 'decantr test'
|
|
15
|
-
},
|
|
16
|
-
dependencies: {
|
|
17
|
-
decantr: '^0.1.0'
|
|
18
|
-
}
|
|
19
|
-
}, null, 2),
|
|
20
|
-
|
|
21
|
-
config: (name, routerMode, port) => JSON.stringify({
|
|
22
|
-
$schema: 'https://decantr.ai/schemas/config.v1.json',
|
|
23
|
-
name,
|
|
24
|
-
router: routerMode,
|
|
25
|
-
dev: { port },
|
|
26
|
-
build: { outDir: 'dist', inline: false, sourcemap: false }
|
|
27
|
-
}, null, 2),
|
|
28
|
-
|
|
29
|
-
indexHtml: (name) => `<!DOCTYPE html>
|
|
30
|
-
<html lang="en">
|
|
31
|
-
<head>
|
|
32
|
-
<meta charset="UTF-8">
|
|
33
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
34
|
-
<title>${name}</title>
|
|
35
|
-
<style>:root{--c0:#111;--c1:#2563eb;--c2:#f8fafc;--c3:#e2e8f0;--c4:#94a3b8;--c5:#475569;--c6:#1e293b;--c7:#f59e0b;--c8:#10b981;--c9:#ef4444}*{margin:0;box-sizing:border-box}body{font-family:system-ui,-apple-system,sans-serif;color:var(--c0);background:var(--c2)}</style>
|
|
36
|
-
</head>
|
|
37
|
-
<body>
|
|
38
|
-
<div id="app"></div>
|
|
39
|
-
<script type="module" src="/src/app.js"></script>
|
|
40
|
-
</body>
|
|
41
|
-
</html>`,
|
|
42
|
-
|
|
43
|
-
appJs: (routerMode, includeCounter) => `import { h, mount } from 'decantr/core';
|
|
44
|
-
import { createRouter, link } from 'decantr/router';
|
|
45
|
-
import { Home } from './pages/home.js';
|
|
46
|
-
import { About } from './pages/about.js';
|
|
47
|
-
|
|
48
|
-
const router = createRouter({
|
|
49
|
-
mode: '${routerMode}',
|
|
50
|
-
routes: [
|
|
51
|
-
{ path: '/', component: Home },
|
|
52
|
-
{ path: '/about', component: About }
|
|
53
|
-
]
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
function App() {
|
|
57
|
-
return h('div', null,
|
|
58
|
-
h('nav', { class: 'flex gap4 p4 bg1' },
|
|
59
|
-
link({ href: '/', class: 'fg2 nounder' }, 'Home'),
|
|
60
|
-
link({ href: '/about', class: 'fg2 nounder' }, 'About')
|
|
61
|
-
),
|
|
62
|
-
router.outlet()
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
mount(document.getElementById('app'), App);
|
|
67
|
-
`,
|
|
68
|
-
|
|
69
|
-
homeJs: (includeCounter) => `import { h } from 'decantr/core';
|
|
70
|
-
${includeCounter ? "import { Counter } from '../components/counter.js';\n" : ''}
|
|
71
|
-
/** @returns {HTMLElement} */
|
|
72
|
-
export function Home() {
|
|
73
|
-
return h('main', { class: 'p8' },
|
|
74
|
-
h('h1', { class: 't32 bold' }, 'Welcome to Decantr'),
|
|
75
|
-
h('p', { class: 'py4 fg5' }, 'AI-first web framework. Zero dependencies.')${includeCounter ? ',\n h(\'div\', { class: \'py4\' }, Counter({ initial: 0 }))' : ''}
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
`,
|
|
79
|
-
|
|
80
|
-
aboutJs: () => `import { h } from 'decantr/core';
|
|
81
|
-
|
|
82
|
-
/** @returns {HTMLElement} */
|
|
83
|
-
export function About() {
|
|
84
|
-
return h('main', { class: 'p8' },
|
|
85
|
-
h('h1', { class: 't32 bold' }, 'About'),
|
|
86
|
-
h('p', { class: 'py4 fg5' }, 'Built with Decantr — the AI-first framework.')
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
`,
|
|
90
|
-
|
|
91
|
-
counterJs: () => `import { h, text } from 'decantr/core';
|
|
92
|
-
import { createSignal } from 'decantr/state';
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* @param {{ initial?: number }} props
|
|
96
|
-
* @returns {HTMLElement}
|
|
97
|
-
*/
|
|
98
|
-
export function Counter({ initial = 0 } = {}) {
|
|
99
|
-
const [count, setCount] = createSignal(initial);
|
|
100
|
-
|
|
101
|
-
return h('div', { class: 'flex gap2 p4 aic' },
|
|
102
|
-
h('button', { onclick: () => setCount(c => c - 1), class: 'p2 px4 r2 bg1 fg2 pointer b0 t16' }, '-'),
|
|
103
|
-
h('span', { class: 'p2 t20 bold' }, text(() => String(count()))),
|
|
104
|
-
h('button', { onclick: () => setCount(c => c + 1), class: 'p2 px4 r2 bg1 fg2 pointer b0 t16' }, '+')
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
`,
|
|
108
|
-
|
|
109
|
-
counterTestJs: () => `import { describe, it, assert, render, fire, flush } from 'decantr/test';
|
|
110
|
-
import { Counter } from '../src/components/counter.js';
|
|
111
|
-
|
|
112
|
-
describe('Counter', () => {
|
|
113
|
-
it('renders with initial value', () => {
|
|
114
|
-
const { container } = render(() => Counter({ initial: 5 }));
|
|
115
|
-
assert.ok(container.textContent.includes('5'));
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('increments on + click', async () => {
|
|
119
|
-
const { container, getByText } = render(() => Counter({ initial: 0 }));
|
|
120
|
-
fire(getByText('+'), 'click');
|
|
121
|
-
await flush();
|
|
122
|
-
assert.ok(container.textContent.includes('1'));
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('decrements on - click', async () => {
|
|
126
|
-
const { container, getByText } = render(() => Counter({ initial: 5 }));
|
|
127
|
-
fire(getByText('-'), 'click');
|
|
128
|
-
await flush();
|
|
129
|
-
assert.ok(container.textContent.includes('4'));
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
`,
|
|
133
|
-
|
|
134
|
-
manifest: (name, routerMode) => JSON.stringify({
|
|
135
|
-
$schema: 'https://decantr.ai/schemas/manifest.v1.json',
|
|
136
|
-
version: '0.1.0',
|
|
137
|
-
name,
|
|
138
|
-
router: routerMode,
|
|
139
|
-
entrypoint: 'src/app.js',
|
|
140
|
-
shell: 'public/index.html',
|
|
141
|
-
mountTarget: '#app',
|
|
142
|
-
components: '.decantr/components.json',
|
|
143
|
-
routes: '.decantr/routes.json',
|
|
144
|
-
state: '.decantr/state.json'
|
|
145
|
-
}, null, 2),
|
|
146
|
-
|
|
147
|
-
components: (includeCounter) => JSON.stringify({
|
|
148
|
-
$schema: 'https://decantr.ai/schemas/components.v1.json',
|
|
149
|
-
components: includeCounter ? [{
|
|
150
|
-
id: 'counter',
|
|
151
|
-
file: 'src/components/counter.js',
|
|
152
|
-
exportName: 'Counter',
|
|
153
|
-
description: 'Increment/decrement counter with display',
|
|
154
|
-
props: [{ name: 'initial', type: 'number', default: 0, required: false }],
|
|
155
|
-
signals: ['count'],
|
|
156
|
-
effects: [],
|
|
157
|
-
children: false
|
|
158
|
-
}] : []
|
|
159
|
-
}, null, 2),
|
|
160
|
-
|
|
161
|
-
routes: (routerMode) => JSON.stringify({
|
|
162
|
-
$schema: 'https://decantr.ai/schemas/routes.v1.json',
|
|
163
|
-
mode: routerMode,
|
|
164
|
-
routes: [
|
|
165
|
-
{ path: '/', component: 'home', file: 'src/pages/home.js', exportName: 'Home', title: 'Home' },
|
|
166
|
-
{ path: '/about', component: 'about', file: 'src/pages/about.js', exportName: 'About', title: 'About' }
|
|
167
|
-
]
|
|
168
|
-
}, null, 2),
|
|
169
|
-
|
|
170
|
-
state: (includeCounter) => JSON.stringify({
|
|
171
|
-
$schema: 'https://decantr.ai/schemas/state.v1.json',
|
|
172
|
-
signals: includeCounter ? [{
|
|
173
|
-
id: 'count',
|
|
174
|
-
file: 'src/components/counter.js',
|
|
175
|
-
type: 'number',
|
|
176
|
-
initial: 0,
|
|
177
|
-
usedBy: ['Counter']
|
|
178
|
-
}] : [],
|
|
179
|
-
effects: [],
|
|
180
|
-
memos: []
|
|
181
|
-
}, null, 2)
|
|
182
|
-
};
|
|
5
|
+
import { welcome, success, info, heading } from '../art.js';
|
|
6
|
+
import { packageJson, configJson, indexHtml, manifest } from '../templates/shared.js';
|
|
7
|
+
import { dashboardFiles } from '../templates/dashboard.js';
|
|
8
|
+
import { landingFiles } from '../templates/landing.js';
|
|
9
|
+
import { demoFiles } from '../templates/demo.js';
|
|
183
10
|
|
|
184
11
|
async function ask(rl, question, defaultVal) {
|
|
185
|
-
const answer = await rl.question(
|
|
12
|
+
const answer = await rl.question(` ${question} (${defaultVal}): `);
|
|
186
13
|
return answer.trim() || defaultVal;
|
|
187
14
|
}
|
|
188
15
|
|
|
189
|
-
async function askChoice(rl, question, options,
|
|
190
|
-
console.log(
|
|
191
|
-
options.forEach((opt, i) =>
|
|
192
|
-
|
|
16
|
+
async function askChoice(rl, question, options, defaultIdx = 0) {
|
|
17
|
+
console.log(heading(question));
|
|
18
|
+
options.forEach((opt, i) => {
|
|
19
|
+
const marker = i === defaultIdx ? '\x1b[36m>' : ' ';
|
|
20
|
+
console.log(` ${marker} ${i + 1}) ${opt.label}${opt.desc ? ` \x1b[2m— ${opt.desc}\x1b[0m` : ''}\x1b[0m`);
|
|
21
|
+
});
|
|
22
|
+
const answer = await rl.question(` Choose [${defaultIdx + 1}]: `);
|
|
193
23
|
const idx = parseInt(answer.trim()) - 1;
|
|
194
|
-
return (idx >= 0 && idx < options.length) ? options[idx] :
|
|
24
|
+
return (idx >= 0 && idx < options.length) ? options[idx].value : options[defaultIdx].value;
|
|
195
25
|
}
|
|
196
26
|
|
|
197
27
|
export async function run() {
|
|
198
28
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
199
29
|
const cwd = process.cwd();
|
|
200
30
|
|
|
201
|
-
console.log('
|
|
31
|
+
console.log(welcome('0.2.0'));
|
|
202
32
|
|
|
203
33
|
try {
|
|
34
|
+
// 1. Project name
|
|
204
35
|
const name = await ask(rl, 'Project name?', basename(cwd));
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
36
|
+
|
|
37
|
+
// 2. Project type (or demo mode)
|
|
38
|
+
const projectType = await askChoice(rl, 'Project type?', [
|
|
39
|
+
{ label: 'Dashboard', desc: 'Sidebar, header, data tables', value: 'dashboard' },
|
|
40
|
+
{ label: 'Landing Page', desc: 'Hero, features, pricing', value: 'landing' },
|
|
41
|
+
{ label: "Don't ask me, just show me!", desc: 'Demo showcase of everything', value: 'demo' }
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// 3. Color theme
|
|
45
|
+
const theme = await askChoice(rl, 'Color theme?', [
|
|
46
|
+
{ label: 'Light', value: 'light' },
|
|
47
|
+
{ label: 'Dark', value: 'dark' },
|
|
48
|
+
{ label: 'AI', desc: 'Deep purples & cyans', value: 'ai' },
|
|
49
|
+
{ label: 'Nature', desc: 'Earthy greens', value: 'nature' },
|
|
50
|
+
{ label: 'Pastel', desc: 'Soft pinks', value: 'pastel' },
|
|
51
|
+
{ label: 'Spice', desc: 'Warm oranges', value: 'spice' },
|
|
52
|
+
{ label: 'Monochromatic', desc: 'Pure grayscale', value: 'mono' }
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// 4. Design style
|
|
56
|
+
const style = await askChoice(rl, 'Design style?', [
|
|
57
|
+
{ label: 'Glassmorphism', desc: 'Frosted glass, blur', value: 'glass' },
|
|
58
|
+
{ label: 'Claymorphism', desc: 'Soft, puffy, rounded', value: 'clay' },
|
|
59
|
+
{ label: 'Minimal', desc: 'Clean lines, no effects', value: 'flat' },
|
|
60
|
+
{ label: 'Neobrutalism', desc: 'Bold borders, offset shadows', value: 'brutalist' },
|
|
61
|
+
{ label: 'Skeuomorphic', desc: 'Gradients, 3D depth', value: 'skeuo' },
|
|
62
|
+
{ label: 'Monochromatic', desc: 'Black & white elegance', value: 'mono' },
|
|
63
|
+
{ label: 'Hand-drawn', desc: 'Wobbly borders, sketchy', value: 'sketchy' }
|
|
64
|
+
], 2);
|
|
65
|
+
|
|
66
|
+
// 5. Router mode
|
|
67
|
+
const router = await askChoice(rl, 'Router mode?', [
|
|
68
|
+
{ label: 'History', desc: 'Clean URLs (needs server)', value: 'history' },
|
|
69
|
+
{ label: 'Hash', desc: 'Works everywhere', value: 'hash' }
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// 6. Icons
|
|
73
|
+
const iconsChoice = await askChoice(rl, 'Icon library?', [
|
|
74
|
+
{ label: 'None', desc: 'Skip for now', value: 'none' },
|
|
75
|
+
{ label: 'Material Icons', desc: 'Google Material Design', value: 'material' },
|
|
76
|
+
{ label: 'Lucide', desc: 'Beautiful open-source icons', value: 'lucide' }
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
let icons = null;
|
|
80
|
+
let iconDelivery = null;
|
|
81
|
+
if (iconsChoice !== 'none') {
|
|
82
|
+
icons = iconsChoice;
|
|
83
|
+
iconDelivery = await askChoice(rl, 'Icon delivery?', [
|
|
84
|
+
{ label: 'CDN', desc: 'Link tag, no install', value: 'cdn' },
|
|
85
|
+
{ label: 'npm', desc: 'Install as dependency', value: 'npm' }
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 7. Port
|
|
208
90
|
const port = parseInt(await ask(rl, 'Dev server port?', '3000'));
|
|
209
91
|
|
|
92
|
+
const opts = { name, projectType, theme, style, router, icons, iconDelivery, port };
|
|
93
|
+
|
|
94
|
+
console.log(heading('Decanting your project...'));
|
|
95
|
+
|
|
210
96
|
// Create directories
|
|
211
|
-
const dirs = ['
|
|
97
|
+
const dirs = ['public', '.decantr', 'test', 'src/pages', 'src/components'];
|
|
98
|
+
if (projectType === 'landing') dirs.push('src/sections');
|
|
212
99
|
for (const dir of dirs) {
|
|
213
100
|
await mkdir(join(cwd, dir), { recursive: true });
|
|
214
101
|
}
|
|
215
102
|
|
|
216
|
-
//
|
|
103
|
+
// Shared files
|
|
217
104
|
const files = [
|
|
218
|
-
['package.json',
|
|
219
|
-
['decantr.config.json',
|
|
220
|
-
['public/index.html',
|
|
221
|
-
['
|
|
222
|
-
['src/pages/home.js', templates.homeJs(includeCounter)],
|
|
223
|
-
['src/pages/about.js', templates.aboutJs()],
|
|
224
|
-
['.decantr/manifest.json', templates.manifest(name, routerMode)],
|
|
225
|
-
['.decantr/components.json', templates.components(includeCounter)],
|
|
226
|
-
['.decantr/routes.json', templates.routes(routerMode)],
|
|
227
|
-
['.decantr/state.json', templates.state(includeCounter)]
|
|
105
|
+
['package.json', packageJson(name)],
|
|
106
|
+
['decantr.config.json', configJson(opts)],
|
|
107
|
+
['public/index.html', indexHtml(opts)],
|
|
108
|
+
['.decantr/manifest.json', manifest(opts)]
|
|
228
109
|
];
|
|
229
110
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
111
|
+
// Project-type files
|
|
112
|
+
let typeFiles;
|
|
113
|
+
if (projectType === 'dashboard') {
|
|
114
|
+
typeFiles = dashboardFiles(opts);
|
|
115
|
+
} else if (projectType === 'landing') {
|
|
116
|
+
typeFiles = landingFiles(opts);
|
|
117
|
+
} else {
|
|
118
|
+
typeFiles = demoFiles(opts);
|
|
233
119
|
}
|
|
120
|
+
files.push(...typeFiles);
|
|
234
121
|
|
|
122
|
+
// Write all files
|
|
235
123
|
for (const [path, content] of files) {
|
|
236
124
|
await writeFile(join(cwd, path), content + '\n');
|
|
125
|
+
console.log(' ' + success(path));
|
|
237
126
|
}
|
|
238
127
|
|
|
239
|
-
console.log(
|
|
240
|
-
console.log('
|
|
241
|
-
console.log(
|
|
242
|
-
console.log(
|
|
128
|
+
console.log(heading('Your project is ready!'));
|
|
129
|
+
console.log(info('Next steps:'));
|
|
130
|
+
console.log(` npm install`);
|
|
131
|
+
console.log(` npx decantr dev`);
|
|
132
|
+
console.log('');
|
|
243
133
|
|
|
244
134
|
} finally {
|
|
245
135
|
rl.close();
|
package/cli/index.js
CHANGED
|
@@ -18,21 +18,24 @@ switch (command) {
|
|
|
18
18
|
case 'test':
|
|
19
19
|
await import('./commands/test.js').then(m => m.run());
|
|
20
20
|
break;
|
|
21
|
-
default:
|
|
21
|
+
default: {
|
|
22
|
+
const { art } = await import('./art.js');
|
|
23
|
+
console.log(art());
|
|
22
24
|
console.log(`
|
|
23
|
-
|
|
25
|
+
\x1b[1mdecantr\x1b[0m v0.2.0 \x1b[2m— AI-first web framework\x1b[0m
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
\x1b[1mCommands:\x1b[0m
|
|
26
28
|
init Create a new decantr project
|
|
27
29
|
dev Start development server
|
|
28
30
|
build Build for production
|
|
29
31
|
test Run tests
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
\x1b[1mUsage:\x1b[0m
|
|
32
34
|
npx decantr init
|
|
33
35
|
npx decantr dev
|
|
34
36
|
npx decantr build
|
|
35
37
|
npx decantr test [--watch]
|
|
36
38
|
`);
|
|
37
39
|
break;
|
|
40
|
+
}
|
|
38
41
|
}
|