@multiplatform.one/cli 2.0.0 → 2.1.5
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/bin/multiplatformOne.mjs +1 -1
- package/lib/bin/multiplatformOne.mjs +227 -15
- package/package.json +10 -6
- package/src/bin/multiplatformOne.ts +347 -61
- package/src/index.ts +1 -1
- package/src/types.ts +2 -0
- package/lib/bin/multiplatformOne.d.mts +0 -2
- package/lib/index.d.mts +0 -2
- package/lib/types.d.mts +0 -7
package/bin/multiplatformOne.mjs
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
1
4
|
// src/bin/multiplatformOne.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
5
|
+
import fsSync from "node:fs";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import axios from "axios";
|
|
9
11
|
import { program } from "commander";
|
|
12
|
+
import dotenv from "dotenv";
|
|
13
|
+
import { execa } from "execa";
|
|
14
|
+
import inquirer from "inquirer";
|
|
15
|
+
import ora from "ora";
|
|
16
|
+
import pg from "pg";
|
|
17
|
+
var availableBackends = [
|
|
18
|
+
"api",
|
|
19
|
+
"frappe"
|
|
20
|
+
];
|
|
21
|
+
var availablePlatforms = [
|
|
22
|
+
"electron",
|
|
23
|
+
"expo",
|
|
24
|
+
"keycloak",
|
|
25
|
+
"next",
|
|
26
|
+
"storybook",
|
|
27
|
+
"storybook-expo"
|
|
28
|
+
];
|
|
10
29
|
process.env.COOKIECUTTER = `sh ${path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../scripts/cookiecutter.sh")}`;
|
|
11
30
|
program.name("multiplatform.one");
|
|
12
31
|
program.version(JSON.parse(fsSync.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8"))?.version);
|
|
13
|
-
program.command("init").option("-r, --remote <remote>", "the remote to use", "https://gitlab.com/bitspur/multiplatform.one/cookiecutter").option("-c, --checkout <branch>", "branch, tag or commit to checkout", "main").argument("[name]", "the name of the project", "").description("init multiplatform.one").action(async (name, options) => {
|
|
32
|
+
program.command("init").option("-r, --remote <remote>", "the remote to use", "https://gitlab.com/bitspur/multiplatform.one/cookiecutter").option("-c, --checkout <branch>", "branch, tag or commit to checkout", "main").option("-p, --platforms <platforms>", "platforms to keep").option("-b, --backends <backends>", "backends to keep").argument("[name]", "the name of the project", "").description("init multiplatform.one").action(async (name, options) => {
|
|
33
|
+
let { backends, platforms } = options;
|
|
14
34
|
if ((await execa("git", [
|
|
15
35
|
"rev-parse",
|
|
16
36
|
"--is-inside-work-tree"
|
|
@@ -28,9 +48,37 @@ program.command("init").option("-r, --remote <remote>", "the remote to use", "ht
|
|
|
28
48
|
}
|
|
29
49
|
])).name;
|
|
30
50
|
}
|
|
51
|
+
if (!backends) {
|
|
52
|
+
const backendsResult = (await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
message: "What backends are you using?",
|
|
55
|
+
name: "backends",
|
|
56
|
+
type: "checkbox",
|
|
57
|
+
choices: availableBackends.map((name2) => ({
|
|
58
|
+
name: name2
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
])).backends;
|
|
62
|
+
backends = backendsResult.join(",");
|
|
63
|
+
}
|
|
64
|
+
if (!platforms) {
|
|
65
|
+
const platformsResult = (await inquirer.prompt([
|
|
66
|
+
{
|
|
67
|
+
message: "What platforms are you using?",
|
|
68
|
+
name: "platforms",
|
|
69
|
+
type: "checkbox",
|
|
70
|
+
choices: availablePlatforms.map((name2) => ({
|
|
71
|
+
name: name2
|
|
72
|
+
}))
|
|
73
|
+
}
|
|
74
|
+
])).platforms;
|
|
75
|
+
platforms = platformsResult.join(",");
|
|
76
|
+
}
|
|
31
77
|
const cookieCutterConfig = {
|
|
32
78
|
default_context: {
|
|
33
|
-
name
|
|
79
|
+
name,
|
|
80
|
+
platforms,
|
|
81
|
+
backends
|
|
34
82
|
}
|
|
35
83
|
};
|
|
36
84
|
const cookieCutterConfigFile = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "multiplatform-")), "config.json");
|
|
@@ -55,7 +103,34 @@ program.command("init").option("-r, --remote <remote>", "the remote to use", "ht
|
|
|
55
103
|
});
|
|
56
104
|
}
|
|
57
105
|
});
|
|
58
|
-
program.command("update").option("-r, --remote <remote>", "the remote to use", "https://gitlab.com/bitspur/multiplatform.one/cookiecutter").option("-c, --checkout <branch>", "branch, tag or commit to checkout", "main").description("update multiplatform.one").action(async (options) => {
|
|
106
|
+
program.command("update").option("-r, --remote <remote>", "the remote to use", "https://gitlab.com/bitspur/multiplatform.one/cookiecutter").option("-c, --checkout <branch>", "branch, tag or commit to checkout", "main").option("-p, --platforms <platforms>", "platforms to keep").option("-b, --backends <backends>", "backends to keep").description("update multiplatform.one").action(async (options) => {
|
|
107
|
+
let { backends, platforms } = options;
|
|
108
|
+
if (!backends) {
|
|
109
|
+
const backendsResult = await inquirer.prompt([
|
|
110
|
+
{
|
|
111
|
+
message: "What backends are you using?",
|
|
112
|
+
name: "backends",
|
|
113
|
+
type: "checkbox",
|
|
114
|
+
choices: availableBackends.map((name) => ({
|
|
115
|
+
name
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
]);
|
|
119
|
+
backends = backendsResult.backends.join(",");
|
|
120
|
+
}
|
|
121
|
+
if (!platforms) {
|
|
122
|
+
const platformsResult = await inquirer.prompt([
|
|
123
|
+
{
|
|
124
|
+
message: "What platforms are you using?",
|
|
125
|
+
name: "platforms",
|
|
126
|
+
type: "checkbox",
|
|
127
|
+
choices: availablePlatforms.map((name) => ({
|
|
128
|
+
name
|
|
129
|
+
}))
|
|
130
|
+
}
|
|
131
|
+
]);
|
|
132
|
+
platforms = platformsResult.platforms.join(",");
|
|
133
|
+
}
|
|
59
134
|
if ((await execa("git", [
|
|
60
135
|
"rev-parse",
|
|
61
136
|
"--is-inside-work-tree"
|
|
@@ -80,11 +155,15 @@ program.command("update").option("-r, --remote <remote>", "the remote to use", "
|
|
|
80
155
|
let cookieCutterConfig;
|
|
81
156
|
if ((await fs.stat(path.resolve(projectRoot, "package.json"))).isFile() && (await fs.stat(path.resolve(projectRoot, "app/package.json"))).isFile() && JSON.parse(await fs.readFile(path.resolve(projectRoot, "app/package.json"), "utf8"))?.dependencies?.["multiplatform.one"]?.length) {
|
|
82
157
|
const name = JSON.parse(await fs.readFile(path.resolve(projectRoot, "package.json"), "utf8"))?.name;
|
|
83
|
-
if (name)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
158
|
+
if (name) {
|
|
159
|
+
cookieCutterConfig = {
|
|
160
|
+
default_context: {
|
|
161
|
+
name,
|
|
162
|
+
backends,
|
|
163
|
+
platforms
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
88
167
|
}
|
|
89
168
|
if (!cookieCutterConfig) throw new Error("not a multiplatform.one project");
|
|
90
169
|
const cookieCutterConfigFile = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "multiplatform-")), "config.json");
|
|
@@ -110,4 +189,137 @@ program.command("update").option("-r, --remote <remote>", "the remote to use", "
|
|
|
110
189
|
});
|
|
111
190
|
}
|
|
112
191
|
});
|
|
192
|
+
var waitServices = [
|
|
193
|
+
"api",
|
|
194
|
+
"frappe",
|
|
195
|
+
"postgres",
|
|
196
|
+
"keycloak"
|
|
197
|
+
];
|
|
198
|
+
program.command("wait").description("wait for a service to be ready").option("-i, --interval <interval>", "interval to wait for", 1e3).option("-t, --timeout <timeout>", "timeout to wait for", 6e5).option("-e, --dotenv <dotenv>", "dotenv file path", ".env").argument("<services>", `the services to wait for (${waitServices.join(", ")})`).action(async (servicesString, options) => {
|
|
199
|
+
dotenv.config({
|
|
200
|
+
path: options.dotenv
|
|
201
|
+
});
|
|
202
|
+
const interval = Number.parseInt(options.interval);
|
|
203
|
+
const timeout = Number.parseInt(options.timeout);
|
|
204
|
+
const services = servicesString.split(",");
|
|
205
|
+
const unreadyServices = [
|
|
206
|
+
...services
|
|
207
|
+
];
|
|
208
|
+
const spinner = ora(`waiting for ${formatServiceList(unreadyServices)}`).start();
|
|
209
|
+
function updateSpinner(readyService) {
|
|
210
|
+
unreadyServices.splice(unreadyServices.indexOf(readyService), 1);
|
|
211
|
+
spinner.stop();
|
|
212
|
+
spinner.succeed(`${readyService} is ready`);
|
|
213
|
+
if (!unreadyServices.length) return;
|
|
214
|
+
spinner.start(`waiting for ${formatServiceList(unreadyServices)}`);
|
|
215
|
+
}
|
|
216
|
+
__name(updateSpinner, "updateSpinner");
|
|
217
|
+
let timeoutId;
|
|
218
|
+
try {
|
|
219
|
+
await Promise.race([
|
|
220
|
+
Promise.all(services.map(async (service) => {
|
|
221
|
+
switch (service) {
|
|
222
|
+
case "api": {
|
|
223
|
+
await waitForApi(interval);
|
|
224
|
+
updateSpinner("api");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
case "frappe": {
|
|
228
|
+
await waitForFrappe(interval);
|
|
229
|
+
updateSpinner("frappe");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
case "postgres": {
|
|
233
|
+
await waitForPostgres(interval);
|
|
234
|
+
updateSpinner("postgres");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
case "keycloak": {
|
|
238
|
+
await waitForKeycloak(interval);
|
|
239
|
+
updateSpinner("keycloak");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
ora(`available services are ${formatServiceList(waitServices)}`).fail();
|
|
244
|
+
})),
|
|
245
|
+
new Promise((_, reject) => {
|
|
246
|
+
timeoutId = setTimeout(() => reject(new Error("Timeout")), timeout);
|
|
247
|
+
})
|
|
248
|
+
]);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
if (err instanceof Error && err.message === "Timeout") {
|
|
251
|
+
spinner.fail(`${formatServiceList(unreadyServices)} timed out after ${timeout}ms`);
|
|
252
|
+
} else {
|
|
253
|
+
spinner.fail(err);
|
|
254
|
+
}
|
|
255
|
+
process.exit(1);
|
|
256
|
+
} finally {
|
|
257
|
+
clearTimeout(timeoutId);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
function formatServiceList(services) {
|
|
261
|
+
if (services.length === 1) return services[0];
|
|
262
|
+
if (services.length === 2) return `${services[0]} and ${services[1]}`;
|
|
263
|
+
return `${services.slice(0, -1).join(", ")} and ${services[services.length - 1]}`;
|
|
264
|
+
}
|
|
265
|
+
__name(formatServiceList, "formatServiceList");
|
|
113
266
|
program.parse(process.argv);
|
|
267
|
+
async function waitForApi(interval) {
|
|
268
|
+
try {
|
|
269
|
+
const res = await axios.get(`http://localhost:${process.env.API_PORT || "5001"}/healthz`);
|
|
270
|
+
if ((res?.status || 500) < 300) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
}
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
276
|
+
return waitForApi(interval);
|
|
277
|
+
}
|
|
278
|
+
__name(waitForApi, "waitForApi");
|
|
279
|
+
async function waitForFrappe(interval) {
|
|
280
|
+
try {
|
|
281
|
+
const res = await axios.get(`${process.env.FRAPPE_BASE_URL || "http://frappe.localhost"}/api/method/ping`);
|
|
282
|
+
if ((res?.status || 500) < 300) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
}
|
|
287
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
288
|
+
return waitForFrappe(interval);
|
|
289
|
+
}
|
|
290
|
+
__name(waitForFrappe, "waitForFrappe");
|
|
291
|
+
async function waitForPostgres(interval) {
|
|
292
|
+
const client = new pg.Client({
|
|
293
|
+
database: process.env.POSTGRES_DATABASE || "postgres",
|
|
294
|
+
host: process.env.POSTGRES_HOSTNAME || "localhost",
|
|
295
|
+
password: process.env.POSTGRES_PASSWORD || "postgres",
|
|
296
|
+
port: Number.parseInt(process.env.POSTGRES_PORT || "5432"),
|
|
297
|
+
user: process.env.POSTGRES_USERNAME || "postgres"
|
|
298
|
+
});
|
|
299
|
+
try {
|
|
300
|
+
await client.connect();
|
|
301
|
+
while (true) {
|
|
302
|
+
try {
|
|
303
|
+
await client.query("SELECT 1");
|
|
304
|
+
return;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
}
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
308
|
+
}
|
|
309
|
+
} finally {
|
|
310
|
+
await client.end();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
__name(waitForPostgres, "waitForPostgres");
|
|
314
|
+
async function waitForKeycloak(interval) {
|
|
315
|
+
try {
|
|
316
|
+
const res = await axios.get(`${process.env.KEYCLOAK_BASE_URL || "http://localhost:8080"}/realms/master/.well-known/openid-configuration`);
|
|
317
|
+
if ((res?.status || 500) < 300) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
}
|
|
322
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
323
|
+
return waitForKeycloak(interval);
|
|
324
|
+
}
|
|
325
|
+
__name(waitForKeycloak, "waitForKeycloak");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"author": "BitSpur <support@bitspur.com> (https://bitspur.com)",
|
|
5
5
|
"contributors": [
|
|
6
6
|
{
|
|
@@ -48,16 +48,20 @@
|
|
|
48
48
|
"node": ">= 16.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
+
"@multiplatform.one/utils": "0.1.6",
|
|
51
52
|
"@types/inquirer": "^9.0.7",
|
|
52
53
|
"@types/node": "~20.12.13",
|
|
53
|
-
"tsup": "^8.
|
|
54
|
-
"typescript": "
|
|
55
|
-
"@multiplatform.one/utils": "^1.0.0"
|
|
54
|
+
"tsup": "^8.3.0",
|
|
55
|
+
"typescript": "^5.6.2"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"axios": "^1.7.7",
|
|
58
59
|
"commander": "^9.5.0",
|
|
59
|
-
"
|
|
60
|
-
"
|
|
60
|
+
"dotenv": "^16.4.5",
|
|
61
|
+
"execa": "^9.4.1",
|
|
62
|
+
"inquirer": "^9.3.7",
|
|
63
|
+
"ora": "^8.1.0",
|
|
64
|
+
"pg": "^8.13.0"
|
|
61
65
|
},
|
|
62
66
|
"transpileModules": [
|
|
63
67
|
"#ansi-styles",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* File: /src/bin/multiplatformOne.ts
|
|
3
3
|
* Project: @multiplatform.one/cli
|
|
4
|
-
* File Created:
|
|
4
|
+
* File Created: 01-01-1970 00:00:00
|
|
5
5
|
* Author: Clay Risser
|
|
6
6
|
* -----
|
|
7
|
-
* BitSpur Copyright 2021 - 2024
|
|
7
|
+
* BitSpur (c) Copyright 2021 - 2024
|
|
8
8
|
*
|
|
9
9
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
10
|
* you may not use this file except in compliance with the License.
|
|
@@ -19,63 +19,144 @@
|
|
|
19
19
|
* limitations under the License.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
29
|
-
import
|
|
30
|
-
import {
|
|
22
|
+
import fsSync from "node:fs";
|
|
23
|
+
import fs from "node:fs/promises";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
import axios from "axios";
|
|
28
|
+
import { program } from "commander";
|
|
29
|
+
import dotenv from "dotenv";
|
|
30
|
+
import { execa } from "execa";
|
|
31
|
+
import inquirer from "inquirer";
|
|
32
|
+
import ora from "ora";
|
|
33
|
+
import pg from "pg";
|
|
34
|
+
import type { CookieCutterConfig } from "../types";
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
const availableBackends = ["api", "frappe"];
|
|
37
|
+
const availablePlatforms = [
|
|
38
|
+
"electron",
|
|
39
|
+
"expo",
|
|
40
|
+
"keycloak",
|
|
41
|
+
"next",
|
|
42
|
+
"storybook",
|
|
43
|
+
"storybook-expo",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
process.env.COOKIECUTTER = `sh ${path.resolve(
|
|
47
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
48
|
+
"../../scripts/cookiecutter.sh",
|
|
49
|
+
)}`;
|
|
50
|
+
program.name("multiplatform.one");
|
|
34
51
|
program.version(
|
|
35
52
|
JSON.parse(
|
|
36
|
-
fsSync.readFileSync(
|
|
53
|
+
fsSync.readFileSync(
|
|
54
|
+
path.resolve(
|
|
55
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
56
|
+
"../../package.json",
|
|
57
|
+
),
|
|
58
|
+
"utf8",
|
|
59
|
+
),
|
|
37
60
|
)?.version,
|
|
38
61
|
);
|
|
39
62
|
|
|
40
63
|
program
|
|
41
|
-
.command(
|
|
42
|
-
.option(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
.command("init")
|
|
65
|
+
.option(
|
|
66
|
+
"-r, --remote <remote>",
|
|
67
|
+
"the remote to use",
|
|
68
|
+
"https://gitlab.com/bitspur/multiplatform.one/cookiecutter",
|
|
69
|
+
)
|
|
70
|
+
.option(
|
|
71
|
+
"-c, --checkout <branch>",
|
|
72
|
+
"branch, tag or commit to checkout",
|
|
73
|
+
"main",
|
|
74
|
+
)
|
|
75
|
+
.option("-p, --platforms <platforms>", "platforms to keep")
|
|
76
|
+
.option("-b, --backends <backends>", "backends to keep")
|
|
77
|
+
.argument("[name]", "the name of the project", "")
|
|
78
|
+
.description("init multiplatform.one")
|
|
46
79
|
.action(async (name, options) => {
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
let { backends, platforms } = options;
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
(
|
|
84
|
+
await execa("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
85
|
+
reject: false,
|
|
86
|
+
})
|
|
87
|
+
).exitCode === 0
|
|
88
|
+
) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"multiplatform.one cannot be initialized inside a git repository",
|
|
91
|
+
);
|
|
49
92
|
}
|
|
50
93
|
if (!name) {
|
|
51
94
|
name = (
|
|
52
95
|
await inquirer.prompt([
|
|
53
96
|
{
|
|
54
|
-
message:
|
|
55
|
-
name:
|
|
56
|
-
type:
|
|
97
|
+
message: "What is the project name?",
|
|
98
|
+
name: "name",
|
|
99
|
+
type: "input",
|
|
57
100
|
},
|
|
58
101
|
])
|
|
59
102
|
).name;
|
|
60
103
|
}
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
if (!backends) {
|
|
105
|
+
const backendsResult = (
|
|
106
|
+
await inquirer.prompt([
|
|
107
|
+
{
|
|
108
|
+
message: "What backends are you using?",
|
|
109
|
+
name: "backends",
|
|
110
|
+
type: "checkbox",
|
|
111
|
+
choices: availableBackends.map((name) => ({ name })),
|
|
112
|
+
},
|
|
113
|
+
])
|
|
114
|
+
).backends;
|
|
115
|
+
|
|
116
|
+
backends = backendsResult.join(",");
|
|
117
|
+
}
|
|
118
|
+
if (!platforms) {
|
|
119
|
+
const platformsResult = (
|
|
120
|
+
await inquirer.prompt([
|
|
121
|
+
{
|
|
122
|
+
message: "What platforms are you using?",
|
|
123
|
+
name: "platforms",
|
|
124
|
+
type: "checkbox",
|
|
125
|
+
choices: availablePlatforms.map((name) => ({ name })),
|
|
126
|
+
},
|
|
127
|
+
])
|
|
128
|
+
).platforms;
|
|
129
|
+
platforms = platformsResult.join(",");
|
|
130
|
+
}
|
|
131
|
+
const cookieCutterConfig: CookieCutterConfig = {
|
|
132
|
+
default_context: { name, platforms, backends },
|
|
133
|
+
};
|
|
134
|
+
const cookieCutterConfigFile = path.join(
|
|
135
|
+
await fs.mkdtemp(path.join(os.tmpdir(), "multiplatform-")),
|
|
136
|
+
"config.json",
|
|
137
|
+
);
|
|
63
138
|
try {
|
|
64
|
-
await fs.writeFile(
|
|
139
|
+
await fs.writeFile(
|
|
140
|
+
cookieCutterConfigFile,
|
|
141
|
+
JSON.stringify(cookieCutterConfig, null, 2),
|
|
142
|
+
);
|
|
65
143
|
await execa(
|
|
66
|
-
|
|
144
|
+
"sh",
|
|
67
145
|
[
|
|
68
|
-
path.resolve(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
146
|
+
path.resolve(
|
|
147
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
148
|
+
"../../scripts/init.sh",
|
|
149
|
+
),
|
|
150
|
+
"--no-input",
|
|
151
|
+
"-f",
|
|
152
|
+
"--config-file",
|
|
72
153
|
cookieCutterConfigFile,
|
|
73
|
-
|
|
154
|
+
"--checkout",
|
|
74
155
|
options.checkout,
|
|
75
156
|
options.remote,
|
|
76
157
|
],
|
|
77
158
|
{
|
|
78
|
-
stdio:
|
|
159
|
+
stdio: "inherit",
|
|
79
160
|
},
|
|
80
161
|
);
|
|
81
162
|
} finally {
|
|
@@ -84,54 +165,115 @@ program
|
|
|
84
165
|
});
|
|
85
166
|
|
|
86
167
|
program
|
|
87
|
-
.command(
|
|
88
|
-
.option(
|
|
89
|
-
|
|
90
|
-
|
|
168
|
+
.command("update")
|
|
169
|
+
.option(
|
|
170
|
+
"-r, --remote <remote>",
|
|
171
|
+
"the remote to use",
|
|
172
|
+
"https://gitlab.com/bitspur/multiplatform.one/cookiecutter",
|
|
173
|
+
)
|
|
174
|
+
.option(
|
|
175
|
+
"-c, --checkout <branch>",
|
|
176
|
+
"branch, tag or commit to checkout",
|
|
177
|
+
"main",
|
|
178
|
+
)
|
|
179
|
+
.option("-p, --platforms <platforms>", "platforms to keep")
|
|
180
|
+
.option("-b, --backends <backends>", "backends to keep")
|
|
181
|
+
.description("update multiplatform.one")
|
|
91
182
|
.action(async (options) => {
|
|
92
|
-
|
|
93
|
-
|
|
183
|
+
let { backends, platforms } = options;
|
|
184
|
+
if (!backends) {
|
|
185
|
+
const backendsResult = await inquirer.prompt([
|
|
186
|
+
{
|
|
187
|
+
message: "What backends are you using?",
|
|
188
|
+
name: "backends",
|
|
189
|
+
type: "checkbox",
|
|
190
|
+
choices: availableBackends.map((name) => ({ name })),
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
backends = backendsResult.backends.join(",");
|
|
194
|
+
}
|
|
195
|
+
if (!platforms) {
|
|
196
|
+
const platformsResult = await inquirer.prompt([
|
|
197
|
+
{
|
|
198
|
+
message: "What platforms are you using?",
|
|
199
|
+
name: "platforms",
|
|
200
|
+
type: "checkbox",
|
|
201
|
+
choices: availablePlatforms.map((name) => ({ name })),
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
204
|
+
platforms = platformsResult.platforms.join(",");
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
(
|
|
208
|
+
await execa("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
209
|
+
reject: false,
|
|
210
|
+
})
|
|
211
|
+
).exitCode !== 0
|
|
212
|
+
) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"multiplatform.one cannot be updated outside of a git repository",
|
|
215
|
+
);
|
|
94
216
|
}
|
|
95
217
|
if (
|
|
96
218
|
(
|
|
97
|
-
await execa(
|
|
219
|
+
await execa("git", ["diff", "--cached", "--quiet"], {
|
|
98
220
|
reject: false,
|
|
99
221
|
})
|
|
100
222
|
).exitCode !== 0
|
|
101
223
|
) {
|
|
102
|
-
throw new Error(
|
|
224
|
+
throw new Error(
|
|
225
|
+
"multiplatform.one cannot be updated with uncommitted changes",
|
|
226
|
+
);
|
|
103
227
|
}
|
|
104
|
-
const projectRoot = (
|
|
228
|
+
const projectRoot = (
|
|
229
|
+
await execa("git", ["rev-parse", "--show-toplevel"])
|
|
230
|
+
).stdout.trim();
|
|
105
231
|
let cookieCutterConfig: CookieCutterConfig | undefined;
|
|
106
232
|
if (
|
|
107
|
-
(await fs.stat(path.resolve(projectRoot,
|
|
108
|
-
(await fs.stat(path.resolve(projectRoot,
|
|
109
|
-
JSON.parse(
|
|
110
|
-
|
|
111
|
-
|
|
233
|
+
(await fs.stat(path.resolve(projectRoot, "package.json"))).isFile() &&
|
|
234
|
+
(await fs.stat(path.resolve(projectRoot, "app/package.json"))).isFile() &&
|
|
235
|
+
JSON.parse(
|
|
236
|
+
await fs.readFile(
|
|
237
|
+
path.resolve(projectRoot, "app/package.json"),
|
|
238
|
+
"utf8",
|
|
239
|
+
),
|
|
240
|
+
)?.dependencies?.["multiplatform.one"]?.length
|
|
112
241
|
) {
|
|
113
|
-
const name = JSON.parse(
|
|
114
|
-
|
|
242
|
+
const name = JSON.parse(
|
|
243
|
+
await fs.readFile(path.resolve(projectRoot, "package.json"), "utf8"),
|
|
244
|
+
)?.name;
|
|
245
|
+
if (name) {
|
|
246
|
+
cookieCutterConfig = { default_context: { name, backends, platforms } };
|
|
247
|
+
}
|
|
115
248
|
}
|
|
116
|
-
if (!cookieCutterConfig) throw new Error(
|
|
117
|
-
const cookieCutterConfigFile = path.join(
|
|
249
|
+
if (!cookieCutterConfig) throw new Error("not a multiplatform.one project");
|
|
250
|
+
const cookieCutterConfigFile = path.join(
|
|
251
|
+
await fs.mkdtemp(path.join(os.tmpdir(), "multiplatform-")),
|
|
252
|
+
"config.json",
|
|
253
|
+
);
|
|
118
254
|
try {
|
|
119
|
-
await fs.writeFile(
|
|
255
|
+
await fs.writeFile(
|
|
256
|
+
cookieCutterConfigFile,
|
|
257
|
+
JSON.stringify(cookieCutterConfig, null, 2),
|
|
258
|
+
);
|
|
120
259
|
await execa(
|
|
121
|
-
|
|
260
|
+
"sh",
|
|
122
261
|
[
|
|
123
|
-
path.resolve(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
262
|
+
path.resolve(
|
|
263
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
264
|
+
"../../scripts/update.sh",
|
|
265
|
+
),
|
|
266
|
+
"--no-input",
|
|
267
|
+
"-f",
|
|
268
|
+
"--config-file",
|
|
127
269
|
cookieCutterConfigFile,
|
|
128
|
-
|
|
270
|
+
"--checkout",
|
|
129
271
|
options.checkout,
|
|
130
272
|
options.remote,
|
|
131
273
|
],
|
|
132
274
|
{
|
|
133
275
|
cwd: projectRoot,
|
|
134
|
-
stdio:
|
|
276
|
+
stdio: "inherit",
|
|
135
277
|
},
|
|
136
278
|
);
|
|
137
279
|
} finally {
|
|
@@ -139,4 +281,148 @@ program
|
|
|
139
281
|
}
|
|
140
282
|
});
|
|
141
283
|
|
|
284
|
+
const waitServices = ["api", "frappe", "postgres", "keycloak"];
|
|
285
|
+
program
|
|
286
|
+
.command("wait")
|
|
287
|
+
.description("wait for a service to be ready")
|
|
288
|
+
.option("-i, --interval <interval>", "interval to wait for", 1000)
|
|
289
|
+
.option("-t, --timeout <timeout>", "timeout to wait for", 600000)
|
|
290
|
+
.option("-e, --dotenv <dotenv>", "dotenv file path", ".env")
|
|
291
|
+
.argument(
|
|
292
|
+
"<services>",
|
|
293
|
+
`the services to wait for (${waitServices.join(", ")})`,
|
|
294
|
+
)
|
|
295
|
+
.action(async (servicesString, options) => {
|
|
296
|
+
dotenv.config({ path: options.dotenv });
|
|
297
|
+
const interval = Number.parseInt(options.interval);
|
|
298
|
+
const timeout = Number.parseInt(options.timeout);
|
|
299
|
+
const services: string[] = servicesString.split(",");
|
|
300
|
+
const unreadyServices = [...services];
|
|
301
|
+
const spinner = ora(
|
|
302
|
+
`waiting for ${formatServiceList(unreadyServices)}`,
|
|
303
|
+
).start();
|
|
304
|
+
function updateSpinner(readyService: string) {
|
|
305
|
+
unreadyServices.splice(unreadyServices.indexOf(readyService), 1);
|
|
306
|
+
spinner.stop();
|
|
307
|
+
spinner.succeed(`${readyService} is ready`);
|
|
308
|
+
if (!unreadyServices.length) return;
|
|
309
|
+
spinner.start(`waiting for ${formatServiceList(unreadyServices)}`);
|
|
310
|
+
}
|
|
311
|
+
let timeoutId: NodeJS.Timeout;
|
|
312
|
+
try {
|
|
313
|
+
await Promise.race([
|
|
314
|
+
Promise.all(
|
|
315
|
+
services.map(async (service) => {
|
|
316
|
+
switch (service) {
|
|
317
|
+
case "api": {
|
|
318
|
+
await waitForApi(interval);
|
|
319
|
+
updateSpinner("api");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
case "frappe": {
|
|
323
|
+
await waitForFrappe(interval);
|
|
324
|
+
updateSpinner("frappe");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
case "postgres": {
|
|
328
|
+
await waitForPostgres(interval);
|
|
329
|
+
updateSpinner("postgres");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
case "keycloak": {
|
|
333
|
+
await waitForKeycloak(interval);
|
|
334
|
+
updateSpinner("keycloak");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
ora(
|
|
339
|
+
`available services are ${formatServiceList(waitServices)}`,
|
|
340
|
+
).fail();
|
|
341
|
+
}),
|
|
342
|
+
),
|
|
343
|
+
new Promise((_, reject) => {
|
|
344
|
+
timeoutId = setTimeout(() => reject(new Error("Timeout")), timeout);
|
|
345
|
+
}),
|
|
346
|
+
]);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
if (err instanceof Error && err.message === "Timeout") {
|
|
349
|
+
spinner.fail(
|
|
350
|
+
`${formatServiceList(unreadyServices)} timed out after ${timeout}ms`,
|
|
351
|
+
);
|
|
352
|
+
} else {
|
|
353
|
+
spinner.fail(err);
|
|
354
|
+
}
|
|
355
|
+
process.exit(1);
|
|
356
|
+
} finally {
|
|
357
|
+
clearTimeout(timeoutId);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
function formatServiceList(services: string[]): string {
|
|
362
|
+
if (services.length === 1) return services[0];
|
|
363
|
+
if (services.length === 2) return `${services[0]} and ${services[1]}`;
|
|
364
|
+
return `${services.slice(0, -1).join(", ")} and ${services[services.length - 1]}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
142
367
|
program.parse(process.argv);
|
|
368
|
+
|
|
369
|
+
async function waitForApi(interval: number) {
|
|
370
|
+
try {
|
|
371
|
+
const res = await axios.get(
|
|
372
|
+
`http://localhost:${process.env.API_PORT || "5001"}/healthz`,
|
|
373
|
+
);
|
|
374
|
+
if ((res?.status || 500) < 300) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {}
|
|
378
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
379
|
+
return waitForApi(interval);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function waitForFrappe(interval: number) {
|
|
383
|
+
try {
|
|
384
|
+
const res = await axios.get(
|
|
385
|
+
`${process.env.FRAPPE_BASE_URL || "http://frappe.localhost"}/api/method/ping`,
|
|
386
|
+
);
|
|
387
|
+
if ((res?.status || 500) < 300) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {}
|
|
391
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
392
|
+
return waitForFrappe(interval);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function waitForPostgres(interval: number) {
|
|
396
|
+
const client = new pg.Client({
|
|
397
|
+
database: process.env.POSTGRES_DATABASE || "postgres",
|
|
398
|
+
host: process.env.POSTGRES_HOSTNAME || "localhost",
|
|
399
|
+
password: process.env.POSTGRES_PASSWORD || "postgres",
|
|
400
|
+
port: Number.parseInt(process.env.POSTGRES_PORT || "5432"),
|
|
401
|
+
user: process.env.POSTGRES_USERNAME || "postgres",
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
await client.connect();
|
|
405
|
+
while (true) {
|
|
406
|
+
try {
|
|
407
|
+
await client.query("SELECT 1");
|
|
408
|
+
return;
|
|
409
|
+
} catch (error) {}
|
|
410
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
411
|
+
}
|
|
412
|
+
} finally {
|
|
413
|
+
await client.end();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function waitForKeycloak(interval: number) {
|
|
418
|
+
try {
|
|
419
|
+
const res = await axios.get(
|
|
420
|
+
`${process.env.KEYCLOAK_BASE_URL || "http://localhost:8080"}/realms/master/.well-known/openid-configuration`,
|
|
421
|
+
);
|
|
422
|
+
if ((res?.status || 500) < 300) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
} catch (err) {}
|
|
426
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
427
|
+
return waitForKeycloak(interval);
|
|
428
|
+
}
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
package/lib/index.d.mts
DELETED