sandstone-cli 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +234 -5
- package/lib/create.js +2 -2
- package/lib/index.js +3135 -3087
- package/package.json +7 -4
- package/src/commands/build/export.ts +295 -0
- package/src/commands/build/externalResources.ts +122 -0
- package/src/commands/build/index.ts +548 -0
- package/src/commands/create.ts +1 -1
- package/src/commands/dependency.ts +1 -1
- package/src/commands/index.ts +1 -1
- package/src/commands/watch.ts +37 -2
- package/src/ui/WatchUI.tsx +10 -2
- package/src/utils.ts +6 -0
- package/src/version.ts +1 -1
- package/src/commands/build.ts +0 -869
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sandstone-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "The CLI for Sandstone - the minecraft pack creation library.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./lib/index.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@inquirer/prompts": "^8.2.0",
|
|
47
47
|
"@parcel/watcher": "^2.5.6",
|
|
48
|
-
"@sandstone-mc/hot-hook": "
|
|
48
|
+
"@sandstone-mc/hot-hook": "^0.1.0",
|
|
49
49
|
"@types/chalk": "^2.2.4",
|
|
50
50
|
"adm-zip": "^0.5.10",
|
|
51
51
|
"chalk": "^5.6.2",
|
|
@@ -69,7 +69,10 @@
|
|
|
69
69
|
"@types/semver": "^7.5.3",
|
|
70
70
|
"bun-types": "^1.3.9",
|
|
71
71
|
"node-pty": "^1.1.0",
|
|
72
|
-
"sandstone": "
|
|
72
|
+
"sandstone": "^1.0.0-beta.2",
|
|
73
73
|
"typescript": "^5.2.2"
|
|
74
|
-
}
|
|
74
|
+
},
|
|
75
|
+
"trustedDependencies": [
|
|
76
|
+
"@parcel/watcher"
|
|
77
|
+
]
|
|
75
78
|
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import AdmZip from 'adm-zip'
|
|
5
|
+
|
|
6
|
+
import { log } from '../../ui/logger.js'
|
|
7
|
+
import { canUseSymlinks } from '../../utils.js'
|
|
8
|
+
|
|
9
|
+
import type { handlerReadFile, PackType } from 'sandstone/pack'
|
|
10
|
+
|
|
11
|
+
export type SandstoneCache = {
|
|
12
|
+
files: Record<string, string>
|
|
13
|
+
archives?: string[]
|
|
14
|
+
canUseSymlinks?: boolean
|
|
15
|
+
symlinks?: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Module-level symlink availability cache
|
|
19
|
+
let symlinksAvailable: boolean | undefined
|
|
20
|
+
|
|
21
|
+
export async function checkSymlinksAvailable(cachedValue?: boolean): Promise<boolean> {
|
|
22
|
+
if (symlinksAvailable === undefined) {
|
|
23
|
+
if (cachedValue !== undefined) {
|
|
24
|
+
symlinksAvailable = cachedValue
|
|
25
|
+
} else {
|
|
26
|
+
symlinksAvailable = await canUseSymlinks()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return symlinksAvailable
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getSymlinksAvailable(): boolean {
|
|
33
|
+
return symlinksAvailable ?? false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Minecraft path detection
|
|
37
|
+
|
|
38
|
+
function getMCPath(): string {
|
|
39
|
+
switch (os.platform()) {
|
|
40
|
+
case 'win32':
|
|
41
|
+
return path.join(os.homedir(), 'AppData/Roaming/.minecraft')
|
|
42
|
+
case 'darwin':
|
|
43
|
+
return path.join(os.homedir(), 'Library/Application Support/minecraft')
|
|
44
|
+
case 'linux':
|
|
45
|
+
default:
|
|
46
|
+
return path.join(os.homedir(), '.minecraft')
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getClientPath(): Promise<string | undefined> {
|
|
51
|
+
const mcPath = getMCPath()
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await fs.stat(mcPath)
|
|
55
|
+
} catch {
|
|
56
|
+
log('Unable to locate the .minecraft folder. Will not be able to export to client.')
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return mcPath
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getClientWorldPath(worldName: string, minecraftPath?: string): Promise<string> {
|
|
64
|
+
const mcPath = minecraftPath ?? (await getClientPath())!
|
|
65
|
+
const savesPath = path.join(mcPath, 'saves')
|
|
66
|
+
const worldPath = path.join(savesPath, worldName)
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(worldPath)) {
|
|
69
|
+
const existingWorlds = (await fs.readdir(savesPath, { withFileTypes: true }))
|
|
70
|
+
.filter((f) => f.isDirectory())
|
|
71
|
+
.map((f) => f.name)
|
|
72
|
+
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Unable to locate the "${worldPath}" folder. World ${worldName} does not exist. List of existing worlds: ${JSON.stringify(existingWorlds, null, 2)}`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return worldPath
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Symlink handling
|
|
82
|
+
|
|
83
|
+
export async function createSymlink(
|
|
84
|
+
folder: string,
|
|
85
|
+
packName: string,
|
|
86
|
+
newCache: SandstoneCache,
|
|
87
|
+
minecraftPath: string,
|
|
88
|
+
targetPath: string,
|
|
89
|
+
linkPath: string
|
|
90
|
+
) {
|
|
91
|
+
// Update allowed_symlinks.txt for Minecraft
|
|
92
|
+
let rawPath = path.resolve(path.join(folder))
|
|
93
|
+
let sep: string = path.sep
|
|
94
|
+
if (os.platform() === 'win32') {
|
|
95
|
+
sep = `${path.sep}${path.sep}`
|
|
96
|
+
rawPath = rawPath.replace(path.sep, sep)
|
|
97
|
+
}
|
|
98
|
+
const allowPath = `[glob]${rawPath}${sep}**${sep}*`
|
|
99
|
+
|
|
100
|
+
const allowedList = path.join(minecraftPath, 'allowed_symlinks.txt')
|
|
101
|
+
|
|
102
|
+
const comment = `# Sandstone Pack: ${packName}\n`
|
|
103
|
+
try {
|
|
104
|
+
const currentlyAllowed = await fs.readFile(allowedList, 'utf-8')
|
|
105
|
+
|
|
106
|
+
if (!currentlyAllowed.includes(allowPath)) {
|
|
107
|
+
log('[symlink] Adding workspace to allowed_symlinks.txt. If the game is running please restart it.')
|
|
108
|
+
await fs.writeFile(allowedList, `${currentlyAllowed}\n#\n${comment}${allowPath}`)
|
|
109
|
+
} else {
|
|
110
|
+
log('[symlink] Workspace already in allowed_symlinks.txt, skipping...')
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
log('[symlink] Creating allowed_symlinks.txt. If the game is running please restart it.')
|
|
114
|
+
await fs.writeFile(allowedList, `${comment}${allowPath}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if symlink already exists
|
|
118
|
+
let skip = false
|
|
119
|
+
let errored = false
|
|
120
|
+
try {
|
|
121
|
+
const stats = await fs.lstat(linkPath)
|
|
122
|
+
if (stats.isSymbolicLink() && await fs.readlink(linkPath) === path.resolve(targetPath)) {
|
|
123
|
+
log('[symlink] Symlink already created, skipping...')
|
|
124
|
+
skip = true
|
|
125
|
+
} else {
|
|
126
|
+
errored = true
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
129
|
+
|
|
130
|
+
if (errored) {
|
|
131
|
+
throw new Error(`Tried to add a symlink at "${linkPath}",\n encountered an existing FS entry.`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create symlink
|
|
135
|
+
if (!skip) {
|
|
136
|
+
log(`[symlink] Creating symlink for ${targetPath.replace(`${path.dirname(targetPath)}${path.sep}`, '')}`)
|
|
137
|
+
await fs.symlink(path.resolve(targetPath), linkPath)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Track in cache
|
|
141
|
+
newCache.symlinks ??= []
|
|
142
|
+
newCache.symlinks.push(linkPath)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Archive creation
|
|
146
|
+
|
|
147
|
+
export async function createArchive(
|
|
148
|
+
outputFolder: string,
|
|
149
|
+
packName: string,
|
|
150
|
+
packType: PackType,
|
|
151
|
+
newCache: SandstoneCache
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
const input = path.join(outputFolder, packType.type)
|
|
154
|
+
|
|
155
|
+
const files = await fs.readdir(input).catch(() => [])
|
|
156
|
+
if (files.length === 0) return false
|
|
157
|
+
|
|
158
|
+
const archiveName = `${packName}_${packType.type}.zip`
|
|
159
|
+
newCache.archives ??= []
|
|
160
|
+
newCache.archives.push(archiveName)
|
|
161
|
+
|
|
162
|
+
const archive = new AdmZip()
|
|
163
|
+
await archive.addLocalFolderPromise(input, {})
|
|
164
|
+
await fs.ensureDir(path.join(outputFolder, 'archives'))
|
|
165
|
+
await archive.writeZipPromise(
|
|
166
|
+
path.join(outputFolder, 'archives', archiveName),
|
|
167
|
+
{ overwrite: true },
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Run pack type's export handler for client/server destinations
|
|
174
|
+
|
|
175
|
+
export async function runExportHandler(
|
|
176
|
+
packType: PackType,
|
|
177
|
+
target: 'client' | 'server',
|
|
178
|
+
exportPath: string
|
|
179
|
+
) {
|
|
180
|
+
if (!packType.handleOutput) return
|
|
181
|
+
|
|
182
|
+
await packType.handleOutput(
|
|
183
|
+
target,
|
|
184
|
+
(async (relativePath: string, encoding: BufferEncoding = 'utf8') =>
|
|
185
|
+
await fs.readFile(path.join(exportPath, relativePath), encoding)) as unknown as handlerReadFile,
|
|
186
|
+
async (relativePath: string, contents: any) => {
|
|
187
|
+
if (contents === undefined) {
|
|
188
|
+
await fs.unlink(path.join(exportPath, relativePath))
|
|
189
|
+
} else {
|
|
190
|
+
await fs.writeFile(path.join(exportPath, relativePath), contents)
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Export destination helpers
|
|
197
|
+
|
|
198
|
+
export function preserveSymlink(
|
|
199
|
+
symlinkPath: string | undefined,
|
|
200
|
+
oldCache: SandstoneCache,
|
|
201
|
+
newCache: SandstoneCache
|
|
202
|
+
) {
|
|
203
|
+
if (!getSymlinksAvailable() || !symlinkPath) return
|
|
204
|
+
if (!oldCache.symlinks?.includes(symlinkPath)) return
|
|
205
|
+
|
|
206
|
+
newCache.symlinks ??= []
|
|
207
|
+
if (!newCache.symlinks.includes(symlinkPath)) {
|
|
208
|
+
newCache.symlinks.push(symlinkPath)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function exportPack(
|
|
213
|
+
destPath: string,
|
|
214
|
+
minecraftPath: string,
|
|
215
|
+
outputPath: string,
|
|
216
|
+
outputFolder: string,
|
|
217
|
+
folder: string,
|
|
218
|
+
packName: string,
|
|
219
|
+
packType: PackType,
|
|
220
|
+
archivedOutput: boolean,
|
|
221
|
+
exportZips: boolean | undefined,
|
|
222
|
+
oldCache: SandstoneCache,
|
|
223
|
+
newCache: SandstoneCache
|
|
224
|
+
) {
|
|
225
|
+
if (packType.archiveOutput && archivedOutput && exportZips) {
|
|
226
|
+
// Copy archive
|
|
227
|
+
const archivePath = path.join(outputFolder, 'archives', `${packName}_${packType.type}.zip`)
|
|
228
|
+
await fs.copyFile(archivePath, `${destPath}.zip`)
|
|
229
|
+
} else if (getSymlinksAvailable()) {
|
|
230
|
+
// Create symlink (only if it doesn't already exist)
|
|
231
|
+
if (!oldCache.symlinks?.includes(destPath)) {
|
|
232
|
+
await createSymlink(folder, packName, newCache, minecraftPath, outputPath, destPath)
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
// Copy files
|
|
236
|
+
await fs.remove(destPath)
|
|
237
|
+
await fs.copy(outputPath, destPath)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getExportPath(
|
|
242
|
+
packType: PackType,
|
|
243
|
+
basePath: string,
|
|
244
|
+
target: 'client' | 'server',
|
|
245
|
+
packName: string,
|
|
246
|
+
worldName: string | undefined,
|
|
247
|
+
exportZips: boolean | undefined
|
|
248
|
+
): string {
|
|
249
|
+
if (target === 'server') {
|
|
250
|
+
return path.join(basePath, packType.serverPath).replace('$packName$', packName)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Client path: use world path or root path
|
|
254
|
+
const useWorldPath = worldName && (packType.type !== 'resourcepack' || exportZips)
|
|
255
|
+
if (useWorldPath) {
|
|
256
|
+
return path.join(basePath, packType.clientPath)
|
|
257
|
+
.replace('$packName$', packName)
|
|
258
|
+
.replace('$worldName$', worldName)
|
|
259
|
+
}
|
|
260
|
+
return path.join(basePath, packType.rootPath).replace('$packName$', packName)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Cleanup
|
|
264
|
+
|
|
265
|
+
export async function cleanupOldSymlinks(oldCache: SandstoneCache, newCache: SandstoneCache) {
|
|
266
|
+
if (!oldCache.symlinks) return
|
|
267
|
+
|
|
268
|
+
const newSymlinks = new Set(newCache.symlinks)
|
|
269
|
+
|
|
270
|
+
for (const symlink of oldCache.symlinks) {
|
|
271
|
+
if (!newSymlinks.has(symlink)) {
|
|
272
|
+
await fs.unlink(symlink)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function cleanupOldArchives(
|
|
278
|
+
outputFolder: string,
|
|
279
|
+
oldCache: SandstoneCache,
|
|
280
|
+
newCache: SandstoneCache
|
|
281
|
+
) {
|
|
282
|
+
if (!oldCache.archives) return
|
|
283
|
+
|
|
284
|
+
const archivesDir = path.join(outputFolder, 'archives')
|
|
285
|
+
if (!newCache.archives || newCache.archives.length === 0) {
|
|
286
|
+
await fs.rm(archivesDir, { force: true, recursive: true })
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
for (const archive of oldCache.archives) {
|
|
291
|
+
if (!newCache.archives.includes(archive)) {
|
|
292
|
+
await fs.rm(path.join(archivesDir, archive))
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
|
|
4
|
+
import type { SandstoneCache } from './export.js'
|
|
5
|
+
import { hash } from '../../utils.js'
|
|
6
|
+
|
|
7
|
+
export type FileExclusions = {
|
|
8
|
+
generated: RegExp[] | undefined
|
|
9
|
+
existing: RegExp[] | undefined
|
|
10
|
+
} | false
|
|
11
|
+
|
|
12
|
+
export type FileHandler = {
|
|
13
|
+
path: RegExp
|
|
14
|
+
callback: (contents: string | Buffer | Promise<Buffer>) => Promise<Buffer>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function walk(dir: string): Promise<string[]> {
|
|
18
|
+
const files: string[] = []
|
|
19
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const fullPath = path.join(dir, entry.name)
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
files.push(...(await walk(fullPath)))
|
|
24
|
+
} else {
|
|
25
|
+
files.push(fullPath)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return files
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if external resources exist and register pack types accordingly.
|
|
33
|
+
*/
|
|
34
|
+
export async function autoRegisterPackTypes(
|
|
35
|
+
folder: string,
|
|
36
|
+
sandstonePack: { resourcePack: () => void; dataPack: () => void }
|
|
37
|
+
) {
|
|
38
|
+
const resourcesFolder = path.join(folder, 'resources')
|
|
39
|
+
|
|
40
|
+
if (await fs.pathExists(path.join(resourcesFolder, 'resourcepack'))) {
|
|
41
|
+
const files = await fs.readdir(path.join(resourcesFolder, 'resourcepack'))
|
|
42
|
+
if (files.length > 0) {
|
|
43
|
+
sandstonePack.resourcePack()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (await fs.pathExists(path.join(resourcesFolder, 'datapack'))) {
|
|
48
|
+
const files = await fs.readdir(path.join(resourcesFolder, 'datapack'))
|
|
49
|
+
if (files.length > 0) {
|
|
50
|
+
sandstonePack.dataPack()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Process external resources from the resources/ folder.
|
|
57
|
+
*/
|
|
58
|
+
export async function processExternalResources(
|
|
59
|
+
packType: string,
|
|
60
|
+
folder: string,
|
|
61
|
+
outputFolder: string,
|
|
62
|
+
oldCache: SandstoneCache,
|
|
63
|
+
newCache: SandstoneCache,
|
|
64
|
+
changedPackTypes: Set<string>,
|
|
65
|
+
newDirs: Set<string>,
|
|
66
|
+
fileExclusions: FileExclusions,
|
|
67
|
+
fileHandlers: FileHandler[] | false
|
|
68
|
+
) {
|
|
69
|
+
const working = path.join(folder, 'resources', packType)
|
|
70
|
+
|
|
71
|
+
if (!(await fs.pathExists(working))) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const file of await walk(working)) {
|
|
76
|
+
const relativePath = path.join(packType, file.substring(working.length + 1))
|
|
77
|
+
|
|
78
|
+
// Check exclusions
|
|
79
|
+
let pathPass = true
|
|
80
|
+
if (fileExclusions && fileExclusions.existing) {
|
|
81
|
+
for (const exclude of fileExclusions.existing) {
|
|
82
|
+
pathPass = Array.isArray(exclude) ? !exclude[0].test(relativePath) : !exclude.test(relativePath)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!pathPass) continue
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
let content = await fs.readFile(file)
|
|
90
|
+
|
|
91
|
+
// Apply file handlers
|
|
92
|
+
if (fileHandlers) {
|
|
93
|
+
for (const handler of fileHandlers) {
|
|
94
|
+
if (handler.path.test(relativePath)) {
|
|
95
|
+
content = (await handler.callback(content)) as Buffer<ArrayBuffer>
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hashValue = hash(content + relativePath)
|
|
101
|
+
newCache.files[relativePath] = hashValue
|
|
102
|
+
|
|
103
|
+
// Track directories
|
|
104
|
+
for (let dir = path.dirname(relativePath); dir && dir !== '.'; dir = path.dirname(dir)) {
|
|
105
|
+
if (newDirs.has(dir)) {
|
|
106
|
+
break
|
|
107
|
+
} else {
|
|
108
|
+
newDirs.add(dir)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Write if changed
|
|
113
|
+
if (oldCache.files[relativePath] !== hashValue) {
|
|
114
|
+
changedPackTypes.add(packType)
|
|
115
|
+
|
|
116
|
+
const realPath = path.join(outputFolder, relativePath)
|
|
117
|
+
await fs.ensureDir(path.dirname(realPath))
|
|
118
|
+
await fs.writeFile(realPath, content)
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|