@caleb-collar/steamcmd 1.0.0-alpha.1 → 1.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.
package/src/install.js DELETED
@@ -1,356 +0,0 @@
1
- /**
2
- * @module steamcmd/install
3
- * @description Spawns SteamCMD processes to install applications and workshop items
4
- * @private
5
- */
6
-
7
- const childProcess = require('child_process')
8
- const { EventEmitter } = require('events')
9
-
10
- /**
11
- * Custom error class for installation failures
12
- * @extends Error
13
- */
14
- class InstallError extends Error {
15
- constructor (message, code, exitCode) {
16
- super(message)
17
- this.name = 'InstallError'
18
- this.code = code
19
- this.exitCode = exitCode
20
- }
21
- }
22
-
23
- /**
24
- * Validate installation options
25
- * @param {Object} options - Installation options
26
- * @throws {InstallError} If options are invalid
27
- */
28
- function validateOptions (options) {
29
- if (!options || typeof options !== 'object') {
30
- throw new InstallError('Options must be an object', 'INVALID_OPTIONS')
31
- }
32
-
33
- if (options.applicationId !== undefined) {
34
- const appId = Number(options.applicationId)
35
- if (isNaN(appId) || appId <= 0 || !Number.isInteger(appId)) {
36
- throw new InstallError(
37
- 'applicationId must be a positive integer',
38
- 'INVALID_APP_ID'
39
- )
40
- }
41
- }
42
-
43
- if (options.workshopId !== undefined) {
44
- if (!options.applicationId) {
45
- throw new InstallError(
46
- 'workshopId requires applicationId to be specified',
47
- 'MISSING_APP_ID'
48
- )
49
- }
50
- const workshopId = Number(options.workshopId)
51
- if (isNaN(workshopId) || workshopId <= 0 || !Number.isInteger(workshopId)) {
52
- throw new InstallError(
53
- 'workshopId must be a positive integer',
54
- 'INVALID_WORKSHOP_ID'
55
- )
56
- }
57
- }
58
-
59
- if (options.platform !== undefined) {
60
- const validPlatforms = ['windows', 'macos', 'linux']
61
- if (!validPlatforms.includes(options.platform)) {
62
- throw new InstallError(
63
- `platform must be one of: ${validPlatforms.join(', ')}`,
64
- 'INVALID_PLATFORM'
65
- )
66
- }
67
- }
68
-
69
- if (options.password && !options.username) {
70
- throw new InstallError(
71
- 'password requires username to be specified',
72
- 'MISSING_USERNAME'
73
- )
74
- }
75
-
76
- if (options.steamGuardCode && !options.username) {
77
- throw new InstallError(
78
- 'steamGuardCode requires username to be specified',
79
- 'MISSING_USERNAME'
80
- )
81
- }
82
- }
83
-
84
- /**
85
- * Build SteamCMD command line arguments
86
- * @param {Object} options - Installation options
87
- * @returns {string[]} Array of command line arguments
88
- */
89
- function createArguments (options) {
90
- const args = []
91
-
92
- // Force platform type for download
93
- if (options.platform) {
94
- args.push('+@sSteamCmdForcePlatformType ' + options.platform)
95
- }
96
-
97
- // Use supplied password
98
- args.push('+@NoPromptForPassword 1')
99
-
100
- // Quit on fail
101
- args.push('+@ShutdownOnFailedCommand 1')
102
-
103
- if (options.steamGuardCode) {
104
- args.push('+set_steam_guard_code ' + options.steamGuardCode)
105
- }
106
-
107
- // Authentication
108
- if (options.username && options.password) {
109
- args.push('+login ' + options.username + ' ' + options.password)
110
- } else if (options.username) {
111
- args.push('+login ' + options.username)
112
- } else {
113
- args.push('+login anonymous')
114
- }
115
-
116
- // Installation directory
117
- if (options.path) {
118
- args.push('+force_install_dir "' + options.path + '"')
119
- }
120
-
121
- // App id to install and/or validate
122
- if (options.applicationId && !options.workshopId) {
123
- args.push('+app_update ' + options.applicationId + ' validate')
124
- }
125
-
126
- // Workshop id to install and/or validate
127
- if (options.applicationId && options.workshopId) {
128
- args.push(
129
- '+workshop_download_item ' +
130
- options.applicationId +
131
- ' ' +
132
- options.workshopId
133
- )
134
- }
135
-
136
- // Quit when done
137
- args.push('+quit')
138
-
139
- return args
140
- }
141
-
142
- /**
143
- * Parse SteamCMD output for progress information
144
- * @param {string} data - Raw output from SteamCMD
145
- * @returns {Object|null} Parsed progress info or null if not progress data
146
- */
147
- function parseProgress (data) {
148
- const str = data.toString()
149
-
150
- // Match update/download progress: "Update state (0x61) downloading, progress: 45.23 (1234567890 / 2732853760)"
151
- const updateMatch = str.match(
152
- /Update state \(0x[\da-f]+\) (\w+), progress: ([\d.]+) \((\d+) \/ (\d+)\)/i
153
- )
154
- if (updateMatch) {
155
- return {
156
- phase: updateMatch[1].toLowerCase(),
157
- percent: Math.round(parseFloat(updateMatch[2])),
158
- bytesDownloaded: parseInt(updateMatch[3], 10),
159
- totalBytes: parseInt(updateMatch[4], 10)
160
- }
161
- }
162
-
163
- // Match validation progress: "Validating: 45%"
164
- const validateMatch = str.match(/Validating[^\d]*(\d+)%/i)
165
- if (validateMatch) {
166
- return {
167
- phase: 'validating',
168
- percent: parseInt(validateMatch[1], 10),
169
- bytesDownloaded: 0,
170
- totalBytes: 0
171
- }
172
- }
173
-
174
- // Match download progress: "[#### ] 45%"
175
- const percentMatch = str.match(/\[(#+\s*)\]\s*(\d+)%/i)
176
- if (percentMatch) {
177
- return {
178
- phase: 'downloading',
179
- percent: parseInt(percentMatch[2], 10),
180
- bytesDownloaded: 0,
181
- totalBytes: 0
182
- }
183
- }
184
-
185
- return null
186
- }
187
-
188
- /**
189
- * Run SteamCMD with the given options
190
- * @param {string} steamCmdPath - Path to SteamCMD executable
191
- * @param {Object} options - Installation options
192
- * @param {Function} [options.onProgress] - Progress callback(progress) with { phase, percent, bytesDownloaded, totalBytes }
193
- * @param {Function} [options.onOutput] - Output callback(data, type) where type is 'stdout' or 'stderr'
194
- * @param {Function} [callback] - Optional callback(err). If omitted, returns a Promise.
195
- * @returns {Promise<void>|undefined} Promise if no callback provided
196
- *
197
- * @example
198
- * // With progress callback
199
- * await install(execPath, {
200
- * applicationId: 740,
201
- * onProgress: (p) => console.log(`${p.phase}: ${p.percent}%`),
202
- * onOutput: (data, type) => console.log(`[${type}] ${data}`)
203
- * });
204
- */
205
- function install (steamCmdPath, options, callback) {
206
- // Support Promise-based usage
207
- if (typeof callback !== 'function') {
208
- return new Promise((resolve, reject) => {
209
- install(steamCmdPath, options, (err) => {
210
- if (err) reject(err)
211
- else resolve()
212
- })
213
- })
214
- }
215
-
216
- // Validate options
217
- try {
218
- validateOptions(options)
219
- } catch (err) {
220
- callback(err)
221
- return
222
- }
223
-
224
- // Validate steamCmdPath
225
- if (!steamCmdPath || typeof steamCmdPath !== 'string') {
226
- callback(
227
- new InstallError(
228
- 'steamCmdPath must be a non-empty string',
229
- 'INVALID_PATH'
230
- )
231
- )
232
- return
233
- }
234
-
235
- const onProgress =
236
- typeof options.onProgress === 'function' ? options.onProgress : () => {}
237
- const onOutput =
238
- typeof options.onOutput === 'function' ? options.onOutput : null
239
-
240
- const proc = childProcess.execFile(steamCmdPath, createArguments(options))
241
-
242
- let stdoutData = ''
243
- let stderrData = ''
244
-
245
- onProgress({
246
- phase: 'starting',
247
- percent: 0,
248
- bytesDownloaded: 0,
249
- totalBytes: 0
250
- })
251
-
252
- proc.stdout.on('data', (data) => {
253
- stdoutData += data
254
- if (onOutput) {
255
- onOutput(data.toString(), 'stdout')
256
- } else {
257
- console.log('stdout: ' + data)
258
- }
259
-
260
- // Parse progress from output
261
- const progress = parseProgress(data)
262
- if (progress) {
263
- onProgress(progress)
264
- }
265
- })
266
-
267
- proc.stderr.on('data', (data) => {
268
- stderrData += data
269
- if (onOutput) {
270
- onOutput(data.toString(), 'stderr')
271
- } else {
272
- console.log('stderr: ' + data)
273
- }
274
- })
275
-
276
- proc.on('error', (err) => {
277
- callback(
278
- new InstallError(
279
- `Failed to spawn SteamCMD: ${err.message}`,
280
- 'SPAWN_ERROR'
281
- )
282
- )
283
- })
284
-
285
- proc.on('close', (code) => {
286
- if (onOutput) {
287
- onOutput(`Process exited with code ${code}\n`, 'stdout')
288
- } else {
289
- console.log('child process exited with code ' + code)
290
- }
291
-
292
- if (code > 0) {
293
- const err = new InstallError(
294
- `SteamCMD exited with code ${code}`,
295
- 'EXIT_ERROR',
296
- code
297
- )
298
- err.stdout = stdoutData
299
- err.stderr = stderrData
300
- callback(err)
301
- } else {
302
- onProgress({
303
- phase: 'complete',
304
- percent: 100,
305
- bytesDownloaded: 0,
306
- totalBytes: 0
307
- })
308
- callback(null)
309
- }
310
- })
311
- }
312
-
313
- /**
314
- * Run SteamCMD with EventEmitter-based progress
315
- * @param {string} steamCmdPath - Path to SteamCMD executable
316
- * @param {Object} options - Installation options
317
- * @returns {EventEmitter} Emitter that fires 'progress', 'output', 'error', and 'complete' events
318
- *
319
- * @example
320
- * const emitter = installWithProgress(execPath, { applicationId: 740 });
321
- * emitter.on('progress', (p) => console.log(`${p.percent}%`));
322
- * emitter.on('output', (data, type) => console.log(`[${type}] ${data}`));
323
- * emitter.on('complete', () => console.log('Done!'));
324
- * emitter.on('error', (err) => console.error(err));
325
- */
326
- function installWithProgress (steamCmdPath, options) {
327
- const emitter = new EventEmitter()
328
-
329
- // Run install in next tick to allow event binding
330
- process.nextTick(() => {
331
- install(
332
- steamCmdPath,
333
- {
334
- ...options,
335
- onProgress: (progress) => emitter.emit('progress', progress),
336
- onOutput: (data, type) => emitter.emit('output', data, type)
337
- },
338
- (err) => {
339
- if (err) {
340
- emitter.emit('error', err)
341
- } else {
342
- emitter.emit('complete')
343
- }
344
- }
345
- )
346
- })
347
-
348
- return emitter
349
- }
350
-
351
- module.exports = install
352
- module.exports.installWithProgress = installWithProgress
353
- module.exports.createArguments = createArguments
354
- module.exports.validateOptions = validateOptions
355
- module.exports.parseProgress = parseProgress
356
- module.exports.InstallError = InstallError