hot-updater 0.1.0 → 0.1.2
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/dist/chunk-XCQPUXG2.js +10 -0
- package/dist/config.js +2 -0
- package/dist/index.cjs +97 -88
- package/dist/index.js +100 -87
- package/package.json +7 -6
- package/src/commands/console.ts +15 -0
- package/src/commands/deploy.ts +105 -0
- package/src/commands/generateSecretKey.ts +11 -0
- package/src/commands/prune.ts +56 -0
- package/src/components/banner.tsx +20 -0
- package/src/config.ts +4 -0
- package/src/index.ts +88 -0
- package/src/packageJson.ts +7 -0
- package/src/prompts/getPlatform.ts +19 -0
- package/src/utils/createZip.ts +47 -0
- package/src/utils/delay.ts +2 -0
- package/src/utils/formatDate.ts +21 -0
- package/src/utils/getDefaultTargetVersion.ts +76 -0
- package/src/utils/getFileHash.ts +22 -0
- package/src/utils/toRotated.tsx +26 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// ../../node_modules/.pnpm/tsup@8.3.0_@swc+core@1.7.42_@swc+helpers@0.5.13__jiti@1.21.6_postcss@8.4.47_typescript@5.6.2_yaml@2.4.5/node_modules/tsup/assets/esm_shims.js
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
5
|
+
var getDirname = () => path.dirname(getFilename());
|
|
6
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__dirname
|
|
10
|
+
};
|
package/dist/config.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -179,66 +179,72 @@ var import_plugin_core = require("@hot-updater/plugin-core");
|
|
|
179
179
|
var import_utils = require("@hot-updater/utils");
|
|
180
180
|
var deploy = async (options) => {
|
|
181
181
|
const s = (0, import_prompts.spinner)();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
182
|
+
try {
|
|
183
|
+
const config = await (0, import_plugin_core.loadConfig)();
|
|
184
|
+
if (!config) {
|
|
185
|
+
console.error("No config found. Please run `hot-updater init` first.");
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
(0, import_prompts.intro)("Please provide a description for the bundle.");
|
|
189
|
+
const description = await (0, import_prompts.text)({ message: "Description" });
|
|
190
|
+
const cwd = (0, import_plugin_core.getCwd)();
|
|
191
|
+
const targetVersion = options.targetVersion ?? await getDefaultTargetVersion(cwd, options.platform);
|
|
192
|
+
if (!targetVersion) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"Target version not found. Please provide a target version."
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
s.start("Build in progress");
|
|
198
|
+
const { buildPath } = await config.build({
|
|
199
|
+
cwd,
|
|
200
|
+
platform: options.platform
|
|
201
|
+
});
|
|
202
|
+
s.message("Checking existing updates...");
|
|
203
|
+
await createZip(buildPath, "build.zip");
|
|
204
|
+
const bundlePath = buildPath.concat(".zip");
|
|
205
|
+
const hash = await getFileHashFromFile(bundlePath);
|
|
206
|
+
const newBundleVersion = formatDate(/* @__PURE__ */ new Date());
|
|
207
|
+
const deployPlugin = config.deploy({
|
|
208
|
+
cwd
|
|
209
|
+
});
|
|
210
|
+
const updateSources = await deployPlugin.getUpdateSources();
|
|
211
|
+
const targetVersions = (0, import_utils.filterTargetVersion)(
|
|
212
|
+
updateSources ?? [],
|
|
213
|
+
targetVersion,
|
|
214
|
+
options.platform
|
|
194
215
|
);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
await createZip(buildPath, "build.zip");
|
|
203
|
-
const bundlePath = buildPath.concat(".zip");
|
|
204
|
-
const hash = await getFileHashFromFile(bundlePath);
|
|
205
|
-
const newBundleVersion = formatDate(/* @__PURE__ */ new Date());
|
|
206
|
-
const deployPlugin = config.deploy({
|
|
207
|
-
cwd
|
|
208
|
-
});
|
|
209
|
-
const updateSources = await deployPlugin.getUpdateSources();
|
|
210
|
-
const targetVersions = (0, import_utils.filterTargetVersion)(
|
|
211
|
-
updateSources ?? [],
|
|
212
|
-
targetVersion,
|
|
213
|
-
options.platform
|
|
214
|
-
);
|
|
215
|
-
if (targetVersions.length > 0) {
|
|
216
|
-
const recentVersion = targetVersions[0];
|
|
217
|
-
const recentHash = recentVersion?.hash;
|
|
218
|
-
if (recentHash === hash) {
|
|
219
|
-
s.stop("The update already exists.", -1);
|
|
220
|
-
return;
|
|
216
|
+
if (targetVersions.length > 0) {
|
|
217
|
+
const recentVersion = targetVersions[0];
|
|
218
|
+
const recentHash = recentVersion?.hash;
|
|
219
|
+
if (recentHash === hash) {
|
|
220
|
+
s.stop("The update already exists.", -1);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
221
223
|
}
|
|
224
|
+
s.message("Uploading bundle...");
|
|
225
|
+
const { file } = await deployPlugin.uploadBundle(
|
|
226
|
+
options.platform,
|
|
227
|
+
newBundleVersion,
|
|
228
|
+
bundlePath
|
|
229
|
+
);
|
|
230
|
+
await deployPlugin.appendUpdateSource({
|
|
231
|
+
forceUpdate: options.forceUpdate,
|
|
232
|
+
platform: options.platform,
|
|
233
|
+
file,
|
|
234
|
+
hash,
|
|
235
|
+
description: String(description),
|
|
236
|
+
targetVersion,
|
|
237
|
+
bundleVersion: newBundleVersion,
|
|
238
|
+
enabled: true
|
|
239
|
+
});
|
|
240
|
+
await deployPlugin.commitUpdateSource();
|
|
241
|
+
await import_promises4.default.rm(bundlePath);
|
|
242
|
+
s.stop("Uploading Success !", 0);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
s.stop("Uploading Failed !", -1);
|
|
245
|
+
console.error(e);
|
|
246
|
+
process.exit(-1);
|
|
222
247
|
}
|
|
223
|
-
s.message("Uploading bundle...");
|
|
224
|
-
const { file } = await deployPlugin.uploadBundle(
|
|
225
|
-
options.platform,
|
|
226
|
-
newBundleVersion,
|
|
227
|
-
bundlePath
|
|
228
|
-
);
|
|
229
|
-
await deployPlugin.appendUpdateSource({
|
|
230
|
-
forceUpdate: options.forceUpdate,
|
|
231
|
-
platform: options.platform,
|
|
232
|
-
file,
|
|
233
|
-
hash,
|
|
234
|
-
description: String(description),
|
|
235
|
-
targetVersion,
|
|
236
|
-
bundleVersion: newBundleVersion,
|
|
237
|
-
enabled: true
|
|
238
|
-
});
|
|
239
|
-
await deployPlugin.commitUpdateSource();
|
|
240
|
-
await import_promises4.default.rm(bundlePath);
|
|
241
|
-
s.stop("Uploading Success !", 0);
|
|
242
248
|
};
|
|
243
249
|
|
|
244
250
|
// src/commands/generateSecretKey.ts
|
|
@@ -258,42 +264,45 @@ var import_plugin_core2 = require("@hot-updater/plugin-core");
|
|
|
258
264
|
var import_plugin_core3 = require("@hot-updater/plugin-core");
|
|
259
265
|
var prune = async (options) => {
|
|
260
266
|
const s = p.spinner();
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
267
|
+
try {
|
|
268
|
+
const config = await (0, import_plugin_core3.loadConfig)();
|
|
269
|
+
if (!config) {
|
|
270
|
+
console.error("No config found. Please run `hot-updater init` first.");
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
const cwd = (0, import_plugin_core2.getCwd)();
|
|
274
|
+
const deployPlugin = config.deploy({
|
|
275
|
+
cwd
|
|
276
|
+
// spinner: s,
|
|
277
|
+
});
|
|
278
|
+
s.start("Checking existing updates");
|
|
279
|
+
const updateSources = await deployPlugin.getUpdateSources();
|
|
280
|
+
const activeSources = updateSources.filter((source) => source.enabled);
|
|
281
|
+
const inactiveSources = updateSources.filter((source) => !source.enabled);
|
|
282
|
+
if (inactiveSources.length === 0) {
|
|
283
|
+
s.stop("No inactive versions found", -1);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
s.message("Pruning updates");
|
|
287
|
+
await deployPlugin.setUpdateSources(activeSources);
|
|
288
|
+
await deployPlugin.commitUpdateSource();
|
|
289
|
+
for (const source of inactiveSources) {
|
|
290
|
+
const key = await deployPlugin.deleteBundle(
|
|
291
|
+
options.platform,
|
|
292
|
+
source.bundleVersion
|
|
293
|
+
);
|
|
294
|
+
p.log.info(`deleting: ${key}`);
|
|
295
|
+
}
|
|
296
|
+
s.stop("Done");
|
|
297
|
+
} catch (e) {
|
|
298
|
+
s.stop("Pruning Failed !", -1);
|
|
299
|
+
console.error(e);
|
|
300
|
+
process.exit(-1);
|
|
288
301
|
}
|
|
289
|
-
s.stop("Done");
|
|
290
302
|
};
|
|
291
303
|
|
|
292
304
|
// src/packageJson.ts
|
|
293
|
-
var import_url = require("url");
|
|
294
305
|
var import_read_package_up = require("read-package-up");
|
|
295
|
-
var import_meta = {};
|
|
296
|
-
var __dirname = (0, import_url.fileURLToPath)(new URL(".", import_meta.url));
|
|
297
306
|
var packageJsonData = (0, import_read_package_up.readPackageUpSync)({
|
|
298
307
|
cwd: __dirname
|
|
299
308
|
});
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
__dirname
|
|
4
|
+
} from "./chunk-XCQPUXG2.js";
|
|
2
5
|
|
|
3
6
|
// src/commands/console.ts
|
|
4
7
|
import { serve } from "@hono/node-server";
|
|
@@ -156,66 +159,72 @@ import { getCwd, loadConfig } from "@hot-updater/plugin-core";
|
|
|
156
159
|
import { filterTargetVersion } from "@hot-updater/utils";
|
|
157
160
|
var deploy = async (options) => {
|
|
158
161
|
const s = spinner();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
try {
|
|
163
|
+
const config = await loadConfig();
|
|
164
|
+
if (!config) {
|
|
165
|
+
console.error("No config found. Please run `hot-updater init` first.");
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
intro("Please provide a description for the bundle.");
|
|
169
|
+
const description = await text({ message: "Description" });
|
|
170
|
+
const cwd = getCwd();
|
|
171
|
+
const targetVersion = options.targetVersion ?? await getDefaultTargetVersion(cwd, options.platform);
|
|
172
|
+
if (!targetVersion) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
"Target version not found. Please provide a target version."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
s.start("Build in progress");
|
|
178
|
+
const { buildPath } = await config.build({
|
|
179
|
+
cwd,
|
|
180
|
+
platform: options.platform
|
|
181
|
+
});
|
|
182
|
+
s.message("Checking existing updates...");
|
|
183
|
+
await createZip(buildPath, "build.zip");
|
|
184
|
+
const bundlePath = buildPath.concat(".zip");
|
|
185
|
+
const hash = await getFileHashFromFile(bundlePath);
|
|
186
|
+
const newBundleVersion = formatDate(/* @__PURE__ */ new Date());
|
|
187
|
+
const deployPlugin = config.deploy({
|
|
188
|
+
cwd
|
|
189
|
+
});
|
|
190
|
+
const updateSources = await deployPlugin.getUpdateSources();
|
|
191
|
+
const targetVersions = filterTargetVersion(
|
|
192
|
+
updateSources ?? [],
|
|
193
|
+
targetVersion,
|
|
194
|
+
options.platform
|
|
171
195
|
);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await createZip(buildPath, "build.zip");
|
|
180
|
-
const bundlePath = buildPath.concat(".zip");
|
|
181
|
-
const hash = await getFileHashFromFile(bundlePath);
|
|
182
|
-
const newBundleVersion = formatDate(/* @__PURE__ */ new Date());
|
|
183
|
-
const deployPlugin = config.deploy({
|
|
184
|
-
cwd
|
|
185
|
-
});
|
|
186
|
-
const updateSources = await deployPlugin.getUpdateSources();
|
|
187
|
-
const targetVersions = filterTargetVersion(
|
|
188
|
-
updateSources ?? [],
|
|
189
|
-
targetVersion,
|
|
190
|
-
options.platform
|
|
191
|
-
);
|
|
192
|
-
if (targetVersions.length > 0) {
|
|
193
|
-
const recentVersion = targetVersions[0];
|
|
194
|
-
const recentHash = recentVersion?.hash;
|
|
195
|
-
if (recentHash === hash) {
|
|
196
|
-
s.stop("The update already exists.", -1);
|
|
197
|
-
return;
|
|
196
|
+
if (targetVersions.length > 0) {
|
|
197
|
+
const recentVersion = targetVersions[0];
|
|
198
|
+
const recentHash = recentVersion?.hash;
|
|
199
|
+
if (recentHash === hash) {
|
|
200
|
+
s.stop("The update already exists.", -1);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
198
203
|
}
|
|
204
|
+
s.message("Uploading bundle...");
|
|
205
|
+
const { file } = await deployPlugin.uploadBundle(
|
|
206
|
+
options.platform,
|
|
207
|
+
newBundleVersion,
|
|
208
|
+
bundlePath
|
|
209
|
+
);
|
|
210
|
+
await deployPlugin.appendUpdateSource({
|
|
211
|
+
forceUpdate: options.forceUpdate,
|
|
212
|
+
platform: options.platform,
|
|
213
|
+
file,
|
|
214
|
+
hash,
|
|
215
|
+
description: String(description),
|
|
216
|
+
targetVersion,
|
|
217
|
+
bundleVersion: newBundleVersion,
|
|
218
|
+
enabled: true
|
|
219
|
+
});
|
|
220
|
+
await deployPlugin.commitUpdateSource();
|
|
221
|
+
await fs4.rm(bundlePath);
|
|
222
|
+
s.stop("Uploading Success !", 0);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
s.stop("Uploading Failed !", -1);
|
|
225
|
+
console.error(e);
|
|
226
|
+
process.exit(-1);
|
|
199
227
|
}
|
|
200
|
-
s.message("Uploading bundle...");
|
|
201
|
-
const { file } = await deployPlugin.uploadBundle(
|
|
202
|
-
options.platform,
|
|
203
|
-
newBundleVersion,
|
|
204
|
-
bundlePath
|
|
205
|
-
);
|
|
206
|
-
await deployPlugin.appendUpdateSource({
|
|
207
|
-
forceUpdate: options.forceUpdate,
|
|
208
|
-
platform: options.platform,
|
|
209
|
-
file,
|
|
210
|
-
hash,
|
|
211
|
-
description: String(description),
|
|
212
|
-
targetVersion,
|
|
213
|
-
bundleVersion: newBundleVersion,
|
|
214
|
-
enabled: true
|
|
215
|
-
});
|
|
216
|
-
await deployPlugin.commitUpdateSource();
|
|
217
|
-
await fs4.rm(bundlePath);
|
|
218
|
-
s.stop("Uploading Success !", 0);
|
|
219
228
|
};
|
|
220
229
|
|
|
221
230
|
// src/commands/generateSecretKey.ts
|
|
@@ -235,41 +244,45 @@ import { getCwd as getCwd2 } from "@hot-updater/plugin-core";
|
|
|
235
244
|
import { loadConfig as loadConfig2 } from "@hot-updater/plugin-core";
|
|
236
245
|
var prune = async (options) => {
|
|
237
246
|
const s = p.spinner();
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
247
|
+
try {
|
|
248
|
+
const config = await loadConfig2();
|
|
249
|
+
if (!config) {
|
|
250
|
+
console.error("No config found. Please run `hot-updater init` first.");
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const cwd = getCwd2();
|
|
254
|
+
const deployPlugin = config.deploy({
|
|
255
|
+
cwd
|
|
256
|
+
// spinner: s,
|
|
257
|
+
});
|
|
258
|
+
s.start("Checking existing updates");
|
|
259
|
+
const updateSources = await deployPlugin.getUpdateSources();
|
|
260
|
+
const activeSources = updateSources.filter((source) => source.enabled);
|
|
261
|
+
const inactiveSources = updateSources.filter((source) => !source.enabled);
|
|
262
|
+
if (inactiveSources.length === 0) {
|
|
263
|
+
s.stop("No inactive versions found", -1);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
s.message("Pruning updates");
|
|
267
|
+
await deployPlugin.setUpdateSources(activeSources);
|
|
268
|
+
await deployPlugin.commitUpdateSource();
|
|
269
|
+
for (const source of inactiveSources) {
|
|
270
|
+
const key = await deployPlugin.deleteBundle(
|
|
271
|
+
options.platform,
|
|
272
|
+
source.bundleVersion
|
|
273
|
+
);
|
|
274
|
+
p.log.info(`deleting: ${key}`);
|
|
275
|
+
}
|
|
276
|
+
s.stop("Done");
|
|
277
|
+
} catch (e) {
|
|
278
|
+
s.stop("Pruning Failed !", -1);
|
|
279
|
+
console.error(e);
|
|
280
|
+
process.exit(-1);
|
|
265
281
|
}
|
|
266
|
-
s.stop("Done");
|
|
267
282
|
};
|
|
268
283
|
|
|
269
284
|
// src/packageJson.ts
|
|
270
|
-
import { fileURLToPath } from "url";
|
|
271
285
|
import { readPackageUpSync } from "read-package-up";
|
|
272
|
-
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
273
286
|
var packageJsonData = readPackageUpSync({
|
|
274
287
|
cwd: __dirname
|
|
275
288
|
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hot-updater",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"bin": {
|
|
6
|
-
"hot-updater": "./dist/index.
|
|
6
|
+
"hot-updater": "./dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
"./config": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
+
"src",
|
|
17
18
|
"package.json"
|
|
18
19
|
],
|
|
19
20
|
"description": "React Native OTA solution for self-hosted",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@clack/prompts": "^0.7.0",
|
|
32
33
|
"@hono/node-server": "^1.13.4",
|
|
33
|
-
"@hot-updater/console": "0.1.
|
|
34
|
+
"@hot-updater/console": "0.1.2",
|
|
34
35
|
"boxen": "^8.0.1",
|
|
35
36
|
"commander": "^11.1.0",
|
|
36
37
|
"cosmiconfig": "^9.0.0",
|
|
@@ -40,8 +41,8 @@
|
|
|
40
41
|
"plist": "^3.1.0",
|
|
41
42
|
"read-package-up": "^11.0.0",
|
|
42
43
|
"workspace-tools": "^0.36.4",
|
|
43
|
-
"@hot-updater/plugin-core": "0.1.
|
|
44
|
-
"@hot-updater/utils": "0.1.
|
|
44
|
+
"@hot-updater/plugin-core": "0.1.2",
|
|
45
|
+
"@hot-updater/utils": "0.1.2"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/connect": "^3.4.38",
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"@types/plist": "^3.0.5"
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
|
-
"build": "tsup src/index.ts src/config.ts --format esm,cjs --dts",
|
|
53
|
+
"build": "tsup src/index.ts src/config.ts --format esm,cjs --dts --shims",
|
|
53
54
|
"test:type": "tsc --noEmit"
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
|
|
3
|
+
import app from "@hot-updater/console";
|
|
4
|
+
|
|
5
|
+
export const openConsole = () => {
|
|
6
|
+
serve(
|
|
7
|
+
{
|
|
8
|
+
fetch: app.fetch,
|
|
9
|
+
port: 1422,
|
|
10
|
+
},
|
|
11
|
+
(info) => {
|
|
12
|
+
console.log(`Server running on port ${info.port}`);
|
|
13
|
+
},
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { intro, spinner, text } from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
import { createZip } from "@/utils/createZip";
|
|
5
|
+
import { formatDate } from "@/utils/formatDate";
|
|
6
|
+
import { getDefaultTargetVersion } from "@/utils/getDefaultTargetVersion";
|
|
7
|
+
import { getFileHashFromFile } from "@/utils/getFileHash";
|
|
8
|
+
import { getCwd, loadConfig } from "@hot-updater/plugin-core";
|
|
9
|
+
import { type Platform, filterTargetVersion } from "@hot-updater/utils";
|
|
10
|
+
|
|
11
|
+
export interface DeployOptions {
|
|
12
|
+
targetVersion?: string;
|
|
13
|
+
platform: Platform;
|
|
14
|
+
forceUpdate: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const deploy = async (options: DeployOptions) => {
|
|
18
|
+
const s = spinner();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const config = await loadConfig();
|
|
22
|
+
if (!config) {
|
|
23
|
+
console.error("No config found. Please run `hot-updater init` first.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
intro("Please provide a description for the bundle.");
|
|
28
|
+
const description = await text({ message: "Description" });
|
|
29
|
+
|
|
30
|
+
const cwd = getCwd();
|
|
31
|
+
const targetVersion =
|
|
32
|
+
options.targetVersion ??
|
|
33
|
+
(await getDefaultTargetVersion(cwd, options.platform));
|
|
34
|
+
|
|
35
|
+
if (!targetVersion) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Target version not found. Please provide a target version.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
s.start("Build in progress");
|
|
42
|
+
|
|
43
|
+
const { buildPath } = await config.build({
|
|
44
|
+
cwd,
|
|
45
|
+
platform: options.platform,
|
|
46
|
+
});
|
|
47
|
+
s.message("Checking existing updates...");
|
|
48
|
+
|
|
49
|
+
await createZip(buildPath, "build.zip");
|
|
50
|
+
|
|
51
|
+
const bundlePath = buildPath.concat(".zip");
|
|
52
|
+
|
|
53
|
+
const hash = await getFileHashFromFile(bundlePath);
|
|
54
|
+
|
|
55
|
+
const newBundleVersion = formatDate(new Date());
|
|
56
|
+
|
|
57
|
+
const deployPlugin = config.deploy({
|
|
58
|
+
cwd,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const updateSources = await deployPlugin.getUpdateSources();
|
|
62
|
+
const targetVersions = filterTargetVersion(
|
|
63
|
+
updateSources ?? [],
|
|
64
|
+
targetVersion,
|
|
65
|
+
options.platform,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// hash check
|
|
69
|
+
if (targetVersions.length > 0) {
|
|
70
|
+
const recentVersion = targetVersions[0];
|
|
71
|
+
const recentHash = recentVersion?.hash;
|
|
72
|
+
|
|
73
|
+
if (recentHash === hash) {
|
|
74
|
+
s.stop("The update already exists.", -1);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
s.message("Uploading bundle...");
|
|
80
|
+
const { file } = await deployPlugin.uploadBundle(
|
|
81
|
+
options.platform,
|
|
82
|
+
newBundleVersion,
|
|
83
|
+
bundlePath,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await deployPlugin.appendUpdateSource({
|
|
87
|
+
forceUpdate: options.forceUpdate,
|
|
88
|
+
platform: options.platform,
|
|
89
|
+
file,
|
|
90
|
+
hash,
|
|
91
|
+
description: String(description),
|
|
92
|
+
targetVersion,
|
|
93
|
+
bundleVersion: newBundleVersion,
|
|
94
|
+
enabled: true,
|
|
95
|
+
});
|
|
96
|
+
await deployPlugin.commitUpdateSource();
|
|
97
|
+
|
|
98
|
+
await fs.rm(bundlePath);
|
|
99
|
+
s.stop("Uploading Success !", 0);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
s.stop("Uploading Failed !", -1);
|
|
102
|
+
console.error(e);
|
|
103
|
+
process.exit(-1);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { text } from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
export const generateSecretKey = async () => {
|
|
5
|
+
const secretKey = crypto.randomBytes(32).toString("hex");
|
|
6
|
+
|
|
7
|
+
await text({
|
|
8
|
+
message: "Secret Key: ",
|
|
9
|
+
initialValue: secretKey,
|
|
10
|
+
});
|
|
11
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { type Platform, getCwd } from "@hot-updater/plugin-core";
|
|
3
|
+
import { loadConfig } from "@hot-updater/plugin-core";
|
|
4
|
+
|
|
5
|
+
export interface PruneOptions {
|
|
6
|
+
platform: Platform;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const prune = async (options: PruneOptions) => {
|
|
10
|
+
const s = p.spinner();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const config = await loadConfig();
|
|
14
|
+
if (!config) {
|
|
15
|
+
console.error("No config found. Please run `hot-updater init` first.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const cwd = getCwd();
|
|
20
|
+
|
|
21
|
+
const deployPlugin = config.deploy({
|
|
22
|
+
cwd,
|
|
23
|
+
// spinner: s,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
s.start("Checking existing updates");
|
|
27
|
+
const updateSources = await deployPlugin.getUpdateSources();
|
|
28
|
+
|
|
29
|
+
const activeSources = updateSources.filter((source) => source.enabled);
|
|
30
|
+
const inactiveSources = updateSources.filter((source) => !source.enabled);
|
|
31
|
+
|
|
32
|
+
if (inactiveSources.length === 0) {
|
|
33
|
+
s.stop("No inactive versions found", -1);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
s.message("Pruning updates");
|
|
38
|
+
|
|
39
|
+
await deployPlugin.setUpdateSources(activeSources);
|
|
40
|
+
await deployPlugin.commitUpdateSource();
|
|
41
|
+
|
|
42
|
+
for (const source of inactiveSources) {
|
|
43
|
+
const key = await deployPlugin.deleteBundle(
|
|
44
|
+
options.platform,
|
|
45
|
+
source.bundleVersion,
|
|
46
|
+
);
|
|
47
|
+
p.log.info(`deleting: ${key}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
s.stop("Done");
|
|
51
|
+
} catch (e) {
|
|
52
|
+
s.stop("Pruning Failed !", -1);
|
|
53
|
+
console.error(e);
|
|
54
|
+
process.exit(-1);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { version } from "@/packageJson";
|
|
2
|
+
import boxen from "boxen";
|
|
3
|
+
import picocolors from "picocolors";
|
|
4
|
+
|
|
5
|
+
export const banner = boxen(
|
|
6
|
+
[
|
|
7
|
+
`${picocolors.bold("Hot Updater - React Native OTA Solution")} v${version}`,
|
|
8
|
+
"",
|
|
9
|
+
`Github: ${picocolors.magenta(
|
|
10
|
+
picocolors.underline("https://github.com/gronxb/hot-updater"),
|
|
11
|
+
)}`,
|
|
12
|
+
"Give a ⭐️ if you like it!",
|
|
13
|
+
].join("\n"),
|
|
14
|
+
{
|
|
15
|
+
padding: 1,
|
|
16
|
+
borderStyle: "round",
|
|
17
|
+
borderColor: "redBright",
|
|
18
|
+
textAlignment: "center",
|
|
19
|
+
},
|
|
20
|
+
);
|
package/src/config.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { openConsole } from "@/commands/console";
|
|
3
|
+
import { type DeployOptions, deploy } from "@/commands/deploy";
|
|
4
|
+
import { generateSecretKey } from "@/commands/generateSecretKey";
|
|
5
|
+
import { prune } from "@/commands/prune";
|
|
6
|
+
import { banner } from "@/components/banner";
|
|
7
|
+
import { version } from "@/packageJson";
|
|
8
|
+
import { getPlatform } from "@/prompts/getPlatform";
|
|
9
|
+
import { getDefaultTargetVersion } from "@/utils/getDefaultTargetVersion";
|
|
10
|
+
import { getCwd, log } from "@hot-updater/plugin-core";
|
|
11
|
+
import { Command, Option } from "commander";
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name("hot-updater")
|
|
17
|
+
.description(banner)
|
|
18
|
+
.version(version as string);
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("init")
|
|
22
|
+
.description("Initialize Hot Updater")
|
|
23
|
+
.action(() => {
|
|
24
|
+
console.log("Initializing Hot Updater");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command("deploy")
|
|
29
|
+
.description("deploy a new version")
|
|
30
|
+
.addOption(
|
|
31
|
+
new Option("-p, --platform <platform>", "specify the platform").choices([
|
|
32
|
+
"ios",
|
|
33
|
+
"android",
|
|
34
|
+
]),
|
|
35
|
+
)
|
|
36
|
+
.addOption(
|
|
37
|
+
new Option("-t, --target-version <targetVersion>", "specify the platform"),
|
|
38
|
+
)
|
|
39
|
+
.addOption(
|
|
40
|
+
new Option("-f, --force-update", "force update the app").default(false),
|
|
41
|
+
)
|
|
42
|
+
.action(async (options: DeployOptions) => {
|
|
43
|
+
if (!options.platform) {
|
|
44
|
+
options.platform = await getPlatform(
|
|
45
|
+
"Which platform do you want to deploy?",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
deploy(options);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
program.command("console").description("open the console").action(openConsole);
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command("generate-secret-key")
|
|
55
|
+
.description("generate a new secret key")
|
|
56
|
+
.action(generateSecretKey);
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command("app-version")
|
|
60
|
+
.description("get the current app version")
|
|
61
|
+
|
|
62
|
+
.action(async () => {
|
|
63
|
+
const path = getCwd();
|
|
64
|
+
const androidVersion = await getDefaultTargetVersion(path, "android");
|
|
65
|
+
const iosVersion = await getDefaultTargetVersion(path, "ios");
|
|
66
|
+
|
|
67
|
+
log.info(`Android version: ${androidVersion}`);
|
|
68
|
+
log.info(`iOS version: ${iosVersion}`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command("prune")
|
|
73
|
+
.description("prune all the inactive versions")
|
|
74
|
+
.addOption(
|
|
75
|
+
new Option("-p, --platform <platform>", "specify the platform").choices([
|
|
76
|
+
"ios",
|
|
77
|
+
"android",
|
|
78
|
+
]),
|
|
79
|
+
)
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
if (!options.platform) {
|
|
82
|
+
options.platform = await getPlatform(
|
|
83
|
+
"Which platform do you want to prune?",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
prune(options);
|
|
87
|
+
});
|
|
88
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { select } from "@clack/prompts";
|
|
2
|
+
import type { Platform } from "@hot-updater/utils";
|
|
3
|
+
|
|
4
|
+
export const getPlatform = async (message: string) => {
|
|
5
|
+
const platform = await select({
|
|
6
|
+
message: message,
|
|
7
|
+
initialValue: "ios" as Platform,
|
|
8
|
+
options: [
|
|
9
|
+
{ label: "ios", value: "ios" },
|
|
10
|
+
{ label: "android", value: "android" },
|
|
11
|
+
],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (typeof platform !== "string") {
|
|
15
|
+
throw new Error("Invalid platform");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return platform;
|
|
19
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
|
|
4
|
+
import JSZip from "jszip";
|
|
5
|
+
|
|
6
|
+
export const createZip = async (dirPath: string, filename: string) => {
|
|
7
|
+
const zip = new JSZip();
|
|
8
|
+
|
|
9
|
+
async function addFiles(dir: string, zipFolder: JSZip) {
|
|
10
|
+
const files = await fs.readdir(dir);
|
|
11
|
+
files.sort();
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const fullPath = path.join(dir, file);
|
|
14
|
+
const stats = await fs.stat(fullPath);
|
|
15
|
+
|
|
16
|
+
if (stats.isDirectory()) {
|
|
17
|
+
const folder = zipFolder.folder(file);
|
|
18
|
+
if (!folder) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await addFiles(fullPath, folder);
|
|
23
|
+
} else {
|
|
24
|
+
const data = await fs.readFile(fullPath);
|
|
25
|
+
zipFolder.file(file, data);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await addFiles(dirPath, zip);
|
|
31
|
+
|
|
32
|
+
// fix hash
|
|
33
|
+
zip.forEach((_, file) => {
|
|
34
|
+
file.date = new Date(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const content = await zip.generateAsync({
|
|
38
|
+
type: "nodebuffer",
|
|
39
|
+
compression: "DEFLATE",
|
|
40
|
+
compressionOptions: {
|
|
41
|
+
level: 9,
|
|
42
|
+
},
|
|
43
|
+
platform: "UNIX",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await fs.writeFile(filename, content);
|
|
47
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const formatDate = (date: Date) => {
|
|
2
|
+
const year = date.getFullYear();
|
|
3
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
4
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
5
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
6
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
7
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
8
|
+
|
|
9
|
+
return Number(`${year}${month}${day}${hours}${minutes}${seconds}`);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function formatDateTimeFromBundleVersion(input: string): string {
|
|
13
|
+
const year = input.substring(0, 4);
|
|
14
|
+
const month = input.substring(4, 6);
|
|
15
|
+
const day = input.substring(6, 8);
|
|
16
|
+
const hour = input.substring(8, 10);
|
|
17
|
+
const minute = input.substring(10, 12);
|
|
18
|
+
const second = input.substring(12, 14);
|
|
19
|
+
|
|
20
|
+
return `${year}/${month}/${day} ${hour}:${minute}:${second}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import util from "util";
|
|
4
|
+
import type { Platform } from "@hot-updater/plugin-core";
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
|
|
7
|
+
const findXCodeProjectFilename = async (
|
|
8
|
+
cwd: string,
|
|
9
|
+
): Promise<string | null> => {
|
|
10
|
+
try {
|
|
11
|
+
const iosDirPath = path.join(cwd, "ios");
|
|
12
|
+
const dirContent = await fs.readdir(iosDirPath);
|
|
13
|
+
for (const item of dirContent) {
|
|
14
|
+
const itemPath = path.join(iosDirPath, item);
|
|
15
|
+
const stats = await fs.stat(itemPath);
|
|
16
|
+
if (stats.isDirectory()) {
|
|
17
|
+
const pbxprojPath = path.join(itemPath, "project.pbxproj");
|
|
18
|
+
try {
|
|
19
|
+
await fs.access(pbxprojPath);
|
|
20
|
+
return item;
|
|
21
|
+
} catch {
|
|
22
|
+
// Not the directory we are looking for
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getIOSVersion = async (cwd: string): Promise<string | null> => {
|
|
33
|
+
const filename = await findXCodeProjectFilename(cwd);
|
|
34
|
+
if (!filename) return null;
|
|
35
|
+
|
|
36
|
+
const projectPath = path.join(cwd, "ios", filename);
|
|
37
|
+
try {
|
|
38
|
+
const execPromise = util.promisify(exec);
|
|
39
|
+
|
|
40
|
+
const { stdout } = await execPromise(
|
|
41
|
+
`xcodebuild -project ${projectPath} -showBuildSettings | grep MARKETING_VERSION`,
|
|
42
|
+
);
|
|
43
|
+
const versionMatch = stdout.match(/MARKETING_VERSION = ([\d.]+)/);
|
|
44
|
+
return versionMatch?.[1] ? versionMatch[1] : null;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const getAndroidVersion = async (
|
|
51
|
+
cwd: string,
|
|
52
|
+
): Promise<string | null> => {
|
|
53
|
+
const buildGradlePath = path.join(cwd, "android", "app", "build.gradle");
|
|
54
|
+
try {
|
|
55
|
+
const buildGradleContent = await fs.readFile(buildGradlePath, "utf8");
|
|
56
|
+
const versionNameMatch = buildGradleContent.match(
|
|
57
|
+
/versionName\s+"([\d.]+)"/,
|
|
58
|
+
);
|
|
59
|
+
return versionNameMatch?.[1] ? versionNameMatch[1] : null;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const getDefaultTargetVersion = async (
|
|
66
|
+
cwd: string,
|
|
67
|
+
platform: Platform,
|
|
68
|
+
): Promise<string | null> => {
|
|
69
|
+
switch (platform) {
|
|
70
|
+
case "ios":
|
|
71
|
+
return getIOSVersion(cwd);
|
|
72
|
+
case "android":
|
|
73
|
+
return getAndroidVersion(cwd);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
|
|
4
|
+
export const getFileHashFromFile = async (filepath: string) => {
|
|
5
|
+
try {
|
|
6
|
+
// Read the file
|
|
7
|
+
const fileBuffer = await fs.readFile(filepath).catch((error) => {
|
|
8
|
+
console.error("Error reading the file:", error);
|
|
9
|
+
throw error;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Calculate the hash
|
|
13
|
+
const hash = crypto.createHash("sha256");
|
|
14
|
+
hash.update(fileBuffer);
|
|
15
|
+
const fileHash = hash.digest("hex");
|
|
16
|
+
|
|
17
|
+
return fileHash;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error("Error fetching or processing the file:", error);
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function toRotated<T>(array: T[], steps: number): T[] {
|
|
2
|
+
if (!Array.isArray(array)) {
|
|
3
|
+
throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (!Number.isSafeInteger(steps)) {
|
|
7
|
+
throw new TypeError(
|
|
8
|
+
`The \`steps\` parameter must be an integer, got ${steps}.`,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { length } = array;
|
|
13
|
+
if (length === 0) {
|
|
14
|
+
return [...array];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const normalizedSteps = ((steps % length) + length) % length;
|
|
18
|
+
if (normalizedSteps === 0) {
|
|
19
|
+
return [...array];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
...array.slice(-normalizedSteps),
|
|
24
|
+
...array.slice(0, -normalizedSteps),
|
|
25
|
+
];
|
|
26
|
+
}
|