@calljmp/cli
Version:
35 lines (34 loc) • 15.5 kB
JavaScript
"use strict";var E=Object.create;var v=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var N=Object.getPrototypeOf,K=Object.prototype.hasOwnProperty;var L=(n,e)=>{for(var i in e)v(n,i,{get:e[i],enumerable:!0})},F=(n,e,i,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of U(e))!K.call(n,o)&&o!==i&&v(n,o,{get:()=>e[o],enumerable:!(a=W(e,o))||a.enumerable});return n};var y=(n,e,i)=>(i=n!=null?E(N(n)):{},F(e||!n||!n.__esModule?v(i,"default",{value:n,enumerable:!0}):i,n)),M=n=>F(v({},"__esModule",{value:!0}),n);var ae={};L(ae,{default:()=>oe});module.exports=M(ae);var D=require("commander"),I=y(require("../config")),T=require("../configure"),f=y(require("enquirer")),g=y(require("ora")),t=y(require("chalk")),x=require("../account"),r=y(require("../logger")),$=require("../project"),h=require("../common"),m=y(require("path")),z=y(require("../retry")),Y=y(require("../ios")),G=y(require("../android")),A=y(require("fs/promises")),q=require("child_process"),O=require("util"),R=y(require("os")),b=require("../mobile");const Z=(0,O.promisify)(q.exec);async function _(n){try{const e=R.default.platform();let i;switch(e){case"darwin":{i=`open -R "${n}"`;break}case"win32":{i=`explorer /select,"${n.replace(/\//g,"\\")}"`;break}case"linux":{const a=m.default.dirname(n);i=`(nautilus --select "${n}" 2>/dev/null || dolphin --select "${n}" 2>/dev/null || thunar "${a}" 2>/dev/null || xdg-open "${a}")`;break}default:{i=`xdg-open "${m.default.dirname(n)}"`;break}}return await Z(i),!0}catch(e){return r.default.error("Failed to open file explorer:",e),!1}}function k(n){let e=n.trim();return(e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'"))&&(e=e.slice(1,-1)),e=e.replace(/\\(.)/g,"$1"),e}async function H(){return new Promise(n=>{const e=(0,g.default)(t.default.blue("Waiting for .enc file... (drag and drop the file into this terminal or press Ctrl+C to cancel)")).start();let i="",a=null;const o=()=>{a&&(clearTimeout(a),a=null),process.stdin.removeAllListeners("data"),process.stdin.isTTY&&process.stdin.setRawMode(!1),process.stdin.pause(),e.stop()},c=async u=>{try{const s=k(u);await A.access(s),o(),e.stop(),n(s)}catch{}};process.stdin.isTTY&&process.stdin.setRawMode(!0),process.stdin.resume(),process.stdin.setEncoding("utf8"),process.stdin.on("data",u=>{const s=u.toString();if(s===""&&(o(),e.stop(),process.exit(1)),(s==="\r"||s===`
`)&&i.trim()===""){o(),e.stop(),n(-1);return}if(i+=s,s.includes(`
`)||s.includes("\r")||s.includes(" ")){const l=i.replace(/[\r\n\s]+$/g,"").trim();if(l.length>0&&(l.includes("/")||l.includes("\\")||l.includes("."))){c(l),i="";return}}const p=i.trim();if(p.length>10&&(p.includes("/")||p.includes("\\"))&&(p.match(/[/\\]/g)||[]).length>=2&&!p.endsWith("/")&&!p.endsWith("\\")){c(p),i="";return}}),a=setTimeout(()=>{e.isSpinning&&(e.text=t.default.blue("Waiting for .enc file... (drag and drop, or press Enter to type manually)"))},15e3)})}async function J({project:n,selectedProject:e,projectDirectory:i}){const a=await Y.resolveProjectInfo({projectDirectory:i}).catch(()=>null);if(!a)return r.default.info(t.default.dim("No iOS project detected, skipping iOS configuration.")),e;r.default.info(t.default.yellow(`
iOS Project Configuration`));let o=a.bundleId,c=a.teamId;const u=()=>f.default.prompt({type:"input",name:"newBundleId",message:"Enter iOS Bundle ID (or leave blank to skip):",initial:o,validate:l=>l?/^[a-zA-Z0-9.-]+$/.test(l)?!0:"Invalid iOS Bundle ID format":!0}),s=()=>f.default.prompt({type:"input",name:"newTeamId",message:"Enter Apple Team ID (or leave blank to skip):",initial:c,validate:l=>l?/^[A-Z0-9]{10}$/.test(l)?!0:"Invalid Apple Team ID format":!0}),{confirmBundleId:p}=await f.default.prompt({type:"confirm",name:"confirmBundleId",message:`iOS Bundle ID: ${o}. Use this value?`,initial:!0});if(!p){const{newBundleId:l}=await u();o=l||void 0}if(c){const{confirmTeamId:l}=await f.default.prompt({type:"confirm",name:"confirmTeamId",message:`Apple Team ID: ${c}. Use this value?`,initial:!0});if(!l){const{newTeamId:d}=await s();c=d||void 0}}else{const{newTeamId:l}=await s();c=l||void 0}if(c||o){const l=(0,g.default)(t.default.blue("Configuring iOS project...")).start();try{const d=await n.update({projectId:e.id,appleIosTeamId:c,appleIosBundleId:o});return l.stop(),d}catch(d){l.fail(t.default.red("Failed to configure iOS project!")),r.default.error(d),process.exit(1)}}return e}async function Q({project:n,selectedProject:e,projectDirectory:i}){const a=await G.resolveProjectInfo({projectDirectory:i}).catch(()=>null);if(!a)return r.default.info(t.default.dim("No Android project detected, skipping Android configuration.")),{selectedProject:e,androidProjectInfo:null};r.default.info(t.default.yellow(`
Android Project Configuration`));let o=a.applicationId;const c=()=>f.default.prompt({type:"input",name:"newApplicationId",message:"Enter Android Application ID (or leave blank to skip):",initial:o,validate:s=>s?/^[a-zA-Z0-9._-]+$/.test(s)?!0:"Invalid Android Application ID format":!0}),{confirmApplicationId:u}=await f.default.prompt({type:"confirm",name:"confirmApplicationId",message:`Android Application ID: ${o}. Use this value?`,initial:!0});if(!u){const{newApplicationId:s}=await c();o=s||void 0}if(o){const s=(0,g.default)(t.default.blue("Configuring Android project...")).start();try{const p=await n.update({projectId:e.id,googleAndroidPackageName:o});return s.stop(),{selectedProject:p,androidProjectInfo:a}}catch(p){s.fail(t.default.red("Failed to configure Android project!")),r.default.error(p),process.exit(1)}}return{selectedProject:e,androidProjectInfo:a}}async function V({project:n,projectId:e}){const i=(0,g.default)(t.default.blue("Waiting for project to be provisioned...")).start();try{await(0,z.default)(()=>n.retrieve({projectId:e}),{retries:10,delay:3e3,shouldRetry:a=>a instanceof h.ServiceError&&a.code===h.ServiceErrorCode.ResourceBusy}),i.stop()}catch(a){i.fail(t.default.red("Timed out waiting for project to be provisioned! Please try again later.")),r.default.error(a),process.exit(1)}}async function X(n){return await f.default.prompt([{type:"input",name:"module",message:"Service module directory",initial:m.default.relative(n.project,n.module),required:!0,validate:e=>e?!0:"Service module directory is required"},{type:"input",name:"migrations",message:"Migrations directory",initial:m.default.relative(n.project,n.migrations),required:!0,validate:e=>e?!0:"Migrations directory is required"},{type:"input",name:"schema",message:"Schema directory",initial:m.default.relative(n.project,n.schema),required:!0,validate:e=>e?!0:"Schema directory is required"}])}async function ee({project:n,projectId:e}){const i=(0,g.default)(t.default.blue("Synchronizing service bindings...")).start();try{let c=function(u,s=""){Object.entries(u).forEach(([p,l],d,j)=>{const w=d===j.length-1,P=w?"\u2514\u2500\u2500 ":"\u251C\u2500\u2500 ",B=s+(w?" ":"\u2502 ");typeof l=="object"&&l!==null&&!Array.isArray(l)?(r.default.info(`${s}${t.default.dim(P)}${p}`),c(l,B)):r.default.info(`${s}${t.default.dim(P)}${p}: ${l}`)})};var a=c;const o=await n.bindings({projectId:e});return i.stop(),r.default.info("Service bindings"),c(o),o}catch(o){i.fail(t.default.red("Failed to synchronize service bindings!")),r.default.error(o),process.exit(1)}}async function te({project:n,selectedProject:e,projectDirectory:i,androidProjectInfo:a}){if(e.googleAndroidPlayIntegrityHasResponseKeys)return e;r.default.info(t.default.yellow(`
Android Play Integrity Setup`)),r.default.info(`Android Play Integrity helps protect your app from potentially risky and fraudulent interactions.
`);const{setupPlayIntegrity:o}=await f.default.prompt({type:"confirm",name:"setupPlayIntegrity",message:"Would you like to configure Play Integrity now?",initial:!0});if(!o)return r.default.info(t.default.dim("Skipping Play Integrity setup for now.")),e;r.default.info(`
Please follow these steps to configure Play Integrity API:`),r.default.info(t.default.cyan("1. Go to https://play.google.com/console")),r.default.info(t.default.cyan("2. Select or create your app")),r.default.info(t.default.cyan("3. Navigate to: App Dashboard \u2192 Test and Release \u2192 App Integrity")),r.default.info(t.default.cyan('4. Click on "Play Integrity API settings"')),r.default.info(t.default.cyan("5. Link your Google Cloud project"));const{readyForPem:c}=await f.default.prompt({type:"confirm",name:"readyForPem",message:"Ready to download .pem file for upload to Play Console? This will download the public key to encrypt responses.",initial:!0});if(!c)return r.default.info(t.default.dim("Skipping Play Integrity setup for now.")),e;const u=m.default.join(i,"play-integrity-key.pem"),s=(0,g.default)(t.default.blue("Downloading .pem file...")).start();try{const d=await n.retrieveGoogleAndroidPlayIntegrityPem({projectId:e.id});await A.writeFile(u,d),s.succeed(t.default.green(`Downloaded .pem file to: ${u}`)),r.default.info(t.default.blue(`
Opening folder with the .pem file...`)),await _(u)?r.default.info(t.default.dim("The file explorer should now be open showing the .pem file.")):r.default.info(t.default.dim("Could not open file explorer automatically."))}catch(d){s.fail(t.default.red("Failed to download .pem file!")),r.default.error(d),process.exit(1)}r.default.info(t.default.yellow(`
Upload to Play Console:`)),r.default.info(t.default.cyan("1. In Play Console \u2192 App Integrity \u2192 Play Integrity API settings")),r.default.info(t.default.cyan('2. Find the "Classic requests" section')),r.default.info(t.default.cyan(`3. Upload the downloaded file: ${m.default.basename(u)}`)),r.default.info(t.default.cyan('4. Download the "Classic requests encryption keys" (.enc file)')),r.default.info(t.default.cyan(`5. Once you have the .enc file, you can drag and drop it into the terminal when prompted
`));let p;try{const d=await H();if(d===-1){const{manualEncFilePath:j}=await f.default.prompt({type:"input",name:"manualEncFilePath",message:"Enter the path to your .enc file:",initial:m.default.join(i,`${a.packageName}.enc`),validate:async w=>{if(!w)return"File path is required";const P=k(w);try{await A.access(P)}catch{return"File does not exist. Please check the path."}return P.endsWith(".enc")?!0:"File must have .enc extension"}});p=k(j)}else if(typeof d=="string")p=d;else return r.default.error(t.default.red("No .enc file provided!")),e}catch(d){return r.default.error("Error waiting for file:",d),e}const l=(0,g.default)(t.default.blue("Uploading Play Integrity response keys...")).start();try{const j=(await A.readFile(p)).toString("base64"),w=await n.update({projectId:e.id,googleAndroidPlayIntegrityResponseKeys:j});return l.succeed(t.default.green("Play Integrity API configured successfully!")),w}catch(d){l.fail(t.default.red("Failed to upload response keys!")),r.default.error(d),process.exit(1)}}async function S({project:n}){const{name:e,description:i}=await f.default.prompt([{type:"input",name:"name",message:"Project name",required:!0,validate:o=>o?/^[a-z-]+$/.test(o)?!0:"Only lowercase letters and hyphens are allowed":"Project name is required"},{type:"input",name:"description",message:"Project description (optional)"}]),a=(0,g.default)(t.default.blue("Creating project...")).start();try{const o=await n.create({name:e,description:i||void 0});return a.stop(),o}catch(o){if(o instanceof h.ServiceError&&o.code===h.ServiceErrorCode.ProjectAlreadyExists)return a.fail(t.default.red("Project with this name exists or recently deleted!")),S({project:n});a.fail(t.default.red("Failed to create project!")),r.default.error(o),process.exit(1)}}async function C({project:n,offset:e}){const{projects:i,nextOffset:a}=await n.list({offset:e});if(i.length===0){r.default.info(t.default.yellow("Create a new project to get started!"));return}const o=[...i.map(s=>({name:s.name,value:s.name})),...a?[{name:"More projects...",value:-1}]:[],{name:"Create new project",value:-2}],c=await f.default.prompt({type:"autocomplete",name:"value",message:"Select a project",choices:o});if(c.value===-1)return C({project:n,offset:a});if(c.value===-2)return;const u=i.find(s=>s.name===c.value);if(!u){r.default.error(t.default.red("Project not found!"));return}return u}async function ne(n){let e;{const i=(0,g.default)(t.default.blue("Requesting authorization...")).start();try{const{requestId:a,authorizationUrl:o}=await n.requestAccess();e=a,i.stop(),r.default.info("Open the following URL to authorize CLI:"),r.default.info(t.default.yellow(o))}catch{i.fail(t.default.red("Failed to request authorization!"));return}}{const i=(0,g.default)(t.default.blue("Waiting for authorization...")).start();try{const a=await n.pollAccess(e);return i.stop(),a}catch{i.fail(t.default.red("Authorization failed!"));return}}}function re(n){switch((0,b.detectProjectType)(n)){case b.ProjectType.ReactNative:{r.default.info(t.default.green(`
You can now use the Calljmp SDK in your React Native app.
Follow the instructions below to get started:
`)),r.default.info(t.default.cyan.bold("1.")+t.default.cyan(` Install the Calljmp SDK:
`)+t.default.green(` npm install @calljmp/react-native
`)+t.default.green(` # or
`)+t.default.green(` yarn add @calljmp/react-native
`)),r.default.info(t.default.cyan.bold("2.")+t.default.cyan(` Import and initialize Calljmp in your app:
`)+t.default.yellow(` import { Calljmp } from '@calljmp/react-native';
`)+t.default.yellow(` const calljmp = new Calljmp();
`));break}case b.ProjectType.Flutter:{r.default.info(t.default.green(`
You can now use the Calljmp SDK in your Flutter app.
Follow the instructions below to get started:
`)),r.default.info(t.default.cyan.bold("1.")+t.default.cyan(` Add the Calljmp SDK to your pubspec.yaml:
`)+t.default.green(` dependencies:
`)+t.default.green(` calljmp: ^latest
`)),r.default.info(t.default.cyan.bold("2.")+t.default.cyan(` Install the dependencies:
`)+t.default.green(` flutter pub get
`)),r.default.info(t.default.cyan.bold("3.")+t.default.cyan(` Import and initialize Calljmp in your app:
`)+t.default.yellow(` import 'package:calljmp/calljmp.dart';
`)+t.default.yellow(` final calljmp = Calljmp();
`));break}default:r.default.info(t.default.green(`
You can now start using Calljmp in your mobile app.
`))}}const ie=()=>new D.Command("setup").description("Setup environment, account, and project.").addOption(I.ConfigOptions.ProjectDirectory).action(async n=>{const e=await(0,I.default)(n),i=new x.Account(e);if(!await i.authorized()){const w=await ne(i);w||process.exit(1),e.accessToken=w.accessToken}e.accessToken||(r.default.error(t.default.red("No authorization token found!")),process.exit(1));const o=new $.Project({baseUrl:e.baseUrl,accessToken:e.accessToken});let c=await C({project:o});c||(c=await S({project:o}),c||process.exit(1)),e.projectId=c.id,await V({project:o,projectId:e.projectId}),c=await J({project:o,selectedProject:c,projectDirectory:e.project});const{selectedProject:u,androidProjectInfo:s}=await Q({project:o,selectedProject:c,projectDirectory:e.project});c=u,s&&(c=await te({project:o,selectedProject:c,projectDirectory:e.project,androidProjectInfo:s})),await(0,T.configureDependencies)({directory:e.project});const{module:p,migrations:l,schema:d}=await X(e),j=await ee({project:o,projectId:e.projectId});e.module=m.default.resolve(e.project,p),e.migrations=m.default.resolve(e.project,l),e.schema=m.default.resolve(e.project,d),e.bindings=j,await(0,I.writeConfig)(e),re(e.project)});var oe=ie;