@cellajs/create-cella 0.1.7 → 0.2.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/README.md +57 -0
- package/configs/default-config.ts.template +404 -0
- package/dist/index.js +620 -455
- package/dist/index.js.map +1 -1
- package/index.js +1 -1
- package/package.json +35 -16
- package/src/add-remote.ts +0 -50
- package/src/cli.ts +0 -106
- package/src/constants.ts +0 -61
- package/src/create.ts +0 -193
- package/src/index.ts +0 -137
- package/src/utils/clean-template.ts +0 -150
- package/src/utils/extract-package-json-version-from-uri.ts +0 -29
- package/src/utils/is-empty-directory.ts +0 -15
- package/src/utils/run-git-command.ts +0 -57
- package/src/utils/run-package-manager-command.ts +0 -74
- package/src/utils/validate-project-name.ts +0 -22
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -22
package/dist/index.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
3
|
+
// src/create-cella-cli.ts
|
|
4
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5
|
+
import { basename as basename2, resolve as resolve3 } from "path";
|
|
6
|
+
import { input, select } from "@inquirer/prompts";
|
|
7
|
+
import pc5 from "picocolors";
|
|
8
8
|
|
|
9
|
-
// src/
|
|
10
|
-
import
|
|
11
|
-
import { Command, InvalidArgumentError } from "commander";
|
|
9
|
+
// src/constants.ts
|
|
10
|
+
import pc from "picocolors";
|
|
12
11
|
|
|
13
12
|
// package.json
|
|
14
13
|
var package_default = {
|
|
15
14
|
name: "@cellajs/create-cella",
|
|
16
|
-
version: "0.1
|
|
15
|
+
version: "0.2.1",
|
|
17
16
|
private: false,
|
|
18
17
|
license: "MIT",
|
|
19
18
|
description: "Cella is a TypeScript template to create web apps with sync and offline capabilities.",
|
|
@@ -28,39 +27,59 @@ var package_default = {
|
|
|
28
27
|
homepage: "https://cellajs.com",
|
|
29
28
|
author: "CellaJS <info@cellajs.com>",
|
|
30
29
|
engines: {
|
|
31
|
-
node: "
|
|
30
|
+
node: "24.x"
|
|
32
31
|
},
|
|
33
32
|
type: "module",
|
|
34
|
-
main: "./
|
|
33
|
+
main: "./dist/index.js",
|
|
34
|
+
files: [
|
|
35
|
+
"configs",
|
|
36
|
+
"dist",
|
|
37
|
+
"index.js",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
imports: {
|
|
41
|
+
"#/*": "./src/*"
|
|
42
|
+
},
|
|
35
43
|
bin: {
|
|
36
44
|
"create-cella": "index.js"
|
|
37
45
|
},
|
|
38
46
|
scripts: {
|
|
39
|
-
start: "tsx ./src/
|
|
47
|
+
start: "tsx ./src/create-cella-cli.ts",
|
|
40
48
|
clean: "rimraf ./dist",
|
|
41
49
|
build: "tsup",
|
|
42
50
|
"test-build": "pnpm run build && node index.js",
|
|
43
|
-
prepublishOnly: "pnpm run
|
|
51
|
+
prepublishOnly: "pnpm run test:release",
|
|
52
|
+
check: "pnpm ts && pnpm biome check --write .",
|
|
53
|
+
ts: "tsgo --pretty",
|
|
54
|
+
lint: "biome check .",
|
|
55
|
+
"lint:fix": "biome check --write .",
|
|
56
|
+
test: "vitest run",
|
|
57
|
+
"test:release": "pnpm run build && vitest run tests/release-artifact.test.ts",
|
|
58
|
+
"test:watch": "vitest"
|
|
44
59
|
},
|
|
45
|
-
packageManager: "pnpm@9.11.0",
|
|
46
60
|
dependencies: {
|
|
47
|
-
"@inquirer/prompts": "^
|
|
48
|
-
axios: "^1.
|
|
49
|
-
commander: "^
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
61
|
+
"@inquirer/prompts": "^8.5.2",
|
|
62
|
+
axios: "^1.17.0",
|
|
63
|
+
commander: "^15.0.0",
|
|
64
|
+
giget: "^3.1.2",
|
|
65
|
+
"nano-spawn": "^2.0.0",
|
|
66
|
+
ora: "^9.3.0",
|
|
67
|
+
picocolors: "^1.1.1",
|
|
68
|
+
"validate-npm-package-name": "^8.0.0"
|
|
55
69
|
},
|
|
56
70
|
devDependencies: {
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
"@types/node": "catalog:",
|
|
72
|
+
"@typescript/native-preview": "catalog:",
|
|
73
|
+
"@types/validate-npm-package-name": "^4.0.2",
|
|
74
|
+
tsup: "catalog:",
|
|
75
|
+
tsx: "catalog:",
|
|
76
|
+
vitest: "catalog:"
|
|
59
77
|
}
|
|
60
78
|
};
|
|
61
79
|
|
|
62
80
|
// src/constants.ts
|
|
63
|
-
var NAME = "create
|
|
81
|
+
var NAME = "create cella";
|
|
82
|
+
var DIVIDER = "\u2500".repeat(60);
|
|
64
83
|
var TEMPLATE_URL = "github:cellajs/cella";
|
|
65
84
|
var CELLA_REMOTE_URL = "git@github.com:cellajs/cella.git";
|
|
66
85
|
var DESCRIPTION = package_default.description;
|
|
@@ -68,41 +87,190 @@ var VERSION = package_default.version;
|
|
|
68
87
|
var AUTHOR = package_default.author;
|
|
69
88
|
var WEBSITE = package_default.homepage;
|
|
70
89
|
var GITHUB = package_default.repository.url;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
function getHeaderLine(templateVersion) {
|
|
91
|
+
const leftText = `\u29C8 ${NAME} \xB7 v${VERSION} \xB7 cella v${templateVersion}`;
|
|
92
|
+
const rightText = package_default.homepage.replace("https://", "");
|
|
93
|
+
const left = `${pc.cyan(`\u29C8 ${NAME}`)} ${pc.dim(`\xB7 v${VERSION} \xB7 cella v${templateVersion}`)}`;
|
|
94
|
+
const right = pc.cyan(rightText);
|
|
95
|
+
const padding = Math.max(1, 60 - leftText.length - rightText.length);
|
|
96
|
+
return `${left}${" ".repeat(padding)}${right}`;
|
|
97
|
+
}
|
|
98
|
+
var TO_REMOVE = ["./cli/create", "./info/QUICKSTART.md"];
|
|
99
|
+
var TO_CLEAN = ["./backend/drizzle"];
|
|
78
100
|
var TO_COPY = {
|
|
79
|
-
"./backend/.env.example": "./backend/.env",
|
|
80
101
|
"./frontend/.env.example": "./frontend/.env",
|
|
81
102
|
"./info/QUICKSTART.md": "README.md"
|
|
82
103
|
};
|
|
83
|
-
var
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
var PLACEHOLDER_CONFIG = "./cli/create-cella/configs/default-config.ts.template";
|
|
105
|
+
async function generateEnvFromExample(examplePath, replacements) {
|
|
106
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
107
|
+
let content;
|
|
108
|
+
try {
|
|
109
|
+
content = await readFile2(examplePath, "utf8");
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return content.replace(/^([A-Z_][A-Z0-9_]*)=(.*)$/gm, (match, key, _value) => {
|
|
114
|
+
if (key in replacements) return `${key}=${replacements[key]}`;
|
|
115
|
+
return match;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function getRootEnvReplacements(slug, portOffset) {
|
|
119
|
+
return {
|
|
120
|
+
PROJECT_SLUG: slug,
|
|
121
|
+
DB_PORT: String(5432 + portOffset),
|
|
122
|
+
DB_TEST_PORT: String(5434 + portOffset)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getBackendEnvReplacements(adminEmail, portOffset) {
|
|
126
|
+
const db = 5432 + portOffset;
|
|
127
|
+
return {
|
|
128
|
+
DATABASE_URL: `postgres://runtime_role:dev_password@0.0.0.0:${db}/postgres`,
|
|
129
|
+
DATABASE_ADMIN_URL: `postgres://postgres:postgres@0.0.0.0:${db}/postgres`,
|
|
130
|
+
DATABASE_CDC_URL: `postgres://admin_role:dev_password@0.0.0.0:${db}/postgres`,
|
|
131
|
+
ADMIN_EMAIL: adminEmail,
|
|
132
|
+
PORT: String(4e3 + portOffset)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function generateEnvConfigs(slug, name, portOffset) {
|
|
136
|
+
const fe = 3e3 + portOffset;
|
|
137
|
+
const be = 4e3 + portOffset;
|
|
138
|
+
const header = "import type { DeepPartial } from '../src/builder/types';\nimport type _default from './config.default';\n";
|
|
139
|
+
const envs = {
|
|
140
|
+
development: {
|
|
141
|
+
props: {
|
|
142
|
+
slug: `${slug}-development`,
|
|
143
|
+
debug: false,
|
|
144
|
+
domain: "",
|
|
145
|
+
frontendUrl: `http://localhost:${fe}`,
|
|
146
|
+
backendUrl: `http://localhost:${be}`,
|
|
147
|
+
backendAuthUrl: `http://localhost:${be}/auth`
|
|
148
|
+
}
|
|
88
149
|
},
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
150
|
+
staging: {
|
|
151
|
+
props: {
|
|
152
|
+
slug: `${slug}-staging`,
|
|
153
|
+
debug: false,
|
|
154
|
+
domain: `${slug}.example.com`,
|
|
155
|
+
frontendUrl: `https://staging.${slug}.example.com`,
|
|
156
|
+
backendUrl: `https://api-staging.${slug}.example.com`,
|
|
157
|
+
backendAuthUrl: `https://api-staging.${slug}.example.com/auth`
|
|
158
|
+
}
|
|
92
159
|
},
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
160
|
+
tunnel: {
|
|
161
|
+
props: {
|
|
162
|
+
frontendUrl: `https://localhost:${fe}`,
|
|
163
|
+
backendUrl: `https://${slug}.ngrok.dev`,
|
|
164
|
+
backendAuthUrl: `https://${slug}.ngrok.dev/auth`
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
test: {
|
|
168
|
+
imports: "import development from './config.development';\n",
|
|
169
|
+
props: {
|
|
170
|
+
debug: false,
|
|
171
|
+
domain: "",
|
|
172
|
+
frontendUrl: "=development.frontendUrl",
|
|
173
|
+
backendUrl: "=development.backendUrl",
|
|
174
|
+
backendAuthUrl: "=development.backendAuthUrl"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
production: { props: { maintenance: false } }
|
|
178
|
+
};
|
|
179
|
+
const lit = (v) => {
|
|
180
|
+
if (typeof v === "boolean") return String(v);
|
|
181
|
+
if (v.startsWith("=")) return v.slice(1);
|
|
182
|
+
return `'${v}'`;
|
|
183
|
+
};
|
|
184
|
+
const result = {};
|
|
185
|
+
for (const [mode, { imports = "", props }] of Object.entries(envs)) {
|
|
186
|
+
const nameEntry = mode !== "production" ? ` name: '${name} ${mode.toUpperCase()}',
|
|
187
|
+
` : "";
|
|
188
|
+
const body = Object.entries(props).map(([k, v]) => ` ${k}: ${lit(v)},`).join("\n");
|
|
189
|
+
result[`./shared/config/config.${mode}.ts`] = `${imports}${header}
|
|
190
|
+
export default {
|
|
191
|
+
mode: '${mode}',
|
|
192
|
+
${nameEntry}${body}
|
|
193
|
+
} satisfies DeepPartial<typeof _default>;
|
|
105
194
|
`;
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/create.ts
|
|
200
|
+
import { existsSync } from "fs";
|
|
201
|
+
import { cp, mkdir } from "fs/promises";
|
|
202
|
+
import { join, relative, resolve as resolve2 } from "path";
|
|
203
|
+
import { downloadTemplate } from "giget";
|
|
204
|
+
|
|
205
|
+
// src/utils/git/command.ts
|
|
206
|
+
import { execFile } from "child_process";
|
|
207
|
+
import { promisify } from "util";
|
|
208
|
+
var execFileAsync = promisify(execFile);
|
|
209
|
+
async function runGitCommand(args, repoPath, options2 = {}) {
|
|
210
|
+
const gitArgs = repoPath ? ["-C", repoPath, ...args] : args;
|
|
211
|
+
const env = {
|
|
212
|
+
...process.env,
|
|
213
|
+
...options2.skipEditor ? { GIT_EDITOR: "true" } : {}
|
|
214
|
+
};
|
|
215
|
+
const maxBuffer = options2.maxBuffer ?? 10 * 1024 * 1024;
|
|
216
|
+
const { stdout } = await execFileAsync("git", gitArgs, { env, maxBuffer });
|
|
217
|
+
return stdout.trim();
|
|
218
|
+
}
|
|
219
|
+
async function gitInit(repoPath) {
|
|
220
|
+
return runGitCommand(["init"], repoPath);
|
|
221
|
+
}
|
|
222
|
+
async function gitAddAll(repoPath) {
|
|
223
|
+
return runGitCommand(["add", "."], repoPath);
|
|
224
|
+
}
|
|
225
|
+
async function gitCommit(repoPath, message) {
|
|
226
|
+
return runGitCommand(["commit", "-m", message], repoPath);
|
|
227
|
+
}
|
|
228
|
+
async function gitBranch(repoPath, branchName) {
|
|
229
|
+
return runGitCommand(["branch", branchName], repoPath);
|
|
230
|
+
}
|
|
231
|
+
async function gitCheckout(repoPath, branchName) {
|
|
232
|
+
return runGitCommand(["checkout", branchName], repoPath);
|
|
233
|
+
}
|
|
234
|
+
async function gitRemoteGetUrl(repoPath, remoteName) {
|
|
235
|
+
return runGitCommand(["remote", "get-url", remoteName], repoPath);
|
|
236
|
+
}
|
|
237
|
+
async function gitRemoteAdd(repoPath, remoteName, remoteUrl) {
|
|
238
|
+
return runGitCommand(["remote", "add", remoteName, remoteUrl], repoPath);
|
|
239
|
+
}
|
|
240
|
+
async function gitRemoteRemove(repoPath, remoteName) {
|
|
241
|
+
return runGitCommand(["remote", "remove", remoteName], repoPath);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/add-remote.ts
|
|
245
|
+
async function addRemote({
|
|
246
|
+
targetFolder,
|
|
247
|
+
remoteUrl = CELLA_REMOTE_URL,
|
|
248
|
+
remoteName = "upstream",
|
|
249
|
+
silent = false
|
|
250
|
+
}) {
|
|
251
|
+
try {
|
|
252
|
+
let remote = null;
|
|
253
|
+
try {
|
|
254
|
+
remote = await gitRemoteGetUrl(targetFolder, remoteName);
|
|
255
|
+
} catch {
|
|
256
|
+
remote = null;
|
|
257
|
+
}
|
|
258
|
+
if (!remote) {
|
|
259
|
+
await gitRemoteAdd(targetFolder, remoteName, remoteUrl);
|
|
260
|
+
} else if (remote !== remoteUrl) {
|
|
261
|
+
await gitRemoteRemove(targetFolder, remoteName);
|
|
262
|
+
await gitRemoteAdd(targetFolder, remoteName, remoteUrl);
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (!silent) {
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/modules/cli/commands.ts
|
|
272
|
+
import { basename, resolve } from "path";
|
|
273
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
106
274
|
|
|
107
275
|
// src/utils/validate-project-name.ts
|
|
108
276
|
import validate from "validate-npm-package-name";
|
|
@@ -113,180 +281,137 @@ function validateProjectName(name) {
|
|
|
113
281
|
}
|
|
114
282
|
return {
|
|
115
283
|
valid: false,
|
|
116
|
-
problems: [
|
|
117
|
-
...nameValidation.errors || [],
|
|
118
|
-
...nameValidation.warnings || []
|
|
119
|
-
]
|
|
284
|
+
problems: [...nameValidation.errors || [], ...nameValidation.warnings || []]
|
|
120
285
|
};
|
|
121
286
|
}
|
|
122
287
|
|
|
123
|
-
// src/cli.ts
|
|
288
|
+
// src/modules/cli/commands.ts
|
|
124
289
|
var directory = null;
|
|
125
290
|
var newBranchName = null;
|
|
126
|
-
var createNewBranch = null;
|
|
127
291
|
var packageManager = "pnpm";
|
|
128
|
-
var command = new Command(NAME).version(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
).argument("[directory]", "The directory name for the new project.").usage("[directory] [options]").helpOption("-h, --help", "Display this help message.").option("--skip-new-branch", "Skip creating a new branch during initialization.", false).option("--skip-install", "Skip the installation of packages.", false).option("--skip-generate", "Skip generating SQL files.", false).option("--skip-clean", "Skip cleaning the `cella` template.", false).option("--skip-git", "Skip initializing a git repository.", false).option(
|
|
133
|
-
"--new-branch-name <name>",
|
|
134
|
-
"Specify a new branch name to create and use.",
|
|
135
|
-
(name) => {
|
|
136
|
-
if (typeof name === "string") {
|
|
137
|
-
name = name.trim();
|
|
138
|
-
}
|
|
139
|
-
if (name) {
|
|
140
|
-
const validation = validateProjectName(basename(resolve(name)));
|
|
141
|
-
if (!validation.valid) {
|
|
142
|
-
throw new InvalidArgumentError(
|
|
143
|
-
`Invalid branch name: ${validation.problems[0]}`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
createNewBranch = true;
|
|
147
|
-
newBranchName = name;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
).action((name) => {
|
|
151
|
-
if (typeof name === "string") {
|
|
152
|
-
name = name.trim();
|
|
153
|
-
}
|
|
154
|
-
if (name) {
|
|
155
|
-
const validation = validateProjectName(basename(resolve(name)));
|
|
292
|
+
var command = new Command(NAME).version(VERSION, "-v, --version", `output the current version of ${NAME}`).argument("[directory]", "the directory name for the new project").usage("[directory] [options]").helpOption("-h, --help", "display this help message").option("--template <path>", "use a custom template (local path or github:user/repo)").action((name) => {
|
|
293
|
+
const trimmedName = typeof name === "string" ? name.trim() : name;
|
|
294
|
+
if (trimmedName) {
|
|
295
|
+
const validation = validateProjectName(basename(resolve(trimmedName)));
|
|
156
296
|
if (!validation.valid) {
|
|
157
|
-
throw new InvalidArgumentError(
|
|
158
|
-
`Invalid project name: ${validation.problems[0]}`
|
|
159
|
-
);
|
|
297
|
+
throw new InvalidArgumentError(`Invalid project name: ${validation.problems?.[0] ?? "unknown error"}`);
|
|
160
298
|
}
|
|
161
|
-
directory =
|
|
299
|
+
directory = trimmedName;
|
|
162
300
|
}
|
|
163
301
|
}).parse();
|
|
164
|
-
var options = command.opts(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
args: command.args,
|
|
174
|
-
directory,
|
|
175
|
-
newBranchName,
|
|
176
|
-
createNewBranch,
|
|
177
|
-
packageManager
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
// src/utils/extract-package-json-version-from-uri.ts
|
|
181
|
-
import axios from "axios";
|
|
182
|
-
async function extractPackageJsonVersionFromUri(repositoryUrl, branch = "main") {
|
|
183
|
-
const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
|
|
184
|
-
const packageJsonUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/package.json`;
|
|
185
|
-
try {
|
|
186
|
-
const response = await axios.get(packageJsonUrl);
|
|
187
|
-
const packageJson = response.data;
|
|
188
|
-
return packageJson.version || "unknown";
|
|
189
|
-
} catch (error) {
|
|
190
|
-
return "unknown";
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// src/utils/is-empty-directory.ts
|
|
195
|
-
import { readdir } from "node:fs/promises";
|
|
196
|
-
async function isEmptyDirectory(path2) {
|
|
197
|
-
const files = await readdir(path2);
|
|
198
|
-
return files.length === 0 || files.length === 1 && files[0] === ".git";
|
|
302
|
+
var options = command.opts();
|
|
303
|
+
function runCli() {
|
|
304
|
+
return {
|
|
305
|
+
options,
|
|
306
|
+
args: command.args,
|
|
307
|
+
directory,
|
|
308
|
+
newBranchName,
|
|
309
|
+
packageManager
|
|
310
|
+
};
|
|
199
311
|
}
|
|
312
|
+
var cli = runCli();
|
|
200
313
|
|
|
201
|
-
// src/
|
|
202
|
-
import
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// src/utils/run-package-manager-command.ts
|
|
210
|
-
import spawn from "cross-spawn";
|
|
211
|
-
async function runPackageManagerCommand(packageManager2, args, env = {}) {
|
|
212
|
-
return new Promise((resolve3, reject) => {
|
|
213
|
-
const child = spawn(packageManager2, args, {
|
|
214
|
-
env: {
|
|
215
|
-
...process.env,
|
|
216
|
-
...env
|
|
217
|
-
},
|
|
218
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
219
|
-
});
|
|
220
|
-
let stderrBuffer = "";
|
|
221
|
-
let stdoutBuffer = "";
|
|
222
|
-
child.stderr?.on("data", (data) => {
|
|
223
|
-
stderrBuffer += data.toString();
|
|
224
|
-
});
|
|
225
|
-
child.stdout?.on("data", (data) => {
|
|
226
|
-
stdoutBuffer += data.toString();
|
|
227
|
-
});
|
|
228
|
-
child.on("close", (code) => {
|
|
229
|
-
if (code !== 0) {
|
|
230
|
-
reject(
|
|
231
|
-
`"${packageManager2} ${args.join(" ")}" failed ${stdoutBuffer} ${stderrBuffer}`
|
|
232
|
-
);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
resolve3();
|
|
236
|
-
});
|
|
237
|
-
});
|
|
314
|
+
// src/modules/cli/display.ts
|
|
315
|
+
import pc2 from "picocolors";
|
|
316
|
+
function showAscii() {
|
|
317
|
+
console.info(pc2.cyan(" _ _ "));
|
|
318
|
+
console.info(pc2.cyan("\u2592\u2593\u2588\u2588\u2588\u2588\u2588\u2593\u2592 ___ ___| | | __ _ "));
|
|
319
|
+
console.info(pc2.cyan("\u2592\u2593\u2588 \u2588\u2593\u2592 / __/ _ \\ | |/ _` | "));
|
|
320
|
+
console.info(pc2.cyan("\u2592\u2593\u2588 \u2588\u2593\u2592 | (_| __/ | | (_| | "));
|
|
321
|
+
console.info(pc2.cyan("\u2592\u2593\u2588\u2588\u2588\u2588\u2588\u2593\u2592 \\___\\___|_|_|\\__,_| "));
|
|
238
322
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
323
|
+
function showWelcome(templateVersion) {
|
|
324
|
+
console.info();
|
|
325
|
+
showAscii();
|
|
326
|
+
console.info();
|
|
327
|
+
console.info(pc2.dim(DESCRIPTION));
|
|
328
|
+
console.info();
|
|
329
|
+
console.info(getHeaderLine(templateVersion));
|
|
330
|
+
console.info(DIVIDER);
|
|
243
331
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
332
|
+
function showSuccess(projectName, _targetFolder, relativePath, needsCd, packageManager2) {
|
|
333
|
+
console.info(DIVIDER);
|
|
334
|
+
console.info();
|
|
335
|
+
if (needsCd) {
|
|
336
|
+
console.info(`${pc2.green("\u2192")} cd ${pc2.cyan(relativePath)}`);
|
|
337
|
+
console.info();
|
|
338
|
+
}
|
|
339
|
+
console.info(`${pc2.green("\u2192")} ${pc2.cyan(`${packageManager2} quick`)} ${pc2.gray("(pglite, no docker)")}`);
|
|
340
|
+
console.info();
|
|
341
|
+
console.info(pc2.gray("or, for full setup:"));
|
|
342
|
+
console.info();
|
|
343
|
+
console.info(
|
|
344
|
+
`${pc2.green("\u2192")} ${pc2.cyan(`${packageManager2} docker`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} seed`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} dev`)}`
|
|
345
|
+
);
|
|
346
|
+
console.info();
|
|
347
|
+
console.info(`sign in: ${pc2.gray("admin-test@cellajs.com / 12345678")}`);
|
|
348
|
+
console.info();
|
|
349
|
+
console.info(`enjoy building ${pc2.green(projectName)} with cella!`);
|
|
350
|
+
console.info();
|
|
248
351
|
}
|
|
249
352
|
|
|
250
353
|
// src/utils/clean-template.ts
|
|
251
|
-
import fs from "
|
|
252
|
-
import path from "
|
|
253
|
-
import
|
|
354
|
+
import fs from "fs/promises";
|
|
355
|
+
import path from "path";
|
|
356
|
+
import pc3 from "picocolors";
|
|
357
|
+
var warningMark = pc3.yellow("\u26A0");
|
|
254
358
|
async function cleanTemplate({
|
|
255
359
|
targetFolder,
|
|
256
|
-
projectName
|
|
360
|
+
projectName,
|
|
361
|
+
displayName,
|
|
362
|
+
portOffset = 0,
|
|
363
|
+
adminEmail = `admin@${projectName}.example.com`
|
|
257
364
|
}) {
|
|
258
365
|
if (process.cwd() !== targetFolder) {
|
|
259
366
|
process.chdir(targetFolder);
|
|
260
367
|
}
|
|
261
|
-
return new Promise(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
368
|
+
return new Promise((resolve4, reject) => {
|
|
369
|
+
(async () => {
|
|
370
|
+
try {
|
|
371
|
+
for (const [src, dest] of Object.entries(TO_COPY)) {
|
|
372
|
+
const srcAbsolutePath = path.resolve(targetFolder, src);
|
|
373
|
+
const destAbsolutePath = path.resolve(targetFolder, dest);
|
|
374
|
+
await copyFile(srcAbsolutePath, destAbsolutePath);
|
|
375
|
+
}
|
|
376
|
+
await applyPlaceholderConfig(targetFolder, projectName);
|
|
377
|
+
const rootReplacements = getRootEnvReplacements(projectName, portOffset);
|
|
378
|
+
const rootEnv = await generateEnvFromExample(path.resolve(targetFolder, ".env.example"), rootReplacements);
|
|
379
|
+
await fs.writeFile(
|
|
380
|
+
path.resolve(targetFolder, ".env"),
|
|
381
|
+
rootEnv ?? Object.entries(rootReplacements).map(([k, v]) => `${k}=${v}`).join("\n"),
|
|
382
|
+
"utf8"
|
|
383
|
+
);
|
|
384
|
+
const backendReplacements = getBackendEnvReplacements(adminEmail, portOffset);
|
|
385
|
+
const backendEnv = await generateEnvFromExample(
|
|
386
|
+
path.resolve(targetFolder, "backend/.env.example"),
|
|
387
|
+
backendReplacements
|
|
388
|
+
);
|
|
389
|
+
if (backendEnv) {
|
|
390
|
+
await fs.writeFile(path.resolve(targetFolder, "backend/.env"), backendEnv, "utf8");
|
|
391
|
+
}
|
|
392
|
+
const envConfigs = generateEnvConfigs(projectName, displayName, portOffset);
|
|
393
|
+
await Promise.all(
|
|
394
|
+
Object.entries(envConfigs).map(
|
|
395
|
+
([filePath, content]) => fs.writeFile(path.resolve(targetFolder, filePath), content, "utf8")
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
await Promise.all(
|
|
399
|
+
TO_CLEAN.map((folderPath) => {
|
|
400
|
+
const absolutePath = path.resolve(targetFolder, folderPath);
|
|
401
|
+
return removeFolderContents(absolutePath);
|
|
402
|
+
})
|
|
403
|
+
);
|
|
404
|
+
await Promise.all(
|
|
405
|
+
TO_REMOVE.map((filePath) => {
|
|
406
|
+
const absolutePath = path.resolve(targetFolder, filePath);
|
|
407
|
+
return removeFileOrFolder(absolutePath);
|
|
408
|
+
})
|
|
409
|
+
);
|
|
410
|
+
resolve4();
|
|
411
|
+
} catch (err) {
|
|
412
|
+
reject(`Error during the cleaning process: ${err}`);
|
|
267
413
|
}
|
|
268
|
-
|
|
269
|
-
TO_CLEAN.map((folderPath) => {
|
|
270
|
-
const absolutePath = path.resolve(targetFolder, folderPath);
|
|
271
|
-
return removeFolderContents(absolutePath);
|
|
272
|
-
})
|
|
273
|
-
);
|
|
274
|
-
await Promise.all(
|
|
275
|
-
TO_REMOVE.map((filePath) => {
|
|
276
|
-
const absolutePath = path.resolve(targetFolder, filePath);
|
|
277
|
-
return removeFileOrFolder(absolutePath);
|
|
278
|
-
})
|
|
279
|
-
);
|
|
280
|
-
await Promise.all(
|
|
281
|
-
Object.entries(TO_EDIT).map(async ([filePath, edits]) => {
|
|
282
|
-
const absolutePath = path.resolve(targetFolder, filePath);
|
|
283
|
-
await editFile(absolutePath, edits);
|
|
284
|
-
})
|
|
285
|
-
);
|
|
286
|
-
resolve3();
|
|
287
|
-
} catch (err) {
|
|
288
|
-
reject(`Error during the cleaning process: ${err}`);
|
|
289
|
-
}
|
|
414
|
+
})();
|
|
290
415
|
});
|
|
291
416
|
}
|
|
292
417
|
async function removeFolderContents(folderPath) {
|
|
@@ -314,320 +439,360 @@ async function copyFile(src, dest) {
|
|
|
314
439
|
} catch (err) {
|
|
315
440
|
if (err.code === "ENOENT") {
|
|
316
441
|
console.info(`
|
|
317
|
-
${
|
|
442
|
+
${warningMark} Source file "${src}" does not exist > Skip copy`);
|
|
318
443
|
} else {
|
|
319
444
|
throw err;
|
|
320
445
|
}
|
|
321
446
|
}
|
|
322
447
|
}
|
|
323
|
-
async function
|
|
448
|
+
async function applyPlaceholderConfig(targetFolder, projectName) {
|
|
449
|
+
const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
450
|
+
const src = path.resolve(targetFolder, PLACEHOLDER_CONFIG);
|
|
451
|
+
const dest = path.resolve(targetFolder, "./shared/config/config.default.ts");
|
|
324
452
|
try {
|
|
325
|
-
await fs.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
updatedContent = updatedContent.replace(regexMatch, replaceWith);
|
|
330
|
-
});
|
|
331
|
-
if (fileContent !== updatedContent) {
|
|
332
|
-
await fs.writeFile(filePath, updatedContent, "utf8");
|
|
333
|
-
}
|
|
453
|
+
let content = await fs.readFile(src, "utf8");
|
|
454
|
+
content = content.replaceAll("__project_name__", displayName);
|
|
455
|
+
content = content.replaceAll("__project_slug__", projectName);
|
|
456
|
+
await fs.writeFile(dest, content, "utf8");
|
|
334
457
|
} catch (err) {
|
|
335
458
|
if (err.code === "ENOENT") {
|
|
336
459
|
console.info(`
|
|
337
|
-
${
|
|
460
|
+
${warningMark} Placeholder config "${src}" not found > Skip`);
|
|
338
461
|
} else {
|
|
339
462
|
throw err;
|
|
340
463
|
}
|
|
341
464
|
}
|
|
342
465
|
}
|
|
343
466
|
|
|
344
|
-
// src/utils/
|
|
345
|
-
import
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
467
|
+
// src/utils/progress.ts
|
|
468
|
+
import ora from "ora";
|
|
469
|
+
import pc4 from "picocolors";
|
|
470
|
+
var activeSpinner = null;
|
|
471
|
+
function createProgress(title, silent = false) {
|
|
472
|
+
const completedSteps = [];
|
|
473
|
+
const spinner = ora({
|
|
474
|
+
text: title,
|
|
475
|
+
isSilent: silent
|
|
476
|
+
});
|
|
477
|
+
activeSpinner = spinner;
|
|
478
|
+
spinner.start();
|
|
479
|
+
const log = (msg) => {
|
|
480
|
+
if (!silent) console.info(msg);
|
|
481
|
+
};
|
|
482
|
+
return {
|
|
483
|
+
step: (message) => {
|
|
484
|
+
if (completedSteps.length > 0) {
|
|
485
|
+
spinner.stop();
|
|
486
|
+
log(`${pc4.green("\u2713")} ${completedSteps[completedSteps.length - 1]}`);
|
|
487
|
+
}
|
|
488
|
+
completedSteps.push(message);
|
|
489
|
+
spinner.text = message;
|
|
490
|
+
spinner.start();
|
|
491
|
+
},
|
|
492
|
+
done: (message) => {
|
|
493
|
+
if (completedSteps.length > 0) {
|
|
494
|
+
spinner.stop();
|
|
495
|
+
log(`${pc4.green("\u2713")} ${completedSteps[completedSteps.length - 1]}`);
|
|
368
496
|
} else {
|
|
369
|
-
|
|
370
|
-
`Git ${gitCommand} command failed with exit code ${code}, stderr: "${errOutput.trim()}", stdout: "${output.trim()}"`
|
|
371
|
-
);
|
|
497
|
+
spinner.stop();
|
|
372
498
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
499
|
+
activeSpinner = null;
|
|
500
|
+
if (message) {
|
|
501
|
+
log("");
|
|
502
|
+
log(`${pc4.green("\u2713")} ${message}`);
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
fail: (message) => {
|
|
506
|
+
spinner.stop();
|
|
507
|
+
activeSpinner = null;
|
|
508
|
+
log(`${pc4.red("\u2717")} ${pc4.red(message)}`);
|
|
509
|
+
},
|
|
510
|
+
wrap: async (fn) => {
|
|
511
|
+
try {
|
|
512
|
+
return await fn();
|
|
513
|
+
} catch (error) {
|
|
514
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
515
|
+
spinner.stop();
|
|
516
|
+
activeSpinner = null;
|
|
517
|
+
log(pc4.cyan(`
|
|
518
|
+
${title}`));
|
|
519
|
+
for (const step of completedSteps) {
|
|
520
|
+
log(pc4.gray(` \u251C\u2500 ${step}`));
|
|
521
|
+
}
|
|
522
|
+
log(pc4.red(` \u2514\u2500 \u2717 ${errorMessage}`));
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
381
527
|
}
|
|
382
528
|
|
|
383
|
-
// src/
|
|
384
|
-
import
|
|
385
|
-
async function
|
|
386
|
-
targetFolder,
|
|
387
|
-
remoteUrl = CELLA_REMOTE_URL,
|
|
388
|
-
remoteName = "upstream"
|
|
389
|
-
}) {
|
|
390
|
-
const remoteSpinner = yoctoSpinner({
|
|
391
|
-
text: "Adding remote"
|
|
392
|
-
}).start();
|
|
529
|
+
// src/utils/run-package-manager-command.ts
|
|
530
|
+
import spawn from "nano-spawn";
|
|
531
|
+
async function runPackageManagerCommand(packageManager2, args, env = {}) {
|
|
393
532
|
try {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
remote = null;
|
|
399
|
-
}
|
|
400
|
-
if (!remote) {
|
|
401
|
-
await runGitCommand({ targetFolder, command: `remote add ${remoteName} ${remoteUrl}` });
|
|
402
|
-
remoteSpinner.success("Remote added successfully.");
|
|
403
|
-
} else if (remote !== remoteUrl) {
|
|
404
|
-
await runGitCommand({ targetFolder, command: `remote remove ${remoteName}` });
|
|
405
|
-
await runGitCommand({ targetFolder, command: `remote add ${remoteName} ${remoteUrl}` });
|
|
406
|
-
remoteSpinner.success("Remote updated successfully.");
|
|
407
|
-
} else {
|
|
408
|
-
remoteSpinner.success("Remote is already configured correctly.");
|
|
409
|
-
}
|
|
533
|
+
await spawn(packageManager2, args, {
|
|
534
|
+
env: { ...process.env, ...env },
|
|
535
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
536
|
+
});
|
|
410
537
|
} catch (error) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
process.exit(1);
|
|
538
|
+
const err = error;
|
|
539
|
+
throw new Error(`"${packageManager2} ${args.join(" ")}" failed ${err.stdout || ""} ${err.stderr || ""}`);
|
|
414
540
|
}
|
|
415
541
|
}
|
|
542
|
+
async function install(packageManager2) {
|
|
543
|
+
return runPackageManagerCommand(packageManager2, ["install"], {
|
|
544
|
+
NODE_ENV: "development"
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
async function generate(packageManager2) {
|
|
548
|
+
return runPackageManagerCommand(packageManager2, ["generate"], {
|
|
549
|
+
NODE_ENV: "development"
|
|
550
|
+
});
|
|
551
|
+
}
|
|
416
552
|
|
|
417
553
|
// src/create.ts
|
|
554
|
+
function isLocalPath(path2) {
|
|
555
|
+
return path2.startsWith("/") || path2.startsWith("./") || path2.startsWith("../");
|
|
556
|
+
}
|
|
418
557
|
async function create({
|
|
419
558
|
projectName,
|
|
420
559
|
targetFolder,
|
|
421
560
|
newBranchName: newBranchName2,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
561
|
+
packageManager: packageManager2,
|
|
562
|
+
templateUrl,
|
|
563
|
+
portOffset,
|
|
564
|
+
adminEmail,
|
|
565
|
+
silent = false
|
|
427
566
|
}) {
|
|
428
567
|
const originalCwd = process.cwd();
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
await
|
|
452
|
-
|
|
453
|
-
|
|
568
|
+
const template = templateUrl || TEMPLATE_URL;
|
|
569
|
+
const isLocalTemplate = templateUrl && isLocalPath(templateUrl);
|
|
570
|
+
const progress = createProgress("creating project", silent);
|
|
571
|
+
try {
|
|
572
|
+
progress.step("creating project folder");
|
|
573
|
+
await mkdir(targetFolder, { recursive: true });
|
|
574
|
+
process.chdir(targetFolder);
|
|
575
|
+
if (isLocalTemplate) {
|
|
576
|
+
progress.step("copying local template");
|
|
577
|
+
const sourcePath = resolve2(originalCwd, templateUrl);
|
|
578
|
+
if (targetFolder.startsWith(sourcePath)) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Cannot create project inside the template directory.
|
|
581
|
+
Run from outside: cd ~ && pnpm create @cellajs/cella ${projectName} --template ${templateUrl}`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
await cp(sourcePath, targetFolder, {
|
|
585
|
+
recursive: true,
|
|
586
|
+
filter: (src) => !src.includes("node_modules") && !src.includes(".git")
|
|
587
|
+
});
|
|
588
|
+
} else {
|
|
589
|
+
progress.step("downloading cella template");
|
|
590
|
+
await downloadTemplate(template, {
|
|
591
|
+
cwd: process.cwd(),
|
|
592
|
+
dir: targetFolder,
|
|
593
|
+
force: true,
|
|
594
|
+
provider: "github"
|
|
454
595
|
});
|
|
455
|
-
cleanSpinner.success("`cella` template cleaned");
|
|
456
|
-
} catch (e) {
|
|
457
|
-
console.error(e);
|
|
458
|
-
cleanSpinner.error("Failed to clean `cella` template");
|
|
459
|
-
process.exit(1);
|
|
460
|
-
}
|
|
461
|
-
} else {
|
|
462
|
-
console.info(`${colors2.yellow("\u26A0")} --skip-clean > Skip cleaning \`cella\` template`);
|
|
463
|
-
}
|
|
464
|
-
if (!skipInstall) {
|
|
465
|
-
const installSpinner = yoctoSpinner2({
|
|
466
|
-
text: "installing dependencies"
|
|
467
|
-
}).start();
|
|
468
|
-
try {
|
|
469
|
-
await install(packageManager2);
|
|
470
|
-
installSpinner.success("Dependencies installed");
|
|
471
|
-
} catch (e) {
|
|
472
|
-
console.error(e);
|
|
473
|
-
installSpinner.error("Failed to install dependencies");
|
|
474
|
-
process.exit(1);
|
|
475
|
-
}
|
|
476
|
-
} else {
|
|
477
|
-
console.info(`${colors2.yellow("\u26A0")} --skip-install > Skip installing dependencies`);
|
|
478
|
-
}
|
|
479
|
-
if (!skipGenerate) {
|
|
480
|
-
const generateSpinner = yoctoSpinner2({
|
|
481
|
-
text: "generating SQL files"
|
|
482
|
-
}).start();
|
|
483
|
-
try {
|
|
484
|
-
await generate(packageManager2);
|
|
485
|
-
generateSpinner.success("SQL files generated");
|
|
486
|
-
} catch (e) {
|
|
487
|
-
console.error(e);
|
|
488
|
-
generateSpinner.error("Failed to generate SQL files");
|
|
489
|
-
process.exit(1);
|
|
490
596
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
597
|
+
progress.step("cleaning template");
|
|
598
|
+
const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
599
|
+
await cleanTemplate({ targetFolder, projectName, displayName, portOffset, adminEmail });
|
|
600
|
+
progress.step("installing dependencies");
|
|
601
|
+
await install(packageManager2);
|
|
602
|
+
progress.step("generating migrations");
|
|
603
|
+
await generate(packageManager2);
|
|
498
604
|
const gitFolderPath = join(targetFolder, ".git");
|
|
499
605
|
if (!existsSync(gitFolderPath)) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
} else {
|
|
509
|
-
gitSpinner.success("Git repository initialized and initial commit created");
|
|
510
|
-
}
|
|
511
|
-
} catch (e) {
|
|
512
|
-
console.error(e);
|
|
513
|
-
gitSpinner.error("Failed to initialize Git repository or create branch");
|
|
514
|
-
process.exit(1);
|
|
606
|
+
progress.step("initializing git");
|
|
607
|
+
await gitInit(targetFolder);
|
|
608
|
+
await gitAddAll(targetFolder);
|
|
609
|
+
await gitCommit(targetFolder, "Initial commit");
|
|
610
|
+
if (newBranchName2) {
|
|
611
|
+
progress.step(`creating branch '${newBranchName2}'`);
|
|
612
|
+
await gitBranch(targetFolder, newBranchName2);
|
|
613
|
+
await gitCheckout(targetFolder, newBranchName2);
|
|
515
614
|
}
|
|
516
|
-
} else {
|
|
517
|
-
gitSpinner.warning("Git repository already initialized > Skip git init");
|
|
518
615
|
}
|
|
519
|
-
|
|
520
|
-
|
|
616
|
+
progress.step("adding upstream remote");
|
|
617
|
+
await addRemote({ targetFolder, silent: true });
|
|
618
|
+
progress.done(`created ${projectName}`);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
progress.fail(error instanceof Error ? error.message : String(error));
|
|
621
|
+
process.exit(1);
|
|
521
622
|
}
|
|
522
|
-
await addRemote({ targetFolder });
|
|
523
|
-
console.info();
|
|
524
|
-
console.info(`${colors2.green("Success")} Created ${projectName} at ${targetFolder}`);
|
|
525
|
-
console.info();
|
|
526
623
|
const needsCd = originalCwd !== targetFolder;
|
|
527
624
|
const relativePath = relative(originalCwd, targetFolder);
|
|
528
|
-
|
|
529
|
-
console.info("now go to your project using:");
|
|
530
|
-
console.info(colors2.cyan(` cd ${relativePath}`));
|
|
531
|
-
console.info();
|
|
532
|
-
}
|
|
533
|
-
console.info(`${needsCd ? "then " : ""}quick start using pglite with:`);
|
|
534
|
-
console.info(colors2.cyan(` ${packageManager2} quick`));
|
|
535
|
-
console.info();
|
|
536
|
-
console.info("Already have docker installed? Then you can run a full setup:");
|
|
537
|
-
console.info(colors2.cyan(` ${packageManager2} docker`));
|
|
538
|
-
console.info(colors2.cyan(` ${packageManager2} dev`));
|
|
539
|
-
console.info(colors2.cyan(` ${packageManager2} seed`));
|
|
540
|
-
console.info();
|
|
541
|
-
console.info(`Once running, you can sign in using:`);
|
|
542
|
-
console.info(`email: ${colors2.greenBright("admin-test@cellajs.com")}`);
|
|
543
|
-
console.info(`password: ${colors2.greenBright("12345678")}`);
|
|
544
|
-
console.info();
|
|
545
|
-
console.info(`For more info, check out: ${relativePath}/README.md`);
|
|
546
|
-
console.info(`Enjoy building ${projectName} using cella! \u{1F389}`);
|
|
547
|
-
console.info();
|
|
625
|
+
showSuccess(projectName, targetFolder, relativePath, needsCd, packageManager2);
|
|
548
626
|
}
|
|
549
627
|
|
|
550
|
-
// src/
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
console.info();
|
|
562
|
-
if (cli.options.skipNewBranch || cli.options.skipGit) {
|
|
563
|
-
cli.createNewBranch = false;
|
|
564
|
-
cli.newBranchName = null;
|
|
565
|
-
}
|
|
566
|
-
if (cli.options.skipGenerate === true) {
|
|
567
|
-
cli.options.skipGenerate = true;
|
|
628
|
+
// src/utils/detect-used-ports.ts
|
|
629
|
+
import { readdir, readFile } from "fs/promises";
|
|
630
|
+
import { dirname, join as join2 } from "path";
|
|
631
|
+
async function detectUsedPorts(targetFolder) {
|
|
632
|
+
const parentDir = dirname(targetFolder);
|
|
633
|
+
const used = [];
|
|
634
|
+
let siblings;
|
|
635
|
+
try {
|
|
636
|
+
siblings = await readdir(parentDir);
|
|
637
|
+
} catch {
|
|
638
|
+
return used;
|
|
568
639
|
}
|
|
569
|
-
|
|
570
|
-
|
|
640
|
+
for (const name of siblings) {
|
|
641
|
+
const configPath = join2(parentDir, name, "shared/config/config.development.ts");
|
|
642
|
+
try {
|
|
643
|
+
const content = await readFile(configPath, "utf8");
|
|
644
|
+
const feMatch = content.match(/frontendUrl:\s*'http:\/\/localhost:(\d+)'/);
|
|
645
|
+
const beMatch = content.match(/backendUrl:\s*'http:\/\/localhost:(\d+)'/);
|
|
646
|
+
if (feMatch && beMatch) {
|
|
647
|
+
const frontend = Number(feMatch[1]);
|
|
648
|
+
const backend = Number(beMatch[1]);
|
|
649
|
+
used.push({
|
|
650
|
+
project: name,
|
|
651
|
+
frontend,
|
|
652
|
+
backend,
|
|
653
|
+
offset: frontend - 3e3
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
} catch {
|
|
657
|
+
}
|
|
571
658
|
}
|
|
572
|
-
|
|
573
|
-
|
|
659
|
+
return used;
|
|
660
|
+
}
|
|
661
|
+
function findNextOffset(usedPorts) {
|
|
662
|
+
const usedOffsets = new Set(usedPorts.map((p) => p.offset));
|
|
663
|
+
for (let offset = 0; offset <= 490; offset += 10) {
|
|
664
|
+
if (!usedOffsets.has(offset)) return offset;
|
|
574
665
|
}
|
|
575
|
-
|
|
576
|
-
|
|
666
|
+
return 0;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/utils/extract-package-json-version-from-uri.ts
|
|
670
|
+
import axios from "axios";
|
|
671
|
+
async function extractPackageJsonVersionFromUri(repositoryUrl, branch = "main") {
|
|
672
|
+
const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
|
|
673
|
+
const packageJsonUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/package.json`;
|
|
674
|
+
try {
|
|
675
|
+
const response = await axios.get(packageJsonUrl);
|
|
676
|
+
const packageJson = response.data;
|
|
677
|
+
return packageJson.version || "unknown";
|
|
678
|
+
} catch (error) {
|
|
679
|
+
return "unknown";
|
|
577
680
|
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// src/utils/is-empty-directory.ts
|
|
684
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
685
|
+
async function isEmptyDirectory(path2) {
|
|
686
|
+
const files = await readdir2(path2);
|
|
687
|
+
return files.length === 0 || files.length === 1 && files[0] === ".git";
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/create-cella-cli.ts
|
|
691
|
+
async function main() {
|
|
692
|
+
const templateVersion = await extractPackageJsonVersionFromUri(TEMPLATE_URL);
|
|
693
|
+
showWelcome(templateVersion);
|
|
694
|
+
const promptTheme = { prefix: "", style: { answer: (text) => text } };
|
|
695
|
+
const promptContext = { clearPromptOnDone: true };
|
|
578
696
|
if (!cli.directory) {
|
|
579
|
-
cli.directory = await input(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
default: true
|
|
592
|
-
});
|
|
697
|
+
cli.directory = await input(
|
|
698
|
+
{
|
|
699
|
+
message: "Enter your project name",
|
|
700
|
+
default: "my-cella-app",
|
|
701
|
+
theme: promptTheme,
|
|
702
|
+
validate: (name) => {
|
|
703
|
+
const validation = validateProjectName(basename2(resolve3(name)));
|
|
704
|
+
return validation.valid ? true : `Invalid project name: ${validation.problems?.[0] ?? "unknown error"}`;
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
promptContext
|
|
708
|
+
);
|
|
593
709
|
}
|
|
594
|
-
if (!cli.newBranchName
|
|
595
|
-
cli.newBranchName =
|
|
596
|
-
message: "Enter the new branch name",
|
|
597
|
-
default: "development",
|
|
598
|
-
validate: (name) => {
|
|
599
|
-
const validation = validateProjectName(basename2(resolve2(name)));
|
|
600
|
-
return validation.valid ? true : `Invalid branch name: ${validation.problems[0]}`;
|
|
601
|
-
}
|
|
602
|
-
});
|
|
710
|
+
if (!cli.newBranchName) {
|
|
711
|
+
cli.newBranchName = "development";
|
|
603
712
|
}
|
|
604
|
-
const targetFolder =
|
|
605
|
-
const projectName = basename2(targetFolder);
|
|
713
|
+
const targetFolder = resolve3(cli.directory);
|
|
714
|
+
const projectName = basename2(targetFolder)?.toLowerCase() || "project";
|
|
606
715
|
if (existsSync2(targetFolder) && !await isEmptyDirectory(targetFolder)) {
|
|
607
716
|
const dirName = cli.directory === "." ? "Current directory" : `Target directory "${targetFolder}"`;
|
|
608
717
|
const message = `${dirName} is not empty. Please choose how you would like to proceed:`;
|
|
609
|
-
const action = await select(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
718
|
+
const action = await select(
|
|
719
|
+
{
|
|
720
|
+
message,
|
|
721
|
+
theme: promptTheme,
|
|
722
|
+
choices: [
|
|
723
|
+
{ name: "Cancel and exit", value: "cancel" },
|
|
724
|
+
{ name: "Ignore existing files and continue", value: "ignore" }
|
|
725
|
+
]
|
|
726
|
+
},
|
|
727
|
+
promptContext
|
|
728
|
+
);
|
|
616
729
|
if (action === "cancel") {
|
|
617
730
|
process.exit(1);
|
|
618
731
|
}
|
|
619
732
|
}
|
|
733
|
+
const portOffset = await promptPortOffset(targetFolder, promptTheme, promptContext);
|
|
734
|
+
const adminEmail = await input(
|
|
735
|
+
{
|
|
736
|
+
message: "Admin email for initial seed user",
|
|
737
|
+
default: `admin@${projectName}.com`,
|
|
738
|
+
theme: promptTheme
|
|
739
|
+
},
|
|
740
|
+
promptContext
|
|
741
|
+
);
|
|
620
742
|
const createOptions = {
|
|
621
743
|
projectName,
|
|
622
744
|
targetFolder,
|
|
623
745
|
newBranchName: cli.newBranchName,
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
packageManager: cli.packageManager
|
|
746
|
+
packageManager: cli.packageManager,
|
|
747
|
+
templateUrl: cli.options.template,
|
|
748
|
+
portOffset,
|
|
749
|
+
adminEmail
|
|
629
750
|
};
|
|
630
751
|
await create(createOptions);
|
|
631
752
|
}
|
|
632
753
|
main().catch(console.error);
|
|
754
|
+
function formatOffset(o, suffix = "") {
|
|
755
|
+
return `${o} \u2192 :${3e3 + o} / :${4e3 + o} / :${5432 + o}${suffix}`;
|
|
756
|
+
}
|
|
757
|
+
async function promptPortOffset(targetFolder, theme, context) {
|
|
758
|
+
const usedPorts = await detectUsedPorts(targetFolder);
|
|
759
|
+
const suggested = findNextOffset(usedPorts);
|
|
760
|
+
if (usedPorts.length > 0) {
|
|
761
|
+
console.info(pc5.dim("\nDetected cella forks in sibling directories:"));
|
|
762
|
+
for (const p of usedPorts) {
|
|
763
|
+
console.info(pc5.dim(` ${p.project}: frontend :${p.frontend}, backend :${p.backend} (offset ${p.offset})`));
|
|
764
|
+
}
|
|
765
|
+
console.info();
|
|
766
|
+
}
|
|
767
|
+
const presets = [0, 10, 20, 30].filter((o) => o !== suggested);
|
|
768
|
+
const choice = await select(
|
|
769
|
+
{
|
|
770
|
+
message: "Port offset (avoids conflicts with sibling forks)",
|
|
771
|
+
theme,
|
|
772
|
+
choices: [
|
|
773
|
+
...suggested > 0 ? [{ name: formatOffset(suggested, " (suggested)"), value: suggested }] : [],
|
|
774
|
+
{ name: formatOffset(0, " (default)"), value: 0 },
|
|
775
|
+
...presets.map((o) => ({ name: formatOffset(o), value: o })),
|
|
776
|
+
{ name: "Custom offset", value: -1 }
|
|
777
|
+
]
|
|
778
|
+
},
|
|
779
|
+
context
|
|
780
|
+
);
|
|
781
|
+
if (choice !== -1) return choice;
|
|
782
|
+
const custom = await input(
|
|
783
|
+
{
|
|
784
|
+
message: "Enter custom offset (0-490, multiples of 10)",
|
|
785
|
+
default: String(suggested),
|
|
786
|
+
theme,
|
|
787
|
+
validate: (val) => {
|
|
788
|
+
const n = Number(val);
|
|
789
|
+
if (Number.isNaN(n) || n < 0 || n > 490) return "Must be between 0 and 490";
|
|
790
|
+
if (n % 10 !== 0) return "Must be a multiple of 10";
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
context
|
|
795
|
+
);
|
|
796
|
+
return Number(custom);
|
|
797
|
+
}
|
|
633
798
|
//# sourceMappingURL=index.js.map
|