@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.
@@ -21,4 +21,4 @@
21
21
  * limitations under the License.
22
22
  */
23
23
 
24
- import '@multiplatform.one/cli/bin/multiplatformOne';
24
+ import "@multiplatform.one/cli/bin/multiplatformOne";
@@ -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 fs from "fs/promises";
3
- import fsSync from "fs";
4
- import inquirer from "inquirer";
5
- import os from "os";
6
- import path from "path";
7
- import { execa } from "execa";
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) cookieCutterConfig = {
84
- default_context: {
85
- name
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.0.0",
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.1.0",
54
- "typescript": "~5.3.3",
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
- "execa": "^9.2.0",
60
- "inquirer": "^9.2.23"
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: 22-06-2024 14:17:12
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 fs from 'fs/promises';
23
- import fsSync from 'fs';
24
- import inquirer from 'inquirer';
25
- import os from 'os';
26
- import path from 'path';
27
- import type { CookieCutterConfig } from '../types';
28
- import { execa } from 'execa';
29
- import { fileURLToPath } from 'url';
30
- import { program } from 'commander';
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
- process.env.COOKIECUTTER = `sh ${path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../scripts/cookiecutter.sh')}`;
33
- program.name('multiplatform.one');
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(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf8'),
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('init')
42
- .option('-r, --remote <remote>', 'the remote to use', 'https://gitlab.com/bitspur/multiplatform.one/cookiecutter')
43
- .option('-c, --checkout <branch>', 'branch, tag or commit to checkout', 'main')
44
- .argument('[name]', 'the name of the project', '')
45
- .description('init multiplatform.one')
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
- if ((await execa('git', ['rev-parse', '--is-inside-work-tree'], { reject: false })).exitCode === 0) {
48
- throw new Error('multiplatform.one cannot be initialized inside a git repository');
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: 'What is the project name?',
55
- name: 'name',
56
- type: 'input',
97
+ message: "What is the project name?",
98
+ name: "name",
99
+ type: "input",
57
100
  },
58
101
  ])
59
102
  ).name;
60
103
  }
61
- const cookieCutterConfig: CookieCutterConfig = { default_context: { name } };
62
- const cookieCutterConfigFile = path.join(await fs.mkdtemp(path.join(os.tmpdir(), 'multiplatform-')), 'config.json');
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(cookieCutterConfigFile, JSON.stringify(cookieCutterConfig, null, 2));
139
+ await fs.writeFile(
140
+ cookieCutterConfigFile,
141
+ JSON.stringify(cookieCutterConfig, null, 2),
142
+ );
65
143
  await execa(
66
- 'sh',
144
+ "sh",
67
145
  [
68
- path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../scripts/init.sh'),
69
- '--no-input',
70
- '-f',
71
- '--config-file',
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
- '--checkout',
154
+ "--checkout",
74
155
  options.checkout,
75
156
  options.remote,
76
157
  ],
77
158
  {
78
- stdio: 'inherit',
159
+ stdio: "inherit",
79
160
  },
80
161
  );
81
162
  } finally {
@@ -84,54 +165,115 @@ program
84
165
  });
85
166
 
86
167
  program
87
- .command('update')
88
- .option('-r, --remote <remote>', 'the remote to use', 'https://gitlab.com/bitspur/multiplatform.one/cookiecutter')
89
- .option('-c, --checkout <branch>', 'branch, tag or commit to checkout', 'main')
90
- .description('update multiplatform.one')
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
- if ((await execa('git', ['rev-parse', '--is-inside-work-tree'], { reject: false })).exitCode !== 0) {
93
- throw new Error('multiplatform.one cannot be updated outside of a git repository');
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('git', ['diff', '--cached', '--quiet'], {
219
+ await execa("git", ["diff", "--cached", "--quiet"], {
98
220
  reject: false,
99
221
  })
100
222
  ).exitCode !== 0
101
223
  ) {
102
- throw new Error('multiplatform.one cannot be updated with uncommitted changes');
224
+ throw new Error(
225
+ "multiplatform.one cannot be updated with uncommitted changes",
226
+ );
103
227
  }
104
- const projectRoot = (await execa('git', ['rev-parse', '--show-toplevel'])).stdout.trim();
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, 'package.json'))).isFile() &&
108
- (await fs.stat(path.resolve(projectRoot, 'app/package.json'))).isFile() &&
109
- JSON.parse(await fs.readFile(path.resolve(projectRoot, 'app/package.json'), 'utf8'))?.dependencies?.[
110
- 'multiplatform.one'
111
- ]?.length
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(await fs.readFile(path.resolve(projectRoot, 'package.json'), 'utf8'))?.name;
114
- if (name) cookieCutterConfig = { default_context: { name } };
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('not a multiplatform.one project');
117
- const cookieCutterConfigFile = path.join(await fs.mkdtemp(path.join(os.tmpdir(), 'multiplatform-')), 'config.json');
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(cookieCutterConfigFile, JSON.stringify(cookieCutterConfig, null, 2));
255
+ await fs.writeFile(
256
+ cookieCutterConfigFile,
257
+ JSON.stringify(cookieCutterConfig, null, 2),
258
+ );
120
259
  await execa(
121
- 'sh',
260
+ "sh",
122
261
  [
123
- path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../scripts/update.sh'),
124
- '--no-input',
125
- '-f',
126
- '--config-file',
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
- '--checkout',
270
+ "--checkout",
129
271
  options.checkout,
130
272
  options.remote,
131
273
  ],
132
274
  {
133
275
  cwd: projectRoot,
134
- stdio: 'inherit',
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
@@ -19,4 +19,4 @@
19
19
  * limitations under the License.
20
20
  */
21
21
 
22
- export {};
22
+ export type {};
package/src/types.ts CHANGED
@@ -21,6 +21,8 @@
21
21
 
22
22
  export interface CookieCutterConfig {
23
23
  default_context: {
24
+ backends: string;
24
25
  name: string;
26
+ platforms: string;
25
27
  };
26
28
  }
@@ -1,2 +0,0 @@
1
-
2
- export { }
package/lib/index.d.mts DELETED
@@ -1,2 +0,0 @@
1
-
2
- export { }
package/lib/types.d.mts DELETED
@@ -1,7 +0,0 @@
1
- interface CookieCutterConfig {
2
- default_context: {
3
- name: string;
4
- };
5
- }
6
-
7
- export type { CookieCutterConfig };