sandstone-cli 2.1.3 → 2.2.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/CLAUDE.md +107 -0
- package/bun.lock +8 -228
- package/lib/create.js +381 -9
- package/lib/index.js +421 -47
- package/package.json +7 -4
- package/scripts/test-harness.ts +341 -0
- package/src/commands/create.ts +101 -4
- package/src/launchers/index.ts +15 -0
- package/src/launchers/providers/modrinth.ts +134 -0
- package/src/launchers/providers/prism.ts +150 -0
- package/src/launchers/providers/vanilla.ts +48 -0
- package/src/launchers/registry.ts +46 -0
- package/src/launchers/types.ts +32 -0
- package/src/version.ts +1 -1
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import type { LauncherProvider, MinecraftInstance } from '../types.js'
|
|
5
|
+
|
|
6
|
+
function getPrismCandidatePaths(): string[] {
|
|
7
|
+
const home = os.homedir()
|
|
8
|
+
const paths: string[] = []
|
|
9
|
+
|
|
10
|
+
switch (os.platform()) {
|
|
11
|
+
case 'win32':
|
|
12
|
+
paths.push(path.join(os.homedir(), 'AppData/Roaming/PrismLauncher'))
|
|
13
|
+
break
|
|
14
|
+
case 'darwin':
|
|
15
|
+
paths.push(path.join(home, 'Library/Application Support/PrismLauncher'))
|
|
16
|
+
break
|
|
17
|
+
case 'linux':
|
|
18
|
+
default: {
|
|
19
|
+
// Check XDG_DATA_HOME first
|
|
20
|
+
const xdgDataHome = process.env.XDG_DATA_HOME
|
|
21
|
+
if (xdgDataHome) {
|
|
22
|
+
paths.push(path.join(xdgDataHome, 'PrismLauncher'))
|
|
23
|
+
}
|
|
24
|
+
// Standard location
|
|
25
|
+
paths.push(path.join(home, '.local/share/PrismLauncher'))
|
|
26
|
+
// Flatpak location
|
|
27
|
+
paths.push(path.join(home, '.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher'))
|
|
28
|
+
break
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return paths
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPrismDataPath(): string | null {
|
|
36
|
+
for (const candidate of getPrismCandidatePaths()) {
|
|
37
|
+
if (fs.existsSync(candidate)) {
|
|
38
|
+
return candidate
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse INI-style instance.cfg to extract instance name */
|
|
45
|
+
function parseInstanceConfig(configPath: string): { name?: string } {
|
|
46
|
+
try {
|
|
47
|
+
const content = fs.readFileSync(configPath, 'utf-8')
|
|
48
|
+
const result: { name?: string } = {}
|
|
49
|
+
|
|
50
|
+
for (const line of content.split('\n')) {
|
|
51
|
+
const trimmed = line.trim()
|
|
52
|
+
if (trimmed.startsWith('name=')) {
|
|
53
|
+
result.name = trimmed.slice(5)
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result
|
|
59
|
+
} catch {
|
|
60
|
+
return {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Parse mmc-pack.json to extract Minecraft version */
|
|
65
|
+
function parsePackJson(packPath: string): { version?: string } {
|
|
66
|
+
try {
|
|
67
|
+
const content = fs.readFileSync(packPath, 'utf-8')
|
|
68
|
+
const pack = JSON.parse(content)
|
|
69
|
+
|
|
70
|
+
// Look for net.minecraft component
|
|
71
|
+
const components = pack.components as Array<{ uid: string; version: string }> | undefined
|
|
72
|
+
if (components) {
|
|
73
|
+
const minecraft = components.find(c => c.uid === 'net.minecraft')
|
|
74
|
+
if (minecraft?.version) {
|
|
75
|
+
return { version: minecraft.version }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {}
|
|
80
|
+
} catch {
|
|
81
|
+
return {}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const prismProvider: LauncherProvider = {
|
|
86
|
+
type: 'prism',
|
|
87
|
+
displayName: 'Prism Launcher',
|
|
88
|
+
|
|
89
|
+
async isInstalled(): Promise<boolean> {
|
|
90
|
+
return getPrismDataPath() !== null
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
getDataPath(): string | null {
|
|
94
|
+
return getPrismDataPath()
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async discoverInstances(): Promise<MinecraftInstance[]> {
|
|
98
|
+
const dataPath = getPrismDataPath()
|
|
99
|
+
if (!dataPath) return []
|
|
100
|
+
|
|
101
|
+
const instancesDir = path.join(dataPath, 'instances')
|
|
102
|
+
if (!fs.existsSync(instancesDir)) return []
|
|
103
|
+
|
|
104
|
+
const instances: MinecraftInstance[] = []
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const entries = fs.readdirSync(instancesDir, { withFileTypes: true })
|
|
108
|
+
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (!entry.isDirectory()) continue
|
|
111
|
+
// Skip hidden folders and special folders
|
|
112
|
+
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue
|
|
113
|
+
|
|
114
|
+
const instanceDir = path.join(instancesDir, entry.name)
|
|
115
|
+
const minecraftDir = path.join(instanceDir, 'minecraft')
|
|
116
|
+
const dotMinecraftDir = path.join(instanceDir, '.minecraft')
|
|
117
|
+
|
|
118
|
+
// Prism uses 'minecraft' or '.minecraft' subdirectory
|
|
119
|
+
let minecraftPath: string | null = null
|
|
120
|
+
if (fs.existsSync(minecraftDir)) {
|
|
121
|
+
minecraftPath = minecraftDir
|
|
122
|
+
} else if (fs.existsSync(dotMinecraftDir)) {
|
|
123
|
+
minecraftPath = dotMinecraftDir
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!minecraftPath) continue
|
|
127
|
+
|
|
128
|
+
// Parse instance.cfg for display name
|
|
129
|
+
const configPath = path.join(instanceDir, 'instance.cfg')
|
|
130
|
+
const config = parseInstanceConfig(configPath)
|
|
131
|
+
|
|
132
|
+
// Parse mmc-pack.json for Minecraft version
|
|
133
|
+
const packPath = path.join(instanceDir, 'mmc-pack.json')
|
|
134
|
+
const pack = parsePackJson(packPath)
|
|
135
|
+
|
|
136
|
+
instances.push({
|
|
137
|
+
id: `prism-${entry.name}`,
|
|
138
|
+
name: config.name || entry.name,
|
|
139
|
+
launcher: 'prism',
|
|
140
|
+
minecraftPath,
|
|
141
|
+
version: pack.version,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Directory read failed
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return instances
|
|
149
|
+
},
|
|
150
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import type { LauncherProvider, MinecraftInstance } from '../types.js'
|
|
5
|
+
|
|
6
|
+
function getVanillaPath(): string | null {
|
|
7
|
+
let mcPath: string
|
|
8
|
+
|
|
9
|
+
switch (os.platform()) {
|
|
10
|
+
case 'win32':
|
|
11
|
+
mcPath = path.join(os.homedir(), 'AppData/Roaming/.minecraft')
|
|
12
|
+
break
|
|
13
|
+
case 'darwin':
|
|
14
|
+
mcPath = path.join(os.homedir(), 'Library/Application Support/minecraft')
|
|
15
|
+
break
|
|
16
|
+
case 'linux':
|
|
17
|
+
default:
|
|
18
|
+
mcPath = path.join(os.homedir(), '.minecraft')
|
|
19
|
+
break
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return fs.existsSync(mcPath) ? mcPath : null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const vanillaProvider: LauncherProvider = {
|
|
26
|
+
type: 'vanilla',
|
|
27
|
+
displayName: 'Vanilla Minecraft',
|
|
28
|
+
|
|
29
|
+
async isInstalled(): Promise<boolean> {
|
|
30
|
+
return getVanillaPath() !== null
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
getDataPath(): string | null {
|
|
34
|
+
return getVanillaPath()
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async discoverInstances(): Promise<MinecraftInstance[]> {
|
|
38
|
+
const dataPath = getVanillaPath()
|
|
39
|
+
if (!dataPath) return []
|
|
40
|
+
|
|
41
|
+
return [{
|
|
42
|
+
id: 'vanilla',
|
|
43
|
+
name: 'Vanilla Minecraft',
|
|
44
|
+
launcher: 'vanilla',
|
|
45
|
+
minecraftPath: dataPath,
|
|
46
|
+
}]
|
|
47
|
+
},
|
|
48
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LauncherProvider, LauncherType, DiscoveryResult } from './types.js'
|
|
2
|
+
|
|
3
|
+
const providers = new Map<LauncherType, LauncherProvider>()
|
|
4
|
+
|
|
5
|
+
/** Register a launcher provider */
|
|
6
|
+
export function registerProvider(provider: LauncherProvider): void {
|
|
7
|
+
providers.set(provider.type, provider)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Get all registered providers */
|
|
11
|
+
export function getProviders(): LauncherProvider[] {
|
|
12
|
+
return Array.from(providers.values())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Get a specific provider by type */
|
|
16
|
+
export function getProvider(type: LauncherType): LauncherProvider | undefined {
|
|
17
|
+
return providers.get(type)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Discover instances from all registered providers */
|
|
21
|
+
export async function discoverAllInstances(): Promise<DiscoveryResult> {
|
|
22
|
+
const result: DiscoveryResult = {
|
|
23
|
+
instances: [],
|
|
24
|
+
errors: new Map(),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const discoveries = await Promise.allSettled(
|
|
28
|
+
getProviders().map(async (provider) => {
|
|
29
|
+
const instances = await provider.discoverInstances()
|
|
30
|
+
return { type: provider.type, instances }
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
for (const discovery of discoveries) {
|
|
35
|
+
if (discovery.status === 'fulfilled') {
|
|
36
|
+
result.instances.push(...discovery.value.instances)
|
|
37
|
+
} else {
|
|
38
|
+
// Extract the provider type from the error if possible
|
|
39
|
+
const error = discovery.reason as Error
|
|
40
|
+
// We can't easily get the type here, so we'll handle errors differently
|
|
41
|
+
console.error('Discovery error:', error.message)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type LauncherType = 'vanilla' | 'prism' | 'modrinth'
|
|
2
|
+
|
|
3
|
+
export interface MinecraftInstance {
|
|
4
|
+
/** Unique identifier, e.g., "prism-Homestead" */
|
|
5
|
+
id: string
|
|
6
|
+
/** Display name from config or folder name */
|
|
7
|
+
name: string
|
|
8
|
+
/** Which launcher this instance belongs to */
|
|
9
|
+
launcher: LauncherType
|
|
10
|
+
/** Path to minecraft directory (where saves/ lives) */
|
|
11
|
+
minecraftPath: string
|
|
12
|
+
/** Optional game version for display */
|
|
13
|
+
version?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LauncherProvider {
|
|
17
|
+
/** Unique identifier for this launcher */
|
|
18
|
+
readonly type: LauncherType
|
|
19
|
+
/** Human-readable name for display */
|
|
20
|
+
readonly displayName: string
|
|
21
|
+
/** Check if this launcher is installed on the system */
|
|
22
|
+
isInstalled(): Promise<boolean>
|
|
23
|
+
/** Get the data path for this launcher (first valid path found) */
|
|
24
|
+
getDataPath(): string | null
|
|
25
|
+
/** Discover all instances for this launcher */
|
|
26
|
+
discoverInstances(): Promise<MinecraftInstance[]>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DiscoveryResult {
|
|
30
|
+
instances: MinecraftInstance[]
|
|
31
|
+
errors: Map<LauncherType, Error>
|
|
32
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '2.
|
|
1
|
+
export const CLI_VERSION = '2.2.0'
|
package/tsconfig.json
CHANGED