diff --git a/.github/workflows/deno.yaml b/.github/workflows/deno.yaml index 5566f77..06932de 100644 --- a/.github/workflows/deno.yaml +++ b/.github/workflows/deno.yaml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - deno-version: [1.42.0, "v1.x"] + deno-version: [1.43.3, "v1.x"] steps: - name: Git Checkout diff --git a/.gitignore b/.gitignore index b4d5943..ffc3218 100644 --- a/.gitignore +++ b/.gitignore @@ -45,11 +45,15 @@ coverage # Lumocs generated site _site -# Node files -.npmrc +# Node/npm files package-lock.json -package.json node_modules +.npmrc +package.json +*.tgz + +# Bun files +.bun.lockb # VSCode files .vscode \ No newline at end of file diff --git a/README.md b/README.md index 7debbe0..9cc1e3c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![PUP](https://cdn.jsdelivr.net/gh/hexagon/pup@master/docs/src/resources/pup_dark.png) -Pup is a powerful universal process manager developed with Deno, designed to keep your scripts, applications and services alive. +Pup is a powerful universal process manager developed with JavaScript, designed to keep your scripts, applications and services alive. _For detailed documentation, visit [pup.56k.guru](https://pup.56k.guru)._ @@ -25,10 +25,12 @@ _For detailed documentation, visit [pup.56k.guru](https://pup.56k.guru)._ ### Installation -To install Pup, make sure you run the latest version of Deno (`deno upgrade`), then open your terminal and execute the following command: +To install Pup, make sure you run the latest version of your runtime environment, then open your terminal and execute the following command: + +**Deno**: ```bash -deno run -Ar jsr:@pup/pup@1.0.0-rc.39 setup --channel prerelease +deno run -Ar jsr:@pup/pup@1.0.0-rc.40 setup --channel prerelease ``` This command downloads the latest version of Pup and installs it on your system. The `--channel prerelease` option is included as there is no stable version of Pup yet. Read more abour release diff --git a/application.meta.ts b/application.meta.ts index 69c70ca..a333afa 100644 --- a/application.meta.ts +++ b/application.meta.ts @@ -21,10 +21,11 @@ const Application = { name: "pup", - version: "1.0.0-rc.39", + version: "1.0.0-rc.40", url: "jsr:@pup/pup@$VERSION", + description: "Powerful universal process manager, designed to keep your scripts, applications and services alive.", canary_url: "https://raw.githubusercontent.com/Hexagon/pup/main/pup.ts", - deno: null, /* Minimum stable version of Deno required to run Pup (without --unstable-* flags) */ + deno: "1.43.0", /* Minimum stable version of Deno required to run Pup (without --unstable-* flags) */ deno_unstable: "1.43.0", /* Minimum version of Deno required to run Pup (with --unstable-* flags) */ repository: "https://github.com/hexagon/pup", changelog: "https://hexagon.github.io/pup/changelog.html", diff --git a/deno.json b/deno.json index 6e272bf..533c458 100644 --- a/deno.json +++ b/deno.json @@ -1,16 +1,12 @@ { "name": "@pup/pup", - "version": "1.0.0-rc.39", + "version": "1.0.0-rc.40", "exports": { ".": "./pup.ts", "./lib": "./mod.ts" }, - "unstable": [ - "kv" - ], - "fmt": { "lineWidth": 200, "semiColons": false, @@ -29,40 +25,43 @@ }, "tasks": { - "update-deps": "deno run --allow-read=. --allow-net=jsr.io,registry.npmjs.org jsr:@check/deps", - "check": "deno fmt --check && deno lint && deno check --unstable-kv pup.ts && deno test --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run --unstable-kv --coverage=cov_profile && echo \"Generating coverage\" && deno coverage cov_profile --exclude=pup/test --lcov --output=cov_profile.lcov", + "check-deps": "deno run -r --allow-read=. --allow-net=jsr.io,registry.npmjs.org jsr:@check/deps", + "check": "deno fmt --check && deno lint && deno check pup.ts && deno test --trace-leaks --allow-read --allow-write --allow-env --allow-net --allow-sys --allow-run --coverage=cov_profile && echo \"Generating coverage\" && deno coverage cov_profile --exclude=pup/test --lcov --output=cov_profile.lcov", "check-coverage": "deno task check && genhtml cov_profile.lcov --output-directory cov_profile/html && lcov --list cov_profile.lcov && deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts cov_profile/html", "build-schema": "deno run --allow-write --allow-read --allow-env=XDG_DATA_HOME,HOME tools/build-schema.ts && deno fmt", "build-versions": "deno run --allow-read --allow-write --allow-env tools/release.ts && deno fmt", - "build": "deno task check && deno task build-schema && deno task build-versions" + "generate-package-json": "deno run -A tools/generate-package-json.ts", + "build": "deno task check && deno task build-schema && deno task build-versions && deno task build-package-json && npm pack", + "publish-npm": "deno task build && npm publish" }, "imports": { - "@cross/deepmerge": "jsr:@cross/deepmerge@^1.0.0", - "@cross/env": "jsr:@cross/env@^1.0.2", - "@cross/fs": "jsr:@cross/fs@^0.1.11", - "@cross/jwt": "jsr:@cross/jwt@^0.4.7", - "@cross/runtime": "jsr:@cross/runtime@^1.0.0", - "@cross/service": "jsr:@cross/service@^1.0.3", - "@cross/test": "jsr:@cross/test@^0.0.9", - "@cross/utils": "jsr:@cross/utils@^0.12.0", + "@cross/deepmerge": "jsr:@cross/deepmerge@~1.0.0", + "@cross/env": "jsr:@cross/env@~1.0.2", + "@cross/fs": "jsr:@cross/fs@~0.1.11", + "@cross/jwt": "jsr:@cross/jwt@~0.4.7", + "@cross/kv": "jsr:@cross/kv@^0.16.3", + "@cross/runtime": "jsr:@cross/runtime@~1.0.0", + "@cross/service": "jsr:@cross/service@~1.0.3", + "@cross/test": "jsr:@cross/test@~0.0.9", + "@cross/utils": "jsr:@cross/utils@~0.13.0", "@hexagon/croner": "jsr:@hexagon/croner@^8.0.2", - "@oak/oak": "jsr:@oak/oak@^15.0.0", - "@pup/api-client": "jsr:@pup/api-client@^1.0.6", - "@pup/api-definitions": "jsr:@pup/api-definitions@^1.0.2", - "@pup/common": "jsr:@pup/common@^1.0.3", - "@pup/plugin": "jsr:@pup/plugin@^1.0.1", - "@std/assert": "jsr:@std/assert@^0.224.0", - "@std/async": "jsr:@std/async@^0.224.0", - "@std/encoding": "jsr:@std/encoding@^0.224.0", - "@std/io": "jsr:@std/io@^0.224.0", - "@std/path": "jsr:@std/path@^0.224.0", - "@std/semver": "jsr:@std/semver@^0.224.0", - "dax-sh": "npm:dax-sh@^0.41.0", - "filesize": "npm:filesize@^10.1.1", - "json5": "npm:json5@^2.2.3", - "timeago.js": "npm:timeago.js@^4.0.2", - "zod": "npm:zod@^3.23.6", - "zod-to-json-schema": "npm:zod-to-json-schema@^3.23.0" + "@oak/oak": "jsr:@oak/oak@^16.1.0", + "@pup/api-client": "jsr:@pup/api-client@~2.0.0", + "@pup/api-definitions": "jsr:@pup/api-definitions@~2.0.0", + "@pup/common": "jsr:@pup/common@~1.0.3", + "@pup/plugin": "jsr:@pup/plugin@~1.0.1", + "@std/assert": "jsr:@std/assert@^0.226.0", + "@std/async": "jsr:@std/async@~0.224.0", + "@std/encoding": "jsr:@std/encoding@^1.0.0", + "@std/io": "jsr:@std/io@~0.224.0", + "@std/path": "jsr:@std/path@^0.225.1", + "@std/semver": "jsr:@std/semver@~0.224.0", + "dax-sh": "npm:dax-sh@~0.41.0", + "filesize": "npm:filesize@~10.1.1", + "json5": "npm:json5@~2.2.3", + "timeago.js": "npm:timeago.js@~4.0.2", + "zod": "npm:zod@~3.23.6", + "zod-to-json-schema": "npm:zod-to-json-schema@~3.23.0" } } diff --git a/docs/src/_data.json b/docs/src/_data.json index 4cf688d..b792588 100644 --- a/docs/src/_data.json +++ b/docs/src/_data.json @@ -6,7 +6,7 @@ "description": "Universal Process Manager" }, "substitute": { - "$PUP_VERSION": "1.0.0-rc.39" + "$PUP_VERSION": "1.0.0-rc.40" }, "top_links": [ { diff --git a/docs/src/changelog.md b/docs/src/changelog.md index f18657d..d9a6b28 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -9,6 +9,12 @@ nav_order: 13 All notable changes to this project will be documented in this section. +## [1.0.0-rc.40] - Unreleased + +- fix(core): Replace `Deno.Kv` with `@cross/kv` for cross-runtime compatibility, more compact logs and avoiding `--unstable` +- fix(core): Internal changes to support Node and Bun +- fix(core): Removes the `sysinfo.ts`-module in favor of `@cross/utils/sysinfo` + ## [1.0.0-rc.39] - 2024-05-04 - fix(core): Clustered processes were duplicated in API and `pup status` diff --git a/docs/src/contributing/packaging.md b/docs/src/contributing/packaging.md index 1e560bf..bf35513 100644 --- a/docs/src/contributing/packaging.md +++ b/docs/src/contributing/packaging.md @@ -14,8 +14,7 @@ If you have experience with software packaging, your contribution can greatly en - Pup can be compiled into an executable prior to packaging using `deno compile`. The procedure is described at [https://deno.com/manual@v1.34.3/tools/compiler](https://deno.com/manual@v1.34.3/tools/compiler). The command should be similar to - `deno compile --allow-all --reload --unstable-kv --output pup pup.ts --external-installer`. The `--unstable-kv` flag should be included if the version you are packaging (mostly pre-releases) - requires unstable features according to `versions.json`. + `deno compile --allow-all --reload --output pup pup.ts --external-installer`. - The `--external-installer` argument to the Pup script disables the built-in installer, hiding `setup` and `upgrade` options from `--help`. diff --git a/lib/cli/main.ts b/lib/cli/main.ts index 096e951..5ad2024 100644 --- a/lib/cli/main.ts +++ b/lib/cli/main.ts @@ -429,21 +429,19 @@ async function main() { * Base argument: logs */ if (baseArgument === "logs") { - const logStore = `${await toPersistentPath(configFile as string)}/.main.log` + const logStore = `${await toPersistentPath(configFile as string)}/.main.db` const logger = new Logger(configuration!.logger || {}, logStore) + await logger.init() const startTimestamp = checkedArgs.get("start") ? new Date(Date.parse(checkedArgs.get("start")!)).getTime() : undefined const endTimestamp = checkedArgs.get("end") ? new Date(Date.parse(checkedArgs.get("end")!)).getTime() : undefined const numberOfRows = checkedArgs.get("n") ? parseInt(checkedArgs.get("n")!, 10) : undefined - let logs = await logger.getLogContents(checkedArgs.get("id"), startTimestamp, endTimestamp) - logs = logs.filter((log) => { - const { processId, severity } = log - const severityFilter = !checkedArgs.get("severity") || checkedArgs.get("severity") === "" || checkedArgs.get("severity")!.toLowerCase() === severity.toLowerCase() - const processFilter = !checkedArgs.get("id") || checkedArgs.get("id") === "" || checkedArgs.get("id")!.toLowerCase() === processId.toLowerCase() - return severityFilter && processFilter - }) - if (numberOfRows) { - logs = logs.slice(-numberOfRows) - } + const logs = await logger.getLogContents( + (!checkedArgs.get("id") || checkedArgs.get("id") === "") ? undefined : checkedArgs.get("id")!.toLowerCase(), + startTimestamp, + endTimestamp, + (!checkedArgs.get("severity") || checkedArgs.get("severity") === "") ? undefined : checkedArgs.get("severity")!.toLowerCase(), + numberOfRows, + ) if (logs && logs.length > 0) { const logWithColors = configuration!.logger?.colors ?? true for (const log of logs) { @@ -496,7 +494,7 @@ async function main() { exit(1) } } catch (_e) { - console.error("Action failed: Could not contact the Pup instance.") + console.error("Action failed: Could not contact the Pup instance.", _e) exit(1) } } @@ -587,14 +585,16 @@ async function main() { pup.init() // Register for running pup.terminate() if not already run on clean exit - let hasRunShutdownCode = false - globalThis.addEventListener("beforeunload", (evt) => { - if (!hasRunShutdownCode) { - evt.preventDefault() - hasRunShutdownCode = true - ;(async () => await pup.terminate(30000))() - } - }) + if (globalThis.addEventListener) { + let hasRunShutdownCode = false + globalThis.addEventListener("beforeunload", (evt) => { + if (!hasRunShutdownCode) { + evt.preventDefault() + hasRunShutdownCode = true + ;(async () => await pup.terminate(30000))() + } + }) + } if (CurrentRuntime === Runtime.Deno) { // This is needed to trigger termination in Deno diff --git a/lib/cli/status.ts b/lib/cli/status.ts index c456f3e..ceeb874 100644 --- a/lib/cli/status.ts +++ b/lib/cli/status.ts @@ -14,8 +14,8 @@ import { filesize } from "filesize" import { blockedFormatter, codeFormatter, naFormatter, restartsFormatter, statusFormatter } from "./formatters/strings.ts" import { timeagoFormatter } from "./formatters/times.ts" import { Configuration, DEFAULT_REST_API_HOSTNAME } from "../core/configuration.ts" -import { resolve } from "@std/path" import { ApiApplicationState } from "@pup/api-definitions" +import { toResolvedAbsolutePath } from "@pup/common/path" /** * Helper which print the status of all running processes, @@ -29,8 +29,8 @@ import { ApiApplicationState } from "@pup/api-definitions" export function printStatus(configFile: string, configuration: Configuration, cwd: string | undefined, status: ApiApplicationState) { // Print configuration console.log("") - console.log(Colors.bold("Configuration:") + "\t" + resolve(configFile)) - console.log(Colors.bold("Working dir:") + "\t" + cwd || "Not set (default: pup)") + console.log(Colors.bold("Configuration:") + "\t" + toResolvedAbsolutePath(configFile)) + console.log(Colors.bold("Working dir:") + "\t" + (cwd ? toResolvedAbsolutePath(cwd as string) : "Not set (default: pup)")) console.log(Colors.bold("Instance name:") + "\t" + (configuration.name || "Not set")) console.log(Colors.bold("Rest API URL:") + "\thttp://" + (configuration.api?.hostname || DEFAULT_REST_API_HOSTNAME) + ":" + status.port) diff --git a/lib/cli/upgrade.ts b/lib/cli/upgrade.ts index 14f8f31..6a20df7 100644 --- a/lib/cli/upgrade.ts +++ b/lib/cli/upgrade.ts @@ -176,9 +176,6 @@ export async function upgrade( if (ignoreCertificateErrorsString && ignoreCertificateErrorsString !== "") { installCmd.push(ignoreCertificateErrorsString) } - if (unstableInstall) { - installCmd.push("--unstable-kv") - } installCmd.push("-n", "pup") // Installed command name = pup installCmd.push(canaryInstall ? versions.canary_url : (requestedVersion as Version).url) diff --git a/lib/core/api.ts b/lib/core/api.ts index 094fbe2..013dc05 100644 --- a/lib/core/api.ts +++ b/lib/core/api.ts @@ -100,6 +100,6 @@ export class PupApi { this._pup.logger[severity](`api-${consumer}`, message) } public async getLogs(processId?: string, startTimeStamp?: number, endTimeStamp?: number, nRows?: number): Promise { - return await this._pup.logger.getLogContents(processId, startTimeStamp, endTimeStamp, nRows) + return await this._pup.logger.getLogContents(processId, startTimeStamp, endTimeStamp, undefined, nRows) } } diff --git a/lib/core/cluster.ts b/lib/core/cluster.ts index 8bdf86b..e7002e8 100644 --- a/lib/core/cluster.ts +++ b/lib/core/cluster.ts @@ -25,7 +25,7 @@ class Cluster extends Process { this.setInstances(this.config.cluster?.instances || 1) } - public override start = async (reason?: string, restart?: boolean) => { + public override start = async (reason?: string, restart?: boolean): Promise => { await Promise.all( this.processes.map((process) => process.start(reason, restart)), ) @@ -35,7 +35,7 @@ class Cluster extends Process { return true } - public setInstances = (nInstances: number) => { + public setInstances = (nInstances: number): void => { const backends = [] // ToDo: If there already are processes, reuse, stop or add @@ -125,15 +125,15 @@ class Cluster extends Process { return Promise.allSettled(results).then((results) => results.every((result) => result)) } - public override restart = (reason: string) => { + public override restart = (reason: string): void => { this.processes.forEach((process) => process.restart(reason)) } - public override block = (reason: string) => { + public override block = (reason: string): void => { this.processes.forEach((process) => process.block(reason)) } - public override unblock = (reason: string) => { + public override unblock = (reason: string): void => { this.processes.forEach((process) => process.unblock(reason)) } @@ -175,7 +175,7 @@ class Cluster extends Process { return clusterStatus } - public cleanup = () => { + public cleanup = (): void => { this.loadBalancerWorker?.terminate() this.loadBalancerWorker = null } diff --git a/lib/core/configuration.ts b/lib/core/configuration.ts index c67c4ad..a74f72f 100644 --- a/lib/core/configuration.ts +++ b/lib/core/configuration.ts @@ -10,7 +10,7 @@ import { PluginConfiguration } from "@pup/plugin" // Logger constants export const DEFAULT_INTERNAL_LOG_HOURS = 48 -export const KV_SIZE_LIMIT_BYTES = 65_536 +export const KV_LIMIT_STRING_LENGTH_BYTES = 65_536 // Core constants export const MAINTENANCE_INTERVAL_MS = 900_000 diff --git a/lib/core/loadbalancer.ts b/lib/core/loadbalancer.ts index 054ec87..7d3be50 100644 --- a/lib/core/loadbalancer.ts +++ b/lib/core/loadbalancer.ts @@ -5,6 +5,7 @@ * @license MIT */ +import { CurrentRuntime, Runtime } from "@cross/runtime" import { LOAD_BALANCER_DEFAULT_VALIDATION_INTERVAL_S } from "./configuration.ts" export enum BalancingStrategy { @@ -77,7 +78,13 @@ export class LoadBalancer { private setupValidationTimer(): number { const timer = setInterval(() => this.validateBackends(), this.validationInterval * 1000) // Make the timer non-blocking - Deno.unrefTimer(timer) + if (CurrentRuntime === Runtime.Deno) { + Deno.unrefTimer(timer) + // @ts-ignore unref exists in node and bun + } else if (timer.unref) { + // @ts-ignore unref exists in node and bun + timer.unref() + } return timer } diff --git a/lib/core/logger.ts b/lib/core/logger.ts index 1cdcdfd..37cb28c 100644 --- a/lib/core/logger.ts +++ b/lib/core/logger.ts @@ -7,7 +7,9 @@ */ import { stripAnsi } from "@cross/utils" -import { type GlobalLoggerConfiguration, KV_SIZE_LIMIT_BYTES, type ProcessConfiguration } from "./configuration.ts" +import { type GlobalLoggerConfiguration, KV_LIMIT_STRING_LENGTH_BYTES, type ProcessConfiguration } from "./configuration.ts" +import { KV, KVQuery, KVQueryRange } from "@cross/kv" +import { writeFile } from "@cross/fs" export interface LogEvent { severity: string @@ -30,10 +32,19 @@ class Logger { private config: GlobalLoggerConfiguration = {} private attachedLogger?: AttachedLogger private storeName?: string + private kv: KV constructor(globalConfiguration: GlobalLoggerConfiguration, storeName?: string) { this.config = globalConfiguration this.storeName = storeName + this.kv = new KV({ autoSync: false }) + } + + public async init(): Promise { + await this.kv.open(this.storeName!) + + // Forcefully unlock ledger in case of a stale lock, this can be done as there is other means of preventing multiple running instances + await this.kv.forceUnlockLedger() } // Used for attaching the logger hook @@ -42,81 +53,82 @@ class Logger { } // Prepare log event selector - private prepareSelector(processId?: string, startTimeStamp?: number, endTimeStamp?: number): { prefix: Deno.KvKey } | { start: Deno.KvKey; end: Deno.KvKey } { - const key = processId ? ["logs_by_process", processId] : ["logs_by_time"] + private prepareSelector(processId?: string, startTimeStamp?: number, endTimeStamp?: number, severity?: string): KVQuery { + const key: KVQuery = ["logs_by_time"] if (startTimeStamp || endTimeStamp) { - const startKey: (string | number)[] = [...key, startTimeStamp || 0] - const endKey: (string | number)[] = [...key, endTimeStamp || Infinity] - return { start: startKey, end: endKey } + const rangeSelector: KVQueryRange = {} + if (startTimeStamp) { + rangeSelector.from = startTimeStamp + } + if (endTimeStamp) { + rangeSelector.to = endTimeStamp + } + key.push(rangeSelector) + } else if (processId || severity) { + key.push({}) } - return { prefix: key } + if (processId) { + key.push(processId) + } else { + key.push({}) + } + if (severity) { + key.push(severity) + } + return key } // Fetch logs from store - private async fetchLogsFromStore(selector: { prefix: Deno.KvKey } | { start: Deno.KvKey; end: Deno.KvKey }, nRows?: number): Promise { - const store = await Deno.openKv(this.storeName) - const result = await store.list(selector) + private async fetchLogsFromStore(selector: KVQuery, nRows?: number): Promise { const resultArray: LogEventData[] = [] - for await (const res of result) resultArray.push(res.value as LogEventData) - if (nRows) { - const spliceNumber = Math.max(0, resultArray.length - nRows) - resultArray.splice(0, spliceNumber) + + // Use the generator for efficient iteration + for await (const { data } of this.kv.iterate(selector, nRows, true)) { + resultArray.unshift(data) } - store.close() return resultArray } - public async getLogContents(processId?: string, startTimeStamp?: number, endTimeStamp?: number, nRows?: number): Promise { - const selector = this.prepareSelector(processId, startTimeStamp, endTimeStamp) + public async getLogContents(processId?: string, startTimeStamp?: number, endTimeStamp?: number, severity?: string, nRows?: number): Promise { + const selector = this.prepareSelector(processId, startTimeStamp, endTimeStamp, severity) return await this.fetchLogsFromStore(selector, nRows) } public async getLogsByProcess(processId: string, nRows?: number): Promise { - return await this.getLogContents(processId, undefined, undefined, nRows) + return await this.getLogContents(processId, undefined, undefined, undefined, nRows) } public async getLogsByTime(startTimeStamp: number, endTimeStamp: number, nRows?: number): Promise { - return await this.getLogContents(undefined, startTimeStamp, endTimeStamp, nRows) + return await this.getLogContents(undefined, startTimeStamp, endTimeStamp, undefined, nRows) } public async getLogsByProcessAndTime(processId: string, startTimeStamp: number, endTimeStamp: number, nRows?: number): Promise { - return await this.getLogContents(processId, startTimeStamp, endTimeStamp, nRows) + return await this.getLogContents(processId, startTimeStamp, endTimeStamp, undefined, nRows) } private async internalLog(severity: string, category: string, text: string, process?: ProcessConfiguration, timeStamp?: number) { - // Default initiator to + // Default initiator to core const initiator = process?.id || "core" timeStamp = timeStamp || Date.now() // Write to persistent log store (if a name is supplied and internal logging is enabled) const logHours = this.config.internalLogHours === undefined ? 72 : this.config.internalLogHours - if (this.storeName && logHours > 0) { - const textBytes = new TextEncoder().encode(text) - const store = await Deno.openKv(this.storeName) - let i = 0, offset = 0 - while (offset < textBytes.length) { - // Give 6000 bytes margin to make it less likely that the full serialized object exceeds the KV limit - const slice = textBytes.subarray(offset, offset + KV_SIZE_LIMIT_BYTES - 6000) - // Ignore errors when writing to log store - try { - const logObj: LogEventData = { - severity, - category, - text: new TextDecoder().decode(slice), - processId: initiator, - timeStamp: timeStamp + i, - } - await store.set(["logs_by_time", timeStamp + i], logObj) - await store.set(["logs_by_process", initiator, timeStamp + i], logObj) - await store.set(["logs_by_process_lookup", timeStamp + i], ["logs_by_process", initiator, timeStamp + i]) - i++ - } catch (error) { - console.error(`Failed to write log to store '${this.storeName}' due to '${error.message}'. The following message was not logged: ${text}.`) + if (this.kv.isOpen() && logHours > 0) { + // Ignore errors when writing to log store + try { + const logObj: LogEventData = { + severity, + category, + text: text.length > KV_LIMIT_STRING_LENGTH_BYTES ? text.substring(0, KV_LIMIT_STRING_LENGTH_BYTES) + "..." : text, + processId: initiator, + timeStamp: timeStamp, } - offset += KV_SIZE_LIMIT_BYTES - 6000 + // Append a random uuid to the key, in case two logs should arrive at the same time + await this.kv.defer(this.kv.set(["logs_by_time", timeStamp, initiator, severity, crypto.randomUUID()], logObj)) + } catch (e) { + console.error("Error while writing to log store", e) } - store.close() } // Delegate to attached logger if there is one @@ -199,7 +211,7 @@ class Logger { // Strip colors text = stripAnsi(text) try { - await Deno.writeTextFile(fileName, `${text}\n`, { append: true }) + await writeFile(fileName, `${text}\n`, { flag: "a+" }) } catch (_e) { if (!quiet) console.error(`Failed to write log '${fileName}'. The following message were not logged: ${text}.`) } @@ -226,40 +238,36 @@ class Logger { await this.internalLog("error", category, text, process, timestamp) } public async purge(keepHours: number): Promise { - if (!this.storeName) { + if (!this.kv?.isOpen()) { return 0 } + try { - const store = await Deno.openKv(this.storeName) const now = Date.now() const startTime = now - keepHours * 60 * 60 * 1000 - const logsByTimeSelector = { - prefix: ["logs_by_time"], - end: ["logs_by_time", startTime], - } + const logsByTimeSelector: KVQuery = ["logs_by_time", { to: startTime }] let rowsDeleted = 0 - for await (const entry of store.list(logsByTimeSelector)) { - await store.delete(entry.key) + for await (const entry of this.kv.iterate(logsByTimeSelector)) { + await this.kv.defer(this.kv.delete(entry.key)) rowsDeleted++ } - const logsByProcessSelector = { - prefix: ["logs_by_process_lookup"], - end: ["logs_by_process_lookup", startTime], - } - let rowsDeletedProcess = 0 - for await (const entry of store.list(logsByProcessSelector)) { - // Delete both the lookup key and the actual key - await store.delete(entry.value as Deno.KvKey) - await store.delete(entry.key) - rowsDeletedProcess++ - } - store.close() - return rowsDeleted + rowsDeletedProcess + await this.kv.defer(this.kv.vacuum()) + return rowsDeleted } catch (error) { this.log("error", `Failed to purge logs from store '${this.storeName}': ${error.message}`) return 0 } } + /** + * Gracefully shut down the logger, allowing an optional timeout + */ + public async cleanup(timeoutMs = 5000) { + try { + await this.kv?.close(timeoutMs) + } catch (e) { + console.error("Error while closing kv store: " + e.message) + } + } } export { Logger } diff --git a/lib/core/process.ts b/lib/core/process.ts index 89d9578..3327389 100644 --- a/lib/core/process.ts +++ b/lib/core/process.ts @@ -16,6 +16,7 @@ import { Cron } from "@hexagon/croner" import { delay } from "@std/async" import { ApiProcessState } from "@pup/api-definitions" +import { CurrentOS, OperatingSystem } from "@cross/runtime" interface ProcessStateChangedEvent { old?: ApiProcessState @@ -274,7 +275,7 @@ class Process { persistent: true, } delay((this.config.terminateGracePeriod ?? this.pup.configuration.terminateGracePeriod ?? 0) * 1000, graceDelayOptions).then(() => { - if (Deno.build.os == "windows") { + if (CurrentOS == OperatingSystem.Windows) { // On Windows, SIGTERM kills the process because Windows can't handle signals without cumbersome workarounds: https://stackoverflow.com/questions/35772001/how-to-handle-a-signal-sigint-on-a-windows-os-machine#35792192 this.pup.logger.log("stopping", `Killing process, reason: ${reason}`, this.config) } else { @@ -332,7 +333,7 @@ class Process { * Stops the process. * @param {string} reason - The reason for stopping the process. */ - public restart = (reason: string) => { + public restart = (reason: string): void => { this.stop(reason) this.pendingRestartReason = reason } @@ -341,7 +342,7 @@ class Process { * Blocks the process. * @param {string} reason - The reason for blocking the process. */ - public block = (reason: string) => { + public block = (reason: string): void => { this.blocked = true this.pup.logger.log("block", `Process blocked, reason: ${reason}`, this.config) } @@ -350,7 +351,7 @@ class Process { * Unblocks the process. * @param {string} reason - The reason for unblocking the process. */ - public unblock = (reason: string) => { + public unblock = (reason: string): void => { this.blocked = false this.pup.logger.log("unblock", `Process unblocked, reason: ${reason}`, this.config) } diff --git a/lib/core/pup.ts b/lib/core/pup.ts index eea171e..95fd568 100644 --- a/lib/core/pup.ts +++ b/lib/core/pup.ts @@ -31,6 +31,8 @@ import { rm } from "@cross/fs" import { findFreePort } from "./port.ts" import { Plugin } from "./plugin.ts" import { GenerateToken, SecondsToExpiry } from "../common/token.ts" +import { CurrentRuntime, Runtime } from "@cross/runtime" +import { delay } from "@std/async" interface InstructionResponse { success: boolean action?: string @@ -80,17 +82,17 @@ class Pup { this.persistentStoragePath = persistentStoragePath - statusFile = `${this.persistentStoragePath}/.main.status` // Deno KV store + statusFile = `${this.persistentStoragePath}/.main.status.db` // Cross/KV store secretFile = `${this.persistentStoragePath}/.main.secret` // Plain text file containing the JWT secret for the rest api portFile = `${this.temporaryStoragePath}/.main.port` // Plain text file containing the port number for the API - logStore = `${this.persistentStoragePath}/.main.log` // Deno KV store + logStore = `${this.persistentStoragePath}/.main.db` // Cross/KV store } // Throw on invalid configuration this.configuration = validateConfiguration(unvalidatedConfiguration) // Initialise core logger - this.logger = new Logger(this.configuration.logger ?? {}, logStore) + this.logger = new Logger(this.configuration.logger ?? {}, logStore || "./main.db") // Global error handler this.registerGlobalErrorHandler() @@ -114,7 +116,7 @@ class Pup { * This is intended to be called by global unload event * and clears any stray files */ - public cleanup = async () => { + public cleanup = async (): Promise => { for (const cleanupFilePath of this.cleanupQueue) { try { await rm(cleanupFilePath, { recursive: true }) @@ -126,9 +128,15 @@ class Pup { // Unset last application state await this.status.cleanup() + + // Close logger + await this.logger.cleanup() } - public init = async () => { + public init = async (): Promise => { + // Intialize logging + await this.logger.init() + // Initialize api await this.api() @@ -398,7 +406,13 @@ class Pup { this.maintenanceTimer = setTimeout(() => { this.maintenance() }, MAINTENANCE_INTERVAL_MS) - Deno.unrefTimer(this.maintenanceTimer) + if (CurrentRuntime === Runtime.Deno) { + Deno.unrefTimer(this.maintenanceTimer) + // @ts-ignore unref exists in node and bun + } else if (this.maintenanceTimer?.unref) { + // @ts-ignore unref exists in node and bun + this.maintenanceTimer.unref() + } } public restart(id: string, requestor: string): boolean { @@ -493,33 +507,41 @@ class Pup { // Block and stop all processes for (const process of this.processes) { process.block("terminating") - stoppingProcesses.push( - process.stop("terminating").then((result) => { - process.cleanup() - return result - }), - ) + const terminationPromise = process.stop("terminating") + stoppingProcesses.push(terminationPromise) + terminationPromise.then((result) => { + process.cleanup() + return result + }) } // Terminate api if (this.restApi) this.restApi.terminate() + await Promise.allSettled(stoppingProcesses) + + // Allow some extra time to pass to allow untracked async tasks + // (such as logs after killing a process) to finish + // - But only if at least 500ms were used as grace period + if (forceQuitMs >= 500) await delay(500) + // Cleanup await this.cleanup() - await Promise.allSettled(stoppingProcesses) - - // Deno should exit gracefully now + // Pup should exit gracefully now } private registerGlobalErrorHandler() { - addEventListener("error", (event) => { - this.logger.error( - "fatal", - `Unhandled error caught by core: ${event.error.message}`, - ) - event.preventDefault() - }) + // @ts-ignore Cross Runtime + if (globalThis.addEventListener) { + addEventListener("error", (event) => { + this.logger.error( + "fatal", + `Unhandled error caught by core: ${event.error.message}`, + ) + event.preventDefault() + }) + } } } diff --git a/lib/core/runner.ts b/lib/core/runner.ts index b7f9b1f..9fdaac1 100644 --- a/lib/core/runner.ts +++ b/lib/core/runner.ts @@ -68,6 +68,7 @@ class Runner extends BaseRunner { */ public kill(signal: Deno.Signal = "SIGTERM") { try { + // @ts-ignore Wierd complaint about "SIGPOLL" this.process?.kill(signal) } catch (_e) { // Ignore diff --git a/lib/core/status.ts b/lib/core/status.ts index c807ece..274340a 100644 --- a/lib/core/status.ts +++ b/lib/core/status.ts @@ -8,32 +8,16 @@ import { Application } from "../../application.meta.ts" import { APPLICATION_STATE_WRITE_LIMIT_MS } from "./configuration.ts" import { type Process, type ProcessInformation } from "./process.ts" -import { ApiProcessState } from "@pup/api-definitions" +import { ApiApplicationState, ApiProcessState } from "@pup/api-definitions" +import { KV } from "@cross/kv" import { Prop } from "../common/prop.ts" +import { loadAvg, memoryUsage, systemMemoryInfo, uptime } from "@cross/utils/sysinfo" +import { getCurrentOS, getCurrentRuntime, getCurrentVersion } from "@cross/runtime" +import { pid } from "@cross/utils" const started = new Date() -/** - * Represents the current status of the application. - */ -export interface ApplicationState { - pid: number - version: string - status: string - updated: string - started: string - port: number - memory: Deno.MemoryUsage - systemMemory: Deno.SystemMemoryInfo - loadAvg: number[] - osUptime: number - osRelease: string - denoVersion: { deno: string; v8: string; typescript: string } - type: string - processes: ProcessInformation[] -} - /** * Represents the status of the application and provides methods to write the status to disk or store. */ @@ -56,26 +40,29 @@ class Status { * Key ["application_state", ] is written at most once per 20 seconds. * @param applicationState The application state to be stored. */ - public async writeToStore(applicationState: ApplicationState) { - try { - const kv = await Deno.openKv(this.storeName) + public async writeToStore(applicationState: ApiApplicationState) { + if (this.storeName) { + try { + const kv = new KV({ autoSync: false, disableIndex: true }) + await kv.open(this.storeName) - // Initialize lastWrite if it's not set - if (!this.lastWrite) { - this.lastWrite = 0 - } + // Initialize lastWrite if it's not set + if (!this.lastWrite) { + this.lastWrite = 0 + } - // Write application_state at most once per APPLICATION_STATE_WRITE_LIMIT_MS - if (Date.now() - this.lastWrite > APPLICATION_STATE_WRITE_LIMIT_MS) { - this.lastWrite = Date.now() - await kv.set(["application_state", Date.now()], applicationState) - } + // Write application_state at most once per APPLICATION_STATE_WRITE_LIMIT_MS + if (Date.now() - this.lastWrite > APPLICATION_STATE_WRITE_LIMIT_MS) { + this.lastWrite = Date.now() + await kv.set(["application_state", Date.now()], applicationState) + } - // Always write last_application_state - await kv.set(["last_application_state"], applicationState) - kv.close() - } catch (e) { - console.error("Error while writing status to kv store: " + e.message) + // Always write last_application_state + await kv.set(["last_application_state"], applicationState) + await kv.close() + } catch (e) { + console.error("Error while writing status to kv store: " + e.message) + } } } @@ -84,12 +71,15 @@ class Status { * unsetting last_application_state in the kv store. */ public async cleanup() { - try { - const kv = await Deno.openKv(this.storeName) - await kv.delete(["last_application_state"]) - kv.close() - } catch (e) { - console.error("Error while writing status to kv store: " + e.message) + if (this.storeName) { + try { + const kv = new KV({ autoSync: false, disableIndex: true }) + await kv.open(this.storeName) + await kv.delete(["last_application_state"]) + await kv.close() + } catch (e) { + console.error("Error while writing status to kv store: " + e.message) + } } } @@ -104,19 +94,18 @@ class Status { return 0 } try { - const store = await Deno.openKv(this.storeName) + const kv = new KV({ autoSync: false }) + await kv.open(this.storeName) const now = Date.now() const startTime = now - keepHours * 60 * 60 * 1000 - const logsByTimeSelector = { - prefix: ["application_state"], - end: ["application_state", startTime], - } + const logsByTimeSelector = ["application_state", { to: startTime }] let rowsDeleted = 0 - for await (const entry of store.list(logsByTimeSelector)) { + for await (const entry of kv.iterate(logsByTimeSelector)) { rowsDeleted++ - await store.delete(entry.key) + await kv.delete(entry.key) } - store.close() + await kv.vacuum() + await kv.close() return rowsDeleted } catch (error) { console.error(`Failed to purge logs from store '${this.storeName}': ${error.message}`) @@ -129,24 +118,30 @@ class Status { * @param processes The list of processes to retrieve the statuses from. * @returns The application state object. */ - public applicationState(processes: Process[], port?: Prop): ApplicationState { + public applicationState(processes: Process[], port?: Prop): ApiApplicationState { const processStates: ProcessInformation[] = [] for (const p of processes) { processStates.push(p.getStatus()) } + const memory = memoryUsage() + const systemMemory = systemMemoryInfo() + const loadAverage = loadAvg() + const osUptime = uptime() + const osRelease = getCurrentOS().toString() + const runtime = getCurrentRuntime().toString() + " " + getCurrentVersion() return { - pid: Deno.pid, + pid: pid(), version: Application.version, status: ApiProcessState[ApiProcessState.RUNNING], updated: new Date().toISOString(), started: started.toISOString(), - memory: Deno.memoryUsage(), + memory, port: port ? parseInt(port.fromCache()!, 10) : 0, - systemMemory: Deno.systemMemoryInfo(), - loadAvg: Deno.loadavg(), - osUptime: Deno.osUptime(), - osRelease: Deno.osRelease(), - denoVersion: Deno.version, + systemMemory, + loadAvg: loadAverage, + osUptime, + osRelease, + runtime, type: "main", processes: processStates, } diff --git a/lib/core/worker.ts b/lib/core/worker.ts index 9f23abb..b324ebd 100644 --- a/lib/core/worker.ts +++ b/lib/core/worker.ts @@ -9,6 +9,7 @@ import type { ProcessConfiguration, Pup } from "./pup.ts" import { readLines, StringReader } from "@std/io" import { resolve } from "@std/path" import { BaseRunner, type RunnerCallback, type RunnerResult } from "../types/runner.ts" +import { cwd } from "@cross/fs" class WorkerRunner extends BaseRunner { private worker?: Worker @@ -55,7 +56,7 @@ class WorkerRunner extends BaseRunner { if (this.pup.temporaryStoragePath) env.PUP_TEMP_STORAGE = this.pup.temporaryStoragePath if (this.pup.persistentStoragePath) env.PUP_DATA_STORAGE = this.pup.persistentStoragePath - const workingDir = this.processConfig.cwd || Deno.cwd() + const workingDir = this.processConfig.cwd || cwd() const workingDirUrl = new URL(`file://${resolve(workingDir)}/`).href try { diff --git a/lib/types/runner.ts b/lib/types/runner.ts index b4d0819..d829157 100644 --- a/lib/types/runner.ts +++ b/lib/types/runner.ts @@ -13,7 +13,7 @@ export type RunnerCallback = (pid?: number) => void export interface RunnerResult { success: boolean code?: number - signal?: Deno.Signal | null + signal?: unknown } export abstract class BaseRunner { protected readonly processConfig: ProcessConfiguration @@ -25,5 +25,5 @@ export abstract class BaseRunner { } abstract run(runningCallback: RunnerCallback): Promise - abstract kill(signal?: Deno.Signal): void + abstract kill(signal?: unknown): void } diff --git a/test/core/logger.test.ts b/test/core/logger.test.ts index 3d02ceb..369b09f 100644 --- a/test/core/logger.test.ts +++ b/test/core/logger.test.ts @@ -1,9 +1,10 @@ -import { assertEquals, assertGreater } from "@std/assert" +import { assertEquals } from "@std/assert" import { type AttachedLogger, type LogEventData, Logger } from "../../lib/core/logger.ts" import type { ProcessConfiguration } from "../../mod.ts" import { test } from "@cross/test" +import { readFile, tempfile, unlink } from "@cross/fs" -test("Logger - Creation with Global Configuration", () => { +test("Logger - Creation with Global Configuration", async () => { const globalConfig = { console: false, colors: true, @@ -15,9 +16,11 @@ test("Logger - Creation with Global Configuration", () => { const logger = new Logger(globalConfig) assertEquals(logger instanceof Logger, true) + + await logger.cleanup() }) -test("Logger - Attachment of External Logger", () => { +test("Logger - Attachment of External Logger", async () => { let externalLoggerCalled = false let externalLoggerText = "" const expectedExteralLoggerText = "Testing attached logger" @@ -34,52 +37,25 @@ test("Logger - Attachment of External Logger", () => { const logger = new Logger({}) logger.attach(externalLogger) - logger.log("test", expectedExteralLoggerText) + await logger.log("test", expectedExteralLoggerText) assertEquals(externalLoggerCalled, true) assertEquals(externalLoggerText, expectedExteralLoggerText) + + await logger.cleanup() }) -test("Logger - Logging with Different Methods", () => { +test("Logger - Logging with Different Methods", async () => { const logger = new Logger({ console: false }) - logger.log("test", "Testing log method") - logger.info("test", "Testing info method") - logger.warn("test", "Testing warn method") - logger.error("test", "Testing error method") + await logger.log("test", "Testing log method") + await logger.info("test", "Testing info method") + await logger.warn("test", "Testing warn method") + await logger.error("test", "Testing error method") assertEquals(true, true) // This is just to assert that the test passed if no errors are thrown -}) - -test("Logger - Logging Line Larger than KV Limit", async () => { - const tempStore = Deno.makeTempFileSync() - const logger = new Logger({ console: false }, tempStore) - - let chars = 60000 - let data = "" - while (chars--) { - data += "ๅœ‹" - } - await logger.log("test", data) - assertGreater(Deno.statSync(tempStore).size, 200_000) - - chars = 70000 - data = "" - while (chars--) { - data += "a" - } - await logger.log("test", data) - assertGreater(Deno.statSync(tempStore).size, 400_000) - chars = 50000 - data = "" - while (chars--) { - data += "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ" - } - await logger.log("test", data) - assertGreater(Deno.statSync(tempStore).size, 2_000_000) - - await Deno.remove(tempStore) + await logger.cleanup() }) test("Logger - File Writing with writeFile Method", async () => { @@ -88,15 +64,18 @@ test("Logger - File Writing with writeFile Method", async () => { const testText = "Testing writeFile" await logger["writeFile"](testFileName, testText) - const fileContent = await Deno.readTextFile(testFileName) + const fileContentData = await readFile(testFileName) + const fileContent = new TextDecoder().decode(fileContentData) assertEquals(fileContent, `${testText}\n`) - await Deno.remove(testFileName) + await unlink(testFileName) + + await logger.cleanup() }) test("Logger - getLogContents: Fetch all logs", async () => { - const tempStore = await Deno.makeTempDir() + "/.store" - const logger = new Logger({}, tempStore) + const logger = new Logger({}, await tempfile()) + await logger.init() const expectedLogs: LogEventData[] = [ { severity: "info", category: "test1", text: "Log 1", processId: "process-1", timeStamp: 1623626400000 }, @@ -111,11 +90,13 @@ test("Logger - getLogContents: Fetch all logs", async () => { const logs = await logger.getLogContents() assertEquals(logs, expectedLogs) + + await logger.cleanup() }) test("Logger - getLogContents: Fetch logs by process ID", async () => { - const tempStore = await Deno.makeTempDir() + "/.store" - const logger = new Logger({}, tempStore) + const logger = new Logger({}, await tempfile()) + await logger.init() const processId = "process-1" @@ -131,11 +112,13 @@ test("Logger - getLogContents: Fetch logs by process ID", async () => { const logs = await logger.getLogContents(processId) assertEquals(logs, expectedLogs) + + await logger.cleanup() }) test("Logger - getLogContents: Fetch logs by time range", async () => { - const tempStore = await Deno.makeTempDir() + "/.store" - const logger = new Logger({}, tempStore) + const logger = new Logger({}, await tempfile()) + await logger.init() const startTimeStamp = 1623626400000 // 2023-06-13T12:00:00.000Z const endTimeStamp = 1623626500000 // 2023-06-13T12:01:40.000Z @@ -152,11 +135,13 @@ test("Logger - getLogContents: Fetch logs by time range", async () => { const logs = await logger.getLogContents(undefined, startTimeStamp, endTimeStamp) assertEquals(logs, expectedLogs) + + await logger.cleanup() }) test("Logger - getLogContents: Fetch logs by process ID and time range", async () => { - const tempStore = await Deno.makeTempDir() + "/.store" - const logger = new Logger({}, tempStore) + const logger = new Logger({}, await tempfile()) + await logger.init() const processId = "process-1" const startTimeStamp = 1623626400000 // 2023-06-13T12:00:00.000Z @@ -174,4 +159,6 @@ test("Logger - getLogContents: Fetch logs by process ID and time range", async ( const logs = await logger.getLogContents(processId, startTimeStamp, endTimeStamp) assertEquals(logs, expectedLogs) + + await logger.cleanup() }) diff --git a/test/core/pup.test.ts b/test/core/pup.test.ts index aae4eed..5d694ac 100644 --- a/test/core/pup.test.ts +++ b/test/core/pup.test.ts @@ -60,8 +60,8 @@ test("Create test process. Test start, block, stop, start, unblock, start in seq assertEquals(startResult3, true) assertEquals(testProcess?.getStatus().status, ApiProcessState.STARTING) - // Terminate pup instantly - await pup.terminate(0) + // Terminate pup, allow 2.5 seconds for graceful shutdown + await pup.terminate(2500) }) test("Create test cluster. Test start, block, stop, start, unblock, start in sequence.", async () => { @@ -117,6 +117,6 @@ test("Create test cluster. Test start, block, stop, start, unblock, start in seq assertEquals(startResult3, true) assertEquals(testProcess?.getStatus().status, ApiProcessState.STARTING) - // Terminate pup instantly - await pup.terminate(0) + // Terminate pup, allow 2.5 seconds for graceful shutdown + await pup.terminate(2500) }) diff --git a/tools/generate-package-json.ts b/tools/generate-package-json.ts new file mode 100644 index 0000000..69eade0 --- /dev/null +++ b/tools/generate-package-json.ts @@ -0,0 +1,62 @@ +import { $ } from "jsr:@david/dax" + +import { Application } from "../application.meta.ts" + +import config from "../deno.json" with { type: "json" } + +// Remove existing package.json +try { + Deno.remove("./package.json") +} catch (_e) { + console.warn("Existing package.json could not be removed.") +} + +// Construct package.json Data +const packageJson = { + type: "module", + name: `@hexagon/${Application.name}`, + version: Application.version, + description: Application.description, + module: "./mod.ts", + bin: "./pup.ts", + files: [ + "lib/*", + "application.meta.ts", + "mod.ts", + "pup.ts", + "versions.json", + ], +} + +// Write initial package.json File +await Deno.writeTextFile("./package.json", JSON.stringify(packageJson, null, 2)) +console.log("package.json created successfully!") + +// 4. Install Dependencies Using jsr and npm +const npmDepdencies = [] +const jsrDepdencies = [] +for (const dependency in config.imports) { + if ((config.imports as Record)[dependency].startsWith("npm:")) { + npmDepdencies.push(dependency) + console.log(`Found npm dependency: ${dependency}`) + } else { + jsrDepdencies.push(dependency) + console.log(`Found jsr dependency: ${dependency}`) + } +} +if (npmDepdencies.length) { + await $.raw`npm i ${npmDepdencies.join(" ")}` +} +if (jsrDepdencies.length) { + await $.raw`npx jsr add ${jsrDepdencies.join(" ")}` +} + +// Read updated package.json +const updatedPackageJson = JSON.parse(await Deno.readTextFile("./package.json")) + +// Add all jsr deps as bundledDependencies +updatedPackageJson.bundledDependencies = jsrDepdencies + +// Write updated package.json +await Deno.writeTextFile("./package.json", JSON.stringify(updatedPackageJson, null, 2)) +console.log("package.json created successfully!")