sandstone-cli 2.0.8 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,664 +0,0 @@
1
- import path from 'node:path';
2
- import os from 'node:os';
3
- import crypto from 'node:crypto';
4
- import { pathToFileURL } from 'node:url';
5
- import fs from 'fs-extra';
6
- import chalk from 'chalk';
7
- import AdmZip from 'adm-zip';
8
- import { log, logInfo, logWarn, logError as logErrorFn, logDebug, logTrace, initLoggerNoFile, setSilent } from '../ui/logger.js';
9
- import { canUseSymlinks } from '../utils.js';
10
- import { split } from 'obliterator';
11
- // Console capture for watch mode - wraps console to redirect output to our log file
12
- const originalConsole = globalThis.console;
13
- let consoleWrapped = false;
14
- export function enableConsoleCapture() {
15
- if (consoleWrapped)
16
- return;
17
- consoleWrapped = true;
18
- globalThis.console.log = (...args) => log(...args);
19
- globalThis.console.info = (...args) => logInfo(...args);
20
- globalThis.console.warn = (...args) => logWarn(...args);
21
- globalThis.console.error = (...args) => logErrorFn(args.join(' '));
22
- globalThis.console.debug = (...args) => logDebug(...args);
23
- globalThis.console.trace = (...args) => {
24
- const traceObj = { stack: '' };
25
- Error.captureStackTrace(traceObj, globalThis.console.trace);
26
- const cleanedStack = traceObj.stack
27
- .replace(/^Error\n/, '') // Remove "Error" header line
28
- .replace(/\?hot-hook=\d+/g, '')
29
- .replace(/file:\/\/\/?/g, '');
30
- logTrace(...args, '\n' + cleanedStack);
31
- };
32
- }
33
- export function disableConsoleCapture() {
34
- if (!consoleWrapped)
35
- return;
36
- consoleWrapped = false;
37
- // Restore original methods
38
- const methodsToRestore = ['log', 'info', 'warn', 'error', 'debug', 'trace'];
39
- for (const method of methodsToRestore) {
40
- ;
41
- globalThis.console[method] = originalConsole[method].bind(originalConsole);
42
- }
43
- }
44
- function hash(stringToHash) {
45
- return crypto.createHash('md5').update(stringToHash).digest('hex');
46
- }
47
- let cache;
48
- let symlinksAvailable;
49
- async function getClientPath() {
50
- function getMCPath() {
51
- switch (os.platform()) {
52
- case 'win32':
53
- return path.join(os.homedir(), 'AppData/Roaming/.minecraft');
54
- case 'darwin':
55
- return path.join(os.homedir(), 'Library/Application Support/minecraft');
56
- case 'linux':
57
- default:
58
- return path.join(os.homedir(), '.minecraft');
59
- }
60
- }
61
- const mcPath = getMCPath();
62
- try {
63
- await fs.stat(mcPath);
64
- }
65
- catch {
66
- log('Unable to locate the .minecraft folder. Will not be able to export to client.');
67
- return undefined;
68
- }
69
- return mcPath;
70
- }
71
- async function getClientWorldPath(worldName, minecraftPath) {
72
- const mcPath = minecraftPath ?? (await getClientPath());
73
- const savesPath = path.join(mcPath, 'saves');
74
- const worldPath = path.join(savesPath, worldName);
75
- if (!fs.existsSync(worldPath)) {
76
- const existingWorlds = (await fs.readdir(savesPath, { withFileTypes: true }))
77
- .filter((f) => f.isDirectory())
78
- .map((f) => f.name);
79
- throw new Error(`Unable to locate the "${worldPath}" folder. World ${worldName} does not exist. List of existing worlds: ${JSON.stringify(existingWorlds, null, 2)}`);
80
- }
81
- return worldPath;
82
- }
83
- // Boilerplate resources to exclude from counts
84
- const BOILERPLATE_NAMESPACES = new Set(['load', '__sandstone__']);
85
- const BOILERPLATE_FUNCTIONS = new Set(['__init__']);
86
- const BOILERPLATE_TAG = { namespace: 'minecraft', name: 'load' };
87
- function isBoilerplateResource(resource) {
88
- const ns = resource.namespace || '';
89
- const pathParts = resource.path || [];
90
- const name = pathParts[pathParts.length - 1] || '';
91
- // Exclude load namespace and __sandstone__ namespace
92
- if (BOILERPLATE_NAMESPACES.has(ns))
93
- return true;
94
- // Exclude __init__ functions
95
- if (BOILERPLATE_FUNCTIONS.has(name))
96
- return true;
97
- if (ns === BOILERPLATE_TAG.namespace && name === BOILERPLATE_TAG.name)
98
- return true;
99
- return false;
100
- }
101
- function countResources(sandstonePack) {
102
- let functions = 0;
103
- let other = 0;
104
- for (const node of sandstonePack.core.resourceNodes) {
105
- const resource = node.resource;
106
- // Skip boilerplate resources
107
- if (isBoilerplateResource(resource))
108
- continue;
109
- // Check if it's a function (MCFunctionClass)
110
- if (resource.constructor?.name === '_RawMCFunctionClass') {
111
- functions++;
112
- }
113
- else {
114
- other++;
115
- }
116
- }
117
- return { functions, other };
118
- }
119
- function splitPath(split) {
120
- let parts = [];
121
- let current = split;
122
- while (current !== 'C:') {
123
- let dirname = path.dirname(current);
124
- if (dirname.endsWith('\\')) {
125
- dirname = dirname.slice(0, -1);
126
- }
127
- parts.unshift(current.replace(`${dirname}${path.sep}`, ''));
128
- current = dirname;
129
- }
130
- parts.unshift('C:');
131
- return parts;
132
- }
133
- async function handleSymlink(folder, packName, cache, minecraftPath, targetPath, linkPath) {
134
- let rawPath = path.resolve(path.join(folder));
135
- let sep = path.sep;
136
- if (os.platform() === 'win32') {
137
- sep = `${path.sep}${path.sep}`;
138
- rawPath = splitPath(rawPath).join(sep);
139
- }
140
- const allowPath = `[glob]${rawPath}${sep}**${sep}*`;
141
- const allowedList = path.join(minecraftPath, 'allowed_symlinks.txt');
142
- const comment = `# Sandstone Pack: ${packName}\n`;
143
- try {
144
- const currentlyAllowed = await fs.readFile(allowedList, 'utf-8');
145
- if (!currentlyAllowed.includes(allowPath)) {
146
- log('[handleSymlink] Adding workspace to allowed_symlinks.txt at minecraft path. If the game is running please restart it.');
147
- await fs.writeFile(allowedList, `${currentlyAllowed}\n#\n${comment}${allowPath}`);
148
- }
149
- else {
150
- log('[handleSymlink] Workspace is already in allowed_symlinks.txt at minecraft path, skipping...');
151
- }
152
- }
153
- catch (e) {
154
- log('[handleSymlink] Creating allowed_symlinks.txt at minecraft path. If the game is running please restart it.');
155
- await fs.writeFile(allowedList, `${comment}${allowPath}`);
156
- }
157
- let skip = false;
158
- let errored = false;
159
- try {
160
- const stats = await fs.lstat(linkPath);
161
- if (stats.isSymbolicLink() && await fs.readlink(linkPath) === path.resolve(targetPath)) {
162
- log('[handleSymlink] Symlink already created, skipping...');
163
- skip = true;
164
- }
165
- else {
166
- errored = true;
167
- }
168
- }
169
- catch { }
170
- if (errored) {
171
- throw new Error(`Tried to add a symlink at "${linkPath}",\n encountered an existing FS entry.`);
172
- }
173
- if (!skip) {
174
- log(`[handleSymlink] Creating symlink for ${targetPath.replace(`${path.dirname(targetPath)}${path.sep}`, '')}`);
175
- await fs.symlink(path.resolve(targetPath), linkPath);
176
- }
177
- cache.symlinks ??= [];
178
- cache.symlinks.push(linkPath);
179
- }
180
- export async function loadBuildContext(cliOptions, folder) {
181
- // Load sandstone.config.ts
182
- const configPath = path.join(folder, 'sandstone.config.ts');
183
- const configUrl = pathToFileURL(configPath).toString();
184
- const sandstoneConfig = (await import(configUrl)).default;
185
- // Build the context for sandstone
186
- const namespace = cliOptions.namespace || sandstoneConfig.namespace;
187
- const conflictStrategies = {};
188
- if (sandstoneConfig.onConflict) {
189
- for (const [resource, strategy] of Object.entries(sandstoneConfig.onConflict)) {
190
- conflictStrategies[resource] = strategy;
191
- }
192
- }
193
- // Import sandstone from the project's node_modules, not the CLI's
194
- // This ensures we use the same module instance as the user code
195
- const sandstoneUrl = pathToFileURL(path.join(folder, 'node_modules', 'sandstone', 'dist', 'index.js'));
196
- /* @ts-ignore */
197
- const { createSandstonePack, resetSandstonePack } = (await import(sandstoneUrl));
198
- const context = {
199
- workingDir: folder,
200
- namespace,
201
- packUid: sandstoneConfig.packUid,
202
- packOptions: sandstoneConfig.packs,
203
- conflictStrategies,
204
- loadVersion: sandstoneConfig.loadVersion,
205
- };
206
- // Create the pack with context
207
- const sandstonePack = createSandstonePack(context);
208
- return { sandstoneConfig, sandstonePack, resetSandstonePack };
209
- }
210
- async function _buildProject(cliOptions, folder, silent = false, existingContext, watching = false) {
211
- // Read project package.json to get entrypoint
212
- const packageJsonPath = path.join(folder, 'package.json');
213
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
214
- // Get the entrypoint from the "module" field
215
- const entrypoint = packageJson.module;
216
- if (!entrypoint) {
217
- throw new Error('No "module" field found in package.json. Please specify the entrypoint for your pack code.');
218
- }
219
- const entrypointPath = path.join(folder, entrypoint);
220
- // Load or use existing context
221
- const { sandstoneConfig, sandstonePack, resetSandstonePack } = existingContext ??
222
- await loadBuildContext(cliOptions, folder);
223
- // Reset pack state before each build
224
- resetSandstonePack();
225
- const { scripts, resources } = sandstoneConfig;
226
- const saveOptions = sandstoneConfig.saveOptions || {};
227
- const outputFolder = path.join(folder, '.sandstone', 'output');
228
- // Resolve options
229
- const worldName = cliOptions.world || saveOptions.world;
230
- const root = cliOptions.root !== undefined ? cliOptions.root : saveOptions.root;
231
- // Only use explicitly configured client path for now
232
- // We'll auto-detect after save() if there are client-side packs that need exporting
233
- let clientPath = !cliOptions.production
234
- ? cliOptions.clientPath || saveOptions.clientPath
235
- : undefined;
236
- if (worldName && !cliOptions.production) {
237
- // Need client path for world export
238
- clientPath ??= await getClientPath();
239
- if (clientPath) {
240
- await getClientWorldPath(worldName, clientPath);
241
- }
242
- }
243
- else if (root && !cliOptions.production) {
244
- // Need client path for root export
245
- clientPath ??= await getClientPath();
246
- }
247
- const serverPath = !cliOptions.production
248
- ? cliOptions.serverPath || saveOptions.serverPath
249
- : undefined;
250
- const packName = cliOptions.name ?? sandstoneConfig.name;
251
- if (worldName && root) {
252
- throw new Error("Expected only 'world' or 'root'. Got both.");
253
- }
254
- // Run beforeAll script
255
- await scripts?.beforeAll?.();
256
- // Import user code (this executes their pack definitions)
257
- if (!silent) {
258
- log('Compiling source...');
259
- }
260
- try {
261
- if (await fs.pathExists(entrypointPath)) {
262
- const isBun = Object.hasOwn(globalThis, 'Bun');
263
- const entrypointUrl = pathToFileURL(entrypointPath).toString();
264
- if (watching && !isBun) {
265
- // Hot-hook for Node.js - only this should be hot reloaded
266
- // Bun doesn't support hot-hook, we clear require.cache instead in watch.ts
267
- await import(entrypointUrl, { with: { hot: 'true' } });
268
- }
269
- else {
270
- await import(entrypointUrl);
271
- }
272
- }
273
- }
274
- catch (e) {
275
- const errorMsg = `While loading "${entrypointPath}":\n${cliOptions.fullTrace ? e : (e.message || e)}`;
276
- if (!silent) {
277
- console.error(chalk.bgRed.white('BuildError') + chalk.gray(':'), errorMsg);
278
- }
279
- log('BuildError:', errorMsg);
280
- throw e; // Re-throw for buildCommand to handle
281
- }
282
- // Add dependencies if specified
283
- if (cliOptions.dependencies) {
284
- for (const dependency of cliOptions.dependencies) {
285
- sandstonePack.core.depend(...dependency);
286
- }
287
- }
288
- // Setup cache
289
- const newCache = { files: {}, archives: [] };
290
- const cacheFile = path.join(folder, '.sandstone', 'cache.json');
291
- // Track which pack types have changed files
292
- const changedPackTypes = new Set();
293
- // Track directories containing new files
294
- const newDirs = new Set();
295
- if (cache === undefined) {
296
- try {
297
- const fileRead = await fs.readFile(cacheFile, 'utf8');
298
- if (fileRead) {
299
- const parsed = JSON.parse(fileRead);
300
- // Handle legacy cache format (plain Record<string, string>)
301
- if (parsed.files) {
302
- cache = parsed;
303
- }
304
- else {
305
- cache = { files: parsed };
306
- }
307
- }
308
- }
309
- catch {
310
- cache = { files: {} };
311
- }
312
- }
313
- // Check symlink availability (use cached value if available)
314
- if (symlinksAvailable === undefined) {
315
- if (cache.canUseSymlinks !== undefined) {
316
- symlinksAvailable = cache.canUseSymlinks;
317
- }
318
- else {
319
- symlinksAvailable = await canUseSymlinks();
320
- }
321
- }
322
- newCache.canUseSymlinks = symlinksAvailable;
323
- // Run beforeSave script
324
- await scripts?.beforeSave?.();
325
- // File exclusion setup
326
- const excludeOption = resources?.exclude;
327
- const fileExclusions = excludeOption
328
- ? {
329
- generated: ('generated' in excludeOption ? excludeOption.generated : excludeOption),
330
- existing: ('existing' in excludeOption ? excludeOption.existing : excludeOption),
331
- }
332
- : false;
333
- const fileHandlers = resources?.handle || false;
334
- // Save the pack
335
- const packTypes = await sandstonePack.save({
336
- dry: cliOptions.dry ?? false,
337
- verbose: cliOptions.verbose ?? false,
338
- fileHandler: saveOptions.customFileHandler ??
339
- (async (relativePath, content) => {
340
- let pathPass = true;
341
- if (fileExclusions && fileExclusions.generated) {
342
- for (const exclude of fileExclusions.generated) {
343
- if (!Array.isArray(exclude)) {
344
- pathPass = !exclude.test(relativePath);
345
- }
346
- }
347
- }
348
- if (fileHandlers) {
349
- for (const handler of fileHandlers) {
350
- if (handler.path.test(relativePath)) {
351
- content = await handler.callback(content);
352
- }
353
- }
354
- }
355
- if (pathPass) {
356
- const hashValue = hash(content + relativePath);
357
- newCache.files[relativePath] = hashValue;
358
- // Track parent directories
359
- for (let dir = path.dirname(relativePath); dir && dir !== '.'; dir = path.dirname(dir)) {
360
- newDirs.add(dir);
361
- }
362
- if (cache.files[relativePath] === hashValue) {
363
- return;
364
- }
365
- // Track that this pack type has changed
366
- const packTypeDir = relativePath.split(/[/\\]/)[0];
367
- changedPackTypes.add(packTypeDir);
368
- const realPath = path.join(outputFolder, relativePath);
369
- await fs.ensureDir(path.dirname(realPath));
370
- return await fs.writeFile(realPath, content);
371
- }
372
- }),
373
- });
374
- // Handle resources folder
375
- async function handleResources(packType) {
376
- const working = path.join(folder, 'resources', packType);
377
- if (!(await fs.pathExists(working))) {
378
- return;
379
- }
380
- const walk = async (dir) => {
381
- const files = [];
382
- const entries = await fs.readdir(dir, { withFileTypes: true });
383
- for (const entry of entries) {
384
- const fullPath = path.join(dir, entry.name);
385
- if (entry.isDirectory()) {
386
- files.push(...(await walk(fullPath)));
387
- }
388
- else {
389
- files.push(fullPath);
390
- }
391
- }
392
- return files;
393
- };
394
- for (const file of await walk(working)) {
395
- const relativePath = path.join(packType, file.substring(working.length + 1));
396
- let pathPass = true;
397
- if (fileExclusions && fileExclusions.existing) {
398
- for (const exclude of fileExclusions.existing) {
399
- pathPass = Array.isArray(exclude) ? !exclude[0].test(relativePath) : !exclude.test(relativePath);
400
- }
401
- }
402
- if (!pathPass)
403
- continue;
404
- try {
405
- let content = await fs.readFile(file);
406
- if (fileHandlers) {
407
- for (const handler of fileHandlers) {
408
- if (handler.path.test(relativePath)) {
409
- content = (await handler.callback(content));
410
- }
411
- }
412
- }
413
- const hashValue = hash(content + relativePath);
414
- newCache.files[relativePath] = hashValue;
415
- for (let dir = path.dirname(relativePath); dir && dir !== '.'; dir = path.dirname(dir)) {
416
- if (newDirs.has(dir)) {
417
- break;
418
- }
419
- else {
420
- newDirs.add(dir);
421
- }
422
- }
423
- if (cache.files[relativePath] !== hashValue) {
424
- // Track that this pack type has changed
425
- changedPackTypes.add(packType);
426
- const realPath = path.join(outputFolder, relativePath);
427
- await fs.ensureDir(path.dirname(realPath));
428
- await fs.writeFile(realPath, content);
429
- }
430
- }
431
- catch { }
432
- }
433
- }
434
- // Archive output if needed
435
- async function archiveOutput(packType) {
436
- const input = path.join(outputFolder, packType.type);
437
- const files = await fs.readdir(input).catch(() => []);
438
- if (files.length === 0)
439
- return false;
440
- const archiveName = `${packName}_${packType.type}.zip`;
441
- newCache.archives.push(archiveName);
442
- const archive = new AdmZip();
443
- await archive.addLocalFolderPromise(input, {});
444
- await fs.ensureDir(path.join(outputFolder, 'archives'));
445
- await archive.writeZipPromise(path.join(outputFolder, 'archives', archiveName), { overwrite: true });
446
- return true;
447
- }
448
- // Export to client/server
449
- if (!cliOptions.production) {
450
- // Check if there are any client-side packs that need exporting
451
- // If so and clientPath not set, try to find it now (after dependencies resolved)
452
- const packTypesArray = [...packTypes];
453
- const hasClientPacks = packTypesArray.some(([, pt]) => pt.networkSides === 'client');
454
- if (hasClientPacks && !clientPath && (root || worldName)) {
455
- clientPath = await getClientPath();
456
- }
457
- // When no world/root specified, only export client-side packs (resource packs + dependencies)
458
- const resourcePackOnlyExport = !worldName && !root;
459
- for (const [, packType] of packTypesArray) {
460
- const outputPath = path.join(outputFolder, packType.type);
461
- await fs.ensureDir(outputPath);
462
- if (packType.handleOutput) {
463
- await packType.handleOutput('output', (async (relativePath, encoding = 'utf8') => await fs.readFile(path.join(outputPath, relativePath), encoding)), async (relativePath, contents) => {
464
- if (contents === undefined) {
465
- await fs.unlink(path.join(outputPath, relativePath));
466
- }
467
- else {
468
- await fs.writeFile(path.join(outputPath, relativePath), contents.replaceAll('"min_version"', '"min_format"').replace('"max_version"', '"max_format"'));
469
- }
470
- });
471
- }
472
- await handleResources(packType.type);
473
- // Skip archive and export if no files in this pack type changed
474
- if (!changedPackTypes.has(packType.type)) {
475
- continue;
476
- }
477
- let archivedOutput = false;
478
- if (packType.archiveOutput && saveOptions.exportZips) {
479
- archivedOutput = await archiveOutput(packType);
480
- }
481
- // Handle client export
482
- // Skip non-client packs (datapacks) when in resource pack only mode (no world/root specified)
483
- if (clientPath && !(resourcePackOnlyExport && packType.networkSides !== 'client')) {
484
- let fullClientPath;
485
- // Only export the resource pack to `$worldName$/resources.zip` if exportZips is on
486
- if (worldName && (packType.type !== 'resourcepack' || saveOptions.exportZips)) {
487
- fullClientPath = path.join(clientPath, packType.clientPath)
488
- .replace('$packName$', packName)
489
- .replace('$worldName$', worldName);
490
- }
491
- else {
492
- fullClientPath = path.join(clientPath, packType.rootPath).replace('$packName$', packName);
493
- }
494
- if (packType.archiveOutput && archivedOutput && saveOptions.exportZips) {
495
- const archivePath = path.join(outputFolder, 'archives', `${packName}_${packType.type}.zip`);
496
- await fs.copyFile(archivePath, `${fullClientPath}.zip`);
497
- }
498
- else if (symlinksAvailable) {
499
- if (cache.symlinks === undefined || !cache.symlinks.includes(fullClientPath)) {
500
- await handleSymlink(folder, packName, newCache, clientPath, outputPath, fullClientPath);
501
- }
502
- }
503
- else {
504
- await fs.remove(fullClientPath);
505
- await fs.copy(outputPath, fullClientPath);
506
- }
507
- }
508
- // Handle server export (skip client-only packs like resource packs)
509
- if (serverPath && packType.networkSides === 'server') {
510
- const fullServerPath = path.join(serverPath, packType.serverPath).replace('$packName$', packName);
511
- if (packType.archiveOutput && archivedOutput && saveOptions.exportZips) {
512
- const archivePath = path.join(outputFolder, 'archives', `${packName}_${packType.type}.zip`);
513
- await fs.copyFile(archivePath, `${fullServerPath}.zip`);
514
- }
515
- else if (symlinksAvailable) {
516
- if (cache.symlinks === undefined || !cache.symlinks.includes(fullServerPath)) {
517
- await handleSymlink(folder, packName, newCache, serverPath, outputPath, fullServerPath);
518
- }
519
- }
520
- else {
521
- await fs.remove(fullServerPath);
522
- await fs.copy(outputPath, fullServerPath);
523
- }
524
- }
525
- }
526
- }
527
- else {
528
- // Production mode
529
- for await (const [, packType] of packTypes) {
530
- const outputPath = path.join(outputFolder, packType.type);
531
- if (packType.handleOutput) {
532
- await packType.handleOutput('output', (async (relativePath, encoding = 'utf8') => await fs.readFile(path.join(outputPath, relativePath), encoding)), async (relativePath, contents) => {
533
- if (contents === undefined) {
534
- await fs.unlink(path.join(outputPath, relativePath));
535
- }
536
- else {
537
- await fs.writeFile(path.join(outputPath, relativePath), contents.replaceAll('"min_version"', '"min_format"').replace('"max_version"', '"max_format"'));
538
- }
539
- });
540
- }
541
- await handleResources(packType.type);
542
- }
543
- }
544
- // Clean up old files, directories, and symlinks not in new cache
545
- if (cliOptions.dry !== true) {
546
- for (const file of Object.keys(cache.files)) {
547
- if (!(file in newCache.files)) {
548
- await fs.rm(path.join(outputFolder, file));
549
- let dir = undefined;
550
- for (const segment of split(new RegExp(RegExp.escape(path.sep)), path.dirname(file))) {
551
- dir = dir === undefined ? segment : path.join(dir, segment);
552
- if (!newDirs.has(dir)) {
553
- await fs.rm(path.join(outputFolder, dir), { force: true, recursive: true });
554
- break;
555
- }
556
- }
557
- }
558
- }
559
- // Clean up old archives
560
- if (cache.archives) {
561
- const archivesDir = path.join(outputFolder, 'archives');
562
- if (newCache.archives === undefined || newCache.archives.length === 0) {
563
- await fs.rm(archivesDir, { force: true, recursive: true });
564
- }
565
- for (const archive of cache.archives) {
566
- if (!newCache.archives.includes(archive)) {
567
- await fs.rm(path.join(archivesDir, archive));
568
- }
569
- }
570
- }
571
- if (cache.symlinks) {
572
- const newSymlinks = new Set(newCache.symlinks);
573
- for (const symlink of cache.symlinks) {
574
- if (!newSymlinks.has(symlink)) {
575
- await fs.rm(symlink);
576
- }
577
- }
578
- }
579
- // Update cache
580
- cache = newCache;
581
- await fs.ensureDir(path.dirname(cacheFile));
582
- await fs.writeFile(cacheFile, JSON.stringify(cache));
583
- }
584
- // Run afterAll script
585
- await scripts?.afterAll?.();
586
- // Count resources (excluding boilerplate)
587
- const resourceCounts = countResources(sandstonePack);
588
- const exports = [clientPath && 'client', serverPath && 'server'].filter(Boolean).join(' & ') || false;
589
- const countMsg = `${resourceCounts.functions} functions, ${resourceCounts.other} other resources`;
590
- if (!silent) {
591
- log(`Pack(s) compiled! (${countMsg})${exports ? ` Exported to ${exports}.` : ''}`);
592
- }
593
- return { resourceCounts, sandstoneConfig, sandstonePack, resetSandstonePack };
594
- }
595
- export async function _buildCommand(opts, _folder, existingContext, watching = false) {
596
- const folder = _folder ?? opts.path;
597
- try {
598
- const result = await _buildProject(opts, folder, true, existingContext, watching);
599
- return {
600
- success: true,
601
- resourceCounts: result?.resourceCounts ?? { functions: 0, other: 0 },
602
- timestamp: Date.now(),
603
- sandstoneConfig: result?.sandstoneConfig,
604
- sandstonePack: result?.sandstonePack,
605
- resetSandstonePack: result?.resetSandstonePack,
606
- };
607
- }
608
- catch (err) {
609
- const errorMessage = err.message || String(err);
610
- // Always include stack trace for better debugging - format paths for terminal clickability
611
- const stack = err.stack || '';
612
- // Clean up stack trace: remove ?hot-hook query params and convert file:// URLs to paths
613
- const cleanedStack = stack
614
- .replace(/\?hot-hook=\d+/g, '') // Remove hot-hook cache busting params
615
- .replace(/file:\/\/\//g, '') // Convert file:/// URLs to paths (Windows)
616
- .replace(/file:\/\//g, ''); // Convert file:// URLs to paths (Unix)
617
- const formattedError = cleanedStack ? `${errorMessage}\n${cleanedStack}` : errorMessage;
618
- log('Build failed:', errorMessage);
619
- return {
620
- success: false,
621
- error: formattedError,
622
- resourceCounts: { functions: 0, other: 0 },
623
- timestamp: Date.now(),
624
- };
625
- }
626
- }
627
- export async function buildCommand(opts, _folder, silent = false) {
628
- // Commander passes Command object as second arg, so check for string explicitly
629
- const folder = (typeof _folder === 'string') ? _folder : opts.path;
630
- // Initialize logger without file for build mode
631
- initLoggerNoFile();
632
- setSilent(silent);
633
- try {
634
- const result = await _buildProject(opts, folder, silent);
635
- if (silent) {
636
- return {
637
- success: true,
638
- resourceCounts: result?.resourceCounts ?? { functions: 0, other: 0 },
639
- timestamp: Date.now(),
640
- sandstoneConfig: result?.sandstoneConfig,
641
- sandstonePack: result?.sandstonePack,
642
- resetSandstonePack: result?.resetSandstonePack,
643
- };
644
- }
645
- }
646
- catch (err) {
647
- const errorMessage = err.message || String(err);
648
- if (!silent) {
649
- console.error(chalk.red('Build failed:'), errorMessage);
650
- if (opts.fullTrace) {
651
- console.error(err);
652
- }
653
- }
654
- log('Build failed:', errorMessage);
655
- if (silent) {
656
- return {
657
- success: false,
658
- error: opts.fullTrace ? String(err) : errorMessage,
659
- resourceCounts: { functions: 0, other: 0 },
660
- timestamp: Date.now(),
661
- };
662
- }
663
- }
664
- }
@@ -1,8 +0,0 @@
1
- type CreateOptions = {
2
- root: boolean;
3
- world?: string;
4
- clientPath?: string;
5
- serverPath?: string;
6
- };
7
- export declare function createCommand(_project: string, opts: CreateOptions): Promise<void>;
8
- export {};