@bobschlowinskii/clicraft 0.4.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,260 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getConfigDir } from '../config.js';
4
+
5
+ // Auth directory for storing multiple accounts
6
+ const CONFIG_DIR = getConfigDir();
7
+ const AUTH_DIR = path.join(CONFIG_DIR, 'auth');
8
+ const ACCOUNTS_FILE = path.join(AUTH_DIR, 'accounts.json');
9
+
10
+ // Ensure auth directory exists
11
+ function ensureAuthDir() {
12
+ if (!fs.existsSync(AUTH_DIR)) {
13
+ fs.mkdirSync(AUTH_DIR, { recursive: true });
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Load all accounts data
19
+ * @returns {{ accounts: Object.<string, AccountData>, currentAccount: string|null }}
20
+ */
21
+ export function loadAccounts() {
22
+ ensureAuthDir();
23
+ if (fs.existsSync(ACCOUNTS_FILE)) {
24
+ try {
25
+ const data = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8'));
26
+ return {
27
+ accounts: data.accounts || {},
28
+ currentAccount: data.currentAccount || null
29
+ };
30
+ } catch {
31
+ return { accounts: {}, currentAccount: null };
32
+ }
33
+ }
34
+ return { accounts: {}, currentAccount: null };
35
+ }
36
+
37
+ /**
38
+ * Save accounts data
39
+ * @param {{ accounts: Object, currentAccount: string|null }} data
40
+ */
41
+ export function saveAccounts(data) {
42
+ ensureAuthDir();
43
+ fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(data, null, 2));
44
+ }
45
+
46
+ /**
47
+ * Get the currently active account
48
+ * @returns {AccountData|null}
49
+ */
50
+ export function getCurrentAccount() {
51
+ const data = loadAccounts();
52
+ if (data.currentAccount && data.accounts[data.currentAccount]) {
53
+ return data.accounts[data.currentAccount];
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Get account by UUID or username
60
+ * @param {string} identifier - UUID or username
61
+ * @returns {AccountData|null}
62
+ */
63
+ export function getAccount(identifier) {
64
+ const data = loadAccounts();
65
+ const lowerIdentifier = identifier.toLowerCase();
66
+
67
+ // Try UUID first
68
+ if (data.accounts[identifier]) {
69
+ return data.accounts[identifier];
70
+ }
71
+
72
+ // Try username (case-insensitive)
73
+ for (const [uuid, account] of Object.entries(data.accounts)) {
74
+ if (account.username.toLowerCase() === lowerIdentifier) {
75
+ return account;
76
+ }
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Add or update an account
84
+ * @param {AccountData} accountData
85
+ * @param {boolean} setAsCurrent - Whether to set as current account
86
+ */
87
+ export function addAccount(accountData, setAsCurrent = true) {
88
+ const data = loadAccounts();
89
+ data.accounts[accountData.uuid] = accountData;
90
+
91
+ if (setAsCurrent || !data.currentAccount) {
92
+ data.currentAccount = accountData.uuid;
93
+ }
94
+
95
+ saveAccounts(data);
96
+ }
97
+
98
+ /**
99
+ * Remove an account
100
+ * @param {string} identifier - UUID or username
101
+ * @returns {boolean} - Whether the account was removed
102
+ */
103
+ export function removeAccount(identifier) {
104
+ const data = loadAccounts();
105
+ const lowerIdentifier = identifier.toLowerCase();
106
+
107
+ let uuidToRemove = null;
108
+
109
+ // Find by UUID
110
+ if (data.accounts[identifier]) {
111
+ uuidToRemove = identifier;
112
+ } else {
113
+ // Find by username
114
+ for (const [uuid, account] of Object.entries(data.accounts)) {
115
+ if (account.username.toLowerCase() === lowerIdentifier) {
116
+ uuidToRemove = uuid;
117
+ break;
118
+ }
119
+ }
120
+ }
121
+
122
+ if (!uuidToRemove) {
123
+ return false;
124
+ }
125
+
126
+ const removedAccount = data.accounts[uuidToRemove];
127
+ delete data.accounts[uuidToRemove];
128
+
129
+ // If we removed the current account, switch to another one
130
+ if (data.currentAccount === uuidToRemove) {
131
+ const remaining = Object.keys(data.accounts);
132
+ data.currentAccount = remaining.length > 0 ? remaining[0] : null;
133
+ }
134
+
135
+ saveAccounts(data);
136
+ return removedAccount;
137
+ }
138
+
139
+ /**
140
+ * Switch to a different account
141
+ * @param {string} identifier - UUID or username
142
+ * @returns {AccountData|null} - The account switched to, or null if not found
143
+ */
144
+ export function switchAccount(identifier) {
145
+ const data = loadAccounts();
146
+ const lowerIdentifier = identifier.toLowerCase();
147
+
148
+ let targetUuid = null;
149
+
150
+ // Find by UUID
151
+ if (data.accounts[identifier]) {
152
+ targetUuid = identifier;
153
+ } else {
154
+ // Find by username
155
+ for (const [uuid, account] of Object.entries(data.accounts)) {
156
+ if (account.username.toLowerCase() === lowerIdentifier) {
157
+ targetUuid = uuid;
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ if (!targetUuid) {
164
+ return null;
165
+ }
166
+
167
+ data.currentAccount = targetUuid;
168
+ saveAccounts(data);
169
+
170
+ return data.accounts[targetUuid];
171
+ }
172
+
173
+ /**
174
+ * Get all accounts
175
+ * @returns {AccountData[]}
176
+ */
177
+ export function getAllAccounts() {
178
+ const data = loadAccounts();
179
+ return Object.values(data.accounts);
180
+ }
181
+
182
+ /**
183
+ * Get account count
184
+ * @returns {number}
185
+ */
186
+ export function getAccountCount() {
187
+ const data = loadAccounts();
188
+ return Object.keys(data.accounts).length;
189
+ }
190
+
191
+ /**
192
+ * Check if an account exists
193
+ * @param {string} identifier - UUID or username
194
+ * @returns {boolean}
195
+ */
196
+ export function accountExists(identifier) {
197
+ return getAccount(identifier) !== null;
198
+ }
199
+
200
+ /**
201
+ * Update account data (e.g., refresh token)
202
+ * @param {string} uuid
203
+ * @param {Partial<AccountData>} updates
204
+ */
205
+ export function updateAccount(uuid, updates) {
206
+ const data = loadAccounts();
207
+ if (data.accounts[uuid]) {
208
+ data.accounts[uuid] = { ...data.accounts[uuid], ...updates };
209
+ saveAccounts(data);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Migrate from old auth.json format
215
+ */
216
+ export function migrateFromLegacy() {
217
+ const legacyFile = path.join(CONFIG_DIR, 'auth.json');
218
+
219
+ if (fs.existsSync(legacyFile)) {
220
+ try {
221
+ const legacyAuth = JSON.parse(fs.readFileSync(legacyFile, 'utf-8'));
222
+
223
+ if (legacyAuth.uuid && legacyAuth.username) {
224
+ const data = loadAccounts();
225
+
226
+ // Only migrate if not already present
227
+ if (!data.accounts[legacyAuth.uuid]) {
228
+ data.accounts[legacyAuth.uuid] = legacyAuth;
229
+ data.currentAccount = legacyAuth.uuid;
230
+ saveAccounts(data);
231
+ }
232
+
233
+ // Backup and remove legacy file
234
+ const backupFile = path.join(CONFIG_DIR, 'auth.json.backup');
235
+ fs.renameSync(legacyFile, backupFile);
236
+
237
+ return true;
238
+ }
239
+ } catch {
240
+ // Ignore migration errors
241
+ }
242
+ }
243
+
244
+ return false;
245
+ }
246
+
247
+ export default {
248
+ loadAccounts,
249
+ saveAccounts,
250
+ getCurrentAccount,
251
+ getAccount,
252
+ addAccount,
253
+ removeAccount,
254
+ switchAccount,
255
+ getAllAccounts,
256
+ getAccountCount,
257
+ accountExists,
258
+ updateAccount,
259
+ migrateFromLegacy
260
+ };
@@ -0,0 +1,258 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ // Get the .clicraft config directory path
6
+ export function getConfigDir() {
7
+ const configDir = path.join(os.homedir(), '.clicraft');
8
+ if (!fs.existsSync(configDir)) {
9
+ fs.mkdirSync(configDir, { recursive: true });
10
+ }
11
+ return configDir;
12
+ }
13
+
14
+ // Default settings
15
+ const DEFAULT_SETTINGS = {
16
+ // to implement in the future
17
+
18
+ // Default Java path (null = auto-detect)
19
+ // javaPath: null,
20
+ // Default memory allocation
21
+ // minMemory: '1G',
22
+ // maxMemory: '2G',
23
+
24
+ checkUpdates: true,
25
+ autoSaveToConfig: true,
26
+ autoLoadConfigOnLaunch: true,
27
+ settingsVersion: '0.3.0',
28
+ };
29
+
30
+ // Default game settings to apply to new instances
31
+ // These are Minecraft options.txt settings
32
+ const DEFAULT_GAME_SETTINGS = {
33
+ // Common defaults - users can customize in ~/.clicraft/default-game-settings.json
34
+ // renderDistance: 12,
35
+ // fov: 70,
36
+ // guiScale: 0,
37
+ // gamma: 0.5,
38
+ // maxFps: 120,
39
+ // lang: 'en_us'
40
+ };
41
+
42
+ // Default list of game settings to ignore when sharing configs
43
+ // These are typically user-specific or machine-specific
44
+ const DEFAULT_GAME_SETTINGS_IGNORE = [
45
+ // Window/display settings (machine-specific)
46
+ 'fullscreen',
47
+ 'overrideWidth',
48
+ 'overrideHeight',
49
+ 'fullscreenResolution',
50
+ // Account/identity (user-specific)
51
+ 'lastServer',
52
+ 'resourcePacks',
53
+ 'incompatibleResourcePacks',
54
+ // Keybinds (often personal preference, can cause issues)
55
+ 'key_*',
56
+ // Accessibility (personal)
57
+ 'narrator',
58
+ 'highContrast',
59
+ // Telemetry
60
+ 'telemetryOptInExtra',
61
+ 'onboardAccessibility'
62
+ ];
63
+
64
+
65
+
66
+ // Load global settings from ~/.clicraft/settings.json
67
+ export function loadSettings() {
68
+ const configDir = getConfigDir();
69
+ const settingsPath = path.join(configDir, 'settings.json');
70
+
71
+ if (!fs.existsSync(settingsPath)) {
72
+ // Create default settings file
73
+ saveSettings(DEFAULT_SETTINGS);
74
+ return { ...DEFAULT_SETTINGS };
75
+ }
76
+
77
+ try {
78
+ const content = fs.readFileSync(settingsPath, 'utf-8');
79
+ const userSettings = JSON.parse(content);
80
+ // Merge with defaults to ensure all keys exist
81
+ return { ...DEFAULT_SETTINGS, ...userSettings };
82
+ } catch (err) {
83
+ console.error('Warning: Could not parse settings.json, using defaults');
84
+ return { ...DEFAULT_SETTINGS };
85
+ }
86
+ }
87
+
88
+ // Save global settings to ~/.clicraft/settings.json
89
+ export function saveSettings(settings) {
90
+ const configDir = getConfigDir();
91
+ const settingsPath = path.join(configDir, 'settings.json');
92
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
93
+ }
94
+
95
+ // Load the game settings ignore list from ~/.clicraft/game-settings-ignore.json
96
+ export function loadGameSettingsIgnore() {
97
+ const configDir = getConfigDir();
98
+ const ignorePath = path.join(configDir, 'game-settings-ignore.json');
99
+
100
+ if (!fs.existsSync(ignorePath)) {
101
+ // Create default ignore file
102
+ saveGameSettingsIgnore(DEFAULT_GAME_SETTINGS_IGNORE);
103
+ return [...DEFAULT_GAME_SETTINGS_IGNORE];
104
+ }
105
+
106
+ try {
107
+ const content = fs.readFileSync(ignorePath, 'utf-8');
108
+ return JSON.parse(content);
109
+ } catch (err) {
110
+ console.error('Warning: Could not parse game-settings-ignore.json, using defaults');
111
+ return [...DEFAULT_GAME_SETTINGS_IGNORE];
112
+ }
113
+ }
114
+
115
+ // Save the game settings ignore list
116
+ export function saveGameSettingsIgnore(ignoreList) {
117
+ const configDir = getConfigDir();
118
+ const ignorePath = path.join(configDir, 'game-settings-ignore.json');
119
+ fs.writeFileSync(ignorePath, JSON.stringify(ignoreList, null, 2));
120
+ }
121
+
122
+ // Load default game settings from ~/.clicraft/default-game-settings.json
123
+ export function loadDefaultGameSettings() {
124
+ const configDir = getConfigDir();
125
+ const defaultsPath = path.join(configDir, 'default-game-settings.json');
126
+
127
+ if (!fs.existsSync(defaultsPath)) {
128
+ // Create default file with commented example
129
+ saveDefaultGameSettings(DEFAULT_GAME_SETTINGS);
130
+ return { ...DEFAULT_GAME_SETTINGS };
131
+ }
132
+
133
+ try {
134
+ const content = fs.readFileSync(defaultsPath, 'utf-8');
135
+ return JSON.parse(content);
136
+ } catch (err) {
137
+ console.error('Warning: Could not parse default-game-settings.json, using defaults');
138
+ return { ...DEFAULT_GAME_SETTINGS };
139
+ }
140
+ }
141
+
142
+ // Save default game settings
143
+ export function saveDefaultGameSettings(settings) {
144
+ const configDir = getConfigDir();
145
+ const defaultsPath = path.join(configDir, 'default-game-settings.json');
146
+ fs.writeFileSync(defaultsPath, JSON.stringify(settings, null, 2));
147
+ }
148
+
149
+ // Check if a setting key matches the ignore list (supports wildcards)
150
+ export function isSettingIgnored(key, ignoreList) {
151
+ for (const pattern of ignoreList) {
152
+ if (pattern.endsWith('*')) {
153
+ // Wildcard match
154
+ const prefix = pattern.slice(0, -1);
155
+ if (key.startsWith(prefix)) {
156
+ return true;
157
+ }
158
+ } else if (key === pattern) {
159
+ return true;
160
+ }
161
+ }
162
+ return false;
163
+ }
164
+
165
+ // Parse Minecraft options.txt into an object
166
+ export function parseOptionsTxt(content) {
167
+ const options = {};
168
+ const lines = content.split('\n');
169
+
170
+ for (const line of lines) {
171
+ const trimmed = line.trim();
172
+ if (!trimmed || !trimmed.includes(':')) continue;
173
+
174
+ const colonIndex = trimmed.indexOf(':');
175
+ const key = trimmed.substring(0, colonIndex);
176
+ const value = trimmed.substring(colonIndex + 1);
177
+
178
+ // Try to parse as number or boolean
179
+ if (value === 'true') {
180
+ options[key] = true;
181
+ } else if (value === 'false') {
182
+ options[key] = false;
183
+ } else if (!isNaN(value) && value !== '') {
184
+ options[key] = parseFloat(value);
185
+ } else {
186
+ options[key] = value;
187
+ }
188
+ }
189
+
190
+ return options;
191
+ }
192
+
193
+ // Generate options.txt content from an object
194
+ export function generateOptionsTxt(options) {
195
+ const lines = [];
196
+
197
+ for (const [key, value] of Object.entries(options)) {
198
+ lines.push(`${key}:${value}`);
199
+ }
200
+
201
+ return lines.join('\n');
202
+ }
203
+
204
+ // Extract game settings from options.txt, filtering out ignored settings
205
+ export function extractGameSettings(optionsTxtPath) {
206
+ if (!fs.existsSync(optionsTxtPath)) {
207
+ return null;
208
+ }
209
+
210
+ const content = fs.readFileSync(optionsTxtPath, 'utf-8');
211
+ const allOptions = parseOptionsTxt(content);
212
+ const ignoreList = loadGameSettingsIgnore();
213
+
214
+ const filteredOptions = {};
215
+ for (const [key, value] of Object.entries(allOptions)) {
216
+ if (!isSettingIgnored(key, ignoreList)) {
217
+ filteredOptions[key] = value;
218
+ }
219
+ }
220
+
221
+ return filteredOptions;
222
+ }
223
+
224
+ // Write game settings to options.txt in an instance
225
+ export function writeGameSettings(instancePath, gameSettings) {
226
+ if (!gameSettings || Object.keys(gameSettings).length === 0) {
227
+ return;
228
+ }
229
+
230
+ const optionsPath = path.join(instancePath, 'options.txt');
231
+
232
+ // If options.txt already exists, merge with existing settings
233
+ let existingOptions = {};
234
+ if (fs.existsSync(optionsPath)) {
235
+ const content = fs.readFileSync(optionsPath, 'utf-8');
236
+ existingOptions = parseOptionsTxt(content);
237
+ }
238
+
239
+ // Merge: gameSettings values override existing
240
+ const mergedOptions = { ...existingOptions, ...gameSettings };
241
+
242
+ fs.writeFileSync(optionsPath, generateOptionsTxt(mergedOptions));
243
+ }
244
+
245
+ export default {
246
+ getConfigDir,
247
+ loadSettings,
248
+ saveSettings,
249
+ loadGameSettingsIgnore,
250
+ saveGameSettingsIgnore,
251
+ loadDefaultGameSettings,
252
+ saveDefaultGameSettings,
253
+ isSettingIgnored,
254
+ parseOptionsTxt,
255
+ generateOptionsTxt,
256
+ extractGameSettings,
257
+ writeGameSettings
258
+ };
@@ -0,0 +1,38 @@
1
+ import version from './getv.js';
2
+
3
+ // User agent for all API requests
4
+ export const USER_AGENT = `clicraft/${version} (https://github.com/theinfamousben/clicraft)`;
5
+
6
+ // Modrinth API base URL
7
+ export const MODRINTH_API = 'https://api.modrinth.com/v2';
8
+
9
+ // Mojang/Minecraft APIs
10
+ export const MOJANG_VERSION_MANIFEST = 'https://launchermeta.mojang.com/mc/game/version_manifest.json';
11
+ export const MOJANG_RESOURCES = 'https://resources.download.minecraft.net';
12
+
13
+ // Fabric APIs
14
+ export const FABRIC_META = 'https://meta.fabricmc.net/v2';
15
+ export const FABRIC_MAVEN = 'https://maven.fabricmc.net';
16
+
17
+ // Forge APIs
18
+ export const FORGE_MAVEN = 'https://maven.minecraftforge.net';
19
+ export const FORGE_PROMOS = 'https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json';
20
+
21
+ // Pagination constants
22
+ export const PAGE_SIZE = 10;
23
+ export const NEXT_PAGE = '__NEXT_PAGE__';
24
+ export const PREV_PAGE = '__PREV_PAGE__';
25
+
26
+ export default {
27
+ USER_AGENT,
28
+ MODRINTH_API,
29
+ MOJANG_VERSION_MANIFEST,
30
+ MOJANG_RESOURCES,
31
+ FABRIC_META,
32
+ FABRIC_MAVEN,
33
+ FORGE_MAVEN,
34
+ FORGE_PROMOS,
35
+ PAGE_SIZE,
36
+ NEXT_PAGE,
37
+ PREV_PAGE
38
+ };
@@ -0,0 +1,5 @@
1
+ import pkg from '../package.json' with { type: "json" };
2
+
3
+ const version = pkg.version ?? pkg.default.version;
4
+
5
+ export default version;