@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 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.1 is **unreleased**; registry commands become available after publication.
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
- Before publication, run the packed CLI with npm exec and `--packages` pointing
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' ? 'PostgreSQL 17 and Redis 7 run in Docker. Start them with docker compose up -d --wait; stop with docker 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`
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
- 'docker',
273
- ['compose', '--env-file', '.env', 'up', '-d', '--wait', '--wait-timeout', '120'],
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}\nCreated files and databases are retained so you can inspect and resume setup.`
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.