sandstone-cli 2.1.2 → 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.
@@ -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.2'
1
+ export const CLI_VERSION = '2.2.0'
package/tsconfig.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "esModuleInterop": true,
16
16
  "allowSyntheticDefaultImports": true,
17
17
  "jsx": "react-jsx",
18
- "types": ["@sandstone-mc/hot-hook/import-meta"]
18
+ "types": ["bun-types", "@sandstone-mc/hot-hook/import-meta"]
19
19
  },
20
20
  "include": [
21
21
  "src/**/*"