@adula/create-app 0.2.0-alpha.1 → 0.2.0-alpha.2
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/README.md +25 -2
- package/build/cli.mjs +48 -6
- package/build/prerequisites.mjs +71 -0
- package/build/project.mjs +1 -1
- package/build/system.mjs +59 -0
- package/build/template.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Creates a complete, project-owned AdonisJS 7 application with adula, React/Inertia,
|
|
4
4
|
shadcn/ui, authentication, administration and the managed business-design skill.
|
|
5
|
-
Version 0.2.0-alpha.
|
|
5
|
+
Version 0.2.0-alpha.2 is **experimental**, intended for the alpha channel.
|
|
6
|
+
The owner-authorized latest alias remains on 0.2.0-alpha.1; next is absent.
|
|
6
7
|
|
|
7
8
|
```sh
|
|
8
9
|
npm create @adula/app@alpha my-app
|
|
@@ -13,6 +14,28 @@ through an existing Docker installation with Compose. pnpm is bootstrapped throu
|
|
|
13
14
|
npm and installed as a project-local development dependency; neither a global
|
|
14
15
|
pnpm installation nor a pre-existing AdonisJS application is required.
|
|
15
16
|
|
|
17
|
+
Use a lowercase directory such as `dental-gate`; the company display name can be
|
|
18
|
+
`Dental-Gate`. The creator checks a supplied directory and Docker before asking
|
|
19
|
+
for company details or creating project files.
|
|
20
|
+
|
|
21
|
+
On Windows, setup checks native Docker, the standard Docker Desktop location,
|
|
22
|
+
then Docker in the default WSL distribution. When WSL Docker is selected, Node.js
|
|
23
|
+
and application files remain on Windows; Compose runs through `wsl.exe --exec`.
|
|
24
|
+
The generated README records the matching service start/stop command.
|
|
25
|
+
|
|
26
|
+
If Docker is absent, the interactive wizard asks for explicit permission before
|
|
27
|
+
installing Docker Desktop using `winget`. Pressing Enter or answering no never
|
|
28
|
+
installs software: you can supply an existing-services profile or cancel. Windows
|
|
29
|
+
may request administrator approval or a restart; complete Docker Desktop setup
|
|
30
|
+
and rerun the same command if needed. No application files/databases are created
|
|
31
|
+
before the service preflight succeeds. `--yes` never authorizes host installation.
|
|
32
|
+
On other operating systems, setup provides installation guidance and the existing
|
|
33
|
+
services alternative. A stopped engine is detected separately and can be retried.
|
|
34
|
+
Existing PostgreSQL 17 **and Redis** are supported via the connection profile below.
|
|
35
|
+
|
|
36
|
+
Host installation references: [Docker Desktop for Windows](https://docs.docker.com/desktop/setup/install/windows-install/)
|
|
37
|
+
and [WinGet install options](https://learn.microsoft.com/windows/package-manager/winget/install).
|
|
38
|
+
|
|
16
39
|
The wizard requests the company name, administrator email and company identity.
|
|
17
40
|
An optional identity JSON file contains `primaryColor` (six-digit hex), `logo`
|
|
18
41
|
(local PNG/JPEG/WebP path relative to the JSON), `fontFamily` and `guidelines`.
|
|
@@ -58,6 +81,6 @@ runtime processes documented in the application README. SMTP, OAuth, S3, product
|
|
|
58
81
|
hosting and off-site backups need real configuration; no external accounts are
|
|
59
82
|
created. Node.js and Docker themselves are host prerequisites.
|
|
60
83
|
|
|
61
|
-
|
|
84
|
+
For local archive verification, run the packed CLI with npm exec and `--packages` pointing
|
|
62
85
|
to the directory containing matching `adula-kit-VERSION.tgz` and
|
|
63
86
|
`adula-ui-VERSION.tgz`. `pnpm test:create` exercises this path from an empty folder.
|
package/build/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Progress } from './progress.mjs'
|
|
3
|
+
import { prepareDocker } from './prerequisites.mjs'
|
|
3
4
|
import { parseArgs } from 'node:util'
|
|
4
5
|
import { createInterface } from 'node:readline/promises'
|
|
5
6
|
import { randomBytes } from 'node:crypto'
|
|
@@ -117,6 +118,15 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
117
118
|
let email = values['admin-email']
|
|
118
119
|
let identityFile = values.identity
|
|
119
120
|
const interactive = process.stdin.isTTY && !values.yes
|
|
121
|
+
// Reject a supplied destination before collecting company details.
|
|
122
|
+
if (directory) {
|
|
123
|
+
projectName(resolve(directory))
|
|
124
|
+
await assertEmpty(resolve(directory))
|
|
125
|
+
}
|
|
126
|
+
if (!interactive && (!directory || !company || !email))
|
|
127
|
+
throw new Error('Provide a directory, --company and --admin-email, or run interactively.')
|
|
128
|
+
// Host prerequisites must pass before prompts, package setup or project writes.
|
|
129
|
+
let docker
|
|
120
130
|
if (interactive) {
|
|
121
131
|
const prompt = createInterface({
|
|
122
132
|
input: process.stdin,
|
|
@@ -124,6 +134,28 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
124
134
|
})
|
|
125
135
|
try {
|
|
126
136
|
directory ||= await prompt.question('Application directory: ')
|
|
137
|
+
projectName(resolve(directory))
|
|
138
|
+
await assertEmpty(resolve(directory))
|
|
139
|
+
if (values.services === 'docker') {
|
|
140
|
+
const services = await prepareDocker({
|
|
141
|
+
question: (message) => prompt.question(message),
|
|
142
|
+
execute: async (command, args) => {
|
|
143
|
+
prompt.pause()
|
|
144
|
+
try {
|
|
145
|
+
await run(command, args)
|
|
146
|
+
} finally {
|
|
147
|
+
prompt.resume()
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
if (services.connection) {
|
|
152
|
+
values.services = 'existing'
|
|
153
|
+
values.connection = services.connection
|
|
154
|
+
} else {
|
|
155
|
+
docker = services.docker
|
|
156
|
+
console.log(`Docker is ready (${docker.display}). Continuing setup.`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
127
159
|
company ||= await prompt.question('Company name as it should appear in the application: ')
|
|
128
160
|
email ||= await prompt.question('Administrator email: ')
|
|
129
161
|
identityFile ||= await prompt.question(
|
|
@@ -132,6 +164,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
132
164
|
} finally {
|
|
133
165
|
prompt.close()
|
|
134
166
|
}
|
|
167
|
+
} else if (values.services === 'docker') {
|
|
168
|
+
docker = (await prepareDocker()).docker
|
|
135
169
|
}
|
|
136
170
|
if (!directory || !company || !email)
|
|
137
171
|
throw new Error('Provide a directory, --company and --admin-email, or run interactively.')
|
|
@@ -166,8 +200,6 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
166
200
|
try {
|
|
167
201
|
progress.start('Check services and prepare configuration')
|
|
168
202
|
if (values.services === 'docker') {
|
|
169
|
-
await run('docker', ['compose', 'version'], { env, quiet: true })
|
|
170
|
-
await run('docker', ['info'], { env, quiet: true })
|
|
171
203
|
profile = {
|
|
172
204
|
postgres: {
|
|
173
205
|
host: '127.0.0.1',
|
|
@@ -255,7 +287,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
255
287
|
await writeNew(
|
|
256
288
|
target,
|
|
257
289
|
'README.md',
|
|
258
|
-
`# ${name}\n\nCreated with @adula/create-app ${template.version}. Node.js 24+ is required.\n\n## Local development\n\n\`\`\`sh\nnpm run dev\n\`\`\`\n\nOpen ${appEnv.APP_URL}. Administrator credentials are in ignored tmp/dev-admin.txt. After signing in, open /admin/setup and follow docs/initial-setup.md to review identity and verify service readiness.\nRun workers separately with node ace adula:worker, the outbox dispatcher with node ace adula:outbox, and the scheduler with node ace scheduler:run.\n\n${values.services === 'docker' ?
|
|
290
|
+
`# ${name}\n\nCreated with @adula/create-app ${template.version}. Node.js 24+ is required.\n\n## Local development\n\n\`\`\`sh\nnpm run dev\n\`\`\`\n\nOpen ${appEnv.APP_URL}. Administrator credentials are in ignored tmp/dev-admin.txt. After signing in, open /admin/setup and follow docs/initial-setup.md to review identity and verify service readiness.\nRun workers separately with node ace adula:worker, the outbox dispatcher with node ace adula:outbox, and the scheduler with node ace scheduler:run.\n\n${values.services === 'docker' ? `PostgreSQL 17 and Redis 7 run in Docker. Start them with ${docker.display} compose up -d --wait; stop with ${docker.display} compose stop. Named volumes retain data; never use down -v unless deliberately deleting the local databases.` : 'PostgreSQL 17 and Redis use your existing services. Installation created fresh databases; keep .env and .env.test private.'}\n\n## Verification\n\nRun npm run typecheck, npm test, npm run lint and npm run build. Tests use the separate *_test database and a distinct Redis namespace.\n\n## Company identity\n\nRead docs/design-identity.md and the managed design skill before changing UI. Source files and shadcn components belong to this application.\n\n## External services and production\n\nLocal file storage is enabled. SMTP (including a local relay), OAuth provider credentials, S3, off-site backups, and production deployment need actual destination configuration. The creator does not enable unconfigured external services. Production requires backup variables validated in start/env.ts.\n\n## Incomplete installation\n\nReview the reported failing step; files and databases are retained. After resolving it, use npm exec --yes --package=pnpm@11.19.0 -- pnpm install, node ace migration:run, and node ace adula:setup. Setup reads tmp/dev-admin.txt; it refuses to promote an existing account with a different password. Then run node ace adula:doctor and npm run build. Never recreate over a nonempty directory.\n`
|
|
259
291
|
)
|
|
260
292
|
await writeNew(target, 'tmp/install.log', '', true)
|
|
261
293
|
const options = {
|
|
@@ -269,8 +301,18 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
269
301
|
if (values.services === 'docker') {
|
|
270
302
|
await writeNew(target, 'compose.yaml', composeFile())
|
|
271
303
|
await run(
|
|
272
|
-
|
|
273
|
-
[
|
|
304
|
+
docker.command,
|
|
305
|
+
[
|
|
306
|
+
...docker.prefix,
|
|
307
|
+
'compose',
|
|
308
|
+
'--env-file',
|
|
309
|
+
'.env',
|
|
310
|
+
'up',
|
|
311
|
+
'-d',
|
|
312
|
+
'--wait',
|
|
313
|
+
'--wait-timeout',
|
|
314
|
+
'120',
|
|
315
|
+
],
|
|
274
316
|
options
|
|
275
317
|
)
|
|
276
318
|
await pingRedis(profile.redis)
|
|
@@ -324,7 +366,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
324
366
|
if (process.argv[1] && import.meta.url === pathToFileURL(await realpath(process.argv[1])).href)
|
|
325
367
|
main().catch((error) => {
|
|
326
368
|
console.error(
|
|
327
|
-
`\nSetup did not complete: ${error.message}\
|
|
369
|
+
`\nSetup did not complete: ${error.message}\nIf setup created files or databases, they have been retained for inspection. See the generated README for recovery steps.`
|
|
328
370
|
)
|
|
329
371
|
process.exitCode = 1
|
|
330
372
|
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { checkDocker, run } from './system.mjs'
|
|
2
|
+
|
|
3
|
+
/** Never install host software in unattended mode or without explicit consent. */
|
|
4
|
+
export async function prepareDocker({
|
|
5
|
+
question = undefined,
|
|
6
|
+
detect = checkDocker,
|
|
7
|
+
execute = run,
|
|
8
|
+
platform = process.platform,
|
|
9
|
+
} = {}) {
|
|
10
|
+
try {
|
|
11
|
+
return { docker: await detect() }
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (!question) throw error
|
|
14
|
+
if (error.code === 'DOCKER_STOPPED') {
|
|
15
|
+
const answer = await question(
|
|
16
|
+
`${error.message}\nStart the engine, then press Enter to retry (or type cancel): `
|
|
17
|
+
)
|
|
18
|
+
if (answer.trim().toLowerCase() === 'cancel')
|
|
19
|
+
throw new Error('Setup cancelled before project creation.')
|
|
20
|
+
return { docker: await detect() }
|
|
21
|
+
}
|
|
22
|
+
if (error.code !== 'DOCKER_MISSING') throw error
|
|
23
|
+
if (platform === 'win32') {
|
|
24
|
+
const consent = await question(
|
|
25
|
+
'Docker was not found. Install Docker Desktop using winget? Windows may request administrator approval or a restart. Docker license terms remain in the installer. [y/N]: '
|
|
26
|
+
)
|
|
27
|
+
if (/^(y|yes)$/i.test(consent.trim())) {
|
|
28
|
+
try {
|
|
29
|
+
await execute('winget', [
|
|
30
|
+
'install',
|
|
31
|
+
'--id',
|
|
32
|
+
'Docker.DockerDesktop',
|
|
33
|
+
'--exact',
|
|
34
|
+
'--source',
|
|
35
|
+
'winget',
|
|
36
|
+
'--interactive',
|
|
37
|
+
])
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'Docker Desktop installation did not complete. Review the installer output. If Windows requests a restart, restart and rerun the same npm create command. If winget is unavailable, install Docker Desktop from https://docs.docker.com/desktop/setup/install/windows-install/ . No application files or databases have been created.'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
const answer = await question(
|
|
44
|
+
'Open Docker Desktop, complete its setup and wait for the engine. Press Enter when ready, or type restart to exit and rerun this command after restarting Windows: '
|
|
45
|
+
)
|
|
46
|
+
if (answer.trim().toLowerCase() === 'restart')
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Restart Windows, start Docker Desktop, then rerun the same npm create command. No application files or databases have been created.'
|
|
49
|
+
)
|
|
50
|
+
try {
|
|
51
|
+
return { docker: await detect() }
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'Docker is not ready yet. Complete Docker Desktop setup, restart Windows if requested, then rerun the same npm create command. No application files or databases have been created.'
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// Host installation differs by OS/distro; do not guess a privileged script.
|
|
60
|
+
console.log(error.message)
|
|
61
|
+
}
|
|
62
|
+
const connection = (
|
|
63
|
+
await question(
|
|
64
|
+
'Use existing PostgreSQL 17 and Redis instead? Enter the connection JSON path, or press Enter to cancel: '
|
|
65
|
+
)
|
|
66
|
+
).trim()
|
|
67
|
+
if (!connection)
|
|
68
|
+
throw new Error('Setup cancelled before project creation. No software was installed.')
|
|
69
|
+
return { connection }
|
|
70
|
+
}
|
|
71
|
+
}
|
package/build/project.mjs
CHANGED
|
@@ -5,7 +5,7 @@ export function projectName(target) {
|
|
|
5
5
|
const name = basename(resolve(target))
|
|
6
6
|
if (!/^[a-z][a-z0-9-]{0,49}$/.test(name))
|
|
7
7
|
throw new Error(
|
|
8
|
-
'Project directory name must start with a lowercase letter and use lowercase letters, digits or hyphens (up to 50 characters).'
|
|
8
|
+
'Project directory name must start with a lowercase letter and use lowercase letters, digits or hyphens (up to 50 characters). For example, use dental-gate for the directory; the company display name can still be Dental-Gate.'
|
|
9
9
|
)
|
|
10
10
|
return name
|
|
11
11
|
}
|
package/build/system.mjs
CHANGED
|
@@ -41,6 +41,65 @@ export async function run(
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export async function checkDocker(execute = run, platform = process.platform) {
|
|
45
|
+
const alternative =
|
|
46
|
+
'Alternatively, use --services existing --connection ./local.json with PostgreSQL 17 and Redis (see the creator README).'
|
|
47
|
+
let command = 'docker'
|
|
48
|
+
let prefix = []
|
|
49
|
+
try {
|
|
50
|
+
await execute('docker', ['compose', 'version'], { quiet: true })
|
|
51
|
+
} catch {
|
|
52
|
+
if (platform === 'win32') {
|
|
53
|
+
try {
|
|
54
|
+
// A newly installed Docker Desktop may not be in this process's PATH yet.
|
|
55
|
+
command = join(
|
|
56
|
+
process.env.ProgramFiles ?? 'C:\\Program Files',
|
|
57
|
+
'Docker/Docker/resources/bin/docker.exe'
|
|
58
|
+
)
|
|
59
|
+
await execute(command, ['compose', 'version'], { quiet: true })
|
|
60
|
+
} catch {
|
|
61
|
+
try {
|
|
62
|
+
command = 'wsl.exe'
|
|
63
|
+
prefix = ['--exec', 'docker']
|
|
64
|
+
await execute(command, [...prefix, 'compose', 'version'], { quiet: true })
|
|
65
|
+
} catch {
|
|
66
|
+
throw Object.assign(
|
|
67
|
+
new Error(
|
|
68
|
+
'Docker with Compose was not found in PowerShell or the default WSL distribution. Install and start Docker Desktop, or provide Docker with Compose in your default WSL distribution. ' +
|
|
69
|
+
alternative
|
|
70
|
+
),
|
|
71
|
+
{ code: 'DOCKER_MISSING' }
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
throw Object.assign(
|
|
77
|
+
new Error(
|
|
78
|
+
'Docker with Compose is required. Install Docker and Compose, then verify docker compose version. ' +
|
|
79
|
+
alternative
|
|
80
|
+
),
|
|
81
|
+
{ code: 'DOCKER_MISSING' }
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await execute(command, [...prefix, 'info'], { quiet: true })
|
|
87
|
+
} catch {
|
|
88
|
+
throw Object.assign(
|
|
89
|
+
new Error(
|
|
90
|
+
`Docker Compose is available, but the Docker engine is not reachable. Start Docker Desktop or your Docker service and verify ${[command, ...prefix, 'info'].join(' ')} in this terminal. ` +
|
|
91
|
+
alternative
|
|
92
|
+
),
|
|
93
|
+
{ code: 'DOCKER_STOPPED' }
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
command,
|
|
98
|
+
prefix,
|
|
99
|
+
display: [command.includes(' ') ? `& "${command}"` : command, ...prefix].join(' '),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
44
103
|
export async function packageManager() {
|
|
45
104
|
// Keep the installer outside the project whose dependency tree it updates.
|
|
46
105
|
// Direct JavaScript entries also avoid shell interpretation on Windows.
|