@hantera/cli 20250707.0.0-develop.1 → 20250714.1.0

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.
Files changed (2) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import yargs from"yargs";import{hideBin}from"yargs/helpers";import chalk from"chalk";import open from"open";import express from"express";import crypto from"crypto";import nodeFetch,{FetchError,FormData,File}from"node-fetch";import keytar from"keytar";import https from"https";import readline from"readline-sync";import{AsciiTable3}from"ascii-table3";import fs from"fs";import path from"path";import yaml,{stringify,parseAllDocuments}from"yaml";import os from"os";import{execSync}from"child_process";import boxen from"boxen";import{createServer,build}from"vite";import vue from"@vitejs/plugin-vue";import mkcert from"vite-plugin-mkcert";import{spawn}from"node:child_process";import mime from"mime-types";import{BlobWriter,ZipWriter,BlobReader,TextReader}from"@zip.js/zip.js";let enabled=false;function enable(){enabled=true}function isEnabled(){return enabled}function verbose(func){if(!enabled){return Promise.resolve()}const value=func();if(value instanceof Promise){return new Promise((resolve=>{value.then((v=>{console.log(chalk.gray(v));resolve()}))}))}else{console.log(chalk.gray(value));return Promise.resolve()}}const service="hantera-cli";async function getSessions(){return(await keytar.findCredentials(service)).map((c=>{try{return JSON.parse(c.password)}catch(e){return null}})).filter((c=>c?.refreshExpiresAt>Date.now()))}async function saveSession(token){await keytar.setPassword(service,token.tenantId,JSON.stringify(token))}async function removeSession(sessionId){await keytar.deletePassword(service,sessionId)}async function setDefaultSession(sessionId){await keytar.setPassword(service,"default",sessionId)}async function getDefaultSession(){return await keytar.getPassword(service,"default")}async function fetch$1(sessionId="default",relativePath,init){var tokenRaw=await keytar.getPassword(service,sessionId);if(!tokenRaw){throw new Error("No token found for the environment")}const token=JSON.parse(tokenRaw);if(token.expiresAt<Date.now()&&(!token.refreshExpiresAt||token.refreshExpiresAt<Date.now())){throw new Error("No token found for the environment")}if(token.expiresAt<Date.now()){if(token.refreshExpiresAt<Date.now()){throw new Error("Tenant session expired")}verbose((()=>"Token expired, attempting to refresh"));const payload=new URLSearchParams;payload.append("grant_type","refresh_token");payload.append("refresh_token",token.refreshToken);payload.append("code_verifier",token.codeVerifier);var response=await nodeFetch(`${token.serverUrl}/oauth/token`,{agent:token.tenantId==="localhost"?new https.Agent({rejectUnauthorized:false}):undefined,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:payload.toString()});if(!response.ok){throw new Error(`Failed to refresh token (${response.statusText}): ${await response.text()}`)}const data=await response.json();const expiresAt=Date.now()+data.expires_in*1e3;const refreshExpiresAt=data.refresh_expires_in?Date.now()+data.refresh_expires_in*1e3:expiresAt;token.accessToken=data.access_token;token.refreshToken=data.refresh_token;token.expiresAt=expiresAt;token.refreshExpiresAt=refreshExpiresAt;token.tenantId=data.tenant_id;token.tenantName=data.tenant_name;await saveSession(token)}if(!init){init={}}if(!init.headers){init.headers={}}init.headers["Authorization"]=`Bearer ${token.accessToken}`;let url;if(token.tenantId==="localhost"){url="https://localhost:3300"}else{url=`https://${token.tenantId}.core.ams.hantera.cloud`}if(!relativePath.startsWith("/")){relativePath="/"+relativePath}await verbose((()=>{let result=`${init.method||"GET"} ${url+relativePath}`;if(init.headers){result+="\n"+Object.keys(init.headers).map((k=>`${k}: ${init.headers[k]}`)).join("\n")+"\n"}if(init.body&&typeof init.body==="object"&&init.headers["Content-Type"]==="application/json"){result+=`\n${JSON.stringify(init.body,null,2)}\n`}else if(init.body){result+=`\n${init.body}\n`}return result}));if(init.body&&typeof init.body==="object"&&init.headers["Content-Type"]==="application/json"){await verbose((()=>"Converting body to JSON"));init.body=JSON.stringify(init.body)}if(token.tenantId==="localhost"){init.agent=new https.Agent({rejectUnauthorized:false})}return nodeFetch(url+relativePath,init)}const clientId="00000000-0000-0000-0000-000000000010";var login={command:"login <host>",describe:"Command logging in to a Hantera tenant",builder:yargs=>yargs.option("host",{describe:"The id or hostname of the tenant to login to.",type:"string"}),handler:async argv=>{if(!argv.host){console.log(chalk.red(`No host specified`))}console.log(`Logging in to '${argv.host}'...`);let serverUrl=null;if(argv.host==="localhost"){serverUrl="https://localhost:3300"}else{serverUrl=`https://${argv.host}.core.ams.hantera.cloud`}const state=Math.random().toString(36).substring(7);let redirectUri="";const codeVerifier=Math.random().toString(36).substring(7);let codeChallenge=crypto.createHash("sha256").update(codeVerifier).digest("base64");codeChallenge=codeChallenge.replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");const web=express();let connections=new Set;web.get("/callback",(async(req,res)=>{if(req.query.state!==state){console.error("Invalid state reported by the server.");res.status(400).send("Invalid state");server.close((()=>{process.exit(1)}));return}try{const response=await nodeFetch(`${serverUrl}/oauth/token`,{agent:argv.host==="localhost"?new https.Agent({rejectUnauthorized:false}):undefined,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",client_id:clientId,redirect_uri:redirectUri,code:req.query.code,code_verifier:codeVerifier})});if(!response.ok){throw response.statusText}const data=await response.json();const expiresAt=Date.now()+data.expires_in*1e3;const refreshExpiresAt=data.refresh_expires_in?Date.now()+data.refresh_expires_in*1e3:expiresAt;var token={accessToken:data.access_token,refreshToken:data.refresh_token,expiresAt,refreshExpiresAt,tenantId:data.tenant_id,tenantName:data.tenant_name,codeVerifier,serverUrl};saveSession(token);console.log(chalk.green("Successfully logged in to "+token.tenantName))}catch(e){console.error(chalk.red("Failed to fetch token:",e));res.redirect(req.query.error_uri+encodeURIComponent(e));res.end();server.close((()=>{process.exit(1)}));return}res.redirect(req.query.success_uri);res.end();setTimeout((()=>{server.close();connections.forEach((conn=>conn.destroy()))}),100);const answer=readline.question(`Do you want to set '${token.tenantId}' as default tenant? [Y/n]`);if(!answer||!(answer==="N"||answer==="n")){await setDefaultSession(token.tenantId);console.log("Default session set to:",chalk.italic(token.tenantId))}}));const server=web.listen(0,(()=>{const port=server.address().port;redirectUri=`http://localhost:${port}/callback`;const scope="registry:* components:* apps:* actors:* rules:* graph:* jobs:* job-definitions:* ingresses:*";const authorizeUrl=`${serverUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`;open(authorizeUrl,{wait:false})}));server.on("connection",(conn=>{connections.add(conn);conn.on("close",(()=>connections.delete(conn)))}))}};var sessions={command:"sessions",describe:"Lists active sessions",builder:yargs=>yargs,handler:async argv=>{const table=new AsciiTable3;table.setHeading("ID","Name");var sessions=await getSessions();sessions.forEach((session=>{table.addRow(session.tenantId,session.tenantName)}));console.log(table.toString())}};var logout={command:"logout <session>",describe:"Logs out of a session",builder:yargs=>yargs.option("session",{describe:"The id of the session to logout of",type:"string"}),handler:async argv=>{const session=(await getSessions()).find((s=>s.tenantId===argv.session));if(!session){console.log(chalk.yellow(`Session ${argv.session} not found`));return}removeSession(session.tenantId);console.log(chalk.green(`Logged out of session ${session.tenantName} (${session.tenantId})`))}};var use={command:"use [session]",describe:"Gets or sets the default session to use for management commands",builder:yargs=>yargs.option("session",{describe:"The session to use",type:"string"}),handler:async argv=>{if(argv.session){await setDefaultSession(argv.session);console.log("Default session set to:",chalk.italic(argv.session))}else{const current=await getDefaultSession();if(!current){console.log(chalk.yellow("No session set"));console.log("Use 'h_ manage use <session>' to set the default session");return}console.log("Current session:",chalk.italic(current))}}};function printErrors(errors){if(errors?.errors!==undefined&&Array.isArray(errors.errors)){errors=errors.errors}if(Array.isArray(errors)){errors.forEach((err=>{console.log(chalk.red(`[${err.code}]: ${err.message}`));if(err.details&&Object.keys(err.details).length>0){console.log(chalk.red(err.details))}}))}else{console.log(chalk.red(`[${errors.code}]: ${errors.message}`));if(errors.details&&Object.keys(errors.details).length>0){console.log(chalk.red(errors.details))}}}function parsePath$6(resource){const matches=resource.match(/^\/?(?:resources\/)?components(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}function endsWith$1(str,suffix){return str.indexOf(suffix,str.length-suffix.length)!==-1}var components={get:async(sessionId,resource)=>{const path=parsePath$6(resource);const response=await fetch$1(sessionId,`/resources/components/${path||""}?searchType=prefix`);if(response.status!==200){throw new Error(`Failed to get components: ${response.statusText}`)}return(await response.json()).map(mapToManifest$5)},apply:async(sessionId,manifests,sourceFile)=>{for(const manifest of manifests){const componentId=parsePath$6(manifest.uri);if(!componentId){throw new Error("Invalid resource path")}if(manifest.spec.codeFile){if(!sourceFile){throw new Error("codeFile can not be used when piping manifest data")}const codeFilePath=path.join(path.dirname(sourceFile),manifest.spec.codeFile);if(!fs.existsSync(codeFilePath)){throw new Error(`Could not find codeFile '${codeFilePath}'`)}const code=fs.readFileSync(codeFilePath);manifest.spec.code=code.toString();let expectedType;if(endsWith$1(codeFilePath,".module.hreactor")||endsWith$1(codeFilePath,".module.hrc")){expectedType="reactor-module"}else if(endsWith$1(codeFilePath,".hreactor")||endsWith$1(codeFilePath,".hrc")){expectedType="reactor"}else if(endsWith$1(codeFilePath,".hrule")||endsWith$1(codeFilePath,".hrl")){expectedType="rule"}else if(endsWith$1(codeFilePath,".hdiscount")||endsWith$1(codeFilePath,".hdc")){expectedType="discount"}if(!manifest.spec.type&&expectedType){manifest.spec.type=expectedType}if(expectedType&&manifest.spec.type!==expectedType){console.log(chalk.yellow(`${manifest.uri}:\nComponent type was specified as '${manifest.spec.type}', but it looks like you're adding a '${expectedType}'.`))}}const response=await fetch$1(sessionId,`/resources/components/${componentId}`,{method:"POST",headers:{"Content-Type":"application/json"},body:{...manifest.spec}});if(!response.ok){console.log(chalk.red(`Failed to apply ${manifest.uri}`));const e=await response.json();if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}continue}await verbose((async()=>{const updated=await response.json();return"Updated component:\n"+JSON.stringify(updated,null,2)}));console.log(chalk.green(`Applied ${manifest.uri}`))}},remove:async(sessionId,resource)=>{const componentId=parsePath$6(resource);if(!componentId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/components/${componentId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$5(c){return{uri:`/resources/components/${c.componentId}`,spec:{displayName:c.displayName,description:c.description,runtime:c.runtime,componentVersion:c.componentVersion,code:c.code}}}function parsePath$5(resource){const matches=resource.match(/^\/?(?:resources\/)?registry(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var registry={get:async(sessionId,resource)=>{const path=parsePath$5(resource);const response=await fetch$1(sessionId,`/resources/registry/${path||""}?searchType=prefix`);if(response.status!==200){throw new Error(`Failed to get registry entries: ${response.statusText}`)}return(await response.json()).map(mapToManifest$4)},apply:async(sessionId,manifests)=>{const payload=manifests.map((m=>({key:parsePath$5(m.uri),value:m.spec.value,isSecret:m.spec.isSecret})));const response=await fetch$1(sessionId,"/resources/registry",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!response.ok){console.log(chalk.red(`Failed to apply manifests:`));printErrors(await response.json());return}const updated=await response.json();if(updated.length===0){console.log(chalk.yellow(`No registry keys needed updating`));return}else{console.log(chalk.green(`Updated registry keys:`));for(const update of updated){console.log(chalk.green(`- ${update.key}`));console.log(chalk.green(` - Old: ${update.oldValue}`));console.log(chalk.green(` - New: ${update.newValue}`))}}},remove:async(sessionId,resource)=>{const path=parsePath$5(resource);const response=await fetch$1(sessionId,`/resources/registry/${path}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$4(c){return{uri:`/resources/registry/${c.key}`,spec:{value:c.value,isSecret:false}}}function parsePath$4(resource){const matches=resource.match(/^\/?(?:resources\/)?rules(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var rules={get:async(sessionId,resource)=>{const path=parsePath$4(resource);const response=await fetch$1(sessionId,`/resources/rules/${path||""}?searchType=prefix`);if(response.status!==200){throw new Error(`Failed to get rules: ${response.statusText}`)}return(await response.json()).map(mapToManifest$3)},apply:async(sessionId,manifests)=>{for(const manifest of manifests){const ruleId=parsePath$4(manifest.uri);if(!ruleId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/rules/${ruleId}`,{method:"POST",headers:{"Content-Type":"application/json"},body:{...manifest.spec}});if(!response.ok){console.log(chalk.red(`Failed to apply ${manifest.uri}`));const e=await response.json();if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}continue}await verbose((async()=>{const updated=await response.json();return"Updated rule:\n"+JSON.stringify(updated,null,2)}));console.log(chalk.green(`Applied ${manifest.uri}`))}},remove:async(sessionId,resource)=>{const ruleId=parsePath$4(resource);if(!ruleId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/rules/${ruleId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$3(rule){return{uri:`/resources/rules/${rule.ruleId}`,spec:{displayName:rule.displayName,activeFrom:rule.activeFrom,activeTo:rule.activeTo,components:rule.components?.map((c=>({namespace:c.componentNamespace,name:c.componentName,parameters:c.parameters})))}}}function parsePath$3(resource){const matches=resource.match(/^\/?(?:resources\/)?actors\/([a-z]+?)\/([^\/]+)\/?$/);if(!matches){throw new Error("Invalid resource actor path: "+resource)}const[,actorType,actorId]=matches;return{actorType,actorId}}var actors={get:async(sessionId,resource,dump)=>{const{actorType,actorId}=parsePath$3(resource);const response=await fetch$1(sessionId,`/resources/actors/${actorType}/${actorId}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify([{type:dump?"debugDump":"get"}])});if(response.status!==200){await verbose((async()=>`Response:\n${await response.text()}`));throw new Error(`Failed to get debug dump: ${response.statusText}`)}return await response.json()},apply:async(sessionId,manifests)=>{throw new Error("'apply' is not supported on actors")},remove:async(sessionId,resource)=>{throw new Error("'remove' is not supported on actors")}};function parsePath$2(resource){const matches=resource.match(/^\/?(?:resources\/)?job-definitions(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var jobDefinitions={get:async(sessionId,resource)=>{const path=parsePath$2(resource);const response=await fetch$1(sessionId,`/resources/job-definitions/${path||""}`);if(response.status!==200){throw new Error(`Failed to get job definition: ${response.statusText}`)}if(!path){return(await response.json()).map(mapToManifest$2)}return[mapToManifest$2(await response.json())]},apply:async(sessionId,manifests)=>{for(const manifest of manifests){const jobDefinitionId=parsePath$2(manifest.uri);if(!jobDefinitionId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/job-definitions/${jobDefinitionId}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:{...manifest.spec}});if(!response.ok){console.log(chalk.red(`Failed to apply ${manifest.uri}`));console.log(response.headers,response.headers["content-type"]);let e;const contentType=response.headers["content-type"];if(typeof contentType==="string"&&contentType.startsWith("application/json")){e=await response.json()}else{e=await response.text()}if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}continue}await verbose((async()=>{const updated=await response.json();return"Updated job definition:\n"+JSON.stringify(updated,null,2)}));console.log(chalk.green(`Applied ${manifest.uri}`))}},remove:async(sessionId,resource)=>{const reactorId=parsePath$2(resource);if(!reactorId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/job-definitions/${reactorId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$2(jobDefinition){return{uri:`/resources/job-definitions/${jobDefinition.jobDefinitionId}`,spec:{componentId:jobDefinition.componentId}}}function parsePath$1(resource){const matches=resource.match(/^\/?(?:resources\/)?jobs(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var jobs={get:async(sessionId,resource)=>{const path=parsePath$1(resource);if(!path){throw new Error("Please provide job ID")}const response=await fetch$1(sessionId,`/resources/jobs/${path}`);if(response.status!==200){throw new Error(`Failed to get job: ${response.statusText}`)}return[mapToManifest$1(await response.json())]},apply:(sessionId,manifests)=>Promise.reject("Not supported"),remove:async(sessionId,resource)=>{const jobId=parsePath$1(resource);if(!jobId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/jobs/${jobId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$1(job){return{uri:`/resources/jobs/${job.jobId}`,spec:{},status:{state:job.state,scheduleAt:job.scheduledAt,startedAt:job.startedAt,finishedAt:job.finishedAt,result:job.result}}}function parsePath(resource){const matches=resource.match(/^\/?(?:resources\/)?ingresses(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var ingresses={get:async(sessionId,resource)=>{const path=parsePath(resource);const response=await fetch$1(sessionId,`/resources/ingresses/${path||""}`);if(response.status!==200){throw new Error(`Failed to get ingress: ${response.statusText}`)}if(!path){return(await response.json()).map(mapToManifest)}return[mapToManifest(await response.json())]},apply:async(sessionId,manifests)=>{for(const manifest of manifests){const ingressId=parsePath(manifest.uri);if(!ingressId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/ingresses/${ingressId}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:{...manifest.spec}});if(!response.ok){console.log(chalk.red(`Failed to apply ${manifest.uri}`));console.log(response.headers,response.headers["content-type"]);let e;const contentType=response.headers["content-type"];if(typeof contentType==="string"&&contentType.startsWith("application/json")){e=await response.json()}else{e=await response.text()}if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}continue}await verbose((async()=>{const updated=await response.json();return"Updated ingress:\n"+JSON.stringify(updated,null,2)}));console.log(chalk.green(`Applied ${manifest.uri}`))}},remove:async(sessionId,resource)=>{const reactorId=parsePath(resource);if(!reactorId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/ingresses/${reactorId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest(ingress){const result={uri:`/resources/ingresses/${ingress.ingressId}`,spec:{type:ingress.type,componentId:ingress.componentId,acl:ingress.acl,properties:ingress.properties}};if(ingress.type==="http"){result.status={endpointPath:`/ingress/${trimSlash(ingress.properties.route)}`}}return result;function trimSlash(str){return str.replace(/\/$/,"").replace(/^\//,"")}}var adapters={getAdapter(resourcePath){const matches=resourcePath.match(/^\/?(?:resources\/)?([\w-]+?)(?:\/|$)/);if(!matches){console.log(chalk.red("Invalid resource path"));return}const[,resource]=matches;if(resource==="components"){return components}else if(resource==="registry"){return registry}else if(resource==="rules"){return rules}else if(resource==="actors"){return actors}else if(resource==="job-definitions"){return jobDefinitions}else if(resource==="jobs"){return jobs}else if(resource==="ingresses"){return ingresses}else{console.log(chalk.red("Invalid resource or not supported"));return}}};var get={command:"get <resource>",describe:"Gets the manifest for the given resource (if supported)",builder:yargs=>yargs.positional("resource",{describe:"The resource path to get",type:"string",demandOption:true}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}).option("out",{alias:"o",describe:"The file to output to",type:"string",demandOption:false}).option("dump",{type:"boolean",demandOption:false,hidden:true}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}let adapter=adapters.getAdapter(argv.resource);if(!adapter){console.log(chalk.red(`Invalid resource path '${argv.resource}'`));return}try{const manifests=await(adapter?.get(sessionId,argv.resource,argv.dump));if(!manifests){console.log(chalk.red("Failed to get manifest"));return}let result;if(Array.isArray(manifests)){result=manifests.map((s=>stringify(s))).join("---\n")}else{result=stringify(manifests)}if(argv.out){console.log(chalk.green(`Writing to ${argv.out}`));fs.writeFileSync(argv.out,result)}else{console.log(result)}}catch(e){console.log(chalk.red(`Failed to get ${argv.resource}`));if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var remove={command:"remove <resource>",describe:"Removes the given resource (if supported)",builder:yargs=>yargs.positional("resource",{describe:"The resource to remove",type:"string",demandOption:true}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}let adapter=adapters.getAdapter(argv.resource);if(!adapter){console.log(chalk.red(`Invalid resource path '${argv.resource}'`));return}try{await adapter.remove(sessionId,argv.resource);console.log(chalk.green(`Removed ${argv.resource}`))}catch(e){console.log(chalk.red(`Failed to remove ${argv.resource}`));printErrors(e.errors)}}};var apply={command:"apply [file]",describe:"Applies the given manifest file to the remote Hantera installation. If not specified, stdin is used.",builder:yargs=>yargs.positional("file",{describe:"The file to apply",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}let data="";if(argv.file){data=fs.readFileSync(argv.file,"utf8")}else if(!process.stdin.isTTY){process.stdin.on("data",(chunk=>{data+=chunk}));process.stdin.on("end",(()=>{console.log("Received input:");console.log(data)}))}else{console.log(chalk.red("No input provided"));return}const manifests=parseAllDocuments(data).map((r=>r.toJS()));if(!manifests){throw`'${argv.file}' is empty or invalid.`}const mappedManifests=new Map;for(const manifest of manifests){const adapter=adapters.getAdapter(manifest.uri);if(!adapter){console.log(chalk.red(`Invalid resource path '${manifest.uri}'`));continue}mappedManifests.set(manifest,adapter)}for(const[manifest,adapter]of mappedManifests.entries()){try{await adapter.apply(sessionId,[manifest],argv.file)}catch(e){console.log(chalk.red(`Failed to apply manifest`));if(e instanceof FetchError){console.log(chalk.red(`Could not connect to ${sessionId}`))}else if(!isEnabled()){console.log(chalk.red("Unknown error. Use --verbose flag to see full error."))}if(isEnabled()){console.log(e)}}}}};var list={command:"list",describe:"Lists installed apps",builder:yargs=>yargs.option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{const response=await fetch$1(sessionId,"/resources/apps");if(!response.ok){throw new Error(`Failed to get apps: ${response.statusText}`)}const apps=await response.json();const table=new AsciiTable3(`Apps in '${sessionId}'`);table.setHeading("Id","Name","Current Version","Authors","Description");table.setWidth(5,30).setWrapped(4);let count=0;for(var app of apps){count+=1;table.addRow(app.id,app.name,app.currentVersion,app.authors.join("\n"),app.description)}if(count>0){console.log(table.toString())}else{console.log("No apps installed")}}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var details={command:"details <appId>",describe:"Gets details of an app",builder:yargs=>yargs.positional("appId",{describe:"The id of the app",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{const response=await fetch$1(sessionId,"/resources/apps/"+argv.appId);if(response.status===404){console.log(chalk.red(`App '${argv.appId}' not found`));return}if(!response.ok){throw new Error(`Failed to get apps: ${response.statusText}`)}const app=await response.json();console.log(chalk.bold("App Details"));console.log("-------------");console.log("Id:",app.id);console.log("Name:",app.name);if(app.description){console.log("Description:");console.log(app.description)}if(app.authors?.length>0){console.log("Authors:");for(var author of app.authors){console.log("- "+author)}}if(app.reactors?.length>0){console.log("Reactors:");for(var reactors of app.reactors){console.log(`- /resources/reactors/${reactors}`)}}console.log("Installed Versions:");for(var version of app.installedVersions){if(version===app.currentVersion){console.log(chalk.green(`- ${version} (active)`))}else{console.log(`- ${version}`)}}if(!app.currentVersion){console.log(chalk.yellow("No active version"))}else{if(app.files?.length>0){console.log();const table=new AsciiTable3("Files");table.setHeading("Path","Mime Type","Size","Metadata");for(var file of app.files){table.addRow(file.path,file.mimeType,file.size,JSON.stringify(file.metadata))}console.log(table.toString())}}}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var install={command:"install <hapk file>",describe:"Gets details of an app",builder:yargs=>yargs.positional("hapk file",{describe:"The app package to install",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{var packagePath=path.resolve(argv.hapkfile);if(!fs.existsSync(packagePath)){console.log(chalk.red(`File not found: ${packagePath}`));return}const formData=new FormData;const packageFile=new File([fs.readFileSync(packagePath)],path.basename(packagePath),{type:"application/zip"});formData.set("package",packageFile,path.basename(packagePath));const response=await fetch$1(sessionId,"/resources/apps",{method:"PUT",body:formData});if(!response.ok){throw new Error(`Failed to install app (${response.statusText}): ${await response.text()}`)}const app=await response.json();console.log(chalk.green(`Installed app ${app.name} (${app.id}) version ${app.currentVersion}`))}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var activate={command:"activate <appId> [-v <version>]",describe:"Activate a deactivated app or change active version",builder:yargs=>yargs.positional("appId",{describe:"The id of the app",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}).option("app-version",{alias:"v",describe:"The version to activate",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{const appId=argv.appId;let version=argv["app-version"];if(!version){version=await getLatestVersion(sessionId,appId)}const response=await fetch$1(sessionId,"/resources/apps/"+appId,{method:"POST",headers:{"Content-Type":"application/json"},body:{version}});if(response.status===404){console.log(chalk.red(`App '${argv.appId}' not found`));return}if(!response.ok){throw new Error(`Failed to activate app (${response.statusText}): ${await response.text()}`)}console.log(chalk.green(`Activated app ${appId} version ${version}`))}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};async function getLatestVersion(sessionId,appId){const response=await fetch$1(sessionId,"/resources/apps/"+appId);if(response.status===404){throw new Error(`App '${appId}' not found`)}if(!response.ok){throw new Error(`Failed to get current app version (${response.statusText}): ${await response.text()}`)}const app=await response.json();app.installedVersions.sort(((a,b)=>a.localeCompare(b,undefined,{numeric:true,sensitivity:"base"})));return app.installedVersions[app.installedVersions.length-1]}var deactivate={command:"deactivate <appId>",describe:"Deactivates an app",builder:yargs=>yargs.positional("appId",{describe:"The id of the app",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{const appId=argv.appId;const response=await fetch$1(sessionId,"/resources/apps/"+appId,{method:"POST",headers:{"Content-Type":"application/json"},body:{version:null}});if(response.status===404){console.log(chalk.red(`App '${argv.appId}' not found`));return}if(!response.ok){throw new Error(`Failed to activate app (${response.statusText}): ${await response.text()}`)}console.log(chalk.green(`Deactivated app ${appId}`))}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var apps={command:"apps <command>",describe:"Commands for managing tenant",builder(yargs){return yargs.command(list).command(details).command(install).command(activate).command(deactivate).demandCommand(1,"You need at least one sub-command")}};var updateSearch={command:"update-search <node...>",describe:"Update/rebuild search indices",builder:yargs=>yargs.positional("node",{description:"The nodes to rebuild. Pass 'all' to rebuild all indices",demandOption:true}).option("rebuild",{alias:"r",type:"boolean",description:`If set, indices will be cleared before refreshing them. Faster but will leave search partially inoperable during build.`,demandOption:false}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}const session=(await getSessions()).find((r=>r.tenantId===sessionId));if(!session){console.log(chalk.red(`Session '${sessionId}' not found`));return}let nodes=argv.node;const rebuild=argv.rebuild;let question;if(rebuild){question=chalk.yellow(`Rebuilding index for ${nodes} in '${session.tenantName}', this will clear the index before rebuilding, proceed? [y/N]`)}else{question=`Update index for ${nodes} in '${session.tenantName}', proceed? [y/N]`}const answer=readline.question(question);if(!answer||!(answer==="Y"||answer==="y")){console.log("Aborting");return}if(nodes.length===1&&nodes[0]==="all"){nodes=[]}try{const payload={nodes,rebuild};console.log(`Scheduling index update..`);const response=await fetch$1(sessionId,"/api/system/searchIndex/update",{method:"POST",headers:{"Content-Type":"application/json"},body:payload});if(!response.ok){throw new Error(`Unable to schedule rebuild: (${response.status} ${response.statusText}) ${await response.text()}`)}console.log(chalk.green(`Rebuild scheduled successfully!`));console.log(`This operation may take a while to complete. Use 'h_ manage signals graph' to check progress.`)}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};const placeholderRegex=/\{(.+?)}/gi;function renderMessage(template,properties){if(!template){return undefined}if(!properties){properties={}}return template.replace(placeholderRegex,((_,p1)=>{if(properties){let token=properties[p1];if(typeof token!=="undefined"){return chalk.blue(token?.toString())}}return`{${p1.toLowerCase()}}`}))}var signals={command:"signals [path]",describe:"Displays system signals",builder:yargs=>yargs.positional("path",{description:"Filter signals by path",demandOption:false}).option("watch",{alias:"w",type:"boolean",description:`Refreshes signals every seconds`,demandOption:false}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}const session=(await getSessions()).find((r=>r.tenantId===sessionId));if(!session){console.log(chalk.red(`Session '${sessionId}' not found`));return}if(argv.watch){console.clear();console.log("Loading signals...");setInterval((()=>{printSignals(true)}),1e3)}else{await printSignals()}async function printSignals(clearConsole=false){try{const response=await fetch$1(sessionId,`/resources/registry/signals/${argv.path??""}?searchType=prefix`,{method:"GET"});if(!response.ok){throw new Error(`Unable to get signals: (${response.status} ${response.statusText}) ${await response.text()}`)}let signals=await response.json();if(clearConsole){console.clear()}signals.filter((r=>r.value!==undefined)).forEach((signal=>{const id=signal.key.substring(8);const message=renderMessage(signal.value.messageTemplate,signal.value.properties);if(signal.value.severity==="information"){print("INFO",chalk.gray,id,message)}else if(signal.value.severity==="warning"){print("WARN",chalk.yellow,id,message)}else if(signal.value.severity==="error"){print("ERR ",chalk.red,id,message)}function print(status,color,signalId,message){console.log("["+color(status)+`] ${signalId}${message?": "+message:""}`)}}));console.log("Updated: "+(new Date).toLocaleTimeString())}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}}};var manage={command:"manage <command>",describe:"Commands for managing tenant",builder(yargs){return yargs.command(login).command(logout).command(sessions).command(use).command(get).command(apply).command(remove).command(apps).command(updateSearch).command(signals).demandCommand(1,"You need at least one sub-command")}};var $new={command:"new",describe:"Creates a new app",builder:yargs=>yargs,handler:async argv=>{let appName=question("App Name",(value=>{if(!value){console.log(chalk.red("App Name is required."));return false}return true}));let defaultAppId=appName.replace(/[^a-z0-9]/gi,"-").replace(/^-+|-+$/g,"").toLowerCase();const appId=question(`App ID [${defaultAppId}]`,(value=>{if(!value){value=defaultAppId}if(!/^[a-z0-9_-]+$/.test(value)){console.log(chalk.red("App ID can only contain lowercase letters, numbers, dash and underscope."));return false}return true}))||defaultAppId;let appFolder=path.resolve(question(`Path [./${appId}/]`)||`./${appId}/`);let description=question("Description");let author=question(`Author [${os.userInfo().username}]`)||os.userInfo().username;let setupPortalApp=question("Setup portal app? (y/n)",(value=>{if(value!=="y"&&value!=="n"){console.log(chalk.red('Invalid value. Please enter "y" or "n".'));return false}return true}))==="y";const appConfig={id:appId,name:appName,description,authors:[author]};if(setupPortalApp){appConfig.extensions={portal:"./portal"}}if(!fs.existsSync(appFolder)){fs.mkdirSync(appFolder)}const filePath=path.join(appFolder,"h_app.yaml");if(fs.existsSync(filePath)){console.log(chalk.red(`'h_app.yaml' already exists. Run 'h_ app new' in an empty folder.`));return}console.log(chalk.gray(`Creating ${filePath}...`));fs.writeFileSync(filePath,stringify(appConfig));console.log(chalk.green("Done."));const ignore=[".hantera/"];if(setupPortalApp){const portalFolder=path.join(path.resolve(appFolder),"portal");if(!fs.existsSync(portalFolder)){console.log(chalk.gray(`Creating ${portalFolder}...`));fs.mkdirSync(portalFolder);console.log(chalk.green("Done."))}await createPortalApp(appId,appFolder,portalFolder);const relativePortalFolder=path.relative(appFolder,portalFolder);ignore.push(path.join(relativePortalFolder,"node_modules"))}console.log(chalk.gray(`Creating ${path.join(appFolder,".gitignore")}...`));fs.writeFileSync(path.join(appFolder,".gitignore"),ignore.join("\n"));console.log(chalk.green("Done."));console.log(`App '${appId}' created in '${appFolder}'. Run '${chalk.green("h_ app package")}' to create a hapk package.`)}};function question(label,validator){let value="";while(true){value=readline.question(label+": ");if(validator&&!validator(value)){continue}break}return value}async function createPortalApp(appId,appFolder,portalFolder){if(fs.existsSync(path.join(portalFolder,"package.json"))){console.log(chalk.red(`'package.json' already exists. Run 'h_ app new' in an empty folder.`));return}const portalAppVersion=await getNpmPackageVersion("@hantera/portal-app");const designSystemVersion=await getNpmPackageVersion("@hantera/design-system");async function getNpmPackageVersion(packageId){const response=await fetch("https://registry.npmjs.org/"+packageId);const json=await response.json();return json["dist-tags"].latest}console.log(chalk.gray(`Creating ${path.join(portalFolder,"package.json")}...`));fs.writeFileSync(path.join(portalFolder,"package.json"),`{\n "name": "${appId}",\n "version": "1.0.0",\n "main": "index.ts",\n "type": "module",\n "dependencies": {\n "@hantera/portal-app": "${portalAppVersion}",\n "@hantera/design-system": "${designSystemVersion}",\n "vue": "^3.4.37"\n }\n}\n`);console.log(chalk.green("Done."));console.log(chalk.gray(`Creating ${path.join(portalFolder,"tsconfig.json")}...`));fs.writeFileSync(path.join(portalFolder,"tsconfig.json"),`{\n "compilerOptions": {\n "target": "ESNext",\n "useDefineForClassFields": true,\n "module": "ESNext",\n "moduleResolution": "Node",\n "strict": true,\n "jsx": "preserve",\n "sourceMap": true,\n "resolveJsonModule": true,\n "isolatedModules": true,\n "esModuleInterop": true,\n "lib": ["ESNext", "DOM"],\n "skipLibCheck": true\n },\n "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"]\n}\n`);console.log(chalk.green("Done."));console.log(chalk.gray(`Creating ${path.join(portalFolder,"index.ts")}...`));fs.writeFileSync(path.join(portalFolder,"index.ts"),`import { apps, Portal } from '@hantera/portal-app';\nimport MyComponent from './components/MyComponent.vue';\n\nexport default function(portal: Portal) {\n portal.registerComponent(apps.orderViewSlots.delivery.footer, MyComponent);\n}\n`);console.log(chalk.green("Done."));const componentsDir=path.join(portalFolder,"components");if(!fs.existsSync(componentsDir)){console.log(chalk.gray(`Creating ${componentsDir}...`));fs.mkdirSync(componentsDir,484);console.log(chalk.green("Done."))}console.log(chalk.gray(`Creating ${path.join(componentsDir,"MyComponent.vue")}...`));fs.writeFileSync(path.join(componentsDir,"MyComponent.vue"),`<script setup lang="ts">\nimport { apps } from '@hantera/portal-app'\n\nconst context = apps.componentContext(apps.orderViewSlots.delivery.footer)\n\nconst delivery = context.delivery;\n<\/script>\n\n<template>\n Current delivery number: {{ delivery?.deliveryNumber }}\n</template>\n`);console.log(chalk.green("Done."));console.log(chalk.gray(`Installing dependencies...`));execSync("npm install",{cwd:portalFolder,stdio:"inherit"});console.log(chalk.green("Done."));console.log("\n");console.log(`Portal app created. Run '${chalk.green("h_ app dev")}' to start the local development server.`)}function packageToImportMapKey$1(p){return"__portal_"+p.replace("@","").replace("/","_").replace("-","_")}function portalResolve$1(packages){const packageIds=packages.map(packageToImportMapKey$1);return{name:"vite-plugin-portal-resolve",enforce:"pre",config(config){config.optimizeDeps={...config.optimizeDeps??{},exclude:[...config.optimizeDeps?.exclude??[],...packages]};if(!config.build){config.build={}}if(!config.build.rollupOptions){config.build.rollupOptions={}}config.build.rollupOptions.external=[...config.build.rollupOptions.external??[],...packages.map(packageToImportMapKey$1)]},configResolved(resolvedConfig){const VALID_ID_PREFIX=`/@id/`;const reg=new RegExp(`${VALID_ID_PREFIX}(${packages.map((r=>packageToImportMapKey$1(r))).join("|")})`,"g");resolvedConfig.plugins.push({name:"vite-plugin-portal-resolve-replace-idprefix",transform:code=>{const result=reg.test(code)?code.replace(reg,((m,s1)=>s1)):code;return result}})},async resolveId(source,importer,options){if(packages.includes(source)){return{id:packageToImportMapKey$1(source),external:true,resolvedBy:"vite-plugin-portal-resolve"}}},load(id){if(packageIds.indexOf(id)!==-1){return`export default {};`}}}}function packageToImportMapKey(p){return"__portal_"+p.replace("@","").replace("/","_").replace("-","_")}function portalResolve(packages){const reg=new RegExp(`(import\\s+(?:[\\s\\S]+?)\\s+from\\s+["'])(${packages.map((r=>r.replace("/","\\/"))).join("|")})(["'];?)`,"g");return{name:"vite-plugin-portal-resolve",enforce:"pre",config(config){config.optimizeDeps={...config.optimizeDeps??{},exclude:[...config.optimizeDeps?.exclude??[],...packages]};if(!config.build){config.build={}}if(!config.build.rollupOptions){config.build.rollupOptions={}}config.build.rollupOptions.external=[...config.build.rollupOptions.external??[],...packages.map(packageToImportMapKey)]},configResolved(resolvedConfig){resolvedConfig.plugins.push({name:"vite-plugin-portal-resolve-replace-idprefix",transform:code=>{const result=reg.test(code)?code.replace(reg,((m,s1,s2,s3)=>s1+packageToImportMapKey(s2)+s3)):code;return result}})}}}function developmentConfig(cacheFolder,isVerbose=false){return{configFile:false,mode:"development",root:cacheFolder,plugins:[vue(),portalResolve$1(["vue","@hantera/portal-app","@hantera/design-system"]),mkcert()],logLevel:isVerbose?"info":"error",server:{port:4734,cors:true},build:{target:"esnext"}}}function buildConfig(portalFolder,entry,outFolder){const result=developmentConfig(portalFolder);result.mode="production";delete result.server;result.logLevel="warn";result.build.lib={entry,name:"hantera-app",fileName:"index",formats:["es"]};result.plugins=[vue(),portalResolve(["vue","@hantera/portal-app","@hantera/design-system"])];result.build.emptyOutDir=true;result.build.outDir=outFolder;if(!result.define){result.define={}}result.define["process.env.NODE_ENV"]=JSON.stringify("production");return result}var dev={command:"dev [path]",describe:"Starts a portal app in local development mode",builder:yargs=>yargs.positional("path",{describe:"Path to the app",type:"string",default:"."}),handler:async argv=>{const appFolder=path.resolve(argv.path);const appConfigPath=path.join(appFolder,"h_app.yaml");if(!fs.existsSync(appConfigPath)){console.log(chalk.red(`'h_app.yaml' not found in path '${appFolder}'`));return}const appConfig=yaml.parse(fs.readFileSync(appConfigPath,"utf8"));if(!appConfig.extensions?.portal){console.log(chalk.red(`'h_app.yaml' missing 'extensions.portal' property.`));return}const portalFolder=path.join(appFolder,appConfig.extensions.portal);if(!fs.existsSync(path.join(portalFolder,"package.json"))){console.log(chalk.red(`'package.json' not found in path '${portalFolder}'`));return}const packageJson=JSON.parse(fs.readFileSync(path.join(portalFolder,"package.json"),"utf8"));const entryPath=path.normalize(path.join(portalFolder,packageJson.main||"index.ts"));let cacheDir=path.join(appFolder,".hantera");if(!fs.existsSync(cacheDir)){fs.mkdirSync(cacheDir,484)}cacheDir=path.join(cacheDir,"portal");if(!fs.existsSync(cacheDir)){fs.mkdirSync(cacheDir,484)}fs.writeFile(path.join(cacheDir,"index.html"),`<!DOCTYPE html>\n <html lang="en">\n <head>\n </head>\n <body>\n <script type="module" src="loader.ts"><\/script>\n </body>\n </html>\n `,(err=>{if(err){console.error(err)}}));const loader_ts=`export const appId = ${JSON.stringify(packageJson.name)};\n export { default as default } from '${path.relative(cacheDir,entryPath).replaceAll("\\","/").replaceAll(".ts","")}';`;fs.writeFile(path.join(cacheDir,"loader.ts"),loader_ts,(err=>{if(err){console.error(err)}}));const server=await createServer(developmentConfig(cacheDir,isEnabled()));await server.listen();if(isEnabled()){server.printUrls()}console.log(chalk.green("Local app development is running.."));console.log(boxen(chalk.black(`To enable local development mode in Hantera portal:\n- Go to Developer settings\n- Click "Enable Local Development"`),{borderStyle:"none",backgroundColor:"blue",padding:1}))}};const ingressIdRegex=/^[a-z0-9_.-]+$/;function endsWith(str,suffix){return str.indexOf(suffix,str.length-suffix.length)!==-1}var pack={command:"pack [path] [-v 1.0.0]",describe:"Builds and package the app",builder:yargs=>yargs.positional("path",{describe:"Path to the app",type:"string",default:"."}).option("package-version",{alias:"v",describe:"Version of the app",type:"string",default:"1.0.0"}),handler:async argv=>{const appFolder=path.resolve(argv.path);const appConfigPath=path.join(appFolder,"h_app.yaml");if(!fs.existsSync(appConfigPath)){console.log(chalk.red(`'h_app.yaml' not found in path '${appFolder}'`));return}const appConfig=yaml.parse(fs.readFileSync(appConfigPath,"utf8"));const packageManifest={id:appConfig.id,name:appConfig.name,description:appConfig.description,authors:appConfig.authors,version:argv.packageVersion,files:[],components:[],ingresses:[]};if(!packageManifest.id){console.log(chalk.red(`'id' property is required in 'h_app.yaml'`));return}if(!packageManifest.name){console.log(chalk.red(`'name' property is required in 'h_app.yaml'`));return}const zipFileWriter=new BlobWriter;const zipWriter=new ZipWriter(zipFileWriter);if(Array.isArray(appConfig.files)){for(const file of appConfig.files){if(!file.id){console.log(chalk.red(`'id' property is required for each file in 'h_app.yaml'`));return}let id=file.id;if(!id&&file.path){id=path.basename(file.path)}id=id.replace("\\","/");if(id.startsWith("portal/")){console.log(chalk.red(`File ID cannot start with 'portal/'`));return}let mimeType=file.mimeType;if(!mimeType){mimeType=mime.lookup(file.source)||"application/octet-stream"}packageManifest.files.push({id,mimeType,metadata:file.metadata||{}});const entryId=path.join("files",id).replace("\\","/");verbose((()=>`Adding file: ${entryId}`));const fileReader=new BlobReader(new Blob([fs.readFileSync(path.resolve(file.source))]));console.log(entryId);await zipWriter.add(entryId,fileReader)}}if(Array.isArray(appConfig.components)){for(const component of appConfig.components){if(!component.id){console.log(chalk.red(`'id' property is required for each component in 'h_app.yaml'`));return}let id=component.id.replaceAll("\\","/");let sourcePath=component.path;if(!sourcePath){sourcePath=path.join("components",id)}let expectedType;if(endsWith(sourcePath,".module.hreactor")||endsWith(sourcePath,".module.hrc")){expectedType="reactor-module"}else if(endsWith(sourcePath,".hreactor")||endsWith(sourcePath,".hrc")){expectedType="reactor"}else if(endsWith(sourcePath,".hrule")||endsWith(sourcePath,".hrl")){expectedType="rule"}else if(endsWith(sourcePath,".hdiscount")||endsWith(sourcePath,".hdc")){expectedType="discount"}if(!component.type&&expectedType){component.type=expectedType}if(expectedType&&component.type!==expectedType){console.log(chalk.yellow(`${component.id}:\nComponent type was specified as '${component.type}', but it looks like you're adding a '${expectedType}'.`))}if(!component.type){console.log(chalk.red(`${component.id}:\nComponent type must be specified.`));return}let componentType=component.type;if(!componentType){componentType=path.extname(id).substring(1)}packageManifest.components.push({id,displayName:component.displayName,description:component.description,type:component.type});const entryId=path.join("components",id).replaceAll("\\","/");verbose((()=>`Adding Component: ${entryId}`));const fileReader=new BlobReader(new Blob([fs.readFileSync(path.resolve(sourcePath))]));await zipWriter.add(entryId,fileReader)}}if(appConfig.extensions?.portal){const portalFolder=path.join(appFolder,appConfig.extensions.portal);if(!fs.existsSync(path.join(portalFolder,"package.json"))){console.log(chalk.red(`'package.json' not found in path '${portalFolder}'`));return}const packageJson=JSON.parse(fs.readFileSync(path.join(portalFolder,"package.json"),"utf8"));const entryPath=path.normalize(path.join(portalFolder,packageJson.main||"index.ts"));const distFolder=path.join(appFolder,".hantera","portal","dist");fs.rmSync(distFolder,{recursive:true,force:true});if(!await typeCheck(portalFolder)){return}await build(buildConfig(portalFolder,entryPath,distFolder));const files=fs.readdirSync(distFolder);for(const file of files){const filePath=path.join(distFolder,file);const mimeType=path.extname(file)===".js"?"application/javascript":"text/css";const fileId=path.join("portal",file).replaceAll("\\","/");packageManifest.files.push({id:fileId,mimeType,metadata:{portal:"true"}});const entryId=path.join("files","portal",file).replaceAll("\\","/");verbose((()=>`Adding file: ${entryId}`));const fileReader=new BlobReader(new Blob([fs.readFileSync(filePath)]));await zipWriter.add(entryId,fileReader)}}if(Array.isArray(appConfig.ingresses)){for(const ingress of appConfig.ingresses){if(!ingress.id){console.log(chalk.red(`'id' property is required for each ingress in 'h_app.yaml'`));return}if(!ingress.componentId){console.log(chalk.red(`'componentd' property is required for each ingress in 'h_app.yaml'`));return}if(!ingressIdRegex.test(ingress.id)){console.log(chalk.red(`Ingress id '{ingress.id}' can only contain the following: a-z . _ -`));return}if(ingress.type!="http"){console.log(chalk.yellow(`Unknown ingress type '${ingress.type}' in 'h_app.yaml'`))}const component=packageManifest.components.find((r=>r.id===ingress.componentId));if(!component){console.log(chalk.red(`Component '${ingress.componentId}' for Ingress '${ingress.id}' was not found in 'h_app.yaml'`));return}verbose((()=>`Adding Ingress: ${ingress.id}`));packageManifest.ingresses.push({id:ingress.id,componentId:ingress.componentId,type:ingress.type,properties:ingress.properties,acl:ingress.acl??[]})}}const manifestReader=new TextReader(JSON.stringify(packageManifest,null,2));await zipWriter.add("manifest.json",manifestReader);await zipWriter.close();const zipFileBlob=await zipFileWriter.getData();const zipFileArrayBuffer=await zipFileBlob.arrayBuffer();const packagePath=path.join(appFolder,`${packageManifest.id}-${packageManifest.version}.hapk`);fs.writeFileSync(packagePath,Buffer.from(zipFileArrayBuffer));console.log(`App '${packageManifest.id}' packaged in '${packagePath}'.`)}};function typeCheck(portalFolder){const vueTscPath=import.meta.resolve("vue-tsc/bin/vue-tsc.js").replace("file:///","");return new Promise(((resolve,reject)=>{const child=spawn("node",[vueTscPath,"--noEmit","--project",portalFolder],{stdio:"inherit"});child.on("exit",((code,_)=>{if(code==0){resolve(true)}resolve(false)}));child.on("error",(()=>{reject()}))}))}var app={command:"app <command>",describe:"Commands for authoring apps",builder(yargs){return yargs.command($new).command(dev).command(pack).demandCommand(1,"You need at least one sub-command")}};const commands=[app,manage];if(process.argv.find((v=>v==="--verbose"))){enable()}yargs(hideBin(process.argv.filter((r=>r!=="--verbose")))).scriptName("h_").showHelpOnFail(true).command(commands).demandCommand().strict().parse();
2
+ import yargs from"yargs";import{hideBin}from"yargs/helpers";import chalk from"chalk";import open from"open";import express from"express";import crypto from"crypto";import nodeFetch,{FetchError,FormData,File}from"node-fetch";import keytar from"keytar";import https from"https";import readline from"readline-sync";import{AsciiTable3}from"ascii-table3";import fs from"fs";import path from"path";import yaml,{stringify,parseAllDocuments}from"yaml";import os from"os";import{execSync}from"child_process";import boxen from"boxen";import{createServer,build}from"vite";import vue from"@vitejs/plugin-vue";import mkcert from"vite-plugin-mkcert";import{spawn}from"node:child_process";import mime from"mime-types";import{BlobWriter,ZipWriter,BlobReader,TextReader}from"@zip.js/zip.js";let enabled=false;function enable(){enabled=true}function isEnabled(){return enabled}function verbose(func){if(!enabled){return Promise.resolve()}const value=func();if(value instanceof Promise){return new Promise((resolve=>{value.then((v=>{console.log(chalk.gray(v));resolve()}))}))}else{console.log(chalk.gray(value));return Promise.resolve()}}const service="hantera-cli";async function getSessions(){return(await keytar.findCredentials(service)).map((c=>{try{return JSON.parse(c.password)}catch(e){return null}})).filter((c=>c?.refreshExpiresAt>Date.now()))}async function saveSession(token){await keytar.setPassword(service,token.tenantId,JSON.stringify(token))}async function removeSession(sessionId){await keytar.deletePassword(service,sessionId)}async function setDefaultSession(sessionId){await keytar.setPassword(service,"default",sessionId)}async function getDefaultSession(){return await keytar.getPassword(service,"default")}async function fetch$1(sessionId="default",relativePath,init){var tokenRaw=await keytar.getPassword(service,sessionId);if(!tokenRaw){throw new Error("No token found for the environment")}const token=JSON.parse(tokenRaw);if(token.expiresAt<Date.now()&&(!token.refreshExpiresAt||token.refreshExpiresAt<Date.now())){throw new Error("No token found for the environment")}if(token.expiresAt<Date.now()){if(token.refreshExpiresAt<Date.now()){throw new Error("Tenant session expired")}verbose((()=>"Token expired, attempting to refresh"));const payload=new URLSearchParams;payload.append("grant_type","refresh_token");payload.append("refresh_token",token.refreshToken);payload.append("code_verifier",token.codeVerifier);var response=await nodeFetch(`${token.serverUrl}/oauth/token`,{agent:token.tenantId==="localhost"?new https.Agent({rejectUnauthorized:false}):undefined,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:payload.toString()});if(!response.ok){throw new Error(`Failed to refresh token (${response.statusText}): ${await response.text()}`)}const data=await response.json();const expiresAt=Date.now()+data.expires_in*1e3;const refreshExpiresAt=data.refresh_expires_in?Date.now()+data.refresh_expires_in*1e3:expiresAt;token.accessToken=data.access_token;token.refreshToken=data.refresh_token;token.expiresAt=expiresAt;token.refreshExpiresAt=refreshExpiresAt;token.tenantId=data.tenant_id;token.tenantName=data.tenant_name;await saveSession(token)}if(!init){init={}}if(!init.headers){init.headers={}}init.headers["Authorization"]=`Bearer ${token.accessToken}`;let url;if(token.tenantId==="localhost"){url="https://localhost:3300"}else{url=`https://${token.tenantId}.core.ams.hantera.cloud`}if(!relativePath.startsWith("/")){relativePath="/"+relativePath}await verbose((()=>{let result=`${init.method||"GET"} ${url+relativePath}`;if(init.headers){result+="\n"+Object.keys(init.headers).map((k=>`${k}: ${init.headers[k]}`)).join("\n")+"\n"}if(init.body&&typeof init.body==="object"&&init.headers["Content-Type"]==="application/json"){result+=`\n${JSON.stringify(init.body,null,2)}\n`}else if(init.body){result+=`\n${init.body}\n`}return result}));if(init.body&&typeof init.body==="object"&&init.headers["Content-Type"]==="application/json"){await verbose((()=>"Converting body to JSON"));init.body=JSON.stringify(init.body)}if(token.tenantId==="localhost"){init.agent=new https.Agent({rejectUnauthorized:false})}return nodeFetch(url+relativePath,init)}const clientId="00000000-0000-0000-0000-000000000010";var login={command:"login <host>",describe:"Command logging in to a Hantera tenant",builder:yargs=>yargs.option("host",{describe:"The id or hostname of the tenant to login to.",type:"string"}),handler:async argv=>{if(!argv.host){console.log(chalk.red(`No host specified`))}console.log(`Logging in to '${argv.host}'...`);let serverUrl=null;if(argv.host==="localhost"){serverUrl="https://localhost:3300"}else{serverUrl=`https://${argv.host}.core.ams.hantera.cloud`}const state=Math.random().toString(36).substring(7);let redirectUri="";const codeVerifier=Math.random().toString(36).substring(7);let codeChallenge=crypto.createHash("sha256").update(codeVerifier).digest("base64");codeChallenge=codeChallenge.replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");const web=express();let connections=new Set;web.get("/callback",(async(req,res)=>{if(req.query.state!==state){console.error("Invalid state reported by the server.");res.status(400).send("Invalid state");server.close((()=>{process.exit(1)}));return}try{const response=await nodeFetch(`${serverUrl}/oauth/token`,{agent:argv.host==="localhost"?new https.Agent({rejectUnauthorized:false}):undefined,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",client_id:clientId,redirect_uri:redirectUri,code:req.query.code,code_verifier:codeVerifier})});if(!response.ok){throw response.statusText}const data=await response.json();const expiresAt=Date.now()+data.expires_in*1e3;const refreshExpiresAt=data.refresh_expires_in?Date.now()+data.refresh_expires_in*1e3:expiresAt;var token={accessToken:data.access_token,refreshToken:data.refresh_token,expiresAt,refreshExpiresAt,tenantId:data.tenant_id,tenantName:data.tenant_name,codeVerifier,serverUrl};saveSession(token);console.log(chalk.green("Successfully logged in to "+token.tenantName))}catch(e){console.error(chalk.red("Failed to fetch token:",e));res.redirect(req.query.error_uri+encodeURIComponent(e));res.end();server.close((()=>{process.exit(1)}));return}res.redirect(req.query.success_uri);res.end();setTimeout((()=>{server.close();connections.forEach((conn=>conn.destroy()))}),100);const answer=readline.question(`Do you want to set '${token.tenantId}' as default tenant? [Y/n]`);if(!answer||!(answer==="N"||answer==="n")){await setDefaultSession(token.tenantId);console.log("Default session set to:",chalk.italic(token.tenantId))}}));const server=web.listen(0,(()=>{const port=server.address().port;redirectUri=`http://localhost:${port}/callback`;const scope="registry:* components:* apps:* actors:* rules:* graph:* jobs:* job-definitions:* ingresses:*";const authorizeUrl=`${serverUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`;open(authorizeUrl,{wait:false})}));server.on("connection",(conn=>{connections.add(conn);conn.on("close",(()=>connections.delete(conn)))}))}};var sessions={command:"sessions",describe:"Lists active sessions",builder:yargs=>yargs,handler:async argv=>{const table=new AsciiTable3;table.setHeading("ID","Name");var sessions=await getSessions();sessions.forEach((session=>{table.addRow(session.tenantId,session.tenantName)}));console.log(table.toString())}};var logout={command:"logout <session>",describe:"Logs out of a session",builder:yargs=>yargs.option("session",{describe:"The id of the session to logout of",type:"string"}),handler:async argv=>{const session=(await getSessions()).find((s=>s.tenantId===argv.session));if(!session){console.log(chalk.yellow(`Session ${argv.session} not found`));return}removeSession(session.tenantId);console.log(chalk.green(`Logged out of session ${session.tenantName} (${session.tenantId})`))}};var use={command:"use [session]",describe:"Gets or sets the default session to use for management commands",builder:yargs=>yargs.option("session",{describe:"The session to use",type:"string"}),handler:async argv=>{if(argv.session){await setDefaultSession(argv.session);console.log("Default session set to:",chalk.italic(argv.session))}else{const current=await getDefaultSession();if(!current){console.log(chalk.yellow("No session set"));console.log("Use 'h_ manage use <session>' to set the default session");return}console.log("Current session:",chalk.italic(current))}}};function printErrors(errors){if(errors?.errors!==undefined&&Array.isArray(errors.errors)){errors=errors.errors}if(Array.isArray(errors)){errors.forEach((err=>{console.log(chalk.red(`[${err.code}]: ${err.message}`));if(err.details&&Object.keys(err.details).length>0){console.log(chalk.red(err.details))}}))}else{console.log(chalk.red(`[${errors.code}]: ${errors.message}`));if(errors.details&&Object.keys(errors.details).length>0){console.log(chalk.red(errors.details))}}}function parsePath$6(resource){const matches=resource.match(/^\/?(?:resources\/)?components(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}function endsWith$1(str,suffix){return str.indexOf(suffix,str.length-suffix.length)!==-1}var components={get:async(sessionId,resource)=>{const path=parsePath$6(resource);const response=await fetch$1(sessionId,`/resources/components/${path||""}?searchType=prefix`);if(response.status!==200){throw new Error(`Failed to get components: ${response.statusText}`)}return(await response.json()).map(mapToManifest$5)},apply:async(sessionId,manifests,sourceFile)=>{for(const manifest of manifests){const componentId=parsePath$6(manifest.uri);if(!componentId){throw new Error("Invalid resource path")}if(manifest.spec.codeFile){if(!sourceFile){throw new Error("codeFile can not be used when piping manifest data")}const codeFilePath=path.join(path.dirname(sourceFile),manifest.spec.codeFile);if(!fs.existsSync(codeFilePath)){throw new Error(`Could not find codeFile '${codeFilePath}'`)}const code=fs.readFileSync(codeFilePath);manifest.spec.code=code.toString();let expectedType;if(endsWith$1(codeFilePath,".module.hreactor")||endsWith$1(codeFilePath,".module.hrc")){expectedType="reactor-module"}else if(endsWith$1(codeFilePath,".hreactor")||endsWith$1(codeFilePath,".hrc")){expectedType="reactor"}else if(endsWith$1(codeFilePath,".hrule")||endsWith$1(codeFilePath,".hrl")){expectedType="rule"}else if(endsWith$1(codeFilePath,".hdiscount")||endsWith$1(codeFilePath,".hdc")){expectedType="discount"}if(!manifest.spec.type&&expectedType){manifest.spec.type=expectedType}if(expectedType&&manifest.spec.type!==expectedType){console.log(chalk.yellow(`${manifest.uri}:\nComponent type was specified as '${manifest.spec.type}', but it looks like you're adding a '${expectedType}'.`))}}const response=await fetch$1(sessionId,`/resources/components/${componentId}`,{method:"POST",headers:{"Content-Type":"application/json"},body:{...manifest.spec}});if(!response.ok){console.log(chalk.red(`Failed to apply ${manifest.uri}`));const e=await response.json();if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}continue}await verbose((async()=>{const updated=await response.json();return"Updated component:\n"+JSON.stringify(updated,null,2)}));console.log(chalk.green(`Applied ${manifest.uri}`))}},remove:async(sessionId,resource)=>{const componentId=parsePath$6(resource);if(!componentId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/components/${componentId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$5(c){return{uri:`/resources/components/${c.componentId}`,spec:{displayName:c.displayName,description:c.description,runtime:c.runtime,componentVersion:c.componentVersion,code:c.code}}}function parsePath$5(resource){const matches=resource.match(/^\/?(?:resources\/)?registry(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var registry={get:async(sessionId,resource)=>{const path=parsePath$5(resource);const response=await fetch$1(sessionId,`/resources/registry/${path||""}?searchType=prefix`);if(response.status!==200){throw new Error(`Failed to get registry entries: ${response.statusText}`)}return(await response.json()).map(mapToManifest$4)},apply:async(sessionId,manifests)=>{const payload=manifests.map((m=>({key:parsePath$5(m.uri),value:m.spec.value,isSecret:m.spec.isSecret})));const response=await fetch$1(sessionId,"/resources/registry",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!response.ok){console.log(chalk.red(`Failed to apply manifests:`));printErrors(await response.json());return}const updated=await response.json();if(updated.length===0){console.log(chalk.yellow(`No registry keys needed updating`));return}else{console.log(chalk.green(`Updated registry keys:`));for(const update of updated){console.log(chalk.green(`- ${update.key}`));console.log(chalk.green(` - Old: ${update.oldValue}`));console.log(chalk.green(` - New: ${update.newValue}`))}}},remove:async(sessionId,resource)=>{const path=parsePath$5(resource);const response=await fetch$1(sessionId,`/resources/registry/${path}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$4(c){return{uri:`/resources/registry/${c.key}`,spec:{value:c.value,isSecret:false}}}function parsePath$4(resource){const matches=resource.match(/^\/?(?:resources\/)?rules(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var rules={get:async(sessionId,resource)=>{const path=parsePath$4(resource);const response=await fetch$1(sessionId,`/resources/rules/${path||""}?searchType=prefix`);if(response.status!==200){throw new Error(`Failed to get rules: ${response.statusText}`)}return(await response.json()).map(mapToManifest$3)},apply:async(sessionId,manifests)=>{for(const manifest of manifests){const ruleId=parsePath$4(manifest.uri);if(!ruleId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/rules/${ruleId}`,{method:"POST",headers:{"Content-Type":"application/json"},body:{...manifest.spec}});if(!response.ok){console.log(chalk.red(`Failed to apply ${manifest.uri}`));const e=await response.json();if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}continue}await verbose((async()=>{const updated=await response.json();return"Updated rule:\n"+JSON.stringify(updated,null,2)}));console.log(chalk.green(`Applied ${manifest.uri}`))}},remove:async(sessionId,resource)=>{const ruleId=parsePath$4(resource);if(!ruleId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/rules/${ruleId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$3(rule){return{uri:`/resources/rules/${rule.ruleId}`,spec:{displayName:rule.displayName,activeFrom:rule.activeFrom,activeTo:rule.activeTo,components:rule.components?.map((c=>({namespace:c.componentNamespace,name:c.componentName,parameters:c.parameters})))}}}function parsePath$3(resource){const matches=resource.match(/^\/?(?:resources\/)?actors\/([a-z]+?)\/([^\/]+)\/?$/);if(!matches){throw new Error("Invalid resource actor path: "+resource)}const[,actorType,actorId]=matches;return{actorType,actorId}}var actors={get:async(sessionId,resource,dump)=>{const{actorType,actorId}=parsePath$3(resource);const response=await fetch$1(sessionId,`/resources/actors/${actorType}/${actorId}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify([{type:dump?"debugDump":"get"}])});if(response.status!==200){await verbose((async()=>`Response:\n${await response.text()}`));throw new Error(`Failed to get debug dump: ${response.statusText}`)}return await response.json()},apply:async(sessionId,manifests)=>{throw new Error("'apply' is not supported on actors")},remove:async(sessionId,resource)=>{throw new Error("'remove' is not supported on actors")}};function parsePath$2(resource){const matches=resource.match(/^\/?(?:resources\/)?job-definitions(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var jobDefinitions={get:async(sessionId,resource)=>{const path=parsePath$2(resource);const response=await fetch$1(sessionId,`/resources/job-definitions/${path||""}`);if(response.status!==200){throw new Error(`Failed to get job definition: ${response.statusText}`)}if(!path){return(await response.json()).map(mapToManifest$2)}return[mapToManifest$2(await response.json())]},apply:async(sessionId,manifests)=>{for(const manifest of manifests){const jobDefinitionId=parsePath$2(manifest.uri);if(!jobDefinitionId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/job-definitions/${jobDefinitionId}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:{...manifest.spec}});if(!response.ok){console.log(chalk.red(`Failed to apply ${manifest.uri}`));console.log(response.headers,response.headers["content-type"]);let e;const contentType=response.headers["content-type"];if(typeof contentType==="string"&&contentType.startsWith("application/json")){e=await response.json()}else{e=await response.text()}if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}continue}await verbose((async()=>{const updated=await response.json();return"Updated job definition:\n"+JSON.stringify(updated,null,2)}));console.log(chalk.green(`Applied ${manifest.uri}`))}},remove:async(sessionId,resource)=>{const reactorId=parsePath$2(resource);if(!reactorId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/job-definitions/${reactorId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$2(jobDefinition){return{uri:`/resources/job-definitions/${jobDefinition.jobDefinitionId}`,spec:{componentId:jobDefinition.componentId}}}function parsePath$1(resource){const matches=resource.match(/^\/?(?:resources\/)?jobs(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var jobs={get:async(sessionId,resource)=>{const path=parsePath$1(resource);if(!path){throw new Error("Please provide job ID")}const response=await fetch$1(sessionId,`/resources/jobs/${path}`);if(response.status!==200){throw new Error(`Failed to get job: ${response.statusText}`)}return[mapToManifest$1(await response.json())]},apply:(sessionId,manifests)=>Promise.reject("Not supported"),remove:async(sessionId,resource)=>{const jobId=parsePath$1(resource);if(!jobId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/jobs/${jobId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest$1(job){return{uri:`/resources/jobs/${job.jobId}`,spec:{},status:{state:job.state,scheduleAt:job.scheduledAt,startedAt:job.startedAt,finishedAt:job.finishedAt,result:job.result}}}function parsePath(resource){const matches=resource.match(/^\/?(?:resources\/)?ingresses(?:\/([^\?]+))?$/);if(!matches){throw new Error("Invalid resource path")}const[,path]=matches;return path}var ingresses={get:async(sessionId,resource)=>{const path=parsePath(resource);const response=await fetch$1(sessionId,`/resources/ingresses/${path||""}`);if(response.status!==200){throw new Error(`Failed to get ingress: ${response.statusText}`)}if(!path){return(await response.json()).map(mapToManifest)}return[mapToManifest(await response.json())]},apply:async(sessionId,manifests)=>{for(const manifest of manifests){const ingressId=parsePath(manifest.uri);if(!ingressId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/ingresses/${ingressId}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:{...manifest.spec}});if(!response.ok){console.log(chalk.red(`Failed to apply ${manifest.uri}`));console.log(response.headers,response.headers["content-type"]);let e;const contentType=response.headers["content-type"];if(typeof contentType==="string"&&contentType.startsWith("application/json")){e=await response.json()}else{e=await response.text()}if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}continue}await verbose((async()=>{const updated=await response.json();return"Updated ingress:\n"+JSON.stringify(updated,null,2)}));console.log(chalk.green(`Applied ${manifest.uri}`))}},remove:async(sessionId,resource)=>{const reactorId=parsePath(resource);if(!reactorId){throw new Error("Invalid resource path")}const response=await fetch$1(sessionId,`/resources/ingresses/${reactorId}`,{method:"DELETE"});if(response.status!==200){throw await response.json()}}};function mapToManifest(ingress){const result={uri:`/resources/ingresses/${ingress.ingressId}`,spec:{type:ingress.type,componentId:ingress.componentId,acl:ingress.acl,properties:ingress.properties}};if(ingress.type==="http"){result.status={endpointPath:`/ingress/${ingress.ingressId}`}}return result}var adapters={getAdapter(resourcePath){const matches=resourcePath.match(/^\/?(?:resources\/)?([\w-]+?)(?:\/|$)/);if(!matches){console.log(chalk.red("Invalid resource path"));return}const[,resource]=matches;if(resource==="components"){return components}else if(resource==="registry"){return registry}else if(resource==="rules"){return rules}else if(resource==="actors"){return actors}else if(resource==="job-definitions"){return jobDefinitions}else if(resource==="jobs"){return jobs}else if(resource==="ingresses"){return ingresses}else{console.log(chalk.red("Invalid resource or not supported"));return}}};var get={command:"get <resource>",describe:"Gets the manifest for the given resource (if supported)",builder:yargs=>yargs.positional("resource",{describe:"The resource path to get",type:"string",demandOption:true}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}).option("out",{alias:"o",describe:"The file to output to",type:"string",demandOption:false}).option("dump",{type:"boolean",demandOption:false,hidden:true}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}let adapter=adapters.getAdapter(argv.resource);if(!adapter){console.log(chalk.red(`Invalid resource path '${argv.resource}'`));return}try{const manifests=await(adapter?.get(sessionId,argv.resource,argv.dump));if(!manifests){console.log(chalk.red("Failed to get manifest"));return}let result;if(Array.isArray(manifests)){result=manifests.map((s=>stringify(s))).join("---\n")}else{result=stringify(manifests)}if(argv.out){console.log(chalk.green(`Writing to ${argv.out}`));fs.writeFileSync(argv.out,result)}else{console.log(result)}}catch(e){console.log(chalk.red(`Failed to get ${argv.resource}`));if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var remove={command:"remove <resource>",describe:"Removes the given resource (if supported)",builder:yargs=>yargs.positional("resource",{describe:"The resource to remove",type:"string",demandOption:true}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}let adapter=adapters.getAdapter(argv.resource);if(!adapter){console.log(chalk.red(`Invalid resource path '${argv.resource}'`));return}try{await adapter.remove(sessionId,argv.resource);console.log(chalk.green(`Removed ${argv.resource}`))}catch(e){console.log(chalk.red(`Failed to remove ${argv.resource}`));printErrors(e.errors)}}};var apply={command:"apply [file]",describe:"Applies the given manifest file to the remote Hantera installation. If not specified, stdin is used.",builder:yargs=>yargs.positional("file",{describe:"The file to apply",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}let data="";if(argv.file){data=fs.readFileSync(argv.file,"utf8")}else if(!process.stdin.isTTY){process.stdin.on("data",(chunk=>{data+=chunk}));process.stdin.on("end",(()=>{console.log("Received input:");console.log(data)}))}else{console.log(chalk.red("No input provided"));return}const manifests=parseAllDocuments(data).map((r=>r.toJS()));if(!manifests){throw`'${argv.file}' is empty or invalid.`}const mappedManifests=new Map;for(const manifest of manifests){const adapter=adapters.getAdapter(manifest.uri);if(!adapter){console.log(chalk.red(`Invalid resource path '${manifest.uri}'`));continue}mappedManifests.set(manifest,adapter)}for(const[manifest,adapter]of mappedManifests.entries()){try{await adapter.apply(sessionId,[manifest],argv.file)}catch(e){console.log(chalk.red(`Failed to apply manifest`));if(e instanceof FetchError){console.log(chalk.red(`Could not connect to ${sessionId}`))}else if(!isEnabled()){console.log(chalk.red("Unknown error. Use --verbose flag to see full error."))}if(isEnabled()){console.log(e)}}}}};var list={command:"list",describe:"Lists installed apps",builder:yargs=>yargs.option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{const response=await fetch$1(sessionId,"/resources/apps");if(!response.ok){throw new Error(`Failed to get apps: ${response.statusText}`)}const apps=await response.json();const table=new AsciiTable3(`Apps in '${sessionId}'`);table.setHeading("Id","Name","Current Version","Authors","Description");table.setWidth(5,30).setWrapped(4);let count=0;for(var app of apps){count+=1;table.addRow(app.id,app.name,app.currentVersion,app.authors.join("\n"),app.description)}if(count>0){console.log(table.toString())}else{console.log("No apps installed")}}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var details={command:"details <appId>",describe:"Gets details of an app",builder:yargs=>yargs.positional("appId",{describe:"The id of the app",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{const response=await fetch$1(sessionId,"/resources/apps/"+argv.appId);if(response.status===404){console.log(chalk.red(`App '${argv.appId}' not found`));return}if(!response.ok){throw new Error(`Failed to get apps: ${response.statusText}`)}const app=await response.json();console.log(chalk.bold("App Details"));console.log("-------------");console.log("Id:",app.id);console.log("Name:",app.name);if(app.description){console.log("Description:");console.log(app.description)}if(app.authors?.length>0){console.log("Authors:");for(var author of app.authors){console.log("- "+author)}}if(app.reactors?.length>0){console.log("Reactors:");for(var reactors of app.reactors){console.log(`- /resources/reactors/${reactors}`)}}console.log("Installed Versions:");for(var version of app.installedVersions){if(version===app.currentVersion){console.log(chalk.green(`- ${version} (active)`))}else{console.log(`- ${version}`)}}if(!app.currentVersion){console.log(chalk.yellow("No active version"))}else{if(app.files?.length>0){console.log();const table=new AsciiTable3("Files");table.setHeading("Path","Mime Type","Size","Metadata");for(var file of app.files){table.addRow(file.path,file.mimeType,file.size,JSON.stringify(file.metadata))}console.log(table.toString())}}}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var install={command:"install <hapk file>",describe:"Gets details of an app",builder:yargs=>yargs.positional("hapk file",{describe:"The app package to install",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{var packagePath=path.resolve(argv.hapkfile);if(!fs.existsSync(packagePath)){console.log(chalk.red(`File not found: ${packagePath}`));return}const formData=new FormData;const packageFile=new File([fs.readFileSync(packagePath)],path.basename(packagePath),{type:"application/zip"});formData.set("package",packageFile,path.basename(packagePath));const response=await fetch$1(sessionId,"/resources/apps",{method:"PUT",body:formData});if(!response.ok){throw new Error(`Failed to install app (${response.statusText}): ${await response.text()}`)}const app=await response.json();console.log(chalk.green(`Installed app ${app.name} (${app.id}) version ${app.currentVersion}`))}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var activate={command:"activate <appId> [-v <version>]",describe:"Activate a deactivated app or change active version",builder:yargs=>yargs.positional("appId",{describe:"The id of the app",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}).option("app-version",{alias:"v",describe:"The version to activate",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{const appId=argv.appId;let version=argv["app-version"];if(!version){version=await getLatestVersion(sessionId,appId)}const response=await fetch$1(sessionId,"/resources/apps/"+appId,{method:"POST",headers:{"Content-Type":"application/json"},body:{version}});if(response.status===404){console.log(chalk.red(`App '${argv.appId}' not found`));return}if(!response.ok){throw new Error(`Failed to activate app (${response.statusText}): ${await response.text()}`)}console.log(chalk.green(`Activated app ${appId} version ${version}`))}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};async function getLatestVersion(sessionId,appId){const response=await fetch$1(sessionId,"/resources/apps/"+appId);if(response.status===404){throw new Error(`App '${appId}' not found`)}if(!response.ok){throw new Error(`Failed to get current app version (${response.statusText}): ${await response.text()}`)}const app=await response.json();app.installedVersions.sort(((a,b)=>a.localeCompare(b,undefined,{numeric:true,sensitivity:"base"})));return app.installedVersions[app.installedVersions.length-1]}var deactivate={command:"deactivate <appId>",describe:"Deactivates an app",builder:yargs=>yargs.positional("appId",{describe:"The id of the app",type:"string"}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}try{const appId=argv.appId;const response=await fetch$1(sessionId,"/resources/apps/"+appId,{method:"POST",headers:{"Content-Type":"application/json"},body:{version:null}});if(response.status===404){console.log(chalk.red(`App '${argv.appId}' not found`));return}if(!response.ok){throw new Error(`Failed to activate app (${response.statusText}): ${await response.text()}`)}console.log(chalk.green(`Deactivated app ${appId}`))}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};var apps={command:"apps <command>",describe:"Commands for managing tenant",builder(yargs){return yargs.command(list).command(details).command(install).command(activate).command(deactivate).demandCommand(1,"You need at least one sub-command")}};var updateSearch={command:"update-search <node...>",describe:"Update/rebuild search indices",builder:yargs=>yargs.positional("node",{description:"The nodes to rebuild. Pass 'all' to rebuild all indices",demandOption:true}).option("rebuild",{alias:"r",type:"boolean",description:`If set, indices will be cleared before refreshing them. Faster but will leave search partially inoperable during build.`,demandOption:false}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}const session=(await getSessions()).find((r=>r.tenantId===sessionId));if(!session){console.log(chalk.red(`Session '${sessionId}' not found`));return}let nodes=argv.node;const rebuild=argv.rebuild;let question;if(rebuild){question=chalk.yellow(`Rebuilding index for ${nodes} in '${session.tenantName}', this will clear the index before rebuilding, proceed? [y/N]`)}else{question=`Update index for ${nodes} in '${session.tenantName}', proceed? [y/N]`}const answer=readline.question(question);if(!answer||!(answer==="Y"||answer==="y")){console.log("Aborting");return}if(nodes.length===1&&nodes[0]==="all"){nodes=[]}try{const payload={nodes,rebuild};console.log(`Scheduling index update..`);const response=await fetch$1(sessionId,"/api/system/searchIndex/update",{method:"POST",headers:{"Content-Type":"application/json"},body:payload});if(!response.ok){throw new Error(`Unable to schedule rebuild: (${response.status} ${response.statusText}) ${await response.text()}`)}console.log(chalk.green(`Rebuild scheduled successfully!`));console.log(`This operation may take a while to complete. Use 'h_ manage signals graph' to check progress.`)}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}};const placeholderRegex=/\{(.+?)}/gi;function renderMessage(template,properties){if(!template){return undefined}if(!properties){properties={}}return template.replace(placeholderRegex,((_,p1)=>{if(properties){let token=properties[p1];if(typeof token!=="undefined"){return chalk.blue(token?.toString())}}return`{${p1.toLowerCase()}}`}))}var signals={command:"signals [path]",describe:"Displays system signals",builder:yargs=>yargs.positional("path",{description:"Filter signals by path",demandOption:false}).option("watch",{alias:"w",type:"boolean",description:`Refreshes signals every seconds`,demandOption:false}).option("session",{alias:"s",describe:"The session to use",type:"string",demandOption:false}),handler:async argv=>{var sessionId=argv.session||await getDefaultSession();if(!sessionId){console.log(chalk.red("No session set. Use h_ manage use <session> to set the default session or pass the session with -s <session>"));return}const session=(await getSessions()).find((r=>r.tenantId===sessionId));if(!session){console.log(chalk.red(`Session '${sessionId}' not found`));return}if(argv.watch){console.clear();console.log("Loading signals...");setInterval((()=>{printSignals(true)}),1e3)}else{await printSignals()}async function printSignals(clearConsole=false){try{const response=await fetch$1(sessionId,`/resources/registry/signals/${argv.path??""}?searchType=prefix`,{method:"GET"});if(!response.ok){throw new Error(`Unable to get signals: (${response.status} ${response.statusText}) ${await response.text()}`)}let signals=await response.json();if(clearConsole){console.clear()}signals.filter((r=>r.value!==undefined)).forEach((signal=>{const id=signal.key.substring(8);const message=renderMessage(signal.value.messageTemplate,signal.value.properties);if(signal.value.severity==="information"){print("INFO",chalk.gray,id,message)}else if(signal.value.severity==="warning"){print("WARN",chalk.yellow,id,message)}else if(signal.value.severity==="error"){print("ERR ",chalk.red,id,message)}function print(status,color,signalId,message){console.log("["+color(status)+`] ${signalId}${message?": "+message:""}`)}}));console.log("Updated: "+(new Date).toLocaleTimeString())}catch(e){if(e.errors){printErrors(e.errors)}else{console.log(chalk.red(e))}}}}};var manage={command:"manage <command>",describe:"Commands for managing tenant",builder(yargs){return yargs.command(login).command(logout).command(sessions).command(use).command(get).command(apply).command(remove).command(apps).command(updateSearch).command(signals).demandCommand(1,"You need at least one sub-command")}};var $new={command:"new",describe:"Creates a new app",builder:yargs=>yargs,handler:async argv=>{let appName=question("App Name",(value=>{if(!value){console.log(chalk.red("App Name is required."));return false}return true}));let defaultAppId=appName.replace(/[^a-z0-9]/gi,"-").replace(/^-+|-+$/g,"").toLowerCase();const appId=question(`App ID [${defaultAppId}]`,(value=>{if(!value){value=defaultAppId}if(!/^[a-z0-9_-]+$/.test(value)){console.log(chalk.red("App ID can only contain lowercase letters, numbers, dash and underscope."));return false}return true}))||defaultAppId;let appFolder=path.resolve(question(`Path [./${appId}/]`)||`./${appId}/`);let description=question("Description");let author=question(`Author [${os.userInfo().username}]`)||os.userInfo().username;let setupPortalApp=question("Setup portal app? (y/n)",(value=>{if(value!=="y"&&value!=="n"){console.log(chalk.red('Invalid value. Please enter "y" or "n".'));return false}return true}))==="y";const appConfig={id:appId,name:appName,description,authors:[author]};if(setupPortalApp){appConfig.extensions={portal:"./portal"}}if(!fs.existsSync(appFolder)){fs.mkdirSync(appFolder)}const filePath=path.join(appFolder,"h_app.yaml");if(fs.existsSync(filePath)){console.log(chalk.red(`'h_app.yaml' already exists. Run 'h_ app new' in an empty folder.`));return}console.log(chalk.gray(`Creating ${filePath}...`));fs.writeFileSync(filePath,stringify(appConfig));console.log(chalk.green("Done."));const ignore=[".hantera/"];if(setupPortalApp){const portalFolder=path.join(path.resolve(appFolder),"portal");if(!fs.existsSync(portalFolder)){console.log(chalk.gray(`Creating ${portalFolder}...`));fs.mkdirSync(portalFolder);console.log(chalk.green("Done."))}await createPortalApp(appId,appFolder,portalFolder);const relativePortalFolder=path.relative(appFolder,portalFolder);ignore.push(path.join(relativePortalFolder,"node_modules"))}console.log(chalk.gray(`Creating ${path.join(appFolder,".gitignore")}...`));fs.writeFileSync(path.join(appFolder,".gitignore"),ignore.join("\n"));console.log(chalk.green("Done."));console.log(`App '${appId}' created in '${appFolder}'. Run '${chalk.green("h_ app package")}' to create a hapk package.`)}};function question(label,validator){let value="";while(true){value=readline.question(label+": ");if(validator&&!validator(value)){continue}break}return value}async function createPortalApp(appId,appFolder,portalFolder){if(fs.existsSync(path.join(portalFolder,"package.json"))){console.log(chalk.red(`'package.json' already exists. Run 'h_ app new' in an empty folder.`));return}const portalAppVersion=await getNpmPackageVersion("@hantera/portal-app");const designSystemVersion=await getNpmPackageVersion("@hantera/design-system");async function getNpmPackageVersion(packageId){const response=await fetch("https://registry.npmjs.org/"+packageId);const json=await response.json();return json["dist-tags"].latest}console.log(chalk.gray(`Creating ${path.join(portalFolder,"package.json")}...`));fs.writeFileSync(path.join(portalFolder,"package.json"),`{\n "name": "${appId}",\n "version": "1.0.0",\n "main": "index.ts",\n "type": "module",\n "dependencies": {\n "@hantera/portal-app": "${portalAppVersion}",\n "@hantera/design-system": "${designSystemVersion}",\n "vue": "^3.4.37"\n }\n}\n`);console.log(chalk.green("Done."));console.log(chalk.gray(`Creating ${path.join(portalFolder,"tsconfig.json")}...`));fs.writeFileSync(path.join(portalFolder,"tsconfig.json"),`{\n "compilerOptions": {\n "target": "ESNext",\n "useDefineForClassFields": true,\n "module": "ESNext",\n "moduleResolution": "Node",\n "strict": true,\n "jsx": "preserve",\n "sourceMap": true,\n "resolveJsonModule": true,\n "isolatedModules": true,\n "esModuleInterop": true,\n "lib": ["ESNext", "DOM"],\n "skipLibCheck": true\n },\n "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"]\n}\n`);console.log(chalk.green("Done."));console.log(chalk.gray(`Creating ${path.join(portalFolder,"index.ts")}...`));fs.writeFileSync(path.join(portalFolder,"index.ts"),`import { apps, Portal } from '@hantera/portal-app';\nimport MyComponent from './components/MyComponent.vue';\n\nexport default function(portal: Portal) {\n portal.registerComponent(apps.orderViewSlots.delivery.footer, MyComponent);\n}\n`);console.log(chalk.green("Done."));const componentsDir=path.join(portalFolder,"components");if(!fs.existsSync(componentsDir)){console.log(chalk.gray(`Creating ${componentsDir}...`));fs.mkdirSync(componentsDir,484);console.log(chalk.green("Done."))}console.log(chalk.gray(`Creating ${path.join(componentsDir,"MyComponent.vue")}...`));fs.writeFileSync(path.join(componentsDir,"MyComponent.vue"),`<script setup lang="ts">\nimport { apps } from '@hantera/portal-app'\n\nconst context = apps.componentContext(apps.orderViewSlots.delivery.footer)\n\nconst delivery = context.delivery;\n<\/script>\n\n<template>\n Current delivery number: {{ delivery?.deliveryNumber }}\n</template>\n`);console.log(chalk.green("Done."));console.log(chalk.gray(`Installing dependencies...`));execSync("npm install",{cwd:portalFolder,stdio:"inherit"});console.log(chalk.green("Done."));console.log("\n");console.log(`Portal app created. Run '${chalk.green("h_ app dev")}' to start the local development server.`)}function packageToImportMapKey$1(p){return"__portal_"+p.replace("@","").replace("/","_").replace("-","_")}function portalResolve$1(packages){const packageIds=packages.map(packageToImportMapKey$1);return{name:"vite-plugin-portal-resolve",enforce:"pre",config(config){config.optimizeDeps={...config.optimizeDeps??{},exclude:[...config.optimizeDeps?.exclude??[],...packages]};if(!config.build){config.build={}}if(!config.build.rollupOptions){config.build.rollupOptions={}}config.build.rollupOptions.external=[...config.build.rollupOptions.external??[],...packages.map(packageToImportMapKey$1)]},configResolved(resolvedConfig){const VALID_ID_PREFIX=`/@id/`;const reg=new RegExp(`${VALID_ID_PREFIX}(${packages.map((r=>packageToImportMapKey$1(r))).join("|")})`,"g");resolvedConfig.plugins.push({name:"vite-plugin-portal-resolve-replace-idprefix",transform:code=>{const result=reg.test(code)?code.replace(reg,((m,s1)=>s1)):code;return result}})},async resolveId(source,importer,options){if(packages.includes(source)){return{id:packageToImportMapKey$1(source),external:true,resolvedBy:"vite-plugin-portal-resolve"}}},load(id){if(packageIds.indexOf(id)!==-1){return`export default {};`}}}}function packageToImportMapKey(p){return"__portal_"+p.replace("@","").replace("/","_").replace("-","_")}function portalResolve(packages){const reg=new RegExp(`(import\\s+(?:[\\s\\S]+?)\\s+from\\s+["'])(${packages.map((r=>r.replace("/","\\/"))).join("|")})(["'];?)`,"g");return{name:"vite-plugin-portal-resolve",enforce:"pre",config(config){config.optimizeDeps={...config.optimizeDeps??{},exclude:[...config.optimizeDeps?.exclude??[],...packages]};if(!config.build){config.build={}}if(!config.build.rollupOptions){config.build.rollupOptions={}}config.build.rollupOptions.external=[...config.build.rollupOptions.external??[],...packages.map(packageToImportMapKey)]},configResolved(resolvedConfig){resolvedConfig.plugins.push({name:"vite-plugin-portal-resolve-replace-idprefix",transform:code=>{const result=reg.test(code)?code.replace(reg,((m,s1,s2,s3)=>s1+packageToImportMapKey(s2)+s3)):code;return result}})}}}function developmentConfig(cacheFolder,isVerbose=false){return{configFile:false,mode:"development",root:cacheFolder,plugins:[vue(),portalResolve$1(["vue","@hantera/portal-app","@hantera/design-system"]),mkcert()],logLevel:isVerbose?"info":"error",server:{port:4734,cors:true},build:{target:"esnext"}}}function buildConfig(portalFolder,entry,outFolder){const result=developmentConfig(portalFolder);result.mode="production";delete result.server;result.logLevel="warn";result.build.lib={entry,name:"hantera-app",fileName:"index",formats:["es"]};result.plugins=[vue(),portalResolve(["vue","@hantera/portal-app","@hantera/design-system"])];result.build.emptyOutDir=true;result.build.outDir=outFolder;if(!result.define){result.define={}}result.define["process.env.NODE_ENV"]=JSON.stringify("production");return result}var dev={command:"dev [path]",describe:"Starts a portal app in local development mode",builder:yargs=>yargs.positional("path",{describe:"Path to the app",type:"string",default:"."}),handler:async argv=>{const appFolder=path.resolve(argv.path);const appConfigPath=path.join(appFolder,"h_app.yaml");if(!fs.existsSync(appConfigPath)){console.log(chalk.red(`'h_app.yaml' not found in path '${appFolder}'`));return}const appConfig=yaml.parse(fs.readFileSync(appConfigPath,"utf8"));if(!appConfig.extensions?.portal){console.log(chalk.red(`'h_app.yaml' missing 'extensions.portal' property.`));return}const portalFolder=path.join(appFolder,appConfig.extensions.portal);if(!fs.existsSync(path.join(portalFolder,"package.json"))){console.log(chalk.red(`'package.json' not found in path '${portalFolder}'`));return}const packageJson=JSON.parse(fs.readFileSync(path.join(portalFolder,"package.json"),"utf8"));const entryPath=path.normalize(path.join(portalFolder,packageJson.main||"index.ts"));let cacheDir=path.join(appFolder,".hantera");if(!fs.existsSync(cacheDir)){fs.mkdirSync(cacheDir,484)}cacheDir=path.join(cacheDir,"portal");if(!fs.existsSync(cacheDir)){fs.mkdirSync(cacheDir,484)}fs.writeFile(path.join(cacheDir,"index.html"),`<!DOCTYPE html>\n <html lang="en">\n <head>\n </head>\n <body>\n <script type="module" src="loader.ts"><\/script>\n </body>\n </html>\n `,(err=>{if(err){console.error(err)}}));const loader_ts=`export const appId = ${JSON.stringify(packageJson.name)};\n export { default as default } from '${path.relative(cacheDir,entryPath).replaceAll("\\","/").replaceAll(".ts","")}';`;fs.writeFile(path.join(cacheDir,"loader.ts"),loader_ts,(err=>{if(err){console.error(err)}}));const server=await createServer(developmentConfig(cacheDir,isEnabled()));await server.listen();if(isEnabled()){server.printUrls()}console.log(chalk.green("Local app development is running.."));console.log(boxen(chalk.black(`To enable local development mode in Hantera portal:\n- Go to Developer settings\n- Click "Enable Local Development"`),{borderStyle:"none",backgroundColor:"blue",padding:1}))}};const ingressIdRegex=/^[a-z0-9_.-]+$/;function endsWith(str,suffix){return str.indexOf(suffix,str.length-suffix.length)!==-1}var pack={command:"pack [path] [-v 1.0.0]",describe:"Builds and package the app",builder:yargs=>yargs.positional("path",{describe:"Path to the app",type:"string",default:"."}).option("package-version",{alias:"v",describe:"Version of the app",type:"string",default:"1.0.0"}),handler:async argv=>{const appFolder=path.resolve(argv.path);const appConfigPath=path.join(appFolder,"h_app.yaml");if(!fs.existsSync(appConfigPath)){console.log(chalk.red(`'h_app.yaml' not found in path '${appFolder}'`));return}const appConfig=yaml.parse(fs.readFileSync(appConfigPath,"utf8"));const packageManifest={id:appConfig.id,name:appConfig.name,description:appConfig.description,authors:appConfig.authors,version:argv.packageVersion,files:[],components:[],ingresses:[]};if(!packageManifest.id){console.log(chalk.red(`'id' property is required in 'h_app.yaml'`));return}if(!packageManifest.name){console.log(chalk.red(`'name' property is required in 'h_app.yaml'`));return}const zipFileWriter=new BlobWriter;const zipWriter=new ZipWriter(zipFileWriter);if(Array.isArray(appConfig.files)){for(const file of appConfig.files){if(!file.id){console.log(chalk.red(`'id' property is required for each file in 'h_app.yaml'`));return}let id=file.id;if(!id&&file.path){id=path.basename(file.path)}id=id.replace("\\","/");if(id.startsWith("portal/")){console.log(chalk.red(`File ID cannot start with 'portal/'`));return}let mimeType=file.mimeType;if(!mimeType){mimeType=mime.lookup(file.source)||"application/octet-stream"}packageManifest.files.push({id,mimeType,metadata:file.metadata||{}});const entryId=path.join("files",id).replace("\\","/");verbose((()=>`Adding file: ${entryId}`));const fileReader=new BlobReader(new Blob([fs.readFileSync(path.resolve(file.source))]));console.log(entryId);await zipWriter.add(entryId,fileReader)}}if(Array.isArray(appConfig.components)){for(const component of appConfig.components){if(!component.id){console.log(chalk.red(`'id' property is required for each component in 'h_app.yaml'`));return}let id=component.id.replaceAll("\\","/");let sourcePath=component.path;if(!sourcePath){sourcePath=path.join("components",id)}let expectedType;if(endsWith(sourcePath,".module.hreactor")||endsWith(sourcePath,".module.hrc")){expectedType="reactor-module"}else if(endsWith(sourcePath,".hreactor")||endsWith(sourcePath,".hrc")){expectedType="reactor"}else if(endsWith(sourcePath,".hrule")||endsWith(sourcePath,".hrl")){expectedType="rule"}else if(endsWith(sourcePath,".hdiscount")||endsWith(sourcePath,".hdc")){expectedType="discount"}if(!component.type&&expectedType){component.type=expectedType}if(expectedType&&component.type!==expectedType){console.log(chalk.yellow(`${component.id}:\nComponent type was specified as '${component.type}', but it looks like you're adding a '${expectedType}'.`))}if(!component.type){console.log(chalk.red(`${component.id}:\nComponent type must be specified.`));return}let componentType=component.type;if(!componentType){componentType=path.extname(id).substring(1)}packageManifest.components.push({id,displayName:component.displayName,description:component.description,type:component.type});const entryId=path.join("components",id).replaceAll("\\","/");verbose((()=>`Adding Component: ${entryId}`));const fileReader=new BlobReader(new Blob([fs.readFileSync(path.resolve(sourcePath))]));await zipWriter.add(entryId,fileReader)}}if(appConfig.extensions?.portal){const portalFolder=path.join(appFolder,appConfig.extensions.portal);if(!fs.existsSync(path.join(portalFolder,"package.json"))){console.log(chalk.red(`'package.json' not found in path '${portalFolder}'`));return}const packageJson=JSON.parse(fs.readFileSync(path.join(portalFolder,"package.json"),"utf8"));const entryPath=path.normalize(path.join(portalFolder,packageJson.main||"index.ts"));const distFolder=path.join(appFolder,".hantera","portal","dist");fs.rmSync(distFolder,{recursive:true,force:true});if(!await typeCheck(portalFolder)){return}await build(buildConfig(portalFolder,entryPath,distFolder));const files=fs.readdirSync(distFolder);for(const file of files){const filePath=path.join(distFolder,file);const mimeType=path.extname(file)===".js"?"application/javascript":"text/css";const fileId=path.join("portal",file).replaceAll("\\","/");packageManifest.files.push({id:fileId,mimeType,metadata:{portal:"true"}});const entryId=path.join("files","portal",file).replaceAll("\\","/");verbose((()=>`Adding file: ${entryId}`));const fileReader=new BlobReader(new Blob([fs.readFileSync(filePath)]));await zipWriter.add(entryId,fileReader)}}if(Array.isArray(appConfig.ingresses)){for(const ingress of appConfig.ingresses){if(!ingress.id){console.log(chalk.red(`'id' property is required for each ingress in 'h_app.yaml'`));return}if(!ingress.componentId){console.log(chalk.red(`'componentd' property is required for each ingress in 'h_app.yaml'`));return}if(!ingressIdRegex.test(ingress.id)){console.log(chalk.red(`Ingress id '{ingress.id}' can only contain the following: a-z . _ -`));return}if(ingress.type!="http"){console.log(chalk.yellow(`Unknown ingress type '${ingress.type}' in 'h_app.yaml'`))}const component=packageManifest.components.find((r=>r.id===ingress.componentId));if(!component){console.log(chalk.red(`Component '${ingress.componentId}' for Ingress '${ingress.id}' was not found in 'h_app.yaml'`));return}verbose((()=>`Adding Ingress: ${ingress.id}`));packageManifest.ingresses.push({id:ingress.id,componentId:ingress.componentId,type:ingress.type,properties:ingress.properties,acl:ingress.acl??[]})}}const manifestReader=new TextReader(JSON.stringify(packageManifest,null,2));await zipWriter.add("manifest.json",manifestReader);await zipWriter.close();const zipFileBlob=await zipFileWriter.getData();const zipFileArrayBuffer=await zipFileBlob.arrayBuffer();const packagePath=path.join(appFolder,`${packageManifest.id}-${packageManifest.version}.hapk`);fs.writeFileSync(packagePath,Buffer.from(zipFileArrayBuffer));console.log(`App '${packageManifest.id}' packaged in '${packagePath}'.`)}};function typeCheck(portalFolder){const vueTscPath=import.meta.resolve("vue-tsc/bin/vue-tsc.js").replace("file:///","");return new Promise(((resolve,reject)=>{const child=spawn("node",[vueTscPath,"--noEmit","--project",portalFolder],{stdio:"inherit"});child.on("exit",((code,_)=>{if(code==0){resolve(true)}resolve(false)}));child.on("error",(()=>{reject()}))}))}var app={command:"app <command>",describe:"Commands for authoring apps",builder(yargs){return yargs.command($new).command(dev).command(pack).demandCommand(1,"You need at least one sub-command")}};const commands=[app,manage];if(process.argv.find((v=>v==="--verbose"))){enable()}yargs(hideBin(process.argv.filter((r=>r!=="--verbose")))).scriptName("h_").showHelpOnFail(true).command(commands).demandCommand().strict().parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hantera/cli",
3
- "version": "20250707.0.0-develop.1",
3
+ "version": "20250714.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {