dcs-git-utils 1.0.0 → 1.0.9
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/.github/workflows/pr.yml +33 -33
- package/.github/workflows/release.yml +71 -64
- package/.vscode/launch.json +22 -22
- package/LICENSE +21 -21
- package/README.md +24 -15
- package/dist/compression.js +119 -109
- package/dist/compression.js.map +1 -1
- package/dist/config.js +149 -139
- package/dist/config.js.map +1 -1
- package/dist/fengari-wrapper.js +88 -78
- package/dist/fengari-wrapper.js.map +1 -1
- package/dist/files/file-picker.js +75 -76
- package/dist/files/file-picker.js.map +1 -1
- package/dist/files/miz-selector.js +23 -23
- package/dist/files/miz-selector.js.map +1 -1
- package/dist/lua +72 -72
- package/dist/main.js +51 -48
- package/dist/main.js.map +1 -1
- package/dist/sorting.js +153 -143
- package/dist/sorting.js.map +1 -1
- package/dist/watchers.js +92 -82
- package/dist/watchers.js.map +1 -1
- package/lua/sorter_lib.lua +72 -72
- package/package.json +61 -62
- package/src/compression.ts +87 -87
- package/src/config.ts +119 -119
- package/src/fengari-wrapper.ts +67 -67
- package/src/files/file-picker.ts +63 -63
- package/src/files/miz-selector.ts +9 -9
- package/src/main.ts +53 -49
- package/src/sorting.ts +127 -127
- package/src/watchers.ts +57 -57
- package/tests/sorting.spec.ts +25 -25
- package/tsconfig.json +100 -100
- package/vitest.config.ts +9 -9
package/src/config.ts
CHANGED
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
|
-
import { getMizPath } from './files/miz-selector';
|
|
5
|
-
|
|
6
|
-
interface AppConfig {
|
|
7
|
-
lastMissionPath: string | null;
|
|
8
|
-
backupEnabled: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// 2. Define Defaults (Used if file is missing or partial)
|
|
12
|
-
const DEFAULT_CONFIG: AppConfig = {
|
|
13
|
-
lastMissionPath: null,
|
|
14
|
-
backupEnabled: true
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export function saveConfig(data: AppConfig) {
|
|
18
|
-
// 1. Determine the directory of the Executable
|
|
19
|
-
// Check if we are running inside a 'pkg' executable
|
|
20
|
-
const isPkg = (process as any).pkg !== undefined;
|
|
21
|
-
|
|
22
|
-
const exeDir = isPkg
|
|
23
|
-
? path.dirname(process.execPath) // PROD: The folder containing .exe
|
|
24
|
-
: process.cwd(); // DEV: The folder where you ran 'npm start'
|
|
25
|
-
|
|
26
|
-
// 2. Construct the full path
|
|
27
|
-
const configPath = path.join(exeDir, 'config.json');
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
// 3. Write the file
|
|
31
|
-
fs.writeFileSync(configPath, JSON.stringify(data, null, 4), 'utf8');
|
|
32
|
-
console.log(`Config saved to: ${configPath}`);
|
|
33
|
-
} catch (err) {
|
|
34
|
-
console.error(`Failed to write config`, err);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
export function readConfig(): AppConfig {
|
|
41
|
-
// A. Determine the directory (Compatible with 'pkg')
|
|
42
|
-
const isPkg = (process as any).pkg !== undefined;
|
|
43
|
-
const exeDir = isPkg
|
|
44
|
-
? path.dirname(process.execPath) // PROD: Next to the .exe
|
|
45
|
-
: process.cwd(); // DEV: Current working directory
|
|
46
|
-
|
|
47
|
-
const configPath = path.join(exeDir, 'config.json');
|
|
48
|
-
|
|
49
|
-
// B. Check if file exists
|
|
50
|
-
if (!fs.existsSync(configPath)) {
|
|
51
|
-
console.log("No config found. Using defaults.");
|
|
52
|
-
return DEFAULT_CONFIG;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
// C. Read and Parse
|
|
57
|
-
const rawData = fs.readFileSync(configPath, 'utf8');
|
|
58
|
-
const userConfig = JSON.parse(rawData);
|
|
59
|
-
|
|
60
|
-
// D. Merge with Defaults
|
|
61
|
-
// (Ensures your app doesn't crash if the user deletes a key from the JSON)
|
|
62
|
-
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
63
|
-
|
|
64
|
-
} catch (err) {
|
|
65
|
-
console.error(`Error reading config (corrupt JSON?)`, err);
|
|
66
|
-
console.log("Reverting to default configuration.");
|
|
67
|
-
return DEFAULT_CONFIG;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export const initializeConfig = async (options?: { file?: string }) => {
|
|
72
|
-
const config = readConfig();
|
|
73
|
-
let missionPath = options?.file ?? "";
|
|
74
|
-
let outDir = "";
|
|
75
|
-
|
|
76
|
-
if (missionPath) {
|
|
77
|
-
const mizDir = missionPath.replace(path.basename(missionPath), "");
|
|
78
|
-
outDir = path.join(mizDir, "out");
|
|
79
|
-
}
|
|
80
|
-
else if (config.lastMissionPath === null) {
|
|
81
|
-
const { mizPath, dir } = await getMizPath();
|
|
82
|
-
missionPath = mizPath;
|
|
83
|
-
outDir = path.join(dir, "out");
|
|
84
|
-
} else {
|
|
85
|
-
missionPath = config.lastMissionPath;
|
|
86
|
-
const { action } = await inquirer.prompt([
|
|
87
|
-
{
|
|
88
|
-
type: 'rawlist',
|
|
89
|
-
name: 'action',
|
|
90
|
-
message: 'Target file already exists. What would you like to do?',
|
|
91
|
-
choices: [
|
|
92
|
-
{
|
|
93
|
-
name: `Reuse the existing path: ${config.lastMissionPath}`,
|
|
94
|
-
value: 'reusePath'
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: "Select new .miz",
|
|
98
|
-
value: 'selectMiz'
|
|
99
|
-
}
|
|
100
|
-
]
|
|
101
|
-
}
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
|
-
const mizDir = missionPath.replace(path.basename(missionPath), "");
|
|
105
|
-
|
|
106
|
-
outDir = path.join(mizDir, "out");
|
|
107
|
-
|
|
108
|
-
if (action === "selectMiz") {
|
|
109
|
-
const { mizPath, dir } = await getMizPath(mizDir);
|
|
110
|
-
missionPath = mizPath;
|
|
111
|
-
outDir = path.join(dir, "out");;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
saveConfig({ ...config, lastMissionPath: missionPath });
|
|
118
|
-
|
|
119
|
-
return { missionPath, outDir };
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { getMizPath } from './files/miz-selector';
|
|
5
|
+
|
|
6
|
+
interface AppConfig {
|
|
7
|
+
lastMissionPath: string | null;
|
|
8
|
+
backupEnabled: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 2. Define Defaults (Used if file is missing or partial)
|
|
12
|
+
const DEFAULT_CONFIG: AppConfig = {
|
|
13
|
+
lastMissionPath: null,
|
|
14
|
+
backupEnabled: true
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function saveConfig(data: AppConfig) {
|
|
18
|
+
// 1. Determine the directory of the Executable
|
|
19
|
+
// Check if we are running inside a 'pkg' executable
|
|
20
|
+
const isPkg = (process as any).pkg !== undefined;
|
|
21
|
+
|
|
22
|
+
const exeDir = isPkg
|
|
23
|
+
? path.dirname(process.execPath) // PROD: The folder containing .exe
|
|
24
|
+
: process.cwd(); // DEV: The folder where you ran 'npm start'
|
|
25
|
+
|
|
26
|
+
// 2. Construct the full path
|
|
27
|
+
const configPath = path.join(exeDir, 'config.json');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// 3. Write the file
|
|
31
|
+
fs.writeFileSync(configPath, JSON.stringify(data, null, 4), 'utf8');
|
|
32
|
+
console.log(`Config saved to: ${configPath}`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`Failed to write config`, err);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export function readConfig(): AppConfig {
|
|
41
|
+
// A. Determine the directory (Compatible with 'pkg')
|
|
42
|
+
const isPkg = (process as any).pkg !== undefined;
|
|
43
|
+
const exeDir = isPkg
|
|
44
|
+
? path.dirname(process.execPath) // PROD: Next to the .exe
|
|
45
|
+
: process.cwd(); // DEV: Current working directory
|
|
46
|
+
|
|
47
|
+
const configPath = path.join(exeDir, 'config.json');
|
|
48
|
+
|
|
49
|
+
// B. Check if file exists
|
|
50
|
+
if (!fs.existsSync(configPath)) {
|
|
51
|
+
console.log("No config found. Using defaults.");
|
|
52
|
+
return DEFAULT_CONFIG;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// C. Read and Parse
|
|
57
|
+
const rawData = fs.readFileSync(configPath, 'utf8');
|
|
58
|
+
const userConfig = JSON.parse(rawData);
|
|
59
|
+
|
|
60
|
+
// D. Merge with Defaults
|
|
61
|
+
// (Ensures your app doesn't crash if the user deletes a key from the JSON)
|
|
62
|
+
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
63
|
+
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`Error reading config (corrupt JSON?)`, err);
|
|
66
|
+
console.log("Reverting to default configuration.");
|
|
67
|
+
return DEFAULT_CONFIG;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const initializeConfig = async (options?: { file?: string }) => {
|
|
72
|
+
const config = readConfig();
|
|
73
|
+
let missionPath = options?.file ?? "";
|
|
74
|
+
let outDir = "";
|
|
75
|
+
|
|
76
|
+
if (missionPath) {
|
|
77
|
+
const mizDir = missionPath.replace(path.basename(missionPath), "");
|
|
78
|
+
outDir = path.join(mizDir, "out");
|
|
79
|
+
}
|
|
80
|
+
else if (config.lastMissionPath === null) {
|
|
81
|
+
const { mizPath, dir } = await getMizPath();
|
|
82
|
+
missionPath = mizPath;
|
|
83
|
+
outDir = path.join(dir, "out");
|
|
84
|
+
} else {
|
|
85
|
+
missionPath = config.lastMissionPath;
|
|
86
|
+
const { action } = await inquirer.prompt([
|
|
87
|
+
{
|
|
88
|
+
type: 'rawlist',
|
|
89
|
+
name: 'action',
|
|
90
|
+
message: 'Target file already exists. What would you like to do?',
|
|
91
|
+
choices: [
|
|
92
|
+
{
|
|
93
|
+
name: `Reuse the existing path: ${config.lastMissionPath}`,
|
|
94
|
+
value: 'reusePath'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "Select new .miz",
|
|
98
|
+
value: 'selectMiz'
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const mizDir = missionPath.replace(path.basename(missionPath), "");
|
|
105
|
+
|
|
106
|
+
outDir = path.join(mizDir, "out");
|
|
107
|
+
|
|
108
|
+
if (action === "selectMiz") {
|
|
109
|
+
const { mizPath, dir } = await getMizPath(mizDir);
|
|
110
|
+
missionPath = mizPath;
|
|
111
|
+
outDir = path.join(dir, "out");;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
saveConfig({ ...config, lastMissionPath: missionPath });
|
|
118
|
+
|
|
119
|
+
return { missionPath, outDir };
|
|
120
120
|
}
|
package/src/fengari-wrapper.ts
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as fengari from 'fengari';
|
|
4
|
-
|
|
5
|
-
const lua = fengari.lua;
|
|
6
|
-
const lauxlib = fengari.lauxlib;
|
|
7
|
-
const lualib = fengari.lualib;
|
|
8
|
-
const to_luastring = fengari.to_luastring;
|
|
9
|
-
|
|
10
|
-
const scriptPath = path.join(__dirname, "../lua/sorter_lib.lua");
|
|
11
|
-
|
|
12
|
-
const runSorter = (dataFilePath: string, varName: string) => {
|
|
13
|
-
// 1. Read the Data File
|
|
14
|
-
const dataContent = fs.readFileSync(dataFilePath, 'utf8');
|
|
15
|
-
|
|
16
|
-
// --- DETECT LINE ENDINGS ---
|
|
17
|
-
// Check if the file contains Carriage Return + Line Feed
|
|
18
|
-
const useCRLF = dataContent.includes("\r\n");
|
|
19
|
-
const EOL = useCRLF ? "\r\n" : "\n";
|
|
20
|
-
// ---------------------------
|
|
21
|
-
|
|
22
|
-
const L = lauxlib.luaL_newstate();
|
|
23
|
-
lualib.luaL_openlibs(L);
|
|
24
|
-
|
|
25
|
-
// 2. Load Data into VM
|
|
26
|
-
// Strip shebang just in case, convert to Lua String
|
|
27
|
-
const cleanContent = dataContent.replace(/^#!\/.*\n/, "");
|
|
28
|
-
let status = lauxlib.luaL_dostring(L, to_luastring(cleanContent));
|
|
29
|
-
|
|
30
|
-
if (status !== lua.LUA_OK) {
|
|
31
|
-
throw new Error(`Lua Load Error: ${lua.lua_tojsstring(L, -1)}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 3. Load Sorter Logic
|
|
35
|
-
const scriptContent = fs.readFileSync(scriptPath, 'utf8');
|
|
36
|
-
status = lauxlib.luaL_loadstring(L, to_luastring(scriptContent));
|
|
37
|
-
|
|
38
|
-
if (status !== lua.LUA_OK) {
|
|
39
|
-
throw new Error(`Script Load Error: ${lua.lua_tojsstring(L, -1)}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 4. Run Sorter
|
|
43
|
-
lua.lua_pushstring(L, varName);
|
|
44
|
-
status = lua.lua_pcall(L, 1, 2, 0);
|
|
45
|
-
|
|
46
|
-
if (status !== lua.LUA_OK) {
|
|
47
|
-
throw new Error(`Runtime Error: ${lua.lua_tojsstring(L, -1)}`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const errorMsg = lua.lua_tojsstring(L, -1);
|
|
51
|
-
let resultString = lua.lua_tojsstring(L, -2); // This will contain \n
|
|
52
|
-
|
|
53
|
-
if (!resultString && errorMsg) {
|
|
54
|
-
throw new Error(`Logic Error: ${errorMsg}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
resultString = resultString.replace(/\r?\n/g, EOL);
|
|
58
|
-
|
|
59
|
-
// 5. Write back
|
|
60
|
-
fs.writeFileSync(dataFilePath, resultString);
|
|
61
|
-
console.log(`Success: Sorted ${varName}`);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export const fengariSortFile = (filepath: string) => {
|
|
65
|
-
const targetVar = path.basename(filepath);
|
|
66
|
-
runSorter(filepath, targetVar);
|
|
67
|
-
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fengari from 'fengari';
|
|
4
|
+
|
|
5
|
+
const lua = fengari.lua;
|
|
6
|
+
const lauxlib = fengari.lauxlib;
|
|
7
|
+
const lualib = fengari.lualib;
|
|
8
|
+
const to_luastring = fengari.to_luastring;
|
|
9
|
+
|
|
10
|
+
const scriptPath = path.join(__dirname, "../lua/sorter_lib.lua");
|
|
11
|
+
|
|
12
|
+
const runSorter = (dataFilePath: string, varName: string) => {
|
|
13
|
+
// 1. Read the Data File
|
|
14
|
+
const dataContent = fs.readFileSync(dataFilePath, 'utf8');
|
|
15
|
+
|
|
16
|
+
// --- DETECT LINE ENDINGS ---
|
|
17
|
+
// Check if the file contains Carriage Return + Line Feed
|
|
18
|
+
const useCRLF = dataContent.includes("\r\n");
|
|
19
|
+
const EOL = useCRLF ? "\r\n" : "\n";
|
|
20
|
+
// ---------------------------
|
|
21
|
+
|
|
22
|
+
const L = lauxlib.luaL_newstate();
|
|
23
|
+
lualib.luaL_openlibs(L);
|
|
24
|
+
|
|
25
|
+
// 2. Load Data into VM
|
|
26
|
+
// Strip shebang just in case, convert to Lua String
|
|
27
|
+
const cleanContent = dataContent.replace(/^#!\/.*\n/, "");
|
|
28
|
+
let status = lauxlib.luaL_dostring(L, to_luastring(cleanContent));
|
|
29
|
+
|
|
30
|
+
if (status !== lua.LUA_OK) {
|
|
31
|
+
throw new Error(`Lua Load Error: ${lua.lua_tojsstring(L, -1)}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 3. Load Sorter Logic
|
|
35
|
+
const scriptContent = fs.readFileSync(scriptPath, 'utf8');
|
|
36
|
+
status = lauxlib.luaL_loadstring(L, to_luastring(scriptContent));
|
|
37
|
+
|
|
38
|
+
if (status !== lua.LUA_OK) {
|
|
39
|
+
throw new Error(`Script Load Error: ${lua.lua_tojsstring(L, -1)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 4. Run Sorter
|
|
43
|
+
lua.lua_pushstring(L, varName);
|
|
44
|
+
status = lua.lua_pcall(L, 1, 2, 0);
|
|
45
|
+
|
|
46
|
+
if (status !== lua.LUA_OK) {
|
|
47
|
+
throw new Error(`Runtime Error: ${lua.lua_tojsstring(L, -1)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const errorMsg = lua.lua_tojsstring(L, -1);
|
|
51
|
+
let resultString = lua.lua_tojsstring(L, -2); // This will contain \n
|
|
52
|
+
|
|
53
|
+
if (!resultString && errorMsg) {
|
|
54
|
+
throw new Error(`Logic Error: ${errorMsg}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resultString = resultString.replace(/\r?\n/g, EOL);
|
|
58
|
+
|
|
59
|
+
// 5. Write back
|
|
60
|
+
fs.writeFileSync(dataFilePath, resultString);
|
|
61
|
+
console.log(`Success: Sorted ${varName}`);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const fengariSortFile = (filepath: string) => {
|
|
65
|
+
const targetVar = path.basename(filepath);
|
|
66
|
+
runSorter(filepath, targetVar);
|
|
67
|
+
|
|
68
68
|
}
|
package/src/files/file-picker.ts
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import os from "node:os"
|
|
3
|
-
|
|
4
|
-
export async function pickMissionFileNative(): Promise<string> {
|
|
5
|
-
const platform = os.platform();
|
|
6
|
-
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
let cmd = '';
|
|
9
|
-
let args: string[] = [];
|
|
10
|
-
|
|
11
|
-
if (platform === 'win32') {
|
|
12
|
-
// WINDOWS: Use PowerShell with .NET Forms
|
|
13
|
-
cmd = 'powershell';
|
|
14
|
-
const psScript = `
|
|
15
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
16
|
-
$f = New-Object System.Windows.Forms.OpenFileDialog
|
|
17
|
-
$f.Filter = "Mission Files (*.miz)|*.miz"
|
|
18
|
-
$f.Title = "Select a DCS Mission File"
|
|
19
|
-
$f.ShowHelp = $true
|
|
20
|
-
$result = $f.ShowDialog()
|
|
21
|
-
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
22
|
-
Write-Host $f.FileName
|
|
23
|
-
}
|
|
24
|
-
`;
|
|
25
|
-
// Encode command to avoid escaping issues
|
|
26
|
-
args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', psScript];
|
|
27
|
-
}
|
|
28
|
-
else if (platform === 'linux') {
|
|
29
|
-
// LINUX: Use Zenity (Common on Ubuntu/Desktop Linux)
|
|
30
|
-
cmd = 'zenity';
|
|
31
|
-
args = ['--file-selection', '--file-filter=*.miz', '--title=Select a Mission File'];
|
|
32
|
-
}
|
|
33
|
-
else if (platform === 'darwin') {
|
|
34
|
-
// MAC: Use AppleScript
|
|
35
|
-
cmd = 'osascript';
|
|
36
|
-
const appleScript = `
|
|
37
|
-
set theFile to choose file with prompt "Select a Mission File" of type {"miz"}
|
|
38
|
-
POSIX path of theFile
|
|
39
|
-
`;
|
|
40
|
-
args = ['-e', appleScript];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const child = spawn(cmd, args);
|
|
44
|
-
let output = '';
|
|
45
|
-
let errorOutput = '';
|
|
46
|
-
|
|
47
|
-
child.stdout.on('data', (data) => { output += data.toString(); });
|
|
48
|
-
child.stderr.on('data', (data) => { errorOutput += data.toString(); });
|
|
49
|
-
|
|
50
|
-
child.on('close', (code) => {
|
|
51
|
-
const resultPath = output.trim();
|
|
52
|
-
if (code === 0 && resultPath) {
|
|
53
|
-
resolve(resultPath);
|
|
54
|
-
} else {
|
|
55
|
-
// If user cancelled, usually code is 1 or path is empty
|
|
56
|
-
reject(new Error("Selection cancelled"));
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
child.on('error', (err) => {
|
|
61
|
-
reject(new Error(`Failed to launch dialog: ${err.message}`));
|
|
62
|
-
});
|
|
63
|
-
});
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
|
|
4
|
+
export async function pickMissionFileNative(): Promise<string> {
|
|
5
|
+
const platform = os.platform();
|
|
6
|
+
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
let cmd = '';
|
|
9
|
+
let args: string[] = [];
|
|
10
|
+
|
|
11
|
+
if (platform === 'win32') {
|
|
12
|
+
// WINDOWS: Use PowerShell with .NET Forms
|
|
13
|
+
cmd = 'powershell';
|
|
14
|
+
const psScript = `
|
|
15
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
16
|
+
$f = New-Object System.Windows.Forms.OpenFileDialog
|
|
17
|
+
$f.Filter = "Mission Files (*.miz)|*.miz"
|
|
18
|
+
$f.Title = "Select a DCS Mission File"
|
|
19
|
+
$f.ShowHelp = $true
|
|
20
|
+
$result = $f.ShowDialog()
|
|
21
|
+
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
22
|
+
Write-Host $f.FileName
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
// Encode command to avoid escaping issues
|
|
26
|
+
args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', psScript];
|
|
27
|
+
}
|
|
28
|
+
else if (platform === 'linux') {
|
|
29
|
+
// LINUX: Use Zenity (Common on Ubuntu/Desktop Linux)
|
|
30
|
+
cmd = 'zenity';
|
|
31
|
+
args = ['--file-selection', '--file-filter=*.miz', '--title=Select a Mission File'];
|
|
32
|
+
}
|
|
33
|
+
else if (platform === 'darwin') {
|
|
34
|
+
// MAC: Use AppleScript
|
|
35
|
+
cmd = 'osascript';
|
|
36
|
+
const appleScript = `
|
|
37
|
+
set theFile to choose file with prompt "Select a Mission File" of type {"miz"}
|
|
38
|
+
POSIX path of theFile
|
|
39
|
+
`;
|
|
40
|
+
args = ['-e', appleScript];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const child = spawn(cmd, args);
|
|
44
|
+
let output = '';
|
|
45
|
+
let errorOutput = '';
|
|
46
|
+
|
|
47
|
+
child.stdout.on('data', (data) => { output += data.toString(); });
|
|
48
|
+
child.stderr.on('data', (data) => { errorOutput += data.toString(); });
|
|
49
|
+
|
|
50
|
+
child.on('close', (code) => {
|
|
51
|
+
const resultPath = output.trim();
|
|
52
|
+
if (code === 0 && resultPath) {
|
|
53
|
+
resolve(resultPath);
|
|
54
|
+
} else {
|
|
55
|
+
// If user cancelled, usually code is 1 or path is empty
|
|
56
|
+
reject(new Error("Selection cancelled"));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.on('error', (err) => {
|
|
61
|
+
reject(new Error(`Failed to launch dialog: ${err.message}`));
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
64
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import { pickMissionFileNative } from "./file-picker"
|
|
4
|
-
|
|
5
|
-
export const getMizPath = async (root: string = ".") => {
|
|
6
|
-
const mizPath = await pickMissionFileNative();
|
|
7
|
-
|
|
8
|
-
const dir = mizPath.replace(path.basename(mizPath), "");
|
|
9
|
-
return { mizPath, dir };
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { pickMissionFileNative } from "./file-picker"
|
|
4
|
+
|
|
5
|
+
export const getMizPath = async (root: string = ".") => {
|
|
6
|
+
const mizPath = await pickMissionFileNative();
|
|
7
|
+
|
|
8
|
+
const dir = mizPath.replace(path.basename(mizPath), "");
|
|
9
|
+
return { mizPath, dir };
|
|
10
10
|
}
|
package/src/main.ts
CHANGED
|
@@ -1,49 +1,53 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import { promises as fs, existsSync } from "node:fs";
|
|
5
|
-
import path from "node:path"
|
|
6
|
-
import { startWatchers } from "./watchers";
|
|
7
|
-
import { handleArchive, unpackMiz } from "./compression";
|
|
8
|
-
import { sortFiles } from "./sorting";
|
|
9
|
-
import { initializeConfig } from "./config";
|
|
10
|
-
|
|
11
|
-
const [_0, _1, file] = process.argv;
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { promises as fs, existsSync } from "node:fs";
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { startWatchers } from "./watchers";
|
|
7
|
+
import { handleArchive, unpackMiz } from "./compression";
|
|
8
|
+
import { sortFiles } from "./sorting";
|
|
9
|
+
import { initializeConfig } from "./config";
|
|
10
|
+
|
|
11
|
+
const [_0, _1, file, backup] = process.argv;
|
|
12
|
+
|
|
13
|
+
const shouldBackup = backup === "--backup";
|
|
14
|
+
|
|
15
|
+
const backupMiz = async (filepath: string) => {
|
|
16
|
+
|
|
17
|
+
if (!filepath?.indexOf(".miz")) {
|
|
18
|
+
throw new Error('no miz file ext');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fileWithoutExt = filepath.replace(path.extname(filepath), "");
|
|
22
|
+
await fs.copyFile(filepath, `${fileWithoutExt}_backup_${new Date().toISOString().replace(/:/g, '-')}.miz`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const initialize = async () => {
|
|
26
|
+
|
|
27
|
+
const { missionPath, outDir, } = await initializeConfig({ file });
|
|
28
|
+
|
|
29
|
+
if (shouldBackup) {
|
|
30
|
+
backupMiz(missionPath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (existsSync(outDir)) {
|
|
34
|
+
console.log('out exists, using this as source');
|
|
35
|
+
await handleArchive(outDir, missionPath);
|
|
36
|
+
} else {
|
|
37
|
+
console.log("Initial unpack of .miz");
|
|
38
|
+
const files = await unpackMiz(missionPath, outDir);
|
|
39
|
+
const filePaths = files.filter(f => f.type === "file").map(f => path.join(outDir, f.path));
|
|
40
|
+
await sortFiles(filePaths);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { mizPath: missionPath, outDir };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
(async () => {
|
|
48
|
+
|
|
49
|
+
const { mizPath, outDir } = await initialize();
|
|
50
|
+
|
|
51
|
+
startWatchers(mizPath, outDir);
|
|
52
|
+
})()
|
|
53
|
+
|