airborne-devkit 0.9.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/CHANGELOG.md +59 -0
- package/README.md +289 -0
- package/package.json +51 -0
- package/src/bundleRC.rb +61 -0
- package/src/index.js +728 -0
- package/src/utils/common.js +321 -0
- package/src/utils/file.js +380 -0
- package/src/utils/package.js +68 -0
- package/src/utils/prompt.js +81 -0
- package/src/utils/release.js +337 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { createReadStream } from "fs";
|
|
5
|
+
import { promptWithType } from "./prompt.js";
|
|
6
|
+
|
|
7
|
+
const cliToConfigMap = {
|
|
8
|
+
platform: "platform",
|
|
9
|
+
tag: "tag",
|
|
10
|
+
organisation: "organisation",
|
|
11
|
+
namespace: "namespace",
|
|
12
|
+
jsEntryFile: "js_entry_file",
|
|
13
|
+
androidIndex: "android.index_file_path",
|
|
14
|
+
iosIndex: "ios.index_file_path",
|
|
15
|
+
upload: "upload",
|
|
16
|
+
directoryPath: "directory_path",
|
|
17
|
+
bootTimeout: "boot_timeout",
|
|
18
|
+
releaseConfigTimeout: "release_config_timeout",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function normalizeOptions(options = {}) {
|
|
22
|
+
const normalized = {};
|
|
23
|
+
|
|
24
|
+
for (const [key, value] of Object.entries(options)) {
|
|
25
|
+
const mappedKey = cliToConfigMap[key] || key;
|
|
26
|
+
normalized[mappedKey] = value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readAirborneConfig(directoryPath) {
|
|
33
|
+
const configPath = path.join(directoryPath, "airborne-config.json");
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Check if file exists
|
|
37
|
+
await fs.promises.access(configPath);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`ā Airborne config not found at ${configPath}, try using create-local-release-config`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const configContent = await fs.promises.readFile(configPath, "utf8");
|
|
46
|
+
return JSON.parse(configContent);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("ā Failed to read airborne-config.json:", error.message);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function writeAirborneConfig(options) {
|
|
54
|
+
try {
|
|
55
|
+
const filledOptions = await fillAirborneConfigOptions(options);
|
|
56
|
+
const config = {
|
|
57
|
+
organisation: filledOptions.organisation,
|
|
58
|
+
namespace: filledOptions.namespace,
|
|
59
|
+
js_entry_file: filledOptions.js_entry_file,
|
|
60
|
+
android: {
|
|
61
|
+
index_file_path: filledOptions.android.index_file_path,
|
|
62
|
+
},
|
|
63
|
+
ios: {
|
|
64
|
+
index_file_path: filledOptions.ios.index_file_path,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const configPath = path.join(
|
|
68
|
+
filledOptions.directory_path,
|
|
69
|
+
"airborne-config.json"
|
|
70
|
+
);
|
|
71
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
72
|
+
console.log(`ā
Config written to ${configPath}`);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error("ā Failed to create local airborne config:", err.message);
|
|
75
|
+
process.exit(1); // Exit with failure code
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function fillAirborneConfigOptions(options = {}) {
|
|
80
|
+
const questions = [
|
|
81
|
+
{
|
|
82
|
+
key: "organisation",
|
|
83
|
+
question: "\n Please enter the organisation name: ",
|
|
84
|
+
expectedType: "string",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "namespace",
|
|
88
|
+
question: "\n Please enter namespace/application name: ",
|
|
89
|
+
expectedType: "string",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "js_entry_file",
|
|
93
|
+
question: "\n Please enter the JavaScript entry file (e.g., index.js): ",
|
|
94
|
+
expectedType: "string",
|
|
95
|
+
defaultValue: "index.js",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "android.index_file_path",
|
|
99
|
+
question:
|
|
100
|
+
"\n Please enter the Android index file path (default: index.android.bundle): ",
|
|
101
|
+
expectedType: "string",
|
|
102
|
+
defaultValue: "index.android.bundle",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
key: "ios.index_file_path",
|
|
106
|
+
question:
|
|
107
|
+
"\n Please enter the iOS index file path (default: main.jsbundle): ",
|
|
108
|
+
expectedType: "string",
|
|
109
|
+
defaultValue: "main.jsbundle",
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const result = { ...options };
|
|
114
|
+
const getNested = (obj, path) =>
|
|
115
|
+
path.split(".").reduce((acc, k) => (acc ? acc[k] : undefined), obj);
|
|
116
|
+
|
|
117
|
+
const setNested = (obj, path, value) => {
|
|
118
|
+
const parts = path.split(".");
|
|
119
|
+
let temp = obj;
|
|
120
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
121
|
+
if (!temp[parts[i]]) temp[parts[i]] = {};
|
|
122
|
+
temp = temp[parts[i]];
|
|
123
|
+
}
|
|
124
|
+
temp[parts[parts.length - 1]] = value;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
for (const { key, question, expectedType, defaultValue } of questions) {
|
|
128
|
+
let value;
|
|
129
|
+
|
|
130
|
+
if (getNested(options, key) !== undefined) {
|
|
131
|
+
value = getNested(options, key);
|
|
132
|
+
} else if (question) {
|
|
133
|
+
value = await promptWithType(question, expectedType, defaultValue);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (value !== undefined) {
|
|
137
|
+
setNested(result, key, value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function sha256FileHex(filePath) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const hash = createHash("sha256");
|
|
147
|
+
const stream = createReadStream(filePath);
|
|
148
|
+
|
|
149
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
150
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
151
|
+
stream.on("error", (err) => reject(err));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function hexToBase64(hex) {
|
|
156
|
+
return Buffer.from(hex, "hex").toString("base64");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function stripMetadata(obj) {
|
|
160
|
+
if (Array.isArray(obj)) {
|
|
161
|
+
return obj.map(stripMetadata);
|
|
162
|
+
} else if (obj && typeof obj === "object") {
|
|
163
|
+
const newObj = {};
|
|
164
|
+
for (const key in obj) {
|
|
165
|
+
if (key === "$metadata") continue;
|
|
166
|
+
newObj[key] = stripMetadata(obj[key]);
|
|
167
|
+
}
|
|
168
|
+
return newObj;
|
|
169
|
+
}
|
|
170
|
+
return obj;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function removeToken(text) {
|
|
174
|
+
return (
|
|
175
|
+
text
|
|
176
|
+
// 1. Remove whole `--token <...>` in any usage line
|
|
177
|
+
.replace(/\s*--token\s+<[^>\s]+>/g, "")
|
|
178
|
+
|
|
179
|
+
// 2. Fix cases where we had " \\" at the end of a line
|
|
180
|
+
.replace(/\\\s*\n\s*\n/g, "\n\n")
|
|
181
|
+
|
|
182
|
+
// 3. Remove full parameter definition lines for token
|
|
183
|
+
.replace(/^\s*--token[^\n]*(\n|$)/gm, "")
|
|
184
|
+
|
|
185
|
+
// 4. If leftover text "(required) : Bearer token..." is stuck on another line, drop it
|
|
186
|
+
.replace(/\(required\)\s*:\s*Bearer token[^\n]*/gi, "")
|
|
187
|
+
|
|
188
|
+
// 5. Remove JSON `"token": "..."` entries
|
|
189
|
+
.replace(/"token"\s*:\s*"[^"]*",?\s*\n?/g, "")
|
|
190
|
+
|
|
191
|
+
// 6. Cleanup trailing spaces per line
|
|
192
|
+
.replace(/[ \t]+$/gm, "")
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function formatCommand(cmd) {
|
|
197
|
+
cmd.options = cmd.options.filter((opt) => opt.long !== "--token");
|
|
198
|
+
cmd._description = removeToken(cmd._description);
|
|
199
|
+
cmd._description = cmd._description.replace(
|
|
200
|
+
/airborne-core-cli/g,
|
|
201
|
+
"airborne-devkit"
|
|
202
|
+
);
|
|
203
|
+
cmd.listeners("option:token").forEach((listener) => {
|
|
204
|
+
cmd.removeListener("option:token", listener);
|
|
205
|
+
});
|
|
206
|
+
const afterHelpListeners = cmd.listeners("afterHelp");
|
|
207
|
+
cmd.removeAllListeners("afterHelp");
|
|
208
|
+
afterHelpListeners.forEach((fn) => {
|
|
209
|
+
cmd.on("afterHelp", function (...args) {
|
|
210
|
+
const originalWrite = process.stdout.write;
|
|
211
|
+
let output = "";
|
|
212
|
+
|
|
213
|
+
// hijack stdout to capture output
|
|
214
|
+
process.stdout.write = (chunk, ...rest) => {
|
|
215
|
+
output += chunk;
|
|
216
|
+
return true;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
fn.apply(this, args);
|
|
220
|
+
|
|
221
|
+
// replace airborne_core_Ƅcli with airborne-devkit
|
|
222
|
+
output = output.replace(/airborne-core-cli/g, "airborne-devkit");
|
|
223
|
+
|
|
224
|
+
// restore stdout
|
|
225
|
+
process.stdout.write = originalWrite;
|
|
226
|
+
|
|
227
|
+
// write sanitized output
|
|
228
|
+
process.stdout.write(removeToken(output));
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
cmd.hook("preAction", async (thisCmd) => {
|
|
233
|
+
const token = loadToken(process.cwd());
|
|
234
|
+
if (token?.access_token) {
|
|
235
|
+
thisCmd.setOptionValue("token", token.access_token);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return cmd;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function saveToken(access_token, refresh_token, directory_path) {
|
|
243
|
+
try {
|
|
244
|
+
let tokenPath;
|
|
245
|
+
|
|
246
|
+
if (process.env.CI === "true") {
|
|
247
|
+
tokenPath = path.join("/tmp", "airborne_tokens.json");
|
|
248
|
+
} else {
|
|
249
|
+
if (!directory_path) {
|
|
250
|
+
throw new Error("directory_path is required for non-CI usage.");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const airborneDir = path.join(directory_path, ".airborne");
|
|
254
|
+
|
|
255
|
+
// Create .airborne directory if it doesn't exist
|
|
256
|
+
if (!fs.existsSync(airborneDir)) {
|
|
257
|
+
fs.mkdirSync(airborneDir, { recursive: true, mode: 0o700 }); // rwx------
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
tokenPath = path.join(airborneDir, "credentials.json");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const data = {
|
|
264
|
+
access_token,
|
|
265
|
+
refresh_token,
|
|
266
|
+
saved_at: new Date().toISOString(),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
fs.writeFileSync(tokenPath, JSON.stringify(data, null, 2), {
|
|
270
|
+
mode: 0o600, // rw------- permissions
|
|
271
|
+
});
|
|
272
|
+
const gitignorePath = path.join(directory_path, ".gitignore");
|
|
273
|
+
let gitignoreContent = "";
|
|
274
|
+
if (fs.existsSync(gitignorePath)) {
|
|
275
|
+
gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!gitignoreContent.includes(".airborne")) {
|
|
279
|
+
gitignoreContent +=
|
|
280
|
+
(gitignoreContent.endsWith("\n") ? "" : "\n") + ".airborne\n";
|
|
281
|
+
fs.writeFileSync(gitignorePath, gitignoreContent, "utf8");
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error("ā Failed to save tokens:", err.message);
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function loadToken(directory_path) {
|
|
290
|
+
try {
|
|
291
|
+
let tokenPath;
|
|
292
|
+
|
|
293
|
+
if (process.env.CI === "true") {
|
|
294
|
+
tokenPath = path.join("/tmp", "airborne_tokens.json");
|
|
295
|
+
} else {
|
|
296
|
+
if (!directory_path) {
|
|
297
|
+
throw new Error("directory_path is required for non-CI usage.");
|
|
298
|
+
}
|
|
299
|
+
tokenPath = path.join(directory_path, ".airborne", "credentials.json");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (fs.existsSync(tokenPath)) {
|
|
303
|
+
return JSON.parse(fs.readFileSync(tokenPath, "utf8"));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return null;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error("ā Failed to load tokens:", err.message);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function airborneConfigExists(directoryPath) {
|
|
314
|
+
try {
|
|
315
|
+
const configPath = path.join(directoryPath, "airborne-config.json");
|
|
316
|
+
await fs.promises.access(configPath);
|
|
317
|
+
return true;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { hexToBase64, sha256FileHex } from "./common.js";
|
|
5
|
+
import { UploadFileAction, CreateFileAction } from "airborne-core-cli/action";
|
|
6
|
+
|
|
7
|
+
export function readDirectoryRecursive(dirPath, baseDir = dirPath) {
|
|
8
|
+
const items = [];
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(dirPath)) {
|
|
11
|
+
return items;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const entries = fs.readdirSync(dirPath, {
|
|
15
|
+
withFileTypes: true,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
20
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
21
|
+
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
const result = readDirectoryRecursive(fullPath, baseDir);
|
|
24
|
+
result.forEach((resultEntry) => {
|
|
25
|
+
items.push(resultEntry);
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
items.push({
|
|
29
|
+
name: entry.name,
|
|
30
|
+
type: "file",
|
|
31
|
+
path: relativePath,
|
|
32
|
+
fullPath: fullPath,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return items;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function executeReactNativeBundleCommand(command) {
|
|
41
|
+
console.log("š¦ Executing command: ", command);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const remotebundleDir = path.join(process.cwd(), "remotebundle");
|
|
45
|
+
if (!fs.existsSync(remotebundleDir)) {
|
|
46
|
+
fs.mkdirSync(remotebundleDir, {
|
|
47
|
+
recursive: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Execute the command
|
|
52
|
+
const output = execSync(command, {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
output: output,
|
|
60
|
+
command: command,
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("ā React Native bundle command failed:", error.message);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: error.message,
|
|
68
|
+
command: command,
|
|
69
|
+
exitCode: error.status,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const uploadFiles = async (filesToUpload, config) => {
|
|
74
|
+
console.log(
|
|
75
|
+
`š Starting upload process for ${filesToUpload.length} files...`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const results = {
|
|
79
|
+
uploaded: 0,
|
|
80
|
+
existing: 0,
|
|
81
|
+
failed: 0,
|
|
82
|
+
errors: [],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
for (let index = 0; index < filesToUpload.length; index++) {
|
|
87
|
+
const fileObj = filesToUpload[index];
|
|
88
|
+
const fileProgress = `[${index + 1}/${filesToUpload.length}]`;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
console.log(`${fileProgress} š Processing ${fileObj.file_path}...`);
|
|
92
|
+
|
|
93
|
+
const storedChecksum = await getMappedChecksum(
|
|
94
|
+
config.directory_path,
|
|
95
|
+
fileObj.file_path,
|
|
96
|
+
config.tag
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const baseDir = path.isAbsolute(config.directory_path)
|
|
100
|
+
? config.directory_path
|
|
101
|
+
: path.join(process.cwd(), config.directory_path);
|
|
102
|
+
|
|
103
|
+
const fileFullPath = path.join(
|
|
104
|
+
baseDir,
|
|
105
|
+
config.platform,
|
|
106
|
+
"build",
|
|
107
|
+
"generated",
|
|
108
|
+
"airborne",
|
|
109
|
+
fileObj.file_path
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(fileFullPath)) {
|
|
113
|
+
throw new Error(`File not found: ${fileFullPath}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const checksum = await sha256FileHex(fileFullPath);
|
|
117
|
+
|
|
118
|
+
if (storedChecksum === checksum) {
|
|
119
|
+
console.log(
|
|
120
|
+
`${fileProgress} ā
File already exists, checksum matches`
|
|
121
|
+
);
|
|
122
|
+
results.existing++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`${fileProgress} ā¬ļø Uploading file: ${fileObj.file_path}`);
|
|
127
|
+
|
|
128
|
+
const uploadOptions = {
|
|
129
|
+
file: fileFullPath,
|
|
130
|
+
file_path: fileObj.file_path,
|
|
131
|
+
organisation: config.organisation,
|
|
132
|
+
application: config.namespace,
|
|
133
|
+
token: config.token,
|
|
134
|
+
checksum: hexToBase64(checksum),
|
|
135
|
+
tag: config.tag,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const uploadOutput = await UploadFileAction(null, uploadOptions);
|
|
139
|
+
|
|
140
|
+
if (!uploadOutput.file_path || !uploadOutput.id) {
|
|
141
|
+
throw new Error("Upload failed, invalid response from server");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await createFileMapping(
|
|
145
|
+
config.directory_path,
|
|
146
|
+
uploadOutput.file_path,
|
|
147
|
+
uploadOutput.id,
|
|
148
|
+
uploadOutput.checksum,
|
|
149
|
+
uploadOutput.tag
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Check if this was a new upload or existing file returned
|
|
153
|
+
if (uploadOutput.checksum === checksum) {
|
|
154
|
+
console.log(
|
|
155
|
+
`${fileProgress} ā
Successfully processed ${fileObj.file_path}`
|
|
156
|
+
);
|
|
157
|
+
results.uploaded++;
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`${fileProgress} š File already existed on server`);
|
|
160
|
+
results.existing++;
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(
|
|
164
|
+
`${fileProgress} šØ Error processing ${fileObj.file_path}:`,
|
|
165
|
+
err.message
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
results.failed++;
|
|
169
|
+
results.errors.push({ file: fileObj.file_path, error: err.message });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log("\nš Upload Summary:");
|
|
174
|
+
console.log(`ā
Uploaded: ${results.uploaded}`);
|
|
175
|
+
console.log(`ā»ļø Existing: ${results.existing}`);
|
|
176
|
+
console.log(`ā Failed: ${results.failed}`);
|
|
177
|
+
|
|
178
|
+
if (results.errors.length > 0) {
|
|
179
|
+
console.log("\nš Failed files:");
|
|
180
|
+
results.errors.forEach(({ file, error }) => {
|
|
181
|
+
console.log(` ⢠${file}: ${error}`);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error("\nš„ Upload process failed:", err.message);
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export async function createFiles(filesToCreate, config, prefixUrl) {
|
|
191
|
+
console.log(
|
|
192
|
+
`š Starting file creation process for ${filesToCreate.length} files...`
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const results = {
|
|
196
|
+
created: 0,
|
|
197
|
+
existing: 0,
|
|
198
|
+
failed: 0,
|
|
199
|
+
errors: [],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
for (let index = 0; index < filesToCreate.length; index++) {
|
|
204
|
+
const fileObj = filesToCreate[index];
|
|
205
|
+
const fileProgress = `[${index + 1}/${filesToCreate.length}]`;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
console.log(`${fileProgress} š Processing ${fileObj.file_path}...`);
|
|
209
|
+
|
|
210
|
+
const storedChecksum = await getMappedChecksum(
|
|
211
|
+
config.directory_path,
|
|
212
|
+
fileObj.file_path,
|
|
213
|
+
config.tag
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const baseDir = path.isAbsolute(config.directory_path)
|
|
217
|
+
? config.directory_path
|
|
218
|
+
: path.join(process.cwd(), config.directory_path);
|
|
219
|
+
|
|
220
|
+
const fileFullPath = path.join(
|
|
221
|
+
baseDir,
|
|
222
|
+
config.platform,
|
|
223
|
+
"build",
|
|
224
|
+
"generated",
|
|
225
|
+
"airborne",
|
|
226
|
+
fileObj.file_path
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (!fs.existsSync(fileFullPath)) {
|
|
230
|
+
throw new Error(`File not found: ${fileFullPath}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (storedChecksum) {
|
|
234
|
+
console.log(`${fileProgress} š Calculating checksum...`);
|
|
235
|
+
const checksum = await sha256FileHex(fileFullPath);
|
|
236
|
+
|
|
237
|
+
if (storedChecksum === checksum) {
|
|
238
|
+
console.log(
|
|
239
|
+
`${fileProgress} ā
File already exists, checksum matches`
|
|
240
|
+
);
|
|
241
|
+
results.existing++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(
|
|
247
|
+
`${fileProgress} š Creating file record for ${fileObj.file_path}...`
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const fileUrl = prefixUrl + fileObj.file_path;
|
|
251
|
+
console.log(`${fileProgress} š File URL: ${fileUrl}`);
|
|
252
|
+
|
|
253
|
+
const createOptions = {
|
|
254
|
+
file_path: fileObj.file_path,
|
|
255
|
+
url: fileUrl,
|
|
256
|
+
organisation: config.organisation,
|
|
257
|
+
application: config.namespace,
|
|
258
|
+
token: config.token,
|
|
259
|
+
tag: config.tag,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const output = await CreateFileAction(null, createOptions);
|
|
263
|
+
|
|
264
|
+
if (!output.file_path || !output.id) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"CreateFileAction failed, invalid response from server"
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await createFileMapping(
|
|
271
|
+
config.directory_path,
|
|
272
|
+
output.file_path,
|
|
273
|
+
output.id,
|
|
274
|
+
output.checksum,
|
|
275
|
+
output.tag
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
console.log(
|
|
279
|
+
`${fileProgress} ā
Successfully processed file record for ${fileObj.file_path}`
|
|
280
|
+
);
|
|
281
|
+
results.created++;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
console.error(
|
|
284
|
+
`${fileProgress} šØ Error processing ${fileObj.file_path}:`,
|
|
285
|
+
err.message
|
|
286
|
+
);
|
|
287
|
+
results.failed++;
|
|
288
|
+
results.errors.push({ file: fileObj.file_path, error: err.message });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log("\nš File Creation Summary:");
|
|
293
|
+
console.log(`š Created: ${results.created}`);
|
|
294
|
+
console.log(`ā»ļø Existing: ${results.existing}`);
|
|
295
|
+
console.log(`ā Failed: ${results.failed}`);
|
|
296
|
+
|
|
297
|
+
if (results.errors.length > 0) {
|
|
298
|
+
console.log("\nš Failed files:");
|
|
299
|
+
results.errors.forEach(({ file, error }) => {
|
|
300
|
+
console.log(` ⢠${file}: ${error}`);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.error("\nš„ File creation process failed:", err.message);
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function createFileMapping(
|
|
310
|
+
directory_path,
|
|
311
|
+
file_path,
|
|
312
|
+
id,
|
|
313
|
+
checksum,
|
|
314
|
+
tag
|
|
315
|
+
) {
|
|
316
|
+
const airborneDir = path.join(directory_path, ".airborne");
|
|
317
|
+
const mappingFile = path.join(airborneDir, "mappings.json");
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
// Read existing mappings if file exists
|
|
321
|
+
let mappings = {};
|
|
322
|
+
try {
|
|
323
|
+
const data = await fs.promises.readFile(mappingFile, "utf8");
|
|
324
|
+
mappings = JSON.parse(data);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (err.code !== "ENOENT") throw err; // ignore file not found
|
|
327
|
+
}
|
|
328
|
+
if (!tag) {
|
|
329
|
+
tag = "__default__";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!mappings[tag]) {
|
|
333
|
+
mappings[tag] = {};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Update or insert mapping with checksum
|
|
337
|
+
mappings[tag][file_path] = { id, checksum };
|
|
338
|
+
|
|
339
|
+
// Write updated mappings back
|
|
340
|
+
await fs.promises.writeFile(
|
|
341
|
+
mappingFile,
|
|
342
|
+
JSON.stringify(mappings, null, 2),
|
|
343
|
+
"utf8"
|
|
344
|
+
);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error("ā Failed to update file mapping:", error.message);
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function getMappedChecksum(directory_path, file_path, tag) {
|
|
352
|
+
const mappingFile = path.join(directory_path, ".airborne", "mappings.json");
|
|
353
|
+
if (!tag) {
|
|
354
|
+
tag = "__default__";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const data = await fs.promises.readFile(mappingFile, "utf8");
|
|
359
|
+
const mappings = JSON.parse(data);
|
|
360
|
+
return mappings[tag][file_path]?.checksum || null;
|
|
361
|
+
} catch (err) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function readFileMapping(directory_path, file_path, tag) {
|
|
367
|
+
const mappingFile = path.join(directory_path, ".airborne", "mappings.json");
|
|
368
|
+
|
|
369
|
+
if (!tag) {
|
|
370
|
+
tag = "__default__";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const data = await fs.promises.readFile(mappingFile, "utf8");
|
|
375
|
+
const mappings = JSON.parse(data);
|
|
376
|
+
return mappings[tag][file_path] || null;
|
|
377
|
+
} catch (err) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|