create-nevela 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +55 -3
- package/index.mjs +418 -0
- package/package.json +37 -6
- package/packagist.mjs +25 -0
- package/template/laravel/LICENSE +21 -0
- package/template/laravel/README.md +55 -0
- package/template/laravel/composer.json +59 -0
- package/template/laravel/config/nevela.php +29 -0
- package/template/laravel/src/Console/GenerateCommand.php +77 -0
- package/template/laravel/src/Console/MakeResourceCommand.php +66 -0
- package/template/laravel/src/Console/SeedCommand.php +124 -0
- package/template/laravel/src/Console/UserCommand.php +59 -0
- package/template/laravel/src/Generator/GeneratedFile.php +31 -0
- package/template/laravel/src/Generator/ResourceGenerator.php +639 -0
- package/template/laravel/src/Generator/Writer.php +143 -0
- package/template/laravel/src/Http/FlareErrors.php +92 -0
- package/template/laravel/src/Http/ResourceRequest.php +67 -0
- package/template/laravel/src/Http/TokenController.php +65 -0
- package/template/laravel/src/Nevela.php +151 -0
- package/template/laravel/src/NevelaServiceProvider.php +65 -0
- package/template/laravel/src/Support/Descriptor.php +174 -0
- package/template/laravel/src/Support/Fake.php +171 -0
- package/template/laravel/src/Support/Field.php +146 -0
- package/template/laravel/src/Support/Issue.php +9 -0
- package/template/laravel/src/Support/ListQuery.php +125 -0
- package/template/laravel/src/Support/Naming.php +79 -0
- package/template/web/.env.example +2 -0
- package/template/web/LICENSE-flare +21 -0
- package/template/web/README.md +40 -0
- package/template/web/_gitignore +10 -0
- package/template/web/app/auth-actions.ts +47 -0
- package/template/web/app/dashboard/actions.ts +127 -0
- package/template/web/app/dashboard/error.tsx +37 -0
- package/template/web/app/dashboard/import-actions.ts +89 -0
- package/template/web/app/dashboard/layout.tsx +44 -0
- package/template/web/app/dashboard/loading.tsx +18 -0
- package/template/web/app/dashboard/not-found.tsx +30 -0
- package/template/web/app/dashboard/page.tsx +101 -0
- package/template/web/app/error.tsx +55 -0
- package/template/web/app/globals.css +255 -0
- package/template/web/app/layout.tsx +37 -0
- package/template/web/app/loading.tsx +22 -0
- package/template/web/app/not-found.tsx +48 -0
- package/template/web/app/page.tsx +142 -0
- package/template/web/app/sign-in/page.tsx +21 -0
- package/template/web/components/auth/auth-shell.tsx +284 -0
- package/template/web/components/auth/sign-in-form.tsx +50 -0
- package/template/web/components/auth/ui.tsx +96 -0
- package/template/web/components/dashboard/column-menu.tsx +66 -0
- package/template/web/components/dashboard/dashboard-header.tsx +115 -0
- package/template/web/components/dashboard/dashboard-sidebar.tsx +229 -0
- package/template/web/components/dashboard/dropzone.tsx +92 -0
- package/template/web/components/dashboard/editable.ts +12 -0
- package/template/web/components/dashboard/export-button.tsx +41 -0
- package/template/web/components/dashboard/fields/country-field.tsx +152 -0
- package/template/web/components/dashboard/fields/date-field.tsx +112 -0
- package/template/web/components/dashboard/fields/field-widget.tsx +318 -0
- package/template/web/components/dashboard/fields/file-cell.tsx +68 -0
- package/template/web/components/dashboard/fields/file-field.tsx +310 -0
- package/template/web/components/dashboard/fields/multi-select-field.tsx +46 -0
- package/template/web/components/dashboard/fields/phone-field.tsx +86 -0
- package/template/web/components/dashboard/fields/relation-field.tsx +154 -0
- package/template/web/components/dashboard/import-dialog.tsx +540 -0
- package/template/web/components/dashboard/inline-cell.tsx +165 -0
- package/template/web/components/dashboard/local-time.tsx +77 -0
- package/template/web/components/dashboard/page-header.tsx +57 -0
- package/template/web/components/dashboard/query.ts +27 -0
- package/template/web/components/dashboard/record-actions.tsx +113 -0
- package/template/web/components/dashboard/record-detail.tsx +182 -0
- package/template/web/components/dashboard/resource-form-page.tsx +64 -0
- package/template/web/components/dashboard/resource-form-sheet.tsx +86 -0
- package/template/web/components/dashboard/resource-form.tsx +261 -0
- package/template/web/components/dashboard/resource-icon.tsx +89 -0
- package/template/web/components/dashboard/resource-stats.tsx +49 -0
- package/template/web/components/dashboard/resource-table-toolbar.tsx +95 -0
- package/template/web/components/dashboard/resource-table.tsx +411 -0
- package/template/web/components/dashboard/row-actions.tsx +156 -0
- package/template/web/components/dashboard/skeletons.tsx +146 -0
- package/template/web/components/dashboard/stat-card.tsx +69 -0
- package/template/web/components/dashboard/table-selection.tsx +185 -0
- package/template/web/components/dashboard/user-menu.tsx +63 -0
- package/template/web/components/marketing/product-preview.tsx +70 -0
- package/template/web/components/marketing/site-header.tsx +39 -0
- package/template/web/components/sign-out-button.tsx +18 -0
- package/template/web/components/ui/alert-dialog.tsx +196 -0
- package/template/web/components/ui/alert.tsx +65 -0
- package/template/web/components/ui/avatar.tsx +108 -0
- package/template/web/components/ui/badge.tsx +53 -0
- package/template/web/components/ui/breadcrumb.tsx +110 -0
- package/template/web/components/ui/button.tsx +65 -0
- package/template/web/components/ui/calendar.tsx +220 -0
- package/template/web/components/ui/card.tsx +92 -0
- package/template/web/components/ui/checkbox.tsx +25 -0
- package/template/web/components/ui/command.tsx +184 -0
- package/template/web/components/ui/dialog.tsx +158 -0
- package/template/web/components/ui/dropdown-menu.tsx +256 -0
- package/template/web/components/ui/empty.tsx +103 -0
- package/template/web/components/ui/field.tsx +248 -0
- package/template/web/components/ui/input-group.tsx +170 -0
- package/template/web/components/ui/input.tsx +20 -0
- package/template/web/components/ui/label.tsx +23 -0
- package/template/web/components/ui/pagination.tsx +127 -0
- package/template/web/components/ui/popover.tsx +88 -0
- package/template/web/components/ui/progress.tsx +30 -0
- package/template/web/components/ui/radio-group.tsx +29 -0
- package/template/web/components/ui/select.tsx +189 -0
- package/template/web/components/ui/separator.tsx +27 -0
- package/template/web/components/ui/sheet.tsx +142 -0
- package/template/web/components/ui/sidebar.tsx +726 -0
- package/template/web/components/ui/skeleton.tsx +13 -0
- package/template/web/components/ui/sonner.tsx +58 -0
- package/template/web/components/ui/spinner.tsx +15 -0
- package/template/web/components/ui/switch.tsx +34 -0
- package/template/web/components/ui/table.tsx +115 -0
- package/template/web/components/ui/textarea.tsx +17 -0
- package/template/web/components/ui/tooltip.tsx +56 -0
- package/template/web/components.json +21 -0
- package/template/web/hooks/use-mobile.ts +19 -0
- package/template/web/lib/cache.ts +83 -0
- package/template/web/lib/csv.ts +24 -0
- package/template/web/lib/dashboard-nav.ts +17 -0
- package/template/web/lib/dashboard.ts +130 -0
- package/template/web/lib/laravel.ts +57 -0
- package/template/web/lib/number.ts +33 -0
- package/template/web/lib/resource/index.ts +4 -0
- package/template/web/lib/resource/query.ts +149 -0
- package/template/web/lib/resource/store.ts +149 -0
- package/template/web/lib/session.ts +42 -0
- package/template/web/lib/site.ts +56 -0
- package/template/web/lib/suggest.ts +35 -0
- package/template/web/lib/theme.ts +83 -0
- package/template/web/lib/utils.ts +6 -0
- package/template/web/lib/views.ts +14 -0
- package/template/web/next.config.ts +8 -0
- package/template/web/package.json +42 -0
- package/template/web/policies/index.ts +6 -0
- package/template/web/postcss.config.mjs +7 -0
- package/template/web/proxy.ts +20 -0
- package/template/web/tsconfig.json +46 -0
- package/template.mjs +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Cole MUKISA
|
|
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
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# create-nevela
|
|
2
|
+
|
|
3
|
+
Create a new [Nevela](https://nevela-docs.vercel.app) app: a Laravel API and a Next.js dashboard, generated from one resource description.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm create nevela my-app
|
|
7
|
+
# or
|
|
8
|
+
npm create nevela@latest my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You need PHP 8.3 or newer, Composer, and Node.js 20 or newer.
|
|
12
|
+
|
|
13
|
+
## What you get
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
my-app/
|
|
17
|
+
├─ apps/api/ a Laravel app with Sanctum and Nevela installed
|
|
18
|
+
├─ apps/web/ the Next.js dashboard, pointed at the API
|
|
19
|
+
└─ scripts/ `dev` runs both together
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
cd my-app
|
|
26
|
+
pnpm run dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Open http://localhost:3000/sign-in and sign in as `admin@example.com` with the password `password`. That starter account is in your local database only; add your own with `php artisan nevela:user`.
|
|
30
|
+
|
|
31
|
+
Add your first resource from `apps/api`:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
php artisan nevela:resource Product --fields="name:string, sku:string!, price:money"
|
|
35
|
+
php artisan migrate
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Options
|
|
39
|
+
|
|
40
|
+
| Option | Meaning |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `--pm <pnpm\|npm\|yarn\|bun>` | Package manager for the dashboard. Default: the one you ran this with. |
|
|
43
|
+
| `--no-install` | Don't install the dashboard's dependencies. |
|
|
44
|
+
| `--no-user` | Don't create the starter account. |
|
|
45
|
+
| `--no-git` | Don't run `git init`. |
|
|
46
|
+
| `--bundled-package` | Use the copy of `nevela/laravel` inside this installer instead of the release on Packagist. |
|
|
47
|
+
| `-y`, `--yes` | Ask nothing. Only the app name is ever asked for. |
|
|
48
|
+
|
|
49
|
+
With npm, put options after `--`: `npm create nevela@latest my-app -- --no-install`.
|
|
50
|
+
|
|
51
|
+
## How it installs the Laravel package
|
|
52
|
+
|
|
53
|
+
`nevela/laravel` is installed from [Packagist](https://packagist.org/packages/nevela/laravel), at the release that matches this installer's version.
|
|
54
|
+
|
|
55
|
+
If Packagist has no matching release, or you pass `--bundled-package`, the copy that travels inside this installer is placed in `my-app/packages/nevela-laravel` and Composer installs it from there. That is how Nevela's own CI tests a change to the package before it is released.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Create a new Nevela app: a Laravel API and a Next.js dashboard, side by side.
|
|
3
|
+
//
|
|
4
|
+
// pnpm create nevela my-app
|
|
5
|
+
// npm create nevela@latest my-app
|
|
6
|
+
//
|
|
7
|
+
// No dependencies on purpose: this runs before anything is installed.
|
|
8
|
+
|
|
9
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import readline from 'node:readline/promises';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { onPackagist } from './packagist.mjs';
|
|
16
|
+
import { copyLaravelPackage, copyWebTemplate } from './template.mjs';
|
|
17
|
+
|
|
18
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(here, 'package.json'), 'utf8')).version;
|
|
20
|
+
const DOCS = 'https://nevela-docs.vercel.app';
|
|
21
|
+
const windows = process.platform === 'win32';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The account every new app starts with, so there is something to sign in as straight
|
|
25
|
+
* away. It is created in the app's local database, never in code or a migration, so it
|
|
26
|
+
* does not follow the app to a server.
|
|
27
|
+
*/
|
|
28
|
+
const ADMIN = { name: 'Admin', email: 'admin@example.com', password: 'password' };
|
|
29
|
+
|
|
30
|
+
const colour = (code) => (text) => (process.stdout.isTTY ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
31
|
+
const bold = colour(1);
|
|
32
|
+
const dim = colour(2);
|
|
33
|
+
const green = colour(32);
|
|
34
|
+
const red = colour(31);
|
|
35
|
+
const indigo = colour('38;5;99');
|
|
36
|
+
|
|
37
|
+
const HELP = `
|
|
38
|
+
${bold('create-nevela')} ${dim(`v${VERSION}`)}
|
|
39
|
+
|
|
40
|
+
Create a new Nevela app: a Laravel API and a Next.js dashboard.
|
|
41
|
+
|
|
42
|
+
${bold('Usage')}
|
|
43
|
+
pnpm create nevela <name> [options]
|
|
44
|
+
npm create nevela@latest <name> -- [options]
|
|
45
|
+
|
|
46
|
+
${bold('Options')}
|
|
47
|
+
--pm <pnpm|npm|yarn|bun> Package manager for the dashboard. Default: the one you ran this with.
|
|
48
|
+
--no-install Don't install the dashboard's dependencies.
|
|
49
|
+
--no-user Don't create the starter admin account.
|
|
50
|
+
--no-git Don't run git init.
|
|
51
|
+
--bundled-package Use the copy of nevela/laravel that ships with this installer
|
|
52
|
+
instead of the release on Packagist.
|
|
53
|
+
-y, --yes Ask nothing. (Only the app name is ever asked for.)
|
|
54
|
+
-h, --help Show this.
|
|
55
|
+
-v, --version Show the version.
|
|
56
|
+
|
|
57
|
+
${bold('Needs')} PHP 8.3+, Composer, Node.js 20+
|
|
58
|
+
${bold('Docs')} ${DOCS}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
function fail(message, detail) {
|
|
62
|
+
console.error(`\n ${red('✖')} ${message}`);
|
|
63
|
+
if (detail) console.error(`\n${detail.trim().split('\n').map((line) => ` ${line}`).join('\n')}`);
|
|
64
|
+
console.error('');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Run a command. Quiet unless it fails, when its output is the error message. */
|
|
69
|
+
function run(command, args, { cwd, interactive = false, allowFailure = false } = {}) {
|
|
70
|
+
const result = spawnCommand(command, args, { cwd, stdio: interactive ? 'inherit' : 'pipe', encoding: 'utf8' });
|
|
71
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
72
|
+
if (result.error || result.status !== 0) {
|
|
73
|
+
if (allowFailure) return { ok: false, output };
|
|
74
|
+
fail(`\`${command} ${args.join(' ')}\` failed.`, output || String(result.error ?? ''));
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, output };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* .bat and .cmd shims (composer, pnpm, Herd's php) only resolve through a shell on
|
|
81
|
+
* Windows. A shell takes one command string, so arguments are quoted here; none of them
|
|
82
|
+
* come from anywhere but this file and the validated app name.
|
|
83
|
+
*/
|
|
84
|
+
function spawnCommand(command, args, options) {
|
|
85
|
+
if (!windows) return spawnSync(command, args, options);
|
|
86
|
+
return spawnSync(shellLine(command, args), { ...options, shell: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// `^` is not in the safe list on purpose: unquoted, cmd.exe treats it as an escape
|
|
90
|
+
// character and drops it, which turned the constraint "^0.1" into an exact "0.1".
|
|
91
|
+
const quote = (arg) => (/^[\w./:=@,-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`);
|
|
92
|
+
const shellLine = (command, args) => [command, ...args.map(quote)].join(' ');
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Start a command and carry on. Its output goes to a file rather than a pipe: the steps
|
|
96
|
+
* that follow block this process for minutes, and a pipe nobody is reading fills up and
|
|
97
|
+
* stalls the command.
|
|
98
|
+
*/
|
|
99
|
+
function start(command, args, { cwd }) {
|
|
100
|
+
const log = path.join(os.tmpdir(), `create-nevela-${process.pid}-${command}.log`);
|
|
101
|
+
const out = fs.openSync(log, 'w');
|
|
102
|
+
const started = Date.now();
|
|
103
|
+
const child = windows
|
|
104
|
+
? spawn(shellLine(command, args), { cwd, shell: true, stdio: ['ignore', out, out] })
|
|
105
|
+
: spawn(command, args, { cwd, stdio: ['ignore', out, out] });
|
|
106
|
+
const done = new Promise((resolve) => {
|
|
107
|
+
const finish = (ok) => {
|
|
108
|
+
fs.closeSync(out);
|
|
109
|
+
const output = fs.readFileSync(log, 'utf8');
|
|
110
|
+
// This process hears about the exit only once the blocking steps let it. The log's
|
|
111
|
+
// last write is when the command really finished.
|
|
112
|
+
const ended = Math.max(started, Math.min(Date.now(), fs.statSync(log).mtimeMs));
|
|
113
|
+
fs.rmSync(log, { force: true });
|
|
114
|
+
resolve({ ok, output, seconds: (ended - started) / 1000 });
|
|
115
|
+
};
|
|
116
|
+
child.on('error', () => finish(false));
|
|
117
|
+
child.on('close', (code) => finish(code === 0));
|
|
118
|
+
});
|
|
119
|
+
return { done };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function available(command, args = ['--version']) {
|
|
123
|
+
const result = spawnCommand(command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
124
|
+
return result.status === 0 ? `${result.stdout}${result.stderr}` : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const duration = (seconds) => (seconds >= 60 ? `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s` : `${seconds.toFixed(1)}s`);
|
|
128
|
+
|
|
129
|
+
async function step(label, work) {
|
|
130
|
+
const started = Date.now();
|
|
131
|
+
// The "in progress" line is rewritten in place when the step ends. That only works if
|
|
132
|
+
// it fits on one line; in a narrow terminal it would wrap and be left behind.
|
|
133
|
+
const inPlace = process.stdout.isTTY && label.length + 16 < (process.stdout.columns ?? 80);
|
|
134
|
+
if (inPlace) process.stdout.write(` ${dim('◇')} ${label}…`);
|
|
135
|
+
const result = await work();
|
|
136
|
+
const seconds = result?.seconds ?? (Date.now() - started) / 1000;
|
|
137
|
+
const took = seconds >= 1 ? dim(` (${duration(seconds)})`) : '';
|
|
138
|
+
process.stdout.write(`${inPlace ? '\r' : ''} ${green('✔')} ${label}${took} \n`);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseArgs(argv) {
|
|
143
|
+
const options = { name: undefined, pm: undefined, install: true, user: true, git: true, yes: false, bundledPackage: false };
|
|
144
|
+
for (let i = 0; i < argv.length; i++) {
|
|
145
|
+
const arg = argv[i];
|
|
146
|
+
if (arg === '-h' || arg === '--help') {
|
|
147
|
+
console.log(HELP);
|
|
148
|
+
process.exit(0);
|
|
149
|
+
} else if (arg === '-v' || arg === '--version') {
|
|
150
|
+
console.log(VERSION);
|
|
151
|
+
process.exit(0);
|
|
152
|
+
} else if (arg === '-y' || arg === '--yes') options.yes = true;
|
|
153
|
+
else if (arg === '--no-install') options.install = false;
|
|
154
|
+
else if (arg === '--no-user') options.user = false;
|
|
155
|
+
else if (arg === '--no-git') options.git = false;
|
|
156
|
+
else if (arg === '--bundled-package') options.bundledPackage = true;
|
|
157
|
+
else if (arg === '--pm') options.pm = argv[++i];
|
|
158
|
+
else if (arg.startsWith('--pm=')) options.pm = arg.slice(5);
|
|
159
|
+
else if (arg.startsWith('-')) fail(`Unknown option ${arg}. Run with --help to see the options.`);
|
|
160
|
+
else if (options.name === undefined) options.name = arg;
|
|
161
|
+
else fail(`Unexpected argument "${arg}". The app name is "${options.name}".`);
|
|
162
|
+
}
|
|
163
|
+
return options;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** "my-shop" → "My Shop", for the dashboard's title. */
|
|
167
|
+
const titleCase = (slug) => slug.split(/[-_]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(' ');
|
|
168
|
+
|
|
169
|
+
function detectPackageManager(requested) {
|
|
170
|
+
const known = ['pnpm', 'npm', 'yarn', 'bun'];
|
|
171
|
+
if (requested) {
|
|
172
|
+
if (!known.includes(requested)) fail(`--pm must be one of: ${known.join(', ')}.`);
|
|
173
|
+
if (!available(requested)) fail(`${requested} isn't installed, or isn't on your PATH.`);
|
|
174
|
+
return requested;
|
|
175
|
+
}
|
|
176
|
+
// "pnpm/10.1.0 npm/? node/v22…" when run through `pnpm create`.
|
|
177
|
+
const agent = (process.env.npm_config_user_agent ?? '').split('/')[0];
|
|
178
|
+
if (known.includes(agent)) return agent;
|
|
179
|
+
return available('pnpm') ? 'pnpm' : 'npm';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function checkRequirements() {
|
|
183
|
+
const node = process.versions.node.split('.').map(Number);
|
|
184
|
+
if (node[0] < 20) fail(`Nevela needs Node.js 20 or newer. You have ${process.versions.node}.`);
|
|
185
|
+
|
|
186
|
+
const php = available('php', ['-v']);
|
|
187
|
+
if (!php) fail('PHP isn\'t installed, or isn\'t on your PATH. Nevela needs PHP 8.3 or newer: https://www.php.net/downloads');
|
|
188
|
+
const version = /PHP (\d+)\.(\d+)/.exec(php);
|
|
189
|
+
if (!version || Number(version[1]) < 8 || (Number(version[1]) === 8 && Number(version[2]) < 3)) {
|
|
190
|
+
fail(`Nevela needs PHP 8.3 or newer. You have ${version ? `${version[1]}.${version[2]}` : 'a version I can\'t read'}.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!available('composer')) fail('Composer isn\'t installed, or isn\'t on your PATH: https://getcomposer.org/download/');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Sanctum's trait on the User model. `install:api` asks for this by hand; do it for them. */
|
|
197
|
+
function addApiTokens(userModel) {
|
|
198
|
+
let source = fs.readFileSync(userModel, 'utf8');
|
|
199
|
+
if (source.includes('HasApiTokens')) return true;
|
|
200
|
+
const withImport = source.replace(/^(use Illuminate\\Notifications\\Notifiable;)$/m, '$1\nuse Laravel\\Sanctum\\HasApiTokens;');
|
|
201
|
+
const withTrait = withImport.replace(/^(\s+use )(HasFactory, Notifiable;)$/m, '$1HasApiTokens, $2');
|
|
202
|
+
if (withImport === source || withTrait === withImport) return false;
|
|
203
|
+
fs.writeFileSync(userModel, withTrait);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function write(file, contents) {
|
|
208
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
209
|
+
fs.writeFileSync(file, contents);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function writeRootFiles(root, { name, title, pm, admin }) {
|
|
213
|
+
write(path.join(root, 'package.json'), `${JSON.stringify({
|
|
214
|
+
name,
|
|
215
|
+
version: '0.1.0',
|
|
216
|
+
private: true,
|
|
217
|
+
type: 'module',
|
|
218
|
+
scripts: {
|
|
219
|
+
dev: 'node scripts/dev.mjs',
|
|
220
|
+
'dev:api': 'cd apps/api && php artisan serve',
|
|
221
|
+
'dev:web': `cd apps/web && ${pm} run dev`,
|
|
222
|
+
},
|
|
223
|
+
}, null, 2)}\n`);
|
|
224
|
+
|
|
225
|
+
write(path.join(root, '.gitignore'), 'node_modules/\nvendor/\n.env\n.env.*\n!.env.example\n.next/\n*.tsbuildinfo\nnext-env.d.ts\n');
|
|
226
|
+
|
|
227
|
+
write(path.join(root, 'scripts', 'dev.mjs'), `// Run the Laravel API and the Next.js dashboard together: \`${pm} run dev\`.
|
|
228
|
+
import { spawn } from 'node:child_process';
|
|
229
|
+
|
|
230
|
+
const shell = process.platform === 'win32';
|
|
231
|
+
const apps = [
|
|
232
|
+
{ name: 'api', colour: 35, command: 'php', args: ['artisan', 'serve'], cwd: 'apps/api' },
|
|
233
|
+
{ name: 'web', colour: 36, command: '${pm}', args: ['run', 'dev'], cwd: 'apps/web' },
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const running = apps.map(({ name, colour, command, args, cwd }) => {
|
|
237
|
+
// One string when going through a shell: Node warns about (and doesn't escape) separate args there.
|
|
238
|
+
const child = shell
|
|
239
|
+
? spawn([command, ...args].join(' '), { cwd, shell, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
240
|
+
: spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
241
|
+
const prefix = \`\\x1b[\${colour}m\${name}\\x1b[0m │ \`;
|
|
242
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
243
|
+
let rest = '';
|
|
244
|
+
stream.on('data', (chunk) => {
|
|
245
|
+
const lines = (rest + chunk).split('\\n');
|
|
246
|
+
rest = lines.pop();
|
|
247
|
+
for (const line of lines) console.log(prefix + line);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
child.on('exit', (code) => {
|
|
251
|
+
console.log(\`\${prefix}stopped\${code ? \` (exit \${code})\` : ''}\`);
|
|
252
|
+
stop();
|
|
253
|
+
});
|
|
254
|
+
return child;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
let stopping = false;
|
|
258
|
+
function stop() {
|
|
259
|
+
if (stopping) return;
|
|
260
|
+
stopping = true;
|
|
261
|
+
for (const child of running) {
|
|
262
|
+
// On Windows the child is a shell; killing it alone would leave php and node running.
|
|
263
|
+
if (shell && child.pid) spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
264
|
+
else child.kill();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
process.on('SIGINT', stop);
|
|
268
|
+
process.on('SIGTERM', stop);
|
|
269
|
+
`);
|
|
270
|
+
|
|
271
|
+
write(path.join(root, 'README.md'), `# ${title}
|
|
272
|
+
|
|
273
|
+
A [Nevela](${DOCS}) app: a Laravel API in \`apps/api\` and a Next.js dashboard in \`apps/web\`.
|
|
274
|
+
|
|
275
|
+
## Run it
|
|
276
|
+
|
|
277
|
+
\`\`\`sh
|
|
278
|
+
${pm} run dev
|
|
279
|
+
\`\`\`
|
|
280
|
+
|
|
281
|
+
That starts Laravel on http://127.0.0.1:8000 and the dashboard on http://localhost:3000. Sign in at http://localhost:3000/sign-in.
|
|
282
|
+
${admin ? `
|
|
283
|
+
The app starts with one account, in your local database only:
|
|
284
|
+
|
|
285
|
+
| | |
|
|
286
|
+
|---|---|
|
|
287
|
+
| Email | \`${admin.email}\` |
|
|
288
|
+
| Password | \`${admin.password}\` |
|
|
289
|
+
` : ''}
|
|
290
|
+
To add someone who can sign in:
|
|
291
|
+
|
|
292
|
+
\`\`\`sh
|
|
293
|
+
cd apps/api
|
|
294
|
+
php artisan nevela:user
|
|
295
|
+
\`\`\`
|
|
296
|
+
|
|
297
|
+
## Add a resource
|
|
298
|
+
|
|
299
|
+
\`\`\`sh
|
|
300
|
+
cd apps/api
|
|
301
|
+
php artisan nevela:resource Product --fields="name:string, sku:string!, price:money, notes:text?"
|
|
302
|
+
php artisan migrate
|
|
303
|
+
\`\`\`
|
|
304
|
+
|
|
305
|
+
Reload the dashboard: Products is in the sidebar. Fill it with \`php artisan nevela:seed Product\`.
|
|
306
|
+
|
|
307
|
+
## Before real use
|
|
308
|
+
|
|
309
|
+
Every policy in \`apps/api/app/Policies\` starts by allowing any signed-in user. Write your rules there.
|
|
310
|
+
|
|
311
|
+
Docs: ${DOCS}
|
|
312
|
+
`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function main() {
|
|
316
|
+
const options = parseArgs(process.argv.slice(2));
|
|
317
|
+
|
|
318
|
+
console.log(`\n ${indigo(bold('Nevela'))} ${dim(`v${VERSION}`)}\n`);
|
|
319
|
+
|
|
320
|
+
let name = options.name;
|
|
321
|
+
if (!name) {
|
|
322
|
+
// The only question this ever asks, and only when the name was left off.
|
|
323
|
+
if (options.yes || !process.stdin.isTTY) fail('Give the app a name: pnpm create nevela my-app');
|
|
324
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
325
|
+
name = (await prompt.question(' What is the app called? ')).trim();
|
|
326
|
+
prompt.close();
|
|
327
|
+
}
|
|
328
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/.test(name)) {
|
|
329
|
+
fail(`"${name}" can't be used as a name. Use lowercase letters, numbers and dashes, for example my-app.`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const root = path.resolve(process.cwd(), name);
|
|
333
|
+
if (fs.existsSync(root) && fs.readdirSync(root).length > 0) fail(`${root} already exists and isn't empty.`);
|
|
334
|
+
|
|
335
|
+
checkRequirements();
|
|
336
|
+
const pm = detectPackageManager(options.pm);
|
|
337
|
+
const title = titleCase(name);
|
|
338
|
+
const api = path.join(root, 'apps', 'api');
|
|
339
|
+
const web = path.join(root, 'apps', 'web');
|
|
340
|
+
const began = Date.now();
|
|
341
|
+
fs.mkdirSync(path.join(root, 'apps'), { recursive: true });
|
|
342
|
+
|
|
343
|
+
// The dashboard first, so its packages can download while Composer works on Laravel.
|
|
344
|
+
// The two don't touch each other, and Composer is the long part.
|
|
345
|
+
copyWebTemplate(web, { name, title });
|
|
346
|
+
write(path.join(web, '.env.local'), '# Where the Laravel API is, including its prefix.\nNEVELA_API_URL=http://127.0.0.1:8000/api\n');
|
|
347
|
+
if (pm === 'pnpm') {
|
|
348
|
+
// Its own workspace file: lets sharp build, and keeps pnpm from adopting a workspace further up.
|
|
349
|
+
write(path.join(web, 'pnpm-workspace.yaml'), 'allowBuilds:\n sharp: true\n');
|
|
350
|
+
}
|
|
351
|
+
writeRootFiles(root, { name, title, pm, admin: options.user ? ADMIN : null });
|
|
352
|
+
const packages = options.install ? start(pm, ['install'], { cwd: web }) : null;
|
|
353
|
+
|
|
354
|
+
await step('Creating the Laravel app', () => {
|
|
355
|
+
run('composer', ['create-project', 'laravel/laravel', 'apps/api', '--no-interaction', '--no-progress', '--prefer-dist'], { cwd: root });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const tokens = await step('Installing Nevela and token sign-in', async () => {
|
|
359
|
+
const wanted = VERSION.split('.').slice(0, 2).map(Number);
|
|
360
|
+
let nevela = `nevela/laravel:^${wanted.join('.')}`;
|
|
361
|
+
if (options.bundledPackage || !(await onPackagist(wanted))) {
|
|
362
|
+
// No matching release on Packagist (or --bundled-package): the copy that travels with
|
|
363
|
+
// this installer is placed in the app and installed by path.
|
|
364
|
+
copyLaravelPackage(path.join(root, 'packages', 'nevela-laravel'));
|
|
365
|
+
run('composer', ['config', 'repositories.nevela', 'path', '../../packages/nevela-laravel'], { cwd: api });
|
|
366
|
+
nevela = 'nevela/laravel:@dev';
|
|
367
|
+
}
|
|
368
|
+
// One Composer run for both. `php artisan install:api` would add Sanctum in a run of
|
|
369
|
+
// its own, plus a routes/api.php this app doesn't use: Nevela registers its routes itself.
|
|
370
|
+
run('composer', ['require', 'laravel/sanctum', nevela, '--no-interaction', '--no-progress'], { cwd: api });
|
|
371
|
+
run('php', ['artisan', 'vendor:publish', '--tag=sanctum-migrations', '--no-interaction'], { cwd: api });
|
|
372
|
+
return addApiTokens(path.join(api, 'app', 'Models', 'User.php'));
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
let admin = false;
|
|
376
|
+
await step('Setting up the database', () => {
|
|
377
|
+
// Writes routes/nevela.php and the dashboard's (empty) resource registry.
|
|
378
|
+
run('php', ['artisan', 'nevela:generate'], { cwd: api });
|
|
379
|
+
run('php', ['artisan', 'migrate', '--force', '--no-interaction'], { cwd: api });
|
|
380
|
+
if (options.user && tokens) {
|
|
381
|
+
admin = run('php', ['artisan', 'nevela:user', `--name=${ADMIN.name}`, `--email=${ADMIN.email}`, `--password=${ADMIN.password}`], { cwd: api, allowFailure: true }).ok;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
let installed = false;
|
|
386
|
+
if (packages) {
|
|
387
|
+
// Usually finished long before Composer; the time shown is how long it took, not how long we waited here.
|
|
388
|
+
installed = (await step(`Installing dashboard packages (${pm})`, () => packages.done)).ok;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (options.git && available('git') && !run('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root, allowFailure: true }).ok) {
|
|
392
|
+
run('git', ['init', '--quiet'], { cwd: root, allowFailure: true });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log(`\n ${green('✔')} ${bold(`Created ${name}`)} ${dim(`in ${duration((Date.now() - began) / 1000)}`)}\n`);
|
|
396
|
+
if (!tokens) {
|
|
397
|
+
console.log(` ${red('!')} I couldn't add Sanctum's trait to apps/api/app/Models/User.php. Add it by hand:`);
|
|
398
|
+
console.log(` ${dim('use Laravel\\Sanctum\\HasApiTokens; and use HasApiTokens; inside the class')}\n`);
|
|
399
|
+
}
|
|
400
|
+
if (packages && !installed) {
|
|
401
|
+
console.log(` ${red('!')} The dashboard's packages didn't install. Run it yourself to see why: cd ${name}/apps/web && ${pm} install\n`);
|
|
402
|
+
}
|
|
403
|
+
console.log(' Next:\n');
|
|
404
|
+
console.log(` cd ${name}`);
|
|
405
|
+
if (!options.install) console.log(` cd apps/web && ${pm} install && cd ../..`);
|
|
406
|
+
if (!admin) console.log(` cd apps/api && php artisan nevela:user && cd ../.. ${dim('# someone to sign in as')}`);
|
|
407
|
+
console.log(` ${pm} run dev\n`);
|
|
408
|
+
console.log(` Then open ${indigo('http://localhost:3000/sign-in')}${admin ? ' and sign in with:' : ''}`);
|
|
409
|
+
if (admin) {
|
|
410
|
+
console.log(`\n Email ${bold(ADMIN.email)}`);
|
|
411
|
+
console.log(` Password ${bold(ADMIN.password)}\n`);
|
|
412
|
+
console.log(` ${dim('That account is in this app\'s local database only. Add your own: cd apps/api && php artisan nevela:user')}`);
|
|
413
|
+
}
|
|
414
|
+
console.log(`\n Add your first resource: ${dim('cd apps/api && php artisan nevela:resource Product --fields="name:string, price:money"')}`);
|
|
415
|
+
console.log(` Docs: ${DOCS}/start/quickstart/\n`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
main().catch((error) => fail(error?.message ?? String(error)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-nevela",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "create-nevela",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Create a new Nevela app: a Laravel API and a Next.js dashboard from one resource description.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-nevela": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"template.mjs",
|
|
12
|
+
"packagist.mjs",
|
|
13
|
+
"template",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prepack": "node build-template.mjs"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"nevela",
|
|
24
|
+
"laravel",
|
|
25
|
+
"nextjs",
|
|
26
|
+
"scaffold",
|
|
27
|
+
"create"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/MarkColeMukisa/nevela.git",
|
|
32
|
+
"directory": "packages/create-nevela"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://nevela-docs.vercel.app",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"author": "Mark Cole MUKISA"
|
|
37
|
+
}
|
package/packagist.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Deciding where a new app gets nevela/laravel from.
|
|
2
|
+
|
|
3
|
+
/** Whether `candidate` is a stable release Composer's `^major.minor` would accept. */
|
|
4
|
+
export function satisfiesCaret(candidate, [major, minor]) {
|
|
5
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(candidate);
|
|
6
|
+
if (!match) return false;
|
|
7
|
+
const [theirMajor, theirMinor] = match.slice(1).map(Number);
|
|
8
|
+
// Below 1.0 a caret holds the minor version too: ^0.1 means 0.1.x only.
|
|
9
|
+
return major === 0 ? theirMajor === 0 && theirMinor === minor : theirMajor === major && theirMinor >= minor;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether Packagist has a release of nevela/laravel that goes with this installer. Being
|
|
14
|
+
* listed isn't enough: with no tagged release yet, `composer require` would fail.
|
|
15
|
+
*/
|
|
16
|
+
export async function onPackagist(wanted) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch('https://repo.packagist.org/p2/nevela/laravel.json', { signal: AbortSignal.timeout(8000) });
|
|
19
|
+
if (!response.ok) return false;
|
|
20
|
+
const releases = (await response.json()).packages?.['nevela/laravel'] ?? [];
|
|
21
|
+
return releases.some((release) => satisfiesCaret(release.version, wanted));
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Cole MUKISA
|
|
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.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# nevela/laravel
|
|
2
|
+
|
|
3
|
+
The Laravel side of [Nevela](https://nevela-docs.vercel.app): describe a resource once and get its model, migration, validation, policy and REST API, plus the Next.js dashboard screens for it.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
php artisan nevela:resource Product --fields="name:string, sku:string!, price:money, kind:enum(stock|digital), notes:text?"
|
|
7
|
+
php artisan migrate
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Start a new app
|
|
11
|
+
|
|
12
|
+
The quickest way is the create command, which sets up Laravel, this package and the dashboard together:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm create nevela my-app
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Add it to an existing Laravel app
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
php artisan install:api # Sanctum, if you don't have it yet
|
|
22
|
+
composer require nevela/laravel
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Add Sanctum's trait to your user model:
|
|
26
|
+
|
|
27
|
+
```php
|
|
28
|
+
use Laravel\Sanctum\HasApiTokens;
|
|
29
|
+
|
|
30
|
+
class User extends Authenticatable
|
|
31
|
+
{
|
|
32
|
+
use HasApiTokens, HasFactory, Notifiable;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Needs PHP 8.2 or newer and Laravel 11, 12 or 13.
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
| Command | What it does |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `nevela:resource` | Describe a resource and generate everything for it. |
|
|
43
|
+
| `nevela:generate` | Regenerate after changing a descriptor. Your own code is kept. |
|
|
44
|
+
| `nevela:seed` | Fill a resource with plausible records, and print how long it took. |
|
|
45
|
+
| `nevela:user` | Create someone who can sign in. |
|
|
46
|
+
|
|
47
|
+
Full documentation: https://nevela-docs.vercel.app
|
|
48
|
+
|
|
49
|
+
## Where this code lives
|
|
50
|
+
|
|
51
|
+
This repository is a read-only copy of `packages/laravel` from [MarkColeMukisa/nevela](https://github.com/MarkColeMukisa/nevela), kept so Composer can install the package. Open issues and pull requests there.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT. Copyright (c) 2026 Mark Cole MUKISA.
|