create-aphex 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # create-aphex
2
2
 
3
- Scaffold a new Aphex CMS project. Copies the base template into a new
4
- directory and rewrites `workspace:*` deps to their published versions.
3
+ Scaffold a new Aphex CMS project from an official standalone template.
5
4
 
6
5
  ## Usage
7
6
 
8
7
  ```bash
9
8
  pnpm create aphex my-app
9
+ pnpm create aphex my-site --template website
10
10
  # or
11
11
  npm create aphex my-app
12
12
  # or
@@ -17,38 +17,44 @@ You can also invoke it through the [`aphx`](https://www.npmjs.com/package/aphx)
17
17
  CLI, which shells out to this package:
18
18
 
19
19
  ```bash
20
- pnpm aphex create
20
+ aphx create my-site --template website
21
21
  ```
22
22
 
23
23
  ## What it does
24
24
 
25
25
  1. Prompts for a project name (or takes the positional argument).
26
- 2. Prompts for a template (currently only `base`).
27
- 3. Copies `templates/base/` into the new directory.
28
- 4. Rewrites `workspace:*` dependencies to their published versions.
29
- 5. Writes a default `.env` with working dev defaults.
26
+ 2. Prompts for an official template when `--template` is omitted.
27
+ 3. Downloads the selected standalone template repository.
28
+ 4. Sets the package name and writes a default `.env` with local development defaults.
29
+
30
+ Official templates are pinned to the matching `create-aphex-vX.Y.Z` repository tag. A given CLI
31
+ version therefore always scaffolds the same snapshot even after the template's `main` branch moves.
30
32
 
31
33
  ## Templates
32
34
 
33
35
  ### base
34
36
 
35
- A full-featured Aphex CMS application:
37
+ A minimal Aphex CMS application for defining your own content model and public site.
38
+
39
+ ### website
40
+
41
+ A content-focused website starter with:
36
42
 
37
43
  - Better Auth (email + password, email verification, password reset)
38
44
  - Organizations with parent/child hierarchy
39
- - PostgreSQL + Drizzle ORM with RLS policies
45
+ - SQLite by default with optional PostgreSQL and Turso
40
46
  - S3-compatible storage (`@aphexcms/storage-s3`) with local-filesystem fallback
41
- - Mailpit in dev / Resend in prod
42
- - Auto-generated GraphQL API
43
- - In-memory cache adapter for published-perspective reads
47
+ - Page builder, posts, categories, site navigation, and SEO
48
+
49
+ Choose non-interactively with `--template base` or `--template website`. Set
50
+ `APHEX_TEMPLATE` to a complete [giget](https://github.com/unjs/giget) source such
51
+ as `github:owner/repo#branch` when testing a custom template repository.
44
52
 
45
53
  ## After scaffolding
46
54
 
47
55
  ```bash
48
56
  cd my-app
49
57
  pnpm install
50
- pnpm db:start # Start PostgreSQL + Mailpit via Docker
51
- pnpm db:push # Apply the schema (dev)
52
58
  pnpm dev # http://localhost:5173
53
59
  ```
54
60
 
package/dist/env.js ADDED
@@ -0,0 +1,33 @@
1
+ import { randomBytes } from 'crypto';
2
+ /**
3
+ * Names the app accepts for the session-signing secret. `AUTH_SECRET` is the
4
+ * current one; `BETTER_AUTH_SECRET` is still read for backwards compatibility and
5
+ * is what older published templates ship, so a scaffold has to handle whichever
6
+ * the fetched template happens to use — `create-aphex` downloads the template
7
+ * from its own repo at run time, so the two versions are independent.
8
+ */
9
+ const AUTH_SECRET_KEYS = ['AUTH_SECRET', 'BETTER_AUTH_SECRET'];
10
+ /**
11
+ * Values that mean "nobody has set this yet" — empty, or one of the placeholder
12
+ * strings the templates ship. Anything else is left alone: a real value sitting
13
+ * in an example file is not ours to overwrite.
14
+ */
15
+ function isUnsetSecret(value) {
16
+ const trimmed = value.trim().replace(/^["']|["']$/g, '');
17
+ if (trimmed === '')
18
+ return true;
19
+ return /change[-_ ]?(in[-_ ]?production|me)|your[-_ ]?secret|secret[-_ ]?key[-_ ]?here/i.test(trimmed);
20
+ }
21
+ /**
22
+ * Replace unset auth-secret values in a `.env` body with a freshly generated one.
23
+ *
24
+ * Lives in its own module rather than `index.ts` because that file runs the CLI
25
+ * on import — a test importing the helper from there would launch the prompt.
26
+ */
27
+ export function withGeneratedAuthSecret(envBody) {
28
+ let result = envBody;
29
+ for (const key of AUTH_SECRET_KEYS) {
30
+ result = result.replace(new RegExp(`^(${key}=)(.*)$`, 'gm'), (line, prefix, value) => isUnsetSecret(value) ? `${prefix}${randomBytes(32).toString('base64url')}` : line);
31
+ }
32
+ return result;
33
+ }
package/dist/index.js CHANGED
@@ -1,34 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
  import * as p from '@clack/prompts';
3
- import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
3
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
4
4
  import { join, resolve } from 'path';
5
5
  import pc from 'picocolors';
6
6
  import { downloadTemplate } from 'giget';
7
- const TEMPLATE_REPO = process.env.APHEX_TEMPLATE || 'github:IcelandicIcecream/aphex-base';
7
+ import { parseArgs, templates } from './options.js';
8
+ import { withGeneratedAuthSecret } from './env.js';
8
9
  async function main() {
10
+ const cliOptions = parseArgs(process.argv.slice(2));
11
+ if (cliOptions.help) {
12
+ printHelp();
13
+ return;
14
+ }
9
15
  console.clear();
10
16
  p.intro(pc.bgCyan(pc.black(' create-aphex ')));
11
17
  const options = {
12
18
  projectName: '',
13
- targetDir: ''
19
+ targetDir: '',
20
+ template: cliOptions.template ?? 'base'
14
21
  };
15
- const projectName = await p.text({
16
- message: 'What is your project name?',
17
- placeholder: 'my-aphex-project',
18
- validate: (value) => {
19
- if (!value)
20
- return 'Project name is required';
21
- if (!/^[a-z0-9-]+$/.test(value)) {
22
- return 'Project name can only contain lowercase letters, numbers, and hyphens';
23
- }
24
- }
25
- });
22
+ const projectName = cliOptions.projectName ??
23
+ (await p.text({
24
+ message: 'What is your project name?',
25
+ placeholder: 'my-aphex-project',
26
+ validate: validateProjectName
27
+ }));
26
28
  if (p.isCancel(projectName)) {
27
29
  p.cancel('Operation cancelled');
28
30
  process.exit(0);
29
31
  }
30
32
  options.projectName = projectName;
31
33
  options.targetDir = resolve(process.cwd(), options.projectName);
34
+ const projectNameError = validateProjectName(options.projectName);
35
+ if (projectNameError)
36
+ throw new Error(projectNameError);
37
+ if (!cliOptions.template && !process.env.APHEX_TEMPLATE) {
38
+ const template = await p.select({
39
+ message: 'Which template would you like?',
40
+ options: Object.entries(templates).map(([value, template]) => ({
41
+ value: value,
42
+ label: template.label,
43
+ hint: template.description
44
+ }))
45
+ });
46
+ if (p.isCancel(template)) {
47
+ p.cancel('Operation cancelled');
48
+ process.exit(0);
49
+ }
50
+ options.template = template;
51
+ }
32
52
  if (existsSync(options.targetDir)) {
33
53
  const shouldOverwrite = await p.confirm({
34
54
  message: `Directory ${pc.cyan(options.projectName)} already exists. Overwrite?`,
@@ -39,10 +59,12 @@ async function main() {
39
59
  process.exit(0);
40
60
  }
41
61
  }
62
+ const selectedTemplate = templates[options.template];
63
+ const templateSource = process.env.APHEX_TEMPLATE || selectedTemplate.source;
42
64
  const spinner = p.spinner();
43
- spinner.start(`Fetching template from ${TEMPLATE_REPO}...`);
65
+ spinner.start(`Fetching ${selectedTemplate.label} template...`);
44
66
  try {
45
- await downloadTemplate(TEMPLATE_REPO, {
67
+ await downloadTemplate(templateSource, {
46
68
  dir: options.targetDir,
47
69
  force: true
48
70
  });
@@ -50,22 +72,27 @@ async function main() {
50
72
  if (existsSync(packageJsonPath)) {
51
73
  const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
52
74
  packageJson.name = options.projectName;
53
- delete packageJson.private;
75
+ // `private: true` stays. A scaffolded project is an application, not a
76
+ // package to publish, and dropping the flag turns a stray `npm publish`
77
+ // in the project root from a refusal into an upload. Nothing about local
78
+ // development needs it gone.
79
+ packageJson.private = true;
54
80
  writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, '\t') + '\n');
55
81
  }
56
82
  const envPath = join(options.targetDir, '.env');
57
83
  const envExamplePath = join(options.targetDir, '.env.example');
58
84
  if (!existsSync(envPath) && existsSync(envExamplePath)) {
59
- copyFileSync(envExamplePath, envPath);
85
+ // Copy `.env.example`, then fill in the auth secret. The example ships it
86
+ // unusable on purpose — empty in current templates, a
87
+ // `your-secret-key-here-change-in-production` placeholder in older ones —
88
+ // so a straight copy leaves the very first `pnpm dev` after scaffolding
89
+ // failing on a missing secret, which is the worst possible moment for it.
90
+ // Generating here is safe: the value is per-project, never leaves the
91
+ // machine, and `.env` is gitignored.
92
+ writeFileSync(envPath, withGeneratedAuthSecret(readFileSync(envExamplePath, 'utf-8')));
60
93
  }
61
94
  spinner.stop('Project created successfully!');
62
- const nextSteps = [
63
- `cd ${options.projectName}`,
64
- 'pnpm install',
65
- 'pnpm db:start',
66
- 'pnpm db:push',
67
- 'pnpm dev'
68
- ];
95
+ const nextSteps = [`cd ${options.projectName}`, 'pnpm install', 'pnpm dev'];
69
96
  p.note(nextSteps.join('\n'), 'Next steps');
70
97
  p.outro(pc.green('Your Aphex CMS project is ready! Check out the README.md for more information.'));
71
98
  }
@@ -75,6 +102,27 @@ async function main() {
75
102
  process.exit(1);
76
103
  }
77
104
  }
105
+ function validateProjectName(value) {
106
+ if (!value)
107
+ return 'Project name is required';
108
+ if (!/^[a-z0-9-]+$/.test(value)) {
109
+ return 'Project name can only contain lowercase letters, numbers, and hyphens';
110
+ }
111
+ }
112
+ function printHelp() {
113
+ console.log(`create-aphex
114
+
115
+ Usage:
116
+ create-aphex [project-name] [options]
117
+
118
+ Options:
119
+ -t, --template <name> Template to use: base or website
120
+ -h, --help Show this help message
121
+
122
+ Examples:
123
+ pnpm create aphex my-site
124
+ pnpm create aphex my-site --template website`);
125
+ }
78
126
  main().catch((error) => {
79
127
  console.error('Error:', error);
80
128
  process.exit(1);
@@ -0,0 +1,48 @@
1
+ import { readFileSync } from 'node:fs';
2
+ const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
3
+ const templateTag = `create-aphex-v${packageVersion.version}`;
4
+ export const templates = {
5
+ base: {
6
+ label: 'Base',
7
+ description: 'Minimal starter for building your own content model',
8
+ source: `github:IcelandicIcecream/aphex-base#${templateTag}`
9
+ },
10
+ website: {
11
+ label: 'Website',
12
+ description: 'Page builder, blog, navigation, and SEO',
13
+ source: `github:IcelandicIcecream/aphex-website#${templateTag}`
14
+ }
15
+ };
16
+ export function parseArgs(args) {
17
+ const options = { help: false };
18
+ for (let index = 0; index < args.length; index += 1) {
19
+ const argument = args[index];
20
+ if (argument === '--help' || argument === '-h') {
21
+ options.help = true;
22
+ continue;
23
+ }
24
+ if (argument === '--template' || argument === '-t') {
25
+ const value = args[index + 1];
26
+ if (!value || value.startsWith('-'))
27
+ throw new Error(`${argument} requires a template name`);
28
+ options.template = parseTemplate(value);
29
+ index += 1;
30
+ continue;
31
+ }
32
+ if (argument.startsWith('--template=')) {
33
+ options.template = parseTemplate(argument.slice('--template='.length));
34
+ continue;
35
+ }
36
+ if (argument.startsWith('-'))
37
+ throw new Error(`Unknown option: ${argument}`);
38
+ if (options.projectName)
39
+ throw new Error(`Unexpected argument: ${argument}`);
40
+ options.projectName = argument;
41
+ }
42
+ return options;
43
+ }
44
+ function parseTemplate(value) {
45
+ if (value in templates)
46
+ return value;
47
+ throw new Error(`Unknown template "${value}". Available templates: ${Object.keys(templates).join(', ')}`);
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-aphex",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffold a new Aphex CMS project — invoked via `pnpm create aphex`",
5
5
  "publishConfig": {
6
6
  "provenance": true