diff --git a/README.md b/README.md index d51964e..c8b92d4 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Arguments: Options: -V, --version output the version number -n, --name [name] Specify the process name. (default: "default") - -w, --wallet [wallet] Path to the wallet JWK file. + -w, --wallet [wallet] Path to the wallet JWK file. (Autogenerated if not passed) -l, --lua-path [luaPath] Specify the Lua modules path seperated by semicolon. -d, --deploy [deploy] List of deployment configuration names, separated by commas. -b, --build [build] List of deployment configuration names, separated by commas. @@ -90,6 +90,7 @@ Options: --retry-count [count] Number of retries for deploying contract. (default: 10) --retry-delay [delay] Delay between retries in milliseconds. (default: 3000) --minify Reduce the size of the contract before deployment. (default: false) + --on-boot Load contract when process is spawned. (default: false) -h, --help display help for command ``` @@ -108,6 +109,12 @@ ao-deploy process.lua -n tictactoe -w wallet.json --tags name1:value1 name2:valu ao-deploy process.lua -n tictactoe -w wallet.json --tags name1:value1 name2:value2 --minify ``` +#### Example: Deploy contract with on-boot + +```sh +ao-deploy process.lua -n tictactoe -w wallet.json --tags name1:value1 name2:value2 --on-boot +``` + #### Example: Deploy contracts with configuration Here is an example using a deployment configuration: @@ -257,10 +264,11 @@ async function main() { } }); const processUrl = `https://www.ao.link/#/entity/${processId}`; - const messageUrl = `https://www.ao.link/#/message/${messageId}`; - console.log( - `\nDeployed Process: ${processUrl} \nDeployment Message: ${messageUrl}` - ); + console.log(`Deployed Process: ${processUrl}`); + if (messageId) { + const messageUrl = `https://www.ao.link/#/message/${messageId}`; + console.log(`Deployment Message: ${messageUrl}`); + } } catch (error: any) { console.log( `Deployment failed!: ${error?.message ?? "Failed to deploy contract!"}\n` @@ -287,6 +295,8 @@ The `deployContract` function accepts the following parameters within the Deploy - `processId` (optional): The process id of existing process. - `minify` (optional): Reduce the size of the contract before deployment. - `contractTransformer` (optional): Custom function to transform source code before deployment. +- `onBoot` (optional): Load contract when process is spawned. (default: false) +- `silent` (optional): Disable logging to console. (default: false) #### Example: deployContracts @@ -326,10 +336,11 @@ async function main() { if (result.status === "fulfilled") { const { processId, messageId } = result.value; const processUrl = `https://www.ao.link/#/entity/${processId}`; - const messageUrl = `https://www.ao.link/#/message/${messageId}`; - console.log( - `\nDeployed Process: ${processUrl} \nDeployment Message: ${messageUrl}` - ); + console.log(`Deployed Process: ${processUrl}`); + if (messageId) { + const messageUrl = `https://www.ao.link/#/message/${messageId}`; + console.log(`Deployment Message: ${messageUrl}`); + } } else { console.log(`Failed to deploy contract!: ${result.reason}\n`); } diff --git a/hello.lua b/hello.lua new file mode 100644 index 0000000..e69de29 diff --git a/src/cli.ts b/src/cli.ts index e5d8a24..758917f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -43,14 +43,15 @@ function getPackageJson() { function logDeploymentDetails(result: DeployResult) { const { messageId, processId, isNewProcess, configName } = result; const processUrl = chalk.green(`${aoExplorerUrl}/#/entity/${processId}`); - const messageUrl = chalk.green(`${aoExplorerUrl}/#/message/${messageId}`); const logger = Logger.init(configName); - console.log(""); if (isNewProcess) { logger.log(`Deployed Process: ${processUrl}`); } - logger.log(`Deployment Message: ${messageUrl}`); + if (messageId) { + const messageUrl = chalk.green(`${aoExplorerUrl}/#/message/${messageId}`); + logger.log(`Deployment Message: ${messageUrl}`); + } } function logBundleDetails(result: BundleResult) { @@ -151,7 +152,8 @@ program parseToInt, 3000 ) - .option("--minify", "Reduce the size of the contract before deployment."); + .option("--minify", "Reduce the size of the contract before deployment.") + .option("--on-boot", "Load contract when process is spawned."); program.parse(process.argv); @@ -196,7 +198,8 @@ async function deploymentHandler() { cuUrl: options.cuUrl, muUrl: options.muUrl }, - minify: options.minify + minify: options.minify, + onBoot: options.onBoot }); logDeploymentDetails(result); } else { diff --git a/src/lib/config.ts b/src/lib/config.ts index 15eb325..6cfd9a4 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -128,11 +128,17 @@ export class ConfigManager { "wallet", "outDir" ]; + const optionalBooleanProps: (keyof DeployConfig)[] = [ + "sqlite", + "silent", + "minify", + "onBoot" + ]; optionalAddressProps.forEach((prop) => { if (deployConfig[prop] && !isArweaveAddress(deployConfig[prop])) { throw new Error( - `Invalid optional property "${prop}" in configuration for "${keyName}": ${jsonStringify(deployConfig[prop])}` + `Invalid "${prop}" value in configuration for "${keyName}": ${jsonStringify(deployConfig[prop])}` ); } }); @@ -140,7 +146,18 @@ export class ConfigManager { optionalStringProps.forEach((prop) => { if (deployConfig[prop] && !this.#isString(deployConfig[prop])) { throw new Error( - `Invalid optional property "${prop}" in configuration for "${keyName}": ${jsonStringify(deployConfig[prop])}` + `Invalid "${prop}" value in configuration for "${keyName}": ${jsonStringify(deployConfig[prop])}` + ); + } + }); + + optionalBooleanProps.forEach((prop) => { + if ( + deployConfig[prop] !== undefined && + typeof deployConfig[prop] !== "boolean" + ) { + throw new Error( + `Invalid "${prop}" value in configuration for "${keyName}": ${jsonStringify(deployConfig[prop])}` ); } }); @@ -185,24 +202,6 @@ export class ConfigManager { ); } - if ( - deployConfig.sqlite !== undefined && - typeof deployConfig.sqlite !== "boolean" - ) { - throw new Error( - `Invalid sqlite value in configuration for "${name}": ${jsonStringify(deployConfig.sqlite)}` - ); - } - - if ( - deployConfig.minify !== undefined && - typeof deployConfig.minify !== "boolean" - ) { - throw new Error( - `Invalid minify value in configuration for "${name}": ${jsonStringify(deployConfig.minify)}` - ); - } - if ( deployConfig.contractTransformer !== undefined && typeof deployConfig.contractTransformer !== "function" diff --git a/src/lib/deploy.ts b/src/lib/deploy.ts index d4d7ff5..3672439 100644 --- a/src/lib/deploy.ts +++ b/src/lib/deploy.ts @@ -131,7 +131,9 @@ export class DeploymentsManager { sqlite, services, minify, - contractTransformer + contractTransformer, + onBoot, + silent = false }: DeployConfig): Promise { name = name || "default"; configName = configName || name; @@ -140,7 +142,7 @@ export class DeploymentsManager { delay: parseToInt(retry?.delay, 3000) }; - const logger = new Logger(configName); + const logger = new Logger(configName, silent); const aosConfig = await this.#getAosConfig(); module = isArweaveAddress(module) ? module! @@ -168,7 +170,20 @@ export class DeploymentsManager { const isNewProcess = !processId; - if (!processId) { + const loader = new LuaProjectLoader(configName, luaPath, silent); + let contractSrc = await loader.loadContract(contractPath); + + if (contractTransformer && typeof contractTransformer === "function") { + logger.log("Transforming contract...", false, false); + contractSrc = await contractTransformer(contractSrc); + } + + if (minify) { + logger.log("Minifying contract...", false, false); + contractSrc = await loader.minifyContract(contractSrc); + } + + if (isNewProcess) { logger.log("Spawning new process...", false, true); tags = Array.isArray(tags) ? tags : []; tags = [ @@ -179,6 +194,11 @@ export class DeploymentsManager { ...tags ]; + if (onBoot) { + logger.log(`Deploying: ${contractPath}`, false, true); + tags = [...tags, { name: "On-Boot", value: "Data" }]; + } + if (cron) { this.#validateCron(cron); tags = [ @@ -188,7 +208,7 @@ export class DeploymentsManager { ]; } - const data = "1984"; + const data = onBoot ? contractSrc : "1984"; processId = await retryWithDelay( () => aoInstance.spawn({ module, signer, tags, data, scheduler }), retry.count, @@ -196,64 +216,65 @@ export class DeploymentsManager { ); await pollForProcessSpawn({ processId }); - } else { - logger.log("Updating existing process...", false, true); - } - const loader = new LuaProjectLoader(configName, luaPath); - let contractSrc = await loader.loadContract(contractPath); - - if (contractTransformer && typeof contractTransformer === "function") { - logger.log("Transforming contract...", false, false); - contractSrc = await contractTransformer(contractSrc); - } - - if (minify) { - logger.log("Minifying contract...", false, false); - contractSrc = await loader.minifyContract(contractSrc); + if (onBoot) { + return { name, processId, isNewProcess, configName }; + } } - logger.log(`Deploying: ${contractPath}`, false, true); - // Load contract to process - const messageId = await retryWithDelay( - async () => - aoInstance.message({ - process: processId, - tags: [{ name: "Action", value: "Eval" }], - data: contractSrc, - signer - }), - retry.count, - retry.delay - ); + let messageId: string; + if (!onBoot || !isNewProcess) { + if (!isNewProcess) { + logger.log("Updating existing process...", false, true); + } + logger.log(`Deploying: ${contractPath}`, false, true); + // Load contract to process + messageId = await retryWithDelay( + async () => + aoInstance.message({ + process: processId!, + tags: [{ name: "Action", value: "Eval" }], + data: contractSrc, + signer + }), + retry.count, + retry.delay + ); - const { Output, Error: error } = await retryWithDelay( - async () => - aoInstance.result({ - process: processId, - message: messageId - }), - retry.count, - retry.delay - ); + const { Output, Error: error } = await retryWithDelay( + async () => + aoInstance.result({ + process: processId!, + message: messageId + }), + retry.count, + retry.delay + ); - let errorMessage = null; + let errorMessage = null; - if (Output?.data?.output) { - errorMessage = Output.data.output; - } else if (error) { - if (typeof error === "object" && Object.keys(error).length > 0) { - errorMessage = JSON.stringify(error); - } else { - errorMessage = String(error); + if (Output?.data?.output) { + errorMessage = Output.data.output; + } else if (error) { + if (typeof error === "object" && Object.keys(error).length > 0) { + errorMessage = JSON.stringify(error); + } else { + errorMessage = String(error); + } } - } - if (errorMessage) { - throw new Error(errorMessage); + if (errorMessage) { + throw new Error(errorMessage); + } } - return { name, processId, messageId, isNewProcess, configName }; + return { + name, + processId: processId!, + messageId: messageId!, + isNewProcess, + configName + }; } /** diff --git a/src/lib/loader.ts b/src/lib/loader.ts index 2935ba1..ffc33fd 100644 --- a/src/lib/loader.ts +++ b/src/lib/loader.ts @@ -26,9 +26,9 @@ export class LuaProjectLoader { #luaPath: string; #logger: Logger; - constructor(name: string, luaPath?: string) { + constructor(name: string, luaPath?: string, silent: boolean = false) { this.#luaPath = luaPath || ""; - this.#logger = Logger.init(name); + this.#logger = Logger.init(name, silent); } async #fileExists(path: string): Promise { @@ -193,15 +193,17 @@ export class LuaProjectLoader { false, true ); - console.log( - chalk.dim( - createFileTree([ - ...projectStructure.map((m) => m.path), - `${filePath} ${chalk.reset(chalk.bgGreen(" MAIN "))}` - ]) - ) - ); - console.log(""); + if (!this.#logger.silent) { + console.log( + chalk.dim( + createFileTree([ + ...projectStructure.map((m) => m.path), + `${filePath} ${chalk.reset(chalk.bgGreen(" MAIN "))}` + ]) + ) + ); + console.log(""); + } } return line.trim(); diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 33635ae..14fcab6 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -4,23 +4,30 @@ import { APP_NAME } from "./constants"; export class Logger { static #instances: Map = new Map(); #name: string; + #silent: boolean; - constructor(name: string) { + constructor(name: string, silent: boolean = false) { this.#name = name; + this.#silent = silent; } - static #getInstance(name: string): Logger { - if (!Logger.#instances.has(name)) { - Logger.#instances.set(name, new Logger(name)); + get silent() { + return this.#silent; + } + + static #getInstance(name: string, silent: boolean = false): Logger { + if (!Logger.#instances.has(`${name}-${silent}`)) { + Logger.#instances.set(`${name}-${silent}`, new Logger(name, silent)); } - return Logger.#instances.get(name)!; + return Logger.#instances.get(`${name}-${silent}`)!; } - static init(name: string) { - return this.#getInstance(name); + static init(name: string, silent: boolean = false) { + return this.#getInstance(name, silent); } #logMessage(message: string, prefixNewLine: boolean, suffixNewLine: boolean) { + if (this.#silent) return; const prefix = prefixNewLine ? "\n" : ""; const suffix = suffixNewLine ? "\n" : ""; console.log(`${prefix}${message}${suffix}`); @@ -54,27 +61,38 @@ export class Logger { name: string, message: string, prefixNewLine = false, - suffixNewLine = false + suffixNewLine = false, + silent = false ) { - this.#getInstance(name).log(message, prefixNewLine, suffixNewLine); + this.#getInstance(name, silent).log(message, prefixNewLine, suffixNewLine); } static success( name: string, message: string, prefixNewLine = false, - suffixNewLine = false + suffixNewLine = false, + silent = false ) { - this.#getInstance(name).success(message, prefixNewLine, suffixNewLine); + this.#getInstance(name, silent).success( + message, + prefixNewLine, + suffixNewLine + ); } static error( name: string, message: string, prefixNewLine = false, - suffixNewLine = false + suffixNewLine = false, + silent = false ) { - this.#getInstance(name).error(message, prefixNewLine, suffixNewLine); + this.#getInstance(name, silent).error( + message, + prefixNewLine, + suffixNewLine + ); } } diff --git a/src/types/index.ts b/src/types/index.ts index 7b1fc98..54ece48 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -132,6 +132,21 @@ export interface DeployConfig { * ``` */ contractTransformer?: (source: string) => string | Promise; + + /** + * Enable AOS On-Boot loading to load contract when process is spawned. + * Sets "On-Boot=Data" tag during deployment. + * CLI: --on-boot + * @see https://github.com/permaweb/aos?tab=readme-ov-file#boot-loading + * @default false + */ + onBoot?: boolean; + + /** + * Disable logging to console + * @default false + */ + silent?: boolean; } export type Config = Record; @@ -139,7 +154,7 @@ export type Config = Record; export interface DeployResult { name: string; configName: string; - messageId: string; + messageId?: string; processId: string; isNewProcess: boolean; }