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 +20 -14
- package/dist/env.js +33 -0
- package/dist/index.js +73 -25
- package/dist/options.js +48 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# create-aphex
|
|
2
2
|
|
|
3
|
-
Scaffold a new Aphex CMS project
|
|
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
|
-
|
|
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
|
|
27
|
-
3.
|
|
28
|
-
4.
|
|
29
|
-
|
|
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
|
|
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
|
-
-
|
|
45
|
+
- SQLite by default with optional PostgreSQL and Turso
|
|
40
46
|
- S3-compatible storage (`@aphexcms/storage-s3`) with local-filesystem fallback
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
65
|
+
spinner.start(`Fetching ${selectedTemplate.label} template...`);
|
|
44
66
|
try {
|
|
45
|
-
await downloadTemplate(
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/options.js
ADDED
|
@@ -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
|
+
}
|