playcademy 0.14.28 → 0.14.30
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/dist/cli.d.ts +25 -0
- package/dist/cli.js +3637 -0
- package/dist/constants.d.ts +41 -5
- package/dist/constants.js +24 -3
- package/dist/db.js +1 -1997
- package/dist/index.d.ts +80 -1
- package/dist/index.js +2219 -3755
- package/dist/templates/auth/auth.ts.template +4 -4
- package/dist/templates/config/playcademy.config.js.template +1 -1
- package/dist/templates/config/playcademy.config.json.template +1 -1
- package/dist/templates/database/package.json.template +1 -1
- package/dist/utils.js +398 -2020
- package/dist/version.js +8 -1
- package/package.json +8 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3637 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// ../utils/src/package-json.ts
|
|
12
|
+
function getPackageNameFromData(pkg, fallback = "unknown-package") {
|
|
13
|
+
return pkg?.name || fallback;
|
|
14
|
+
}
|
|
15
|
+
function getPackageVersionFromData(pkg, fallback = "0.0.0") {
|
|
16
|
+
return pkg?.version || fallback;
|
|
17
|
+
}
|
|
18
|
+
function getPackageNameVersionFromData(pkg) {
|
|
19
|
+
const name = getPackageNameFromData(pkg);
|
|
20
|
+
const version2 = getPackageVersionFromData(pkg);
|
|
21
|
+
return `${name}@${version2}`;
|
|
22
|
+
}
|
|
23
|
+
function hasDependencyInData(pkg, packageName) {
|
|
24
|
+
if (!pkg) return false;
|
|
25
|
+
return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName] || pkg.peerDependencies?.[packageName]);
|
|
26
|
+
}
|
|
27
|
+
var init_package_json = __esm({
|
|
28
|
+
"../utils/src/package-json.ts"() {
|
|
29
|
+
"use strict";
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ../utils/src/file-loader.ts
|
|
34
|
+
var file_loader_exports = {};
|
|
35
|
+
__export(file_loader_exports, {
|
|
36
|
+
findFile: () => findFile,
|
|
37
|
+
findFilesByExtension: () => findFilesByExtension,
|
|
38
|
+
getCurrentDirectoryName: () => getCurrentDirectoryName,
|
|
39
|
+
getFileExtension: () => getFileExtension,
|
|
40
|
+
getPackageField: () => getPackageField,
|
|
41
|
+
getPackageName: () => getPackageName,
|
|
42
|
+
getPackageNameVersion: () => getPackageNameVersion,
|
|
43
|
+
getPackageVersion: () => getPackageVersion,
|
|
44
|
+
hasDependency: () => hasDependency,
|
|
45
|
+
loadFile: () => loadFile,
|
|
46
|
+
loadModule: () => loadModule,
|
|
47
|
+
loadPackageJson: () => loadPackageJson,
|
|
48
|
+
scanDirectory: () => scanDirectory
|
|
49
|
+
});
|
|
50
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
51
|
+
import { readFile } from "fs/promises";
|
|
52
|
+
import { dirname, parse, resolve } from "path";
|
|
53
|
+
function findFilePath(filename, startDir, maxLevels = 3) {
|
|
54
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
55
|
+
let currentDir2 = resolve(startDir);
|
|
56
|
+
let levelsSearched = 0;
|
|
57
|
+
while (levelsSearched <= maxLevels) {
|
|
58
|
+
for (const fname of filenames) {
|
|
59
|
+
const filePath = resolve(currentDir2, fname);
|
|
60
|
+
if (existsSync(filePath)) {
|
|
61
|
+
return {
|
|
62
|
+
path: filePath,
|
|
63
|
+
dir: currentDir2,
|
|
64
|
+
filename: fname
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (levelsSearched >= maxLevels) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
const parentDir = dirname(currentDir2);
|
|
72
|
+
if (parentDir === currentDir2) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const parsed = parse(currentDir2);
|
|
76
|
+
if (parsed.root === currentDir2) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
currentDir2 = parentDir;
|
|
80
|
+
levelsSearched++;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
async function loadFile(filename, options = {}) {
|
|
85
|
+
const {
|
|
86
|
+
cwd = process.cwd(),
|
|
87
|
+
required = false,
|
|
88
|
+
searchUp = false,
|
|
89
|
+
maxLevels = 3,
|
|
90
|
+
parseJson = false,
|
|
91
|
+
stripComments = false
|
|
92
|
+
} = options;
|
|
93
|
+
let fileResult;
|
|
94
|
+
if (searchUp) {
|
|
95
|
+
fileResult = findFilePath(filename, cwd, maxLevels);
|
|
96
|
+
} else {
|
|
97
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
98
|
+
fileResult = null;
|
|
99
|
+
for (const fname of filenames) {
|
|
100
|
+
const filePath = resolve(cwd, fname);
|
|
101
|
+
if (existsSync(filePath)) {
|
|
102
|
+
fileResult = {
|
|
103
|
+
path: filePath,
|
|
104
|
+
dir: cwd,
|
|
105
|
+
filename: fname
|
|
106
|
+
};
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!fileResult) {
|
|
112
|
+
if (required) {
|
|
113
|
+
const fileList = Array.isArray(filename) ? filename.join(" or ") : filename;
|
|
114
|
+
const message = searchUp ? `${fileList} not found in ${cwd} or up to ${maxLevels} parent directories` : `${fileList} not found at ${cwd}`;
|
|
115
|
+
throw new Error(message);
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
let content = await readFile(fileResult.path, "utf-8");
|
|
121
|
+
if (parseJson) {
|
|
122
|
+
if (stripComments) {
|
|
123
|
+
content = stripJsonComments(content);
|
|
124
|
+
}
|
|
125
|
+
return JSON.parse(content);
|
|
126
|
+
}
|
|
127
|
+
return content;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Failed to load ${fileResult.filename} from ${fileResult.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function findFile(filename, options = {}) {
|
|
135
|
+
const { cwd = process.cwd(), searchUp = false, maxLevels = 3 } = options;
|
|
136
|
+
if (searchUp) {
|
|
137
|
+
return findFilePath(filename, cwd, maxLevels);
|
|
138
|
+
}
|
|
139
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
140
|
+
for (const fname of filenames) {
|
|
141
|
+
const filePath = resolve(cwd, fname);
|
|
142
|
+
if (existsSync(filePath)) {
|
|
143
|
+
return {
|
|
144
|
+
path: filePath,
|
|
145
|
+
dir: cwd,
|
|
146
|
+
filename: fname
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
async function loadPackageJson(options = {}) {
|
|
153
|
+
return loadFile("package.json", { ...options, parseJson: true });
|
|
154
|
+
}
|
|
155
|
+
async function getPackageName(options) {
|
|
156
|
+
const pkg = await loadPackageJson(options);
|
|
157
|
+
const cwd = options?.cwd || process.cwd();
|
|
158
|
+
const parts = cwd.split(/[/\\]/);
|
|
159
|
+
const dirName = parts[parts.length - 1] || "unknown-package";
|
|
160
|
+
return getPackageNameFromData(pkg, dirName);
|
|
161
|
+
}
|
|
162
|
+
async function getPackageVersion(options) {
|
|
163
|
+
const pkg = await loadPackageJson(options);
|
|
164
|
+
return getPackageVersionFromData(pkg);
|
|
165
|
+
}
|
|
166
|
+
async function getPackageField(field, options) {
|
|
167
|
+
const pkg = await loadPackageJson(options);
|
|
168
|
+
return pkg?.[field];
|
|
169
|
+
}
|
|
170
|
+
async function hasDependency(packageName, options) {
|
|
171
|
+
const pkg = await loadPackageJson(options);
|
|
172
|
+
return hasDependencyInData(pkg, packageName);
|
|
173
|
+
}
|
|
174
|
+
async function getPackageNameVersion(options) {
|
|
175
|
+
const pkg = await loadPackageJson(options);
|
|
176
|
+
if (!pkg) {
|
|
177
|
+
throw new Error("No package.json found");
|
|
178
|
+
}
|
|
179
|
+
return getPackageNameVersionFromData(pkg);
|
|
180
|
+
}
|
|
181
|
+
function getCurrentDirectoryName(fallback = "unknown-directory") {
|
|
182
|
+
const cwd = process.cwd();
|
|
183
|
+
const dirName = cwd.split("/").pop();
|
|
184
|
+
return dirName || fallback;
|
|
185
|
+
}
|
|
186
|
+
function getFileExtension(path2) {
|
|
187
|
+
return path2.split(".").pop()?.toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
function stripJsonComments(jsonc) {
|
|
190
|
+
let result = jsonc.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
191
|
+
result = result.replace(/\/\/.*/g, "");
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
async function loadModule(filename, options = {}) {
|
|
195
|
+
const { cwd = process.cwd(), required = false, searchUp = false, maxLevels = 3 } = options;
|
|
196
|
+
let fileResult;
|
|
197
|
+
if (searchUp) {
|
|
198
|
+
fileResult = findFilePath(filename, cwd, maxLevels);
|
|
199
|
+
} else {
|
|
200
|
+
const filenames = Array.isArray(filename) ? filename : [filename];
|
|
201
|
+
fileResult = null;
|
|
202
|
+
for (const fname of filenames) {
|
|
203
|
+
const filePath = resolve(cwd, fname);
|
|
204
|
+
if (existsSync(filePath)) {
|
|
205
|
+
fileResult = {
|
|
206
|
+
path: filePath,
|
|
207
|
+
dir: cwd,
|
|
208
|
+
filename: fname
|
|
209
|
+
};
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!fileResult) {
|
|
215
|
+
if (required) {
|
|
216
|
+
const fileList = Array.isArray(filename) ? filename.join(" or ") : filename;
|
|
217
|
+
const message = searchUp ? `${fileList} not found in ${cwd} or up to ${maxLevels} parent directories` : `${fileList} not found at ${cwd}`;
|
|
218
|
+
throw new Error(message);
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const ext = getFileExtension(fileResult.filename);
|
|
224
|
+
if (ext === "js" || ext === "mjs" || ext === "cjs") {
|
|
225
|
+
const module = await import(fileResult.path);
|
|
226
|
+
return module.default || module;
|
|
227
|
+
} else {
|
|
228
|
+
throw new Error(`Unsupported module type: ${ext} (only .js, .mjs, .cjs supported)`);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Failed to load module ${fileResult.filename} from ${fileResult.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function scanDirectory(dir, options = {}) {
|
|
237
|
+
const { extensions, allowMissing = false } = options;
|
|
238
|
+
const files = [];
|
|
239
|
+
if (!existsSync(dir)) {
|
|
240
|
+
if (allowMissing) return [];
|
|
241
|
+
throw new Error(`Directory not found: ${dir}`);
|
|
242
|
+
}
|
|
243
|
+
function scan(currentDir2, basePath = "") {
|
|
244
|
+
try {
|
|
245
|
+
const entries = readdirSync(currentDir2);
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
const fullPath = resolve(currentDir2, entry);
|
|
248
|
+
const relativePath = basePath ? `${basePath}/${entry}` : entry;
|
|
249
|
+
const stat = statSync(fullPath);
|
|
250
|
+
if (stat.isDirectory()) {
|
|
251
|
+
scan(fullPath, relativePath);
|
|
252
|
+
} else if (stat.isFile()) {
|
|
253
|
+
if (extensions) {
|
|
254
|
+
const ext = getFileExtension(entry);
|
|
255
|
+
if (!ext || !extensions.includes(ext)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
files.push(relativePath);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
scan(dir);
|
|
266
|
+
return files;
|
|
267
|
+
}
|
|
268
|
+
function findFilesByExtension(dir, extension) {
|
|
269
|
+
if (!existsSync(dir)) {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
const ext = extension.toLowerCase().replace(/^\./, "");
|
|
273
|
+
const files = [];
|
|
274
|
+
try {
|
|
275
|
+
const entries = readdirSync(dir);
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
const fullPath = resolve(dir, entry);
|
|
278
|
+
const stat = statSync(fullPath);
|
|
279
|
+
if (stat.isFile()) {
|
|
280
|
+
const fileExt = getFileExtension(entry);
|
|
281
|
+
if (fileExt === ext) {
|
|
282
|
+
files.push(fullPath);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
return files;
|
|
289
|
+
}
|
|
290
|
+
var init_file_loader = __esm({
|
|
291
|
+
"../utils/src/file-loader.ts"() {
|
|
292
|
+
"use strict";
|
|
293
|
+
init_package_json();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// src/lib/core/error.ts
|
|
298
|
+
import { bold as bold2, dim as dim2, redBright } from "colorette";
|
|
299
|
+
import { ApiError, extractApiErrorInfo } from "@playcademy/sdk/internal";
|
|
300
|
+
|
|
301
|
+
// src/lib/core/logger.ts
|
|
302
|
+
import {
|
|
303
|
+
blue,
|
|
304
|
+
blueBright,
|
|
305
|
+
bold,
|
|
306
|
+
cyan,
|
|
307
|
+
dim,
|
|
308
|
+
gray,
|
|
309
|
+
green,
|
|
310
|
+
greenBright,
|
|
311
|
+
red,
|
|
312
|
+
underline,
|
|
313
|
+
yellow,
|
|
314
|
+
yellowBright
|
|
315
|
+
} from "colorette";
|
|
316
|
+
import { colorize } from "json-colorizer";
|
|
317
|
+
function customTransform(text) {
|
|
318
|
+
let result = text;
|
|
319
|
+
result = result.replace(/`([^`]+)`/g, (_, code) => greenBright(code));
|
|
320
|
+
result = result.replace(/<<([^>]+)>>/g, (_, url) => blueBright(underline(url)));
|
|
321
|
+
result = result.replace(/<([^>]+)>/g, (_, path2) => blueBright(path2));
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
function formatTable(data, title) {
|
|
325
|
+
const ANSI_REGEX = /\u001B\[[0-9;]*m/g;
|
|
326
|
+
const stripAnsi2 = (value) => value.replace(ANSI_REGEX, "");
|
|
327
|
+
const visibleLength = (value) => stripAnsi2(value).length;
|
|
328
|
+
const padCell = (value, width) => {
|
|
329
|
+
const length = visibleLength(value);
|
|
330
|
+
if (length >= width) return value;
|
|
331
|
+
return value + " ".repeat(width - length);
|
|
332
|
+
};
|
|
333
|
+
if (data.length === 0) return;
|
|
334
|
+
const keys = Object.keys(data[0]);
|
|
335
|
+
const rows = data.map((item) => keys.map((key) => String(item[key] ?? "")));
|
|
336
|
+
const widths = keys.map((key, i) => {
|
|
337
|
+
const headerWidth = visibleLength(key);
|
|
338
|
+
const dataWidth = Math.max(...rows.map((row) => visibleLength(row[i])));
|
|
339
|
+
return Math.max(headerWidth, dataWidth);
|
|
340
|
+
});
|
|
341
|
+
const totalWidth = widths.reduce((sum, w) => sum + w + 3, -1);
|
|
342
|
+
const separator = "\u251C" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
|
|
343
|
+
const topBorder = "\u250C" + "\u2500".repeat(totalWidth) + "\u2510";
|
|
344
|
+
const titleSeparator = "\u251C" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2524";
|
|
345
|
+
const bottomBorder = "\u2514" + widths.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
|
|
346
|
+
console.log(topBorder);
|
|
347
|
+
if (title) {
|
|
348
|
+
const titleText = bold(title);
|
|
349
|
+
const titlePadding = totalWidth - title.length - 1;
|
|
350
|
+
const titleRow = "\u2502 " + titleText + " ".repeat(titlePadding) + "\u2502";
|
|
351
|
+
console.log(titleRow);
|
|
352
|
+
console.log(titleSeparator);
|
|
353
|
+
}
|
|
354
|
+
const header = "\u2502 " + keys.map((key, i) => padCell(key, widths[i])).join(" \u2502 ") + " \u2502";
|
|
355
|
+
console.log(header);
|
|
356
|
+
console.log(separator);
|
|
357
|
+
rows.forEach((row) => {
|
|
358
|
+
const dataRow = "\u2502 " + row.map((cell, i) => padCell(cell, widths[i])).join(" \u2502 ") + " \u2502";
|
|
359
|
+
console.log(dataRow);
|
|
360
|
+
});
|
|
361
|
+
console.log(bottomBorder);
|
|
362
|
+
}
|
|
363
|
+
var logger = {
|
|
364
|
+
table: (data, title) => {
|
|
365
|
+
formatTable(data, title);
|
|
366
|
+
},
|
|
367
|
+
/**
|
|
368
|
+
* Info message - general information
|
|
369
|
+
*/
|
|
370
|
+
info: (message, indent = 0) => {
|
|
371
|
+
const spaces = " ".repeat(indent);
|
|
372
|
+
console.log(`${spaces}${blue("\u2139")} ${bold(customTransform(message))}`);
|
|
373
|
+
},
|
|
374
|
+
/**
|
|
375
|
+
* Admonition - highlighted note/tip/warning box (Docusaurus-style)
|
|
376
|
+
*/
|
|
377
|
+
admonition: (type, title, lines, indent = 0) => {
|
|
378
|
+
const spaces = " ".repeat(indent);
|
|
379
|
+
const configs = {
|
|
380
|
+
note: { color: green },
|
|
381
|
+
tip: { color: cyan },
|
|
382
|
+
info: { color: blue },
|
|
383
|
+
warning: { color: yellow }
|
|
384
|
+
};
|
|
385
|
+
const { color: color2 } = configs[type];
|
|
386
|
+
console.log(`${spaces}${color2("\u250C\u2500")} ${bold(color2(title.toUpperCase()))}`);
|
|
387
|
+
if (lines && lines.length > 0) {
|
|
388
|
+
lines.forEach((line) => {
|
|
389
|
+
console.log(`${spaces}${color2("\u2502")} ${customTransform(line)}`);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
console.log(`${spaces}${color2("\u2514\u2500")}`);
|
|
393
|
+
},
|
|
394
|
+
/**
|
|
395
|
+
* Dim message - less important information
|
|
396
|
+
*/
|
|
397
|
+
dim: (message, indent = 0) => {
|
|
398
|
+
const spaces = " ".repeat(indent);
|
|
399
|
+
console.log(`${spaces}${dim(customTransform(message))}`);
|
|
400
|
+
},
|
|
401
|
+
/**
|
|
402
|
+
* Success message - operation completed successfully
|
|
403
|
+
*/
|
|
404
|
+
success: (message, indent = 0) => {
|
|
405
|
+
const spaces = " ".repeat(indent);
|
|
406
|
+
console.log(`${spaces}${green("\u2714")} ${bold(customTransform(message))}`);
|
|
407
|
+
},
|
|
408
|
+
remark: (message, indent = 0) => {
|
|
409
|
+
const spaces = " ".repeat(indent);
|
|
410
|
+
console.log(`${spaces}${bold(yellowBright("\u2726"))} ${bold(customTransform(message))}`);
|
|
411
|
+
},
|
|
412
|
+
/**
|
|
413
|
+
* Error message - operation failed
|
|
414
|
+
*/
|
|
415
|
+
error: (message, indent = 0) => {
|
|
416
|
+
const spaces = " ".repeat(indent);
|
|
417
|
+
console.error(`${spaces}${red("\u2716")} ${customTransform(message)}`);
|
|
418
|
+
},
|
|
419
|
+
bold: (message, indent = 0) => {
|
|
420
|
+
const spaces = " ".repeat(indent);
|
|
421
|
+
console.log(`${spaces}${bold(customTransform(message))}`);
|
|
422
|
+
},
|
|
423
|
+
/**
|
|
424
|
+
* Warning message - something to be aware of
|
|
425
|
+
*/
|
|
426
|
+
warn: (message, indent = 0) => {
|
|
427
|
+
const spaces = " ".repeat(indent);
|
|
428
|
+
console.warn(`${spaces}${yellow("\u26A0")} ${bold(customTransform(message))}`);
|
|
429
|
+
},
|
|
430
|
+
/**
|
|
431
|
+
* Debug message - only shown when DEBUG env var is set
|
|
432
|
+
*/
|
|
433
|
+
debug: (message, indent = 0) => {
|
|
434
|
+
const spaces = " ".repeat(indent);
|
|
435
|
+
if (process.env.DEBUG) {
|
|
436
|
+
console.log(gray("[DEBUG]"), `${spaces}${message}`);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
/**
|
|
440
|
+
* Step message - shows progress through a process
|
|
441
|
+
*/
|
|
442
|
+
step: (step, total, message, indent = 0) => {
|
|
443
|
+
const spaces = " ".repeat(indent);
|
|
444
|
+
console.log(spaces + cyan(`[${step}/${total}]`), customTransform(message));
|
|
445
|
+
},
|
|
446
|
+
/**
|
|
447
|
+
* Highlighted message - draws attention
|
|
448
|
+
*/
|
|
449
|
+
highlight: (message, indent = 0) => {
|
|
450
|
+
const spaces = " ".repeat(indent);
|
|
451
|
+
console.log(bold(`${spaces}${cyan(customTransform(message))}`));
|
|
452
|
+
},
|
|
453
|
+
/**
|
|
454
|
+
* Aside message - for side information
|
|
455
|
+
*/
|
|
456
|
+
aside: (message, indent = 0) => {
|
|
457
|
+
const spaces = " ".repeat(indent);
|
|
458
|
+
console.log(`${spaces}${bold(customTransform(message))}`);
|
|
459
|
+
},
|
|
460
|
+
/**
|
|
461
|
+
* Data display - for structured data output
|
|
462
|
+
*/
|
|
463
|
+
data: (label, value, indent = 0) => {
|
|
464
|
+
const spaces = " ".repeat(indent);
|
|
465
|
+
if (value !== void 0) {
|
|
466
|
+
console.log(`${spaces}${dim(label + ":")} ${bold(value)}`);
|
|
467
|
+
} else {
|
|
468
|
+
console.log(`${spaces}${dim(label)}`);
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
/**
|
|
472
|
+
* Size change display - shows old size → new size with delta
|
|
473
|
+
* @param label - The label for the size change (e.g., 'Build', 'Bundle')
|
|
474
|
+
* @param previousSize - Previous size in bytes
|
|
475
|
+
* @param currentSize - Current size in bytes
|
|
476
|
+
* @param formatSize - Function to format size in bytes
|
|
477
|
+
* @param formatDelta - Function to format size delta
|
|
478
|
+
* @param indent - Indentation level
|
|
479
|
+
*/
|
|
480
|
+
sizeChange: (label, previousSize, currentSize, formatSize, formatDelta, indent = 0) => {
|
|
481
|
+
const spaces = " ".repeat(indent);
|
|
482
|
+
const oldSize = formatSize(previousSize);
|
|
483
|
+
const newSize = formatSize(currentSize);
|
|
484
|
+
const delta = dim(`(${formatDelta(currentSize - previousSize)})`);
|
|
485
|
+
const value = `${red(oldSize)} \u2192 ${green(newSize)} ${delta}`;
|
|
486
|
+
console.log(`${spaces}${dim(label + ":")} ${bold(value)}`);
|
|
487
|
+
},
|
|
488
|
+
/**
|
|
489
|
+
* JSON output - pretty-printed JSON
|
|
490
|
+
*/
|
|
491
|
+
json: (data, indent = 0) => {
|
|
492
|
+
const spaces = " ".repeat(indent);
|
|
493
|
+
const jsonString = colorize(JSON.stringify(data, null, 2));
|
|
494
|
+
jsonString.split("\n").forEach((line) => {
|
|
495
|
+
console.log(`${spaces}${line}`);
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
/**
|
|
499
|
+
* New line
|
|
500
|
+
*/
|
|
501
|
+
newLine: () => {
|
|
502
|
+
console.log();
|
|
503
|
+
},
|
|
504
|
+
/**
|
|
505
|
+
* Raw output - no formatting, useful for ASCII art or pre-formatted text
|
|
506
|
+
*/
|
|
507
|
+
raw: (text, indent = 0) => {
|
|
508
|
+
const spaces = " ".repeat(indent);
|
|
509
|
+
console.log(`${spaces}${text}`);
|
|
510
|
+
},
|
|
511
|
+
customRaw: (text, indent = 0) => {
|
|
512
|
+
const spaces = " ".repeat(indent);
|
|
513
|
+
console.log(`${spaces}${customTransform(text)}`);
|
|
514
|
+
},
|
|
515
|
+
/**
|
|
516
|
+
* Display a configuration error with helpful suggestions
|
|
517
|
+
*/
|
|
518
|
+
configError: (error, indent = 0) => {
|
|
519
|
+
formatError(error, indent);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// src/lib/core/error.ts
|
|
524
|
+
function isConfigError(error) {
|
|
525
|
+
return error !== null && typeof error === "object" && "name" in error && error.name === "ConfigError" && "message" in error;
|
|
526
|
+
}
|
|
527
|
+
function extractEmbeddedJson(message) {
|
|
528
|
+
const jsonMatch = message.match(/(\{.+\})$/);
|
|
529
|
+
if (!jsonMatch) {
|
|
530
|
+
return { cleanMessage: message };
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const json = JSON.parse(jsonMatch[1]);
|
|
534
|
+
const cleanMessage = message.slice(0, jsonMatch.index).trim();
|
|
535
|
+
return { cleanMessage, json };
|
|
536
|
+
} catch {
|
|
537
|
+
return { cleanMessage: message };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function cleanMessageSuffix(message) {
|
|
541
|
+
let cleaned = message.replace(/:\s*\d{3}\s*$/, "").trim();
|
|
542
|
+
if (cleaned.endsWith(":")) {
|
|
543
|
+
cleaned = cleaned.slice(0, -1).trim();
|
|
544
|
+
}
|
|
545
|
+
return cleaned;
|
|
546
|
+
}
|
|
547
|
+
function removeStatusPrefix(message, statusCode) {
|
|
548
|
+
if (message.startsWith(`${statusCode} `)) {
|
|
549
|
+
return message.slice(`${statusCode} `.length);
|
|
550
|
+
}
|
|
551
|
+
return message;
|
|
552
|
+
}
|
|
553
|
+
function displayApiError(error, indent) {
|
|
554
|
+
const spaces = " ".repeat(indent);
|
|
555
|
+
const errorInfo = extractApiErrorInfo(error);
|
|
556
|
+
if (!errorInfo) {
|
|
557
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(error.message)}`);
|
|
558
|
+
if (process.env.DEBUG && error.details) {
|
|
559
|
+
console.error("");
|
|
560
|
+
logger.json(error.details, indent + 1);
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const statusCode = errorInfo.status;
|
|
565
|
+
let displayMessage = errorInfo.statusText;
|
|
566
|
+
displayMessage = removeStatusPrefix(displayMessage, statusCode);
|
|
567
|
+
const { cleanMessage, json: embeddedJson } = extractEmbeddedJson(displayMessage);
|
|
568
|
+
displayMessage = cleanMessageSuffix(cleanMessage);
|
|
569
|
+
let errorCode;
|
|
570
|
+
if (error.details && typeof error.details === "object") {
|
|
571
|
+
const details = error.details;
|
|
572
|
+
if ("code" in details && typeof details.code === "string") {
|
|
573
|
+
errorCode = details.code;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
let errorHeader = "API Error";
|
|
577
|
+
if (errorCode) {
|
|
578
|
+
errorHeader += ` ${redBright(`[${errorCode}]`)}`;
|
|
579
|
+
}
|
|
580
|
+
errorHeader += `: ${displayMessage} ${redBright(`[${statusCode}]`)}`;
|
|
581
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(errorHeader)}`);
|
|
582
|
+
if (process.env.DEBUG) {
|
|
583
|
+
const detailsToShow = embeddedJson || error.details || errorInfo.details;
|
|
584
|
+
if (detailsToShow) {
|
|
585
|
+
console.error("");
|
|
586
|
+
logger.json(detailsToShow, indent + 1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function displayConfigError(error, indent) {
|
|
591
|
+
const spaces = " ".repeat(indent);
|
|
592
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(error.message)}`);
|
|
593
|
+
if (error.field) {
|
|
594
|
+
console.error(`${spaces} ${dim2("Field:")} ${error.field}`);
|
|
595
|
+
}
|
|
596
|
+
if (error.suggestion) {
|
|
597
|
+
console.error(`${spaces} ${dim2("Fix:")} ${error.suggestion}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function displayGenericError(error, indent) {
|
|
601
|
+
const spaces = " ".repeat(indent);
|
|
602
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(error.message)}`);
|
|
603
|
+
if (error.stack && process.env.DEBUG) {
|
|
604
|
+
console.error(`${spaces} ${dim2("Stack:")}`);
|
|
605
|
+
console.error(
|
|
606
|
+
error.stack.split("\n").slice(1).map((line) => `${spaces} ${dim2(line)}`).join("\n")
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function formatError(error, indent = 0) {
|
|
611
|
+
const spaces = " ".repeat(indent);
|
|
612
|
+
if (error instanceof ApiError) {
|
|
613
|
+
displayApiError(error, indent);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (isConfigError(error)) {
|
|
617
|
+
displayConfigError(error, indent);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (error instanceof Error) {
|
|
621
|
+
displayGenericError(error, indent);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
console.error(`${spaces}${redBright("\u2716")} ${bold2(String(error))}`);
|
|
625
|
+
}
|
|
626
|
+
function setupGlobalErrorHandlers() {
|
|
627
|
+
let sigintReceived = false;
|
|
628
|
+
process.on("SIGINT", () => {
|
|
629
|
+
if (sigintReceived) {
|
|
630
|
+
process.exit(130);
|
|
631
|
+
}
|
|
632
|
+
sigintReceived = true;
|
|
633
|
+
setImmediate(() => {
|
|
634
|
+
if (sigintReceived) {
|
|
635
|
+
process.exit(130);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
const isExitPromptError = (error) => {
|
|
640
|
+
return error instanceof Error && error.name === "ExitPromptError" && error.message.includes("force closed the prompt");
|
|
641
|
+
};
|
|
642
|
+
process.on("uncaughtException", (error) => {
|
|
643
|
+
if (isExitPromptError(error)) {
|
|
644
|
+
process.exit(0);
|
|
645
|
+
}
|
|
646
|
+
console.error(error);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
});
|
|
649
|
+
process.on("unhandledRejection", (reason) => {
|
|
650
|
+
if (isExitPromptError(reason)) {
|
|
651
|
+
process.exit(0);
|
|
652
|
+
}
|
|
653
|
+
console.error(reason);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/lib/init/run.ts
|
|
659
|
+
import { execSync as execSync5 } from "child_process";
|
|
660
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
661
|
+
import { resolve as resolve7 } from "path";
|
|
662
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
663
|
+
|
|
664
|
+
// ../utils/src/ansi.ts
|
|
665
|
+
var colors = {
|
|
666
|
+
black: "\x1B[30m",
|
|
667
|
+
red: "\x1B[31m",
|
|
668
|
+
green: "\x1B[32m",
|
|
669
|
+
yellow: "\x1B[33m",
|
|
670
|
+
blue: "\x1B[34m",
|
|
671
|
+
magenta: "\x1B[35m",
|
|
672
|
+
cyan: "\x1B[36m",
|
|
673
|
+
white: "\x1B[37m",
|
|
674
|
+
gray: "\x1B[90m"
|
|
675
|
+
};
|
|
676
|
+
var styles = {
|
|
677
|
+
reset: "\x1B[0m",
|
|
678
|
+
bold: "\x1B[1m",
|
|
679
|
+
dim: "\x1B[2m",
|
|
680
|
+
italic: "\x1B[3m",
|
|
681
|
+
underline: "\x1B[4m"
|
|
682
|
+
};
|
|
683
|
+
var cursor = {
|
|
684
|
+
hide: "\x1B[?25l",
|
|
685
|
+
show: "\x1B[?25h",
|
|
686
|
+
up: (n) => `\x1B[${n}A`,
|
|
687
|
+
down: (n) => `\x1B[${n}B`,
|
|
688
|
+
forward: (n) => `\x1B[${n}C`,
|
|
689
|
+
back: (n) => `\x1B[${n}D`,
|
|
690
|
+
clearLine: "\x1B[K",
|
|
691
|
+
clearScreen: "\x1B[2J",
|
|
692
|
+
home: "\x1B[H"
|
|
693
|
+
};
|
|
694
|
+
function color(text, colorCode) {
|
|
695
|
+
return `${colorCode}${text}${styles.reset}`;
|
|
696
|
+
}
|
|
697
|
+
function dim3(text) {
|
|
698
|
+
return color(text, styles.dim);
|
|
699
|
+
}
|
|
700
|
+
function bold3(text) {
|
|
701
|
+
return color(text, styles.bold);
|
|
702
|
+
}
|
|
703
|
+
var isInteractive = typeof process !== "undefined" && process.stdout?.isTTY && !process.env.CI && process.env.TERM !== "dumb";
|
|
704
|
+
function stripAnsi(text) {
|
|
705
|
+
return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ../utils/src/spinner.ts
|
|
709
|
+
import { stdout } from "process";
|
|
710
|
+
var SPINNER_FRAMES = [
|
|
711
|
+
10251,
|
|
712
|
+
10265,
|
|
713
|
+
10297,
|
|
714
|
+
10296,
|
|
715
|
+
10300,
|
|
716
|
+
10292,
|
|
717
|
+
10278,
|
|
718
|
+
10279,
|
|
719
|
+
10247,
|
|
720
|
+
10255
|
|
721
|
+
].map((code) => String.fromCodePoint(code));
|
|
722
|
+
var CHECK_MARK = String.fromCodePoint(10004);
|
|
723
|
+
var CROSS_MARK = String.fromCodePoint(10006);
|
|
724
|
+
var SPINNER_INTERVAL = 80;
|
|
725
|
+
var Spinner = class {
|
|
726
|
+
tasks = /* @__PURE__ */ new Map();
|
|
727
|
+
frameIndex = 0;
|
|
728
|
+
intervalId = null;
|
|
729
|
+
renderCount = 0;
|
|
730
|
+
previousLineCount = 0;
|
|
731
|
+
printedTasks = /* @__PURE__ */ new Set();
|
|
732
|
+
indent;
|
|
733
|
+
constructor(taskIds, texts, options) {
|
|
734
|
+
this.indent = options?.indent ?? 0;
|
|
735
|
+
taskIds.forEach((id, index) => {
|
|
736
|
+
this.tasks.set(id, {
|
|
737
|
+
text: texts[index] || "",
|
|
738
|
+
status: "pending"
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
start() {
|
|
743
|
+
if (isInteractive) {
|
|
744
|
+
stdout.write(cursor.hide);
|
|
745
|
+
this.render();
|
|
746
|
+
this.intervalId = setInterval(() => {
|
|
747
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
748
|
+
this.render();
|
|
749
|
+
}, SPINNER_INTERVAL);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
updateTask(taskId, status, finalText) {
|
|
753
|
+
const task = this.tasks.get(taskId);
|
|
754
|
+
if (task) {
|
|
755
|
+
task.status = status;
|
|
756
|
+
if (finalText) task.finalText = finalText;
|
|
757
|
+
if (!isInteractive) {
|
|
758
|
+
this.renderNonInteractive(taskId, task);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
renderNonInteractive(taskId, task) {
|
|
763
|
+
const key = `${taskId}-${task.status}`;
|
|
764
|
+
if (this.printedTasks.has(key)) return;
|
|
765
|
+
this.printedTasks.add(key);
|
|
766
|
+
const indentStr = " ".repeat(this.indent);
|
|
767
|
+
let line = "";
|
|
768
|
+
switch (task.status) {
|
|
769
|
+
case "running":
|
|
770
|
+
line = `${indentStr}[RUNNING] ${stripAnsi(task.text)}`;
|
|
771
|
+
break;
|
|
772
|
+
case "success":
|
|
773
|
+
line = `${indentStr}[SUCCESS] ${stripAnsi(task.finalText || task.text)}`;
|
|
774
|
+
break;
|
|
775
|
+
case "error":
|
|
776
|
+
line = `${indentStr}[ERROR] Failed: ${stripAnsi(task.text)}`;
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
console.log(line);
|
|
780
|
+
}
|
|
781
|
+
render() {
|
|
782
|
+
if (this.previousLineCount > 0) {
|
|
783
|
+
stdout.write(cursor.up(this.previousLineCount));
|
|
784
|
+
}
|
|
785
|
+
const spinner = SPINNER_FRAMES[this.frameIndex];
|
|
786
|
+
const indentStr = " ".repeat(this.indent);
|
|
787
|
+
const visibleTasks = Array.from(this.tasks.values()).filter(
|
|
788
|
+
(task) => task.status !== "pending"
|
|
789
|
+
);
|
|
790
|
+
for (const task of visibleTasks) {
|
|
791
|
+
stdout.write(`\r${cursor.clearLine}`);
|
|
792
|
+
let line = "";
|
|
793
|
+
switch (task.status) {
|
|
794
|
+
case "running":
|
|
795
|
+
line = `${indentStr}${colors.blue}${spinner}${styles.reset} ${task.text}`;
|
|
796
|
+
break;
|
|
797
|
+
case "success":
|
|
798
|
+
line = `${indentStr}${colors.green}${CHECK_MARK}${styles.reset} ${task.finalText || task.text}`;
|
|
799
|
+
break;
|
|
800
|
+
case "error":
|
|
801
|
+
line = `${indentStr}${colors.red}${CROSS_MARK}${styles.reset} Failed: ${task.text}`;
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
console.log(line);
|
|
805
|
+
}
|
|
806
|
+
this.previousLineCount = visibleTasks.length;
|
|
807
|
+
this.renderCount++;
|
|
808
|
+
}
|
|
809
|
+
stop() {
|
|
810
|
+
if (this.intervalId) {
|
|
811
|
+
clearInterval(this.intervalId);
|
|
812
|
+
this.intervalId = null;
|
|
813
|
+
}
|
|
814
|
+
if (isInteractive) {
|
|
815
|
+
this.render();
|
|
816
|
+
stdout.write(cursor.show);
|
|
817
|
+
} else {
|
|
818
|
+
this.tasks.forEach((task, taskId) => {
|
|
819
|
+
if (task.status !== "pending") {
|
|
820
|
+
this.renderNonInteractive(taskId, task);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// ../utils/src/log.ts
|
|
828
|
+
function formatDuration(ms) {
|
|
829
|
+
const duration = ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(2)}s`;
|
|
830
|
+
return bold3(dim3(`[${duration}]`));
|
|
831
|
+
}
|
|
832
|
+
async function runStep(text, action, successText, options) {
|
|
833
|
+
const effectiveAction = action ?? (async () => void 0);
|
|
834
|
+
const hasAction = action !== void 0;
|
|
835
|
+
const indent = options?.indent ?? 0;
|
|
836
|
+
const spinner = new Spinner(["task"], [text], { indent });
|
|
837
|
+
if (!options?.silent) {
|
|
838
|
+
spinner.start();
|
|
839
|
+
spinner.updateTask("task", "running");
|
|
840
|
+
}
|
|
841
|
+
const startTime = performance.now();
|
|
842
|
+
try {
|
|
843
|
+
const result = await effectiveAction();
|
|
844
|
+
if (options?.silent) {
|
|
845
|
+
return result;
|
|
846
|
+
}
|
|
847
|
+
const duration = performance.now() - startTime;
|
|
848
|
+
let finalSuccessText;
|
|
849
|
+
if (typeof successText === "function") {
|
|
850
|
+
finalSuccessText = successText(result);
|
|
851
|
+
} else {
|
|
852
|
+
finalSuccessText = successText ?? text;
|
|
853
|
+
}
|
|
854
|
+
finalSuccessText = bold3(finalSuccessText);
|
|
855
|
+
if (hasAction) {
|
|
856
|
+
const durationText = formatDuration(duration);
|
|
857
|
+
finalSuccessText = `${finalSuccessText} ${durationText}`;
|
|
858
|
+
}
|
|
859
|
+
spinner.updateTask("task", "success", finalSuccessText);
|
|
860
|
+
spinner.stop();
|
|
861
|
+
return result;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
if (!options?.silent) {
|
|
864
|
+
spinner.updateTask("task", "error");
|
|
865
|
+
spinner.stop();
|
|
866
|
+
}
|
|
867
|
+
console.error("");
|
|
868
|
+
if (error && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number") {
|
|
869
|
+
console.error(` Exit Code: ${error.exitCode}`);
|
|
870
|
+
const stdout2 = error.stdout?.toString().trim();
|
|
871
|
+
const stderr = error.stderr?.toString().trim();
|
|
872
|
+
const indent2 = (str) => str.split("\n").map((line) => ` ${line}`).join("\n");
|
|
873
|
+
if (stdout2) console.error(` Stdout:
|
|
874
|
+
${indent2(stdout2)}`);
|
|
875
|
+
if (stderr) console.error(` Stderr:
|
|
876
|
+
${indent2(stderr)}`);
|
|
877
|
+
} else {
|
|
878
|
+
console.error(` Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
879
|
+
}
|
|
880
|
+
console.error("");
|
|
881
|
+
throw error;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ../utils/src/string.ts
|
|
886
|
+
function pluralize(count, singular, plural) {
|
|
887
|
+
return count === 1 ? singular : plural || `${singular}s`;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ../utils/src/pure/index.ts
|
|
891
|
+
init_package_json();
|
|
892
|
+
|
|
893
|
+
// ../utils/src/slug.ts
|
|
894
|
+
function generateSlug(text) {
|
|
895
|
+
return text.toString().toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/--+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/lib/init/run.ts
|
|
899
|
+
init_file_loader();
|
|
900
|
+
|
|
901
|
+
// ../utils/src/package-manager.ts
|
|
902
|
+
import { execSync } from "child_process";
|
|
903
|
+
import { existsSync as existsSync2 } from "fs";
|
|
904
|
+
import { join } from "path";
|
|
905
|
+
function isCommandAvailable(command) {
|
|
906
|
+
try {
|
|
907
|
+
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
908
|
+
return true;
|
|
909
|
+
} catch {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
914
|
+
if (existsSync2(join(cwd, "bun.lock")) || existsSync2(join(cwd, "bun.lockb"))) {
|
|
915
|
+
return "bun";
|
|
916
|
+
}
|
|
917
|
+
if (existsSync2(join(cwd, "pnpm-lock.yaml"))) {
|
|
918
|
+
return "pnpm";
|
|
919
|
+
}
|
|
920
|
+
if (existsSync2(join(cwd, "yarn.lock"))) {
|
|
921
|
+
return "yarn";
|
|
922
|
+
}
|
|
923
|
+
if (existsSync2(join(cwd, "package-lock.json"))) {
|
|
924
|
+
return "npm";
|
|
925
|
+
}
|
|
926
|
+
return detectByCommandAvailability();
|
|
927
|
+
}
|
|
928
|
+
function detectByCommandAvailability() {
|
|
929
|
+
if (isCommandAvailable("bun")) {
|
|
930
|
+
return "bun";
|
|
931
|
+
}
|
|
932
|
+
if (isCommandAvailable("pnpm")) {
|
|
933
|
+
return "pnpm";
|
|
934
|
+
}
|
|
935
|
+
if (isCommandAvailable("yarn")) {
|
|
936
|
+
return "yarn";
|
|
937
|
+
}
|
|
938
|
+
return "npm";
|
|
939
|
+
}
|
|
940
|
+
function getInstallCommand(pm) {
|
|
941
|
+
switch (pm) {
|
|
942
|
+
case "bun":
|
|
943
|
+
return "bun install";
|
|
944
|
+
case "pnpm":
|
|
945
|
+
return "pnpm install";
|
|
946
|
+
case "yarn":
|
|
947
|
+
return "yarn install";
|
|
948
|
+
case "npm":
|
|
949
|
+
default:
|
|
950
|
+
return "npm install";
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function getRunCommand(pm, script) {
|
|
954
|
+
switch (pm) {
|
|
955
|
+
case "bun":
|
|
956
|
+
return `bun run ${script}`;
|
|
957
|
+
case "pnpm":
|
|
958
|
+
return `pnpm run ${script}`;
|
|
959
|
+
case "yarn":
|
|
960
|
+
return `yarn run ${script}`;
|
|
961
|
+
case "npm":
|
|
962
|
+
default:
|
|
963
|
+
return `npm run ${script}`;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function getAddDevDependencyCommand(pm, packageName) {
|
|
967
|
+
switch (pm) {
|
|
968
|
+
case "bun":
|
|
969
|
+
return `bun add -D ${packageName}`;
|
|
970
|
+
case "pnpm":
|
|
971
|
+
return `pnpm add -D ${packageName}`;
|
|
972
|
+
case "yarn":
|
|
973
|
+
return `yarn add -D ${packageName}`;
|
|
974
|
+
case "npm":
|
|
975
|
+
default:
|
|
976
|
+
return `npm install --save-dev ${packageName}`;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/constants/api.ts
|
|
981
|
+
import { join as join3 } from "node:path";
|
|
982
|
+
|
|
983
|
+
// src/constants/server.ts
|
|
984
|
+
import { join as join2 } from "node:path";
|
|
985
|
+
var SERVER_ROOT_DIRECTORY = "server";
|
|
986
|
+
var SERVER_LIB_DIRECTORY = join2(SERVER_ROOT_DIRECTORY, "lib");
|
|
987
|
+
|
|
988
|
+
// src/constants/api.ts
|
|
989
|
+
var DEFAULT_API_ROUTES_DIRECTORY = join3(SERVER_ROOT_DIRECTORY, "api");
|
|
990
|
+
var AUTH_API_SUBDIRECTORY = "auth";
|
|
991
|
+
var SAMPLE_API_SUBDIRECTORY = "sample";
|
|
992
|
+
var SAMPLE_CUSTOM_FILENAME = "custom.ts";
|
|
993
|
+
var SAMPLE_DATABASE_FILENAME = "database.ts";
|
|
994
|
+
var SAMPLE_KV_FILENAME = "kv.ts";
|
|
995
|
+
var SAMPLE_BUCKET_FILENAME = "bucket.ts";
|
|
996
|
+
|
|
997
|
+
// ../../package.json
|
|
998
|
+
var package_default = {
|
|
999
|
+
name: "playcademy",
|
|
1000
|
+
devDependencies: {
|
|
1001
|
+
"@aws-sdk/client-s3": "^3.787.0",
|
|
1002
|
+
"@eslint/js": "^9.24.0",
|
|
1003
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
|
|
1004
|
+
"@pulumi/pulumi": "^3.195.0",
|
|
1005
|
+
"@types/bun": "latest",
|
|
1006
|
+
commander: "^14.0.0",
|
|
1007
|
+
eslint: "^9.24.0",
|
|
1008
|
+
globals: "^16.0.0",
|
|
1009
|
+
husky: "^9.1.7",
|
|
1010
|
+
jscodeshift: "^17.3.0",
|
|
1011
|
+
"lint-staged": "^15.5.1",
|
|
1012
|
+
prettier: "3.5.3",
|
|
1013
|
+
"prettier-plugin-svelte": "^3.3.3",
|
|
1014
|
+
rimraf: "^6.0.1",
|
|
1015
|
+
sharp: "^0.34.2",
|
|
1016
|
+
typedoc: "^0.28.5",
|
|
1017
|
+
"typedoc-plugin-markdown": "^4.7.0",
|
|
1018
|
+
"typescript-eslint": "^8.30.1",
|
|
1019
|
+
"yocto-spinner": "^0.2.2"
|
|
1020
|
+
},
|
|
1021
|
+
peerDependencies: {
|
|
1022
|
+
typescript: "^5"
|
|
1023
|
+
},
|
|
1024
|
+
private: true,
|
|
1025
|
+
scripts: {
|
|
1026
|
+
build: "bun run scripts/build.ts",
|
|
1027
|
+
"build:types": "bun tsc -b packages/data",
|
|
1028
|
+
clean: "bun scripts/clean.ts && bun i",
|
|
1029
|
+
"docs:commit": "bun scripts/docs-commit.ts",
|
|
1030
|
+
dev: "bunx sst dev",
|
|
1031
|
+
"dev:reset": "sst shell -- bun run scripts/dev-reset.ts",
|
|
1032
|
+
"db:reseed": "sst shell -- bun run scripts/dev-reseed-db.ts",
|
|
1033
|
+
"db:sync": "sst shell -- bun run scripts/dev-sync-db.ts",
|
|
1034
|
+
"db:studio": "sst shell -- bun run --filter @playcademy/data studio",
|
|
1035
|
+
"upload-games": "sst shell -- bun run scripts/upload-games.ts",
|
|
1036
|
+
"upload-items": "sst shell -- bun run scripts/upload-items.ts",
|
|
1037
|
+
"upload-sprites": "sst shell -- bun run scripts/upload-sprites.ts",
|
|
1038
|
+
"upload-static-assets": "sst shell -- bun run scripts/upload-static-assets.ts",
|
|
1039
|
+
"upload:all": "sst shell -- bun run scripts/upload-all.ts",
|
|
1040
|
+
"sync-engine-assets": "bun scripts/sync-engine-assets.ts",
|
|
1041
|
+
"sync-vite-templates": "bun scripts/sync-vite-templates.ts",
|
|
1042
|
+
"sync-godot-template": "bun scripts/sync-godot-template.ts",
|
|
1043
|
+
"sync:all": "bun scripts/sync-all.ts",
|
|
1044
|
+
cf: "sst shell -- bun scripts/setup-cloudflare-dispatch.ts",
|
|
1045
|
+
"list-s3-bucket": "sst shell -- bun scripts/list-s3-bucket.ts",
|
|
1046
|
+
doctor: "bunx sst shell -- bun scripts/doctor.ts",
|
|
1047
|
+
format: "bun run --filter '*' format",
|
|
1048
|
+
lint: "bun run --filter '*' lint",
|
|
1049
|
+
prepare: "husky",
|
|
1050
|
+
sort: "bunx sort-package-json **/package.json",
|
|
1051
|
+
test: "bun test",
|
|
1052
|
+
"test:unit": "bun scripts/test-unit.ts",
|
|
1053
|
+
"test:integration": "bun scripts/test-integration.ts",
|
|
1054
|
+
"test:watch": "bun test --watch",
|
|
1055
|
+
"docker:relay": "bun run scripts/docker-relay.ts",
|
|
1056
|
+
"debug:leaderboard-notifications": "bunx sst shell -- bun scripts/debug/leaderboard-notifications.ts",
|
|
1057
|
+
"timeback:caliper": "sst shell -- bun scripts/caliper-cli.ts",
|
|
1058
|
+
notify: "NODE_ENV=productionbunx sst shell -- bun scripts/notifications-cli.ts"
|
|
1059
|
+
},
|
|
1060
|
+
type: "module",
|
|
1061
|
+
workspaces: {
|
|
1062
|
+
packages: [
|
|
1063
|
+
"apps/*",
|
|
1064
|
+
"packages/*",
|
|
1065
|
+
"games/*",
|
|
1066
|
+
"templates/*"
|
|
1067
|
+
],
|
|
1068
|
+
catalog: {
|
|
1069
|
+
sst: "^3.14.11",
|
|
1070
|
+
dedent: "^1.6.0",
|
|
1071
|
+
typescript: "^5.7.2",
|
|
1072
|
+
vite: "^6.3.5",
|
|
1073
|
+
eslint: "^9.24.0",
|
|
1074
|
+
prettier: "3.5.3",
|
|
1075
|
+
"@types/bun": "latest",
|
|
1076
|
+
jose: "^5.2.3",
|
|
1077
|
+
zod: "^3.25.53",
|
|
1078
|
+
"better-auth": "1.3.33"
|
|
1079
|
+
},
|
|
1080
|
+
catalogs: {
|
|
1081
|
+
svelte: {
|
|
1082
|
+
svelte: "^5.23.1",
|
|
1083
|
+
"@sveltejs/adapter-auto": "^6.0.0",
|
|
1084
|
+
"@sveltejs/kit": "^2.16.0",
|
|
1085
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
1086
|
+
"svelte-check": "^4.2.1",
|
|
1087
|
+
"prettier-plugin-svelte": "^3.3.3",
|
|
1088
|
+
"eslint-plugin-svelte": "^3.0.0"
|
|
1089
|
+
},
|
|
1090
|
+
database: {
|
|
1091
|
+
"drizzle-orm": "^0.42.0",
|
|
1092
|
+
"drizzle-kit": "^0.31.0",
|
|
1093
|
+
"drizzle-zod": "^0.7.1"
|
|
1094
|
+
},
|
|
1095
|
+
aws: {
|
|
1096
|
+
"@aws-sdk/client-s3": "^3.787.0",
|
|
1097
|
+
"@aws-sdk/s3-request-presigner": "^3.787.0"
|
|
1098
|
+
},
|
|
1099
|
+
linting: {
|
|
1100
|
+
"typescript-eslint": "^8.30.1",
|
|
1101
|
+
"@eslint/js": "^9.24.0",
|
|
1102
|
+
"eslint-config-prettier": "^10.0.1"
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
// src/constants/auth.ts
|
|
1109
|
+
var BETTER_AUTH_VERSION = package_default.workspaces.catalog["better-auth"];
|
|
1110
|
+
var PLAYCADEMY_AUTH_VERSION = "latest";
|
|
1111
|
+
var AUTH_CONFIG_FILE = "auth.ts";
|
|
1112
|
+
|
|
1113
|
+
// src/constants/config.ts
|
|
1114
|
+
var ENV_FILES = [
|
|
1115
|
+
".env",
|
|
1116
|
+
// Loaded first
|
|
1117
|
+
".env.development",
|
|
1118
|
+
// Overrides .env
|
|
1119
|
+
".env.local"
|
|
1120
|
+
// Overrides all (highest priority)
|
|
1121
|
+
];
|
|
1122
|
+
var ENV_EXAMPLE_FILE = ".env.example";
|
|
1123
|
+
var TSCONFIG_JSON = "tsconfig.json";
|
|
1124
|
+
var TSCONFIG_APP_JSON = "tsconfig.app.json";
|
|
1125
|
+
var TSCONFIG_FILES = [
|
|
1126
|
+
TSCONFIG_APP_JSON,
|
|
1127
|
+
// Modern tooling (try first)
|
|
1128
|
+
TSCONFIG_JSON
|
|
1129
|
+
// Standard (fallback)
|
|
1130
|
+
];
|
|
1131
|
+
var TSCONFIG_REQUIRED_INCLUDES = [
|
|
1132
|
+
"playcademy-env.d.ts",
|
|
1133
|
+
// Generated type definitions
|
|
1134
|
+
"server"
|
|
1135
|
+
// Server-side code
|
|
1136
|
+
];
|
|
1137
|
+
|
|
1138
|
+
// src/constants/bucket.ts
|
|
1139
|
+
var BUCKET_ALWAYS_SKIP = [".git", ".DS_Store", ".gitignore", ...ENV_FILES];
|
|
1140
|
+
|
|
1141
|
+
// src/constants/database.ts
|
|
1142
|
+
import { join as join4 } from "path";
|
|
1143
|
+
var DEFAULT_DATABASE_DIRECTORY = join4("server", "db");
|
|
1144
|
+
var SCHEMA_SUBDIRECTORY = "schema";
|
|
1145
|
+
var DB_FILES = {
|
|
1146
|
+
/** Index file name */
|
|
1147
|
+
INDEX: "index.ts",
|
|
1148
|
+
/** Seed file name */
|
|
1149
|
+
SEED: "seed.ts",
|
|
1150
|
+
/** Types file name */
|
|
1151
|
+
TYPES: "types.ts",
|
|
1152
|
+
/** Schema index file name */
|
|
1153
|
+
SCHEMA_INDEX: "index.ts",
|
|
1154
|
+
/** Example schema file name */
|
|
1155
|
+
EXAMPLE_SCHEMA: "example.ts"
|
|
1156
|
+
};
|
|
1157
|
+
var DRIZZLE_CONFIG_FILES = ["drizzle.config.ts", "drizzle.config.js"];
|
|
1158
|
+
|
|
1159
|
+
// src/constants/engine.ts
|
|
1160
|
+
var TEMPLATE_REPOS = {
|
|
1161
|
+
VITE: "vitejs/vite/packages/create-vite",
|
|
1162
|
+
SUPERBUILDERS: "superbuilders"
|
|
1163
|
+
};
|
|
1164
|
+
var DEFAULT_TEMPLATE_REF = "main";
|
|
1165
|
+
var GODOT_ADDON_URL = "https://enginetoolin-production-sharedengineassetsbucketbucket-odxfwvez.s3.us-east-1.amazonaws.com/godot/assets/playcademy.zip";
|
|
1166
|
+
var TEMPLATE_RENAME_FILES = {
|
|
1167
|
+
_gitignore: ".gitignore",
|
|
1168
|
+
_npmrc: ".npmrc",
|
|
1169
|
+
_env: ".env"
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
// src/constants/godot.ts
|
|
1173
|
+
import { join as join5 } from "node:path";
|
|
1174
|
+
var GODOT_PROJECT_FILE = "project.godot";
|
|
1175
|
+
var GODOT_BUILD_DIRECTORIES = {
|
|
1176
|
+
/** Root build directory (cleared before each export) */
|
|
1177
|
+
ROOT: "build",
|
|
1178
|
+
/** Web export subdirectory */
|
|
1179
|
+
WEB: join5("build", "web")
|
|
1180
|
+
};
|
|
1181
|
+
var GODOT_BUILD_OUTPUTS = {
|
|
1182
|
+
/** Exported web build entry point */
|
|
1183
|
+
INDEX_HTML: join5("build", "web", "index.html"),
|
|
1184
|
+
/** Packaged zip file (created by Godot export) */
|
|
1185
|
+
ZIP: join5("build", "web_playcademy.zip")
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
// src/constants/paths.ts
|
|
1189
|
+
import { join as join6 } from "path";
|
|
1190
|
+
var WORKSPACE_NAME = ".playcademy";
|
|
1191
|
+
var CLI_DIRECTORIES = {
|
|
1192
|
+
/** Root directory for CLI artifacts in workspace */
|
|
1193
|
+
WORKSPACE: WORKSPACE_NAME,
|
|
1194
|
+
/** Database directory within workspace */
|
|
1195
|
+
DATABASE: join6(WORKSPACE_NAME, "db"),
|
|
1196
|
+
/** KV storage directory within workspace */
|
|
1197
|
+
KV: join6(WORKSPACE_NAME, "kv"),
|
|
1198
|
+
/** Bucket storage directory within workspace */
|
|
1199
|
+
BUCKET: join6(WORKSPACE_NAME, "bucket")
|
|
1200
|
+
};
|
|
1201
|
+
var CLI_DEFAULT_OUTPUTS = {
|
|
1202
|
+
/** Default worker bundle output for debug command */
|
|
1203
|
+
WORKER_BUNDLE: join6(WORKSPACE_NAME, "worker-bundle.js")
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
// src/constants/timeback.ts
|
|
1207
|
+
var CONFIG_FILE_NAMES = [
|
|
1208
|
+
"playcademy.config.js",
|
|
1209
|
+
"playcademy.config.json",
|
|
1210
|
+
"playcademy.config.mjs"
|
|
1211
|
+
];
|
|
1212
|
+
|
|
1213
|
+
// ../constants/src/overworld.ts
|
|
1214
|
+
var ITEM_SLUGS = {
|
|
1215
|
+
/** Primary platform currency */
|
|
1216
|
+
PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
|
|
1217
|
+
/** Experience points currency */
|
|
1218
|
+
PLAYCADEMY_XP: "PLAYCADEMY_XP",
|
|
1219
|
+
/** Core platform badges */
|
|
1220
|
+
FOUNDING_MEMBER_BADGE: "FOUNDING_MEMBER_BADGE",
|
|
1221
|
+
EARLY_ADOPTER_BADGE: "EARLY_ADOPTER_BADGE",
|
|
1222
|
+
FIRST_GAME_BADGE: "FIRST_GAME_BADGE",
|
|
1223
|
+
/** Example items */
|
|
1224
|
+
COMMON_SWORD: "COMMON_SWORD",
|
|
1225
|
+
SMALL_HEALTH_POTION: "SMALL_HEALTH_POTION",
|
|
1226
|
+
SMALL_BACKPACK: "SMALL_BACKPACK",
|
|
1227
|
+
/** Placeable items */
|
|
1228
|
+
LAVA_LAMP: "LAVA_LAMP",
|
|
1229
|
+
BOOMBOX: "BOOMBOX",
|
|
1230
|
+
CABIN_BED: "CABIN_BED"
|
|
1231
|
+
};
|
|
1232
|
+
var CURRENCIES = {
|
|
1233
|
+
/** Primary platform currency slug */
|
|
1234
|
+
PRIMARY: ITEM_SLUGS.PLAYCADEMY_CREDITS,
|
|
1235
|
+
/** Experience points slug */
|
|
1236
|
+
XP: ITEM_SLUGS.PLAYCADEMY_XP
|
|
1237
|
+
};
|
|
1238
|
+
var BADGES = {
|
|
1239
|
+
FOUNDING_MEMBER: ITEM_SLUGS.FOUNDING_MEMBER_BADGE,
|
|
1240
|
+
EARLY_ADOPTER: ITEM_SLUGS.EARLY_ADOPTER_BADGE,
|
|
1241
|
+
FIRST_GAME: ITEM_SLUGS.FIRST_GAME_BADGE
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// src/constants/urls.ts
|
|
1245
|
+
var DOCS_URL = "https://docs.dev.playcademy.net/platform";
|
|
1246
|
+
|
|
1247
|
+
// src/lib/core/client.ts
|
|
1248
|
+
import { PlaycademyClient } from "@playcademy/sdk/internal";
|
|
1249
|
+
|
|
1250
|
+
// src/lib/core/context.ts
|
|
1251
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1252
|
+
import { resolve as resolve2 } from "path";
|
|
1253
|
+
var context = {};
|
|
1254
|
+
var cache = {
|
|
1255
|
+
packageManager: null
|
|
1256
|
+
};
|
|
1257
|
+
function setCliContext(ctx) {
|
|
1258
|
+
context = { ...context, ...ctx };
|
|
1259
|
+
}
|
|
1260
|
+
function getWorkspace() {
|
|
1261
|
+
return context.workspace || process.cwd();
|
|
1262
|
+
}
|
|
1263
|
+
function getPackageManager() {
|
|
1264
|
+
if (cache.packageManager) return cache.packageManager;
|
|
1265
|
+
cache.packageManager = detectPackageManager(getWorkspace());
|
|
1266
|
+
return cache.packageManager;
|
|
1267
|
+
}
|
|
1268
|
+
function hasPackageJson(dir) {
|
|
1269
|
+
return existsSync3(resolve2(dir || getWorkspace(), "package.json"));
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/lib/core/gitignore.ts
|
|
1273
|
+
function normalizeGitignoreEntry(entry) {
|
|
1274
|
+
return entry.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/lib/core/import.ts
|
|
1278
|
+
import * as esbuild from "esbuild";
|
|
1279
|
+
|
|
1280
|
+
// src/lib/secrets/env.ts
|
|
1281
|
+
init_file_loader();
|
|
1282
|
+
import { existsSync as existsSync4, writeFileSync } from "fs";
|
|
1283
|
+
import { join as join7 } from "path";
|
|
1284
|
+
function parseEnvFile(contents) {
|
|
1285
|
+
const secrets = {};
|
|
1286
|
+
for (const line of contents.split("\n")) {
|
|
1287
|
+
const trimmed = line.trim();
|
|
1288
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
1292
|
+
if (match) {
|
|
1293
|
+
const [, key, value] = match;
|
|
1294
|
+
if (!key || !value) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const unquoted = value.replace(/^["']|["']$/g, "");
|
|
1298
|
+
secrets[key] = unquoted;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return secrets;
|
|
1302
|
+
}
|
|
1303
|
+
async function readEnvFile(workspace) {
|
|
1304
|
+
let secrets = {};
|
|
1305
|
+
for (const filename of ENV_FILES) {
|
|
1306
|
+
try {
|
|
1307
|
+
const contents = await loadFile(filename, { cwd: workspace, searchUp: false });
|
|
1308
|
+
if (contents) {
|
|
1309
|
+
const fileSecrets = parseEnvFile(contents);
|
|
1310
|
+
secrets = { ...secrets, ...fileSecrets };
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return secrets;
|
|
1317
|
+
}
|
|
1318
|
+
function getLoadedEnvFiles(workspace) {
|
|
1319
|
+
return ENV_FILES.filter((filename) => existsSync4(join7(workspace, filename)));
|
|
1320
|
+
}
|
|
1321
|
+
function hasEnvFile(workspace) {
|
|
1322
|
+
return ENV_FILES.some((filename) => existsSync4(join7(workspace, filename)));
|
|
1323
|
+
}
|
|
1324
|
+
async function updateEnvExample(workspace, lines) {
|
|
1325
|
+
const envExamplePath = join7(workspace, ENV_EXAMPLE_FILE);
|
|
1326
|
+
let existing = "";
|
|
1327
|
+
if (existsSync4(envExamplePath)) {
|
|
1328
|
+
const loaded = await loadFile(envExamplePath);
|
|
1329
|
+
if (!loaded) {
|
|
1330
|
+
throw new Error(`Failed to load ${ENV_EXAMPLE_FILE} file`);
|
|
1331
|
+
}
|
|
1332
|
+
existing = loaded;
|
|
1333
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
1334
|
+
for (const line of existing.split("\n")) {
|
|
1335
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
1336
|
+
if (match?.[1]) {
|
|
1337
|
+
existingKeys.add(match[1]);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const linesToAdd = [];
|
|
1341
|
+
for (const line of lines) {
|
|
1342
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
1343
|
+
if (match?.[1]) {
|
|
1344
|
+
if (!existingKeys.has(match[1])) {
|
|
1345
|
+
linesToAdd.push(line);
|
|
1346
|
+
}
|
|
1347
|
+
} else {
|
|
1348
|
+
linesToAdd.push(line);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (linesToAdd.every((line) => !line.match(/^([A-Z_][A-Z0-9_]*)=/))) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const content = linesToAdd.join("\n");
|
|
1355
|
+
writeFileSync(envExamplePath, existing + "\n" + content + "\n");
|
|
1356
|
+
} else {
|
|
1357
|
+
const content = lines.join("\n");
|
|
1358
|
+
writeFileSync(envExamplePath, content + "\n");
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/lib/config/loader.ts
|
|
1363
|
+
init_file_loader();
|
|
1364
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
1365
|
+
var ConfigError = class extends Error {
|
|
1366
|
+
constructor(message, field, suggestion) {
|
|
1367
|
+
super(message);
|
|
1368
|
+
this.field = field;
|
|
1369
|
+
this.suggestion = suggestion;
|
|
1370
|
+
this.name = "ConfigError";
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Format a user-friendly error message with the field and suggestion
|
|
1374
|
+
*/
|
|
1375
|
+
toString() {
|
|
1376
|
+
let msg = `Configuration Error: ${this.message}`;
|
|
1377
|
+
if (this.field) {
|
|
1378
|
+
msg += `
|
|
1379
|
+
Field: ${this.field}`;
|
|
1380
|
+
}
|
|
1381
|
+
if (this.suggestion) {
|
|
1382
|
+
msg += `
|
|
1383
|
+
Suggestion: ${this.suggestion}`;
|
|
1384
|
+
}
|
|
1385
|
+
return msg;
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
async function findConfigPath(configPath) {
|
|
1389
|
+
const { findFile: findFile2 } = await Promise.resolve().then(() => (init_file_loader(), file_loader_exports));
|
|
1390
|
+
if (configPath) {
|
|
1391
|
+
const fullPath = resolve3(configPath);
|
|
1392
|
+
const result2 = await findFile2(fullPath, { cwd: dirname2(fullPath), searchUp: false });
|
|
1393
|
+
if (!result2) {
|
|
1394
|
+
throw new ConfigError(
|
|
1395
|
+
`Config file not found: ${fullPath}`,
|
|
1396
|
+
void 0,
|
|
1397
|
+
"Check the path is correct and the file exists"
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
return result2.path;
|
|
1401
|
+
}
|
|
1402
|
+
const envPath = process.env.PLAYCADEMY_CONFIG_PATH;
|
|
1403
|
+
if (envPath) {
|
|
1404
|
+
const candidate = resolve3(envPath);
|
|
1405
|
+
const result2 = await findFile2(candidate, { cwd: dirname2(candidate) });
|
|
1406
|
+
if (!result2) {
|
|
1407
|
+
throw new ConfigError(
|
|
1408
|
+
`Config file not found: ${candidate}`,
|
|
1409
|
+
"PLAYCADEMY_CONFIG_PATH",
|
|
1410
|
+
"Check that the environment variable points to a valid config file"
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
return result2.path;
|
|
1414
|
+
}
|
|
1415
|
+
const result = await findFile2(CONFIG_FILE_NAMES, { searchUp: true, maxLevels: 3 });
|
|
1416
|
+
if (!result) {
|
|
1417
|
+
throw new ConfigError(
|
|
1418
|
+
"No Playcademy config file found in this directory or any parent directory",
|
|
1419
|
+
void 0,
|
|
1420
|
+
`Run 'playcademy init' to create a config file, or manually create one of: ${CONFIG_FILE_NAMES.join(", ")}`
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
return result.path;
|
|
1424
|
+
}
|
|
1425
|
+
async function loadConfigFile(path2) {
|
|
1426
|
+
if (path2.endsWith(".json")) {
|
|
1427
|
+
return await loadFile(path2, { required: true, parseJson: true });
|
|
1428
|
+
}
|
|
1429
|
+
const module = await import(`${path2}?t=${Date.now()}`);
|
|
1430
|
+
return module.default || module;
|
|
1431
|
+
}
|
|
1432
|
+
async function loadConfig(configPath) {
|
|
1433
|
+
const result = await loadConfigWithPath(configPath);
|
|
1434
|
+
return result.config;
|
|
1435
|
+
}
|
|
1436
|
+
async function loadConfigWithPath(configPath) {
|
|
1437
|
+
try {
|
|
1438
|
+
const actualPath = configPath ? resolve3(configPath) : await findConfigPath();
|
|
1439
|
+
const config = await loadConfigFile(actualPath);
|
|
1440
|
+
if (!config || typeof config !== "object") {
|
|
1441
|
+
throw new ConfigError(
|
|
1442
|
+
"Config file must export or contain a valid configuration object",
|
|
1443
|
+
void 0,
|
|
1444
|
+
actualPath?.endsWith(".json") ? "Check that your JSON file contains a valid object" : 'Make sure your .js file has: export default { name: "...", ... }'
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
validateConfig(config);
|
|
1448
|
+
return {
|
|
1449
|
+
config: processConfigVariables(config),
|
|
1450
|
+
configPath: actualPath,
|
|
1451
|
+
configDir: dirname2(actualPath)
|
|
1452
|
+
};
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
if (error instanceof ConfigError) {
|
|
1455
|
+
throw error;
|
|
1456
|
+
}
|
|
1457
|
+
const errorMsg = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
|
|
1458
|
+
throw new ConfigError(
|
|
1459
|
+
`Failed to load config file`,
|
|
1460
|
+
void 0,
|
|
1461
|
+
`Check your config file syntax. Error details: ${errorMsg}`
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function validateConfig(config) {
|
|
1466
|
+
if (!config || typeof config !== "object") {
|
|
1467
|
+
throw new ConfigError("Configuration must be an object");
|
|
1468
|
+
}
|
|
1469
|
+
const cfg = config;
|
|
1470
|
+
if (!cfg.name || typeof cfg.name !== "string") {
|
|
1471
|
+
throw new ConfigError(
|
|
1472
|
+
'Your config file is missing a "name" field',
|
|
1473
|
+
"name",
|
|
1474
|
+
'Add a name property to your config: name: "My App Name"'
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
if (cfg.integrations) {
|
|
1478
|
+
if (typeof cfg.integrations !== "object") {
|
|
1479
|
+
throw new ConfigError(
|
|
1480
|
+
'The "integrations" field must be an object',
|
|
1481
|
+
"integrations",
|
|
1482
|
+
"Change integrations to an object: integrations: { timeback: { ... } }"
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
const integrations = cfg.integrations;
|
|
1486
|
+
if (integrations.timeback) {
|
|
1487
|
+
if (typeof integrations.timeback !== "object") {
|
|
1488
|
+
throw new ConfigError(
|
|
1489
|
+
'The "integrations.timeback" field must be an object',
|
|
1490
|
+
"integrations.timeback",
|
|
1491
|
+
"Change timeback to an object: timeback: { course: { ... } }"
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
const tb = integrations.timeback;
|
|
1495
|
+
if (!tb.courses) {
|
|
1496
|
+
throw new ConfigError(
|
|
1497
|
+
'TimeBack integration requires a "courses" array',
|
|
1498
|
+
"integrations.timeback.courses",
|
|
1499
|
+
'Add timeback.courses array: timeback: { courses: [{ subject: "Math", grade: 3 }] }'
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
if (!Array.isArray(tb.courses)) {
|
|
1503
|
+
throw new ConfigError(
|
|
1504
|
+
'The "integrations.timeback.courses" field must be an array',
|
|
1505
|
+
"integrations.timeback.courses",
|
|
1506
|
+
'Change to an array: courses: [{ subject: "Math", grade: 3 }]'
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
if (tb.courses.length === 0) {
|
|
1510
|
+
throw new ConfigError(
|
|
1511
|
+
"TimeBack courses array cannot be empty",
|
|
1512
|
+
"integrations.timeback.courses",
|
|
1513
|
+
'Add at least one course: courses: [{ subject: "Math", grade: 3 }]'
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
tb.courses.forEach((course, idx) => {
|
|
1517
|
+
if (typeof course !== "object" || course === null) {
|
|
1518
|
+
throw new ConfigError(
|
|
1519
|
+
`Course at index ${idx} must be an object`,
|
|
1520
|
+
`integrations.timeback.courses[${idx}]`,
|
|
1521
|
+
'Each course must have subject and grade: { subject: "Math", grade: 3 }'
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
const courseObj = course;
|
|
1525
|
+
if (typeof courseObj.subject !== "string") {
|
|
1526
|
+
throw new ConfigError(
|
|
1527
|
+
`Course at index ${idx} is missing required "subject" field`,
|
|
1528
|
+
`integrations.timeback.courses[${idx}].subject`,
|
|
1529
|
+
'Add subject string: { subject: "Math", grade: 3 }'
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
if (typeof courseObj.grade !== "number") {
|
|
1533
|
+
throw new ConfigError(
|
|
1534
|
+
`Course at index ${idx} is missing required "grade" field`,
|
|
1535
|
+
`integrations.timeback.courses[${idx}].grade`,
|
|
1536
|
+
'Add grade number: { subject: "Math", grade: 3 }'
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
const stringOverrides = ["title", "courseCode", "level", "gradingScheme"];
|
|
1540
|
+
stringOverrides.forEach((field) => {
|
|
1541
|
+
if (courseObj[field] !== void 0 && typeof courseObj[field] !== "string") {
|
|
1542
|
+
throw new ConfigError(
|
|
1543
|
+
`Course at index ${idx} has invalid "${field}" field (must be a string)`,
|
|
1544
|
+
`integrations.timeback.courses[${idx}].${field}`,
|
|
1545
|
+
`${field} must be a string if provided`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
if (courseObj.metadata !== void 0 && typeof courseObj.metadata !== "object") {
|
|
1550
|
+
throw new ConfigError(
|
|
1551
|
+
`Course at index ${idx} has invalid "metadata" field (must be an object)`,
|
|
1552
|
+
`integrations.timeback.courses[${idx}].metadata`,
|
|
1553
|
+
"metadata must be an object if provided"
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
if (tb.base) {
|
|
1558
|
+
if (typeof tb.base !== "object") {
|
|
1559
|
+
throw new ConfigError(
|
|
1560
|
+
'The "integrations.timeback.base" field must be an object',
|
|
1561
|
+
"integrations.timeback.base",
|
|
1562
|
+
"Change to an object: base: { organization: { ... }, course: { ... } }"
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
const base = tb.base;
|
|
1566
|
+
if (base.organization) {
|
|
1567
|
+
if (typeof base.organization !== "object") {
|
|
1568
|
+
throw new ConfigError(
|
|
1569
|
+
'The "integrations.timeback.base.organization" field must be an object',
|
|
1570
|
+
"integrations.timeback.base.organization",
|
|
1571
|
+
'Change to an object: organization: { name: "My School", type: "school" }'
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
const org = base.organization;
|
|
1575
|
+
if (org.type) {
|
|
1576
|
+
const validOrgTypes = [
|
|
1577
|
+
"department",
|
|
1578
|
+
"school",
|
|
1579
|
+
"district",
|
|
1580
|
+
"local",
|
|
1581
|
+
"state",
|
|
1582
|
+
"national"
|
|
1583
|
+
];
|
|
1584
|
+
if (!validOrgTypes.includes(org.type)) {
|
|
1585
|
+
throw new ConfigError(
|
|
1586
|
+
`Invalid organization type: "${org.type}"`,
|
|
1587
|
+
"integrations.timeback.base.organization.type",
|
|
1588
|
+
`Use one of: ${validOrgTypes.join(", ")}. Example: type: "school"`
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const baseSections = ["course", "component", "resource", "componentResource"];
|
|
1594
|
+
baseSections.forEach((section) => {
|
|
1595
|
+
if (base[section] !== void 0 && typeof base[section] !== "object") {
|
|
1596
|
+
throw new ConfigError(
|
|
1597
|
+
`The "integrations.timeback.base.${section}" field must be an object`,
|
|
1598
|
+
`integrations.timeback.base.${section}`,
|
|
1599
|
+
`Change to an object: ${section}: { ... }`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function processConfigVariables(config) {
|
|
1608
|
+
const processed = JSON.parse(JSON.stringify(config));
|
|
1609
|
+
const processValue = (value) => {
|
|
1610
|
+
if (typeof value === "string") {
|
|
1611
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, varName) => process.env[varName] || "").replace(/process\.env\.([A-Z_]+)/g, (_, varName) => process.env[varName] || "");
|
|
1612
|
+
}
|
|
1613
|
+
if (Array.isArray(value)) {
|
|
1614
|
+
return value.map(processValue);
|
|
1615
|
+
}
|
|
1616
|
+
if (value && typeof value === "object") {
|
|
1617
|
+
const obj = value;
|
|
1618
|
+
const result = {};
|
|
1619
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
1620
|
+
result[key] = processValue(val);
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
return value;
|
|
1625
|
+
};
|
|
1626
|
+
return processValue(processed);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// ../timeback/dist/constants.js
|
|
1630
|
+
var __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
1631
|
+
var init_auth = () => {
|
|
1632
|
+
};
|
|
1633
|
+
var PLAYCADEMY_BASE_URLS2;
|
|
1634
|
+
var init_domains = __esm2(() => {
|
|
1635
|
+
PLAYCADEMY_BASE_URLS2 = {
|
|
1636
|
+
production: "https://hub.playcademy.net",
|
|
1637
|
+
staging: "https://hub.dev.playcademy.net"
|
|
1638
|
+
};
|
|
1639
|
+
});
|
|
1640
|
+
var init_env_vars = () => {
|
|
1641
|
+
};
|
|
1642
|
+
var ITEM_SLUGS2;
|
|
1643
|
+
var CURRENCIES2;
|
|
1644
|
+
var BADGES2;
|
|
1645
|
+
var init_overworld = __esm2(() => {
|
|
1646
|
+
ITEM_SLUGS2 = {
|
|
1647
|
+
PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
|
|
1648
|
+
PLAYCADEMY_XP: "PLAYCADEMY_XP",
|
|
1649
|
+
FOUNDING_MEMBER_BADGE: "FOUNDING_MEMBER_BADGE",
|
|
1650
|
+
EARLY_ADOPTER_BADGE: "EARLY_ADOPTER_BADGE",
|
|
1651
|
+
FIRST_GAME_BADGE: "FIRST_GAME_BADGE",
|
|
1652
|
+
COMMON_SWORD: "COMMON_SWORD",
|
|
1653
|
+
SMALL_HEALTH_POTION: "SMALL_HEALTH_POTION",
|
|
1654
|
+
SMALL_BACKPACK: "SMALL_BACKPACK",
|
|
1655
|
+
LAVA_LAMP: "LAVA_LAMP",
|
|
1656
|
+
BOOMBOX: "BOOMBOX",
|
|
1657
|
+
CABIN_BED: "CABIN_BED"
|
|
1658
|
+
};
|
|
1659
|
+
CURRENCIES2 = {
|
|
1660
|
+
PRIMARY: ITEM_SLUGS2.PLAYCADEMY_CREDITS,
|
|
1661
|
+
XP: ITEM_SLUGS2.PLAYCADEMY_XP
|
|
1662
|
+
};
|
|
1663
|
+
BADGES2 = {
|
|
1664
|
+
FOUNDING_MEMBER: ITEM_SLUGS2.FOUNDING_MEMBER_BADGE,
|
|
1665
|
+
EARLY_ADOPTER: ITEM_SLUGS2.EARLY_ADOPTER_BADGE,
|
|
1666
|
+
FIRST_GAME: ITEM_SLUGS2.FIRST_GAME_BADGE
|
|
1667
|
+
};
|
|
1668
|
+
});
|
|
1669
|
+
var TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY";
|
|
1670
|
+
var TIMEBACK_ORG_NAME = "Playcademy Studios";
|
|
1671
|
+
var TIMEBACK_ORG_TYPE = "department";
|
|
1672
|
+
var TIMEBACK_COURSE_DEFAULTS;
|
|
1673
|
+
var TIMEBACK_RESOURCE_DEFAULTS;
|
|
1674
|
+
var TIMEBACK_COMPONENT_DEFAULTS;
|
|
1675
|
+
var TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
|
|
1676
|
+
var init_timeback = __esm2(() => {
|
|
1677
|
+
TIMEBACK_COURSE_DEFAULTS = {
|
|
1678
|
+
gradingScheme: "STANDARD",
|
|
1679
|
+
level: {
|
|
1680
|
+
elementary: "Elementary",
|
|
1681
|
+
middle: "Middle",
|
|
1682
|
+
high: "High",
|
|
1683
|
+
ap: "AP"
|
|
1684
|
+
},
|
|
1685
|
+
goals: {
|
|
1686
|
+
dailyXp: 50,
|
|
1687
|
+
dailyLessons: 3
|
|
1688
|
+
},
|
|
1689
|
+
metrics: {
|
|
1690
|
+
totalXp: 1e3,
|
|
1691
|
+
totalLessons: 50
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
TIMEBACK_RESOURCE_DEFAULTS = {
|
|
1695
|
+
vendorId: "playcademy",
|
|
1696
|
+
roles: ["primary"],
|
|
1697
|
+
importance: "primary",
|
|
1698
|
+
metadata: {
|
|
1699
|
+
type: "interactive",
|
|
1700
|
+
toolProvider: "Playcademy",
|
|
1701
|
+
instructionalMethod: "exploratory",
|
|
1702
|
+
language: "en-US"
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
TIMEBACK_COMPONENT_DEFAULTS = {
|
|
1706
|
+
sortOrder: 1,
|
|
1707
|
+
prerequisiteCriteria: "ALL"
|
|
1708
|
+
};
|
|
1709
|
+
TIMEBACK_COMPONENT_RESOURCE_DEFAULTS = {
|
|
1710
|
+
sortOrder: 1,
|
|
1711
|
+
lessonType: "quiz"
|
|
1712
|
+
};
|
|
1713
|
+
});
|
|
1714
|
+
var init_workers = () => {
|
|
1715
|
+
};
|
|
1716
|
+
var init_src = __esm2(() => {
|
|
1717
|
+
init_auth();
|
|
1718
|
+
init_domains();
|
|
1719
|
+
init_env_vars();
|
|
1720
|
+
init_overworld();
|
|
1721
|
+
init_timeback();
|
|
1722
|
+
init_workers();
|
|
1723
|
+
});
|
|
1724
|
+
var TIMEBACK_API_URLS;
|
|
1725
|
+
var TIMEBACK_AUTH_URLS;
|
|
1726
|
+
var CALIPER_API_URLS;
|
|
1727
|
+
var ONEROSTER_ENDPOINTS;
|
|
1728
|
+
var CALIPER_ENDPOINTS;
|
|
1729
|
+
var CALIPER_CONSTANTS;
|
|
1730
|
+
var TIMEBACK_EVENT_TYPES;
|
|
1731
|
+
var TIMEBACK_ACTIONS;
|
|
1732
|
+
var TIMEBACK_TYPES;
|
|
1733
|
+
var ACTIVITY_METRIC_TYPES;
|
|
1734
|
+
var TIME_METRIC_TYPES;
|
|
1735
|
+
var TIMEBACK_SUBJECTS;
|
|
1736
|
+
var TIMEBACK_GRADE_LEVELS;
|
|
1737
|
+
var TIMEBACK_GRADE_LEVEL_LABELS;
|
|
1738
|
+
var CALIPER_SUBJECTS;
|
|
1739
|
+
var ONEROSTER_STATUS;
|
|
1740
|
+
var SCORE_STATUS;
|
|
1741
|
+
var ENV_VARS;
|
|
1742
|
+
var HTTP_DEFAULTS;
|
|
1743
|
+
var AUTH_DEFAULTS;
|
|
1744
|
+
var CACHE_DEFAULTS;
|
|
1745
|
+
var CONFIG_DEFAULTS;
|
|
1746
|
+
var PLAYCADEMY_DEFAULTS;
|
|
1747
|
+
var RESOURCE_DEFAULTS;
|
|
1748
|
+
var HTTP_STATUS;
|
|
1749
|
+
var ERROR_NAMES;
|
|
1750
|
+
var init_constants = __esm2(() => {
|
|
1751
|
+
init_src();
|
|
1752
|
+
TIMEBACK_API_URLS = {
|
|
1753
|
+
production: "https://api.alpha-1edtech.ai",
|
|
1754
|
+
staging: "https://api.staging.alpha-1edtech.com"
|
|
1755
|
+
};
|
|
1756
|
+
TIMEBACK_AUTH_URLS = {
|
|
1757
|
+
production: "https://prod-beyond-timeback-api-2-idp.auth.us-east-1.amazoncognito.com",
|
|
1758
|
+
staging: "https://alpha-auth-development-idp.auth.us-west-2.amazoncognito.com"
|
|
1759
|
+
};
|
|
1760
|
+
CALIPER_API_URLS = {
|
|
1761
|
+
production: "https://caliper.alpha-1edtech.ai",
|
|
1762
|
+
staging: "https://caliper-staging.alpha-1edtech.com"
|
|
1763
|
+
};
|
|
1764
|
+
ONEROSTER_ENDPOINTS = {
|
|
1765
|
+
organizations: "/ims/oneroster/rostering/v1p2/orgs",
|
|
1766
|
+
courses: "/ims/oneroster/rostering/v1p2/courses",
|
|
1767
|
+
courseComponents: "/ims/oneroster/rostering/v1p2/courses/components",
|
|
1768
|
+
resources: "/ims/oneroster/resources/v1p2/resources",
|
|
1769
|
+
componentResources: "/ims/oneroster/rostering/v1p2/courses/component-resources",
|
|
1770
|
+
classes: "/ims/oneroster/rostering/v1p2/classes",
|
|
1771
|
+
enrollments: "/ims/oneroster/rostering/v1p2/enrollments",
|
|
1772
|
+
assessmentLineItems: "/ims/oneroster/gradebook/v1p2/assessmentLineItems",
|
|
1773
|
+
assessmentResults: "/ims/oneroster/gradebook/v1p2/assessmentResults",
|
|
1774
|
+
users: "/ims/oneroster/rostering/v1p2/users"
|
|
1775
|
+
};
|
|
1776
|
+
CALIPER_ENDPOINTS = {
|
|
1777
|
+
events: "/caliper/event",
|
|
1778
|
+
validate: "/caliper/event/validate"
|
|
1779
|
+
};
|
|
1780
|
+
CALIPER_CONSTANTS = {
|
|
1781
|
+
context: "http://purl.imsglobal.org/ctx/caliper/v1p2",
|
|
1782
|
+
profile: "TimebackProfile",
|
|
1783
|
+
dataVersion: "http://purl.imsglobal.org/ctx/caliper/v1p2"
|
|
1784
|
+
};
|
|
1785
|
+
TIMEBACK_EVENT_TYPES = {
|
|
1786
|
+
activityEvent: "ActivityEvent",
|
|
1787
|
+
timeSpentEvent: "TimeSpentEvent"
|
|
1788
|
+
};
|
|
1789
|
+
TIMEBACK_ACTIONS = {
|
|
1790
|
+
completed: "Completed",
|
|
1791
|
+
spentTime: "SpentTime"
|
|
1792
|
+
};
|
|
1793
|
+
TIMEBACK_TYPES = {
|
|
1794
|
+
user: "TimebackUser",
|
|
1795
|
+
activityContext: "TimebackActivityContext",
|
|
1796
|
+
activityMetricsCollection: "TimebackActivityMetricsCollection",
|
|
1797
|
+
timeSpentMetricsCollection: "TimebackTimeSpentMetricsCollection"
|
|
1798
|
+
};
|
|
1799
|
+
ACTIVITY_METRIC_TYPES = {
|
|
1800
|
+
totalQuestions: "totalQuestions",
|
|
1801
|
+
correctQuestions: "correctQuestions",
|
|
1802
|
+
xpEarned: "xpEarned",
|
|
1803
|
+
masteredUnits: "masteredUnits"
|
|
1804
|
+
};
|
|
1805
|
+
TIME_METRIC_TYPES = {
|
|
1806
|
+
active: "active",
|
|
1807
|
+
inactive: "inactive",
|
|
1808
|
+
waste: "waste",
|
|
1809
|
+
unknown: "unknown",
|
|
1810
|
+
antiPattern: "anti-pattern"
|
|
1811
|
+
};
|
|
1812
|
+
TIMEBACK_SUBJECTS = [
|
|
1813
|
+
"Math",
|
|
1814
|
+
"FastMath",
|
|
1815
|
+
"Science",
|
|
1816
|
+
"Social Studies",
|
|
1817
|
+
"Language",
|
|
1818
|
+
"Reading",
|
|
1819
|
+
"Vocabulary",
|
|
1820
|
+
"Writing"
|
|
1821
|
+
];
|
|
1822
|
+
TIMEBACK_GRADE_LEVELS = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
|
1823
|
+
TIMEBACK_GRADE_LEVEL_LABELS = {
|
|
1824
|
+
"-1": "pre-k",
|
|
1825
|
+
"0": "kindergarten",
|
|
1826
|
+
"1": "1st grade",
|
|
1827
|
+
"2": "2nd grade",
|
|
1828
|
+
"3": "3rd grade",
|
|
1829
|
+
"4": "4th grade",
|
|
1830
|
+
"5": "5th grade",
|
|
1831
|
+
"6": "6th grade",
|
|
1832
|
+
"7": "7th grade",
|
|
1833
|
+
"8": "8th grade",
|
|
1834
|
+
"9": "9th grade",
|
|
1835
|
+
"10": "10th grade",
|
|
1836
|
+
"11": "11th grade",
|
|
1837
|
+
"12": "12th grade",
|
|
1838
|
+
"13": "AP"
|
|
1839
|
+
};
|
|
1840
|
+
CALIPER_SUBJECTS = {
|
|
1841
|
+
Reading: "Reading",
|
|
1842
|
+
Language: "Language",
|
|
1843
|
+
Vocabulary: "Vocabulary",
|
|
1844
|
+
SocialStudies: "Social Studies",
|
|
1845
|
+
Writing: "Writing",
|
|
1846
|
+
Science: "Science",
|
|
1847
|
+
FastMath: "FastMath",
|
|
1848
|
+
Math: "Math",
|
|
1849
|
+
None: "None"
|
|
1850
|
+
};
|
|
1851
|
+
ONEROSTER_STATUS = {
|
|
1852
|
+
active: "active",
|
|
1853
|
+
toBeDeleted: "tobedeleted"
|
|
1854
|
+
};
|
|
1855
|
+
SCORE_STATUS = {
|
|
1856
|
+
exempt: "exempt",
|
|
1857
|
+
fullyGraded: "fully graded",
|
|
1858
|
+
notSubmitted: "not submitted",
|
|
1859
|
+
partiallyGraded: "partially graded",
|
|
1860
|
+
submitted: "submitted"
|
|
1861
|
+
};
|
|
1862
|
+
ENV_VARS = {
|
|
1863
|
+
clientId: "TIMEBACK_CLIENT_ID",
|
|
1864
|
+
clientSecret: "TIMEBACK_CLIENT_SECRET",
|
|
1865
|
+
baseUrl: "TIMEBACK_BASE_URL",
|
|
1866
|
+
environment: "TIMEBACK_ENVIRONMENT",
|
|
1867
|
+
vendorResourceId: "TIMEBACK_VENDOR_RESOURCE_ID",
|
|
1868
|
+
launchBaseUrl: "GAME_URL"
|
|
1869
|
+
};
|
|
1870
|
+
HTTP_DEFAULTS = {
|
|
1871
|
+
timeout: 3e4,
|
|
1872
|
+
retries: 3,
|
|
1873
|
+
retryBackoffBase: 2
|
|
1874
|
+
};
|
|
1875
|
+
AUTH_DEFAULTS = {
|
|
1876
|
+
tokenCacheDuration: 5e4
|
|
1877
|
+
};
|
|
1878
|
+
CACHE_DEFAULTS = {
|
|
1879
|
+
defaultTTL: 10 * 60 * 1e3,
|
|
1880
|
+
defaultMaxSize: 500,
|
|
1881
|
+
defaultName: "TimebackCache",
|
|
1882
|
+
studentTTL: 10 * 60 * 1e3,
|
|
1883
|
+
studentMaxSize: 500,
|
|
1884
|
+
assessmentTTL: 30 * 60 * 1e3,
|
|
1885
|
+
assessmentMaxSize: 200,
|
|
1886
|
+
enrollmentTTL: 5 * 1e3,
|
|
1887
|
+
enrollmentMaxSize: 100
|
|
1888
|
+
};
|
|
1889
|
+
CONFIG_DEFAULTS = {
|
|
1890
|
+
fileNames: ["timeback.config.js", "timeback.config.json"]
|
|
1891
|
+
};
|
|
1892
|
+
PLAYCADEMY_DEFAULTS = {
|
|
1893
|
+
organization: TIMEBACK_ORG_SOURCED_ID,
|
|
1894
|
+
launchBaseUrls: PLAYCADEMY_BASE_URLS2
|
|
1895
|
+
};
|
|
1896
|
+
RESOURCE_DEFAULTS = {
|
|
1897
|
+
organization: {
|
|
1898
|
+
name: TIMEBACK_ORG_NAME,
|
|
1899
|
+
type: TIMEBACK_ORG_TYPE
|
|
1900
|
+
},
|
|
1901
|
+
course: {
|
|
1902
|
+
gradingScheme: TIMEBACK_COURSE_DEFAULTS.gradingScheme,
|
|
1903
|
+
level: TIMEBACK_COURSE_DEFAULTS.level,
|
|
1904
|
+
metadata: {
|
|
1905
|
+
goals: TIMEBACK_COURSE_DEFAULTS.goals,
|
|
1906
|
+
metrics: TIMEBACK_COURSE_DEFAULTS.metrics
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
component: TIMEBACK_COMPONENT_DEFAULTS,
|
|
1910
|
+
resource: TIMEBACK_RESOURCE_DEFAULTS,
|
|
1911
|
+
componentResource: TIMEBACK_COMPONENT_RESOURCE_DEFAULTS
|
|
1912
|
+
};
|
|
1913
|
+
HTTP_STATUS = {
|
|
1914
|
+
CLIENT_ERROR_MIN: 400,
|
|
1915
|
+
CLIENT_ERROR_MAX: 500,
|
|
1916
|
+
SERVER_ERROR_MIN: 500
|
|
1917
|
+
};
|
|
1918
|
+
ERROR_NAMES = {
|
|
1919
|
+
timebackAuth: "TimebackAuthError",
|
|
1920
|
+
timebackApi: "TimebackApiError",
|
|
1921
|
+
timebackConfig: "TimebackConfigError",
|
|
1922
|
+
timebackSdk: "TimebackSDKError"
|
|
1923
|
+
};
|
|
1924
|
+
});
|
|
1925
|
+
init_constants();
|
|
1926
|
+
|
|
1927
|
+
// src/lib/templates/loader.ts
|
|
1928
|
+
import { existsSync as existsSync5, readFileSync } from "fs";
|
|
1929
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
1930
|
+
import { fileURLToPath } from "url";
|
|
1931
|
+
var currentDir = dirname3(fileURLToPath(import.meta.url));
|
|
1932
|
+
function loadTemplateString(filename) {
|
|
1933
|
+
const filenamesToTry = filename.endsWith(".template") ? [filename] : [filename, `${filename}.template`];
|
|
1934
|
+
const candidatePaths = filenamesToTry.flatMap((name) => [
|
|
1935
|
+
// Dev (TS sources): ../../templates from src/lib/templates
|
|
1936
|
+
resolve4(currentDir, "../../templates", name),
|
|
1937
|
+
// Bundled build (single-file output): templates relative to dist root
|
|
1938
|
+
resolve4(currentDir, "templates", name)
|
|
1939
|
+
]);
|
|
1940
|
+
for (const candidate of candidatePaths) {
|
|
1941
|
+
if (existsSync5(candidate)) {
|
|
1942
|
+
return readFileSync(candidate, "utf-8");
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
throw new Error(`Template not found: ${filename}. Searched: ${candidatePaths.join(", ")}`);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// src/lib/config/generator.ts
|
|
1949
|
+
function loadTemplate(filename) {
|
|
1950
|
+
return loadTemplateString(`config/${filename}`);
|
|
1951
|
+
}
|
|
1952
|
+
function generateJsConfig(options) {
|
|
1953
|
+
const {
|
|
1954
|
+
name,
|
|
1955
|
+
description,
|
|
1956
|
+
emoji,
|
|
1957
|
+
customRoutesDirectory,
|
|
1958
|
+
databaseDirectory,
|
|
1959
|
+
auth,
|
|
1960
|
+
kv,
|
|
1961
|
+
bucket,
|
|
1962
|
+
timebackCourses
|
|
1963
|
+
} = options;
|
|
1964
|
+
let template = loadTemplate("playcademy.config.js.template");
|
|
1965
|
+
template = template.replace("{{APP_NAME}}", name);
|
|
1966
|
+
const descriptionLine = description ? `
|
|
1967
|
+
description: '${description}',` : "";
|
|
1968
|
+
template = template.replace("{{APP_DESCRIPTION}}", descriptionLine);
|
|
1969
|
+
const emojiLine = emoji ? `
|
|
1970
|
+
emoji: '${emoji}',` : "";
|
|
1971
|
+
template = template.replace("{{APP_EMOJI}}", emojiLine);
|
|
1972
|
+
const integrationsParts = [];
|
|
1973
|
+
if (customRoutesDirectory) {
|
|
1974
|
+
integrationsParts.push(` customRoutes: { directory: '${customRoutesDirectory}' }`);
|
|
1975
|
+
}
|
|
1976
|
+
if (databaseDirectory) {
|
|
1977
|
+
integrationsParts.push(` database: { directory: '${databaseDirectory}' }`);
|
|
1978
|
+
}
|
|
1979
|
+
if (auth) {
|
|
1980
|
+
integrationsParts.push(` auth: true`);
|
|
1981
|
+
}
|
|
1982
|
+
if (kv) {
|
|
1983
|
+
integrationsParts.push(` kv: true`);
|
|
1984
|
+
}
|
|
1985
|
+
if (bucket) {
|
|
1986
|
+
integrationsParts.push(` bucket: true`);
|
|
1987
|
+
}
|
|
1988
|
+
if (timebackCourses && timebackCourses.length > 0) {
|
|
1989
|
+
let coursesJson = JSON.stringify(timebackCourses, null, 12).split("\n").map((line) => ` ${line}`).join("\n").trim();
|
|
1990
|
+
coursesJson = coursesJson.replace(
|
|
1991
|
+
/"totalXp": null(,?)/g,
|
|
1992
|
+
'"totalXp": null$1 // TODO: Set total XP before running `playcademy timeback setup`'
|
|
1993
|
+
);
|
|
1994
|
+
coursesJson = coursesJson.replace(
|
|
1995
|
+
/"masterableUnits": null(,?)/g,
|
|
1996
|
+
'"masterableUnits": null$1 // TODO: Set masterable units before running `playcademy timeback setup`'
|
|
1997
|
+
);
|
|
1998
|
+
const timebackBlock = ` timeback: {
|
|
1999
|
+
courses: ${coursesJson}
|
|
2000
|
+
}`;
|
|
2001
|
+
integrationsParts.push(timebackBlock);
|
|
2002
|
+
}
|
|
2003
|
+
if (integrationsParts.length > 0) {
|
|
2004
|
+
const integrationsContent = integrationsParts.join(",\n");
|
|
2005
|
+
let integrationsTemplate = loadTemplate("integrations-config.js.template");
|
|
2006
|
+
integrationsTemplate = integrationsTemplate.replace(
|
|
2007
|
+
"{{INTEGRATIONS_CONTENT}}",
|
|
2008
|
+
integrationsContent
|
|
2009
|
+
);
|
|
2010
|
+
template = template.replace("{{INTEGRATIONS_CONFIG}}", integrationsTemplate);
|
|
2011
|
+
} else {
|
|
2012
|
+
template = template.replace("{{INTEGRATIONS_CONFIG}}", "");
|
|
2013
|
+
}
|
|
2014
|
+
return template;
|
|
2015
|
+
}
|
|
2016
|
+
function generateJsonConfig(options) {
|
|
2017
|
+
const {
|
|
2018
|
+
name,
|
|
2019
|
+
description,
|
|
2020
|
+
emoji,
|
|
2021
|
+
customRoutesDirectory,
|
|
2022
|
+
databaseDirectory,
|
|
2023
|
+
auth,
|
|
2024
|
+
kv,
|
|
2025
|
+
bucket,
|
|
2026
|
+
timebackCourses
|
|
2027
|
+
} = options;
|
|
2028
|
+
let template = loadTemplate("playcademy.config.json.template");
|
|
2029
|
+
template = template.replace("{{APP_NAME}}", name);
|
|
2030
|
+
const descriptionLine = description ? `,
|
|
2031
|
+
"description": "${description}"` : "";
|
|
2032
|
+
template = template.replace("{{APP_DESCRIPTION}}", descriptionLine);
|
|
2033
|
+
const emojiLine = emoji ? `,
|
|
2034
|
+
"emoji": "${emoji}"` : "";
|
|
2035
|
+
template = template.replace("{{APP_EMOJI}}", emojiLine);
|
|
2036
|
+
const integrationsConfig = {};
|
|
2037
|
+
if (customRoutesDirectory) {
|
|
2038
|
+
integrationsConfig.customRoutes = { directory: customRoutesDirectory };
|
|
2039
|
+
}
|
|
2040
|
+
if (databaseDirectory) {
|
|
2041
|
+
integrationsConfig.database = { directory: databaseDirectory };
|
|
2042
|
+
}
|
|
2043
|
+
if (auth) {
|
|
2044
|
+
integrationsConfig.auth = true;
|
|
2045
|
+
}
|
|
2046
|
+
if (kv) {
|
|
2047
|
+
integrationsConfig.kv = true;
|
|
2048
|
+
}
|
|
2049
|
+
if (bucket) {
|
|
2050
|
+
integrationsConfig.bucket = true;
|
|
2051
|
+
}
|
|
2052
|
+
if (timebackCourses && timebackCourses.length > 0) {
|
|
2053
|
+
integrationsConfig.timeback = {
|
|
2054
|
+
courses: timebackCourses
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
if (Object.keys(integrationsConfig).length > 0) {
|
|
2058
|
+
const integrationsJson = JSON.stringify(integrationsConfig, null, 4).split("\n").join("\n ");
|
|
2059
|
+
template = template.replace(
|
|
2060
|
+
"{{INTEGRATIONS_CONFIG}}",
|
|
2061
|
+
`,
|
|
2062
|
+
"integrations": ${integrationsJson}`
|
|
2063
|
+
);
|
|
2064
|
+
} else {
|
|
2065
|
+
template = template.replace("{{INTEGRATIONS_CONFIG}}", "");
|
|
2066
|
+
}
|
|
2067
|
+
return template;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/lib/config/writer.ts
|
|
2071
|
+
import { execSync as execSync2 } from "child_process";
|
|
2072
|
+
async function formatConfigWithPrettier(configPath) {
|
|
2073
|
+
try {
|
|
2074
|
+
const pm = getPackageManager();
|
|
2075
|
+
const execCommand = pm === "bun" ? "bunx" : "npx";
|
|
2076
|
+
execSync2(`${execCommand} prettier --write ${configPath}`, {
|
|
2077
|
+
stdio: "ignore",
|
|
2078
|
+
// Silent
|
|
2079
|
+
timeout: 5e3
|
|
2080
|
+
// Don't hang
|
|
2081
|
+
});
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// src/lib/init/display.ts
|
|
2087
|
+
function displaySuccessMessage(context2) {
|
|
2088
|
+
logger.newLine();
|
|
2089
|
+
const nextSteps = [];
|
|
2090
|
+
const pm = getPackageManager();
|
|
2091
|
+
const isVite = context2.engineType === "vite";
|
|
2092
|
+
const isGodot = context2.engineType === "godot";
|
|
2093
|
+
nextSteps.push(`1. Review your config file: <${context2.configFileName}>`);
|
|
2094
|
+
if (context2.hasDatabase) {
|
|
2095
|
+
nextSteps.push(`3. Review schema: <db/schema/index.ts>`);
|
|
2096
|
+
nextSteps.push("4. Start dev server: `playcademy dev`");
|
|
2097
|
+
nextSteps.push(`5. Push your schema: \`${getRunCommand(pm, "db:push")}\``);
|
|
2098
|
+
}
|
|
2099
|
+
if (context2.hasAuth) {
|
|
2100
|
+
const stepNum = context2.hasDatabase ? "6" : "2";
|
|
2101
|
+
nextSteps.push(`${stepNum}. Review auth config: <server/lib/auth.ts>`);
|
|
2102
|
+
}
|
|
2103
|
+
if (isVite) {
|
|
2104
|
+
const buildExample = getRunCommand(pm, "build");
|
|
2105
|
+
const buildStepNum = nextSteps.length + 1;
|
|
2106
|
+
nextSteps.push(`${buildStepNum}. Build your app: \`${buildExample}\``);
|
|
2107
|
+
}
|
|
2108
|
+
if (isGodot) {
|
|
2109
|
+
const stepNum = nextSteps.length + 1;
|
|
2110
|
+
nextSteps.push(`${stepNum}. Open in Godot Editor`);
|
|
2111
|
+
}
|
|
2112
|
+
if (context2.apiDirectory) {
|
|
2113
|
+
const stepNum = nextSteps.length + 1;
|
|
2114
|
+
nextSteps.push(`${stepNum}. Customize API routes: <${context2.apiDirectory}>`);
|
|
2115
|
+
}
|
|
2116
|
+
if (context2.timebackConfig) {
|
|
2117
|
+
const stepNum = nextSteps.length + 1;
|
|
2118
|
+
nextSteps.push(`${stepNum}. Set up TimeBack: \`playcademy timeback setup\``);
|
|
2119
|
+
}
|
|
2120
|
+
const deployStep = nextSteps.length + 1;
|
|
2121
|
+
nextSteps.push(`${deployStep}. Deploy: \`playcademy deploy\``);
|
|
2122
|
+
nextSteps.push("");
|
|
2123
|
+
nextSteps.push(`Learn more: <<${DOCS_URL}>>`);
|
|
2124
|
+
logger.admonition("tip", "Next Steps", nextSteps);
|
|
2125
|
+
logger.newLine();
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// src/lib/init/engine/fetch.ts
|
|
2129
|
+
import { readdirSync as readdirSync2, renameSync } from "fs";
|
|
2130
|
+
import { join as join8 } from "path";
|
|
2131
|
+
import { downloadTemplate } from "giget";
|
|
2132
|
+
function renameSpecialFiles(destDir) {
|
|
2133
|
+
const files = readdirSync2(destDir, { withFileTypes: true });
|
|
2134
|
+
for (const file of files) {
|
|
2135
|
+
const newName = TEMPLATE_RENAME_FILES[file.name];
|
|
2136
|
+
if (newName) {
|
|
2137
|
+
const oldPath = join8(destDir, file.name);
|
|
2138
|
+
const newPath = join8(destDir, newName);
|
|
2139
|
+
renameSync(oldPath, newPath);
|
|
2140
|
+
}
|
|
2141
|
+
if (file.isDirectory()) {
|
|
2142
|
+
renameSpecialFiles(join8(destDir, file.name));
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
async function fetchTemplate(template, destDir) {
|
|
2147
|
+
const { source } = template;
|
|
2148
|
+
const ref = source.ref || DEFAULT_TEMPLATE_REF;
|
|
2149
|
+
const templateSource = `github:${source.location}#${ref}`;
|
|
2150
|
+
try {
|
|
2151
|
+
await downloadTemplate(templateSource, {
|
|
2152
|
+
dir: destDir,
|
|
2153
|
+
force: true,
|
|
2154
|
+
forceClean: false
|
|
2155
|
+
});
|
|
2156
|
+
renameSpecialFiles(destDir);
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2159
|
+
throw new Error(
|
|
2160
|
+
`Failed to fetch template from GitHub: ${message}
|
|
2161
|
+
Source: ${templateSource}`
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/lib/init/engine/prompts.ts
|
|
2167
|
+
import { select } from "@inquirer/prompts";
|
|
2168
|
+
import { cyan as cyan3, magenta as magenta2, yellow as yellow3 } from "colorette";
|
|
2169
|
+
|
|
2170
|
+
// src/lib/init/engine/registry.ts
|
|
2171
|
+
import { blue as blue2, cyan as cyan2, green as green2, magenta, red as red2, yellow as yellow2 } from "colorette";
|
|
2172
|
+
|
|
2173
|
+
// src/lib/init/engine/hooks.ts
|
|
2174
|
+
import { existsSync as existsSync6, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2175
|
+
import { rename, rm } from "fs/promises";
|
|
2176
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
2177
|
+
import JSZip from "jszip";
|
|
2178
|
+
var godotAfterFetch = async ({ destDir }) => {
|
|
2179
|
+
const addonsDir = join9(destDir, "addons");
|
|
2180
|
+
const targetDir = join9(addonsDir, "playcademy");
|
|
2181
|
+
let zipData;
|
|
2182
|
+
await runStep(
|
|
2183
|
+
"Downloading Playcademy Addon for Godot",
|
|
2184
|
+
async () => {
|
|
2185
|
+
const response = await fetch(GODOT_ADDON_URL);
|
|
2186
|
+
if (!response.ok) {
|
|
2187
|
+
throw new Error(
|
|
2188
|
+
`Failed to download addon: ${response.status} ${response.statusText}`
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
zipData = await response.arrayBuffer();
|
|
2192
|
+
},
|
|
2193
|
+
"Playcademy Addon downloaded",
|
|
2194
|
+
{ silent: true }
|
|
2195
|
+
);
|
|
2196
|
+
await runStep(
|
|
2197
|
+
"Extracting Playcademy Addon",
|
|
2198
|
+
async () => {
|
|
2199
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
2200
|
+
if (!existsSync6(addonsDir)) {
|
|
2201
|
+
mkdirSync(addonsDir, { recursive: true });
|
|
2202
|
+
}
|
|
2203
|
+
const extractPromises = [];
|
|
2204
|
+
zip.forEach((relativePath, file) => {
|
|
2205
|
+
if (file.dir) return;
|
|
2206
|
+
const extractPath = join9(destDir, relativePath);
|
|
2207
|
+
extractPromises.push(
|
|
2208
|
+
file.async("nodebuffer").then((content) => {
|
|
2209
|
+
const dir = dirname4(extractPath);
|
|
2210
|
+
if (!existsSync6(dir)) {
|
|
2211
|
+
mkdirSync(dir, { recursive: true });
|
|
2212
|
+
}
|
|
2213
|
+
writeFileSync2(extractPath, content);
|
|
2214
|
+
})
|
|
2215
|
+
);
|
|
2216
|
+
});
|
|
2217
|
+
await Promise.all(extractPromises);
|
|
2218
|
+
const extractedRoot = join9(destDir, "playcademy", "addons", "playcademy");
|
|
2219
|
+
if (existsSync6(extractedRoot)) {
|
|
2220
|
+
if (existsSync6(targetDir)) {
|
|
2221
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
2222
|
+
}
|
|
2223
|
+
await rename(extractedRoot, targetDir);
|
|
2224
|
+
const extractedParent = join9(destDir, "playcademy");
|
|
2225
|
+
await rm(extractedParent, { recursive: true, force: true });
|
|
2226
|
+
}
|
|
2227
|
+
},
|
|
2228
|
+
"Playcademy Addon extracted",
|
|
2229
|
+
{ silent: true }
|
|
2230
|
+
);
|
|
2231
|
+
};
|
|
2232
|
+
|
|
2233
|
+
// src/lib/init/engine/registry.ts
|
|
2234
|
+
var viteFrameworks = [
|
|
2235
|
+
{
|
|
2236
|
+
id: "vanilla",
|
|
2237
|
+
display: "Vanilla",
|
|
2238
|
+
color: yellow2,
|
|
2239
|
+
templates: [
|
|
2240
|
+
{
|
|
2241
|
+
id: "vanilla-ts",
|
|
2242
|
+
display: "TypeScript",
|
|
2243
|
+
color: blue2,
|
|
2244
|
+
description: "Vanilla TypeScript starter",
|
|
2245
|
+
source: { location: `${TEMPLATE_REPOS.VITE}/template-vanilla-ts` }
|
|
2246
|
+
}
|
|
2247
|
+
]
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
id: "react",
|
|
2251
|
+
display: "React",
|
|
2252
|
+
color: cyan2,
|
|
2253
|
+
templates: [
|
|
2254
|
+
{
|
|
2255
|
+
id: "react-ts",
|
|
2256
|
+
display: "TypeScript",
|
|
2257
|
+
color: blue2,
|
|
2258
|
+
description: "React with TypeScript",
|
|
2259
|
+
source: { location: `${TEMPLATE_REPOS.VITE}/template-react-ts` }
|
|
2260
|
+
}
|
|
2261
|
+
]
|
|
2262
|
+
},
|
|
2263
|
+
{
|
|
2264
|
+
id: "vue",
|
|
2265
|
+
display: "Vue",
|
|
2266
|
+
color: green2,
|
|
2267
|
+
templates: [
|
|
2268
|
+
{
|
|
2269
|
+
id: "vue-ts",
|
|
2270
|
+
display: "TypeScript",
|
|
2271
|
+
color: blue2,
|
|
2272
|
+
description: "Vue 3 with TypeScript",
|
|
2273
|
+
source: { location: `${TEMPLATE_REPOS.VITE}/template-vue-ts` }
|
|
2274
|
+
}
|
|
2275
|
+
]
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
id: "svelte",
|
|
2279
|
+
display: "Svelte",
|
|
2280
|
+
color: red2,
|
|
2281
|
+
templates: [
|
|
2282
|
+
{
|
|
2283
|
+
id: "svelte-ts",
|
|
2284
|
+
display: "TypeScript",
|
|
2285
|
+
color: blue2,
|
|
2286
|
+
description: "Svelte with TypeScript",
|
|
2287
|
+
source: { location: `${TEMPLATE_REPOS.VITE}/template-svelte-ts` }
|
|
2288
|
+
}
|
|
2289
|
+
]
|
|
2290
|
+
}
|
|
2291
|
+
];
|
|
2292
|
+
var godotFrameworks = [
|
|
2293
|
+
{
|
|
2294
|
+
id: "godot",
|
|
2295
|
+
display: "Godot 4",
|
|
2296
|
+
color: magenta,
|
|
2297
|
+
templates: [
|
|
2298
|
+
{
|
|
2299
|
+
id: "godot-4",
|
|
2300
|
+
display: "Godot 4 with Playcademy SDK",
|
|
2301
|
+
color: magenta,
|
|
2302
|
+
description: "Godot 4 with Playcademy addon",
|
|
2303
|
+
source: { location: `${TEMPLATE_REPOS.SUPERBUILDERS}/godot-template` },
|
|
2304
|
+
afterFetch: godotAfterFetch
|
|
2305
|
+
}
|
|
2306
|
+
]
|
|
2307
|
+
}
|
|
2308
|
+
];
|
|
2309
|
+
var engines = {
|
|
2310
|
+
vite: { type: "vite", frameworks: viteFrameworks },
|
|
2311
|
+
godot: { type: "godot", frameworks: godotFrameworks }
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
// src/lib/init/engine/prompts.ts
|
|
2315
|
+
async function promptForEngine() {
|
|
2316
|
+
return select({
|
|
2317
|
+
message: "Select a project type:",
|
|
2318
|
+
choices: [
|
|
2319
|
+
{
|
|
2320
|
+
value: "vite",
|
|
2321
|
+
name: `${cyan3("Vite")}`
|
|
2322
|
+
},
|
|
2323
|
+
{
|
|
2324
|
+
value: "godot",
|
|
2325
|
+
name: `${magenta2("Godot")}`
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
value: "none",
|
|
2329
|
+
name: `${yellow3("None")}`
|
|
2330
|
+
}
|
|
2331
|
+
]
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
async function promptForFramework(engine) {
|
|
2335
|
+
const config = engines[engine];
|
|
2336
|
+
if (!config || config.frameworks.length === 0) {
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
if (config.frameworks.length === 1) {
|
|
2340
|
+
return config.frameworks[0];
|
|
2341
|
+
}
|
|
2342
|
+
return select({
|
|
2343
|
+
message: "Select a framework:",
|
|
2344
|
+
choices: config.frameworks.map((fw) => ({
|
|
2345
|
+
value: fw,
|
|
2346
|
+
name: fw.color(fw.display)
|
|
2347
|
+
}))
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
async function promptForTemplate(framework) {
|
|
2351
|
+
if (framework.templates.length === 1) {
|
|
2352
|
+
return framework.templates[0];
|
|
2353
|
+
}
|
|
2354
|
+
return select({
|
|
2355
|
+
message: "Select a variant:",
|
|
2356
|
+
choices: framework.templates.map((t) => ({
|
|
2357
|
+
value: t,
|
|
2358
|
+
name: t.color(t.display)
|
|
2359
|
+
}))
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// src/lib/deploy/godot.ts
|
|
2364
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync } from "fs";
|
|
2365
|
+
import { join as join10 } from "path";
|
|
2366
|
+
import { confirm, select as select2 } from "@inquirer/prompts";
|
|
2367
|
+
function isGodotProject() {
|
|
2368
|
+
return existsSync7(join10(getWorkspace(), GODOT_PROJECT_FILE));
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// src/lib/init/vite.ts
|
|
2372
|
+
init_file_loader();
|
|
2373
|
+
import { execSync as execSync3 } from "child_process";
|
|
2374
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2375
|
+
import path from "node:path";
|
|
2376
|
+
var viteConfigTemplate = loadTemplateString("config/vite-config.ts");
|
|
2377
|
+
async function findViteConfig() {
|
|
2378
|
+
const workspace = getWorkspace();
|
|
2379
|
+
const result = await findFile(
|
|
2380
|
+
["vite.config.ts", "vite.config.js", "vite.config.mts", "vite.config.mjs"],
|
|
2381
|
+
{ cwd: workspace }
|
|
2382
|
+
);
|
|
2383
|
+
return result?.path ?? null;
|
|
2384
|
+
}
|
|
2385
|
+
function isPluginConfigured(configPath) {
|
|
2386
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
2387
|
+
return content.includes("@playcademy/vite-plugin") || content.includes("playcademy(");
|
|
2388
|
+
}
|
|
2389
|
+
async function addVitePlugin(options = {}) {
|
|
2390
|
+
const { install = false } = options;
|
|
2391
|
+
const workspace = getWorkspace();
|
|
2392
|
+
const pkgPath = path.join(workspace, "package.json");
|
|
2393
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
2394
|
+
const hasPlugin = pkg.dependencies?.["@playcademy/vite-plugin"] || pkg.devDependencies?.["@playcademy/vite-plugin"];
|
|
2395
|
+
if (hasPlugin) {
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
if (install) {
|
|
2399
|
+
const pm = detectPackageManager(workspace);
|
|
2400
|
+
const command = getAddDevDependencyCommand(pm, "@playcademy/vite-plugin");
|
|
2401
|
+
try {
|
|
2402
|
+
await runStep(
|
|
2403
|
+
"Installing @playcademy/vite-plugin...",
|
|
2404
|
+
async () => {
|
|
2405
|
+
execSync3(command, {
|
|
2406
|
+
cwd: workspace,
|
|
2407
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
2408
|
+
});
|
|
2409
|
+
},
|
|
2410
|
+
"Installed @playcademy/vite-plugin"
|
|
2411
|
+
);
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
throw new Error(`Failed to install @playcademy/vite-plugin: ${error}`);
|
|
2414
|
+
}
|
|
2415
|
+
} else {
|
|
2416
|
+
if (!pkg.devDependencies) pkg.devDependencies = {};
|
|
2417
|
+
pkg.devDependencies["@playcademy/vite-plugin"] = "latest";
|
|
2418
|
+
writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
2419
|
+
}
|
|
2420
|
+
return true;
|
|
2421
|
+
}
|
|
2422
|
+
async function updateViteConfig(configPath) {
|
|
2423
|
+
let content = readFileSync3(configPath, "utf-8");
|
|
2424
|
+
if (!content.includes("@playcademy/vite-plugin")) {
|
|
2425
|
+
const importMatch = content.match(/import .+ from ['"]vite['"]/);
|
|
2426
|
+
if (importMatch) {
|
|
2427
|
+
const importIndex = importMatch.index + importMatch[0].length;
|
|
2428
|
+
content = content.slice(0, importIndex) + `
|
|
2429
|
+
import { playcademy } from '@playcademy/vite-plugin'` + content.slice(importIndex);
|
|
2430
|
+
} else {
|
|
2431
|
+
content = `import { playcademy } from '@playcademy/vite-plugin'
|
|
2432
|
+
|
|
2433
|
+
` + content;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
if (!content.includes("playcademy()")) {
|
|
2437
|
+
const pluginsMatch = content.match(/plugins:\s*\[/);
|
|
2438
|
+
if (pluginsMatch) {
|
|
2439
|
+
const pluginsIndex = pluginsMatch.index + pluginsMatch[0].length;
|
|
2440
|
+
content = content.slice(0, pluginsIndex) + `playcademy(),` + content.slice(pluginsIndex);
|
|
2441
|
+
} else {
|
|
2442
|
+
const defineConfigMatch = content.match(/defineConfig\(\{/);
|
|
2443
|
+
if (defineConfigMatch) {
|
|
2444
|
+
const configIndex = defineConfigMatch.index + defineConfigMatch[0].length;
|
|
2445
|
+
content = content.slice(0, configIndex) + `
|
|
2446
|
+
plugins: [playcademy()],` + content.slice(configIndex);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
writeFileSync3(configPath, content);
|
|
2451
|
+
await formatConfigWithPrettier(configPath);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// src/lib/init/engine/detect.ts
|
|
2455
|
+
async function detectExistingEngine() {
|
|
2456
|
+
if (isGodotProject()) {
|
|
2457
|
+
return "godot";
|
|
2458
|
+
}
|
|
2459
|
+
if (await findViteConfig()) {
|
|
2460
|
+
return "vite";
|
|
2461
|
+
}
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// src/lib/init/engine/index.ts
|
|
2466
|
+
async function selectEngineTemplate() {
|
|
2467
|
+
const engine = await promptForEngine();
|
|
2468
|
+
if (engine === "none") {
|
|
2469
|
+
return null;
|
|
2470
|
+
}
|
|
2471
|
+
const framework = await promptForFramework(engine);
|
|
2472
|
+
if (!framework) {
|
|
2473
|
+
return null;
|
|
2474
|
+
}
|
|
2475
|
+
const template = await promptForTemplate(framework);
|
|
2476
|
+
return { engine, framework, template };
|
|
2477
|
+
}
|
|
2478
|
+
async function applyEngineSelection(selection, destDir, options = {}) {
|
|
2479
|
+
const { engine, framework, template } = selection;
|
|
2480
|
+
const hookOptions = {
|
|
2481
|
+
engine,
|
|
2482
|
+
destDir,
|
|
2483
|
+
...options
|
|
2484
|
+
};
|
|
2485
|
+
if (template.beforeFetch) {
|
|
2486
|
+
await template.beforeFetch(hookOptions);
|
|
2487
|
+
}
|
|
2488
|
+
await fetchTemplate(template, destDir);
|
|
2489
|
+
if (template.afterFetch) {
|
|
2490
|
+
await template.afterFetch(hookOptions);
|
|
2491
|
+
}
|
|
2492
|
+
return {
|
|
2493
|
+
engine,
|
|
2494
|
+
framework,
|
|
2495
|
+
template,
|
|
2496
|
+
destDir
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// src/lib/init/gitignore.ts
|
|
2501
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2502
|
+
import { join as join11 } from "path";
|
|
2503
|
+
var rootGitignoreTemplate = loadTemplateString("gitignore");
|
|
2504
|
+
function ensureRootGitignore(workspace = getWorkspace()) {
|
|
2505
|
+
const gitignorePath = join11(workspace, ".gitignore");
|
|
2506
|
+
if (!existsSync8(gitignorePath)) {
|
|
2507
|
+
writeFileSync4(gitignorePath, rootGitignoreTemplate);
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
const existingContent = readFileSync4(gitignorePath, "utf-8");
|
|
2511
|
+
const existingNormalized = new Set(
|
|
2512
|
+
existingContent.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map(normalizeGitignoreEntry)
|
|
2513
|
+
);
|
|
2514
|
+
const templateLines = rootGitignoreTemplate.split("\n");
|
|
2515
|
+
const entriesToAdd = [];
|
|
2516
|
+
for (const line of templateLines) {
|
|
2517
|
+
const trimmed = line.trim();
|
|
2518
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2519
|
+
if (existingNormalized.has(normalizeGitignoreEntry(trimmed))) continue;
|
|
2520
|
+
entriesToAdd.push(trimmed);
|
|
2521
|
+
}
|
|
2522
|
+
if (entriesToAdd.length === 0) return;
|
|
2523
|
+
const hasTrailingNewline = existingContent.endsWith("\n");
|
|
2524
|
+
const prefix = hasTrailingNewline ? "\n" : "\n\n";
|
|
2525
|
+
const header = "# Generated by Playcademy\n";
|
|
2526
|
+
const newEntries = entriesToAdd.join("\n");
|
|
2527
|
+
writeFileSync4(gitignorePath, existingContent + prefix + header + newEntries + "\n");
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// src/lib/init/project.ts
|
|
2531
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "fs";
|
|
2532
|
+
import { resolve as resolve5 } from "path";
|
|
2533
|
+
import { confirm as confirm2, input, select as select3 } from "@inquirer/prompts";
|
|
2534
|
+
async function promptForProjectDirectory(projectDir) {
|
|
2535
|
+
let dir = projectDir;
|
|
2536
|
+
if (dir === void 0) {
|
|
2537
|
+
const choice = await select3({
|
|
2538
|
+
message: "Where do you want to create the project?",
|
|
2539
|
+
choices: [
|
|
2540
|
+
{ value: ".", name: "Current directory" },
|
|
2541
|
+
{ value: "new", name: "New directory" }
|
|
2542
|
+
]
|
|
2543
|
+
});
|
|
2544
|
+
if (choice === "new") {
|
|
2545
|
+
dir = await input({
|
|
2546
|
+
message: "Directory name:"
|
|
2547
|
+
});
|
|
2548
|
+
} else {
|
|
2549
|
+
dir = ".";
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
if (dir === ".") {
|
|
2553
|
+
return {
|
|
2554
|
+
targetDir: process.cwd(),
|
|
2555
|
+
needsCreation: false,
|
|
2556
|
+
cancelled: false
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
const targetDir = resolve5(process.cwd(), dir);
|
|
2560
|
+
if (existsSync9(targetDir)) {
|
|
2561
|
+
logger.admonition("warning", "Directory Exists", [`Directory "${dir}" already exists.`]);
|
|
2562
|
+
logger.newLine();
|
|
2563
|
+
const shouldContinue = await confirm2({
|
|
2564
|
+
message: "Continue and initialize in existing directory?",
|
|
2565
|
+
default: false
|
|
2566
|
+
});
|
|
2567
|
+
if (!shouldContinue) {
|
|
2568
|
+
logger.newLine();
|
|
2569
|
+
logger.remark("Cancelled");
|
|
2570
|
+
logger.newLine();
|
|
2571
|
+
return {
|
|
2572
|
+
targetDir,
|
|
2573
|
+
needsCreation: false,
|
|
2574
|
+
cancelled: true
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
return {
|
|
2578
|
+
targetDir,
|
|
2579
|
+
needsCreation: false,
|
|
2580
|
+
cancelled: false
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
targetDir,
|
|
2585
|
+
needsCreation: true,
|
|
2586
|
+
cancelled: false
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
function createProjectDirectory(info) {
|
|
2590
|
+
if (info.needsCreation) {
|
|
2591
|
+
mkdirSync3(info.targetDir, { recursive: true });
|
|
2592
|
+
}
|
|
2593
|
+
if (info.targetDir !== process.cwd()) {
|
|
2594
|
+
setCliContext({ workspace: info.targetDir });
|
|
2595
|
+
process.chdir(info.targetDir);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// src/lib/init/prompts.ts
|
|
2600
|
+
import { checkbox, confirm as confirm3, input as input2, select as select4 } from "@inquirer/prompts";
|
|
2601
|
+
import { bold as bold4, cyan as cyan4 } from "colorette";
|
|
2602
|
+
|
|
2603
|
+
// src/lib/init/scaffold.ts
|
|
2604
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
2605
|
+
import { join as join14, resolve as resolve6 } from "path";
|
|
2606
|
+
|
|
2607
|
+
// src/lib/init/auth.ts
|
|
2608
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
2609
|
+
import { join as join12 } from "path";
|
|
2610
|
+
var authTemplate = loadTemplateString("auth/auth.ts");
|
|
2611
|
+
var authCatchAllTemplate = loadTemplateString("auth/auth-catch-all.ts");
|
|
2612
|
+
var authSchemaTemplate = loadTemplateString("auth/auth-schema.ts");
|
|
2613
|
+
var protectedRouteTemplate = loadTemplateString("api/sample-protected.ts");
|
|
2614
|
+
function generateAuthConfig(strategies, appName) {
|
|
2615
|
+
let authContent = authTemplate;
|
|
2616
|
+
const slug = appName ? generateSlug(appName) : "my-app";
|
|
2617
|
+
authContent = authContent.replace("{{APP_SLUG}}", slug);
|
|
2618
|
+
const emailConfig = strategies.includes("email") ? `
|
|
2619
|
+
// CUSTOMIZABLE: Email/password authentication
|
|
2620
|
+
emailAndPassword: {
|
|
2621
|
+
enabled: true,
|
|
2622
|
+
},` : "";
|
|
2623
|
+
authContent = authContent.replace("{{EMAIL_AND_PASSWORD}}", emailConfig);
|
|
2624
|
+
const socialProviders = [];
|
|
2625
|
+
if (strategies.includes("github")) {
|
|
2626
|
+
socialProviders.push(
|
|
2627
|
+
` github: {
|
|
2628
|
+
clientId: c.env.secrets?.GITHUB_CLIENT_ID || '',
|
|
2629
|
+
clientSecret: c.env.secrets?.GITHUB_CLIENT_SECRET || '',
|
|
2630
|
+
}`
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
if (strategies.includes("google")) {
|
|
2634
|
+
socialProviders.push(
|
|
2635
|
+
` google: {
|
|
2636
|
+
clientId: c.env.secrets?.GOOGLE_CLIENT_ID || '',
|
|
2637
|
+
clientSecret: c.env.secrets?.GOOGLE_CLIENT_SECRET || '',
|
|
2638
|
+
}`
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
const socialProvidersConfig = socialProviders.length > 0 ? `
|
|
2642
|
+
// CUSTOMIZABLE: Social auth providers
|
|
2643
|
+
socialProviders: {
|
|
2644
|
+
` + socialProviders.join(",\n") + `
|
|
2645
|
+
},
|
|
2646
|
+
` : "";
|
|
2647
|
+
authContent = authContent.replace("{{SOCIAL_PROVIDERS}}", socialProvidersConfig);
|
|
2648
|
+
return authContent;
|
|
2649
|
+
}
|
|
2650
|
+
function scaffoldAuthConfig(workspace, authContent) {
|
|
2651
|
+
const libDir = join12(workspace, SERVER_LIB_DIRECTORY);
|
|
2652
|
+
if (!existsSync10(libDir)) {
|
|
2653
|
+
mkdirSync4(libDir, { recursive: true });
|
|
2654
|
+
}
|
|
2655
|
+
writeFileSync5(join12(libDir, AUTH_CONFIG_FILE), authContent);
|
|
2656
|
+
}
|
|
2657
|
+
function scaffoldAuthRoutes(workspace) {
|
|
2658
|
+
const apiDir = join12(workspace, DEFAULT_API_ROUTES_DIRECTORY);
|
|
2659
|
+
const authApiDir = join12(apiDir, AUTH_API_SUBDIRECTORY);
|
|
2660
|
+
if (!existsSync10(authApiDir)) {
|
|
2661
|
+
mkdirSync4(authApiDir, { recursive: true });
|
|
2662
|
+
}
|
|
2663
|
+
writeFileSync5(join12(authApiDir, "[...all].ts"), authCatchAllTemplate);
|
|
2664
|
+
}
|
|
2665
|
+
function scaffoldAuthSchema(workspace) {
|
|
2666
|
+
const dbDir = join12(workspace, DEFAULT_DATABASE_DIRECTORY);
|
|
2667
|
+
const schemaDir = join12(dbDir, SCHEMA_SUBDIRECTORY);
|
|
2668
|
+
if (!existsSync10(schemaDir)) {
|
|
2669
|
+
mkdirSync4(schemaDir, { recursive: true });
|
|
2670
|
+
}
|
|
2671
|
+
writeFileSync5(join12(schemaDir, "auth.ts"), authSchemaTemplate);
|
|
2672
|
+
const schemaIndexPath = join12(schemaDir, DB_FILES.SCHEMA_INDEX);
|
|
2673
|
+
if (existsSync10(schemaIndexPath)) {
|
|
2674
|
+
const existing = readFileSync5(schemaIndexPath, "utf-8");
|
|
2675
|
+
if (!existing.includes("export * from './auth'")) {
|
|
2676
|
+
writeFileSync5(schemaIndexPath, existing + "\nexport * from './auth'\n");
|
|
2677
|
+
}
|
|
2678
|
+
} else {
|
|
2679
|
+
writeFileSync5(schemaIndexPath, "export * from './auth'\n");
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
function scaffoldProtectedExample(workspace) {
|
|
2683
|
+
const apiDir = join12(workspace, DEFAULT_API_ROUTES_DIRECTORY);
|
|
2684
|
+
const sampleDir = join12(apiDir, SAMPLE_API_SUBDIRECTORY);
|
|
2685
|
+
if (!existsSync10(sampleDir)) {
|
|
2686
|
+
mkdirSync4(sampleDir, { recursive: true });
|
|
2687
|
+
}
|
|
2688
|
+
writeFileSync5(join12(sampleDir, "protected.ts"), protectedRouteTemplate);
|
|
2689
|
+
}
|
|
2690
|
+
function updateEnvForAuth(workspace, strategies) {
|
|
2691
|
+
const envLines = [];
|
|
2692
|
+
envLines.push("# Better Auth (required)");
|
|
2693
|
+
envLines.push("# Generate with: openssl rand -base64 32");
|
|
2694
|
+
envLines.push("BETTER_AUTH_SECRET=your_secret_here");
|
|
2695
|
+
envLines.push("");
|
|
2696
|
+
if (strategies.includes("github")) {
|
|
2697
|
+
envLines.push("# GitHub OAuth (for standalone auth)");
|
|
2698
|
+
envLines.push("GITHUB_CLIENT_ID=your_github_client_id");
|
|
2699
|
+
envLines.push("GITHUB_CLIENT_SECRET=your_github_client_secret");
|
|
2700
|
+
envLines.push("");
|
|
2701
|
+
}
|
|
2702
|
+
if (strategies.includes("google")) {
|
|
2703
|
+
envLines.push("# Google OAuth (for standalone auth)");
|
|
2704
|
+
envLines.push("GOOGLE_CLIENT_ID=your_google_client_id");
|
|
2705
|
+
envLines.push("GOOGLE_CLIENT_SECRET=your_google_client_secret");
|
|
2706
|
+
envLines.push("");
|
|
2707
|
+
}
|
|
2708
|
+
updateEnvExample(workspace, envLines);
|
|
2709
|
+
}
|
|
2710
|
+
async function scaffoldAuthSetup(options = {}) {
|
|
2711
|
+
const workspace = getWorkspace();
|
|
2712
|
+
const { strategies = [], appName } = options;
|
|
2713
|
+
let packagesUpdated = false;
|
|
2714
|
+
await runStep(
|
|
2715
|
+
"Configuring authentication...",
|
|
2716
|
+
async () => {
|
|
2717
|
+
const authContent = generateAuthConfig(strategies, appName);
|
|
2718
|
+
scaffoldAuthConfig(workspace, authContent);
|
|
2719
|
+
scaffoldAuthRoutes(workspace);
|
|
2720
|
+
scaffoldAuthSchema(workspace);
|
|
2721
|
+
scaffoldProtectedExample(workspace);
|
|
2722
|
+
packagesUpdated = await setupPackageJson(workspace);
|
|
2723
|
+
updateEnvForAuth(workspace, strategies);
|
|
2724
|
+
},
|
|
2725
|
+
"Authentication configured"
|
|
2726
|
+
);
|
|
2727
|
+
return packagesUpdated;
|
|
2728
|
+
}
|
|
2729
|
+
async function setupPackageJson(workspace) {
|
|
2730
|
+
const pkgPath = join12(workspace, "package.json");
|
|
2731
|
+
const authDeps = {
|
|
2732
|
+
"@playcademy/better-auth": PLAYCADEMY_AUTH_VERSION,
|
|
2733
|
+
"better-auth": BETTER_AUTH_VERSION
|
|
2734
|
+
};
|
|
2735
|
+
if (existsSync10(pkgPath)) {
|
|
2736
|
+
await runStep(
|
|
2737
|
+
"Updating package.json deps",
|
|
2738
|
+
async () => {
|
|
2739
|
+
const existing = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
2740
|
+
existing.dependencies = { ...existing.dependencies, ...authDeps };
|
|
2741
|
+
writeFileSync5(pkgPath, JSON.stringify(existing, null, 2) + "\n");
|
|
2742
|
+
},
|
|
2743
|
+
"Updated package.json deps"
|
|
2744
|
+
);
|
|
2745
|
+
return true;
|
|
2746
|
+
}
|
|
2747
|
+
return false;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// src/lib/init/database.ts
|
|
2751
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
2752
|
+
import { join as join13 } from "path";
|
|
2753
|
+
|
|
2754
|
+
// package.json with { type: 'json' }
|
|
2755
|
+
var package_default2 = {
|
|
2756
|
+
name: "playcademy",
|
|
2757
|
+
version: "0.14.29",
|
|
2758
|
+
type: "module",
|
|
2759
|
+
exports: {
|
|
2760
|
+
".": {
|
|
2761
|
+
types: "./dist/index.d.ts",
|
|
2762
|
+
import: "./dist/index.js",
|
|
2763
|
+
require: "./dist/index.js"
|
|
2764
|
+
},
|
|
2765
|
+
"./cli": {
|
|
2766
|
+
types: "./dist/cli.d.ts",
|
|
2767
|
+
import: "./dist/cli.js",
|
|
2768
|
+
require: "./dist/cli.js"
|
|
2769
|
+
},
|
|
2770
|
+
"./db": {
|
|
2771
|
+
types: "./dist/db.d.ts",
|
|
2772
|
+
import: "./dist/db.js",
|
|
2773
|
+
require: "./dist/db.js"
|
|
2774
|
+
},
|
|
2775
|
+
"./utils": {
|
|
2776
|
+
import: "./dist/utils.js",
|
|
2777
|
+
types: "./dist/utils.d.ts"
|
|
2778
|
+
},
|
|
2779
|
+
"./constants": {
|
|
2780
|
+
import: "./dist/constants.js",
|
|
2781
|
+
types: "./dist/constants.d.ts"
|
|
2782
|
+
},
|
|
2783
|
+
"./types": {
|
|
2784
|
+
types: "./dist/index.d.ts"
|
|
2785
|
+
},
|
|
2786
|
+
"./version": {
|
|
2787
|
+
import: "./dist/version.js",
|
|
2788
|
+
types: "./dist/version.d.ts"
|
|
2789
|
+
}
|
|
2790
|
+
},
|
|
2791
|
+
main: "./dist/index.js",
|
|
2792
|
+
module: "./dist/index.js",
|
|
2793
|
+
bin: {
|
|
2794
|
+
playcademy: "./dist/index.js"
|
|
2795
|
+
},
|
|
2796
|
+
files: [
|
|
2797
|
+
"dist"
|
|
2798
|
+
],
|
|
2799
|
+
scripts: {
|
|
2800
|
+
build: "bun build.ts",
|
|
2801
|
+
dev: "PLAYCADEMY_BASE_URL=http://localhost:5174 bun src/index.ts",
|
|
2802
|
+
pub: "bun publish.ts"
|
|
2803
|
+
},
|
|
2804
|
+
dependencies: {
|
|
2805
|
+
"@inquirer/prompts": "^7.8.6",
|
|
2806
|
+
"@playcademy/sdk": "workspace:*",
|
|
2807
|
+
chokidar: "^4.0.3",
|
|
2808
|
+
colorette: "^2.0.20",
|
|
2809
|
+
commander: "^14.0.1",
|
|
2810
|
+
dedent: "catalog:",
|
|
2811
|
+
"drizzle-kit": "^0.31.5",
|
|
2812
|
+
"drizzle-orm": "^0.44.6",
|
|
2813
|
+
esbuild: "^0.25.10",
|
|
2814
|
+
giget: "^2.0.0",
|
|
2815
|
+
hono: "^4.9.9",
|
|
2816
|
+
"json-colorizer": "^3.0.1",
|
|
2817
|
+
jszip: "^3.10.1",
|
|
2818
|
+
miniflare: "^4.20251008.0",
|
|
2819
|
+
open: "^10.2.0"
|
|
2820
|
+
},
|
|
2821
|
+
devDependencies: {
|
|
2822
|
+
"@cloudflare/workers-types": "^4.20251011.0",
|
|
2823
|
+
"@playcademy/constants": "workspace:*",
|
|
2824
|
+
"@playcademy/data": "workspace:*",
|
|
2825
|
+
"@playcademy/edge-play": "workspace:*",
|
|
2826
|
+
"@playcademy/timeback": "workspace:*",
|
|
2827
|
+
"@playcademy/utils": "workspace:*",
|
|
2828
|
+
"@types/bun": "latest",
|
|
2829
|
+
bumpp: "^10.2.3",
|
|
2830
|
+
rollup: "^4.50.2",
|
|
2831
|
+
"rollup-plugin-dts": "^6.2.3"
|
|
2832
|
+
},
|
|
2833
|
+
peerDependencies: {
|
|
2834
|
+
typescript: "^5"
|
|
2835
|
+
}
|
|
2836
|
+
};
|
|
2837
|
+
|
|
2838
|
+
// src/version.ts
|
|
2839
|
+
var version = package_default2.version;
|
|
2840
|
+
|
|
2841
|
+
// src/lib/init/database.ts
|
|
2842
|
+
var drizzleConfigTemplate = loadTemplateString("database/drizzle-config.ts");
|
|
2843
|
+
var dbSchemaExampleTemplate = loadTemplateString("database/db-schema-example.ts");
|
|
2844
|
+
var dbSchemaIndexTemplate = loadTemplateString("database/db-schema-index.ts");
|
|
2845
|
+
var dbIndexTemplate = loadTemplateString("database/db-index.ts");
|
|
2846
|
+
var dbTypesTemplate = loadTemplateString("database/db-types.ts");
|
|
2847
|
+
var dbSeedTemplate = loadTemplateString("database/db-seed.ts");
|
|
2848
|
+
var packageTemplate = loadTemplateString("database/package.json");
|
|
2849
|
+
async function scaffoldDatabaseSetup(options) {
|
|
2850
|
+
const workspace = getWorkspace();
|
|
2851
|
+
let packagesUpdated = false;
|
|
2852
|
+
await runStep(
|
|
2853
|
+
"Configuring database...",
|
|
2854
|
+
async () => {
|
|
2855
|
+
const dbDir = join13(workspace, DEFAULT_DATABASE_DIRECTORY);
|
|
2856
|
+
const schemaDir = join13(dbDir, SCHEMA_SUBDIRECTORY);
|
|
2857
|
+
if (!existsSync11(dbDir)) {
|
|
2858
|
+
mkdirSync5(dbDir, { recursive: true });
|
|
2859
|
+
}
|
|
2860
|
+
if (!existsSync11(schemaDir)) {
|
|
2861
|
+
mkdirSync5(schemaDir, { recursive: true });
|
|
2862
|
+
}
|
|
2863
|
+
const exampleSchemaPath = join13(schemaDir, DB_FILES.EXAMPLE_SCHEMA);
|
|
2864
|
+
writeFileSync6(exampleSchemaPath, dbSchemaExampleTemplate);
|
|
2865
|
+
const schemaIndexPath = join13(schemaDir, DB_FILES.SCHEMA_INDEX);
|
|
2866
|
+
writeFileSync6(schemaIndexPath, dbSchemaIndexTemplate);
|
|
2867
|
+
const dbIndexPath = join13(dbDir, DB_FILES.INDEX);
|
|
2868
|
+
writeFileSync6(dbIndexPath, dbIndexTemplate);
|
|
2869
|
+
const dbTypesPath = join13(dbDir, DB_FILES.TYPES);
|
|
2870
|
+
writeFileSync6(dbTypesPath, dbTypesTemplate);
|
|
2871
|
+
const dbSeedPath = join13(dbDir, DB_FILES.SEED);
|
|
2872
|
+
writeFileSync6(dbSeedPath, dbSeedTemplate);
|
|
2873
|
+
const drizzleConfigPath = join13(workspace, DRIZZLE_CONFIG_FILES[0]);
|
|
2874
|
+
writeFileSync6(drizzleConfigPath, drizzleConfigTemplate);
|
|
2875
|
+
packagesUpdated = await setupPackageJson2(workspace, options.appName);
|
|
2876
|
+
},
|
|
2877
|
+
"Database configured"
|
|
2878
|
+
);
|
|
2879
|
+
return packagesUpdated;
|
|
2880
|
+
}
|
|
2881
|
+
async function setupPackageJson2(workspace, appName) {
|
|
2882
|
+
const pkgPath = join13(workspace, "package.json");
|
|
2883
|
+
const dbDeps = {
|
|
2884
|
+
"drizzle-orm": "^0.42.0",
|
|
2885
|
+
"better-sqlite3": "^12.0.0"
|
|
2886
|
+
};
|
|
2887
|
+
const dbDevDeps = {
|
|
2888
|
+
playcademy: `^${version}`,
|
|
2889
|
+
"drizzle-kit": "^0.30.0",
|
|
2890
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
2891
|
+
tsx: "^4.20.6"
|
|
2892
|
+
};
|
|
2893
|
+
const dbScripts = {
|
|
2894
|
+
"db:generate": "drizzle-kit generate",
|
|
2895
|
+
"db:migrate": "drizzle-kit migrate",
|
|
2896
|
+
"db:push": "drizzle-kit push",
|
|
2897
|
+
"db:studio": "drizzle-kit studio",
|
|
2898
|
+
"db:seed": "playcademy db seed",
|
|
2899
|
+
"db:reset": "playcademy db reset"
|
|
2900
|
+
};
|
|
2901
|
+
if (existsSync11(pkgPath)) {
|
|
2902
|
+
await runStep(
|
|
2903
|
+
"Updating package.json deps",
|
|
2904
|
+
async () => {
|
|
2905
|
+
const existing = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
2906
|
+
existing.dependencies = { ...existing.dependencies, ...dbDeps };
|
|
2907
|
+
existing.devDependencies = { ...existing.devDependencies, ...dbDevDeps };
|
|
2908
|
+
existing.scripts = { ...existing.scripts, ...dbScripts };
|
|
2909
|
+
writeFileSync6(pkgPath, JSON.stringify(existing, null, 2) + "\n");
|
|
2910
|
+
},
|
|
2911
|
+
"Updated package.json deps"
|
|
2912
|
+
);
|
|
2913
|
+
return true;
|
|
2914
|
+
} else {
|
|
2915
|
+
const slugifiedName = generateSlug(appName);
|
|
2916
|
+
const packageContent = packageTemplate.replace("{{APP_NAME}}", slugifiedName).replace("{{PLAYCADEMY_VERSION}}", version);
|
|
2917
|
+
writeFileSync6(pkgPath, packageContent);
|
|
2918
|
+
logger.success("Created package.json");
|
|
2919
|
+
return true;
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
function hasDatabaseSetup() {
|
|
2923
|
+
const workspace = getWorkspace();
|
|
2924
|
+
return DRIZZLE_CONFIG_FILES.some((filename) => existsSync11(join13(workspace, filename)));
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// src/lib/init/scaffold.ts
|
|
2928
|
+
var sampleCustomRouteTemplate = loadTemplateString("api/sample-custom.ts");
|
|
2929
|
+
var sampleDatabaseRouteTemplate = loadTemplateString("api/sample-database.ts");
|
|
2930
|
+
var sampleKvRouteTemplate = loadTemplateString("api/sample-kv.ts");
|
|
2931
|
+
var sampleBucketRouteTemplate = loadTemplateString("api/sample-bucket.ts");
|
|
2932
|
+
var playcademyGitignoreTemplate = loadTemplateString("playcademy-gitignore");
|
|
2933
|
+
async function scaffoldApiDirectory(apiDirectory, sampleRoutes) {
|
|
2934
|
+
const apiPath = resolve6(getWorkspace(), apiDirectory);
|
|
2935
|
+
const samplePath = join14(apiPath, "sample");
|
|
2936
|
+
if (!existsSync12(apiPath)) {
|
|
2937
|
+
mkdirSync6(apiPath, { recursive: true });
|
|
2938
|
+
}
|
|
2939
|
+
if (!existsSync12(samplePath)) {
|
|
2940
|
+
mkdirSync6(samplePath, { recursive: true });
|
|
2941
|
+
}
|
|
2942
|
+
for (const route of sampleRoutes) {
|
|
2943
|
+
writeFileSync7(join14(samplePath, route.filename), route.template, "utf-8");
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
function validateApiDirectoryDoesNotExist(value) {
|
|
2947
|
+
const dirPath = resolve6(getWorkspace(), value.trim());
|
|
2948
|
+
if (existsSync12(dirPath)) {
|
|
2949
|
+
return `Directory "${value.trim()}" already exists. Please choose a different name or remove the existing directory.`;
|
|
2950
|
+
}
|
|
2951
|
+
return true;
|
|
2952
|
+
}
|
|
2953
|
+
function ensurePlaycademyGitignore() {
|
|
2954
|
+
const workspace = getWorkspace();
|
|
2955
|
+
const playcademyDir = join14(workspace, CLI_DIRECTORIES.WORKSPACE);
|
|
2956
|
+
if (!existsSync12(playcademyDir)) {
|
|
2957
|
+
mkdirSync6(playcademyDir, { recursive: true });
|
|
2958
|
+
}
|
|
2959
|
+
const gitignorePath = join14(playcademyDir, ".gitignore");
|
|
2960
|
+
writeFileSync7(gitignorePath, playcademyGitignoreTemplate);
|
|
2961
|
+
}
|
|
2962
|
+
async function scaffoldIntegrations(appName, options) {
|
|
2963
|
+
const { customRoutes, database, auth, kv, bucket } = options;
|
|
2964
|
+
let depsAdded = false;
|
|
2965
|
+
ensurePlaycademyGitignore();
|
|
2966
|
+
if (customRoutes) {
|
|
2967
|
+
const sampleRoutes = [
|
|
2968
|
+
// Always include basic custom route example
|
|
2969
|
+
{ filename: SAMPLE_CUSTOM_FILENAME, template: sampleCustomRouteTemplate }
|
|
2970
|
+
];
|
|
2971
|
+
if (database) {
|
|
2972
|
+
sampleRoutes.push({
|
|
2973
|
+
filename: SAMPLE_DATABASE_FILENAME,
|
|
2974
|
+
template: sampleDatabaseRouteTemplate
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
if (kv) {
|
|
2978
|
+
sampleRoutes.push({ filename: SAMPLE_KV_FILENAME, template: sampleKvRouteTemplate });
|
|
2979
|
+
}
|
|
2980
|
+
if (bucket) {
|
|
2981
|
+
sampleRoutes.push({
|
|
2982
|
+
filename: SAMPLE_BUCKET_FILENAME,
|
|
2983
|
+
template: sampleBucketRouteTemplate
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
await scaffoldApiDirectory(customRoutes.directory, sampleRoutes);
|
|
2987
|
+
}
|
|
2988
|
+
if (database) {
|
|
2989
|
+
const dbDepsAdded = await scaffoldDatabaseSetup({ appName });
|
|
2990
|
+
if (dbDepsAdded) depsAdded = true;
|
|
2991
|
+
}
|
|
2992
|
+
if (auth) {
|
|
2993
|
+
const authDepsAdded = await scaffoldAuthSetup({ ...auth, appName });
|
|
2994
|
+
if (authDepsAdded) depsAdded = true;
|
|
2995
|
+
}
|
|
2996
|
+
return depsAdded;
|
|
2997
|
+
}
|
|
2998
|
+
function addPlaycademySdk() {
|
|
2999
|
+
const pkgPath = resolve6(getWorkspace(), "package.json");
|
|
3000
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
3001
|
+
const hasSdk = pkg.dependencies?.["@playcademy/sdk"] || pkg.devDependencies?.["@playcademy/sdk"];
|
|
3002
|
+
if (hasSdk) {
|
|
3003
|
+
return false;
|
|
3004
|
+
}
|
|
3005
|
+
if (!pkg.dependencies) pkg.dependencies = {};
|
|
3006
|
+
pkg.dependencies["@playcademy/sdk"] = "latest";
|
|
3007
|
+
writeFileSync7(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
3008
|
+
return true;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// src/lib/init/prompts.ts
|
|
3012
|
+
async function promptForProjectInfo() {
|
|
3013
|
+
const name = await input2({
|
|
3014
|
+
message: "App name:",
|
|
3015
|
+
validate: (value) => {
|
|
3016
|
+
if (!value || value.trim().length === 0) {
|
|
3017
|
+
return "App name is required";
|
|
3018
|
+
}
|
|
3019
|
+
return true;
|
|
3020
|
+
}
|
|
3021
|
+
});
|
|
3022
|
+
const description = await input2({
|
|
3023
|
+
message: "App description (optional):",
|
|
3024
|
+
default: ""
|
|
3025
|
+
});
|
|
3026
|
+
const emoji = await input2({
|
|
3027
|
+
message: "App emoji (optional):",
|
|
3028
|
+
default: "\u{1F3AE}"
|
|
3029
|
+
});
|
|
3030
|
+
return { name, description, emoji };
|
|
3031
|
+
}
|
|
3032
|
+
async function promptForTimeBackIntegration(options) {
|
|
3033
|
+
if (!options?.skipConfirm) {
|
|
3034
|
+
const wantsTimeback = await confirm3({
|
|
3035
|
+
message: "TimeBack?",
|
|
3036
|
+
default: false
|
|
3037
|
+
});
|
|
3038
|
+
if (!wantsTimeback) {
|
|
3039
|
+
return null;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
const subjects = await checkbox({
|
|
3043
|
+
message: "Select subjects:",
|
|
3044
|
+
choices: TIMEBACK_SUBJECTS.map((subject) => ({
|
|
3045
|
+
value: subject,
|
|
3046
|
+
name: subject
|
|
3047
|
+
})),
|
|
3048
|
+
instructions: ` ${bold4("(Press ")}${cyan4("<space>")}${bold4(" to select and ")}${cyan4("<enter>")}${bold4(" to proceed)")}`,
|
|
3049
|
+
validate: (selected) => {
|
|
3050
|
+
if (!selected || selected.length === 0) {
|
|
3051
|
+
return "At least one subject is required";
|
|
3052
|
+
}
|
|
3053
|
+
return true;
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
let primarySubject;
|
|
3057
|
+
if (subjects.length === 1) {
|
|
3058
|
+
primarySubject = subjects[0];
|
|
3059
|
+
} else {
|
|
3060
|
+
primarySubject = await select4({
|
|
3061
|
+
message: "Primary subject (for multi-grade courses):",
|
|
3062
|
+
choices: subjects.map((subject) => ({
|
|
3063
|
+
value: subject,
|
|
3064
|
+
name: subject
|
|
3065
|
+
}))
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
const grades = await checkbox({
|
|
3069
|
+
message: "Select grade levels:",
|
|
3070
|
+
choices: TIMEBACK_GRADE_LEVELS.map((grade) => ({
|
|
3071
|
+
value: grade,
|
|
3072
|
+
name: TIMEBACK_GRADE_LEVEL_LABELS[grade.toString()]
|
|
3073
|
+
})),
|
|
3074
|
+
instructions: ` ${bold4("(Press ")}${cyan4("<space>")}${bold4(" to select and ")}${cyan4("<enter>")}${bold4(" to proceed)")}`,
|
|
3075
|
+
validate: (selected) => {
|
|
3076
|
+
if (!selected || selected.length === 0) {
|
|
3077
|
+
return "At least one grade level is required";
|
|
3078
|
+
}
|
|
3079
|
+
return true;
|
|
3080
|
+
}
|
|
3081
|
+
});
|
|
3082
|
+
const courses = [];
|
|
3083
|
+
for (const grade of grades) {
|
|
3084
|
+
courses.push({
|
|
3085
|
+
subject: primarySubject,
|
|
3086
|
+
grade,
|
|
3087
|
+
totalXp: null,
|
|
3088
|
+
masterableUnits: null
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
return courses;
|
|
3092
|
+
}
|
|
3093
|
+
async function promptForDatabase() {
|
|
3094
|
+
const wantsDatabase = await confirm3({
|
|
3095
|
+
message: "Database?",
|
|
3096
|
+
default: false
|
|
3097
|
+
});
|
|
3098
|
+
if (!wantsDatabase) {
|
|
3099
|
+
return null;
|
|
3100
|
+
}
|
|
3101
|
+
const directory = await input2({
|
|
3102
|
+
message: "Database directory:",
|
|
3103
|
+
default: DEFAULT_DATABASE_DIRECTORY,
|
|
3104
|
+
validate: (value) => {
|
|
3105
|
+
if (!value || value.trim().length === 0) {
|
|
3106
|
+
return "Directory name is required";
|
|
3107
|
+
}
|
|
3108
|
+
return true;
|
|
3109
|
+
}
|
|
3110
|
+
});
|
|
3111
|
+
return { directory };
|
|
3112
|
+
}
|
|
3113
|
+
async function promptForAuthStrategies() {
|
|
3114
|
+
const strategies = await checkbox({
|
|
3115
|
+
message: "Strategies (optional):",
|
|
3116
|
+
choices: [
|
|
3117
|
+
{ value: "platform", name: "Platform", checked: true },
|
|
3118
|
+
{ value: "email", name: "Email/password" },
|
|
3119
|
+
{ value: "github", name: "GitHub OAuth" },
|
|
3120
|
+
{ value: "google", name: "Google OAuth" }
|
|
3121
|
+
],
|
|
3122
|
+
instructions: ` ${bold4("(Press ")}${cyan4("<space>")}${bold4(" to select and ")}${cyan4("<enter>")}${bold4(" to proceed)")}`,
|
|
3123
|
+
validate: (selected) => {
|
|
3124
|
+
if (!selected.some((s) => s.value === "platform")) {
|
|
3125
|
+
return "Platform auth is included by default (cannot be deselected)";
|
|
3126
|
+
}
|
|
3127
|
+
return true;
|
|
3128
|
+
}
|
|
3129
|
+
});
|
|
3130
|
+
return strategies.filter((s) => s !== "platform");
|
|
3131
|
+
}
|
|
3132
|
+
async function promptForAuth() {
|
|
3133
|
+
const wantsAuth = await confirm3({
|
|
3134
|
+
message: "Authentication?",
|
|
3135
|
+
default: false
|
|
3136
|
+
});
|
|
3137
|
+
if (!wantsAuth) {
|
|
3138
|
+
logger.newLine();
|
|
3139
|
+
return null;
|
|
3140
|
+
}
|
|
3141
|
+
const strategies = await promptForAuthStrategies();
|
|
3142
|
+
return { strategies };
|
|
3143
|
+
}
|
|
3144
|
+
async function promptForKV() {
|
|
3145
|
+
const wantsKV = await confirm3({
|
|
3146
|
+
message: "KV storage?",
|
|
3147
|
+
default: false
|
|
3148
|
+
});
|
|
3149
|
+
return wantsKV;
|
|
3150
|
+
}
|
|
3151
|
+
async function promptForBucket() {
|
|
3152
|
+
const wantsBucket = await confirm3({
|
|
3153
|
+
message: "Bucket storage?",
|
|
3154
|
+
default: false
|
|
3155
|
+
});
|
|
3156
|
+
return wantsBucket;
|
|
3157
|
+
}
|
|
3158
|
+
async function promptForCustomRoutes(requiresRoutes = false) {
|
|
3159
|
+
let wantsCustomRoutes = requiresRoutes;
|
|
3160
|
+
if (!requiresRoutes) {
|
|
3161
|
+
wantsCustomRoutes = await confirm3({
|
|
3162
|
+
message: "Custom API routes?",
|
|
3163
|
+
default: false
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
if (!wantsCustomRoutes) {
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
const directory = await input2({
|
|
3170
|
+
message: "API routes directory:",
|
|
3171
|
+
default: DEFAULT_API_ROUTES_DIRECTORY,
|
|
3172
|
+
validate: (value) => {
|
|
3173
|
+
if (!value || value.trim().length === 0) {
|
|
3174
|
+
return "Directory name is required";
|
|
3175
|
+
}
|
|
3176
|
+
return validateApiDirectoryDoesNotExist(value);
|
|
3177
|
+
}
|
|
3178
|
+
});
|
|
3179
|
+
return { directory };
|
|
3180
|
+
}
|
|
3181
|
+
async function promptForIntegrations() {
|
|
3182
|
+
const timeback = await promptForTimeBackIntegration();
|
|
3183
|
+
logger.newLine();
|
|
3184
|
+
const database = await promptForDatabase();
|
|
3185
|
+
logger.newLine();
|
|
3186
|
+
const auth = database ? await promptForAuth() : null;
|
|
3187
|
+
if (auth) logger.newLine();
|
|
3188
|
+
const kv = await promptForKV();
|
|
3189
|
+
logger.newLine();
|
|
3190
|
+
const bucket = await promptForBucket();
|
|
3191
|
+
logger.newLine();
|
|
3192
|
+
const customRoutes = await promptForCustomRoutes(!!database || !!auth || kv || bucket);
|
|
3193
|
+
return {
|
|
3194
|
+
timeback,
|
|
3195
|
+
database,
|
|
3196
|
+
auth,
|
|
3197
|
+
kv,
|
|
3198
|
+
bucket,
|
|
3199
|
+
customRoutes
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
async function selectConfigFormat(hasPackageJson2) {
|
|
3203
|
+
if (hasPackageJson2) {
|
|
3204
|
+
return "js";
|
|
3205
|
+
}
|
|
3206
|
+
const formatChoice = await select4({
|
|
3207
|
+
message: "Which config format would you like to use?",
|
|
3208
|
+
choices: [
|
|
3209
|
+
{ value: "json", name: "JSON (playcademy.config.json)" },
|
|
3210
|
+
{ value: "js", name: "JavaScript (playcademy.config.js)" }
|
|
3211
|
+
],
|
|
3212
|
+
default: "json"
|
|
3213
|
+
});
|
|
3214
|
+
return formatChoice;
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
// src/lib/init/types.ts
|
|
3218
|
+
init_file_loader();
|
|
3219
|
+
import { execSync as execSync4 } from "child_process";
|
|
3220
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
3221
|
+
import { dirname as dirname5, join as join17 } from "path";
|
|
3222
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3223
|
+
|
|
3224
|
+
// src/lib/deploy/backend.ts
|
|
3225
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
3226
|
+
import { join as join15 } from "node:path";
|
|
3227
|
+
function getCustomRoutesDirectory(projectPath, config) {
|
|
3228
|
+
const customRoutes = config?.integrations?.customRoutes;
|
|
3229
|
+
const customRoutesDir = typeof customRoutes === "object" && customRoutes.directory || DEFAULT_API_ROUTES_DIRECTORY;
|
|
3230
|
+
return join15(projectPath, customRoutesDir);
|
|
3231
|
+
}
|
|
3232
|
+
function hasLocalCustomRoutes(projectPath, config) {
|
|
3233
|
+
const customRoutesDir = getCustomRoutesDirectory(projectPath, config);
|
|
3234
|
+
return existsSync13(customRoutesDir);
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
// src/lib/init/bucket.ts
|
|
3238
|
+
function hasBucketSetup(config) {
|
|
3239
|
+
return !!config.integrations?.bucket;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// src/lib/init/kv.ts
|
|
3243
|
+
function hasKVSetup(config) {
|
|
3244
|
+
return !!config.integrations?.kv;
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
// src/lib/init/tsconfig.ts
|
|
3248
|
+
init_file_loader();
|
|
3249
|
+
import { existsSync as existsSync14, readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
3250
|
+
import { join as join16 } from "path";
|
|
3251
|
+
function hasAllRequiredIncludes(config) {
|
|
3252
|
+
if (!config.include) return false;
|
|
3253
|
+
return TSCONFIG_REQUIRED_INCLUDES.every((entry) => config.include.includes(entry));
|
|
3254
|
+
}
|
|
3255
|
+
function getMissingIncludes(config) {
|
|
3256
|
+
if (!config.include) return [...TSCONFIG_REQUIRED_INCLUDES];
|
|
3257
|
+
return TSCONFIG_REQUIRED_INCLUDES.filter((entry) => !config.include.includes(entry));
|
|
3258
|
+
}
|
|
3259
|
+
function addRequiredIncludes(config) {
|
|
3260
|
+
if (!config.include) {
|
|
3261
|
+
config.include = [];
|
|
3262
|
+
}
|
|
3263
|
+
for (const entry of getMissingIncludes(config)) {
|
|
3264
|
+
config.include.push(entry);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
function updateExistingIncludeArray(content) {
|
|
3268
|
+
const includeRegex = /"include"\s*:\s*\[([^\]]*)\]/;
|
|
3269
|
+
const match = content.match(includeRegex);
|
|
3270
|
+
if (!match || !match[1]) {
|
|
3271
|
+
return null;
|
|
3272
|
+
}
|
|
3273
|
+
const fullMatch = match[0];
|
|
3274
|
+
const arrayContents = match[1];
|
|
3275
|
+
const missingEntries = TSCONFIG_REQUIRED_INCLUDES.filter(
|
|
3276
|
+
(entry) => !arrayContents.includes(entry)
|
|
3277
|
+
);
|
|
3278
|
+
if (missingEntries.length === 0) {
|
|
3279
|
+
return content;
|
|
3280
|
+
}
|
|
3281
|
+
const trimmedContents = arrayContents.trim();
|
|
3282
|
+
const newEntries = missingEntries.map((entry) => `"${entry}"`);
|
|
3283
|
+
let newArrayContents;
|
|
3284
|
+
if (trimmedContents === "") {
|
|
3285
|
+
newArrayContents = newEntries.join(", ");
|
|
3286
|
+
} else if (arrayContents.includes("\n")) {
|
|
3287
|
+
const entriesStr = newEntries.map((e) => `
|
|
3288
|
+
${e}`).join(",");
|
|
3289
|
+
newArrayContents = `${arrayContents},${entriesStr}
|
|
3290
|
+
`;
|
|
3291
|
+
} else {
|
|
3292
|
+
newArrayContents = `${trimmedContents}, ${newEntries.join(", ")}`;
|
|
3293
|
+
}
|
|
3294
|
+
const newIncludeArray = `"include": [${newArrayContents}]`;
|
|
3295
|
+
return content.replace(fullMatch, newIncludeArray);
|
|
3296
|
+
}
|
|
3297
|
+
function addNewIncludeProperty(content) {
|
|
3298
|
+
const closingBraceMatch = content.match(/\n(\s*)\}(\s*)$/);
|
|
3299
|
+
if (!closingBraceMatch || closingBraceMatch.index === void 0) {
|
|
3300
|
+
return null;
|
|
3301
|
+
}
|
|
3302
|
+
const propertyMatch = content.match(/\n(\s+)"/);
|
|
3303
|
+
const propertyIndent = propertyMatch ? propertyMatch[1] : " ";
|
|
3304
|
+
const insertPosition = closingBraceMatch.index;
|
|
3305
|
+
const beforeClosing = content.slice(0, insertPosition).trim();
|
|
3306
|
+
const needsComma = beforeClosing.endsWith("]") || beforeClosing.endsWith("}") || beforeClosing.endsWith('"');
|
|
3307
|
+
const entriesStr = TSCONFIG_REQUIRED_INCLUDES.map((e) => `"${e}"`).join(", ");
|
|
3308
|
+
const comma = needsComma ? "," : "";
|
|
3309
|
+
const includeEntry = `${comma}
|
|
3310
|
+
${propertyIndent}"include": [${entriesStr}]`;
|
|
3311
|
+
const updatedContent = content.slice(0, insertPosition) + includeEntry + content.slice(insertPosition);
|
|
3312
|
+
return updatedContent;
|
|
3313
|
+
}
|
|
3314
|
+
function addToIncludeArrayPreservingComments(content) {
|
|
3315
|
+
return updateExistingIncludeArray(content) || addNewIncludeProperty(content);
|
|
3316
|
+
}
|
|
3317
|
+
async function ensureTsconfigIncludes(workspace) {
|
|
3318
|
+
for (const filename of TSCONFIG_FILES) {
|
|
3319
|
+
const configPath = join16(workspace, filename);
|
|
3320
|
+
if (!existsSync14(configPath)) {
|
|
3321
|
+
continue;
|
|
3322
|
+
}
|
|
3323
|
+
try {
|
|
3324
|
+
const config = await loadFile(configPath, {
|
|
3325
|
+
parseJson: true,
|
|
3326
|
+
stripComments: true
|
|
3327
|
+
});
|
|
3328
|
+
if (!config) continue;
|
|
3329
|
+
if (config.references && filename !== TSCONFIG_JSON) {
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
if (hasAllRequiredIncludes(config)) {
|
|
3333
|
+
return filename;
|
|
3334
|
+
}
|
|
3335
|
+
try {
|
|
3336
|
+
const rawContent = readFileSync8(configPath, "utf-8");
|
|
3337
|
+
const updatedContent = addToIncludeArrayPreservingComments(rawContent);
|
|
3338
|
+
if (updatedContent && updatedContent !== rawContent) {
|
|
3339
|
+
writeFileSync8(configPath, updatedContent);
|
|
3340
|
+
return filename;
|
|
3341
|
+
}
|
|
3342
|
+
} catch {
|
|
3343
|
+
}
|
|
3344
|
+
addRequiredIncludes(config);
|
|
3345
|
+
writeFileSync8(configPath, JSON.stringify(config, null, 4) + "\n");
|
|
3346
|
+
return filename;
|
|
3347
|
+
} catch {
|
|
3348
|
+
continue;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
return null;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
// src/lib/init/types.ts
|
|
3355
|
+
var playcademyEnvTemplate = loadTemplateString("playcademy-env.d.ts");
|
|
3356
|
+
function detectBackendFeatures(workspace, config) {
|
|
3357
|
+
return {
|
|
3358
|
+
hasDB: hasDatabaseSetup(),
|
|
3359
|
+
hasKV: hasKVSetup(config),
|
|
3360
|
+
hasBucket: hasBucketSetup(config),
|
|
3361
|
+
hasRoutes: hasLocalCustomRoutes(workspace, config),
|
|
3362
|
+
hasSecrets: hasEnvFile(workspace)
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
function hasAnyBackend(features) {
|
|
3366
|
+
return Object.values(features).some(Boolean);
|
|
3367
|
+
}
|
|
3368
|
+
async function setupPlaycademyDependencies(workspace) {
|
|
3369
|
+
const playcademyDir = join17(workspace, CLI_DIRECTORIES.WORKSPACE);
|
|
3370
|
+
const playcademyPkgPath = join17(playcademyDir, "package.json");
|
|
3371
|
+
const __dirname = dirname5(fileURLToPath2(import.meta.url));
|
|
3372
|
+
const cliPkg = await loadPackageJson({ cwd: __dirname, searchUp: true, required: true });
|
|
3373
|
+
const workersTypesVersion = cliPkg?.devDependencies?.["@cloudflare/workers-types"] || "latest";
|
|
3374
|
+
const honoVersion = cliPkg?.dependencies?.hono || "latest";
|
|
3375
|
+
const playcademyPkg = {
|
|
3376
|
+
private: true,
|
|
3377
|
+
dependencies: { hono: honoVersion },
|
|
3378
|
+
devDependencies: { "@cloudflare/workers-types": workersTypesVersion }
|
|
3379
|
+
};
|
|
3380
|
+
writeFileSync9(playcademyPkgPath, JSON.stringify(playcademyPkg, null, 4) + "\n");
|
|
3381
|
+
const pm = detectPackageManager(workspace);
|
|
3382
|
+
const installCmd = getInstallCommand(pm);
|
|
3383
|
+
execSync4(installCmd, {
|
|
3384
|
+
cwd: playcademyDir,
|
|
3385
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
function generateBindingsTypeString(features) {
|
|
3389
|
+
const bindings = [];
|
|
3390
|
+
if (features.hasKV) bindings.push(" KV: KVNamespace");
|
|
3391
|
+
if (features.hasDB) bindings.push(" DB: D1Database");
|
|
3392
|
+
if (features.hasBucket) bindings.push(" BUCKET: R2Bucket");
|
|
3393
|
+
return bindings.length > 0 ? "\n" + bindings.join("\n") : "";
|
|
3394
|
+
}
|
|
3395
|
+
function generateAuthVariablesString(hasAuth, hasAuthFile) {
|
|
3396
|
+
const authImport = hasAuthFile ? "\nimport type { UserInfo } from './node_modules/@playcademy/sdk/dist/types.d.ts'\nimport type { User } from './server/lib/auth'" : "\nimport type { UserInfo } from './node_modules/@playcademy/sdk/dist/types.d.ts'";
|
|
3397
|
+
const variableFields = [" playcademyUser?: UserInfo"];
|
|
3398
|
+
if (hasAuth && hasAuthFile) {
|
|
3399
|
+
variableFields.push(" user?: User");
|
|
3400
|
+
}
|
|
3401
|
+
return {
|
|
3402
|
+
authImport,
|
|
3403
|
+
variables: `
|
|
3404
|
+
|
|
3405
|
+
interface PlaycademyVariables {
|
|
3406
|
+
${variableFields.join("\n")}
|
|
3407
|
+
}`,
|
|
3408
|
+
contextVars: "; Variables: PlaycademyVariables"
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
async function generateSecretsTypeString(workspace, verbose = false) {
|
|
3412
|
+
try {
|
|
3413
|
+
const envSecrets = await readEnvFile(workspace);
|
|
3414
|
+
const secretKeys = Object.keys(envSecrets);
|
|
3415
|
+
if (secretKeys.length === 0) {
|
|
3416
|
+
if (verbose) logger.success("No secrets in <.env> files");
|
|
3417
|
+
return "\n secrets: Record<string, string>";
|
|
3418
|
+
}
|
|
3419
|
+
if (verbose) {
|
|
3420
|
+
const loadedFiles = getLoadedEnvFiles(workspace);
|
|
3421
|
+
const fileList = loadedFiles.map((f) => `<${f}>`).join(", ");
|
|
3422
|
+
logger.success(
|
|
3423
|
+
`Loaded ${secretKeys.length} ${pluralize(secretKeys.length, "secret")} from ${fileList}`
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
const secretTypes = secretKeys.map((key) => ` ${key}: string`).join("\n");
|
|
3427
|
+
return `
|
|
3428
|
+
secrets: {
|
|
3429
|
+
${secretTypes}
|
|
3430
|
+
[key: string]: string
|
|
3431
|
+
}`;
|
|
3432
|
+
} catch {
|
|
3433
|
+
return "\n secrets: Record<string, string>";
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
async function ensurePlaycademyTypes(options = {}) {
|
|
3437
|
+
const { verbose = false } = options;
|
|
3438
|
+
try {
|
|
3439
|
+
const workspace = getWorkspace();
|
|
3440
|
+
const config = await loadConfig();
|
|
3441
|
+
const features = detectBackendFeatures(workspace, config);
|
|
3442
|
+
if (!hasAnyBackend(features)) {
|
|
3443
|
+
if (verbose) {
|
|
3444
|
+
logger.dim("No backend functionality detected");
|
|
3445
|
+
}
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
await setupPlaycademyDependencies(workspace);
|
|
3449
|
+
if (verbose) {
|
|
3450
|
+
logger.success(`Installed packages in <${CLI_DIRECTORIES.WORKSPACE}>`);
|
|
3451
|
+
}
|
|
3452
|
+
const bindingsStr = generateBindingsTypeString(features);
|
|
3453
|
+
const secretsStr = await generateSecretsTypeString(workspace, verbose);
|
|
3454
|
+
const hasAuth = !!config.integrations?.auth;
|
|
3455
|
+
const hasAuthFile = existsSync15(join17(workspace, "server/lib/auth.ts"));
|
|
3456
|
+
const authVariablesString = generateAuthVariablesString(hasAuth, hasAuthFile);
|
|
3457
|
+
let envContent = playcademyEnvTemplate.replace("{{BINDINGS}}", bindingsStr);
|
|
3458
|
+
envContent = envContent.replace("{{SECRETS}}", secretsStr);
|
|
3459
|
+
envContent = envContent.replace("{{AUTH_IMPORT}}", authVariablesString.authImport);
|
|
3460
|
+
envContent = envContent.replace("{{VARIABLES}}", authVariablesString.variables);
|
|
3461
|
+
envContent = envContent.replace("{{CONTEXT_VARS}}", authVariablesString.contextVars);
|
|
3462
|
+
const envPath = join17(workspace, "playcademy-env.d.ts");
|
|
3463
|
+
writeFileSync9(envPath, envContent);
|
|
3464
|
+
if (verbose) {
|
|
3465
|
+
logger.success(`Generated <playcademy-env.d.ts>`);
|
|
3466
|
+
}
|
|
3467
|
+
const tsConfigFilename = await ensureTsconfigIncludes(workspace);
|
|
3468
|
+
if (verbose) {
|
|
3469
|
+
logger.success(`Updated <${tsConfigFilename}>`);
|
|
3470
|
+
}
|
|
3471
|
+
} catch (error) {
|
|
3472
|
+
logger.warn(
|
|
3473
|
+
`Failed to generate TypeScript types: ${error instanceof Error ? error.message : String(error)}`
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
// src/lib/init/run.ts
|
|
3479
|
+
var motd = `
|
|
3480
|
+
_______ ___ _______ ___ ___
|
|
3481
|
+
| _ | | | | _ | | Y |
|
|
3482
|
+
|. 1 | |. | |. 3 | | 4 |
|
|
3483
|
+
|. ____| |. |___ |. _ | \\_ _/
|
|
3484
|
+
|: | |: 2 | |: | | |: |
|
|
3485
|
+
|::.| |::.. . | |::.|:. | |::.|
|
|
3486
|
+
\`---' \`-------' \`--- ---' \`---'
|
|
3487
|
+
_______ _______ ______ _______ ___ ___ ___ ___
|
|
3488
|
+
| _ | | _ | | _ \\ | _ | | Y | | Y |
|
|
3489
|
+
|. 5___| |. 7 | |. | \\ |. 9___| |. 1 | | 2 |
|
|
3490
|
+
|. |___ |. _ | |. | \\ |. __)_ |. \\_/ | \\_ _/
|
|
3491
|
+
|: 6 | |: | | |: 8 / |: 0 | |: | | |: |
|
|
3492
|
+
|::.. . | |::.|:. | |::.. . / |::.. . | |::.|:. | |::.|
|
|
3493
|
+
\`-------' \`--- ---' \`------' \`-------' \`--- ---' \`---'
|
|
3494
|
+
|
|
3495
|
+
`;
|
|
3496
|
+
async function runInit(options = {}) {
|
|
3497
|
+
logger.raw(motd);
|
|
3498
|
+
try {
|
|
3499
|
+
const dirInfo = await promptForProjectDirectory(options.projectDir);
|
|
3500
|
+
if (dirInfo.cancelled) {
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
const engineSelection = await detectExistingEngine() ? null : await selectEngineTemplate();
|
|
3504
|
+
const existingConfig = await findFile(CONFIG_FILE_NAMES, {
|
|
3505
|
+
searchUp: false,
|
|
3506
|
+
maxLevels: 0
|
|
3507
|
+
});
|
|
3508
|
+
if (existingConfig && !options.force) {
|
|
3509
|
+
const relativePath = `./${existingConfig.path.split("/").pop()}`;
|
|
3510
|
+
logger.admonition("warning", "Already Initialized", [
|
|
3511
|
+
`Configuration file already exists: ${relativePath}`
|
|
3512
|
+
]);
|
|
3513
|
+
logger.newLine();
|
|
3514
|
+
const shouldOverwrite = await confirm4({
|
|
3515
|
+
message: "Do you want to overwrite it?",
|
|
3516
|
+
default: false
|
|
3517
|
+
});
|
|
3518
|
+
if (!shouldOverwrite) {
|
|
3519
|
+
logger.newLine();
|
|
3520
|
+
logger.remark("Nothing to do");
|
|
3521
|
+
logger.newLine();
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
let configFormat;
|
|
3526
|
+
if (engineSelection?.engine === "godot") {
|
|
3527
|
+
configFormat = "json";
|
|
3528
|
+
} else if (engineSelection?.engine === "vite") {
|
|
3529
|
+
configFormat = "js";
|
|
3530
|
+
} else {
|
|
3531
|
+
configFormat = await selectConfigFormat(hasPackageJson());
|
|
3532
|
+
}
|
|
3533
|
+
const configFileName = configFormat === "json" ? "playcademy.config.json" : "playcademy.config.js";
|
|
3534
|
+
logger.newLine();
|
|
3535
|
+
logger.highlight("Project Information");
|
|
3536
|
+
logger.newLine();
|
|
3537
|
+
const projectInfo = await promptForProjectInfo();
|
|
3538
|
+
logger.newLine();
|
|
3539
|
+
logger.highlight("Integrations");
|
|
3540
|
+
logger.newLine();
|
|
3541
|
+
const {
|
|
3542
|
+
timeback: timebackConfig,
|
|
3543
|
+
database,
|
|
3544
|
+
auth,
|
|
3545
|
+
kv,
|
|
3546
|
+
bucket,
|
|
3547
|
+
customRoutes
|
|
3548
|
+
} = await promptForIntegrations();
|
|
3549
|
+
logger.newLine();
|
|
3550
|
+
await runStep(
|
|
3551
|
+
"Setting up project",
|
|
3552
|
+
async () => {
|
|
3553
|
+
createProjectDirectory(dirInfo);
|
|
3554
|
+
if (engineSelection) {
|
|
3555
|
+
await applyEngineSelection(engineSelection, getWorkspace());
|
|
3556
|
+
}
|
|
3557
|
+
let depsAdded = false;
|
|
3558
|
+
const hasPackageJsonFile = hasPackageJson();
|
|
3559
|
+
if (hasPackageJsonFile) {
|
|
3560
|
+
ensureRootGitignore();
|
|
3561
|
+
}
|
|
3562
|
+
if (hasPackageJsonFile) {
|
|
3563
|
+
const sdkAdded = addPlaycademySdk();
|
|
3564
|
+
if (sdkAdded) depsAdded = true;
|
|
3565
|
+
}
|
|
3566
|
+
if (engineSelection?.engine === "vite") {
|
|
3567
|
+
const viteConfigPath = await findViteConfig();
|
|
3568
|
+
if (viteConfigPath && !isPluginConfigured(viteConfigPath)) {
|
|
3569
|
+
await updateViteConfig(viteConfigPath);
|
|
3570
|
+
const vitePluginAdded = await addVitePlugin();
|
|
3571
|
+
if (vitePluginAdded) depsAdded = true;
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
if (customRoutes || database || auth) {
|
|
3575
|
+
const scaffoldOptions = { customRoutes, database, auth, kv, bucket };
|
|
3576
|
+
const scaffoldDepsAdded = await scaffoldIntegrations(
|
|
3577
|
+
projectInfo.name,
|
|
3578
|
+
scaffoldOptions
|
|
3579
|
+
);
|
|
3580
|
+
if (scaffoldDepsAdded) depsAdded = true;
|
|
3581
|
+
}
|
|
3582
|
+
if (depsAdded) {
|
|
3583
|
+
const pm = detectPackageManager(getWorkspace());
|
|
3584
|
+
const installCmd = getInstallCommand(pm);
|
|
3585
|
+
execSync5(installCmd, {
|
|
3586
|
+
cwd: getWorkspace(),
|
|
3587
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
},
|
|
3591
|
+
"Project configured"
|
|
3592
|
+
);
|
|
3593
|
+
const configContent = configFormat === "js" ? generateJsConfig({
|
|
3594
|
+
name: projectInfo.name,
|
|
3595
|
+
description: projectInfo.description,
|
|
3596
|
+
emoji: projectInfo.emoji,
|
|
3597
|
+
customRoutesDirectory: customRoutes?.directory ?? void 0,
|
|
3598
|
+
databaseDirectory: database?.directory ?? void 0,
|
|
3599
|
+
auth: !!auth,
|
|
3600
|
+
kv: kv ?? void 0,
|
|
3601
|
+
bucket: bucket ?? void 0,
|
|
3602
|
+
timebackCourses: timebackConfig ?? void 0
|
|
3603
|
+
}) : generateJsonConfig({
|
|
3604
|
+
name: projectInfo.name,
|
|
3605
|
+
description: projectInfo.description,
|
|
3606
|
+
emoji: projectInfo.emoji,
|
|
3607
|
+
customRoutesDirectory: customRoutes?.directory ?? void 0,
|
|
3608
|
+
databaseDirectory: database?.directory ?? void 0,
|
|
3609
|
+
auth: !!auth,
|
|
3610
|
+
kv: kv ?? void 0,
|
|
3611
|
+
bucket: bucket ?? void 0,
|
|
3612
|
+
timebackCourses: timebackConfig ?? void 0
|
|
3613
|
+
});
|
|
3614
|
+
const configPath = resolve7(getWorkspace(), configFileName);
|
|
3615
|
+
writeFileSync10(configPath, configContent, "utf-8");
|
|
3616
|
+
await formatConfigWithPrettier(configPath);
|
|
3617
|
+
if (database || auth || kv || bucket) {
|
|
3618
|
+
await ensurePlaycademyTypes();
|
|
3619
|
+
logger.newLine();
|
|
3620
|
+
}
|
|
3621
|
+
displaySuccessMessage({
|
|
3622
|
+
configFileName,
|
|
3623
|
+
apiDirectory: customRoutes?.directory ?? null,
|
|
3624
|
+
hasDatabase: !!database,
|
|
3625
|
+
hasAuth: !!auth,
|
|
3626
|
+
timebackConfig,
|
|
3627
|
+
engineType: engineSelection?.engine ?? null
|
|
3628
|
+
});
|
|
3629
|
+
} catch (error) {
|
|
3630
|
+
logger.newLine();
|
|
3631
|
+
throw error;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
export {
|
|
3635
|
+
runInit,
|
|
3636
|
+
setupGlobalErrorHandlers
|
|
3637
|
+
};
|