@multiplatform.one/cli
Advanced tools
Comparing version 2.1.7 to 2.1.8
{ | ||
"name": "@multiplatform.one/cli", | ||
"version": "2.1.7", | ||
"version": "2.1.8", | ||
"author": "BitSpur <support@bitspur.com> (https://bitspur.com)", | ||
@@ -14,11 +14,11 @@ "contributors": [ | ||
"source": "src/index.ts", | ||
"types": "lib/index.d.mts", | ||
"types": "types/index.d.ts", | ||
"exports": { | ||
".": { | ||
"import": "./lib/index.mjs", | ||
"types": "./lib/index.d.mts" | ||
"types": "./types/index.d.ts" | ||
}, | ||
"./bin/multiplatformOne": { | ||
"import": "./lib/bin/multiplatformOne.mjs", | ||
"types": "./lib/bin/multiplatformOne.d.mts" | ||
"types": "./types/bin/multiplatformOne.d.ts" | ||
}, | ||
@@ -34,3 +34,4 @@ "./package.json": "./package.json" | ||
"scripts", | ||
"src" | ||
"src", | ||
"types" | ||
], | ||
@@ -53,16 +54,15 @@ "license": "Apache-2.0", | ||
"devDependencies": { | ||
"@multiplatform.one/utils": "0.1.8", | ||
"@types/inquirer": "^9.0.7", | ||
"@types/node": "~20.12.13", | ||
"tsup": "^8.3.0", | ||
"typescript": "^5.6.2" | ||
"@types/node": "~20.12.14", | ||
"tsup": "^8.3.5", | ||
"typescript": "~5.3.3" | ||
}, | ||
"dependencies": { | ||
"axios": "^1.7.7", | ||
"@multiplatform.one/utils": "0.1.9", | ||
"commander": "^9.5.0", | ||
"dotenv": "^16.4.5", | ||
"execa": "^9.4.1", | ||
"dotenv": "^16.4.7", | ||
"execa": "^9.5.2", | ||
"inquirer": "^9.3.7", | ||
"ora": "^8.1.0", | ||
"pg": "^8.13.0" | ||
"ora": "^8.1.1", | ||
"yaml": "^2.7.0" | ||
}, | ||
@@ -75,4 +75,5 @@ "transpileModules": [ | ||
"scripts": { | ||
"build": "tsup" | ||
"build": "rm -rf lib types 2>/dev/null && tsc -b --emitDeclarationOnly && tsup", | ||
"typecheck": "tsc --noEmit" | ||
} | ||
} |
@@ -1,8 +0,8 @@ | ||
/** | ||
/* | ||
* File: /src/bin/multiplatformOne.ts | ||
* Project: @multiplatform.one/cli | ||
* File Created: 01-01-1970 00:00:00 | ||
* File Created: 10-01-2025 21:02:36 | ||
* Author: Clay Risser | ||
* ----- | ||
* BitSpur (c) Copyright 2021 - 2024 | ||
* BitSpur (c) Copyright 2021 - 2025 | ||
* | ||
@@ -27,3 +27,11 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
import { fileURLToPath } from "node:url"; | ||
import axios from "axios"; | ||
import { | ||
formatServiceList, | ||
projectRoot, | ||
waitForApi, | ||
waitForFrappe, | ||
waitForKeycloak, | ||
waitForPostgres, | ||
waitServices, | ||
} from "@multiplatform.one/utils/dev"; | ||
import { program } from "commander"; | ||
@@ -34,6 +42,6 @@ import dotenv from "dotenv"; | ||
import ora from "ora"; | ||
import pg from "pg"; | ||
import type { CookieCutterConfig } from "../types"; | ||
const availableBackends = ["api", "frappe"]; | ||
const availableServices = ["api", "frappe", "solana", "ethereum", "sui"]; | ||
const defaultDotenvPath = path.resolve(projectRoot, ".env"); | ||
const availablePlatforms = [ | ||
@@ -43,5 +51,8 @@ "electron", | ||
"keycloak", | ||
"next", | ||
"one", | ||
"storybook", | ||
"storybook-expo", | ||
"vocs", | ||
"vscode", | ||
"webext", | ||
]; | ||
@@ -78,9 +89,8 @@ | ||
) | ||
.option("-p, --platforms <platforms>", "platforms to keep") | ||
.option("-b, --backends <backends>", "backends to keep") | ||
.option("-p, --platforms <platforms>", "platforms to use") | ||
.option("-s, --services <services>", "services to use") | ||
.argument("[name]", "the name of the project", "") | ||
.description("init multiplatform.one") | ||
.action(async (name, options) => { | ||
let { backends, platforms } = options; | ||
let { services, platforms } = options; | ||
if ( | ||
@@ -108,15 +118,14 @@ ( | ||
} | ||
if (!backends) { | ||
const backendsResult = ( | ||
if (!services) { | ||
const servicesResult = ( | ||
await inquirer.prompt([ | ||
{ | ||
message: "What backends are you using?", | ||
name: "backends", | ||
message: "What services are you using?", | ||
name: "services", | ||
type: "checkbox", | ||
choices: availableBackends.map((name) => ({ name })), | ||
choices: availableServices.map((name) => ({ name })), | ||
}, | ||
]) | ||
).backends; | ||
backends = backendsResult.join(","); | ||
).services; | ||
services = servicesResult.join(","); | ||
} | ||
@@ -137,3 +146,3 @@ if (!platforms) { | ||
const cookieCutterConfig: CookieCutterConfig = { | ||
default_context: { name, platforms, backends }, | ||
default_context: { name, platforms, services }, | ||
}; | ||
@@ -186,16 +195,16 @@ const cookieCutterConfigFile = path.join( | ||
.option("-p, --platforms <platforms>", "platforms to keep") | ||
.option("-b, --backends <backends>", "backends to keep") | ||
.option("-s, --services <services>", "services to keep") | ||
.description("update multiplatform.one") | ||
.action(async (options) => { | ||
let { backends, platforms } = options; | ||
if (!backends) { | ||
const backendsResult = await inquirer.prompt([ | ||
let { services, platforms } = options; | ||
if (!services) { | ||
const servicesResult = await inquirer.prompt([ | ||
{ | ||
message: "What backends are you using?", | ||
name: "backends", | ||
message: "What services are you using?", | ||
name: "services", | ||
type: "checkbox", | ||
choices: availableBackends.map((name) => ({ name })), | ||
choices: availableServices.map((name) => ({ name })), | ||
}, | ||
]); | ||
backends = backendsResult.backends.join(","); | ||
services = servicesResult.services.join(","); | ||
} | ||
@@ -253,3 +262,5 @@ if (!platforms) { | ||
if (name) { | ||
cookieCutterConfig = { default_context: { name, backends, platforms } }; | ||
cookieCutterConfig = { | ||
default_context: { name, services, platforms }, | ||
}; | ||
} | ||
@@ -292,3 +303,2 @@ } | ||
const waitServices = ["api", "frappe", "postgres", "keycloak"]; | ||
program | ||
@@ -305,134 +315,389 @@ .command("wait") | ||
.action(async (servicesString, options) => { | ||
dotenv.config({ path: options.dotenv }); | ||
dotenv.config({ path: options.dotenv || defaultDotenvPath }); | ||
const interval = Number.parseInt(options.interval); | ||
const timeout = Number.parseInt(options.timeout); | ||
const services: string[] = servicesString.split(","); | ||
const unreadyServices = [...services]; | ||
const spinner = ora( | ||
`waiting for ${formatServiceList(unreadyServices)}`, | ||
).start(); | ||
function updateSpinner(readyService: string) { | ||
unreadyServices.splice(unreadyServices.indexOf(readyService), 1); | ||
spinner.stop(); | ||
spinner.succeed(`${readyService} is ready`); | ||
if (!unreadyServices.length) return; | ||
spinner.start(`waiting for ${formatServiceList(unreadyServices)}`); | ||
} | ||
let timeoutId: NodeJS.Timeout; | ||
try { | ||
await Promise.race([ | ||
Promise.all( | ||
services.map(async (service) => { | ||
switch (service) { | ||
case "api": { | ||
await waitForApi(interval); | ||
updateSpinner("api"); | ||
return; | ||
} | ||
case "frappe": { | ||
await waitForFrappe(interval); | ||
updateSpinner("frappe"); | ||
return; | ||
} | ||
case "postgres": { | ||
await waitForPostgres(interval); | ||
updateSpinner("postgres"); | ||
return; | ||
} | ||
case "keycloak": { | ||
await waitForKeycloak(interval); | ||
updateSpinner("keycloak"); | ||
return; | ||
} | ||
} | ||
ora( | ||
`available services are ${formatServiceList(waitServices)}`, | ||
).fail(); | ||
}), | ||
), | ||
new Promise((_, reject) => { | ||
timeoutId = setTimeout(() => reject(new Error("Timeout")), timeout); | ||
}), | ||
]); | ||
await waitWithSpinner(services, { interval, timeout }); | ||
} catch (err) { | ||
if (err instanceof Error && err.message === "Timeout") { | ||
spinner.fail( | ||
`${formatServiceList(unreadyServices)} timed out after ${timeout}ms`, | ||
); | ||
} else { | ||
spinner.fail(err); | ||
} | ||
process.exit(1); | ||
} finally { | ||
clearTimeout(timeoutId); | ||
} | ||
}); | ||
function formatServiceList(services: string[]): string { | ||
if (services.length === 1) return services[0]; | ||
if (services.length === 2) return `${services[0]} and ${services[1]}`; | ||
return `${services.slice(0, -1).join(", ")} and ${services[services.length - 1]}`; | ||
} | ||
program | ||
.command("mesh") | ||
.description("start the mesh server") | ||
.option("-a, --api", "use api", false) | ||
.option("-e, --dotenv <dotenv>", "dotenv file path", ".env") | ||
.option("-f, --frappe", "use frappe", false) | ||
.option("-i, --interval <interval>", "interval to wait for", "1000") | ||
.option("-p, --port <port>", "port to run mesh on", "5002") | ||
.option("-t, --timeout <timeout>", "timeout to wait for", "600000") | ||
.action(async (options) => { | ||
dotenv.config({ path: options.dotenv || defaultDotenvPath }); | ||
process.env.UWS_HTTP_MAX_HEADERS_SIZE = "16384"; | ||
if (options.frappe || options.api) { | ||
process.env.MESH_API = options.api ? "1" : "0"; | ||
process.env.MESH_FRAPPE = options.frappe ? "1" : "0"; | ||
} | ||
if ( | ||
!(await fs | ||
.stat(path.resolve(projectRoot, "app/main.ts")) | ||
.catch(() => false)) | ||
) { | ||
process.env.MESH_APP = "0"; | ||
} | ||
if ( | ||
!(await fs | ||
.stat(path.resolve(projectRoot, "frappe/package.json")) | ||
.catch(() => false)) | ||
) { | ||
process.env.MESH_FRAPPE = "0"; | ||
} | ||
const services = [ | ||
...(process.env.MESH_API === "1" ? ["api"] : []), | ||
...(process.env.MESH_FRAPPE === "1" ? ["frappe"] : []), | ||
]; | ||
if (services.length > 0) { | ||
const interval = Number.parseInt(options.interval); | ||
const timeout = Number.parseInt(options.timeout); | ||
try { | ||
await waitWithSpinner(services, { interval, timeout }); | ||
} catch (err) { | ||
process.exit(1); | ||
} | ||
} | ||
await execa( | ||
"mesh", | ||
[ | ||
"dev", | ||
"--port", | ||
Number(options.port || process.env.MESH_PORT || 5002).toString(), | ||
], | ||
{ | ||
stdio: "inherit", | ||
}, | ||
); | ||
}); | ||
program.parse(process.argv); | ||
program | ||
.command("build") | ||
.argument( | ||
"[args...]", | ||
`build to run: ${[...availableServices, "packages"].join(", ")} (default: all)`, | ||
) | ||
.description("run build command") | ||
.action(async (args: string[]) => { | ||
args = args.flatMap((arg) => arg.split(",")); | ||
const packages = args.includes("packages"); | ||
const projects = args.filter((arg) => arg !== "packages"); | ||
if ((!projects.length && !packages) || packages) { | ||
await execa( | ||
"turbo", | ||
[ | ||
"run", | ||
"build", | ||
"--filter", | ||
"'!./platforms/*'", | ||
"--filter", | ||
"'!./api'", | ||
"--filter", | ||
"'!./solana'", | ||
"--filter", | ||
"'!./ethereum'", | ||
"--filter", | ||
"'!./sui'", | ||
], | ||
{ | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}, | ||
); | ||
} | ||
for (const platform of await fs.readdir("platforms")) { | ||
if ( | ||
((!projects.length && !packages) || projects.includes(platform)) && | ||
(await fs | ||
.access(path.join(projectRoot, "platforms", platform, "package.json")) | ||
.then( | ||
() => | ||
JSON.parse( | ||
fsSync.readFileSync( | ||
path.join(projectRoot, "platforms", platform, "package.json"), | ||
"utf8", | ||
), | ||
).scripts?.build, | ||
) | ||
.catch(() => false)) | ||
) { | ||
await execa("./mkpm", [`${platform}/build`], { | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}); | ||
} | ||
} | ||
for (const service of availableServices) { | ||
if ( | ||
((!projects.length && !packages) || projects.includes(service)) && | ||
(await fs | ||
.access(path.join(projectRoot, service, "package.json")) | ||
.then( | ||
() => | ||
JSON.parse( | ||
fsSync.readFileSync( | ||
path.join(projectRoot, service, "package.json"), | ||
"utf8", | ||
), | ||
).scripts?.build, | ||
) | ||
.catch(() => false)) | ||
) { | ||
await execa("./mkpm", [`${service}/build`], { | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}); | ||
} | ||
} | ||
}); | ||
async function waitForApi(interval: number) { | ||
try { | ||
const res = await axios.get( | ||
`http://localhost:${process.env.API_PORT || "5001"}/healthz`, | ||
); | ||
if ((res?.status || 500) < 300) { | ||
return; | ||
program | ||
.command("test") | ||
.argument( | ||
"[args...]", | ||
`test to run: ${[...availableServices, "packages"].join(", ")} (default: all)`, | ||
) | ||
.description("run test command") | ||
.action(async (args: string[]) => { | ||
args = args.flatMap((arg) => arg.split(",")); | ||
const packages = args.includes("packages"); | ||
const projects = args.filter((arg) => arg !== "packages"); | ||
if ((!projects.length && !packages) || packages) { | ||
await execa( | ||
"turbo", | ||
[ | ||
"run", | ||
"test", | ||
"--filter", | ||
"'!./platforms/*'", | ||
"--filter", | ||
"'!./api'", | ||
"--filter", | ||
"'!./solana'", | ||
"--filter", | ||
"'!./ethereum'", | ||
"--filter", | ||
"'!./sui'", | ||
], | ||
{ | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}, | ||
); | ||
} | ||
} catch (err) {} | ||
await new Promise((resolve) => setTimeout(resolve, interval)); | ||
return waitForApi(interval); | ||
} | ||
for (const platform of await fs.readdir("platforms")) { | ||
if ( | ||
((!projects.length && !packages) || projects.includes(platform)) && | ||
(await fs | ||
.access(path.join(projectRoot, "platforms", platform, "package.json")) | ||
.then( | ||
() => | ||
JSON.parse( | ||
fsSync.readFileSync( | ||
path.join(projectRoot, "platforms", platform, "package.json"), | ||
"utf8", | ||
), | ||
).scripts?.test, | ||
) | ||
.catch(() => false)) | ||
) { | ||
await execa("./mkpm", [`${platform}/test`], { | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}); | ||
} | ||
} | ||
for (const service of availableServices) { | ||
if ( | ||
((!projects.length && !packages) || projects.includes(service)) && | ||
(await fs | ||
.access(path.join(projectRoot, service, "package.json")) | ||
.then( | ||
() => | ||
JSON.parse( | ||
fsSync.readFileSync( | ||
path.join(projectRoot, service, "package.json"), | ||
"utf8", | ||
), | ||
).scripts?.test, | ||
) | ||
.catch(() => false)) | ||
) { | ||
await execa("./mkpm", [`${service}/test`], { | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}); | ||
} | ||
} | ||
}); | ||
async function waitForFrappe(interval: number) { | ||
try { | ||
const res = await axios.get( | ||
`${process.env.FRAPPE_BASE_URL || "http://frappe.localhost"}/api/method/ping`, | ||
); | ||
if ((res?.status || 500) < 300) { | ||
return; | ||
program | ||
.command("check") | ||
.argument("[args...]", "checks to run: spelling, types, lint (default: all)") | ||
.description("run check command") | ||
.action(async (args: string[]) => { | ||
args = args.flatMap((arg) => arg.split(",")); | ||
const spelling = !args.length || args.includes("spelling"); | ||
const types = !args.length || args.includes("types"); | ||
const lint = !args.length || args.includes("lint"); | ||
let exitCode = 0; | ||
if (spelling) { | ||
try { | ||
await execa( | ||
"cspell", | ||
[ | ||
"--unique", | ||
"`(git ls-files && (git lfs ls-files | cut -d' ' -f3))`", | ||
], | ||
{ | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}, | ||
); | ||
} catch (err) { | ||
if (err instanceof Error && "exitCode" in err) { | ||
exitCode = (err as { exitCode: number }).exitCode; | ||
} else { | ||
exitCode = 1; | ||
} | ||
} | ||
} | ||
} catch (err) {} | ||
await new Promise((resolve) => setTimeout(resolve, interval)); | ||
return waitForFrappe(interval); | ||
} | ||
if (types) { | ||
try { | ||
await execa("turbo", ["run", "typecheck"], { | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}); | ||
} catch (err) { | ||
if (err instanceof Error && "exitCode" in err) { | ||
exitCode = (err as { exitCode: number }).exitCode; | ||
} else { | ||
exitCode = 1; | ||
} | ||
} | ||
} | ||
if (lint) { | ||
try { | ||
await execa( | ||
"biome", | ||
[ | ||
"check", | ||
"--fix", | ||
"--unsafe", | ||
"`(git ls-files && (git lfs ls-files | cut -d' ' -f3)) | sort | uniq -u | grep -E '(html)|(s?css)|(md)|(json)|(yaml)|([jt]sx?)$'`", | ||
], | ||
{ | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}, | ||
); | ||
} catch (err) { | ||
if (err instanceof Error && "exitCode" in err) { | ||
exitCode = (err as { exitCode: number }).exitCode; | ||
} else { | ||
exitCode = 1; | ||
} | ||
} | ||
} | ||
process.exit(exitCode); | ||
}); | ||
async function waitForPostgres(interval: number) { | ||
const client = new pg.Client({ | ||
database: process.env.POSTGRES_DATABASE || "postgres", | ||
host: process.env.POSTGRES_HOSTNAME || "localhost", | ||
password: process.env.POSTGRES_PASSWORD || "postgres", | ||
port: Number.parseInt(process.env.POSTGRES_PORT || "5432"), | ||
user: process.env.POSTGRES_USERNAME || "postgres", | ||
program | ||
.command("count") | ||
.description("count lines of code") | ||
.action(async () => { | ||
await execa( | ||
"cloc", | ||
[ | ||
"`(git ls-files && (git lfs ls-files | cut -d' ' -f3)) | sort | uniq -u | grep -E '(html)|(s?css)|(md)|(json)|(yaml)|([jt]sx?)$'`", | ||
], | ||
{ | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}, | ||
); | ||
}); | ||
program | ||
.command("generate") | ||
.description("run generate command") | ||
.action(async () => { | ||
await execa("turbo", ["run", "generate"], { | ||
stdio: "inherit", | ||
shell: true, | ||
cwd: projectRoot, | ||
}); | ||
}); | ||
program.parse(process.argv); | ||
async function waitWithSpinner( | ||
services: string[], | ||
options: { interval?: number; timeout?: number } = {}, | ||
) { | ||
const { interval = 1000, timeout = 600000 } = options; | ||
const waitFunctions = { | ||
api: waitForApi, | ||
frappe: waitForFrappe, | ||
postgres: waitForPostgres, | ||
keycloak: waitForKeycloak, | ||
}; | ||
const unreadyServices = [...services]; | ||
const spinner = ora( | ||
`waiting for ${formatServiceList(unreadyServices)}`, | ||
).start(); | ||
function updateSpinner(readyService: string) { | ||
unreadyServices.splice(unreadyServices.indexOf(readyService), 1); | ||
spinner.succeed(`${readyService} is ready`); | ||
if (unreadyServices.length) { | ||
spinner.start(`waiting for ${formatServiceList(unreadyServices)}`); | ||
} | ||
} | ||
let timeoutId: NodeJS.Timeout | undefined; | ||
try { | ||
await client.connect(); | ||
while (true) { | ||
try { | ||
await client.query("SELECT 1"); | ||
return; | ||
} catch (error) {} | ||
await new Promise((resolve) => setTimeout(resolve, interval)); | ||
await Promise.race([ | ||
Promise.all( | ||
services.map(async (service) => { | ||
const waitFn = waitFunctions[service]; | ||
if (!waitFn) throw new Error(`Unknown service: ${service}`); | ||
await waitFn(interval); | ||
updateSpinner(service); | ||
}), | ||
), | ||
new Promise((_, reject) => { | ||
timeoutId = setTimeout(() => reject(new Error("Timeout")), timeout); | ||
}), | ||
]); | ||
} catch (err) { | ||
const error = err as Error; | ||
if (error.message === "Timeout") { | ||
spinner.fail( | ||
`${formatServiceList(unreadyServices)} timed out after ${timeout}ms`, | ||
); | ||
} else { | ||
spinner.fail(error.message); | ||
} | ||
throw error; | ||
} finally { | ||
await client.end(); | ||
clearTimeout(timeoutId); | ||
} | ||
} | ||
async function waitForKeycloak(interval: number) { | ||
try { | ||
const res = await axios.get( | ||
`${process.env.KEYCLOAK_BASE_URL || "http://localhost:8080"}/realms/master/.well-known/openid-configuration`, | ||
); | ||
if ((res?.status || 500) < 300) { | ||
return; | ||
} | ||
} catch (err) {} | ||
await new Promise((resolve) => setTimeout(resolve, interval)); | ||
return waitForKeycloak(interval); | ||
} |
@@ -1,8 +0,8 @@ | ||
/* | ||
/** | ||
* File: /src/types.ts | ||
* Project: @multiplatform.one/cli | ||
* File Created: 23-06-2024 10:13:38 | ||
* File Created: 10-01-2025 21:02:36 | ||
* Author: Clay Risser | ||
* ----- | ||
* BitSpur (c) Copyright 2021 - 2024 | ||
* BitSpur (c) Copyright 2021 - 2025 | ||
* | ||
@@ -24,3 +24,3 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
default_context: { | ||
backends: string; | ||
services: string; | ||
name: string; | ||
@@ -27,0 +27,0 @@ platforms: string; |
Sorry, the diff of this file is not supported yet
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 5 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 8 instances in 1 package
123098
4
19
1326
+ Addedyaml@^2.7.0
+ Added@multiplatform.one/utils@0.1.9(transitive)
+ Addedyaml@2.7.0(transitive)
- Removedaxios@^1.7.7
- Removedpg@^8.13.0
Updateddotenv@^16.4.7
Updatedexeca@^9.5.2
Updatedora@^8.1.1