@xeplr/cli 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 +95 -0
- package/bin/xeplr.js +77 -0
- package/lib/commands/encrypt.js +144 -0
- package/lib/generate/index.js +147 -0
- package/lib/nextSteps.js +39 -0
- package/lib/plan.js +44 -0
- package/lib/prompt.js +130 -0
- package/lib/questions.js +125 -0
- package/package.json +39 -0
- package/templates/api/app.js +21 -0
- package/templates/api/bin/www +62 -0
- package/templates/api/db/setup.js +10 -0
- package/templates/api/development.env.tpl +47 -0
- package/templates/api/env.required.js +29 -0
- package/templates/api/gitignore.tpl +2 -0
- package/templates/api/migrations/0001_tasks.sql +23 -0
- package/templates/api/migrations-auth/0001_home_menu.sql +14 -0
- package/templates/api/migrations-auth/0002_tasks_menu.sql +11 -0
- package/templates/api/models/Task.js +48 -0
- package/templates/api/models/index.js +7 -0
- package/templates/api/package.json +30 -0
- package/templates/api/routes/index.js +46 -0
- package/templates/ui/dotenv.tpl +8 -0
- package/templates/ui/gitignore.tpl +2 -0
- package/templates/ui/index.html +12 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/public/logo-wide.svg +6 -0
- package/templates/ui/public/logo.svg +4 -0
- package/templates/ui/src/App.jsx +84 -0
- package/templates/ui/src/api/tasks.js +34 -0
- package/templates/ui/src/index.css +275 -0
- package/templates/ui/src/main.jsx +27 -0
- package/templates/ui/src/pages/Home.jsx +73 -0
- package/templates/ui/src/pages/Tasks.jsx +106 -0
- package/templates/ui/src/pages/TasksHelp.jsx +70 -0
- package/templates/ui/src/useTasksController.js +83 -0
- package/templates/ui/vite.config.js +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xeplr
|
|
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,95 @@
|
|
|
1
|
+
# @xeplr/cli
|
|
2
|
+
|
|
3
|
+
Creates a working xeplr application — an API, sign-in, and a UI — from four
|
|
4
|
+
questions.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx @xeplr/cli new myapp
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## What you get
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
myapp/
|
|
14
|
+
├─ api/ backend, sign-in, your data
|
|
15
|
+
└─ ui/ React frontend
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Already working, with nothing to wire up:
|
|
19
|
+
|
|
20
|
+
- **Sign-in** — login, register, forgot and reset password, activation,
|
|
21
|
+
profile, change password, and the admin screens for users, roles and
|
|
22
|
+
permissions. You write none of them.
|
|
23
|
+
- **A guarded API** — every address needs a valid token. An address you want
|
|
24
|
+
open is named explicitly; there is no way to leave the whole thing public by
|
|
25
|
+
forgetting.
|
|
26
|
+
- **A sample form** — a table, a model, one line of routing, and a page, with
|
|
27
|
+
instructions beside it showing how to add your own.
|
|
28
|
+
- **A welcome screen** that checks it can reach everything and tells you which
|
|
29
|
+
files to change first.
|
|
30
|
+
|
|
31
|
+
## Before you start
|
|
32
|
+
|
|
33
|
+
- **Node.js** 18 or later
|
|
34
|
+
- **PostgreSQL**, running, with a user allowed to create databases
|
|
35
|
+
|
|
36
|
+
## Then
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd myapp/api
|
|
40
|
+
npm install
|
|
41
|
+
npm run setup # creates both databases and their tables
|
|
42
|
+
npm run start-auth # sign-in
|
|
43
|
+
npm run start-api # your API
|
|
44
|
+
|
|
45
|
+
cd ../ui
|
|
46
|
+
npm install
|
|
47
|
+
npm run dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Open the UI and sign in with the account you named.
|
|
51
|
+
|
|
52
|
+
## Changing the database connection later
|
|
53
|
+
|
|
54
|
+
A password rotates, the database moves, or the first answer was wrong. From
|
|
55
|
+
the project's `api` folder:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm run db:encrypt
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It reads the encryption key out of your settings, asks for the database
|
|
62
|
+
details, and rewrites every connection setting for you.
|
|
63
|
+
|
|
64
|
+
The same thing without a project to run it in — before `npm install`, or from
|
|
65
|
+
any other folder — is `npx @xeplr/cli encrypt`. It asks for the key too, and
|
|
66
|
+
prints the result rather than writing it.
|
|
67
|
+
|
|
68
|
+
## If you skip the connection question
|
|
69
|
+
|
|
70
|
+
The setting is left **blank**, with `npx @xeplr/cli encrypt` written directly
|
|
71
|
+
above it in `api/development.env`.
|
|
72
|
+
|
|
73
|
+
That is deliberate. Host, port, user and password are encrypted together into a
|
|
74
|
+
single string, and a guessed value would start cleanly, migrate, serve traffic,
|
|
75
|
+
and read as *empty data* rather than as an error — the most expensive way for
|
|
76
|
+
this to go wrong. Until you fill it in, the app refuses to start and names the
|
|
77
|
+
setting.
|
|
78
|
+
|
|
79
|
+
The same rule applies to every database name and storage path in a xeplr app:
|
|
80
|
+
no defaults, ever.
|
|
81
|
+
|
|
82
|
+
## Questions it asks
|
|
83
|
+
|
|
84
|
+
| | |
|
|
85
|
+
|---|---|
|
|
86
|
+
| Project name | the folder, and the database prefix |
|
|
87
|
+
| Port decade | three consecutive ports — ui, sign-in, api |
|
|
88
|
+
| Super admin email | the account you sign in with |
|
|
89
|
+
| Super admin password | its password |
|
|
90
|
+
|
|
91
|
+
Encryption keys and signing secrets are **generated, never asked**.
|
|
92
|
+
|
|
93
|
+
## Licence
|
|
94
|
+
|
|
95
|
+
MIT
|
package/bin/xeplr.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var prompt = require('../lib/prompt');
|
|
5
|
+
var questions = require('../lib/questions');
|
|
6
|
+
var plan = require('../lib/plan');
|
|
7
|
+
var generate = require('../lib/generate');
|
|
8
|
+
var nextSteps = require('../lib/nextSteps');
|
|
9
|
+
var encryptCmd = require('../lib/commands/encrypt');
|
|
10
|
+
|
|
11
|
+
var HELP = [
|
|
12
|
+
'',
|
|
13
|
+
' xeplr — create a working xeplr application',
|
|
14
|
+
'',
|
|
15
|
+
' Usage:',
|
|
16
|
+
' xeplr new [name] create a project',
|
|
17
|
+
' xeplr encrypt redo the database connection',
|
|
18
|
+
'',
|
|
19
|
+
' new Creates a project with an API, sign-in and a UI, all talking',
|
|
20
|
+
' to each other. Asks a few questions; generates the rest.',
|
|
21
|
+
'',
|
|
22
|
+
' encrypt Turns database details into the single string the settings',
|
|
23
|
+
' file holds. Run it inside a project and it finds the key and',
|
|
24
|
+
' writes the result in for you. For when a password changes, the',
|
|
25
|
+
' database moves, or the first answer was wrong.',
|
|
26
|
+
''
|
|
27
|
+
].join('\n');
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
var argv = process.argv.slice(2);
|
|
31
|
+
var command = argv[0];
|
|
32
|
+
|
|
33
|
+
if (!command || command === '-h' || command === '--help' || command === 'help') {
|
|
34
|
+
console.log(HELP);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
if (command === 'encrypt') {
|
|
38
|
+
var s2 = prompt.createSession();
|
|
39
|
+
try {
|
|
40
|
+
return await encryptCmd.run(s2, argv.slice(1));
|
|
41
|
+
} finally {
|
|
42
|
+
s2.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (command !== 'new') {
|
|
47
|
+
console.error('\n Unknown command: ' + command + '\n Try: xeplr new <name> or xeplr encrypt\n');
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('\n Creating a xeplr application.\n');
|
|
52
|
+
|
|
53
|
+
var session = prompt.createSession();
|
|
54
|
+
try {
|
|
55
|
+
var answers = await questions.collect(session, argv[1]);
|
|
56
|
+
console.log(plan.describe(answers));
|
|
57
|
+
|
|
58
|
+
var go = await session.confirm(' Create it?', true);
|
|
59
|
+
if (!go) {
|
|
60
|
+
console.log('\n Nothing was written.\n');
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
session.close();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var target = path.resolve(process.cwd(), answers.name);
|
|
68
|
+
var result = generate.generate(answers, target);
|
|
69
|
+
console.log('\n Wrote ' + result.written.length + ' files.');
|
|
70
|
+
console.log(nextSteps.describe(answers, target));
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main().then(function (code) { process.exit(code); }, function (err) {
|
|
75
|
+
console.error('\n ' + (err && err.message ? err.message : err) + '\n');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var { encrypt } = require('@xeplr/utils/isomorphic/crypto');
|
|
4
|
+
|
|
5
|
+
// Where a settings file usually is, relative to wherever this was run.
|
|
6
|
+
// Running it from the project root and from inside api/ are both normal.
|
|
7
|
+
var CANDIDATES = ['development.env', path.join('api', 'development.env')];
|
|
8
|
+
|
|
9
|
+
function findEnvFile(cwd) {
|
|
10
|
+
for (var i = 0; i < CANDIDATES.length; i++) {
|
|
11
|
+
var full = path.join(cwd, CANDIDATES[i]);
|
|
12
|
+
if (fs.existsSync(full)) return full;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readSetting(file, name) {
|
|
18
|
+
var line = fs.readFileSync(file, 'utf8')
|
|
19
|
+
.split('\n')
|
|
20
|
+
.find(function (l) { return l.indexOf(name + '=') === 0; });
|
|
21
|
+
return line ? line.slice(name.length + 1).trim() : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Rewrite every setting that holds a connection string.
|
|
26
|
+
*
|
|
27
|
+
* BOTH of them, always. The two settings name the same server, and a project
|
|
28
|
+
* where only one was updated fails halfway through setup — the sign-in
|
|
29
|
+
* database is created and the app's own is not, or the reverse — which reads
|
|
30
|
+
* as a broken install rather than as a half-finished edit.
|
|
31
|
+
*/
|
|
32
|
+
function writeConnection(file, value) {
|
|
33
|
+
var body = fs.readFileSync(file, 'utf8');
|
|
34
|
+
var updated = [];
|
|
35
|
+
body = body.split('\n').map(function (line) {
|
|
36
|
+
var m = line.match(/^([A-Z0-9_]*CONNECTION[A-Z0-9_]*)=/);
|
|
37
|
+
if (!m) return line;
|
|
38
|
+
updated.push(m[1]);
|
|
39
|
+
return m[1] + '=' + value;
|
|
40
|
+
}).join('\n');
|
|
41
|
+
fs.writeFileSync(file, body);
|
|
42
|
+
return updated;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `xeplr encrypt` — turn database details into the single string the settings
|
|
47
|
+
* file holds, and optionally write it straight in.
|
|
48
|
+
*
|
|
49
|
+
* Exists because the details change: a password rotates, the database moves,
|
|
50
|
+
* or the first answer was simply wrong. Without this the only way back is a
|
|
51
|
+
* long npx command with two --package flags whose output has to be pasted into
|
|
52
|
+
* two places — which is exactly the kind of step people get wrong at the point
|
|
53
|
+
* they are already frustrated.
|
|
54
|
+
*/
|
|
55
|
+
function parseFlags(argv) {
|
|
56
|
+
var flags = {};
|
|
57
|
+
(argv || []).forEach(function (arg, i) {
|
|
58
|
+
var m = arg.match(/^--([a-z-]+)(?:=(.*))?$/);
|
|
59
|
+
if (!m) return;
|
|
60
|
+
var value = m[2] !== undefined ? m[2] : (argv[i + 1] || '');
|
|
61
|
+
// A flag whose "value" is the next flag was given with nothing after it.
|
|
62
|
+
flags[m[1]] = /^--/.test(value) ? '' : value;
|
|
63
|
+
});
|
|
64
|
+
return flags;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Ask, unless it was already given on the command line.
|
|
69
|
+
*
|
|
70
|
+
* Every value here can arrive either way, so the same command serves somebody
|
|
71
|
+
* typing at a terminal and a script that has no terminal to type at. A
|
|
72
|
+
* password on a command line is visible in shell history and in the process
|
|
73
|
+
* list, so the prompt stays the default and the flag is the opt-in.
|
|
74
|
+
*/
|
|
75
|
+
function given(flags, name, fallback) {
|
|
76
|
+
return Object.prototype.hasOwnProperty.call(flags, name) ? flags[name] : fallback;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function run(session, argv, cwd) {
|
|
80
|
+
cwd = cwd || process.cwd();
|
|
81
|
+
var flags = parseFlags(argv);
|
|
82
|
+
var envFile = findEnvFile(cwd);
|
|
83
|
+
|
|
84
|
+
// The key must match the one the project already uses, or nothing it wrote
|
|
85
|
+
// before can be read back. So it is taken FROM the project rather than
|
|
86
|
+
// asked for, whenever there is a project to take it from.
|
|
87
|
+
var key = given(flags, 'key', null);
|
|
88
|
+
if (!key && envFile) {
|
|
89
|
+
key = readSetting(envFile, 'ENCRYPTION_KEY');
|
|
90
|
+
if (key) console.log('\n Using ENCRYPTION_KEY from ' + path.relative(cwd, envFile));
|
|
91
|
+
}
|
|
92
|
+
if (!key) {
|
|
93
|
+
console.log('\n No settings file found here, so the key has to be given.');
|
|
94
|
+
key = await session.ask(' ENCRYPTION_KEY?', {
|
|
95
|
+
validate: function (v) { return v.length >= 16 ? true : 'That looks too short.'; }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var host = given(flags, 'host', null) ||
|
|
100
|
+
await session.ask(' Database host?', { default: 'localhost' });
|
|
101
|
+
|
|
102
|
+
var port = given(flags, 'port', null) ||
|
|
103
|
+
await session.ask(' Database port?', {
|
|
104
|
+
default: '5432',
|
|
105
|
+
validate: function (v) { return /^\d+$/.test(v) ? true : 'A number.'; }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
var user = given(flags, 'user', null) ||
|
|
109
|
+
await session.ask(' Database user?', { default: 'postgres' });
|
|
110
|
+
|
|
111
|
+
// hasOwnProperty rather than a truthiness check: --password= with nothing
|
|
112
|
+
// after it means an EMPTY password, which some local installs really do
|
|
113
|
+
// have, and must not silently turn into a prompt.
|
|
114
|
+
var password = Object.prototype.hasOwnProperty.call(flags, 'password')
|
|
115
|
+
? flags.password
|
|
116
|
+
: await session.askSecret(' Database password?');
|
|
117
|
+
|
|
118
|
+
var value = await encrypt(JSON.stringify({
|
|
119
|
+
host: host, port: Number(port), user: user, password: password
|
|
120
|
+
}), key);
|
|
121
|
+
|
|
122
|
+
console.log('\n Encrypted connection:\n');
|
|
123
|
+
console.log(' ' + value + '\n');
|
|
124
|
+
|
|
125
|
+
if (!envFile) {
|
|
126
|
+
console.log(' Paste it into every *_CONNECTION setting in your');
|
|
127
|
+
console.log(' development.env, and into AUTH_DB_CONNECTION_INFO_ENCRYPTED.\n');
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
var write = Object.prototype.hasOwnProperty.call(flags, 'no-write') ? false
|
|
132
|
+
: Object.prototype.hasOwnProperty.call(flags, 'write') ? true
|
|
133
|
+
: await session.confirm(' Write it into ' + path.relative(cwd, envFile) + ' now?', true);
|
|
134
|
+
if (!write) {
|
|
135
|
+
console.log('\n Left alone. Paste it in yourself when ready.\n');
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
var updated = writeConnection(envFile, value);
|
|
140
|
+
console.log('\n Updated ' + updated.length + ' settings: ' + updated.join(', ') + '\n');
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { run: run, parseFlags: parseFlags, findEnvFile: findEnvFile, writeConnection: writeConnection };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
var fs = require('fs');
|
|
2
|
+
var path = require('path');
|
|
3
|
+
|
|
4
|
+
var TEMPLATES = path.join(__dirname, '..', '..', 'templates');
|
|
5
|
+
|
|
6
|
+
// Files whose names cannot ship as-is. `.env` and `.gitignore` are both
|
|
7
|
+
// swallowed on the way here — npm refuses to pack a .gitignore, and an .env in
|
|
8
|
+
// a published package is a mistake waiting to happen — so they travel under a
|
|
9
|
+
// safe name and are restored on the way out.
|
|
10
|
+
var RENAME = {
|
|
11
|
+
'development.env.tpl': 'development.env',
|
|
12
|
+
'dotenv.tpl': '.env',
|
|
13
|
+
'gitignore.tpl': '.gitignore'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Everything the templates can refer to. One table, so a placeholder that is
|
|
17
|
+
// never filled shows up as a missing key rather than as the literal
|
|
18
|
+
// __SOMETHING__ in a generated file.
|
|
19
|
+
function substitutions(answers) {
|
|
20
|
+
return {
|
|
21
|
+
__NAME__: answers.name,
|
|
22
|
+
__PREFIX__: answers.prefix,
|
|
23
|
+
__UI_PORT__: String(answers.ports.ui),
|
|
24
|
+
__AUTH_PORT__: String(answers.ports.auth),
|
|
25
|
+
__API_PORT__: String(answers.ports.api),
|
|
26
|
+
__ENCRYPTION_KEY__: answers.secrets.encryptionKey,
|
|
27
|
+
__JWT_SECRET__: answers.secrets.jwtSecret,
|
|
28
|
+
__ADMIN_EMAIL__: answers.admin.email,
|
|
29
|
+
__ADMIN_PASSWORD__: answers.admin.password,
|
|
30
|
+
__DB_CONNECTION__: answers.connection || '',
|
|
31
|
+
__CONNECTION_NOTE__: connectionNote(answers)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The comment above the connection setting.
|
|
37
|
+
*
|
|
38
|
+
* Two different situations, and a reader in the second one needs a command
|
|
39
|
+
* rather than an explanation of why the line is empty.
|
|
40
|
+
*/
|
|
41
|
+
function connectionNote(answers) {
|
|
42
|
+
if (answers.connection) {
|
|
43
|
+
return [
|
|
44
|
+
'# ── Where the databases are ──',
|
|
45
|
+
'# Host, port, user and password, encrypted together from what you told',
|
|
46
|
+
'# the installer. One value is enough because everything needed is inside',
|
|
47
|
+
'# it. Both settings use the same server.',
|
|
48
|
+
'#',
|
|
49
|
+
'# CHANGED? From the api folder, this replaces both lines for you:',
|
|
50
|
+
'#',
|
|
51
|
+
'# npm run db:encrypt'
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
'# ═══════════════════════════════════════════════════════════════════════',
|
|
56
|
+
'# FILL THESE IN BEFORE STARTING ANYTHING',
|
|
57
|
+
'#',
|
|
58
|
+
'# From the api folder, after `npm install`, this fills both lines in:',
|
|
59
|
+
'#',
|
|
60
|
+
'# npm run db:encrypt',
|
|
61
|
+
'#',
|
|
62
|
+
'# Before installing, or from anywhere else:',
|
|
63
|
+
'#',
|
|
64
|
+
'# npx @xeplr/cli encrypt',
|
|
65
|
+
'#',
|
|
66
|
+
'# Blank on purpose. A guessed database location starts cleanly, migrates,',
|
|
67
|
+
'# serves traffic, and reads as EMPTY DATA rather than as an error — so',
|
|
68
|
+
'# nothing is guessed. Until these are filled in the app refuses to start',
|
|
69
|
+
'# and names them.',
|
|
70
|
+
'# ═══════════════════════════════════════════════════════════════════════'
|
|
71
|
+
].join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fill(body, subs) {
|
|
75
|
+
// The inner name must START and END with a letter or digit.
|
|
76
|
+
//
|
|
77
|
+
// Without that, a greedy match on `__PREFIX___PORT` takes `__PREFIX___` —
|
|
78
|
+
// swallowing the underscore that belongs to `_PORT` — and then complains
|
|
79
|
+
// about a token nobody wrote. Anchoring both ends makes `__PREFIX__` the
|
|
80
|
+
// only possible match, leaving `_PORT` alone.
|
|
81
|
+
return body.replace(/__([A-Z][A-Z0-9_]*[A-Z0-9])__/g, function (token) {
|
|
82
|
+
// An unknown placeholder is a bug in the templates, and leaving it in the
|
|
83
|
+
// generated file would be found much later by somebody who did not write
|
|
84
|
+
// it. Fail here, naming the token.
|
|
85
|
+
if (!(token in subs)) throw new Error('Template refers to ' + token + ', which nothing fills in');
|
|
86
|
+
return subs[token];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isBinary(file) {
|
|
91
|
+
return /\.(png|jpe?g|gif|ico|woff2?|ttf)$/i.test(file);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function walk(dir) {
|
|
95
|
+
var out = [];
|
|
96
|
+
fs.readdirSync(dir).forEach(function (entry) {
|
|
97
|
+
var full = path.join(dir, entry);
|
|
98
|
+
if (fs.statSync(full).isDirectory()) return out.push.apply(out, walk(full));
|
|
99
|
+
out.push(full);
|
|
100
|
+
});
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Write the project.
|
|
106
|
+
*
|
|
107
|
+
* REFUSES TO OVERWRITE. Generating into a folder that already has something in
|
|
108
|
+
* it is how somebody loses work they had not committed, and the check is one
|
|
109
|
+
* line.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} answers from lib/questions.js
|
|
112
|
+
* @param {string} target absolute path of the folder to create
|
|
113
|
+
* @returns {{ written: string[] }}
|
|
114
|
+
*/
|
|
115
|
+
function generate(answers, target) {
|
|
116
|
+
if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
|
|
117
|
+
throw new Error(target + ' already exists and is not empty.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
var subs = substitutions(answers);
|
|
121
|
+
var written = [];
|
|
122
|
+
|
|
123
|
+
walk(TEMPLATES).forEach(function (src) {
|
|
124
|
+
var rel = path.relative(TEMPLATES, src);
|
|
125
|
+
var dir = path.dirname(rel);
|
|
126
|
+
var base = path.basename(rel);
|
|
127
|
+
var outName = RENAME[base] || base;
|
|
128
|
+
var dest = path.join(target, dir, outName);
|
|
129
|
+
|
|
130
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
131
|
+
|
|
132
|
+
if (isBinary(src)) {
|
|
133
|
+
fs.copyFileSync(src, dest);
|
|
134
|
+
} else {
|
|
135
|
+
fs.writeFileSync(dest, fill(fs.readFileSync(src, 'utf8'), subs));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// bin/www is executed directly, so it has to keep the bit that says so.
|
|
139
|
+
if (outName === 'www') fs.chmodSync(dest, 0o755);
|
|
140
|
+
|
|
141
|
+
written.push(path.join(dir, outName));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return { written: written.sort() };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { generate: generate, substitutions: substitutions, fill: fill };
|
package/lib/nextSteps.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to do now that the files exist.
|
|
3
|
+
*
|
|
4
|
+
* Printed at the end because the one thing a generator cannot do for you is
|
|
5
|
+
* the database connection — and a project that looks finished but will not
|
|
6
|
+
* start is worse than one that says what is left.
|
|
7
|
+
*/
|
|
8
|
+
function describe(answers, dir) {
|
|
9
|
+
var p = answers.prefix;
|
|
10
|
+
var head = ['', ' Created ' + dir, ''];
|
|
11
|
+
|
|
12
|
+
if (!answers.connection) {
|
|
13
|
+
head = head.concat([
|
|
14
|
+
' ONE THING LEFT — your database connection:',
|
|
15
|
+
'',
|
|
16
|
+
' cd ' + answers.name + '/api && npm install && npm run db:encrypt',
|
|
17
|
+
'',
|
|
18
|
+
' It asks for your database details and fills them in for you.',
|
|
19
|
+
''
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return head.concat([
|
|
24
|
+
' Then:',
|
|
25
|
+
'',
|
|
26
|
+
' cd ' + answers.name + '/api && npm install && npm run setup',
|
|
27
|
+
' npm run start-auth # sign-in, port ' + answers.ports.auth,
|
|
28
|
+
' npm run start-api # your API, port ' + answers.ports.api,
|
|
29
|
+
'',
|
|
30
|
+
' cd ../ui && npm install',
|
|
31
|
+
' npm run dev # the app, port ' + answers.ports.ui,
|
|
32
|
+
'',
|
|
33
|
+
' Then open http://localhost:' + answers.ports.ui + ' and sign in as',
|
|
34
|
+
' ' + answers.admin.email + '.',
|
|
35
|
+
''
|
|
36
|
+
]).join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { describe: describe };
|
package/lib/plan.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the generator is about to do, printed before it does any of it.
|
|
3
|
+
*
|
|
4
|
+
* A generator that writes 30 files and then tells you is one you have to undo
|
|
5
|
+
* by hand when an answer was wrong. This is the last point where "no" is free.
|
|
6
|
+
*/
|
|
7
|
+
function describe(answers) {
|
|
8
|
+
var L = [];
|
|
9
|
+
var p = answers.prefix;
|
|
10
|
+
|
|
11
|
+
L.push('');
|
|
12
|
+
L.push(' ' + answers.name + '/');
|
|
13
|
+
L.push(' ├─ api/ backend, on port ' + answers.ports.api);
|
|
14
|
+
L.push(' └─ ui/ frontend, on port ' + answers.ports.ui);
|
|
15
|
+
L.push('');
|
|
16
|
+
L.push(' Sign-in runs alongside the API on port ' + answers.ports.auth + '.');
|
|
17
|
+
L.push('');
|
|
18
|
+
L.push(' Databases (created for you on first run):');
|
|
19
|
+
L.push(' ' + answers.databases.auth + ' accounts, roles, permissions');
|
|
20
|
+
L.push(' ' + answers.databases.api + ' your own data');
|
|
21
|
+
L.push('');
|
|
22
|
+
L.push(' You will sign in as ' + answers.admin.email + '.');
|
|
23
|
+
L.push('');
|
|
24
|
+
L.push(' Settings written for you:');
|
|
25
|
+
L.push(' ' + p + '_PORT, AUTH_PORT, AUTH_URL, DB_API, AUTH_DB_NAME');
|
|
26
|
+
L.push(' ENCRYPTION_KEY and AUTH_JWT_SECRET generated, not asked');
|
|
27
|
+
L.push(' AUTH_SUPER_ADMIN_EMAIL / _PASSWORD from your answers');
|
|
28
|
+
L.push('');
|
|
29
|
+
if (answers.connection) {
|
|
30
|
+
L.push(' ' + p + '_CONNECTION encrypted from your answers');
|
|
31
|
+
L.push('');
|
|
32
|
+
} else {
|
|
33
|
+
L.push('');
|
|
34
|
+
L.push(' Left BLANK, for you to fill in:');
|
|
35
|
+
L.push(' ' + p + '_CONNECTION and AUTH_DB_CONNECTION_INFO_ENCRYPTED');
|
|
36
|
+
L.push('');
|
|
37
|
+
L.push(' Run `npx @xeplr/cli encrypt` in the project to fill them in.');
|
|
38
|
+
L.push(' Until then the app refuses to start and names them.');
|
|
39
|
+
L.push('');
|
|
40
|
+
}
|
|
41
|
+
return L.join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { describe: describe };
|