create-acmekit-app 2.13.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/.turbo/turbo-build.log +1 -0
- package/CHANGELOG.md +3 -0
- package/README.md +58 -0
- package/dist/commands/create.js +8 -0
- package/dist/index.js +31 -0
- package/dist/utils/clone-repo.js +77 -0
- package/dist/utils/create-abort-controller.js +11 -0
- package/dist/utils/create-db.js +172 -0
- package/dist/utils/execute.js +51 -0
- package/dist/utils/facts.js +87 -0
- package/dist/utils/format-connection-string.js +17 -0
- package/dist/utils/get-config-store.js +10 -0
- package/dist/utils/get-current-os.js +10 -0
- package/dist/utils/log-message.js +21 -0
- package/dist/utils/logger.js +8 -0
- package/dist/utils/nextjs-utils.js +85 -0
- package/dist/utils/node-version.js +5 -0
- package/dist/utils/package-manager.js +266 -0
- package/dist/utils/postgres-client.js +9 -0
- package/dist/utils/prepare-project.js +184 -0
- package/dist/utils/process-manager.js +50 -0
- package/dist/utils/project-creator/acmekit-plugin-creator.js +93 -0
- package/dist/utils/project-creator/acmekit-project-creator.js +204 -0
- package/dist/utils/project-creator/creator.js +46 -0
- package/dist/utils/project-creator/index.js +4 -0
- package/dist/utils/project-creator/project-creator-factory.js +83 -0
- package/dist/utils/start-acmekit.js +13 -0
- package/dist/utils/update-package-versions.js +28 -0
- package/jest.config.cjs +14 -0
- package/package.json +48 -0
- package/src/commands/create.ts +12 -0
- package/src/index.ts +65 -0
- package/src/types.d.ts +1 -0
- package/src/utils/__tests__/create-abort-controller.test.ts +166 -0
- package/src/utils/__tests__/package-manager.test.ts +637 -0
- package/src/utils/clone-repo.ts +117 -0
- package/src/utils/create-abort-controller.ts +16 -0
- package/src/utils/create-db.ts +245 -0
- package/src/utils/execute.ts +86 -0
- package/src/utils/facts.ts +148 -0
- package/src/utils/format-connection-string.ts +29 -0
- package/src/utils/get-config-store.ts +17 -0
- package/src/utils/get-current-os.ts +10 -0
- package/src/utils/log-message.ts +28 -0
- package/src/utils/logger.ts +10 -0
- package/src/utils/nextjs-utils.ts +139 -0
- package/src/utils/node-version.ts +7 -0
- package/src/utils/package-manager.ts +334 -0
- package/src/utils/postgres-client.ts +23 -0
- package/src/utils/prepare-project.ts +325 -0
- package/src/utils/process-manager.ts +60 -0
- package/src/utils/project-creator/acmekit-plugin-creator.ts +127 -0
- package/src/utils/project-creator/acmekit-project-creator.ts +272 -0
- package/src/utils/project-creator/creator.ts +77 -0
- package/src/utils/project-creator/index.ts +4 -0
- package/src/utils/project-creator/project-creator-factory.ts +119 -0
- package/src/utils/start-acmekit.ts +26 -0
- package/src/utils/update-package-versions.ts +37 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://www.acmekit.com">
|
|
3
|
+
<img alt="AcmeKit" src="https://user-images.githubusercontent.com/7554214/153162406-bf8fd16f-aa98-4604-b87b-e13ab4baf604.png" width="100" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
<h1 align="center">
|
|
7
|
+
create-acmekit-app
|
|
8
|
+
</h1>
|
|
9
|
+
|
|
10
|
+
<h4 align="center">
|
|
11
|
+
<a href="https://docs.acmekit.com">Documentation</a> |
|
|
12
|
+
<a href="https://www.acmekit.com">Website</a>
|
|
13
|
+
</h4>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
An open source composable commerce engine built for developers.
|
|
17
|
+
</p>
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://github.com/acmekit/acmekit/blob/master/LICENSE">
|
|
20
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="AcmeKit is released under the MIT license." />
|
|
21
|
+
</a>
|
|
22
|
+
<a href="https://circleci.com/gh/acmekit/acmekit">
|
|
23
|
+
<img src="https://circleci.com/gh/acmekit/acmekit.svg?style=shield" alt="Current CircleCI build status." />
|
|
24
|
+
</a>
|
|
25
|
+
<a href="https://github.com/acmekit/acmekit/blob/master/CONTRIBUTING.md">
|
|
26
|
+
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" />
|
|
27
|
+
</a>
|
|
28
|
+
<a href="https://www.producthunt.com/posts/acmekit"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Day-%23DA552E" alt="Product Hunt"></a>
|
|
29
|
+
<a href="https://discord.gg/xpCwq3Kfn8">
|
|
30
|
+
<img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
|
|
31
|
+
</a>
|
|
32
|
+
<a href="https://twitter.com/intent/follow?screen_name=acmekit">
|
|
33
|
+
<img src="https://img.shields.io/twitter/follow/acmekit.svg?label=Follow%20@acmekit" alt="Follow @acmekit" />
|
|
34
|
+
</a>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
## Overview
|
|
38
|
+
|
|
39
|
+
Using this NPX command, you can setup a AcmeKit backend and admin along with a PostgreSQL database in simple steps.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
Run the following command in your terminal:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx create-acmekit-app@latest
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then, answer the prompted questions to setup your PostgreSQL database and AcmeKit project. Once the setup is done, the AcmeKit admin dashboard will open in your default browser.
|
|
52
|
+
|
|
53
|
+
### Options
|
|
54
|
+
|
|
55
|
+
| Option | Description | Default value |
|
|
56
|
+
|--------------------|-------------------------------------------------------|------------------------------------------------------|
|
|
57
|
+
| `--repo-url <url>` | Create AcmeKit project from a different repository URL | `https://github.com/acmekit/acmekit-starter-default` |
|
|
58
|
+
| `--seed` | Using this option seeds the database with demo data | false |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ProjectCreatorFactory, } from "../utils/project-creator/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Command handler to create a AcmeKit project or plugin
|
|
4
|
+
*/
|
|
5
|
+
export default async (args, options) => {
|
|
6
|
+
const projectCreator = await ProjectCreatorFactory.create(args, options);
|
|
7
|
+
await projectCreator.create();
|
|
8
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Option, program } from "commander";
|
|
3
|
+
import create from "./commands/create.js";
|
|
4
|
+
program
|
|
5
|
+
.description("Create a new AcmeKit project or plugin")
|
|
6
|
+
.argument("[project-name]", "Name of the project to create.")
|
|
7
|
+
.option("--plugin", "Create a plugin instead of a project.")
|
|
8
|
+
.option("--repo-url <url>", "URL of repository to use to setup project.")
|
|
9
|
+
.option("--version <version>", "The version of AcmeKit packages to install.")
|
|
10
|
+
.option("--seed", "Seed the created database with demo data.")
|
|
11
|
+
.option("--skip-db", "Skips creating the database, running migrations, and seeding, and subsequently skips opening the browser.", false)
|
|
12
|
+
.option("--db-url <url>", "Skips database creation and sets the database URL to the provided URL. Throws an error if can't connect to the database. Will still run migrations and open the admin after project creation.")
|
|
13
|
+
.option("--no-migrations", "Skips running migrations, creating admin user, and seeding. If used, it's expected that you pass the --db-url option with a url of a database that has all necessary migrations. Otherwise, unexpected errors will occur.", true)
|
|
14
|
+
.option("--no-browser", "Disables opening the browser at the end of the project creation and only shows success message.", true)
|
|
15
|
+
.option("--directory-path <path>", "Specify the directory path to install the project in.")
|
|
16
|
+
.option("--with-nextjs-starter", "Install the Next.js starter along with the AcmeKit backend", false)
|
|
17
|
+
.option("--verbose", "Show all logs of underlying commands. Useful for debugging.", false)
|
|
18
|
+
.addOption(new Option("--use-npm", "Use npm as the package manager").conflicts([
|
|
19
|
+
"usePnpm",
|
|
20
|
+
"useYarn",
|
|
21
|
+
]))
|
|
22
|
+
.addOption(new Option("--use-yarn", "Use yarn as the package manager").conflicts([
|
|
23
|
+
"useNpm",
|
|
24
|
+
"usePnpm",
|
|
25
|
+
]))
|
|
26
|
+
.addOption(new Option("--use-pnpm", "Use pnpm as the package manager").conflicts([
|
|
27
|
+
"useNpm",
|
|
28
|
+
"useYarn",
|
|
29
|
+
]))
|
|
30
|
+
.parse();
|
|
31
|
+
void create(program.args, program.opts());
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { isAbortError } from "./create-abort-controller.js";
|
|
4
|
+
import execute from "./execute.js";
|
|
5
|
+
import logMessage from "./log-message.js";
|
|
6
|
+
import { execFileSync } from "child_process";
|
|
7
|
+
const DEFAULT_REPO = "https://github.com/acmekit/acmekit-starter-default";
|
|
8
|
+
const DEFAULT_PLUGIN_REPO = "https://github.com/acmekit/acmekit-starter-plugin";
|
|
9
|
+
const BRANCH = "master";
|
|
10
|
+
const PLUGIN_BRANCH = "main";
|
|
11
|
+
export default async function cloneRepo({ directoryName = "", repoUrl, abortController, verbose = false, isPlugin = false, }) {
|
|
12
|
+
const defaultRepo = isPlugin ? DEFAULT_PLUGIN_REPO : DEFAULT_REPO;
|
|
13
|
+
const branch = isPlugin ? PLUGIN_BRANCH : BRANCH;
|
|
14
|
+
await execute([
|
|
15
|
+
`git clone ${repoUrl || defaultRepo} -b ${branch} ${directoryName} --depth 1`,
|
|
16
|
+
{
|
|
17
|
+
signal: abortController?.signal,
|
|
18
|
+
},
|
|
19
|
+
], { verbose });
|
|
20
|
+
}
|
|
21
|
+
export async function runCloneRepo({ projectName, repoUrl, abortController, spinner, verbose = false, isPlugin = false, }) {
|
|
22
|
+
try {
|
|
23
|
+
await cloneRepo({
|
|
24
|
+
directoryName: projectName,
|
|
25
|
+
repoUrl,
|
|
26
|
+
abortController,
|
|
27
|
+
verbose,
|
|
28
|
+
isPlugin,
|
|
29
|
+
});
|
|
30
|
+
deleteGitDirectory(projectName);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (isAbortError(e)) {
|
|
34
|
+
process.exit();
|
|
35
|
+
}
|
|
36
|
+
spinner.stop();
|
|
37
|
+
logMessage({
|
|
38
|
+
message: `An error occurred while setting up your project: ${e}`,
|
|
39
|
+
type: "error",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function deleteGitDirectory(projectDirectory) {
|
|
44
|
+
try {
|
|
45
|
+
fs.rmSync(path.join(projectDirectory, ".git"), {
|
|
46
|
+
recursive: true,
|
|
47
|
+
force: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
deleteWithCommand(projectDirectory, ".git");
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
fs.rmSync(path.join(projectDirectory, ".github"), {
|
|
55
|
+
recursive: true,
|
|
56
|
+
force: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
deleteWithCommand(projectDirectory, ".github");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Useful for deleting directories when fs methods fail (e.g., with Yarn v3)
|
|
65
|
+
*/
|
|
66
|
+
function deleteWithCommand(projectDirectory, dirName) {
|
|
67
|
+
const dirPath = path.normalize(path.join(projectDirectory, dirName));
|
|
68
|
+
if (!fs.existsSync(dirPath)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === "win32") {
|
|
72
|
+
execFileSync("cmd", ["/c", "rmdir", "/s", "/q", dirPath]);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
execFileSync("rm", ["-rf", dirPath]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default (processManager) => {
|
|
2
|
+
const abortController = new AbortController();
|
|
3
|
+
processManager.onTerminated(() => abortController.abort());
|
|
4
|
+
return abortController;
|
|
5
|
+
};
|
|
6
|
+
export const isAbortError = (e) => e !== null && typeof e === "object" && "code" in e && e.code === "ABORT_ERR";
|
|
7
|
+
export const getAbortError = () => {
|
|
8
|
+
return {
|
|
9
|
+
code: "ABORT_ERR",
|
|
10
|
+
};
|
|
11
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { EOL } from "os";
|
|
2
|
+
import postgresClient, { DEFAULT_HOST, DEFAULT_PORT, } from "./postgres-client.js";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import logMessage from "./log-message.js";
|
|
5
|
+
import formatConnectionString from "./format-connection-string.js";
|
|
6
|
+
import terminalLink from "terminal-link";
|
|
7
|
+
export default async function createDb({ client, db }) {
|
|
8
|
+
await client.query(`CREATE DATABASE "${db}"`);
|
|
9
|
+
}
|
|
10
|
+
async function doesDbExist(client, dbName) {
|
|
11
|
+
const result = await client.query(`SELECT datname FROM pg_catalog.pg_database WHERE datname='${dbName}';`);
|
|
12
|
+
return !!result.rowCount;
|
|
13
|
+
}
|
|
14
|
+
export async function runCreateDb({ client, dbName, spinner, }) {
|
|
15
|
+
let newClient = client;
|
|
16
|
+
try {
|
|
17
|
+
// create postgres database
|
|
18
|
+
await createDb({
|
|
19
|
+
client,
|
|
20
|
+
db: dbName,
|
|
21
|
+
});
|
|
22
|
+
// create a new connection with database selected
|
|
23
|
+
await client.end();
|
|
24
|
+
newClient = await postgresClient({
|
|
25
|
+
user: client.user,
|
|
26
|
+
password: client.password,
|
|
27
|
+
database: dbName,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
spinner.stop();
|
|
32
|
+
logMessage({
|
|
33
|
+
message: `An error occurred while trying to create your database: ${e}`,
|
|
34
|
+
type: "error",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return newClient;
|
|
38
|
+
}
|
|
39
|
+
async function getForDbName({ dbName, verbose = false, }) {
|
|
40
|
+
let client;
|
|
41
|
+
let postgresUsername = "postgres";
|
|
42
|
+
let postgresPassword = "";
|
|
43
|
+
const defaultConnectionOptions = {
|
|
44
|
+
host: DEFAULT_HOST,
|
|
45
|
+
port: DEFAULT_PORT,
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
client = await postgresClient({
|
|
49
|
+
user: postgresUsername,
|
|
50
|
+
password: postgresPassword,
|
|
51
|
+
...defaultConnectionOptions,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
if (verbose) {
|
|
56
|
+
logMessage({
|
|
57
|
+
message: `The following error occured when connecting to the database: ${e}`,
|
|
58
|
+
type: "verbose",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// ask for the user's postgres credentials
|
|
62
|
+
const answers = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: "input",
|
|
65
|
+
name: "postgresUsername",
|
|
66
|
+
message: "Enter your Postgres username",
|
|
67
|
+
default: "postgres",
|
|
68
|
+
validate: (input) => {
|
|
69
|
+
return typeof input === "string" && input.length > 0;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: "password",
|
|
74
|
+
name: "postgresPassword",
|
|
75
|
+
message: "Enter your Postgres password",
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
postgresUsername = answers.postgresUsername;
|
|
79
|
+
postgresPassword = answers.postgresPassword;
|
|
80
|
+
const { userDbName } = await inquirer.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: "database",
|
|
83
|
+
name: "userDbName",
|
|
84
|
+
message: "Enter your Postgres user's database name",
|
|
85
|
+
default: answers.postgresUsername,
|
|
86
|
+
validate: (input) => {
|
|
87
|
+
return typeof input === "string" && input.length > 0;
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
try {
|
|
92
|
+
client = await postgresClient({
|
|
93
|
+
user: postgresUsername,
|
|
94
|
+
password: postgresPassword,
|
|
95
|
+
database: userDbName,
|
|
96
|
+
...defaultConnectionOptions,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
logMessage({
|
|
101
|
+
message: `Couldn't connect to PostgreSQL because of the following error: ${e}.${EOL}${EOL}Make sure you have PostgreSQL installed and the credentials you provided are correct.${EOL}${EOL}If you keep running into this issue despite having PostgreSQL installed, please check out our ${terminalLink("troubleshooting guides", "https://docs.acmekit.com/resources/troubleshooting/database-errors")}.`,
|
|
102
|
+
type: "error",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// check if database exists
|
|
107
|
+
if (await doesDbExist(client, dbName)) {
|
|
108
|
+
const { newDbName } = await inquirer.prompt([
|
|
109
|
+
{
|
|
110
|
+
type: "input",
|
|
111
|
+
name: "newDbName",
|
|
112
|
+
message: `A database already exists with the name ${dbName}, please enter a name for the database:`,
|
|
113
|
+
default: dbName,
|
|
114
|
+
validate: (input) => {
|
|
115
|
+
return (typeof input === "string" && input.length > 0 && input !== dbName);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
]);
|
|
119
|
+
dbName = newDbName;
|
|
120
|
+
}
|
|
121
|
+
// format connection string
|
|
122
|
+
const dbConnectionString = formatConnectionString({
|
|
123
|
+
user: postgresUsername,
|
|
124
|
+
password: postgresPassword,
|
|
125
|
+
host: client.host,
|
|
126
|
+
db: dbName,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
client,
|
|
130
|
+
dbConnectionString,
|
|
131
|
+
dbName,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function getForDbUrl({ dbUrl, verbose = false, }) {
|
|
135
|
+
let client;
|
|
136
|
+
try {
|
|
137
|
+
client = await postgresClient({
|
|
138
|
+
connectionString: dbUrl,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
if (verbose) {
|
|
143
|
+
logMessage({
|
|
144
|
+
message: `The following error occured when connecting to the database: ${e}`,
|
|
145
|
+
type: "verbose",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
logMessage({
|
|
149
|
+
message: `Couldn't connect to PostgreSQL using the database URL you passed. Make sure it's correct and try again.`,
|
|
150
|
+
type: "error",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
client,
|
|
155
|
+
dbConnectionString: dbUrl,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export async function getDbClientAndCredentials({ dbName = "", dbUrl = "", verbose = false, }) {
|
|
159
|
+
// Check the db-url first, because the dbName is always defined in AcmeKitProjectCreator->create()->initializeProject()->setupDatabase()
|
|
160
|
+
if (dbUrl) {
|
|
161
|
+
return await getForDbUrl({
|
|
162
|
+
dbUrl,
|
|
163
|
+
verbose,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
return await getForDbName({
|
|
168
|
+
dbName,
|
|
169
|
+
verbose,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { exec, spawnSync } from "child_process";
|
|
2
|
+
import util from "util";
|
|
3
|
+
import { getAbortError } from "./create-abort-controller.js";
|
|
4
|
+
const promiseExec = util.promisify(exec);
|
|
5
|
+
const execute = async (command, { verbose = false, needOutput = false }) => {
|
|
6
|
+
if (verbose) {
|
|
7
|
+
const [commandStr, options] = command;
|
|
8
|
+
const childProcess = spawnSync(commandStr, {
|
|
9
|
+
...options,
|
|
10
|
+
shell: true,
|
|
11
|
+
stdio: needOutput
|
|
12
|
+
? "pipe"
|
|
13
|
+
: [process.stdin, process.stdout, process.stderr],
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
...(options.env || {}),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (childProcess.error || childProcess.status !== 0) {
|
|
20
|
+
throw (childProcess.error ||
|
|
21
|
+
childProcess.stderr?.toString() ||
|
|
22
|
+
`${commandStr} failed with status ${childProcess.status}`);
|
|
23
|
+
}
|
|
24
|
+
if (childProcess.signal &&
|
|
25
|
+
["SIGINT", "SIGTERM"].includes(childProcess.signal)) {
|
|
26
|
+
throw getAbortError();
|
|
27
|
+
}
|
|
28
|
+
if (needOutput) {
|
|
29
|
+
console.log(childProcess.stdout?.toString() || childProcess.stderr?.toString());
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
stdout: childProcess.stdout?.toString() || "",
|
|
33
|
+
stderr: childProcess.stderr?.toString() || "",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const [commandStr, options] = command;
|
|
38
|
+
const childProcess = await promiseExec(commandStr, {
|
|
39
|
+
...options,
|
|
40
|
+
env: {
|
|
41
|
+
...process.env,
|
|
42
|
+
...(options?.env || {}),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
stdout: childProcess.stdout,
|
|
47
|
+
stderr: childProcess.stderr,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export default execute;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import boxen from "boxen";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { emojify } from "node-emoji";
|
|
4
|
+
import terminalLink from "terminal-link";
|
|
5
|
+
const facts = [
|
|
6
|
+
"Specify a product's availability in one or more sales channels.",
|
|
7
|
+
"Payment providers can be configured per region.",
|
|
8
|
+
"Tax-inclusive pricing allows you to set prices for products and shipping options while delegating tax calculations to AcmeKit.",
|
|
9
|
+
"AcmeKit provides multi-currency and region support, with full control over prices for each currency and region.",
|
|
10
|
+
"Organize customers by customer groups and set special prices for them.",
|
|
11
|
+
"Specify the inventory of products per location and sales channel.",
|
|
12
|
+
"Publishable-API Keys allow you to send scoped requests to the server's store API routes.",
|
|
13
|
+
"API Routes expose business logic to clients, such as storefronts and admin customizations.",
|
|
14
|
+
"Subscribers are asynchronous functions that are executed when an event is emitted.",
|
|
15
|
+
"Data models represent tables in the database. They are created using AcmeKit's Data Modeling Language (DML).",
|
|
16
|
+
"AcmeKit's store API routes are prefixed by /store. The admin API routes are prefixed by /admin.",
|
|
17
|
+
"The JS SDK allows you to send requests to the AcmeKit server from your storefront or admin customizations.",
|
|
18
|
+
"Modules are reusable packages of functionalities related to a single commerce domain or integration.",
|
|
19
|
+
"Modules have a main service that provides data-management and integration functionalities.",
|
|
20
|
+
"Modules allow you to replace an entire functionality with your custom logic.",
|
|
21
|
+
"Infrastructure Modules are interchangeable modules that implement features and integrations related to the AcmeKit server's infrastructure.",
|
|
22
|
+
"Commerce Modules are built-in modules that provide core commerce logic specific to domains like Product, Cart and Order.",
|
|
23
|
+
"Workflows are a series of queries and actions, called steps, that complete a task.",
|
|
24
|
+
"A workflow's steps can be retried or rolled back in case of an error.",
|
|
25
|
+
`AcmeKit provides ${terminalLink("Claude Code plugins", "https://github.com/acmekit/acmekit-claude-plugins")} to facilitate your development.`,
|
|
26
|
+
"AcmeKit provides an MCP server at https://docs.acmekit.com/mcp to support your learning and development experience with AI agents",
|
|
27
|
+
`AcmeKit is optimized to build custom commerce software with AI agents through its MCP server and ${terminalLink("Claude Code plugins", "https://github.com/acmekit/acmekit-claude-plugins")}.`,
|
|
28
|
+
];
|
|
29
|
+
export const getFact = () => {
|
|
30
|
+
const randIndex = Math.floor(Math.random() * facts.length);
|
|
31
|
+
return facts[randIndex];
|
|
32
|
+
};
|
|
33
|
+
export const showFact = ({ spinner, title, verbose, }) => {
|
|
34
|
+
const fact = getFact();
|
|
35
|
+
if (verbose) {
|
|
36
|
+
spinner.stopAndPersist({
|
|
37
|
+
symbol: chalk.cyan("⠋"),
|
|
38
|
+
text: title,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
spinner.text = `${title}\n${boxen(`${fact}`, {
|
|
43
|
+
title: chalk.cyan(`${emojify(":bulb:")} AcmeKit Tips`),
|
|
44
|
+
titleAlignment: "center",
|
|
45
|
+
textAlignment: "center",
|
|
46
|
+
padding: 1,
|
|
47
|
+
margin: 1,
|
|
48
|
+
})}`;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export const createFactBox = ({ spinner, title, processManager, verbose, }) => {
|
|
52
|
+
showFact({ spinner, title, verbose });
|
|
53
|
+
const interval = setInterval(() => {
|
|
54
|
+
showFact({ spinner, title, verbose });
|
|
55
|
+
}, 10000);
|
|
56
|
+
processManager.addInterval(interval);
|
|
57
|
+
return interval;
|
|
58
|
+
};
|
|
59
|
+
export const resetFactBox = ({ interval, spinner, successMessage, processManager, newTitle, verbose, }) => {
|
|
60
|
+
if (interval) {
|
|
61
|
+
clearInterval(interval);
|
|
62
|
+
}
|
|
63
|
+
spinner.succeed(chalk.green(successMessage)).start();
|
|
64
|
+
let newInterval = null;
|
|
65
|
+
if (newTitle) {
|
|
66
|
+
newInterval = createFactBox({
|
|
67
|
+
spinner,
|
|
68
|
+
title: newTitle,
|
|
69
|
+
processManager,
|
|
70
|
+
verbose,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return newInterval;
|
|
74
|
+
};
|
|
75
|
+
export function displayFactBox({ interval, spinner, processManager, title = "", message = "", verbose = false, }) {
|
|
76
|
+
if (!message) {
|
|
77
|
+
return createFactBox({ spinner, title, processManager, verbose });
|
|
78
|
+
}
|
|
79
|
+
return resetFactBox({
|
|
80
|
+
interval,
|
|
81
|
+
spinner,
|
|
82
|
+
successMessage: message,
|
|
83
|
+
processManager,
|
|
84
|
+
newTitle: title,
|
|
85
|
+
verbose,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function encodeDbValue(value) {
|
|
2
|
+
return encodeURIComponent(value);
|
|
3
|
+
}
|
|
4
|
+
export default ({ user, password, host, db }) => {
|
|
5
|
+
let connection = `postgres://`;
|
|
6
|
+
if (user) {
|
|
7
|
+
connection += encodeDbValue(user);
|
|
8
|
+
}
|
|
9
|
+
if (password) {
|
|
10
|
+
connection += `:${encodeDbValue(password)}`;
|
|
11
|
+
}
|
|
12
|
+
if (user || password) {
|
|
13
|
+
connection += "@";
|
|
14
|
+
}
|
|
15
|
+
connection += `${host}/${db}`;
|
|
16
|
+
return connection;
|
|
17
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
export default ({ message, type = "info", stack }) => {
|
|
5
|
+
switch (type) {
|
|
6
|
+
case "info":
|
|
7
|
+
logger.info(chalk.white(message));
|
|
8
|
+
break;
|
|
9
|
+
case "success":
|
|
10
|
+
logger.info(chalk.green(message));
|
|
11
|
+
break;
|
|
12
|
+
case "warn":
|
|
13
|
+
logger.warn(chalk.yellow(message));
|
|
14
|
+
break;
|
|
15
|
+
case "verbose":
|
|
16
|
+
logger.info(`${chalk.bgYellowBright("VERBOSE LOG:")} ${message}`);
|
|
17
|
+
break;
|
|
18
|
+
case "error":
|
|
19
|
+
program.error(chalk.bold.red(message.trim() + (stack || "")));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { customAlphabet } from "nanoid";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { isAbortError } from "./create-abort-controller.js";
|
|
7
|
+
import execute from "./execute.js";
|
|
8
|
+
import { displayFactBox } from "./facts.js";
|
|
9
|
+
import logMessage from "./log-message.js";
|
|
10
|
+
import { updatePackageVersions } from "./update-package-versions.js";
|
|
11
|
+
const NEXTJS_REPO = "https://github.com/acmekit/nextjs-starter-acmekit";
|
|
12
|
+
const NEXTJS_BRANCH = "main";
|
|
13
|
+
export async function askForNextjsStarter() {
|
|
14
|
+
const { installNextjs } = await inquirer.prompt([
|
|
15
|
+
{
|
|
16
|
+
type: "confirm",
|
|
17
|
+
name: "installNextjs",
|
|
18
|
+
message: `Would you like to install the Next.js Starter Storefront? You can also install it later.`,
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
21
|
+
]);
|
|
22
|
+
return installNextjs;
|
|
23
|
+
}
|
|
24
|
+
export async function installNextjsStarter({ directoryName, abortController, factBoxOptions, verbose = false, packageManager, version, }) {
|
|
25
|
+
factBoxOptions.interval = displayFactBox({
|
|
26
|
+
...factBoxOptions,
|
|
27
|
+
title: "Installing Next.js Starter Storefront...",
|
|
28
|
+
});
|
|
29
|
+
let nextjsDirectory = `${directoryName}-storefront`;
|
|
30
|
+
if (fs.existsSync(nextjsDirectory) &&
|
|
31
|
+
fs.lstatSync(nextjsDirectory).isDirectory()) {
|
|
32
|
+
// append a random number to the directory name
|
|
33
|
+
nextjsDirectory += `-${customAlphabet(
|
|
34
|
+
// npm throws an error if the directory name has an uppercase letter
|
|
35
|
+
"123456789abcdefghijklmnopqrstuvwxyz", 4)()}`;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await execute([
|
|
39
|
+
`git clone ${NEXTJS_REPO} -b ${NEXTJS_BRANCH} ${nextjsDirectory} --depth 1`,
|
|
40
|
+
{
|
|
41
|
+
signal: abortController?.signal,
|
|
42
|
+
env: process.env,
|
|
43
|
+
},
|
|
44
|
+
], { verbose });
|
|
45
|
+
if (version) {
|
|
46
|
+
const packageJsonPath = path.join(nextjsDirectory, "package.json");
|
|
47
|
+
updatePackageVersions(packageJsonPath, version, { applyChanges: true });
|
|
48
|
+
}
|
|
49
|
+
const execOptions = {
|
|
50
|
+
signal: abortController?.signal,
|
|
51
|
+
cwd: nextjsDirectory,
|
|
52
|
+
};
|
|
53
|
+
await packageManager.installDependencies(execOptions);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (isAbortError(e)) {
|
|
57
|
+
process.exit();
|
|
58
|
+
}
|
|
59
|
+
logMessage({
|
|
60
|
+
message: `An error occurred while installing Next.js Starter Storefront: ${e}`,
|
|
61
|
+
type: "error",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
fs.rmSync(path.join(nextjsDirectory, ".git"), {
|
|
65
|
+
recursive: true,
|
|
66
|
+
force: true,
|
|
67
|
+
});
|
|
68
|
+
fs.renameSync(path.join(nextjsDirectory, ".env.template"), path.join(nextjsDirectory, ".env.local"));
|
|
69
|
+
displayFactBox({
|
|
70
|
+
...factBoxOptions,
|
|
71
|
+
message: `Installed Next.js Starter Storefront successfully in the ${nextjsDirectory} directory.`,
|
|
72
|
+
});
|
|
73
|
+
return nextjsDirectory;
|
|
74
|
+
}
|
|
75
|
+
export function startNextjsStarter({ directory, abortController, verbose = false, packageManager, }) {
|
|
76
|
+
const command = packageManager.getCommandStr(`dev`);
|
|
77
|
+
const childProcess = exec(command, {
|
|
78
|
+
cwd: directory,
|
|
79
|
+
signal: abortController?.signal,
|
|
80
|
+
});
|
|
81
|
+
if (verbose) {
|
|
82
|
+
childProcess.stdout?.pipe(process.stdout);
|
|
83
|
+
childProcess.stderr?.pipe(process.stderr);
|
|
84
|
+
}
|
|
85
|
+
}
|