start-command 0.25.1 → 0.25.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # start-command
2
2
 
3
+ ## 0.25.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 873781e: Record detached isolation output in the tracked log path in real time.
8
+
9
+ ## 0.25.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 8f58d2b: fix: support --session name lookups in --status and track detached session lifecycle
14
+
15
+ `--status` now accepts session names in addition to UUIDs. Detached mode no longer incorrectly reports immediate completion — status is determined at query time by checking if the actual session is still running.
16
+
3
17
  ## 0.25.1
4
18
 
5
19
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
5
5
  "main": "src/bin/cli.js",
6
6
  "exports": {
package/src/bin/cli.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  const process = require('process');
4
4
  const os = require('os');
5
- const fs = require('fs');
6
5
  const path = require('path');
7
6
 
8
7
  // Import modules
@@ -20,6 +19,7 @@ const {
20
19
  createLogHeader,
21
20
  createLogFooter,
22
21
  writeLogFile,
22
+ appendLogFile,
23
23
  createLogPath,
24
24
  getDefaultDockerImage,
25
25
  } = require('../lib/isolation');
@@ -333,7 +333,7 @@ Options:
333
333
  --auto-remove-docker-container Auto-remove docker container after exit
334
334
  --shell <shell> Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
335
335
  --use-command-stream Use command-stream library for execution (experimental)
336
- --status <uuid> Show status of execution by UUID (--output-format: links-notation|json|text)
336
+ --status <id> Show status of execution by UUID or session name (--output-format: links-notation|json|text)
337
337
  --cleanup Clean up stale "executing" records (crashed/killed processes)
338
338
  --cleanup-dry-run Show stale records that would be cleaned up (without cleaning)
339
339
  --version, -v Show version information
@@ -405,7 +405,7 @@ async function runWithIsolation(
405
405
  }
406
406
 
407
407
  // Create log file path
408
- const logFilePath = createLogPath(environment || 'direct');
408
+ const logFilePath = createLogPath(environment || 'direct', sessionId);
409
409
 
410
410
  // Get session name (will be generated by runIsolated if not provided)
411
411
  const sessionName =
@@ -468,12 +468,9 @@ async function runWithIsolation(
468
468
  }
469
469
  }
470
470
 
471
- // Add isolation info to extra lines
471
+ // Add isolation info to extra lines (session name for reconnecting, see issue #67)
472
472
  if (environment) {
473
473
  extraLines.push(`[Isolation] Environment: ${environment}, Mode: ${mode}`);
474
- // Always add the session name so users can reconnect to detached sessions
475
- // This is important for screen, tmux, docker where the session/container name
476
- // is different from the session UUID used for tracking (see issue #67)
477
474
  extraLines.push(`[Isolation] Session: ${sessionName}`);
478
475
  }
479
476
  if (effectiveImage) {
@@ -562,6 +559,8 @@ async function runWithIsolation(
562
559
  );
563
560
  }
564
561
 
562
+ writeLogFile(logFilePath, logContent);
563
+
565
564
  let result;
566
565
 
567
566
  if (environment) {
@@ -569,7 +568,7 @@ async function runWithIsolation(
569
568
  // Note: Isolation environments currently use native spawn/execSync
570
569
  // Future: Add command-stream support with raw() function for multiplexers
571
570
  result = await runIsolated(environment, cmd, {
572
- session: options.session,
571
+ session: sessionName,
573
572
  image: effectiveImage,
574
573
  endpoint: options.endpoint,
575
574
  detached: mode === 'detached',
@@ -577,6 +576,7 @@ async function runWithIsolation(
577
576
  keepAlive: options.keepAlive,
578
577
  autoRemoveDockerContainer: options.autoRemoveDockerContainer,
579
578
  shell: options.shell,
579
+ logPath: logFilePath,
580
580
  });
581
581
  } else if (createdUser) {
582
582
  // Run directly as the created user (no isolation environment)
@@ -595,16 +595,19 @@ async function runWithIsolation(
595
595
  if (!result.success && result.message) {
596
596
  console.error(`Error: ${result.message}`);
597
597
  }
598
- // Add result to log content
599
- logContent += `${result.message}\n`;
600
- logContent += createLogFooter(endTime, exitCode);
601
-
602
- // Write log file
603
- writeLogFile(logFilePath, logContent);
598
+ if (mode === 'detached' && result.success) {
599
+ appendLogFile(logFilePath, `${result.message}\n`);
600
+ } else {
601
+ // Add result to log content
602
+ appendLogFile(logFilePath, `${result.message}\n`);
603
+ appendLogFile(logFilePath, createLogFooter(endTime, exitCode));
604
+ }
604
605
 
605
- // Update execution record as completed and clear global reference
606
+ // Update execution record: detached keeps "executing" (resolved at query time)
606
607
  if (executionRecord && store) {
607
- executionRecord.complete(exitCode);
608
+ if (mode !== 'detached') {
609
+ executionRecord.complete(exitCode);
610
+ }
608
611
  try {
609
612
  store.save(executionRecord);
610
613
  } catch (err) {
@@ -614,7 +617,6 @@ async function runWithIsolation(
614
617
  );
615
618
  }
616
619
  }
617
- // Clear global reference since we've completed normally
618
620
  currentExecutionRecord = null;
619
621
  }
620
622
 
@@ -676,9 +678,7 @@ async function runDirect(cmd, sessionId) {
676
678
  const shellArgs = isWindows ? ['-Command', cmd] : ['-c', cmd];
677
679
 
678
680
  // Setup logging
679
- const logDir = config.logDir || os.tmpdir();
680
- const logFilename = generateLogFilename();
681
- const logFilePath = path.join(logDir, logFilename);
681
+ const logFilePath = createLogPath('direct', sessionId);
682
682
 
683
683
  let logContent = '';
684
684
  const startTime = getTimestamp();
@@ -827,9 +827,7 @@ async function runDirectWithCommandStream(
827
827
  const shell = isWindows ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
828
828
 
829
829
  // Setup logging
830
- const logDir = config.logDir || os.tmpdir();
831
- const logFilename = generateLogFilename();
832
- const logFilePath = path.join(logDir, logFilename);
830
+ const logFilePath = createLogPath('direct', sessionId);
833
831
 
834
832
  let logContent = '';
835
833
  const startTime = getTimestamp();
@@ -913,6 +911,7 @@ async function runDirectWithCommandStream(
913
911
 
914
912
  let exitCode = 0;
915
913
  try {
914
+ writeLogFile(logFilePath, logContent);
916
915
  // Use raw() to pass the command without auto-escaping
917
916
  // This is important for complex commands with pipes, redirects, etc.
918
917
  const result = await $cmd`${raw(cmd)}`;
@@ -926,14 +925,17 @@ async function runDirectWithCommandStream(
926
925
  // Collect output for log
927
926
  if (result.stdout) {
928
927
  logContent += result.stdout;
928
+ appendLogFile(logFilePath, result.stdout);
929
929
  }
930
930
  if (result.stderr) {
931
931
  logContent += result.stderr;
932
+ appendLogFile(logFilePath, result.stderr);
932
933
  }
933
934
  } catch (err) {
934
935
  exitCode = err.code || 1;
935
936
  const errorMessage = `Error executing command: ${err.message}`;
936
937
  logContent += `\n${errorMessage}\n`;
938
+ appendLogFile(logFilePath, `\n${errorMessage}\n`);
937
939
  console.error(`\n${errorMessage}`);
938
940
  }
939
941
 
@@ -946,7 +948,10 @@ async function runDirectWithCommandStream(
946
948
 
947
949
  // Write log file
948
950
  try {
949
- fs.writeFileSync(logFilePath, logContent, 'utf8');
951
+ appendLogFile(
952
+ logFilePath,
953
+ `\n${'='.repeat(50)}\nFinished: ${endTime}\nExit Code: ${exitCode}\n`
954
+ );
950
955
  } catch (err) {
951
956
  console.error(`\nWarning: Could not save log file: ${err.message}`);
952
957
  }
@@ -987,13 +992,3 @@ async function runDirectWithCommandStream(
987
992
 
988
993
  process.exit(exitCode);
989
994
  }
990
-
991
- /**
992
- * Generate unique log filename for direct execution
993
- * @returns {string} Log filename
994
- */
995
- function generateLogFilename() {
996
- const timestamp = Date.now();
997
- const random = Math.random().toString(36).substring(2, 8);
998
- return `start-command-${timestamp}-${random}.log`;
999
- }
@@ -418,13 +418,13 @@ function parseOption(args, index, options) {
418
418
  return 1;
419
419
  }
420
420
 
421
- // --status <uuid>
421
+ // --status <uuid-or-session-name>
422
422
  if (arg === '--status') {
423
423
  if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
424
424
  options.status = args[index + 1];
425
425
  return 2;
426
426
  } else {
427
- throw new Error(`Option ${arg} requires a UUID argument`);
427
+ throw new Error(`Option ${arg} requires a UUID or session name argument`);
428
428
  }
429
429
  }
430
430
 
@@ -470,14 +470,23 @@ class ExecutionStore {
470
470
  }
471
471
 
472
472
  /**
473
- * Get an execution record by UUID
474
- * @param {string} uuid
473
+ * Get an execution record by UUID or session name
474
+ * First tries exact UUID match, then falls back to session name lookup
475
+ * @param {string} identifier - UUID or session name
475
476
  * @returns {ExecutionRecord|null}
476
477
  */
477
- get(uuid) {
478
+ get(identifier) {
478
479
  const records = this.readLinoRecords();
479
- const found = records.find((r) => r.uuid === uuid);
480
- return found || null;
480
+ // First try exact UUID match
481
+ const byUuid = records.find((r) => r.uuid === identifier);
482
+ if (byUuid) {
483
+ return byUuid;
484
+ }
485
+ // Fall back to session name lookup (stored in options.sessionName)
486
+ const bySessionName = records.find(
487
+ (r) => r.options && r.options.sessionName === identifier
488
+ );
489
+ return bySessionName || null;
481
490
  }
482
491
 
483
492
  /**
@@ -5,6 +5,18 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
7
 
8
+ function getTempRoot() {
9
+ return process.env.START_TEMP_ROOT || path.join(os.tmpdir(), 'start-command');
10
+ }
11
+
12
+ function ensureDirectory(dirPath) {
13
+ fs.mkdirSync(dirPath, { recursive: true });
14
+ }
15
+
16
+ function ensureParentDirectory(filePath) {
17
+ ensureDirectory(path.dirname(filePath));
18
+ }
19
+
8
20
  /**
9
21
  * Generate timestamp for logging
10
22
  * @returns {string} ISO timestamp without 'T' and 'Z'
@@ -77,6 +89,7 @@ function createLogFooter(endTime, exitCode) {
77
89
  */
78
90
  function writeLogFile(logPath, content) {
79
91
  try {
92
+ ensureParentDirectory(logPath);
80
93
  fs.writeFileSync(logPath, content, 'utf8');
81
94
  return true;
82
95
  } catch (err) {
@@ -85,23 +98,79 @@ function writeLogFile(logPath, content) {
85
98
  }
86
99
  }
87
100
 
101
+ /**
102
+ * Append to a log file, creating its parent directory when needed.
103
+ * @param {string} logPath - Path to log file
104
+ * @param {string} content - Log content to append
105
+ * @returns {boolean} Success status
106
+ */
107
+ function appendLogFile(logPath, content) {
108
+ try {
109
+ ensureParentDirectory(logPath);
110
+ fs.appendFileSync(logPath, content, 'utf8');
111
+ return true;
112
+ } catch (err) {
113
+ console.error(`\nWarning: Could not append log file: ${err.message}`);
114
+ return false;
115
+ }
116
+ }
117
+
88
118
  /**
89
119
  * Get log directory from environment or use system temp
90
120
  * @returns {string} Log directory path
91
121
  */
92
122
  function getLogDir() {
93
- return process.env.START_LOG_DIR || os.tmpdir();
123
+ return process.env.START_LOG_DIR || path.join(getTempRoot(), 'logs');
124
+ }
125
+
126
+ /**
127
+ * Get a start-command temporary directory for sidecar files.
128
+ * @param {...string} segments - Optional path segments below the temp directory
129
+ * @returns {string} Directory path
130
+ */
131
+ function getTempDir(...segments) {
132
+ const dir = path.join(getTempRoot(), 'tmp', ...segments);
133
+ ensureDirectory(dir);
134
+ return dir;
94
135
  }
95
136
 
96
137
  /**
97
138
  * Create log file path
98
139
  * @param {string} environment - The isolation environment
140
+ * @param {string|null} executionId - Optional execution UUID/session ID
99
141
  * @returns {string} Full path to log file
100
142
  */
101
- function createLogPath(environment) {
143
+ function createLogPath(environment, executionId = null) {
102
144
  const logDir = getLogDir();
145
+ if (executionId) {
146
+ return environment === 'direct'
147
+ ? path.join(logDir, 'direct', `${executionId}.log`)
148
+ : path.join(logDir, 'isolation', environment, `${executionId}.log`);
149
+ }
103
150
  const logFilename = generateLogFilename(environment);
104
- return path.join(logDir, logFilename);
151
+ return environment === 'direct'
152
+ ? path.join(logDir, 'direct', logFilename)
153
+ : path.join(logDir, 'isolation', environment, logFilename);
154
+ }
155
+
156
+ function shellQuote(value) {
157
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
158
+ }
159
+
160
+ function createShellLogFooterSnippet() {
161
+ const dateCommand =
162
+ "date '+%Y-%m-%d %H:%M:%S.%3N' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S'";
163
+ return `printf '\\n==================================================\\nFinished: %s\\nExit Code: %s\\n' "$(${dateCommand})" "$__start_command_exit"`;
164
+ }
165
+
166
+ function wrapCommandWithLogFooter(command, options = {}) {
167
+ const shell = options.shell || 'sh';
168
+ const keepAlive = Boolean(options.keepAlive);
169
+ const footer = createShellLogFooterSnippet();
170
+ const afterFooter = keepAlive
171
+ ? `exec ${shellQuote(shell)}`
172
+ : 'exit "$__start_command_exit"';
173
+ return `(${command}); __start_command_exit=$?; ${footer}; ${afterFooter}`;
105
174
  }
106
175
 
107
176
  /**
@@ -136,12 +205,20 @@ function runAsIsolatedUser(cmd, username) {
136
205
  }
137
206
 
138
207
  module.exports = {
208
+ getTempRoot,
209
+ ensureDirectory,
210
+ ensureParentDirectory,
139
211
  getTimestamp,
140
212
  generateLogFilename,
141
213
  createLogHeader,
142
214
  createLogFooter,
143
215
  writeLogFile,
216
+ appendLogFile,
144
217
  getLogDir,
218
+ getTempDir,
145
219
  createLogPath,
220
+ shellQuote,
221
+ createShellLogFooterSnippet,
222
+ wrapCommandWithLogFooter,
146
223
  runAsIsolatedUser,
147
224
  };
@@ -13,8 +13,15 @@ const {
13
13
  getScreenVersion,
14
14
  supportsLogfileOption,
15
15
  runScreenWithLogCapture: _runScreenWithLogCapture,
16
+ startDetachedScreenWithLogCapture,
16
17
  resetScreenVersionCache,
17
18
  } = require('./screen-isolation');
19
+ const {
20
+ appendLogFile,
21
+ createShellLogFooterSnippet,
22
+ shellQuote,
23
+ wrapCommandWithLogFooter,
24
+ } = require('./isolation-log-utils');
18
25
 
19
26
  /**
20
27
  * Check if a command is available on the system
@@ -170,14 +177,21 @@ function wrapCommandWithUser(command, user) {
170
177
  * @param {string|null} user - Username to run command as (optional)
171
178
  * @returns {Promise<{success: boolean, sessionName: string, message: string, output: string}>}
172
179
  */
173
- function runScreenWithLogCapture(command, sessionName, shellInfo, user = null) {
180
+ function runScreenWithLogCapture(
181
+ command,
182
+ sessionName,
183
+ shellInfo,
184
+ user = null,
185
+ logPath = null
186
+ ) {
174
187
  return _runScreenWithLogCapture(
175
188
  command,
176
189
  sessionName,
177
190
  shellInfo,
178
191
  user,
179
192
  wrapCommandWithUser,
180
- isInteractiveShellCommand
193
+ isInteractiveShellCommand,
194
+ logPath
181
195
  );
182
196
  }
183
197
 
@@ -199,54 +213,24 @@ function runInScreen(command, options = {}) {
199
213
 
200
214
  const sessionName = options.session || generateSessionName('screen');
201
215
  const shellInfo = getShell();
202
- const { shell, shellArg } = shellInfo;
203
216
 
204
217
  try {
205
- // Wrap command with user switch if specified
206
- let effectiveCommand = wrapCommandWithUser(command, options.user);
207
-
208
218
  if (options.detached) {
209
- if (options.keepAlive) {
210
- effectiveCommand = `${effectiveCommand}; exec ${shell}`;
211
- }
212
-
213
- const screenArgs = isInteractiveShellCommand(command)
214
- ? ['-dmS', sessionName, ...command.trim().split(/\s+/)]
215
- : ['-dmS', sessionName, shell, shellArg, effectiveCommand];
216
-
217
- if (DEBUG) {
218
- console.log(`[DEBUG] Running: screen ${screenArgs.join(' ')}`);
219
- console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
220
- }
221
-
222
- // Use spawnSync with array args (not execSync string) to avoid quoting issues (issue #25)
223
- const result = spawnSync('screen', screenArgs, {
224
- stdio: 'inherit',
225
- });
226
-
227
- if (result.error) {
228
- throw result.error;
229
- }
230
-
231
- let message = `Command started in detached screen session: ${sessionName}`;
232
- if (options.keepAlive) {
233
- message += `\nSession will stay alive after command completes.`;
234
- } else {
235
- message += `\nSession will exit automatically after command completes.`;
236
- }
237
- message += `\nReattach with: screen -r ${sessionName}`;
238
-
239
- return Promise.resolve({
240
- success: true,
241
- sessionName,
242
- message,
243
- });
219
+ return Promise.resolve(
220
+ startDetachedScreenWithLogCapture(command, sessionName, shellInfo, {
221
+ user: options.user,
222
+ keepAlive: options.keepAlive,
223
+ logPath: options.logPath,
224
+ wrapCommandWithUser,
225
+ })
226
+ );
244
227
  } else {
245
228
  return runScreenWithLogCapture(
246
229
  command,
247
230
  sessionName,
248
231
  shellInfo,
249
- options.user
232
+ options.user,
233
+ options.logPath
250
234
  );
251
235
  }
252
236
  } catch (err) {
@@ -294,12 +278,40 @@ function runInTmux(command, options = {}) {
294
278
  console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
295
279
  }
296
280
 
297
- execSync(
298
- `tmux new-session -d -s "${sessionName}" "${effectiveCommand}"`,
299
- {
281
+ if (options.logPath) {
282
+ const loggedCommand = wrapCommandWithLogFooter(effectiveCommand, {
283
+ shell,
284
+ keepAlive: options.keepAlive,
285
+ });
286
+ spawnSync('tmux', ['new-session', '-d', '-s', sessionName, shell], {
300
287
  stdio: 'inherit',
301
- }
302
- );
288
+ });
289
+ spawnSync(
290
+ 'tmux',
291
+ [
292
+ 'pipe-pane',
293
+ '-t',
294
+ sessionName,
295
+ '-o',
296
+ `cat >> ${shellQuote(options.logPath)}`,
297
+ ],
298
+ { stdio: 'inherit' }
299
+ );
300
+ spawnSync(
301
+ 'tmux',
302
+ ['send-keys', '-t', sessionName, loggedCommand, 'C-m'],
303
+ {
304
+ stdio: 'inherit',
305
+ }
306
+ );
307
+ } else {
308
+ execSync(
309
+ `tmux new-session -d -s "${sessionName}" "${effectiveCommand}"`,
310
+ {
311
+ stdio: 'inherit',
312
+ }
313
+ );
314
+ }
303
315
 
304
316
  let message = `Command started in detached tmux session: ${sessionName}`;
305
317
  if (options.keepAlive) {
@@ -401,8 +413,8 @@ function runInSsh(command, options = {}) {
401
413
  ? `${remoteShell} ${shellInteractiveFlag}`
402
414
  : remoteShell;
403
415
  const remoteCommand = isInteractiveShellCommand(command)
404
- ? `nohup ${command} > /tmp/${sessionName}.log 2>&1 &`
405
- : `nohup ${shellInvocation} -c ${JSON.stringify(command)} > /tmp/${sessionName}.log 2>&1 &`;
416
+ ? `mkdir -p /tmp/start-command/logs/isolation/ssh && nohup ${command} > /tmp/start-command/logs/isolation/ssh/${sessionName}.log 2>&1 &`
417
+ : `mkdir -p /tmp/start-command/logs/isolation/ssh && nohup ${shellInvocation} -c ${JSON.stringify(command)} > /tmp/start-command/logs/isolation/ssh/${sessionName}.log 2>&1 &`;
406
418
  const sshArgs = [sshTarget, remoteCommand];
407
419
 
408
420
  if (DEBUG) {
@@ -419,7 +431,7 @@ function runInSsh(command, options = {}) {
419
431
  return Promise.resolve({
420
432
  success: true,
421
433
  sessionName,
422
- message: `Command started in detached SSH session on ${sshTarget}\nSession: ${sessionName}\nView logs: ssh ${sshTarget} "tail -f /tmp/${sessionName}.log"`,
434
+ message: `Command started in detached SSH session on ${sshTarget}\nSession: ${sessionName}\nView logs: ssh ${sshTarget} "tail -f /tmp/start-command/logs/isolation/ssh/${sessionName}.log"`,
423
435
  });
424
436
  } else {
425
437
  const extraFlags = shellInteractiveFlag ? [shellInteractiveFlag] : [];
@@ -568,6 +580,19 @@ function runInDocker(command, options = {}) {
568
580
  encoding: 'utf8',
569
581
  }).trim();
570
582
 
583
+ if (options.logPath) {
584
+ const loggerScript = [
585
+ `docker logs -f ${shellQuote(containerName)} >> ${shellQuote(options.logPath)} 2>&1`,
586
+ `__start_command_exit=$(docker inspect -f '{{.State.ExitCode}}' ${shellQuote(containerName)} 2>/dev/null || printf '%s' '-1')`,
587
+ `${createShellLogFooterSnippet()} >> ${shellQuote(options.logPath)}`,
588
+ ].join('; ');
589
+ const logger = spawn('sh', ['-c', loggerScript], {
590
+ detached: true,
591
+ stdio: 'ignore',
592
+ });
593
+ logger.unref();
594
+ }
595
+
571
596
  let message = `Command started in detached docker container: ${containerName}`;
572
597
  message += `\nContainer ID: ${containerId.substring(0, 12)}`;
573
598
  if (options.keepAlive) {
@@ -582,6 +607,9 @@ function runInDocker(command, options = {}) {
582
607
  }
583
608
  message += `\nAttach with: docker attach ${containerName}`;
584
609
  message += `\nView logs: docker logs ${containerName}`;
610
+ if (options.logPath) {
611
+ message += `\nLive log: ${options.logPath}`;
612
+ }
585
613
 
586
614
  return Promise.resolve({
587
615
  success: true,
@@ -696,6 +724,7 @@ function runIsolated(backend, command, options = {}) {
696
724
  ? options.endpointStack[0]
697
725
  : options.endpoint,
698
726
  session: options.sessionStack ? options.sessionStack[0] : options.session,
727
+ logPath: options.logPath,
699
728
  };
700
729
 
701
730
  switch (backend) {
@@ -746,6 +775,7 @@ module.exports = {
746
775
  createLogHeader,
747
776
  createLogFooter,
748
777
  writeLogFile,
778
+ appendLogFile,
749
779
  getLogDir,
750
780
  createLogPath,
751
781
  getScreenVersion,