├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .magicspace └── boilerplate.json ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── example-scripts ├── echo-message.js ├── generate-resources.sh ├── makescript.json ├── operate-sqlite.sql ├── post-trigger-hook-for-a-single-script.sh └── post-trigger-hook.sh ├── images ├── agent-initialize.png ├── ger-started-running-result.png ├── get-started-initialization.png ├── get-started-makescript.png ├── makescript-agents-management-with-join-link-notation.png ├── makescript-default-agent-prompts.png ├── makescript-dir-prompts.png ├── makescript-home-with-agents-management-notation.png ├── makescript-home.png ├── makescript-initialize.png └── makescript-scripts-repo-url-prompts.png ├── inplate.config.js ├── lerna.json ├── makescript.js ├── package.json ├── packages ├── makescript-agent │ ├── package.json │ └── src │ │ └── program │ │ ├── @adapters │ │ ├── adapter.ts │ │ ├── index.ts │ │ ├── node-adapter.ts │ │ ├── process-adapter.ts │ │ ├── shell-adapter.ts │ │ └── sqlite-adapter.ts │ │ ├── @cli.ts │ │ ├── @commands │ │ └── default.ts │ │ ├── @entrances.ts │ │ ├── @services │ │ ├── index.ts │ │ ├── rpc-service.ts │ │ ├── running-service.ts │ │ ├── script-service.ts │ │ └── socket-service.ts │ │ ├── @utils │ │ ├── archiver.ts │ │ └── index.ts │ │ ├── config.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── main.ts │ │ ├── shared │ │ ├── index.ts │ │ ├── logger.ts │ │ └── rpc.ts │ │ ├── tsconfig.json │ │ └── types │ │ ├── index.ts │ │ ├── resource.ts │ │ ├── rpc.ts │ │ ├── running.ts │ │ └── script-definition.ts └── makescript │ ├── package.json │ └── src │ ├── program │ ├── @api │ │ ├── @external │ │ │ ├── @auth.ts │ │ │ ├── @makeflow.http │ │ │ ├── @makeflow.ts │ │ │ ├── @running.http │ │ │ ├── @running.ts │ │ │ ├── external.ts │ │ │ └── index.ts │ │ ├── @web │ │ │ ├── @auth.ts │ │ │ ├── @authorization.ts │ │ │ ├── @makeflow.ts │ │ │ ├── @resources.ts │ │ │ ├── @scripts.ts │ │ │ ├── @tokens.ts │ │ │ ├── index.ts │ │ │ └── web.ts │ │ ├── api.ts │ │ └── index.ts │ ├── @cli.ts │ ├── @commands │ │ ├── check-definition.ts │ │ ├── default.ts │ │ └── generate-hash.ts │ ├── @core │ │ ├── error.ts │ │ ├── index.ts │ │ └── models │ │ │ ├── index.ts │ │ │ ├── makeflow.ts │ │ │ ├── model.ts │ │ │ ├── running-record.ts │ │ │ └── token.ts │ ├── @entrances.ts │ ├── @services │ │ ├── agent-service.ts │ │ ├── app-service.ts │ │ ├── db-service.ts │ │ ├── index.ts │ │ ├── makeflow-service.ts │ │ ├── running-service.ts │ │ ├── socket-service.ts │ │ └── token-service.ts │ ├── @utils │ │ ├── hash.ts │ │ ├── index.ts │ │ ├── resource.ts │ │ └── spawn.ts │ ├── config.ts │ ├── main.ts │ ├── tsconfig.json │ └── types │ │ ├── index.ts │ │ ├── makeflow.ts │ │ ├── running-record.ts │ │ └── token.ts │ └── web │ ├── .browserslistrc │ ├── .eslintrc.js │ ├── @assets │ └── favicon.png │ ├── @components │ ├── button │ │ ├── button.tsx │ │ └── index.ts │ ├── card │ │ ├── card.tsx │ │ └── index.ts │ └── index.ts │ ├── @constants.ts │ ├── @core │ ├── error.ts │ └── index.ts │ ├── @entrances.ts │ ├── @helpers │ ├── fetch.ts │ └── index.ts │ ├── @routes │ ├── index.ts │ └── routes.ts │ ├── @services │ ├── authorization-service.ts │ ├── index.ts │ ├── makeflow-service.ts │ ├── script-service.ts │ └── token-service.ts │ ├── @third-part.ts │ ├── @views │ ├── @home │ │ ├── home-view.tsx │ │ └── index.ts │ ├── @initialize │ │ ├── index.ts │ │ └── initialize-view.tsx │ ├── @login │ │ ├── index.ts │ │ └── login-view.tsx │ ├── @makeflow │ │ ├── @candidates-modal.tsx │ │ ├── index.ts │ │ ├── makeflow-login-view.tsx │ │ └── makeflow-view.tsx │ ├── @scripts │ │ ├── @common.tsx │ │ ├── @output-panel.tsx │ │ ├── @running-record-viewer-view.tsx │ │ ├── @script-definition-viewer.tsx │ │ ├── index.ts │ │ ├── running-records-view.tsx │ │ └── scripts-management-view.tsx │ ├── @status │ │ ├── index.ts │ │ └── status-view.tsx │ ├── @tokens │ │ ├── @generate-modal.tsx │ │ ├── index.ts │ │ └── tokens-view.tsx │ └── app.tsx │ ├── index.html │ ├── main.css │ ├── main.tsx │ ├── modules.d.ts │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["bld", ".bld-cache"], 4 | "extends": ["eslint:recommended"], 5 | "env": { 6 | "node": true, 7 | "es2020": true 8 | }, 9 | "overrides": [ 10 | { 11 | "files": ["**/*.{ts,tsx}"], 12 | "extends": ["plugin:@mufan/default"], 13 | "parserOptions": { 14 | "project": "!(node_modules)/**/tsconfig.json" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [12.x, 14.x] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js ${{matrix.node-version}} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{matrix.node-version}} 19 | - run: npm install --global yarn 20 | - run: yarn install 21 | - run: yarn test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | *.tgz 4 | node_modules/ 5 | yarn-error.log 6 | npm-debug.log 7 | # TypeScript Build Artifacts 8 | bld/ 9 | .bld-cache/ 10 | .cache/ 11 | # Test 12 | /test.* 13 | -------------------------------------------------------------------------------- /.magicspace/boilerplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@mufan/code-boilerplates/typescript-entrances", 3 | "options": { 4 | "name": "makescript", 5 | "license": "MIT", 6 | "author": "Chengdu Mufan Technology Co., Ltd.", 7 | "packages": [ 8 | { 9 | "name": "@makeflow/makescript-agent", 10 | "tsProjects": [ 11 | { 12 | "name": "program" 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "@makeflow/makescript", 18 | "tsProjects": [ 19 | { 20 | "name": "program" 21 | }, 22 | { 23 | "name": "web" 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # TypeScript Build Artifacts 2 | bld/ 3 | .bld-cache/ 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "all", 10 | "bracketSpacing": false, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "avoid" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.insertSpaces": true, 4 | "editor.formatOnSave": true, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | }, 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", 9 | "editor.rulers": [80], 10 | "files.eol": "\n", 11 | "files.insertFinalNewline": true, 12 | "files.trimTrailingWhitespace": true, 13 | "typescript.tsdk": "node_modules/typescript/lib", 14 | "eslint.enable": true, 15 | "eslint.validate": [ 16 | "javascript", 17 | "javascriptreact", 18 | "typescript", 19 | "typescriptreact" 20 | ], 21 | "cSpell.words": [ 22 | "Castable", 23 | "IRPC", 24 | "Makefow", 25 | "SHOWABLE", 26 | "antd", 27 | "authed", 28 | "doctoc", 29 | "inplate", 30 | "makescript", 31 | "tiva", 32 | "uuidv" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Chengdu Mufan Technology Co., Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /example-scripts/echo-message.js: -------------------------------------------------------------------------------- 1 | console.log( 2 | `This text will be cleared\x1Bc${process.env['message'] || 'No message!'}`, 3 | ); 4 | -------------------------------------------------------------------------------- /example-scripts/generate-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p $RESOURCE_PATH 4 | 5 | EXPIRES_AT=$(($(date +"%s"000) + 60000)) 6 | 7 | echo "$text" > $RESOURCE_PATH/index.html 8 | echo "{\"expiresAt\": $EXPIRES_AT}" > $RESOURCE_PATH/config.json 9 | 10 | echo "Page generated. Please click here to check message." 11 | -------------------------------------------------------------------------------- /example-scripts/makescript.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": [ 3 | { 4 | "displayName": "Echo Message", 5 | "name": "echo-message", 6 | "type": "node", 7 | "module": "echo-message.js", 8 | "parameters": {"message": true}, 9 | "manual": false 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /example-scripts/operate-sqlite.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO test_items (message) VALUES ($message); 2 | -------------------------------------------------------------------------------- /example-scripts/post-trigger-hook-for-a-single-script.sh: -------------------------------------------------------------------------------- 1 | # You can process some thing here when a single script has been triggered. 2 | 3 | echo "A single script triggered" 4 | -------------------------------------------------------------------------------- /example-scripts/post-trigger-hook.sh: -------------------------------------------------------------------------------- 1 | # You can proccess something here when any script has been triggered 2 | 3 | echo "Post trigger" 4 | -------------------------------------------------------------------------------- /images/agent-initialize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/agent-initialize.png -------------------------------------------------------------------------------- /images/ger-started-running-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/ger-started-running-result.png -------------------------------------------------------------------------------- /images/get-started-initialization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/get-started-initialization.png -------------------------------------------------------------------------------- /images/get-started-makescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/get-started-makescript.png -------------------------------------------------------------------------------- /images/makescript-agents-management-with-join-link-notation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/makescript-agents-management-with-join-link-notation.png -------------------------------------------------------------------------------- /images/makescript-default-agent-prompts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/makescript-default-agent-prompts.png -------------------------------------------------------------------------------- /images/makescript-dir-prompts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/makescript-dir-prompts.png -------------------------------------------------------------------------------- /images/makescript-home-with-agents-management-notation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/makescript-home-with-agents-management-notation.png -------------------------------------------------------------------------------- /images/makescript-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/makescript-home.png -------------------------------------------------------------------------------- /images/makescript-initialize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/makescript-initialize.png -------------------------------------------------------------------------------- /images/makescript-scripts-repo-url-prompts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/images/makescript-scripts-repo-url-prompts.png -------------------------------------------------------------------------------- /inplate.config.js: -------------------------------------------------------------------------------- 1 | const {Project} = require('ts-morph'); 2 | 3 | const MAKESCRIPT_CONFIG_FILE_PATH = 'packages/makescript/src/program/config.ts'; 4 | const MAKESCRIPT_CONFIG_INTERFACE_NAME = 'JSONConfigFile'; 5 | 6 | const MAKESCRIPT_AGENT_CONFIG_PATH = 7 | 'packages/makescript-agent/src/program/config.ts'; 8 | const MAKESCRIPT_AGENT_CONFIG_INTERFACE_NAME = 'JSONConfigFile'; 9 | 10 | module.exports = { 11 | 'README.md': { 12 | data: { 13 | makescriptConfigTypeText: getMakeScriptConfigTypeText( 14 | MAKESCRIPT_CONFIG_FILE_PATH, 15 | MAKESCRIPT_CONFIG_INTERFACE_NAME, 16 | ), 17 | agentConfigTypeText: getMakeScriptConfigTypeText( 18 | MAKESCRIPT_AGENT_CONFIG_PATH, 19 | MAKESCRIPT_AGENT_CONFIG_INTERFACE_NAME, 20 | ), 21 | }, 22 | }, 23 | }; 24 | 25 | function getMakeScriptConfigTypeText(filePath, interfaceName) { 26 | return new Project() 27 | .addSourceFileAtPath(filePath) 28 | .getSourceFile(filePath) 29 | .getInterface(interfaceName) 30 | .print(); 31 | } 32 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.1-alpha.14", 3 | "npmClient": "yarn", 4 | "command": { 5 | "publish": { 6 | "npmClient": "npm" 7 | } 8 | }, 9 | "packages": [ 10 | "packages/*" 11 | ], 12 | "useWorkspaces": true, 13 | "publishConfig": { 14 | "access": "public" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /makescript.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | password: '$2b$10$Qqz.Lqa1WwtvdFXgHM3pAu/sbzwpKo94zUCYwMiipDJZq.67QB/wW', 3 | hooks: { 4 | install: 'echo "This will run while scripts repo cloned."', 5 | postscript: 'sh ./example-scripts/post-trigger-hook.sh', 6 | }, 7 | scripts: [ 8 | { 9 | displayName: 'Echo Message', 10 | name: 'echo-message', 11 | type: 'node', 12 | module: 'example-scripts/echo-message.js', 13 | parameters: {message: true}, 14 | manual: false, 15 | }, 16 | { 17 | displayName: 'Echo Message (Shell)', 18 | name: 'echo-message-shell', 19 | type: 'shell', 20 | command: 'echo $message', 21 | parameters: {message: true}, 22 | manual: false, 23 | }, 24 | { 25 | displayName: 'Generate Resources', 26 | name: 'generate-resources', 27 | type: 'process', 28 | command: 'example-scripts/generate-resources.sh', 29 | parameters: { 30 | text: { 31 | displayName: 'Text', 32 | required: true, 33 | }, 34 | }, 35 | manual: true, 36 | }, 37 | { 38 | displayName: 'Operate Sqlite', 39 | name: 'operate-sqlite', 40 | type: 'sqlite', 41 | file: 'example-scripts/operate-sqlite.sql', 42 | parameters: { 43 | message: { 44 | displayName: 'Message', 45 | required: true, 46 | }, 47 | }, 48 | manual: true, 49 | db: { 50 | path: '/tmp/database.sqlite3', 51 | password: process.env['SQLITE_PASSWORD'], 52 | }, 53 | password: '$2b$10$WTept7/7AbSZ4hL0mBo92OK9hn6criSZ9bUjSxI4TyueV8BT3DEJ2', 54 | hooks: { 55 | postscript: 56 | 'sh example-scripts/post-trigger-hook-for-a-single-script.sh', 57 | }, 58 | }, 59 | ], 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "makescript", 3 | "private": true, 4 | "license": "MIT", 5 | "author": "Chengdu Mufan Technology Co., Ltd.", 6 | "scripts": { 7 | "lerna:publish": "yarn build && lerna publish prerelease --registry=https://registry.npmjs.org", 8 | "lerna:publish-only": "lerna publish prerelease --registry=https://registry.npmjs.org", 9 | "start:makescript": "node packages/makescript/bld/program/@cli.js", 10 | "start:agent": "node packages/makescript-agent/bld/program/@cli.js", 11 | "watch:web": "yarn workspace @makeflow/makescript watch:web", 12 | "build": "rimraf packages/*/bld && tsc --build && yarn workspace @makeflow/makescript pack:web", 13 | "lint": "eslint .", 14 | "lint-prettier": "prettier --check .", 15 | "test": "yarn lint-prettier && yarn build && yarn lint", 16 | "doc:update": "doctoc README.md && inplate --update" 17 | }, 18 | "workspaces": [ 19 | "packages/makescript-agent", 20 | "packages/makescript" 21 | ], 22 | "devDependencies": { 23 | "@mufan/code": "^0.2.5", 24 | "@mufan/eslint-plugin": "^0.1.36", 25 | "doctoc": "^2.0.0", 26 | "eslint": "^7.13.0", 27 | "inplate": "^0.1.11", 28 | "lerna": "^3.22.1", 29 | "prettier": "^2.1.2", 30 | "rimraf": "^3.0.2", 31 | "ts-morph": "^9.1.0", 32 | "typescript": "^4.0.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/makescript-agent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@makeflow/makescript-agent", 3 | "version": "0.1.1-alpha.13", 4 | "license": "MIT", 5 | "author": "Chengdu Mufan Technology Co., Ltd.", 6 | "main": "bld/program/index.js", 7 | "types": "bld/program/index.d.ts", 8 | "bin": { 9 | "makescript-agent": "bld/program/@cli.js" 10 | }, 11 | "files": [ 12 | "src/**/*.ts", 13 | "bld" 14 | ], 15 | "scripts": { 16 | "build": "rimraf bld && tsc --project src/program/tsconfig.json" 17 | }, 18 | "dependencies": { 19 | "@makeflow/types": "^0.1.26", 20 | "archiver": "^5.0.2", 21 | "bcrypt": "^5.0.0", 22 | "chalk": "^4.1.0", 23 | "clime": "^0.5.14", 24 | "entrance-decorator": "^0.1.0", 25 | "node-fetch": "^2.6.1", 26 | "npm-which": "^3.0.1", 27 | "prompts": "^2.4.0", 28 | "rimraf": "^3.0.2", 29 | "socket.io-client": "^3.0.3", 30 | "sqlite": "^4.0.15", 31 | "sqlite3": "^5.0.0", 32 | "tiva": "^0.2.2", 33 | "tslang": "^0.1.22", 34 | "tslib": "^2.0.3", 35 | "typescript": "^4.1.3", 36 | "uuid": "^7.0.0", 37 | "villa": "^0.3.2" 38 | }, 39 | "devDependencies": { 40 | "@types/archiver": "^3.1.1", 41 | "@types/bcrypt": "^3.0.0", 42 | "@types/node-fetch": "^2.5.7", 43 | "@types/npm-which": "^3.0.0", 44 | "@types/prompts": "^2.0.9", 45 | "@types/rimraf": "^3.0.0", 46 | "@types/uuid": "^7.0.0" 47 | }, 48 | "publishConfig": { 49 | "access": "public" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@adapters/adapter.ts: -------------------------------------------------------------------------------- 1 | import {ProcedureField} from '@makeflow/types'; 2 | import {Dict} from 'tslang'; 3 | 4 | export interface IAdapter< 5 | TDefinition extends MakeScript.Adapter.AdapterScriptDefinition 6 | > { 7 | readonly type: TDefinition['type']; 8 | 9 | runScript( 10 | argument: AdapterRunScriptArgument, 11 | ): Promise; 12 | } 13 | 14 | export interface AdapterRunScriptArgument< 15 | TDefinition extends MakeScript.Adapter.AdapterScriptDefinition 16 | > { 17 | repoPath: string; 18 | cwd: string; 19 | env: Dict; 20 | definition: TDefinition; 21 | resourcesPath: string; 22 | resourcesBaseURL: string; 23 | parameters: AdapterRunScriptArgumentParameters; 24 | onOutput(output: string): void; 25 | onError(error: string): void; 26 | } 27 | 28 | export type AdapterRunScriptArgumentParameters = Dict; 29 | 30 | export type AdapterRunScriptArgumentOptions = Dict; 31 | 32 | export interface AdapterRunScriptResult { 33 | ok: boolean; 34 | message: string; 35 | } 36 | 37 | export interface IScriptDefinition { 38 | displayName?: string; 39 | name: string; 40 | type: string; 41 | manual?: boolean; 42 | parameters?: { 43 | [name: string]: ScriptDefinitionParameter | true; 44 | }; 45 | password?: string; 46 | hooks?: ScriptDefinitionHooks; 47 | } 48 | 49 | export interface ScriptDefinitionHooks { 50 | postscript?: string; 51 | } 52 | 53 | // Parameters 54 | 55 | export interface ScriptDefinitionParameter { 56 | displayName?: string; 57 | required?: boolean; 58 | field?: 59 | | ProcedureField.BuiltInProcedureFieldType 60 | | ScriptDefinitionDetailedParameterField; 61 | } 62 | 63 | export interface ScriptDefinitionDetailedParameterField { 64 | type: ProcedureField.BuiltInProcedureFieldType; 65 | data?: unknown; 66 | } 67 | 68 | declare global { 69 | namespace MakeScript { 70 | namespace Adapter { 71 | interface AdapterOptionsDict {} 72 | 73 | type AdapterScriptDefinition = IScriptDefinition & 74 | { 75 | [TType in keyof AdapterOptionsDict]: { 76 | type: TType; 77 | } & AdapterOptionsDict[TType]; 78 | }[keyof AdapterOptionsDict]; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './node-adapter'; 2 | export * from './shell-adapter'; 3 | export * from './sqlite-adapter'; 4 | export * from './process-adapter'; 5 | export * from './adapter'; 6 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@adapters/node-adapter.ts: -------------------------------------------------------------------------------- 1 | import * as CP from 'child_process'; 2 | 3 | import * as villa from 'villa'; 4 | 5 | import { 6 | AdapterRunScriptArgument, 7 | AdapterRunScriptResult, 8 | IAdapter, 9 | } from '../types'; 10 | 11 | declare global { 12 | namespace MakeScript { 13 | namespace Adapter { 14 | interface AdapterOptionsDict { 15 | node: { 16 | module: string; 17 | }; 18 | } 19 | } 20 | } 21 | } 22 | 23 | export class NodeAdapter 24 | implements 25 | IAdapter< 26 | Extract 27 | > { 28 | type = 'node' as const; 29 | 30 | async runScript({ 31 | cwd, 32 | env, 33 | definition, 34 | parameters, 35 | resourcesPath: resourcePath, 36 | resourcesBaseURL: resourceBaseURL, 37 | onOutput, 38 | onError, 39 | }: AdapterRunScriptArgument< 40 | Extract 41 | >): Promise { 42 | try { 43 | let cp = CP.spawn(`node`, [definition.module], { 44 | cwd, 45 | env: { 46 | ...process.env, 47 | ...env, 48 | RESOURCE_PATH: resourcePath, 49 | RESOURCE_BASE_URL: resourceBaseURL, 50 | ...parameters, 51 | }, 52 | }); 53 | 54 | cp.stdout.on('data', (buffer: Buffer) => { 55 | onOutput(buffer.toString()); 56 | }); 57 | cp.stderr.on('data', (buffer: Buffer) => { 58 | onError(buffer.toString()); 59 | }); 60 | 61 | await villa.awaitable(cp); 62 | 63 | return {ok: true, message: ''}; 64 | } catch (error) { 65 | return {ok: false, message: error.message ?? String(error)}; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@adapters/process-adapter.ts: -------------------------------------------------------------------------------- 1 | import * as CP from 'child_process'; 2 | 3 | import NPMWhich from 'npm-which'; 4 | import * as villa from 'villa'; 5 | 6 | import { 7 | AdapterRunScriptArgument, 8 | AdapterRunScriptResult, 9 | IAdapter, 10 | } from '../types'; 11 | 12 | declare global { 13 | namespace MakeScript { 14 | namespace Adapter { 15 | interface AdapterOptionsDict { 16 | process: { 17 | command: string; 18 | }; 19 | } 20 | } 21 | } 22 | } 23 | 24 | export class ProcessAdapter 25 | implements 26 | IAdapter< 27 | Extract 28 | > { 29 | type = 'process' as const; 30 | 31 | async runScript({ 32 | cwd, 33 | env, 34 | definition, 35 | parameters, 36 | resourcesPath: resourcePath, 37 | resourcesBaseURL: resourceBaseURL, 38 | onOutput, 39 | onError, 40 | }: AdapterRunScriptArgument< 41 | Extract 42 | >): Promise { 43 | const which = NPMWhich(cwd); 44 | 45 | try { 46 | let commandPath = await villa.call(which, definition.command); 47 | 48 | let cp = CP.spawn(commandPath, { 49 | cwd, 50 | env: { 51 | ...process.env, 52 | ...env, 53 | RESOURCE_PATH: resourcePath, 54 | RESOURCE_BASE_URL: resourceBaseURL, 55 | ...parameters, 56 | }, 57 | }); 58 | 59 | cp.stdout.on('data', (buffer: Buffer) => { 60 | onOutput(buffer.toString()); 61 | }); 62 | cp.stderr.on('data', (buffer: Buffer) => { 63 | onError(buffer.toString()); 64 | }); 65 | 66 | await villa.awaitable(cp); 67 | 68 | return {ok: true, message: ''}; 69 | } catch (error) { 70 | return {ok: false, message: error.message ?? String(error)}; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@adapters/shell-adapter.ts: -------------------------------------------------------------------------------- 1 | import * as CP from 'child_process'; 2 | 3 | import * as villa from 'villa'; 4 | 5 | import { 6 | AdapterRunScriptArgument, 7 | AdapterRunScriptResult, 8 | IAdapter, 9 | } from '../types'; 10 | 11 | declare global { 12 | namespace MakeScript { 13 | namespace Adapter { 14 | interface AdapterOptionsDict { 15 | shell: { 16 | command: string; 17 | }; 18 | } 19 | } 20 | } 21 | } 22 | 23 | export class ShellAdapter 24 | implements 25 | IAdapter< 26 | Extract 27 | > { 28 | type = 'shell' as const; 29 | 30 | async runScript({ 31 | cwd, 32 | env, 33 | definition, 34 | parameters, 35 | resourcesPath: resourcePath, 36 | resourcesBaseURL: resourceBaseURL, 37 | onOutput, 38 | onError, 39 | }: AdapterRunScriptArgument< 40 | Extract 41 | >): Promise { 42 | try { 43 | let cp = CP.exec(definition.command, { 44 | cwd, 45 | env: { 46 | ...process.env, 47 | ...env, 48 | RESOURCE_PATH: resourcePath, 49 | RESOURCE_BASE_URL: resourceBaseURL, 50 | ...parameters, 51 | }, 52 | }); 53 | 54 | cp.stdout?.on('data', (buffer: Buffer) => { 55 | onOutput(buffer.toString()); 56 | }); 57 | cp.stderr?.on('data', (buffer: Buffer) => { 58 | onError(buffer.toString()); 59 | }); 60 | 61 | await villa.awaitable(cp); 62 | 63 | return {ok: true, message: ''}; 64 | } catch (error) { 65 | return {ok: false, message: error.message ?? String(error)}; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@adapters/sqlite-adapter.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | import * as Path from 'path'; 3 | 4 | import sqlite from 'sqlite'; 5 | import sqlite3 from 'sqlite3'; 6 | 7 | import { 8 | AdapterRunScriptArgument, 9 | AdapterRunScriptResult, 10 | IAdapter, 11 | } from '../types'; 12 | 13 | declare global { 14 | namespace MakeScript { 15 | namespace Adapter { 16 | interface AdapterOptionsDict { 17 | sqlite: { 18 | file: string; 19 | db: 20 | | { 21 | path: string; 22 | password?: string; 23 | } 24 | | string; 25 | }; 26 | } 27 | } 28 | } 29 | } 30 | 31 | export class SqliteAdapter 32 | implements 33 | IAdapter< 34 | Extract 35 | > { 36 | type = 'sqlite' as const; 37 | 38 | async runScript({ 39 | cwd, 40 | definition, 41 | parameters, 42 | resourcesPath: resourcePath, 43 | resourcesBaseURL: resourceBaseURL, 44 | onOutput, 45 | }: AdapterRunScriptArgument< 46 | Extract 47 | >): Promise { 48 | try { 49 | let dbPath = 50 | typeof definition.db === 'string' ? definition.db : definition.db.path; 51 | 52 | if (!dbPath) { 53 | return { 54 | ok: false, 55 | message: 'The "db" field is not found in definition.', 56 | }; 57 | } 58 | 59 | let db = await sqlite.open({ 60 | filename: dbPath, 61 | driver: sqlite3.Database, 62 | }); 63 | 64 | let buffer = await FS.promises.readFile(Path.join(cwd, definition.file)); 65 | 66 | let result = await db.run(buffer.toString(), { 67 | $resource_path: resourcePath, 68 | $resource_base_url: resourceBaseURL, 69 | ...Object.fromEntries( 70 | Object.entries(parameters).map(([key, value]) => [`$${key}`, value]), 71 | ), 72 | }); 73 | 74 | onOutput( 75 | `本次执行影响了 ${result.changes} 条数据, 最后一条数据 Id 为 ${result.lastID}`, 76 | ); 77 | 78 | return { 79 | ok: true, 80 | message: '', 81 | }; 82 | } catch (error) { 83 | return {ok: false, message: error.message || String(error)}; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as Path from 'path'; 4 | 5 | import {CLI, Shim} from 'clime'; 6 | 7 | import {logger} from './shared'; 8 | 9 | let cli = new CLI('makescript-agent', Path.join(__dirname, '@commands')); 10 | 11 | let shim = new Shim(cli); 12 | shim.execute(process.argv).catch(logger.error); 13 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@commands/default.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | import * as OS from 'os'; 3 | import * as Path from 'path'; 4 | 5 | import {Castable, Command, Options, command, metadata, option} from 'clime'; 6 | import prompts, {PromptObject} from 'prompts'; 7 | import {Tiva} from 'tiva'; 8 | 9 | import {JSONConfigFile} from '../config'; 10 | import {main} from '../main'; 11 | import {logger} from '../shared'; 12 | 13 | const JSON_CONFIG_INDENTATION = 2; 14 | 15 | const DIR_DEFAULT = Path.resolve(OS.homedir(), '.makescript', 'agent'); 16 | 17 | const CONFIG_FILE_NAME = 'makescript-agent.json'; 18 | 19 | export class CLIOptions extends Options { 20 | @option({ 21 | flag: 's', 22 | description: 'MakeScript server URL with token.', 23 | type: String, 24 | }) 25 | serverURL: string | undefined; 26 | 27 | @option({ 28 | flag: 'd', 29 | description: 30 | 'Directory containing MakeScript node config file and contents.', 31 | default: DIR_DEFAULT, 32 | }) 33 | dir!: Castable.Directory; 34 | } 35 | 36 | @command() 37 | export default class extends Command { 38 | @metadata 39 | async execute({dir, serverURL}: CLIOptions): Promise { 40 | let configFilePath = Path.join(dir.fullName, CONFIG_FILE_NAME); 41 | 42 | let configFileExists = FS.existsSync(configFilePath); 43 | 44 | if (!configFileExists) { 45 | let dirname = dir.fullName; 46 | 47 | if (!FS.existsSync(dirname)) { 48 | FS.mkdirSync(dirname, {recursive: true}); 49 | } 50 | 51 | let answer = await initialQuestions(); 52 | 53 | let jsonConfig: JSONConfigFile = { 54 | name: answer.name, 55 | server: { 56 | url: answer.serverURL, 57 | }, 58 | scripts: { 59 | git: answer.repoURL, 60 | // if subPath is "", pass undefined instead. 61 | dir: answer.subPath || undefined, 62 | }, 63 | proxy: undefined, 64 | }; 65 | 66 | writeConfig(jsonConfig); 67 | } 68 | 69 | let jsonConfig = readConfig(); 70 | 71 | if (serverURL && jsonConfig.server.url !== serverURL) { 72 | jsonConfig.server.url = serverURL; 73 | 74 | writeConfig(jsonConfig); 75 | } 76 | 77 | let tiva = new Tiva({ 78 | project: Path.join(__dirname, '../../../src/program'), 79 | }); 80 | 81 | logger.info('Checking config file ...'); 82 | 83 | try { 84 | await tiva.validate( 85 | { 86 | module: './config', 87 | type: 'JSONConfigFile', 88 | }, 89 | jsonConfig, 90 | ); 91 | 92 | await main(tiva, { 93 | ...jsonConfig, 94 | workspace: dir.fullName, 95 | agentModule: undefined, 96 | }); 97 | } catch (error) { 98 | if (error.diagnostics) { 99 | logger.error( 100 | `Config file structure does not match:\n${error.diagnostics}`, 101 | ); 102 | } 103 | 104 | throw error; 105 | } 106 | 107 | function writeConfig(config: JSONConfigFile): void { 108 | let jsonConfigText = JSON.stringify( 109 | config, 110 | undefined, 111 | JSON_CONFIG_INDENTATION, 112 | ); 113 | 114 | FS.writeFileSync(configFilePath, jsonConfigText); 115 | } 116 | 117 | function readConfig(): JSONConfigFile { 118 | let jsonConfigText = FS.readFileSync(configFilePath).toString(); 119 | 120 | return JSON.parse(jsonConfigText); 121 | } 122 | 123 | async function initialQuestions(): Promise<{ 124 | serverURL: string; 125 | name: string; 126 | repoURL: string; 127 | subPath: string | undefined; 128 | }> { 129 | let promptObjects: PromptObject[] = []; 130 | 131 | if (!serverURL) { 132 | promptObjects.push({ 133 | type: 'text' as const, 134 | name: 'serverURL', 135 | message: 'Enter MakeScript server URL with token', 136 | validate: value => /^https?:\/\/.+$/.test(value), 137 | }); 138 | } 139 | 140 | promptObjects.push( 141 | ...([ 142 | { 143 | type: 'text', 144 | name: 'name', 145 | message: 'Enter the name to register as', 146 | }, 147 | { 148 | type: 'text', 149 | name: 'repoURL', 150 | message: 'Enter the scripts repo url', 151 | validate: value => /^(https?:\/\/.+)|(\w+\.git)$/.test(value), 152 | }, 153 | { 154 | type: 'text', 155 | name: 'subPath', 156 | message: 'Enter the scripts definition dir path', 157 | }, 158 | ] as PromptObject[]), 159 | ); 160 | 161 | let answer = await prompts(promptObjects); 162 | 163 | // There is a bug (or unhandled behavior) with 'prompts'. 164 | // When user press CTRL + C , program will continue to execute with empty answers. 165 | // https://github.com/terkelg/prompts/issues/252 166 | if ( 167 | (!serverURL && !answer.serverURL) || 168 | !answer.name || 169 | !answer.repoURL 170 | ) { 171 | process.exit(0); 172 | } 173 | 174 | return { 175 | serverURL: serverURL ?? answer.serverURL, 176 | name: answer.name, 177 | repoURL: answer.repoURL, 178 | subPath: answer.subPath ?? undefined, 179 | }; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@entrances.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @mufan/explicit-return-type */ 2 | import entrance from 'entrance-decorator'; 3 | import {Tiva} from 'tiva'; 4 | 5 | import { 6 | RPCService, 7 | RunningService, 8 | ScriptService, 9 | SocketService, 10 | } from './@services'; 11 | import {Config} from './config'; 12 | 13 | export class Entrances { 14 | readonly ready = Promise.all([this.scriptService.ready]); 15 | 16 | constructor(private tiva: Tiva, private config: Config) { 17 | this.rpcService.up(); 18 | } 19 | 20 | @entrance 21 | get socketService() { 22 | return new SocketService(this.config, this.ready); 23 | } 24 | 25 | @entrance 26 | get scriptService() { 27 | return new ScriptService(this.tiva, this.config); 28 | } 29 | 30 | @entrance 31 | get runningService() { 32 | return new RunningService(this.scriptService, this.socketService); 33 | } 34 | 35 | @entrance 36 | get rpcService() { 37 | return new RPCService( 38 | this.runningService, 39 | this.scriptService, 40 | this.socketService, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './running-service'; 2 | export * from './script-service'; 3 | export * from './rpc-service'; 4 | export * from './socket-service'; 5 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@services/rpc-service.ts: -------------------------------------------------------------------------------- 1 | import {bridgeRPC, logger} from '../shared'; 2 | import { 3 | BriefScriptDefinition, 4 | MakescriptAgentRPC, 5 | MakescriptAgentRPCRunScriptOptions, 6 | ScriptDefinitionHooks, 7 | ScriptRunningResult, 8 | } from '../types'; 9 | 10 | import {RunningService} from './running-service'; 11 | import {ScriptService} from './script-service'; 12 | import {SocketService} from './socket-service'; 13 | 14 | export class RPCService implements MakescriptAgentRPC { 15 | constructor( 16 | private runningService: RunningService, 17 | private scriptService: ScriptService, 18 | private socketService: SocketService, 19 | ) {} 20 | 21 | up(): void { 22 | bridgeRPC(this, this.socketService.socket, logger); 23 | } 24 | 25 | async syncScripts(): Promise { 26 | await this.scriptService.syncScripts(); 27 | } 28 | 29 | async getScripts(): Promise { 30 | return this.scriptService.briefScriptDefinitions; 31 | } 32 | 33 | async runScript({ 34 | id, 35 | name, 36 | parameters, 37 | resourcesBaseURL, 38 | password, 39 | }: MakescriptAgentRPCRunScriptOptions): Promise { 40 | return this.runningService.runScript({ 41 | id, 42 | name, 43 | parameters, 44 | resourcesBaseURL, 45 | password, 46 | }); 47 | } 48 | 49 | async triggerHook( 50 | scriptName: string, 51 | hookName: keyof ScriptDefinitionHooks, 52 | ): Promise { 53 | await this.runningService.triggerHook(scriptName, hookName); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@services/running-service.ts: -------------------------------------------------------------------------------- 1 | import * as CP from 'child_process'; 2 | import * as FS from 'fs'; 3 | import * as OS from 'os'; 4 | import * as Path from 'path'; 5 | 6 | import Bcrypt from 'bcrypt'; 7 | import rimraf from 'rimraf'; 8 | import {Dict} from 'tslang'; 9 | import {v4 as uuidv4} from 'uuid'; 10 | import * as villa from 'villa'; 11 | 12 | import {zip} from '../@utils'; 13 | import {OUTPUT_CLEAR_CHARACTER} from '../constants'; 14 | import {logger} from '../shared'; 15 | import { 16 | AdapterRunScriptArgument, 17 | IAdapter, 18 | ScriptDefinitionHooks, 19 | ScriptRunningArgument, 20 | ScriptRunningResult, 21 | } from '../types'; 22 | 23 | import {ScriptService} from './script-service'; 24 | import {SocketService} from './socket-service'; 25 | 26 | const MAKESCRIPT_TMPDIR = Path.join(OS.tmpdir(), 'makescript-temp'); 27 | 28 | const OUTPUT_FLUSH_INTERVAL_TIME = 1000; 29 | 30 | export class RunningService { 31 | private adapterMap = new Map< 32 | string, 33 | IAdapter 34 | >(); 35 | 36 | constructor( 37 | private scriptService: ScriptService, 38 | private socketService: SocketService, 39 | ) {} 40 | 41 | async triggerHook( 42 | scriptName: string, 43 | hookName: keyof ScriptDefinitionHooks, 44 | ): Promise { 45 | let definition = this.requireScriptDefinition(scriptName); 46 | 47 | let hookContent = definition.hooks?.[hookName]; 48 | 49 | if (!hookContent) { 50 | throw new Error( 51 | `The hook "${hookName}" of script "${scriptName}" is not configured.`, 52 | ); 53 | } 54 | 55 | let cp = CP.exec(hookContent, { 56 | cwd: this.scriptService.scriptsBasePath, 57 | env: { 58 | ...process.env, 59 | ...this.scriptService.getEnvByScriptName(scriptName), 60 | }, 61 | }); 62 | 63 | await villa.awaitable(cp); 64 | } 65 | 66 | async runScript( 67 | argument: ScriptRunningArgument, 68 | ): Promise { 69 | let {name, parameters, resourcesBaseURL} = argument; 70 | 71 | logger.info(`Running record "${argument.id}" of script "${argument.name}"`); 72 | 73 | let definition = this.requireScriptDefinition(name); 74 | 75 | await this.validatePassword(definition, argument); 76 | 77 | let adapter = this.requireAdapter(definition); 78 | let resourcesPath = this.generateRandomResourcesPath(); 79 | 80 | let [allowedParameters, deniedParameters] = validateParameters( 81 | parameters, 82 | definition, 83 | ); 84 | 85 | let {onOutput, done: onOutputDone} = this.getOnOutput(argument, false); 86 | let {onOutput: onError, done: onErrorDone} = this.getOnOutput( 87 | argument, 88 | true, 89 | ); 90 | 91 | let result = await adapter.runScript({ 92 | repoPath: this.scriptService.scriptsBasePath, 93 | cwd: this.scriptService.scriptsPath, 94 | env: this.scriptService.getEnvByScriptName(name), 95 | definition, 96 | parameters: allowedParameters, 97 | resourcesPath, 98 | resourcesBaseURL, 99 | onOutput, 100 | onError, 101 | }); 102 | 103 | let output = await onOutputDone(); 104 | let error = await onErrorDone(); 105 | 106 | await this.handleResources(argument, resourcesPath); 107 | 108 | logger.info( 109 | `Complete running record "${argument.id}" of script "${argument.name}"`, 110 | ); 111 | 112 | return { 113 | name, 114 | parameters, 115 | deniedParameters, 116 | result, 117 | output: { 118 | output, 119 | error, 120 | }, 121 | }; 122 | } 123 | 124 | registerAdapter(type: string, adapter: IAdapter): void { 125 | this.adapterMap.set(type, adapter); 126 | } 127 | 128 | private requireScriptDefinition( 129 | name: string, 130 | ): MakeScript.Adapter.AdapterScriptDefinition { 131 | let scriptDefinition = this.scriptService.getDefaultValueFilledScriptDefinitionByName( 132 | name, 133 | ); 134 | 135 | if (!scriptDefinition) { 136 | throw new Error(`Script definition "${name}" not found`); 137 | } 138 | 139 | return scriptDefinition; 140 | } 141 | 142 | private async validatePassword( 143 | scriptDefinition: MakeScript.Adapter.AdapterScriptDefinition, 144 | {password}: ScriptRunningArgument, 145 | ): Promise { 146 | if (!scriptDefinition.password) { 147 | return; 148 | } 149 | 150 | let result = await Bcrypt.compare(password, scriptDefinition.password); 151 | 152 | if (!result) { 153 | throw new Error( 154 | `Password error: the password provided to running "${scriptDefinition.name}" is not match`, 155 | ); 156 | } 157 | } 158 | 159 | private requireAdapter< 160 | TDefinition extends MakeScript.Adapter.AdapterScriptDefinition 161 | >(scriptDefinition: TDefinition): IAdapter { 162 | let adapter = this.adapterMap.get(scriptDefinition.type); 163 | 164 | if (!adapter) { 165 | // TODO: 166 | throw Error( 167 | `Adapter for script type "${scriptDefinition.type}" not found`, 168 | ); 169 | } 170 | 171 | return adapter as IAdapter; 172 | } 173 | 174 | private generateRandomResourcesPath(): string { 175 | return Path.join(OS.tmpdir(), 'makescript', 'agent', 'resources', uuidv4()); 176 | } 177 | 178 | private async handleResources( 179 | argument: ScriptRunningArgument, 180 | resourcesPath: string, 181 | ): Promise { 182 | if (!FS.existsSync(resourcesPath)) { 183 | return; 184 | } 185 | 186 | logger.info( 187 | `Transmitting resources for record "${argument.id}" of script "${argument.name}"`, 188 | ); 189 | 190 | let temporaryArchiveFilePath = Path.join( 191 | MAKESCRIPT_TMPDIR, 192 | `${uuidv4()}.zip`, 193 | ); 194 | 195 | if (!FS.existsSync(MAKESCRIPT_TMPDIR)) { 196 | FS.mkdirSync(MAKESCRIPT_TMPDIR); 197 | } 198 | 199 | await zip(resourcesPath, temporaryArchiveFilePath); 200 | 201 | let buffer = await villa.async(FS.readFile)(temporaryArchiveFilePath); 202 | 203 | await this.socketService.makescriptRPC.updateResources(argument.id, buffer); 204 | 205 | await villa.async(rimraf)(temporaryArchiveFilePath); 206 | } 207 | 208 | private getOnOutput( 209 | argument: ScriptRunningArgument, 210 | error: boolean, 211 | ): { 212 | onOutput: AdapterRunScriptArgument< 213 | MakeScript.Adapter.AdapterScriptDefinition 214 | >['onOutput']; 215 | done(): Promise; 216 | } { 217 | let output = ''; 218 | 219 | let lastFlushedText: string | undefined; 220 | 221 | let flushOutput = (): void => { 222 | if (error) { 223 | return; 224 | } 225 | 226 | let textToFlush = getTextToFlush(output); 227 | 228 | if (!textToFlush || textToFlush === lastFlushedText) { 229 | return; 230 | } 231 | 232 | // TODO: Ensure sequence in time 233 | this.socketService.makescriptRPC 234 | .updateOutput(argument.id, textToFlush) 235 | .catch(logger.error); 236 | }; 237 | 238 | let timer = setInterval(() => { 239 | flushOutput(); 240 | }, OUTPUT_FLUSH_INTERVAL_TIME); 241 | 242 | return { 243 | onOutput: data => { 244 | output += data; 245 | }, 246 | done: async () => { 247 | clearInterval(timer); 248 | 249 | flushOutput(); 250 | 251 | return output; 252 | }, 253 | }; 254 | } 255 | } 256 | 257 | function validateParameters( 258 | parameters: Dict, 259 | definition: MakeScript.Adapter.AdapterScriptDefinition, 260 | ): [Dict, Dict] { 261 | if (!definition.parameters) { 262 | return [{}, {}]; 263 | } 264 | 265 | let {filteredParameters, missingParameters} = Object.entries( 266 | definition.parameters, 267 | ).reduce<{ 268 | filteredParameters: Dict; 269 | missingParameters: string[]; 270 | }>( 271 | (reducer, [parameterName, parameterDefinition]) => { 272 | let {filteredParameters, missingParameters} = reducer; 273 | 274 | let parameterRequired = 275 | typeof parameterDefinition === 'object' 276 | ? parameterDefinition.required 277 | : false; 278 | 279 | let parameterValue = parameters[parameterName]; 280 | 281 | let serializedValue = 282 | typeof parameterValue === 'object' 283 | ? JSON.stringify(parameterValue) 284 | : parameterValue; 285 | 286 | if (serializedValue !== undefined) { 287 | filteredParameters[parameterName] = serializedValue; 288 | } else { 289 | if (parameterRequired) { 290 | missingParameters.push(parameterName); 291 | } 292 | } 293 | 294 | return reducer; 295 | }, 296 | {filteredParameters: {}, missingParameters: []}, 297 | ); 298 | 299 | if (missingParameters.length) { 300 | throw new Error( 301 | `Missing command required parameters "${missingParameters.join('", "')}"`, 302 | ); 303 | } 304 | 305 | let deniedParameters = Object.fromEntries( 306 | Object.entries(parameters).filter(([key]) => !(key in filteredParameters)), 307 | ); 308 | 309 | return [filteredParameters, deniedParameters]; 310 | } 311 | 312 | function getTextToFlush(text: string): string | undefined { 313 | if (!text.includes(OUTPUT_CLEAR_CHARACTER)) { 314 | return text; 315 | } 316 | 317 | let splitTexts = text.split(OUTPUT_CLEAR_CHARACTER); 318 | 319 | return splitTexts[splitTexts.length - 1]; 320 | } 321 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@services/script-service.ts: -------------------------------------------------------------------------------- 1 | import * as CP from 'child_process'; 2 | import * as FS from 'fs'; 3 | import * as Path from 'path'; 4 | 5 | import rimraf from 'rimraf'; 6 | import {Tiva} from 'tiva'; 7 | import {Dict} from 'tslang'; 8 | import * as villa from 'villa'; 9 | 10 | import {Config} from '../config'; 11 | import {logger} from '../shared'; 12 | import {BriefScriptDefinition, ScriptsDefinition} from '../types'; 13 | 14 | const SCRIPTS_DIRECTORY_NAME = 'scripts'; 15 | const SCRIPTS_CONFIG_FILE_NAME_JSON = 'makescript.json'; 16 | const SCRIPTS_CONFIG_FILE_NAME_JS = 'makescript.js'; 17 | 18 | const AGENT_MODULE_DEFAULT = './types'; 19 | 20 | export class ScriptService { 21 | readonly ready: Promise; 22 | 23 | private scriptsDefinition: ScriptsDefinition | undefined; 24 | 25 | get scriptsBasePath(): string { 26 | return Path.join(this.config.workspace, SCRIPTS_DIRECTORY_NAME); 27 | } 28 | 29 | get scriptsPath(): string { 30 | let scriptsBasePath = this.scriptsBasePath; 31 | let scriptsSubPath = this.config.scripts.dir; 32 | 33 | return scriptsSubPath 34 | ? Path.join(scriptsBasePath, scriptsSubPath) 35 | : scriptsBasePath; 36 | } 37 | 38 | get briefScriptDefinitions(): BriefScriptDefinition[] { 39 | let scriptsDefinition = this.scriptsDefinition; 40 | 41 | if (!scriptsDefinition) { 42 | return []; 43 | } 44 | 45 | return scriptsDefinition.scripts.map(scriptDefinition => 46 | convertScriptDefinitionToBriefScriptDefinition( 47 | fillScriptDefinitionDefaultValue(scriptDefinition, scriptsDefinition!), 48 | ), 49 | ); 50 | } 51 | 52 | constructor(private tiva: Tiva, private config: Config) { 53 | this.ready = this.initialize(); 54 | } 55 | 56 | async syncScripts(): Promise { 57 | logger.info('Syncing scripts ...'); 58 | 59 | try { 60 | if (FS.existsSync(this.scriptsBasePath)) { 61 | let cp = CP.spawn('git', ['remote', 'get-url', 'origin'], { 62 | cwd: this.scriptsBasePath, 63 | }); 64 | 65 | let remoteURL = ''; 66 | 67 | cp.stdout.on('data', (buffer: Buffer) => { 68 | remoteURL += buffer.toString(); 69 | }); 70 | 71 | await villa.awaitable(cp); 72 | 73 | if (remoteURL.trim() !== this.config.scripts.git.trim()) { 74 | logger.info( 75 | 'Scripts repo url changed, start to sync from the new url', 76 | ); 77 | 78 | await villa.call(rimraf, this.scriptsBasePath); 79 | 80 | await villa.awaitable( 81 | CP.spawn('git', [ 82 | 'clone', 83 | this.config.scripts.git, 84 | this.scriptsBasePath, 85 | ]), 86 | ); 87 | } else { 88 | await villa.awaitable( 89 | CP.spawn('git', ['pull'], { 90 | cwd: this.scriptsBasePath, 91 | }), 92 | ); 93 | } 94 | } else { 95 | await villa.awaitable( 96 | CP.spawn('git', [ 97 | 'clone', 98 | this.config.scripts.git, 99 | this.scriptsBasePath, 100 | ]), 101 | ); 102 | } 103 | } catch (error) { 104 | throw new Error(`Failed to sync scripts: ${error.message}`); 105 | } 106 | 107 | let scriptsDefinition = await this.parseScriptsDefinition(); 108 | 109 | this.scriptsDefinition = scriptsDefinition; 110 | 111 | if (scriptsDefinition.hooks?.install) { 112 | try { 113 | logger.info('Initializing scripts ...'); 114 | 115 | await villa.awaitable( 116 | CP.exec(scriptsDefinition.hooks.install, { 117 | cwd: this.scriptsBasePath, 118 | }), 119 | ); 120 | } catch (error) { 121 | throw new Error( 122 | `Cannot to initial script repo with \`${scriptsDefinition.hooks.install}\`: ${error.message}`, 123 | ); 124 | } 125 | } 126 | 127 | logger.info('Scripts initialized'); 128 | } 129 | 130 | getDefaultValueFilledScriptDefinitionByName( 131 | name: string, 132 | ): MakeScript.Adapter.AdapterScriptDefinition | undefined { 133 | let scriptsDefinition = this.scriptsDefinition; 134 | 135 | if (!scriptsDefinition) { 136 | return; 137 | } 138 | 139 | let definition = scriptsDefinition.scripts.find( 140 | definition => definition.name === name, 141 | ); 142 | 143 | if (!definition) { 144 | return undefined; 145 | } 146 | 147 | return fillScriptDefinitionDefaultValue(definition, scriptsDefinition); 148 | } 149 | 150 | getEnvByScriptName(scriptName: string): Dict { 151 | return { 152 | SCRIPT_NAME: scriptName, 153 | NAMESPACE: this.config.name, 154 | }; 155 | } 156 | 157 | private async initialize(): Promise { 158 | await this.syncScripts(); 159 | } 160 | 161 | private async parseScriptsDefinition(): Promise { 162 | let scriptsPath = this.scriptsPath; 163 | 164 | if (!FS.existsSync(this.scriptsBasePath)) { 165 | throw new Error(`Scripts repo not cloned`); 166 | } 167 | 168 | let jsonScriptsDefinitionPath = Path.join( 169 | scriptsPath, 170 | SCRIPTS_CONFIG_FILE_NAME_JSON, 171 | ); 172 | let jsScriptsDefinitionPath = Path.join( 173 | scriptsPath, 174 | SCRIPTS_CONFIG_FILE_NAME_JS, 175 | ); 176 | 177 | let existingScriptsDefinitionPath: string | undefined; 178 | 179 | if (FS.existsSync(jsScriptsDefinitionPath)) { 180 | existingScriptsDefinitionPath = jsScriptsDefinitionPath; 181 | } else if (FS.existsSync(jsonScriptsDefinitionPath)) { 182 | existingScriptsDefinitionPath = jsonScriptsDefinitionPath; 183 | } 184 | 185 | if (!existingScriptsDefinitionPath) { 186 | throw new Error( 187 | 'Scripts definition not found: \n' + 188 | `please ensure the definition file "${SCRIPTS_CONFIG_FILE_NAME_JSON}" or "${SCRIPTS_CONFIG_FILE_NAME_JS}" is existing in scripts repo.`, 189 | ); 190 | } 191 | 192 | logger.info('Checking scripts definition file ...'); 193 | 194 | let {default: definition} = await import(existingScriptsDefinitionPath); 195 | 196 | try { 197 | await this.tiva.validate( 198 | { 199 | module: this.config.agentModule ?? AGENT_MODULE_DEFAULT, 200 | type: 'ScriptsDefinition', 201 | }, 202 | definition, 203 | ); 204 | } catch (error) { 205 | if (error.diagnostics) { 206 | logger.error( 207 | `The structure of the scripts definition file not matched: \n` + 208 | ` ${error.diagnostics}`, 209 | ); 210 | process.exit(1); 211 | } else { 212 | throw error; 213 | } 214 | } 215 | 216 | logger.info('The scripts definition file is correct'); 217 | 218 | return definition; 219 | } 220 | } 221 | 222 | function fillScriptDefinitionDefaultValue( 223 | definition: MakeScript.Adapter.AdapterScriptDefinition, 224 | scriptsDefinition: ScriptsDefinition, 225 | ): MakeScript.Adapter.AdapterScriptDefinition { 226 | return { 227 | ...definition, 228 | password: definition.password ?? scriptsDefinition.password, 229 | manual: definition.manual === true, 230 | hooks: { 231 | ...scriptsDefinition.hooks, 232 | ...definition.hooks, 233 | }, 234 | }; 235 | } 236 | 237 | function convertScriptDefinitionToBriefScriptDefinition( 238 | definition: MakeScript.Adapter.AdapterScriptDefinition, 239 | ): BriefScriptDefinition { 240 | return { 241 | displayName: definition.displayName ?? definition.name, 242 | name: definition.name, 243 | type: definition.type, 244 | manual: definition.manual === true, 245 | parameters: definition.parameters ?? {}, 246 | needsPassword: !!definition.password, 247 | hooks: { 248 | postscript: !!definition.hooks?.postscript, 249 | }, 250 | }; 251 | } 252 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@services/socket-service.ts: -------------------------------------------------------------------------------- 1 | import URL from 'url'; 2 | 3 | import SocketIO from 'socket.io-client'; 4 | 5 | import {Config} from '../config'; 6 | import {logger, wrapSocketToRPC} from '../shared'; 7 | import {MakescriptRPC} from '../types'; 8 | 9 | export class SocketService { 10 | readonly socket: SocketIO.Socket; 11 | readonly makescriptRPC: MakescriptRPC; 12 | 13 | private registered = false; 14 | 15 | constructor( 16 | private config: Config, 17 | private entrancesReady: Promise, 18 | ) { 19 | let makescriptSecretURL = this.config.server.url; 20 | 21 | let url = URL.parse(makescriptSecretURL); 22 | 23 | this.socket = SocketIO.io(`${url.protocol}//${url.host}`, { 24 | path: url.pathname ?? '/', 25 | }); 26 | 27 | let socket = this.socket; 28 | 29 | this.makescriptRPC = wrapSocketToRPC(socket, logger); 30 | 31 | socket.on('connect', () => { 32 | logger.info(`Connected to ${makescriptSecretURL}`); 33 | 34 | (async () => { 35 | await this.entrancesReady; 36 | 37 | await this.makescriptRPC.register(this.config.name, this.registered); 38 | 39 | this.registered = true; 40 | 41 | logger.info(`Successfully to registered as "${this.config.name}"`); 42 | })().catch(logger.error); 43 | }); 44 | socket.on('disconnect', (reason: string) => 45 | logger.error(`Disconnected from ${makescriptSecretURL}: ${reason}`), 46 | ); 47 | socket.on('connect_error', (error: Error) => 48 | logger.error( 49 | `Failed to connect to ${makescriptSecretURL}: ${error.message}`, 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@utils/archiver.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | 3 | import archiver from 'archiver'; 4 | 5 | export async function zip( 6 | sourceDirectoryPath: string, 7 | destFilePath: string, 8 | ): Promise { 9 | let output = FS.createWriteStream(destFilePath); 10 | let archive = archiver('zip'); 11 | 12 | // TODO: remove me 13 | // output.on('close', resolve); 14 | // archive.on('error', reject); 15 | 16 | archive.pipe(output); 17 | 18 | archive.directory(sourceDirectoryPath, false); 19 | 20 | await archive.finalize(); 21 | 22 | output.close(); 23 | } 24 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/@utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './archiver'; 2 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config type use for the app internal 3 | */ 4 | export interface Config extends JSONConfigFile { 5 | /** 6 | * Makescript agent module name, be used for type validation. 7 | * Usually its value should be "@makeflow/makescript-agent". 8 | */ 9 | agentModule: string | undefined; 10 | workspace: string; 11 | } 12 | 13 | export interface JSONConfigFile { 14 | /** 15 | * MakeScript Agent 注册到 MakeScript 时的名称 16 | */ 17 | name: string; 18 | 19 | server: { 20 | /** 21 | * 包含 Token 信息的 MakeScript 地址,类似 https://example.com/token 22 | */ 23 | url: string; 24 | }; 25 | 26 | scripts: { 27 | /** 28 | * 脚本仓库的地址 29 | */ 30 | git: string; 31 | /** 32 | * 脚本定义文件所在目录 33 | */ 34 | dir?: string; 35 | }; 36 | 37 | /** 38 | * Agent 要使用的网络代理 39 | */ 40 | proxy?: string | undefined; 41 | } 42 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/constants.ts: -------------------------------------------------------------------------------- 1 | export const OUTPUT_CLEAR_CHARACTER = '\x1Bc'; 2 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './main'; 3 | export * from './types'; 4 | export * from './shared'; 5 | export * from './constants'; 6 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/main.ts: -------------------------------------------------------------------------------- 1 | import 'villa/platform/node'; 2 | 3 | import {Tiva} from 'tiva'; 4 | 5 | import { 6 | NodeAdapter, 7 | ProcessAdapter, 8 | ShellAdapter, 9 | SqliteAdapter, 10 | } from './@adapters'; 11 | import {Entrances} from './@entrances'; 12 | import {Config} from './config'; 13 | import {logger} from './shared'; 14 | 15 | export async function main(tiva: Tiva, config: Config): Promise { 16 | let entrances = new Entrances(tiva, config); 17 | 18 | let adapters = [ 19 | new ProcessAdapter(), 20 | new NodeAdapter(), 21 | new ShellAdapter(), 22 | new SqliteAdapter(), 23 | ]; 24 | 25 | for (let adapter of adapters) { 26 | // TODO: any 27 | entrances.runningService.registerAdapter(adapter.type, adapter as any); 28 | } 29 | 30 | logger.info('MakeScript agent started.'); 31 | } 32 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './rpc'; 3 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/shared/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export type LOG_TYPE = 'info' | 'warning' | 'error'; 4 | 5 | export type Logger = typeof logger; 6 | 7 | export const logger = { 8 | info(message: string): void { 9 | console.info( 10 | String(message) 11 | .split('\n') 12 | .map(line => `[${chalk.green('INFO')}] ${line}`) 13 | .join('\n'), 14 | ); 15 | }, 16 | 17 | warn(message: string): void { 18 | console.warn( 19 | String(message) 20 | .split('\n') 21 | .map(line => `[${chalk.yellow('WARNING')}] ${line}`) 22 | .join('\n'), 23 | ); 24 | }, 25 | 26 | error(message: string): void { 27 | console.error( 28 | String(message) 29 | .split('\n') 30 | .map(line => `[${chalk.red('ERROR')}] ${line}`) 31 | .join('\n'), 32 | ); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/shared/rpc.ts: -------------------------------------------------------------------------------- 1 | import {Socket} from 'socket.io-client'; 2 | import {Dict} from 'tslang'; 3 | 4 | import {IRPC} from '../types'; 5 | 6 | import {Logger} from './logger'; 7 | 8 | export function bridgeRPC( 9 | rpc: IRPC, 10 | socket: Socket, 11 | logger: Logger = console, 12 | ): void { 13 | socket.onAny((event, {parameters}, callback) => { 14 | let methodName = convertEventNameToRPCMethodName(event); 15 | 16 | if (!methodName) { 17 | return; 18 | } 19 | 20 | if (!(methodName in rpc)) { 21 | logger.error(`Called an unknown RPC method "${methodName}"`); 22 | return; 23 | } 24 | 25 | if (typeof (rpc as any)[methodName] !== 'function') { 26 | logger.error(`RPC method "${methodName}" is not a callable method`); 27 | return; 28 | } 29 | 30 | Promise.resolve((rpc as Dict)[methodName](...parameters)) 31 | .then(result => callback({result})) 32 | .catch(error => callback({error: error.message ?? 'Unknown error'})); 33 | }); 34 | } 35 | 36 | export function wrapSocketToRPC( 37 | socket: Socket, 38 | logger: Logger = console, 39 | ): T { 40 | return new Proxy( 41 | {}, 42 | { 43 | get(_, methodName) { 44 | if (typeof methodName !== 'string') { 45 | throw new Error(`Unknown RPC call "${String(methodName)}"`); 46 | } 47 | 48 | return async (...params: unknown[]) => { 49 | return new Promise((resolve, reject) => { 50 | socket.emit( 51 | convertRPCMethodNameToEventName(methodName), 52 | {parameters: params}, 53 | ({error, result}: {error: string; result: unknown}) => { 54 | if (error) { 55 | logger.error(`RPC calling thrown an error: ${error}`); 56 | reject(new Error(error)); 57 | } else { 58 | resolve(result); 59 | } 60 | }, 61 | ); 62 | }); 63 | }; 64 | }, 65 | }, 66 | ) as T; 67 | } 68 | 69 | export function convertRPCMethodNameToEventName(name: string): string { 70 | return `rpc:${name}`; 71 | } 72 | 73 | export function convertEventNameToRPCMethodName( 74 | name: string, 75 | ): string | undefined { 76 | let execResult = /^rpc:(.+)$/.exec(name); 77 | 78 | if (!execResult) { 79 | return undefined; 80 | } 81 | 82 | return execResult[1]; 83 | } 84 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@mufan/code/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "../../bld/program", 6 | 7 | "types": ["node", "@makeflow/types-nominal/default"], 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "declaration": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../@adapters/adapter'; 2 | export * from './running'; 3 | export * from './script-definition'; 4 | export * from './rpc'; 5 | export * from './resource'; 6 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/types/resource.ts: -------------------------------------------------------------------------------- 1 | export interface ResourceConfig { 2 | expiresAt: number; 3 | } 4 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/types/rpc.ts: -------------------------------------------------------------------------------- 1 | import {ScriptDefinitionHooks} from '../@adapters'; 2 | 3 | import {ScriptRunningArgumentParameters, ScriptRunningResult} from './running'; 4 | import {BriefScriptDefinition} from './script-definition'; 5 | 6 | export interface IRPC {} 7 | 8 | export interface MakescriptAgentRPCRunScriptOptions { 9 | id: string; 10 | name: string; 11 | parameters: ScriptRunningArgumentParameters; 12 | resourcesBaseURL: string; 13 | password: string | undefined; 14 | } 15 | 16 | export interface MakescriptAgentRPC extends IRPC { 17 | syncScripts(): Promise; 18 | getScripts(): Promise; 19 | runScript( 20 | options: MakescriptAgentRPCRunScriptOptions, 21 | ): Promise; 22 | triggerHook( 23 | scriptName: string, 24 | hookName: keyof ScriptDefinitionHooks, 25 | ): Promise; 26 | } 27 | 28 | export interface MakescriptRPC extends IRPC { 29 | register(namespace: string, resume: boolean): Promise; 30 | updateResources(id: string, buffer: Buffer): Promise; 31 | updateOutput(id: string, output: string): Promise; 32 | } 33 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/types/running.ts: -------------------------------------------------------------------------------- 1 | import {Dict} from 'tslang'; 2 | 3 | import {AdapterRunScriptResult} from '../@adapters/adapter'; 4 | 5 | export interface ScriptRunningArgument { 6 | id: string; 7 | name: string; 8 | parameters: ScriptRunningArgumentParameters; 9 | resourcesBaseURL: string; 10 | password: string | undefined; 11 | } 12 | 13 | export type ScriptRunningArgumentParameters = Dict; 14 | 15 | export interface ScriptRunningResult { 16 | name: string; 17 | parameters: ScriptRunningArgumentParameters; 18 | deniedParameters: ScriptRunningArgumentParameters; 19 | result: AdapterRunScriptResult; 20 | output: { 21 | output: string; 22 | error: string; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/makescript-agent/src/program/types/script-definition.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScriptDefinitionHooks, 3 | ScriptDefinitionParameter, 4 | } from '../@adapters/adapter'; 5 | 6 | export interface BriefScriptDefinition { 7 | displayName: string; 8 | name: string; 9 | type: string; 10 | manual: boolean; 11 | parameters: { 12 | [name: string]: ScriptDefinitionParameter | true; 13 | }; 14 | needsPassword: boolean; 15 | hooks: { 16 | [TKey in keyof Required]: boolean; 17 | }; 18 | } 19 | 20 | export interface ScriptsDefinition { 21 | scripts: MakeScript.Adapter.AdapterScriptDefinition[]; 22 | password?: string; 23 | hooks?: ScriptsDefinitionHooks; 24 | } 25 | 26 | export interface ScriptsDefinitionHooks extends ScriptDefinitionHooks { 27 | install: string; 28 | } 29 | -------------------------------------------------------------------------------- /packages/makescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@makeflow/makescript", 3 | "version": "0.1.1-alpha.14", 4 | "license": "MIT", 5 | "author": "Chengdu Mufan Technology Co., Ltd.", 6 | "bin": { 7 | "makescript": "bld/program/@cli.js" 8 | }, 9 | "files": [ 10 | "src/**/*.ts", 11 | "bld" 12 | ], 13 | "scripts": { 14 | "pack:web": "rimraf bld/web && parcel build src/web/index.html --no-minify --out-dir bld/web/", 15 | "watch:web": "parcel watch src/web/index.html --out-dir bld/web/", 16 | "build:web": "rimraf .bld-cache/web && tsc --project src/web/tsconfig.json", 17 | "build": "rimraf bld/program && tsc --project src/program/tsconfig.json && yarn build:web" 18 | }, 19 | "dependencies": { 20 | "@hapi/boom": "^9.1.1", 21 | "@hapi/cookie": "^11.0.2", 22 | "@hapi/hapi": "^20.0.2", 23 | "@hapi/inert": "^6.0.3", 24 | "@hapi/joi": "^17.1.1", 25 | "@makeflow/makescript-agent": "^0.1.1-alpha.13", 26 | "bcrypt": "^5.0.0", 27 | "chalk": "^4.1.0", 28 | "clime": "^0.5.14", 29 | "entrance-decorator": "^0.1.0", 30 | "extendable-error": "^0.1.7", 31 | "extract-zip": "^2.0.1", 32 | "lowdb": "^1.0.0", 33 | "node-fetch": "^2.6.1", 34 | "prompts": "^2.4.0", 35 | "rimraf": "^3.0.2", 36 | "semver": "^7.3.2", 37 | "socket.io": "^3.0.3", 38 | "tiva": "^0.2.2", 39 | "tslib": "^2.0.3", 40 | "typescript": "^4.1.3", 41 | "uuid": "^7.0.0", 42 | "villa": "^0.3.2" 43 | }, 44 | "devDependencies": { 45 | "@ant-design/icons": "^4.3.0", 46 | "@babel/core": "^7.12.10", 47 | "@makeflow/types": "^0.1.24", 48 | "@makeflow/types-nominal": "^0.1.2", 49 | "@types/classnames": "^2.2.11", 50 | "@types/clipboard": "^2.0.1", 51 | "@types/hapi__cookie": "^10.1.1", 52 | "@types/hapi__hapi": "^20.0.2", 53 | "@types/hapi__inert": "^5.2.2", 54 | "@types/hapi__joi": "^17.1.6", 55 | "@types/history": "^4.7.8", 56 | "@types/lowdb": "^1.0.9", 57 | "@types/prompts": "^2.0.9", 58 | "@types/react": "^16.9.55", 59 | "@types/react-dom": "^16.9.9", 60 | "@types/semver": "^7.3.4", 61 | "@types/styled-components": "^5.1.4", 62 | "@types/uuid": "^7.0.0", 63 | "antd": "^4.8.5", 64 | "boring-router": "^0.4.3", 65 | "boring-router-react": "^0.4.0", 66 | "classnames": "^2.2.6", 67 | "clipboard": "^2.0.6", 68 | "highlight.js": "^10.4.0", 69 | "history": "^5.0.0", 70 | "memorize-decorator": "^0.2.4", 71 | "mobx": "^5.15.5", 72 | "mobx-react": "^6.2.5", 73 | "mobx-utils": "^5.6.1", 74 | "parcel-bundler": "^1.12.4", 75 | "react": "^17.0.1", 76 | "react-dom": "^17.0.1", 77 | "semver": "^7.3.2", 78 | "styled-components": "^5.2.1", 79 | "tslang": "^0.1.22" 80 | }, 81 | "publishConfig": { 82 | "access": "public" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@external/@auth.ts: -------------------------------------------------------------------------------- 1 | import Boom from '@hapi/boom'; 2 | import Hapi, {ServerAuthSchemeObject} from '@hapi/hapi'; 3 | 4 | import {TokenService} from '../../@services'; 5 | 6 | const TOKEN_AUTHORIZATION_REGEX = /^Token ([\w\-]+)$/i; 7 | 8 | const TOKEN_AUTH_SCHEME_NAME = 'token'; 9 | export const TOKEN_AUTH_STRATEGY_NAME = 'token'; 10 | 11 | export function setupAuth( 12 | tokenService: TokenService, 13 | server: Hapi.Server, 14 | ): void { 15 | server.auth.scheme( 16 | TOKEN_AUTH_SCHEME_NAME, 17 | (): ServerAuthSchemeObject => { 18 | return { 19 | authenticate(request, h): Hapi.Lifecycle.ReturnValue { 20 | let authorization = request.headers['authorization']?.trim(); 21 | let authorizationExecResult = 22 | authorization && TOKEN_AUTHORIZATION_REGEX.exec(authorization); 23 | 24 | if ( 25 | !authorization || 26 | !authorizationExecResult || 27 | !authorizationExecResult[1] 28 | ) { 29 | return Boom.unauthorized(); 30 | } 31 | 32 | let activeToken = tokenService.getActiveToken( 33 | authorizationExecResult[1], 34 | ); 35 | 36 | if (!activeToken) { 37 | return Boom.unauthorized(); 38 | } 39 | 40 | return h.authenticated({ 41 | credentials: {tokenLabel: activeToken.label}, 42 | }); 43 | }, 44 | }; 45 | }, 46 | ); 47 | 48 | server.auth.strategy(TOKEN_AUTH_STRATEGY_NAME, TOKEN_AUTH_SCHEME_NAME); 49 | } 50 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@external/@makeflow.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8901/api/makeflow/power-item/makescript-agent:echo-message/action/echo-message 2 | Content-Type: application/json 3 | 4 | { 5 | "source": { 6 | "url": "http://localhost:8060", 7 | "token": "6ae75e36-12b7-479e-be14-adef46cb464a", 8 | "organization": { 9 | "id": "323736ed-de2e-4fdd-8471-4d16e65ff735" 10 | }, 11 | "team": { 12 | "id": "a5d91f95-b427-45f1-854a-4106740cd5f4", 13 | "abstract": false 14 | }, 15 | "installation": { 16 | "id": "9e5152c7-f85f-4760-96d7-a70cde79dc62" 17 | }, 18 | "version": "0.1.0" 19 | }, 20 | "token": "66a56faa-976f-42bc-9e7f-0f77c2b96d18", 21 | "inputs": { 22 | "MESSAGE2": "option1", 23 | "MESSAGE1": "Hello", 24 | "taskURL": "http://localhost:8060/app/?_redirect=/task/3", 25 | "taskAssignee": { 26 | "id": "ca8f16aa-6cb7-4f88-b361-ee79803e2d56", 27 | "displayName": "老万" 28 | }, 29 | "taskBrief": "asdadasd", 30 | "taskNumericId": 3 31 | }, 32 | "configs": { 33 | "token": "003be58d-3297-4703-b2dc-0cb506851d98" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@external/@makeflow.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | import Joi from '@hapi/joi'; 3 | import type {Dict} from 'tslang'; 4 | 5 | import {MakeflowService} from '../../@services'; 6 | 7 | export function routeMakeflow( 8 | makeflowService: MakeflowService, 9 | server: Hapi.Server, 10 | ): void { 11 | server.route({ 12 | method: 'POST', 13 | path: '/api/makeflow/power-item/{item}/action/{action}', 14 | async handler(request) { 15 | let actionName = request.params.item as string; 16 | 17 | let { 18 | token: powerItemToken, 19 | configs: {token: accessToken}, 20 | inputs, 21 | } = request.payload as { 22 | token: string; 23 | configs: {token: string}; 24 | inputs: Dict; 25 | }; 26 | 27 | await makeflowService.triggerAction( 28 | actionName, 29 | powerItemToken, 30 | inputs, 31 | accessToken, 32 | ); 33 | 34 | return {}; 35 | }, 36 | options: { 37 | validate: { 38 | payload: Joi.object({ 39 | source: Joi.object(), 40 | token: Joi.string(), 41 | configs: Joi.object({ 42 | token: Joi.string(), 43 | }), 44 | inputs: Joi.object().optional(), 45 | }) as any, 46 | }, 47 | auth: false, 48 | }, 49 | }); 50 | 51 | // To fit power item related api 52 | server.route({ 53 | method: 'POST', 54 | path: '/api/makeflow/power-item/{item}/{action}', 55 | handler() { 56 | return {}; 57 | }, 58 | options: { 59 | auth: false, 60 | }, 61 | }); 62 | 63 | // To fit app installation related api 64 | server.route({ 65 | method: 'POST', 66 | path: '/api/makeflow/installation/{action}', 67 | handler() { 68 | return {}; 69 | }, 70 | options: { 71 | auth: false, 72 | }, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@external/@running.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8900/api/script/default/echo-message/enqueue 2 | Content-Type: application/json 3 | Authorization: Token 2b5e49af-2454-41d8-9a6f-fa99399607de 4 | 5 | { 6 | "parameters": { 7 | "message": "Hello" 8 | } 9 | } 10 | 11 | ### 12 | 13 | POST http://localhost:8900/api/script/default/generate-resources/enqueue 14 | Content-Type: application/json 15 | Authorization: Token 2b5e49af-2454-41d8-9a6f-fa99399607de 16 | 17 | { 18 | "parameters": { 19 | "text": "Hello" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@external/@running.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | import Joi from '@hapi/joi'; 3 | import type {Dict} from 'tslang'; 4 | 5 | import {RunningService} from '../../@services'; 6 | 7 | import {TOKEN_AUTH_STRATEGY_NAME} from './@auth'; 8 | 9 | export function routeRunning( 10 | runningService: RunningService, 11 | server: Hapi.Server, 12 | ): void { 13 | server.route({ 14 | method: 'POST', 15 | path: '/api/script/{namespace}/{name}/enqueue', 16 | async handler(request) { 17 | let {namespace, name} = request.params; 18 | 19 | let {parameters} = request.payload as { 20 | parameters: Dict; 21 | }; 22 | 23 | let tokenLabel = request.auth.credentials.tokenLabel as string; 24 | 25 | let recordId = await runningService.enqueueRunningRecord({ 26 | namespace, 27 | name, 28 | parameters, 29 | triggerTokenLabel: tokenLabel, 30 | makeflowTask: undefined, 31 | }); 32 | 33 | return {id: recordId}; 34 | }, 35 | options: { 36 | validate: { 37 | payload: Joi.object({ 38 | parameters: Joi.object(), 39 | }) as any, 40 | }, 41 | auth: TOKEN_AUTH_STRATEGY_NAME, 42 | }, 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@external/external.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | 3 | import {Entrances} from '../../@entrances'; 4 | 5 | import {setupAuth} from './@auth'; 6 | import {routeMakeflow} from './@makeflow'; 7 | import {routeRunning} from './@running'; 8 | 9 | export async function serveExternalAPI( 10 | server: Hapi.Server, 11 | entrances: Entrances, 12 | ): Promise { 13 | setupAuth(entrances.tokenService, server); 14 | 15 | routeMakeflow(entrances.makeflowService, server); 16 | routeRunning(entrances.runningService, server); 17 | 18 | await server.start(); 19 | } 20 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@external/index.ts: -------------------------------------------------------------------------------- 1 | export * from './external'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@web/@auth.ts: -------------------------------------------------------------------------------- 1 | import Cookie from '@hapi/cookie'; 2 | import Hapi from '@hapi/hapi'; 3 | import type {Dict} from 'tslang'; 4 | 5 | import {AppService} from '../../@services'; 6 | 7 | export const COOKIE_NAME = 'makescript'; 8 | export const COOKIE_PASSWORD = 'makescript-cookie-password-secret'; 9 | 10 | export const SESSION_AUTH_STRATEGY = 'session'; 11 | 12 | export async function setupAuth( 13 | appService: AppService, 14 | server: Hapi.Server, 15 | ): Promise { 16 | await server.register(Cookie); 17 | 18 | server.auth.strategy(SESSION_AUTH_STRATEGY, 'cookie', { 19 | cookie: { 20 | name: COOKIE_NAME, 21 | password: COOKIE_PASSWORD, 22 | isSecure: false, 23 | }, 24 | redirectTo: () => (appService.initialized ? '/login' : '/initialize'), 25 | validateFunc: async (_, session) => { 26 | if (appService.noAuthRequired || (session as Dict).authed) { 27 | return {valid: true}; 28 | } 29 | 30 | return {valid: false}; 31 | }, 32 | }); 33 | 34 | server.auth.default(SESSION_AUTH_STRATEGY); 35 | } 36 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@web/@authorization.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | import Joi from '@hapi/joi'; 3 | import type {Dict} from 'tslang'; 4 | 5 | import {AppService} from '../../@services'; 6 | 7 | export function routeAuthorization( 8 | appService: AppService, 9 | server: Hapi.Server, 10 | ): void { 11 | server.route({ 12 | method: 'GET', 13 | path: '/api/check', 14 | handler() { 15 | return {}; 16 | }, 17 | }); 18 | 19 | server.route({ 20 | method: 'POST', 21 | path: '/api/login', 22 | handler(request, h) { 23 | let {password} = request.payload as { 24 | password: string; 25 | }; 26 | 27 | let passwordCorrect = appService.validatePassword(password); 28 | 29 | if (!passwordCorrect) { 30 | return h.redirect('/login'); 31 | } 32 | 33 | request.cookieAuth.set({authed: true}); 34 | 35 | return h.redirect('/home'); 36 | }, 37 | options: { 38 | auth: { 39 | mode: 'try', 40 | }, 41 | validate: { 42 | payload: (Joi.object({ 43 | password: Joi.string().optional(), 44 | }) as unknown) as Dict, 45 | }, 46 | }, 47 | }); 48 | 49 | server.route({ 50 | method: 'POST', 51 | path: '/api/initialize', 52 | async handler(request, h) { 53 | let {password} = request.payload as { 54 | password: string; 55 | }; 56 | 57 | await appService.initialize(password); 58 | 59 | request.cookieAuth.set({authed: true}); 60 | 61 | return h.redirect('/home'); 62 | }, 63 | options: { 64 | auth: { 65 | mode: 'try', 66 | }, 67 | validate: { 68 | payload: (Joi.object({ 69 | password: Joi.string().optional(), 70 | }) as unknown) as Dict, 71 | }, 72 | }, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@web/@makeflow.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | import Joi from '@hapi/joi'; 3 | 4 | import {MakeflowService} from '../../@services'; 5 | 6 | export function routeMakeflow( 7 | makeflowService: MakeflowService, 8 | server: Hapi.Server, 9 | ): void { 10 | server.route({ 11 | method: 'POST', 12 | path: '/api/makeflow/list-user-candidates', 13 | async handler(request) { 14 | let {username, password} = request.payload as { 15 | username: string; 16 | password: string; 17 | }; 18 | 19 | return makeflowService.listUserCandidates(username, password); 20 | }, 21 | options: { 22 | validate: { 23 | payload: Joi.object({ 24 | username: Joi.string(), 25 | password: Joi.string(), 26 | }) as any, 27 | }, 28 | }, 29 | }); 30 | 31 | server.route({ 32 | method: 'POST', 33 | path: '/api/makeflow/authenticate', 34 | async handler(request) { 35 | let {username, password, userId} = request.payload as { 36 | username: string; 37 | password: string; 38 | userId: string; 39 | }; 40 | 41 | await makeflowService.authenticate(username, password, userId); 42 | 43 | return {}; 44 | }, 45 | options: { 46 | validate: { 47 | payload: Joi.object({ 48 | username: Joi.string(), 49 | password: Joi.string(), 50 | userId: Joi.string(), 51 | }) as any, 52 | }, 53 | }, 54 | }); 55 | 56 | server.route({ 57 | method: 'GET', 58 | path: '/api/makeflow/check-authentication', 59 | handler() { 60 | return {authenticated: makeflowService.checkAuthentication()}; 61 | }, 62 | }); 63 | 64 | server.route({ 65 | method: 'GET', 66 | path: '/api/makeflow/power-app-definition', 67 | async handler() { 68 | return makeflowService.generateAppDefinition(); 69 | }, 70 | }); 71 | 72 | server.route({ 73 | method: 'POST', 74 | path: '/api/makeflow/publish', 75 | async handler() { 76 | await makeflowService.publishPowerApp(); 77 | 78 | return {}; 79 | }, 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@web/@resources.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | 3 | import {getResourcePath} from '../../@utils/resource'; 4 | import {Config} from '../../config'; 5 | 6 | export function routeResources(server: Hapi.Server, config: Config): void { 7 | server.route({ 8 | method: 'GET', 9 | path: '/resources/{id}/{path*}', 10 | async handler(request, h) { 11 | let id = request.params.id; 12 | let path = request.params.path ?? ''; 13 | 14 | let realPath = await getResourcePath(id, path, config); 15 | 16 | return h.file(realPath, {confine: false}); 17 | }, 18 | options: { 19 | auth: false, 20 | }, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@web/@scripts.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | import Joi from '@hapi/joi'; 3 | import type {Dict} from 'tslang'; 4 | 5 | import {AgentService, RunningService} from '../../@services'; 6 | import {Config} from '../../config'; 7 | 8 | export function routeScripts( 9 | agentService: AgentService, 10 | runningService: RunningService, 11 | config: Config, 12 | server: Hapi.Server, 13 | ): void { 14 | server.route({ 15 | method: 'GET', 16 | path: '/api/status', 17 | async handler() { 18 | let scriptDefinitionsMap = await agentService.getScriptDefinitionsMap(); 19 | 20 | return { 21 | url: config.url, 22 | joinLink: agentService.joinLink, 23 | registeredAgents: Array.from(scriptDefinitionsMap).map( 24 | ([namespace, definitions]) => { 25 | return {namespace, scriptQuantity: definitions.length}; 26 | }, 27 | ), 28 | }; 29 | }, 30 | }); 31 | 32 | server.route({ 33 | method: 'POST', 34 | path: '/api/scripts/run', 35 | async handler(request) { 36 | let {namespace, name, parameters, password} = request.payload as { 37 | namespace: string; 38 | name: string; 39 | parameters: Dict; 40 | password: string | undefined; 41 | }; 42 | 43 | await runningService.runScriptDirectly({ 44 | namespace, 45 | name, 46 | parameters, 47 | password, 48 | }); 49 | 50 | return {}; 51 | }, 52 | options: { 53 | validate: { 54 | payload: Joi.object({ 55 | namespace: Joi.string(), 56 | name: Joi.string(), 57 | parameters: Joi.object(), 58 | password: Joi.string().optional(), 59 | }) as any, 60 | }, 61 | }, 62 | }); 63 | 64 | server.route({ 65 | method: 'GET', 66 | path: '/api/scripts', 67 | async handler() { 68 | let scriptDefinitionsMap = await agentService.getScriptDefinitionsMap(); 69 | 70 | return { 71 | url: config.url, 72 | definitionsDict: Object.fromEntries(scriptDefinitionsMap.entries()), 73 | }; 74 | }, 75 | }); 76 | 77 | server.route({ 78 | method: 'GET', 79 | path: '/api/scripts/running-records', 80 | handler() { 81 | return {records: runningService.runningRecords}; 82 | }, 83 | }); 84 | 85 | server.route({ 86 | method: 'POST', 87 | path: '/api/records/run', 88 | async handler(request) { 89 | let {id, password} = request.payload as { 90 | id: string; 91 | password: string | undefined; 92 | }; 93 | 94 | await runningService.runScriptFromRecords(id, password); 95 | 96 | return {}; 97 | }, 98 | options: { 99 | validate: { 100 | payload: Joi.object({ 101 | id: Joi.string(), 102 | password: Joi.string().optional(), 103 | }) as any, 104 | }, 105 | }, 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@web/@tokens.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | import Joi from '@hapi/joi'; 3 | 4 | import {TokenService} from '../../@services'; 5 | 6 | export function routeTokens( 7 | tokenService: TokenService, 8 | server: Hapi.Server, 9 | ): void { 10 | server.route({ 11 | method: 'POST', 12 | path: '/api/token/generate', 13 | async handler(request) { 14 | let {label} = request.payload as {label: string}; 15 | 16 | return {token: await tokenService.generateToken(label)}; 17 | }, 18 | options: { 19 | validate: { 20 | payload: Joi.object({ 21 | label: Joi.string(), 22 | }) as any, 23 | }, 24 | }, 25 | }); 26 | 27 | server.route({ 28 | method: 'GET', 29 | path: '/api/tokens', 30 | handler() { 31 | return {tokens: tokenService.getActiveTokens()}; 32 | }, 33 | }); 34 | 35 | server.route({ 36 | method: 'POST', 37 | path: '/api/token/disable', 38 | async handler(request) { 39 | let {id} = request.payload as {id: string}; 40 | 41 | await tokenService.disableToken(id); 42 | 43 | return {}; 44 | }, 45 | options: { 46 | validate: { 47 | payload: Joi.object({ 48 | id: Joi.string(), 49 | }) as any, 50 | }, 51 | }, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@web/index.ts: -------------------------------------------------------------------------------- 1 | export * from './web'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/@web/web.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | 3 | import Hapi from '@hapi/hapi'; 4 | import Inert from '@hapi/inert'; 5 | 6 | import {Entrances} from '../../@entrances'; 7 | 8 | import {setupAuth} from './@auth'; 9 | import {routeAuthorization} from './@authorization'; 10 | import {routeMakeflow} from './@makeflow'; 11 | import {routeResources} from './@resources'; 12 | import {routeScripts} from './@scripts'; 13 | import {routeTokens} from './@tokens'; 14 | 15 | const WEB_STATIC_PATH = Path.join(__dirname, '..', '..', '..', 'web'); 16 | const WEB_STATIC_PATH_INDEX = Path.join(WEB_STATIC_PATH, 'index.html'); 17 | 18 | export async function serveWeb( 19 | server: Hapi.Server, 20 | entrances: Entrances, 21 | ): Promise { 22 | await server.register(Inert); 23 | 24 | await setupAuth(entrances.appService, server); 25 | 26 | routeAuthorization(entrances.appService, server); 27 | routeScripts( 28 | entrances.agentService, 29 | entrances.runningService, 30 | entrances.config, 31 | server, 32 | ); 33 | routeTokens(entrances.tokenService, server); 34 | routeMakeflow(entrances.makeflowService, server); 35 | 36 | routeResources(server, entrances.config); 37 | 38 | server.route({ 39 | method: 'GET', 40 | path: '/{path*}', 41 | handler(request, h) { 42 | let path = request.params.path; 43 | 44 | if (/.+\..+/.test(path)) { 45 | return h.file(Path.join(WEB_STATIC_PATH, path), {confine: false}); 46 | } 47 | 48 | return h.file(WEB_STATIC_PATH_INDEX, {confine: false}); 49 | }, 50 | options: { 51 | auth: false, 52 | }, 53 | }); 54 | await server.start(); 55 | } 56 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/api.ts: -------------------------------------------------------------------------------- 1 | import Hapi from '@hapi/hapi'; 2 | import {logger} from '@makeflow/makescript-agent'; 3 | 4 | import {Entrances} from '../@entrances'; 5 | 6 | import {serveExternalAPI} from './@external'; 7 | import {serveWeb} from './@web'; 8 | 9 | export async function serveAPI( 10 | server: Hapi.Server, 11 | entrances: Entrances, 12 | ): Promise { 13 | await serveExternalAPI(server, entrances); 14 | await serveWeb(server, entrances); 15 | 16 | logger.info(`MakeScript is running on port ${entrances.config.listen.port}`); 17 | } 18 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as Path from 'path'; 4 | 5 | import {logger} from '@makeflow/makescript-agent'; 6 | import {CLI, Shim} from 'clime'; 7 | 8 | let cli = new CLI('makescript', Path.join(__dirname, '@commands')); 9 | 10 | let shim = new Shim(cli); 11 | shim.execute(process.argv).catch(logger.error); 12 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@commands/check-definition.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | 3 | import {logger} from '@makeflow/makescript-agent'; 4 | import {Castable, Command, command, metadata, param} from 'clime'; 5 | import {Tiva} from 'tiva'; 6 | 7 | export const description = 'Check scripts definition'; 8 | 9 | export const brief = 'check scripts definition'; 10 | 11 | export const PATH_DEFAULT = 'makescript.json'; 12 | 13 | @command() 14 | export default class extends Command { 15 | @metadata 16 | async execute( 17 | @param({ 18 | description: 'The file path fo the scripts definition.', 19 | required: false, 20 | default: PATH_DEFAULT, 21 | }) 22 | path: Castable.File, 23 | ): Promise { 24 | if (!(await path.exists())) { 25 | logger.error(`The file "${path.fullName}" not found`); 26 | process.exit(1); 27 | } 28 | 29 | logger.info(`Checking scripts definition "${path.baseName}"`); 30 | 31 | let tiva = new Tiva({ 32 | project: Path.join(__dirname, '../../../src/program'), 33 | }); 34 | 35 | let {default: content} = await import(path.fullName); 36 | 37 | try { 38 | await tiva.validate( 39 | { 40 | module: '@makeflow/makescript-agent', 41 | type: 'ScriptsDefinition', 42 | }, 43 | content, 44 | ); 45 | } catch (error) { 46 | if (error.diagnostics) { 47 | logger.error(error.diagnostics); 48 | } 49 | 50 | throw error; 51 | } 52 | 53 | logger.info('The content of the scripts definition is correct.'); 54 | process.exit(0); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@commands/default.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | import * as OS from 'os'; 3 | import * as Path from 'path'; 4 | 5 | import { 6 | JSONConfigFile as AgentJSONConfigFile, 7 | logger, 8 | main as agentMain, 9 | } from '@makeflow/makescript-agent'; 10 | import {Command, command, metadata} from 'clime'; 11 | import prompts from 'prompts'; 12 | import {Tiva} from 'tiva'; 13 | import {v4 as uuidv4} from 'uuid'; 14 | 15 | import {JSONConfigFile} from '../config'; 16 | import {main} from '../main'; 17 | 18 | export const JSON_CONFIG_INDENTATION = 2; 19 | 20 | export const WORKSPACE_PATH = Path.resolve(OS.homedir(), '.makescript'); 21 | 22 | export const DEFAULT_AGENT_WORKSPACE_PATH = Path.join( 23 | WORKSPACE_PATH, 24 | 'default-agent', 25 | ); 26 | 27 | export const MAKESCRIPT_CONFIG_FILE_PATH = Path.join( 28 | WORKSPACE_PATH, 29 | 'makescript.json', 30 | ); 31 | export const MAKESCRIPT_DEFAULT_AGENT_CONFIG_FILE_PATH = Path.join( 32 | DEFAULT_AGENT_WORKSPACE_PATH, 33 | 'makescript-agent.json', 34 | ); 35 | 36 | export const MAKESCRIPT_CONFIG_DEFAULT: JSONConfigFile = { 37 | url: `http://localhost:8900`, 38 | 39 | listen: { 40 | host: 'localhost', 41 | port: 8900, 42 | }, 43 | 44 | agent: { 45 | token: uuidv4(), 46 | }, 47 | 48 | makeflow: { 49 | url: 'https://makeflow.com', 50 | powerApp: { 51 | name: 'makescript', 52 | displayName: 'MakeScript', 53 | description: 'Auto generated by makescript', 54 | }, 55 | }, 56 | }; 57 | 58 | export const MAKESCRIPT_DEFAULT_AGENT_CONFIG_DEFAULT = ( 59 | token: string, 60 | git: string, 61 | dir: string | undefined, 62 | ): AgentJSONConfigFile => { 63 | return { 64 | name: 'default', 65 | 66 | server: { 67 | url: `http://localhost:8900/${token}`, 68 | }, 69 | 70 | scripts: { 71 | git, 72 | dir, 73 | }, 74 | 75 | proxy: undefined, 76 | }; 77 | }; 78 | 79 | @command() 80 | export default class extends Command { 81 | @metadata 82 | async execute(): Promise { 83 | let configFileExists = FS.existsSync(MAKESCRIPT_CONFIG_FILE_PATH); 84 | 85 | if (!configFileExists) { 86 | let {withDefaultAgent} = await prompts({ 87 | type: 'confirm', 88 | name: 'withDefaultAgent', 89 | message: 'Run with a default agent?', 90 | initial: true, 91 | }); 92 | 93 | // There is a bug (or unhandled behavior) with 'prompts'. 94 | // When user press CTRL + C , program will continue to execute with empty answers. 95 | // https://github.com/terkelg/prompts/issues/252 96 | if (withDefaultAgent === undefined) { 97 | return; 98 | } 99 | 100 | if (withDefaultAgent) { 101 | let {repoURL, subPath} = await prompts([ 102 | { 103 | type: 'text', 104 | name: 'repoURL', 105 | message: 'Enter the scripts repo url', 106 | validate: value => /^(https?:\/\/.+)|(\w+\.git)$/.test(value), 107 | }, 108 | { 109 | type: 'text', 110 | name: 'subPath', 111 | message: 'Enter the scripts definition dir path', 112 | }, 113 | ]); 114 | 115 | // There is a bug (or unhandled behavior) with 'prompts'. 116 | // When user press CTRL + C , program will continue to execute with empty answers. 117 | // https://github.com/terkelg/prompts/issues/252 118 | if (!repoURL) { 119 | return; 120 | } 121 | 122 | writeConfig( 123 | MAKESCRIPT_DEFAULT_AGENT_CONFIG_FILE_PATH, 124 | MAKESCRIPT_DEFAULT_AGENT_CONFIG_DEFAULT( 125 | MAKESCRIPT_CONFIG_DEFAULT.agent.token, 126 | repoURL, 127 | subPath, 128 | ), 129 | ); 130 | } 131 | 132 | writeConfig(MAKESCRIPT_CONFIG_FILE_PATH, MAKESCRIPT_CONFIG_DEFAULT); 133 | } 134 | 135 | let makescriptConfig = readConfig( 136 | MAKESCRIPT_CONFIG_FILE_PATH, 137 | )!; 138 | 139 | let defaultAgentConfig = readConfig( 140 | MAKESCRIPT_DEFAULT_AGENT_CONFIG_FILE_PATH, 141 | ); 142 | 143 | let tiva = new Tiva({ 144 | project: Path.join(__dirname, '../../../src/program'), 145 | }); 146 | 147 | logger.info('Checking config file ...'); 148 | 149 | try { 150 | await tiva.validate( 151 | {module: './config', type: 'JSONConfigFile'}, 152 | makescriptConfig, 153 | ); 154 | 155 | await main({ 156 | ...makescriptConfig, 157 | workspace: WORKSPACE_PATH, 158 | }); 159 | 160 | if (defaultAgentConfig) { 161 | logger.info('Checking config file of default agent ...'); 162 | await tiva.validate( 163 | {module: '@makeflow/makescript-agent', type: 'JSONConfigFile'}, 164 | defaultAgentConfig, 165 | ); 166 | 167 | await agentMain(tiva, { 168 | ...defaultAgentConfig, 169 | agentModule: '@makeflow/makescript-agent', 170 | workspace: DEFAULT_AGENT_WORKSPACE_PATH, 171 | }); 172 | } 173 | } catch (error) { 174 | if (error.diagnostics) { 175 | logger.error( 176 | `Config file structure does not match:\n${error.diagnostics}`, 177 | ); 178 | } 179 | 180 | throw error; 181 | } 182 | } 183 | } 184 | 185 | function readConfig(path: string): T | undefined { 186 | if (!FS.existsSync(path)) { 187 | return undefined; 188 | } 189 | 190 | let jsonConfigText = FS.readFileSync(path).toString(); 191 | 192 | return JSON.parse(jsonConfigText); 193 | } 194 | 195 | function writeConfig(path: string, config: object): void { 196 | let jsonConfigText = JSON.stringify( 197 | config, 198 | undefined, 199 | JSON_CONFIG_INDENTATION, 200 | ); 201 | 202 | let dirname = Path.dirname(path); 203 | 204 | if (!FS.existsSync(dirname)) { 205 | FS.mkdirSync(dirname, {recursive: true}); 206 | } 207 | 208 | FS.writeFileSync(path, jsonConfigText); 209 | } 210 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@commands/generate-hash.ts: -------------------------------------------------------------------------------- 1 | import {logger} from '@makeflow/makescript-agent'; 2 | import Bcrypt from 'bcrypt'; 3 | import {Command, command, metadata} from 'clime'; 4 | import prompts from 'prompts'; 5 | 6 | const PASSWORD_SALT_ROUNDS = 10; 7 | 8 | export const description = 9 | 'Generate and output a hash used for script password.'; 10 | 11 | export const brief = 'generate hash for password'; 12 | 13 | @command() 14 | export default class extends Command { 15 | @metadata 16 | async execute(): Promise { 17 | // TODO: prompts 2 18 | 19 | let {password, repeatingPassword} = await prompts([ 20 | { 21 | name: 'password', 22 | type: 'password', 23 | message: 'Please enter the password to generate a hash', 24 | validate: value => value?.length > 6, 25 | }, 26 | { 27 | name: 'repeatingPassword', 28 | type: 'password', 29 | message: 'Please repeat the password', 30 | validate: (value, previousValues) => value === previousValues.password, 31 | }, 32 | ]); 33 | 34 | // There is a bug (or unhandled behavior) with 'prompts'. 35 | // When user press CTRL + C , program will continue to execute with empty answers. 36 | // https://github.com/terkelg/prompts/issues/252 37 | if (!password || !repeatingPassword || password !== repeatingPassword) { 38 | return; 39 | } 40 | 41 | let passwordHash = await Bcrypt.hash(password, PASSWORD_SALT_ROUNDS); 42 | 43 | logger.info(`The generated hash is:`); 44 | logger.info(`\t${passwordHash}`); 45 | logger.info( 46 | 'You can copy it to scripts definition as field "passwordHash", then it needs a password when running a script.', 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@core/error.ts: -------------------------------------------------------------------------------- 1 | import ExtendableError from 'extendable-error'; 2 | 3 | export class ExpectedError extends ExtendableError { 4 | constructor(readonly code: string, message?: string) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error'; 2 | export * from './models/model'; 3 | export * from './models'; 4 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@core/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './makeflow'; 2 | export * from './model'; 3 | export * from './token'; 4 | export * from './running-record'; 5 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@core/models/makeflow.ts: -------------------------------------------------------------------------------- 1 | export interface MakeflowInfoModal { 2 | loginToken: string | undefined; 3 | powerAppVersion: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@core/models/model.ts: -------------------------------------------------------------------------------- 1 | import {MakeflowInfoModal} from './makeflow'; 2 | import {RunningRecordModel} from './running-record'; 3 | import {TokenModel} from './token'; 4 | 5 | export const MODEL_VERSION = 1; 6 | 7 | export interface Model { 8 | version: number; 9 | makeflow: MakeflowInfoModal; 10 | initialized: boolean; 11 | passwordHash: string | undefined; 12 | tokens: TokenModel[]; 13 | records: RunningRecordModel[]; 14 | } 15 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@core/models/running-record.ts: -------------------------------------------------------------------------------- 1 | import {RunningRecord, RunningRecordMakeflowInfo} from '../../types'; 2 | 3 | export interface RunningRecordModel extends RunningRecord { 4 | makeflow: RunningRecordModelMakeflowInfo | undefined; 5 | } 6 | 7 | export interface RunningRecordModelMakeflowInfo 8 | extends RunningRecordMakeflowInfo { 9 | powerItemToken: string; 10 | } 11 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@core/models/token.ts: -------------------------------------------------------------------------------- 1 | export interface TokenModel { 2 | id: string; 3 | label: string; 4 | hash: string; 5 | createdAt: number; 6 | disabledAt: number | undefined; 7 | } 8 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@entrances.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {Server} from 'http'; 3 | 4 | import entrance from 'entrance-decorator'; 5 | 6 | import { 7 | AgentService, 8 | AppService, 9 | DBService, 10 | MakeflowService, 11 | RunningService, 12 | SocketService, 13 | TokenService, 14 | } from './@services'; 15 | import {Config} from './config'; 16 | 17 | export class Entrances { 18 | readonly ready = Promise.all([this.dbService.ready]); 19 | 20 | constructor(private httpServer: Server, readonly config: Config) { 21 | this.up(); 22 | } 23 | 24 | up(): void { 25 | this.appService.up(); 26 | } 27 | 28 | // TODO: Type limit 29 | @entrance 30 | get eventEmitter(): EventEmitter { 31 | return new EventEmitter(); 32 | } 33 | 34 | @entrance 35 | get dbService(): DBService { 36 | return new DBService(this.config); 37 | } 38 | 39 | @entrance 40 | get socketService(): SocketService { 41 | return new SocketService(this.httpServer, this.config); 42 | } 43 | 44 | @entrance 45 | get makeflowService(): MakeflowService { 46 | return new MakeflowService( 47 | this.agentService, 48 | this.runningService, 49 | this.tokenService, 50 | this.dbService, 51 | this.eventEmitter, 52 | this.config, 53 | ); 54 | } 55 | 56 | @entrance 57 | get agentService(): AgentService { 58 | return new AgentService(this.config); 59 | } 60 | 61 | @entrance 62 | get runningService(): RunningService { 63 | return new RunningService( 64 | this.agentService, 65 | this.dbService, 66 | this.eventEmitter, 67 | this.config, 68 | ); 69 | } 70 | 71 | @entrance 72 | get tokenService(): TokenService { 73 | return new TokenService(this.dbService); 74 | } 75 | 76 | @entrance 77 | get appService(): AppService { 78 | return new AppService( 79 | this.agentService, 80 | this.makeflowService, 81 | this.socketService, 82 | this.dbService, 83 | this.config, 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@services/agent-service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BriefScriptDefinition, 3 | MakescriptAgentRPC, 4 | ScriptRunningArgumentParameters, 5 | ScriptRunningResult, 6 | logger, 7 | } from '@makeflow/makescript-agent'; 8 | import * as villa from 'villa'; 9 | 10 | import {ExpectedError} from '../@core'; 11 | import {Config} from '../config'; 12 | 13 | export class AgentService { 14 | registeredRPCMap = new Map(); 15 | 16 | get joinLink(): string { 17 | return getConnectURL(this.config); 18 | } 19 | 20 | constructor(private config: Config) {} 21 | 22 | async getScriptDefinitionsMap(): Promise< 23 | Map 24 | > { 25 | return new Map( 26 | await villa.map( 27 | Array.from(this.registeredRPCMap), 28 | async ([namespace, rpc]) => { 29 | return [namespace, await rpc.getScripts()] as const; 30 | }, 31 | ), 32 | ); 33 | } 34 | 35 | async requireScriptDefinition( 36 | namespace: string, 37 | name: string, 38 | ): Promise { 39 | let definitions = await this.registeredRPCMap.get(namespace)?.getScripts(); 40 | 41 | let definition = definitions?.find(definition => definition.name === name); 42 | 43 | if (!definition) { 44 | throw new Error( 45 | `The script definition of type "${name}" in "${name}" not found`, 46 | ); 47 | } 48 | 49 | return definition; 50 | } 51 | 52 | async runScript( 53 | namespace: string, 54 | { 55 | id, 56 | name, 57 | parameters, 58 | resourcesBaseURL, 59 | password, 60 | }: { 61 | id: string; 62 | name: string; 63 | parameters: ScriptRunningArgumentParameters; 64 | resourcesBaseURL: string; 65 | password: string | undefined; 66 | }, 67 | ): Promise { 68 | let agentRPC = this.registeredRPCMap.get(namespace); 69 | 70 | if (!agentRPC) { 71 | logger.error(`Agent for ${namespace} not found.`); 72 | throw new ExpectedError('NAMESPACE_NOT_REGISTERED'); 73 | } 74 | 75 | logger.info(`Running record "${id}" of script "${namespace}:${name}"`); 76 | 77 | let result = await agentRPC.runScript({ 78 | id, 79 | name, 80 | parameters, 81 | resourcesBaseURL, 82 | password, 83 | }); 84 | 85 | logger.info( 86 | `Complete running record "${id}" of script "${namespace}:${name}"`, 87 | ); 88 | 89 | return result; 90 | } 91 | } 92 | 93 | export function getConnectURL(config: Config): string { 94 | return `${config.url}/${config.agent.token}`; 95 | } 96 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@services/app-service.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | import * as OS from 'os'; 3 | import * as Path from 'path'; 4 | 5 | import { 6 | MakescriptAgentRPC, 7 | MakescriptRPC, 8 | bridgeRPC, 9 | logger, 10 | wrapSocketToRPC, 11 | } from '@makeflow/makescript-agent'; 12 | import extractZip from 'extract-zip'; 13 | import {Socket} from 'socket.io'; 14 | import {v4 as uuidv4} from 'uuid'; 15 | import * as villa from 'villa'; 16 | 17 | import {RESOURCES_RELATIVE_PATH, calculateHash} from '../@utils'; 18 | import {Config} from '../config'; 19 | 20 | import {AgentService} from './agent-service'; 21 | import {DBService} from './db-service'; 22 | import {MakeflowService} from './makeflow-service'; 23 | import {SocketService} from './socket-service'; 24 | 25 | const USER_PASSWORD_HASH_SALT = 'makescript-user-password-hash-salt'; 26 | 27 | const MAKESCRIPT_TMPDIR = Path.join(OS.tmpdir(), 'makescript-temp'); 28 | 29 | export class AppService { 30 | get initialized(): boolean { 31 | return this.dbService.db.get('initialized').value(); 32 | } 33 | 34 | get noAuthRequired(): boolean { 35 | return !this.dbService.db.get('passwordHash').value(); 36 | } 37 | 38 | constructor( 39 | private agentService: AgentService, 40 | private makeflowService: MakeflowService, 41 | private socketService: SocketService, 42 | private dbService: DBService, 43 | private config: Config, 44 | ) {} 45 | 46 | up(): void { 47 | this.socketService.server.on('connection', (socket: Socket) => { 48 | logger.info(`New connection from socket client: ${socket.id}`); 49 | 50 | bridgeRPC( 51 | new RPC(this.agentService, this.makeflowService, socket, this.config), 52 | socket as any, 53 | logger, 54 | ); 55 | }); 56 | } 57 | 58 | async initialize(password: string): Promise { 59 | if (this.initialized) { 60 | throw new Error('The application has already been initialized'); 61 | } 62 | 63 | let passwordHash = password && calculatePasswordHash(password); 64 | 65 | await this.dbService.db 66 | .assign({ 67 | initialized: true, 68 | passwordHash, 69 | }) 70 | .write(); 71 | } 72 | 73 | validatePassword(password: string | undefined): boolean { 74 | let passwordHashToValidate = password && calculatePasswordHash(password); 75 | 76 | let passwordHash = this.dbService.db.get('passwordHash').value(); 77 | 78 | return passwordHashToValidate === passwordHash; 79 | } 80 | } 81 | 82 | class RPC implements MakescriptRPC { 83 | private agentRPC = wrapSocketToRPC( 84 | this.socket as any, 85 | logger, 86 | ); 87 | 88 | constructor( 89 | private agentService: AgentService, 90 | private makeflowService: MakeflowService, 91 | private socket: Socket, 92 | private config: Config, 93 | ) {} 94 | 95 | async register(namespace: string, resume: boolean): Promise { 96 | logger.info(`Registering agent "${namespace}" ...`); 97 | 98 | if (this.agentService.registeredRPCMap.has(namespace) && !resume) { 99 | throw new Error(`Agent "${namespace}" has already registered`); 100 | } 101 | 102 | this.socket.on('disconnect', () => { 103 | logger.info(`Agent "${namespace}" disconnected`); 104 | this.agentService.registeredRPCMap.delete(namespace); 105 | }); 106 | 107 | this.agentService.registeredRPCMap.set(namespace, this.agentRPC); 108 | 109 | logger.info(`Agent "${namespace}" registered`); 110 | } 111 | 112 | async updateResources(id: string, buffer: Buffer): Promise { 113 | let temporaryPath = Path.join(MAKESCRIPT_TMPDIR, `${uuidv4()}.zip`); 114 | 115 | if (!FS.existsSync(MAKESCRIPT_TMPDIR)) { 116 | FS.mkdirSync(MAKESCRIPT_TMPDIR); 117 | } 118 | 119 | await villa.async(FS.writeFile)(temporaryPath, buffer); 120 | 121 | await extractZip(temporaryPath, { 122 | dir: Path.join(this.config.workspace, RESOURCES_RELATIVE_PATH, id), 123 | }); 124 | 125 | // TODO: It will throw an error and failed to extract zip file in previous step. 126 | // await villa.async(rimraf)(temporaryPath); 127 | } 128 | 129 | async updateOutput(id: string, output: string): Promise { 130 | await this.makeflowService.updatePowerItem({id, description: output}); 131 | } 132 | } 133 | 134 | function calculatePasswordHash(password: string): string { 135 | return calculateHash(password, USER_PASSWORD_HASH_SALT); 136 | } 137 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@services/db-service.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | 3 | import Lowdb from 'lowdb'; 4 | import FileAsync from 'lowdb/adapters/FileAsync'; 5 | 6 | import {MODEL_VERSION, Model} from '../@core'; 7 | import {Config} from '../config'; 8 | 9 | const DB_FILE_NAME = 'db.json'; 10 | 11 | const DB_MODEL_DEFAULT: Model = { 12 | version: MODEL_VERSION, 13 | makeflow: { 14 | loginToken: undefined, 15 | powerAppVersion: '0.1.0', 16 | }, 17 | initialized: false, 18 | passwordHash: undefined, 19 | tokens: [], 20 | records: [], 21 | }; 22 | 23 | export class DBService { 24 | readonly ready: Promise; 25 | 26 | readonly db!: Lowdb.LowdbAsync; 27 | 28 | constructor(private config: Config) { 29 | this.ready = this.initialize(); 30 | } 31 | 32 | private async initialize(): Promise { 33 | // Assign 'this.db' only while initialization 34 | (this.db as any) = await Lowdb( 35 | new FileAsync(Path.join(this.config.workspace, DB_FILE_NAME)), 36 | ); 37 | 38 | await this.db.defaults(DB_MODEL_DEFAULT).write(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './makeflow-service'; 2 | export * from './running-service'; 3 | export * from './db-service'; 4 | export * from './token-service'; 5 | export * from './agent-service'; 6 | export * from './socket-service'; 7 | export * from './app-service'; 8 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@services/running-service.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | 3 | import { 4 | ScriptRunningArgumentParameters, 5 | logger, 6 | } from '@makeflow/makescript-agent'; 7 | import {v4 as uuidv4} from 'uuid'; 8 | 9 | import { 10 | ExpectedError, 11 | RunningRecordModel, 12 | RunningRecordModelMakeflowInfo, 13 | } from '../@core'; 14 | import {Config} from '../config'; 15 | import {RunningRecord, RunningRecordMakeflowInfo} from '../types'; 16 | 17 | import {AgentService} from './agent-service'; 18 | import {DBService} from './db-service'; 19 | 20 | export class RunningService { 21 | get runningRecords(): RunningRecord[] { 22 | let runningRecordModels = this.dbService.db.get('records').value(); 23 | 24 | return runningRecordModels.map(model => convertRecordModelToRecord(model)); 25 | } 26 | 27 | constructor( 28 | private agentService: AgentService, 29 | private dbService: DBService, 30 | private eventEmitter: EventEmitter, 31 | private config: Config, 32 | ) {} 33 | 34 | async runScriptDirectly({ 35 | namespace, 36 | name, 37 | parameters, 38 | password, 39 | }: { 40 | namespace: string; 41 | name: string; 42 | parameters: ScriptRunningArgumentParameters; 43 | password: string | undefined; 44 | }): Promise { 45 | let recordId = await this.enqueueRunningRecord({ 46 | namespace, 47 | name, 48 | parameters, 49 | triggerTokenLabel: undefined, 50 | makeflowTask: undefined, 51 | tryToRun: false, 52 | }); 53 | 54 | await this.runScriptFromRecords(recordId, password); 55 | } 56 | 57 | async enqueueRunningRecord({ 58 | namespace, 59 | name, 60 | parameters, 61 | triggerTokenLabel, 62 | makeflowTask, 63 | tryToRun = true, 64 | }: { 65 | namespace: string; 66 | name: string; 67 | parameters: ScriptRunningArgumentParameters; 68 | triggerTokenLabel: string | undefined; 69 | makeflowTask: RunningRecordModelMakeflowInfo | undefined; 70 | tryToRun?: boolean; 71 | }): Promise { 72 | let definition = await this.agentService.requireScriptDefinition( 73 | namespace, 74 | name, 75 | ); 76 | 77 | let recordId = uuidv4(); 78 | 79 | await this.dbService.db 80 | .get('records') 81 | .unshift({ 82 | id: recordId, 83 | namespace, 84 | name, 85 | parameters, 86 | deniedParameters: {}, 87 | triggerTokenLabel, 88 | makeflow: makeflowTask, 89 | result: undefined, 90 | output: undefined, 91 | createdAt: Date.now(), 92 | ranAt: undefined, 93 | }) 94 | .write(); 95 | 96 | if (definition.hooks.postscript) { 97 | try { 98 | await this.agentService.registeredRPCMap 99 | .get(namespace) 100 | ?.triggerHook(name, 'postscript'); 101 | } catch (error) { 102 | logger.error( 103 | `Error to trigger hook "postscript" for script "${name}": ${error.message}`, 104 | ); 105 | } 106 | } 107 | 108 | if (tryToRun && !definition.manual && !definition.needsPassword) { 109 | await this.runScriptFromRecords(recordId, undefined); 110 | } 111 | 112 | return recordId; 113 | } 114 | 115 | async runScriptFromRecords( 116 | id: string, 117 | password: string | undefined, 118 | ): Promise { 119 | let record = this.dbService.db.get('records').find({id}).value(); 120 | 121 | if (!record) { 122 | throw new ExpectedError('SCRIPT_RUNNING_RECORD_NOT_FOUND'); 123 | } 124 | 125 | let resourcesBaseURL = `${this.config.url}/resources/${id}`; 126 | 127 | let runningResult = await this.agentService.runScript(record.namespace, { 128 | id: record.id, 129 | name: record.name, 130 | parameters: record.parameters, 131 | resourcesBaseURL, 132 | password, 133 | }); 134 | 135 | let {parameters, deniedParameters, result, output} = runningResult; 136 | 137 | await this.dbService.db 138 | .get('records') 139 | .find({id}) 140 | .assign({ 141 | parameters, 142 | deniedParameters, 143 | result, 144 | output, 145 | ranAt: Date.now(), 146 | }) 147 | .write(); 148 | 149 | this.eventEmitter.emit('script-running-completed', {id}); 150 | } 151 | } 152 | 153 | function convertRecordModelToRecord( 154 | recordModel: RunningRecordModel, 155 | ): RunningRecord { 156 | let {makeflow, ...rest} = recordModel; 157 | 158 | let convertedMakeflow: RunningRecordMakeflowInfo | undefined; 159 | 160 | if (makeflow) { 161 | let {powerItemToken, ...restMakeflow} = makeflow; 162 | 163 | convertedMakeflow = restMakeflow; 164 | } 165 | 166 | return { 167 | ...rest, 168 | makeflow: convertedMakeflow, 169 | }; 170 | } 171 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@services/socket-service.ts: -------------------------------------------------------------------------------- 1 | import {Server} from 'http'; 2 | 3 | import SocketIO from 'socket.io'; 4 | 5 | import {Config} from '../config'; 6 | 7 | export class SocketService { 8 | readonly server = new SocketIO.Server(this.httpServer, { 9 | path: `/${this.config.agent.token}`, 10 | }); 11 | 12 | constructor(private httpServer: Server, private config: Config) {} 13 | } 14 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@services/token-service.ts: -------------------------------------------------------------------------------- 1 | import {v4 as uuidv4} from 'uuid'; 2 | 3 | import {TokenModel} from '../@core'; 4 | import {calculateHash} from '../@utils'; 5 | import {ActiveToken, convertTokenModelToActiveToken} from '../types'; 6 | 7 | import {DBService} from './db-service'; 8 | 9 | export class TokenService { 10 | constructor(private dbService: DBService) {} 11 | 12 | async generateToken(label: string): Promise { 13 | let token = uuidv4(); 14 | 15 | let hash = calculateHash(token); 16 | 17 | await this.dbService.db 18 | .get('tokens') 19 | .push({ 20 | id: uuidv4(), 21 | label, 22 | hash, 23 | createdAt: Date.now(), 24 | disabledAt: undefined, 25 | }) 26 | .write(); 27 | 28 | return token; 29 | } 30 | 31 | async disableToken(id: string): Promise { 32 | await this.dbService.db 33 | .get('tokens') 34 | .find({id}) 35 | .assign({disabledAt: Date.now()}) 36 | .write(); 37 | } 38 | 39 | getActiveToken(token: string): TokenModel | undefined { 40 | let hash = calculateHash(token); 41 | 42 | return this.dbService.db 43 | .get('tokens') 44 | .find(model => model.hash === hash && !model.disabledAt) 45 | .value(); 46 | } 47 | 48 | getActiveTokens(): ActiveToken[] { 49 | return this.dbService.db 50 | .get('tokens') 51 | .filter(tokenModel => !tokenModel.disabledAt) 52 | .value() 53 | .map(model => convertTokenModelToActiveToken(model)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@utils/hash.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export function calculateHash(string: string, salt?: string): string { 4 | let md5 = crypto.createHash('md5'); 5 | 6 | md5.update(string); 7 | 8 | if (salt) { 9 | md5.update(salt); 10 | } 11 | 12 | return md5.digest().toString(); 13 | } 14 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hash'; 2 | export * from './spawn'; 3 | export * from './resource'; 4 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@utils/resource.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | import * as Path from 'path'; 3 | 4 | import {ResourceConfig, logger} from '@makeflow/makescript-agent'; 5 | import rimraf from 'rimraf'; 6 | import * as villa from 'villa'; 7 | 8 | import {Config} from '../config'; 9 | 10 | const INDEX_FILE_NAME = 'index.html'; 11 | 12 | const RESOURCES_CONFIG_FILE_NAME = 'config.json'; 13 | 14 | export const RESOURCES_RELATIVE_PATH = 'outputs'; 15 | 16 | export async function getResourcePath( 17 | id: string, 18 | path: string, 19 | config: Config, 20 | ): Promise { 21 | let resourceBasePath = Path.join( 22 | config.workspace, 23 | RESOURCES_RELATIVE_PATH, 24 | id, 25 | ); 26 | let resourceConfigPath = Path.join( 27 | resourceBasePath, 28 | RESOURCES_CONFIG_FILE_NAME, 29 | ); 30 | 31 | if (FS.existsSync(resourceConfigPath)) { 32 | try { 33 | let configContentBuffer = await villa.async(FS.readFile)( 34 | resourceConfigPath, 35 | ); 36 | let configContent = JSON.parse( 37 | configContentBuffer.toString(), 38 | ) as ResourceConfig; 39 | 40 | if (configContent.expiresAt < Date.now()) { 41 | await villa.async(rimraf)(resourceBasePath); 42 | } 43 | } catch (error) { 44 | logger.warn(`Failed to parse resource config file: ${error.message}`); 45 | } 46 | } 47 | 48 | let pathToVisit = Path.join(resourceBasePath, path); 49 | 50 | if (!path || !/.+\..+/.test(path)) { 51 | return Path.join(pathToVisit, INDEX_FILE_NAME); 52 | } 53 | 54 | return pathToVisit; 55 | } 56 | -------------------------------------------------------------------------------- /packages/makescript/src/program/@utils/spawn.ts: -------------------------------------------------------------------------------- 1 | import * as CP from 'child_process'; 2 | 3 | import * as villa from 'villa'; 4 | 5 | export async function spawn( 6 | command: string, 7 | args: string[], 8 | options: CP.SpawnOptions, 9 | ): Promise { 10 | let cp = CP.spawn(command, args, options); 11 | 12 | if (cp.stdout) { 13 | cp.stdout.pipe(process.stdout); 14 | } 15 | 16 | if (cp.stderr) { 17 | cp.stderr.pipe(process.stderr); 18 | } 19 | 20 | await villa.awaitable(cp); 21 | } 22 | -------------------------------------------------------------------------------- /packages/makescript/src/program/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config extends JSONConfigFile { 2 | workspace: string; 3 | } 4 | 5 | export interface JSONConfigFile { 6 | /** 7 | * 可让外部访问到的地址,默认为 http://localhost:8900 8 | */ 9 | url: string; 10 | 11 | listen: { 12 | /** 13 | * MakeScript 服务监听到的 host,默认为 localhost 14 | */ 15 | host: string; 16 | /** 17 | * MakeScript 服务监听到的端口,默认为 8900 18 | */ 19 | port: number; 20 | }; 21 | 22 | agent: { 23 | /** 24 | * 提供给 Agent 验证身份的 Token 25 | */ 26 | token: string; 27 | }; 28 | 29 | /** 30 | * Makeflow 相关配置 31 | */ 32 | makeflow: { 33 | /** 34 | * Makeflow 的地址,默认为 https://www.makeflow.com 35 | */ 36 | url: string; 37 | powerApp: { 38 | name: string; 39 | displayName: string; 40 | description: string; 41 | }; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/makescript/src/program/main.ts: -------------------------------------------------------------------------------- 1 | import 'villa/platform/node'; 2 | 3 | import Hapi from '@hapi/hapi'; 4 | 5 | import {serveAPI} from './@api'; 6 | import {Entrances} from './@entrances'; 7 | import {Config} from './config'; 8 | 9 | export async function main(config: Config): Promise { 10 | let server = Hapi.server({ 11 | host: config.listen.host, 12 | port: config.listen.port, 13 | state: { 14 | strictHeader: false, 15 | }, 16 | }); 17 | 18 | let entrances = new Entrances(server.listener, config); 19 | 20 | await entrances.ready; 21 | 22 | await serveAPI(server, entrances); 23 | } 24 | -------------------------------------------------------------------------------- /packages/makescript/src/program/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@mufan/code/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "../../bld/program", 6 | 7 | "types": ["node", "@makeflow/types-nominal/default"], 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "declaration": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/makescript/src/program/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './makeflow'; 2 | export * from './token'; 3 | export * from './running-record'; 4 | -------------------------------------------------------------------------------- /packages/makescript/src/program/types/makeflow.ts: -------------------------------------------------------------------------------- 1 | import type {OrganizationId, UserId} from '@makeflow/types-nominal'; 2 | 3 | // TODO: Remove this file while '@makeflow/types' completed. 4 | 5 | export interface MFUserCandidate { 6 | id: UserId; 7 | username: string; 8 | organization: MFOrganizationBasics; 9 | profile: MFUserProfile | undefined; 10 | disabled: boolean | undefined; 11 | } 12 | 13 | export interface MFOrganizationBasics { 14 | id: OrganizationId; 15 | displayName: string; 16 | } 17 | 18 | export interface MFUserProfile { 19 | fullName?: string | undefined; 20 | avatar?: string | undefined; 21 | bio?: string | undefined; 22 | mobile?: string | undefined; 23 | email?: string | undefined; 24 | position?: string | undefined; 25 | } 26 | -------------------------------------------------------------------------------- /packages/makescript/src/program/types/running-record.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AdapterRunScriptResult, 3 | ScriptRunningArgumentParameters, 4 | } from '@makeflow/makescript-agent'; 5 | 6 | export interface RunningRecord { 7 | id: string; 8 | namespace: string; 9 | name: string; 10 | parameters: ScriptRunningArgumentParameters; 11 | deniedParameters: ScriptRunningArgumentParameters; 12 | triggerTokenLabel: string | undefined; 13 | makeflow: RunningRecordMakeflowInfo | undefined; 14 | result: AdapterRunScriptResult | undefined; 15 | output: RunningRecordOutput | undefined; 16 | createdAt: number; 17 | ranAt: number | undefined; 18 | } 19 | 20 | export interface RunningRecordMakeflowInfo { 21 | taskUrl: string; 22 | numericId: number; 23 | brief: string; 24 | assignee: { 25 | displayName: string; 26 | id: string; 27 | }; 28 | } 29 | 30 | export interface RunningRecordOutput { 31 | output: string; 32 | error: string; 33 | } 34 | -------------------------------------------------------------------------------- /packages/makescript/src/program/types/token.ts: -------------------------------------------------------------------------------- 1 | import {TokenModel} from '../@core'; 2 | 3 | export interface ActiveToken { 4 | id: string; 5 | label: string; 6 | createdAt: number; 7 | } 8 | 9 | export function convertTokenModelToActiveToken({ 10 | id, 11 | label, 12 | createdAt, 13 | }: TokenModel): ActiveToken { 14 | return { 15 | id, 16 | label, 17 | createdAt, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/makescript/src/web/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 10 chrome version 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | overrides: [ 4 | { 5 | files: ['**/*.html'], 6 | plugins: ['html'], 7 | extends: ['eslint:recommended'], 8 | }, 9 | { 10 | env: {node: false}, 11 | files: ['**/*.{ts,tsx}'], 12 | extends: ['plugin:@mufan/default'], 13 | parserOptions: { 14 | project: './tsconfig.json', 15 | tsconfigRootDir: __dirname, 16 | }, 17 | rules: { 18 | 'import/no-extraneous-dependencies': ['error', {devDependencies: true}], 19 | }, 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mufancom/makescript/68b22ab209adfca07346ed3af5cb2b8d31170b33/packages/makescript/src/web/@assets/favicon.png -------------------------------------------------------------------------------- /packages/makescript/src/web/@components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Button = styled.button` 4 | padding: 10px 20px; 5 | font-size: 16px; 6 | text-align: center; 7 | color: hsl(0, 100%, 100%); 8 | background-color: hsl(221, 100%, 58%); 9 | border-radius: 4px; 10 | transition: background-color 0.3s; 11 | text-decoration: none; 12 | border: 0; 13 | cursor: pointer; 14 | 15 | &:hover { 16 | background-color: hsl(221, 100%, 50%); 17 | } 18 | 19 | &:disabled { 20 | background-color: hsl(221, 12%, 89%); 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@components/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@components/card/card.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component, ReactNode} from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Wrapper = styled.div` 5 | width: 350px; 6 | border-radius: 5px; 7 | padding: 60px; 8 | box-shadow: hsla(0, 0%, 0%, 0.12) 0 0 12px; 9 | background-color: #fff; 10 | display: flex; 11 | flex-direction: column; 12 | `; 13 | 14 | const Title = styled.h1` 15 | margin: 0; 16 | font-size: 30px; 17 | font-weight: 300; 18 | line-height: 40px; 19 | color: hsl(0, 0%, 20%); 20 | `; 21 | 22 | const Summary = styled.div` 23 | font-size: 14px; 24 | line-height: 20px; 25 | margin: 10px 0; 26 | color: hsl(0, 0%, 60%); 27 | `; 28 | 29 | export interface CardProps { 30 | title: string; 31 | summary: string; 32 | } 33 | 34 | export class Card extends Component { 35 | render(): ReactNode { 36 | let {title, summary, children} = this.props; 37 | 38 | return ( 39 | 40 | {title} 41 | {summary} 42 | {children} 43 | 44 | ); 45 | } 46 | 47 | static Wrapper = Wrapper; 48 | } 49 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@components/card/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card'; 2 | export * from './button'; 3 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@constants.ts: -------------------------------------------------------------------------------- 1 | import {Entrances} from './@entrances'; 2 | 3 | export const ENTRANCES = new Entrances(); 4 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@core/error.ts: -------------------------------------------------------------------------------- 1 | import ExtendableError from 'extendable-error'; 2 | 3 | export class ExpectedError extends ExtendableError { 4 | constructor(readonly code: string, message?: string) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@entrances.ts: -------------------------------------------------------------------------------- 1 | import {Modal} from 'antd'; 2 | import entrance from 'entrance-decorator'; 3 | 4 | import {ENTRANCES} from './@constants'; 5 | /* eslint-disable @mufan/explicit-return-type */ 6 | import {route} from './@routes'; 7 | import { 8 | AuthorizationService, 9 | MakeflowService, 10 | ScriptsService as ScriptService, 11 | TokenService, 12 | } from './@services'; 13 | 14 | export class Entrances { 15 | readonly ready = Promise.all([]); 16 | 17 | constructor() { 18 | this.up(); 19 | } 20 | 21 | @entrance 22 | get tokenService() { 23 | return new TokenService(); 24 | } 25 | 26 | @entrance 27 | get scriptService() { 28 | return new ScriptService(); 29 | } 30 | 31 | @entrance 32 | get authorizationService() { 33 | return new AuthorizationService(); 34 | } 35 | 36 | @entrance 37 | get makeflowService() { 38 | return new MakeflowService(); 39 | } 40 | 41 | up() { 42 | // Route services 43 | route.$beforeEnterOrUpdate(match => { 44 | if (match.$exact) { 45 | ENTRANCES.scriptService.fetchRunningRecords().catch(console.error); 46 | } 47 | 48 | Modal.destroyAll(); 49 | }); 50 | 51 | route.status.$beforeEnterOrUpdate(() => { 52 | this.scriptService.fetchStatus().catch(console.error); 53 | }); 54 | 55 | route.scripts.$beforeEnterOrUpdate(match => { 56 | if (match.$exact) { 57 | route.scripts.records.$replace(); 58 | } 59 | }); 60 | 61 | route.scripts.records.$beforeEnter(() => { 62 | this.scriptService.fetchRunningRecords().catch(console.error); 63 | }); 64 | 65 | route.scripts.management.$beforeEnter(() => { 66 | this.scriptService.fetchScriptDefinitionsMap().catch(console.error); 67 | }); 68 | 69 | route.tokens.$beforeEnter(() => { 70 | this.tokenService.fetchTokens().catch(console.error); 71 | }); 72 | 73 | route.notFound.$beforeEnterOrUpdate(() => { 74 | route.$replace(); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@helpers/fetch.ts: -------------------------------------------------------------------------------- 1 | import {ExpectedError} from '../@core'; 2 | 3 | export async function fetchAPI( 4 | url: string, 5 | init?: RequestInit, 6 | ): Promise { 7 | let response = await fetch(url, { 8 | headers: new Headers({ 9 | 'Content-Type': 'application/json', 10 | }), 11 | ...init, 12 | }); 13 | 14 | if (response.redirected && location.href !== response.url) { 15 | location.href = response.url; 16 | } 17 | 18 | if (!response.ok) { 19 | throw new ExpectedError( 20 | 'REQUEST_FAILED', 21 | `The request expect 200 for response status code, but got ${response.status}: ${response.url}`, 22 | ); 23 | } 24 | 25 | let textBody = await response.text(); 26 | 27 | try { 28 | return JSON.parse(textBody); 29 | } catch {} 30 | 31 | return (textBody as unknown) as TResponse; 32 | } 33 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fetch'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@routes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './routes'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@routes/routes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RootRouteMatchType, 3 | RouteMatch, 4 | Router as BoringRouter, 5 | schema, 6 | } from 'boring-router'; 7 | import {BrowserHistory} from 'boring-router-react'; 8 | 9 | export const routeSchema = schema({ 10 | $children: { 11 | scripts: { 12 | $exact: true, 13 | $children: { 14 | records: { 15 | $exact: true, 16 | $children: { 17 | recordId: { 18 | $exact: true, 19 | $match: RouteMatch.SEGMENT, 20 | }, 21 | }, 22 | $query: { 23 | 'only-unexecuted': true, 24 | }, 25 | }, 26 | management: { 27 | $exact: true, 28 | $children: { 29 | namespace: { 30 | $exact: true, 31 | $match: RouteMatch.SEGMENT, 32 | $children: { 33 | scriptName: { 34 | $exact: true, 35 | $match: RouteMatch.SEGMENT, 36 | }, 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | makeflow: { 44 | $exact: true, 45 | $children: { 46 | login: true, 47 | }, 48 | }, 49 | tokens: true, 50 | status: true, 51 | login: true, 52 | initialize: true, 53 | notFound: { 54 | $match: /.*/, 55 | }, 56 | }, 57 | }); 58 | 59 | export const history = new BrowserHistory(); 60 | 61 | export const router = new BoringRouter(history); 62 | 63 | export const route = router.$route(routeSchema); 64 | 65 | export type Router = RootRouteMatchType; 66 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@services/authorization-service.ts: -------------------------------------------------------------------------------- 1 | import {fetchAPI} from '../@helpers'; 2 | 3 | export class AuthorizationService { 4 | async check(): Promise { 5 | await fetchAPI('/api/check'); 6 | } 7 | 8 | async login(password: string | undefined): Promise { 9 | await fetchAPI('/api/login', { 10 | method: 'post', 11 | body: JSON.stringify({ 12 | password, 13 | }), 14 | }); 15 | } 16 | 17 | async initialize(password: string | undefined): Promise { 18 | await fetchAPI('/api/initialize', { 19 | method: 'POST', 20 | body: JSON.stringify({ 21 | password, 22 | }), 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './script-service'; 2 | export * from './authorization-service'; 3 | export * from './token-service'; 4 | export * from './makeflow-service'; 5 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@services/makeflow-service.ts: -------------------------------------------------------------------------------- 1 | import {MFUserCandidate} from '../../program/types'; 2 | import {fetchAPI} from '../@helpers'; 3 | 4 | export class MakeflowService { 5 | async listUserCandidates( 6 | username: string, 7 | password: string, 8 | ): Promise { 9 | return fetchAPI('/api/makeflow/list-user-candidates', { 10 | method: 'POST', 11 | body: JSON.stringify({username, password}), 12 | }); 13 | } 14 | 15 | async authenticate( 16 | username: string, 17 | password: string, 18 | userId: string, 19 | ): Promise { 20 | await fetchAPI('/api/makeflow/authenticate', { 21 | method: 'POST', 22 | body: JSON.stringify({ 23 | username, 24 | password, 25 | userId, 26 | }), 27 | }); 28 | } 29 | 30 | async checkAuthentication(): Promise { 31 | let {authenticated} = await fetchAPI('/api/makeflow/check-authentication'); 32 | 33 | return authenticated; 34 | } 35 | 36 | async previewAppDefinition(): Promise { 37 | return fetchAPI('/api/makeflow/power-app-definition'); 38 | } 39 | 40 | async publishApp(): Promise { 41 | await fetchAPI('/api/makeflow/publish', {method: 'POST'}); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@services/script-service.ts: -------------------------------------------------------------------------------- 1 | import {BriefScriptDefinition} from '@makeflow/makescript-agent'; 2 | import {observable} from 'mobx'; 3 | import {Dict} from 'tslang'; 4 | 5 | import {RunningRecord} from '../../program/types'; 6 | import {fetchAPI} from '../@helpers'; 7 | 8 | export interface AgentsStatus { 9 | joinLink: string; 10 | registeredAgents: { 11 | namespace: string; 12 | scriptQuantity: number; 13 | }[]; 14 | } 15 | 16 | export class ScriptsService { 17 | @observable 18 | runningRecords: RunningRecord[] = []; 19 | 20 | getRunningRecord(id: string): RunningRecord | undefined { 21 | return this.runningRecords.find(item => item.id === id); 22 | } 23 | 24 | async fetchScriptDefinitionsMap(): Promise<{ 25 | scriptsMap: Map; 26 | baseURL: string; 27 | }> { 28 | let {definitionsDict, url} = await fetchAPI('/api/scripts'); 29 | 30 | return { 31 | scriptsMap: new Map(Object.entries(definitionsDict)), 32 | baseURL: url, 33 | }; 34 | } 35 | 36 | async fetchRunningRecords(): Promise { 37 | let {records} = await fetchAPI('/api/scripts/running-records'); 38 | 39 | this.runningRecords = records; 40 | } 41 | 42 | async fetchStatus(): Promise { 43 | return fetchAPI('/api/status'); 44 | } 45 | 46 | async runScriptFromRecords( 47 | id: string, 48 | password: string | undefined, 49 | ): Promise { 50 | await fetchAPI('/api/records/run', { 51 | method: 'POST', 52 | body: JSON.stringify({id, password}), 53 | }); 54 | 55 | await this.fetchRunningRecords(); 56 | } 57 | 58 | async runScriptDirectly(options: { 59 | namespace: string; 60 | name: string; 61 | parameters: Dict; 62 | password: string | undefined; 63 | }): Promise { 64 | await fetchAPI('/api/scripts/run', { 65 | method: 'POST', 66 | body: JSON.stringify(options), 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@services/token-service.ts: -------------------------------------------------------------------------------- 1 | import {computed, observable} from 'mobx'; 2 | 3 | import {ActiveToken} from '../../program/types'; 4 | import {fetchAPI} from '../@helpers'; 5 | 6 | export class TokenService { 7 | @observable 8 | private _tokens: ActiveToken[] | undefined; 9 | 10 | @computed 11 | get tokens(): ActiveToken[] { 12 | return this._tokens || []; 13 | } 14 | 15 | async fetchTokens(): Promise { 16 | let {tokens} = await fetchAPI('/api/tokens'); 17 | 18 | this._tokens = tokens; 19 | } 20 | 21 | async generateToken(label: string): Promise { 22 | let {token} = await fetchAPI('/api/token/generate', { 23 | method: 'POST', 24 | body: JSON.stringify({label}), 25 | }); 26 | 27 | await this.fetchTokens(); 28 | 29 | return token; 30 | } 31 | 32 | async disableToken(id: string): Promise { 33 | await fetchAPI('/api/token/disable', { 34 | method: 'POST', 35 | body: JSON.stringify({ 36 | id, 37 | }), 38 | }); 39 | 40 | await this.fetchTokens(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@third-part.ts: -------------------------------------------------------------------------------- 1 | declare module 'highlight.js/lib/core' { 2 | export default class { 3 | static registerLanguage(language: string, languageObject: unknown): void; 4 | 5 | static highlight(language: string, content: string): {value: string}; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@home/home-view.tsx: -------------------------------------------------------------------------------- 1 | import {Link as _Link, RouteComponentProps} from 'boring-router-react'; 2 | import {computed} from 'mobx'; 3 | import {observer} from 'mobx-react'; 4 | import React, {Component, ReactNode} from 'react'; 5 | import styled from 'styled-components'; 6 | 7 | import {Button, Card} from '../../@components'; 8 | import {ENTRANCES} from '../../@constants'; 9 | import {Router, route} from '../../@routes'; 10 | 11 | const Wrapper = styled.div` 12 | display: flex; 13 | width: 100vw; 14 | height: 100vh; 15 | justify-content: center; 16 | align-items: center; 17 | 18 | ${Card.Wrapper} { 19 | width: 420px; 20 | } 21 | `; 22 | 23 | const CardContent = styled.div` 24 | margin-top: 40px; 25 | display: flex; 26 | flex-direction: column; 27 | `; 28 | 29 | const Link = styled(_Link)` 30 | display: flex; 31 | 32 | ${Button} { 33 | flex: 1; 34 | } 35 | 36 | & + & { 37 | margin-top: 20px; 38 | } 39 | `; 40 | 41 | const ScriptsButton = styled(Button)` 42 | background-color: rgb(128, 203, 93); 43 | 44 | &:hover { 45 | background-color: rgb(100, 180, 80); 46 | } 47 | `; 48 | 49 | export interface HomeViewProps extends RouteComponentProps {} 50 | 51 | @observer 52 | export class HomeView extends Component { 53 | @computed 54 | private get scriptsQuantityToExecute(): number { 55 | return ENTRANCES.scriptService.runningRecords.filter( 56 | record => !record.ranAt, 57 | ).length; 58 | } 59 | 60 | render(): ReactNode { 61 | return ( 62 | 63 | 64 | 65 | 66 | 67 | 脚本执行 68 | {this.scriptsQuantityToExecute 69 | ? ` (${this.scriptsQuantityToExecute})` 70 | : ''} 71 | 72 | 73 | 74 | Token 管理 75 | 76 | 77 | Makefow 集成 78 | 79 | 80 | 节点管理 81 | 82 | 83 | 84 | 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home-view'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@initialize/index.ts: -------------------------------------------------------------------------------- 1 | export * from './initialize-view'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@initialize/initialize-view.tsx: -------------------------------------------------------------------------------- 1 | import {Input, message} from 'antd'; 2 | import {RouteComponentProps} from 'boring-router-react'; 3 | import {computed, observable} from 'mobx'; 4 | import {observer} from 'mobx-react'; 5 | import React, {ChangeEvent, Component, KeyboardEvent, ReactNode} from 'react'; 6 | import styled from 'styled-components'; 7 | 8 | import {Button, Card} from '../../@components'; 9 | import {ENTRANCES} from '../../@constants'; 10 | import {Router, route} from '../../@routes'; 11 | 12 | const Wrapper = styled.div` 13 | height: 100vh; 14 | width: 100vw; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | 19 | ${Card.Wrapper} { 20 | width: 420px; 21 | 22 | .input, 23 | ${Button} { 24 | margin-top: 20px; 25 | } 26 | } 27 | `; 28 | 29 | export interface InitializeViewProps 30 | extends RouteComponentProps {} 31 | 32 | @observer 33 | export class InitializeView extends Component { 34 | @observable 35 | private password: string | undefined; 36 | 37 | @computed 38 | private get formAvailable(): boolean { 39 | let password = this.password; 40 | 41 | return !!password && password.length >= 6; 42 | } 43 | 44 | render(): ReactNode { 45 | return ( 46 | 47 | 48 | 57 | 61 | 初始化 62 | 63 | 64 | 65 | ); 66 | } 67 | 68 | private onPasswordChange = ({ 69 | currentTarget: {value}, 70 | }: ChangeEvent): void => { 71 | this.password = value; 72 | }; 73 | 74 | private onInitialButtonClick = (): void => { 75 | this.tryToInitialize().catch(console.error); 76 | }; 77 | 78 | private onInputKeyDown = (event: KeyboardEvent): void => { 79 | if (event.key === 'Enter') { 80 | this.tryToInitialize().catch(console.error); 81 | } 82 | }; 83 | 84 | private async tryToInitialize(): Promise { 85 | if (!this.formAvailable) { 86 | return; 87 | } 88 | 89 | try { 90 | let password = this.password!; 91 | 92 | await ENTRANCES.authorizationService.initialize(password); 93 | route.$push(); 94 | } catch (error) { 95 | console.error(error); 96 | 97 | if (error.code === 'HAS_BEEN_INITIALIZED_ALREADY') { 98 | void message.error(应用已经初始化过了); 99 | } else { 100 | void message.error('初始化失败'); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login-view'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@login/login-view.tsx: -------------------------------------------------------------------------------- 1 | import {Input, message} from 'antd'; 2 | import {RouteComponentProps} from 'boring-router-react'; 3 | import {observable} from 'mobx'; 4 | import {observer} from 'mobx-react'; 5 | import React, {ChangeEvent, Component, KeyboardEvent, ReactNode} from 'react'; 6 | import styled from 'styled-components'; 7 | 8 | import {Button, Card} from '../../@components'; 9 | import {ENTRANCES} from '../../@constants'; 10 | import {Router, route} from '../../@routes'; 11 | 12 | const Wrapper = styled.div` 13 | height: 100vh; 14 | width: 100vw; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | 19 | ${Card.Wrapper} { 20 | width: 420px; 21 | 22 | .input, 23 | ${Button} { 24 | margin-top: 20px; 25 | } 26 | } 27 | `; 28 | 29 | export interface LoginViewProps extends RouteComponentProps {} 30 | 31 | @observer 32 | export class LoginView extends Component { 33 | @observable 34 | private password: string | undefined; 35 | 36 | render(): ReactNode { 37 | return ( 38 | 39 | 40 | 49 | 50 | 进入 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | private onPasswordChange = ({ 58 | currentTarget: {value}, 59 | }: ChangeEvent): void => { 60 | this.password = value; 61 | }; 62 | 63 | private onLoginButtonClick = (): void => { 64 | this.tryToLogin().catch(console.error); 65 | }; 66 | 67 | private onInputKeyDown = (event: KeyboardEvent): void => { 68 | if (event.key === 'Enter') { 69 | this.tryToLogin().catch(console.error); 70 | } 71 | }; 72 | 73 | private async tryToLogin(): Promise { 74 | let password = this.password; 75 | 76 | if (!password) { 77 | return; 78 | } 79 | 80 | try { 81 | await ENTRANCES.authorizationService.login(password); 82 | route.$push(); 83 | } catch (error) { 84 | if (error.code === 'PASSWORD_MISMATCH') { 85 | void message.error('密码错误, 请重试'); 86 | } else { 87 | void message.error('登录失败'); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@makeflow/@candidates-modal.tsx: -------------------------------------------------------------------------------- 1 | import {Modal} from 'antd'; 2 | import memorize from 'memorize-decorator'; 3 | import {observer} from 'mobx-react'; 4 | import React, {Component, MouseEventHandler, ReactNode} from 'react'; 5 | import styled from 'styled-components'; 6 | 7 | import {MFUserCandidate} from '../../../program/types'; 8 | import {ENTRANCES} from '../../@constants'; 9 | 10 | const CandidateWrapper = styled.div` 11 | padding: 5px 10px; 12 | display: flex; 13 | align-items: center; 14 | cursor: pointer; 15 | `; 16 | 17 | const CandidateAvatar = styled.img` 18 | width: 24px; 19 | height: 24px; 20 | border-radius: 50%; 21 | `; 22 | 23 | const CandidateName = styled.div` 24 | margin-left: 10px; 25 | `; 26 | 27 | const CandidateOrganization = styled.div` 28 | margin-left: 5px; 29 | font-size: 10px; 30 | color: #888; 31 | `; 32 | 33 | export interface CandidatesModalProps { 34 | candidates: MFUserCandidate[]; 35 | username: string; 36 | password: string; 37 | onCancel(): void; 38 | onSuccess(): void; 39 | onError(): void; 40 | } 41 | 42 | @observer 43 | export class CandidatesModal extends Component { 44 | render(): ReactNode { 45 | let {candidates, onCancel} = this.props; 46 | 47 | return ( 48 | 56 | {candidates.map( 57 | ({ 58 | id, 59 | username, 60 | organization: {displayName: organizationName}, 61 | profile, 62 | }) => ( 63 | 67 | 68 | {profile?.fullName} 69 | 70 | ({organizationName}/{username}) 71 | 72 | 73 | ), 74 | )} 75 | 76 | ); 77 | } 78 | 79 | @memorize() 80 | private getOnCandidateItemClick(userId: string): MouseEventHandler { 81 | return async () => { 82 | let {username, password, onSuccess, onError} = this.props; 83 | 84 | try { 85 | await ENTRANCES.makeflowService.authenticate( 86 | username, 87 | password, 88 | userId, 89 | ); 90 | 91 | onSuccess(); 92 | } catch { 93 | onError(); 94 | } 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@makeflow/index.ts: -------------------------------------------------------------------------------- 1 | export * from './makeflow-login-view'; 2 | export * from './makeflow-view'; 3 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@makeflow/makeflow-login-view.tsx: -------------------------------------------------------------------------------- 1 | import {Input, message} from 'antd'; 2 | import {RouteComponentProps} from 'boring-router-react'; 3 | import {action, computed, observable, runInAction} from 'mobx'; 4 | import {observer} from 'mobx-react'; 5 | import React, {ChangeEvent, Component, KeyboardEvent, ReactNode} from 'react'; 6 | import styled from 'styled-components'; 7 | 8 | import {MFUserCandidate} from '../../../program/types'; 9 | import {Button, Card} from '../../@components'; 10 | import {ENTRANCES} from '../../@constants'; 11 | import {Router, route} from '../../@routes'; 12 | 13 | import {CandidatesModal} from './@candidates-modal'; 14 | 15 | const Wrapper = styled.div` 16 | min-height: 100vh; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | 21 | ${Card.Wrapper} { 22 | width: 420px; 23 | 24 | .input, 25 | ${Button} { 26 | margin-top: 20px; 27 | } 28 | } 29 | `; 30 | 31 | export interface MakeflowLoginViewProps 32 | extends RouteComponentProps {} 33 | 34 | @observer 35 | export class MakeflowLoginView extends Component { 36 | @observable 37 | private candidatesModalVisible = false; 38 | 39 | @observable 40 | private candidates: MFUserCandidate[] | undefined; 41 | 42 | @observable 43 | private username = ''; 44 | 45 | @observable 46 | private password = ''; 47 | 48 | @computed 49 | private get inputsAvailable(): boolean { 50 | return !!this.username && !!this.password; 51 | } 52 | 53 | @computed 54 | private get candidatesModalRendering(): ReactNode { 55 | let candidates = this.candidates; 56 | 57 | if (!this.candidatesModalVisible || !candidates) { 58 | return undefined; 59 | } 60 | 61 | return ( 62 | 70 | ); 71 | } 72 | 73 | render(): ReactNode { 74 | return ( 75 | 76 | {this.candidatesModalRendering} 77 | 78 | 86 | 95 | 99 | 登录到 Makeflow 100 | 101 | 返回 102 | 103 | 104 | ); 105 | } 106 | 107 | private onUsernameChange = ({ 108 | currentTarget: {value}, 109 | }: ChangeEvent): void => { 110 | this.setUsername(value); 111 | }; 112 | 113 | private onPasswordChange = ({ 114 | currentTarget: {value}, 115 | }: ChangeEvent): void => { 116 | this.setPassword(value); 117 | }; 118 | 119 | private onBackButtonClick = (): void => { 120 | this.goBack(); 121 | }; 122 | 123 | private onLoginButtonClick = (): void => { 124 | this.tryToLogin().catch(console.error); 125 | }; 126 | 127 | private onInputKeyDown = (event: KeyboardEvent): void => { 128 | if (event.key === 'Enter') { 129 | this.tryToLogin().catch(console.error); 130 | } 131 | }; 132 | 133 | private async tryToLogin(): Promise { 134 | if (!this.inputsAvailable) { 135 | return; 136 | } 137 | 138 | try { 139 | let candidates = await ENTRANCES.makeflowService.listUserCandidates( 140 | this.username, 141 | this.password, 142 | ); 143 | 144 | if (candidates.length === 1) { 145 | let [candidate] = candidates; 146 | 147 | await ENTRANCES.makeflowService.authenticate( 148 | this.username, 149 | this.password, 150 | candidate.id, 151 | ); 152 | 153 | void message.success('登录成功'); 154 | this.goBack(); 155 | } else { 156 | runInAction(() => { 157 | this.candidates = candidates; 158 | this.candidatesModalVisible = true; 159 | }); 160 | } 161 | } catch { 162 | void message.error('登录失败'); 163 | } 164 | } 165 | 166 | private onModalError = (): void => { 167 | message.error('登录失败').promise.catch(console.error); 168 | 169 | runInAction(() => { 170 | this.candidatesModalVisible = false; 171 | }); 172 | }; 173 | 174 | private onModalSuccess = (): void => { 175 | message.success('登录成功').promise.catch(console.error); 176 | this.goBack(); 177 | 178 | runInAction(() => { 179 | this.candidatesModalVisible = false; 180 | }); 181 | }; 182 | 183 | private onModalCancel = (): void => { 184 | runInAction(() => { 185 | this.candidatesModalVisible = false; 186 | }); 187 | }; 188 | 189 | private goBack(): void { 190 | route.makeflow.$push(); 191 | } 192 | 193 | @action 194 | private setUsername(username: string): void { 195 | this.username = username; 196 | } 197 | 198 | @action 199 | private setPassword(password: string): void { 200 | this.password = password; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@makeflow/makeflow-view.tsx: -------------------------------------------------------------------------------- 1 | import 'highlight.js/styles/github.css'; 2 | 3 | import {Modal, message} from 'antd'; 4 | import {Link as _Link, RouteComponentProps} from 'boring-router-react'; 5 | import highlight from 'highlight.js/lib/core'; 6 | import jsonHighlight from 'highlight.js/lib/languages/json'; 7 | import {observable} from 'mobx'; 8 | import {observer} from 'mobx-react'; 9 | import React, {Component, ReactNode} from 'react'; 10 | import styled from 'styled-components'; 11 | 12 | import {Button, Card} from '../../@components'; 13 | import {ENTRANCES} from '../../@constants'; 14 | import {ExpectedError} from '../../@core'; 15 | import {Router, route} from '../../@routes'; 16 | 17 | highlight.registerLanguage('json', jsonHighlight); 18 | 19 | const Link = styled(_Link)``; 20 | 21 | const Wrapper = styled.div` 22 | min-height: 100vh; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | 27 | ${Button} { 28 | margin-top: 20px; 29 | } 30 | 31 | ${Card.Wrapper} { 32 | width: 420px; 33 | 34 | ${Link} { 35 | margin-top: 20px; 36 | display: flex; 37 | 38 | ${Button} { 39 | margin-top: 0; 40 | flex: 1; 41 | } 42 | } 43 | } 44 | `; 45 | 46 | const PreviewWrapper = styled.div` 47 | max-height: 55vh; 48 | width: 800px; 49 | overflow: auto; 50 | `; 51 | 52 | export interface MakeflowViewProps 53 | extends RouteComponentProps {} 54 | 55 | @observer 56 | export class MakeflowView extends Component { 57 | @observable 58 | private authenticated: boolean | undefined; 59 | 60 | render(): ReactNode { 61 | let authenticated = this.authenticated; 62 | 63 | return ( 64 | 65 | 66 | {typeof authenticated === 'undefined' ? undefined : authenticated ? ( 67 | 发布 Power APP 68 | ) : ( 69 | 70 | 登录到 Makeflow 71 | 72 | )} 73 | 74 | 返回 75 | 76 | 77 | 78 | ); 79 | } 80 | 81 | componentDidMount(): void { 82 | this.checkAuthentication().catch(console.error); 83 | } 84 | 85 | private onPublishButtonClick = async (): Promise => { 86 | try { 87 | let definition = await ENTRANCES.makeflowService.previewAppDefinition(); 88 | 89 | Modal.confirm({ 90 | title: '配置预览', 91 | width: 800, 92 | content: ( 93 | 94 | 102 | 103 | ), 104 | okText: '确认发布', 105 | onOk: this.onPreviewModalConfirm, 106 | cancelText: '取消', 107 | }); 108 | } catch (error) { 109 | if (error instanceof ExpectedError) { 110 | switch (error.code) { 111 | case 'MISSING_REQUIRED_CONFIGS': 112 | Modal.error({ 113 | title: '发布失败', 114 | content: ( 115 | 116 | 缺少发布 Makeflow APP 必要的外部访问地址配置, 117 | 请配置文件进行配置 118 | 119 | ), 120 | }); 121 | break; 122 | case 'MISSING_COMMANDS_CONFIG': 123 | Modal.error({ 124 | title: '发布失败', 125 | content: ( 126 | 127 | 脚本列表暂未初始化, 请进入 128 | 脚本管理界面 129 | 进行初始化. 130 | 131 | ), 132 | }); 133 | break; 134 | default: 135 | void message.error('发布失败'); 136 | break; 137 | } 138 | } else { 139 | void message.error('发布失败'); 140 | } 141 | } 142 | }; 143 | 144 | private onPreviewModalConfirm = async (): Promise => { 145 | try { 146 | await ENTRANCES.makeflowService.publishApp(); 147 | void message.success( 148 | <> 149 | 应用发布成功,安装时可到 Token 管理界面{' '} 150 | 创建 Token 151 | >, 152 | ); 153 | } catch (error) { 154 | if (error instanceof ExpectedError) { 155 | switch (error.code) { 156 | case 'PERMISSION_DENIED': 157 | void message.error( 158 | '尚未登录到 Makeflow 或登录会话已过期, 请先登录', 159 | ); 160 | route.makeflow.login.$push(); 161 | break; 162 | default: 163 | void message.error('发布失败'); 164 | break; 165 | } 166 | } else { 167 | void message.error('发布失败'); 168 | } 169 | } 170 | }; 171 | 172 | private async checkAuthentication(): Promise { 173 | this.authenticated = await ENTRANCES.makeflowService.checkAuthentication(); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@scripts/@common.tsx: -------------------------------------------------------------------------------- 1 | import React, {FunctionComponent} from 'react'; 2 | import styled from 'styled-components'; 3 | import {Dict} from 'tslang'; 4 | 5 | export const Wrapper = styled.div` 6 | box-sizing: border-box; 7 | width: 100vw; 8 | max-width: 1000px; 9 | height: 100vh; 10 | padding: 50px; 11 | margin: 0 auto; 12 | display: flex; 13 | `; 14 | 15 | export const ExecuteButton = styled.div` 16 | position: absolute; 17 | bottom: 40px; 18 | right: 40px; 19 | font-size: 20px; 20 | line-height: 30px; 21 | width: 40px; 22 | height: 40px; 23 | text-align: center; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | color: white; 28 | border-radius: 50%; 29 | background-color: hsl(221, 100%, 58%); 30 | box-shadow: 0 4px 12px -4px hsl(221, 93%, 73%); 31 | transition: background-color 0.3s, box-shadow 0.3s; 32 | cursor: pointer; 33 | 34 | &:hover { 35 | background-color: hsl(221, 100%, 50%); 36 | box-shadow: 0 4px 12px -4px hsl(221, 93%, 50%); 37 | } 38 | `; 39 | 40 | export const ScriptList = styled.div` 41 | display: flex; 42 | flex-direction: column; 43 | flex-shrink: 0; 44 | width: 250px; 45 | box-shadow: hsla(0, 0%, 0%, 0.08) 0 2px 4px 0; 46 | background-color: hsl(0, 0%, 100%); 47 | margin-right: 20px; 48 | border-radius: 2px; 49 | `; 50 | 51 | export const ScriptListContent = styled.div` 52 | flex: 1; 53 | overflow: auto; 54 | `; 55 | 56 | export const ScriptListLabel = styled.div` 57 | line-height: 20px; 58 | font-size: 14px; 59 | padding: 20px; 60 | display: flex; 61 | justify-content: space-between; 62 | flex-shrink: 0; 63 | `; 64 | 65 | export const ScriptBriefItem = styled.div` 66 | font-size: 13px; 67 | display: flex; 68 | flex-direction: column; 69 | align-items: flex-start; 70 | padding: 10px 20px; 71 | cursor: pointer; 72 | transition: background-color 0.3s; 73 | 74 | &.highlight { 75 | background-color: hsl(41, 100%, 97%); 76 | } 77 | 78 | &.error { 79 | background-color: hsl(11, 77%, 97%); 80 | } 81 | 82 | &:hover { 83 | background-color: hsl(0, 0%, 97%); 84 | } 85 | 86 | &.active { 87 | background-color: hsl(221, 100%, 97%); 88 | } 89 | `; 90 | 91 | export const ScriptType = styled.div` 92 | align-self: flex-end; 93 | font-size: 12px; 94 | color: hsl(0, 0%, 60%); 95 | margin-left: 10px; 96 | `; 97 | 98 | export const Title = styled.div` 99 | color: hsl(0, 0%, 100%); 100 | font-size: 16px; 101 | background-color: hsl(221, 100%, 58%); 102 | border-radius: 5px; 103 | padding: 5px 10px; 104 | width: fit-content; 105 | `; 106 | 107 | export const Label = styled.div` 108 | line-height: 16px; 109 | color: hsl(0, 0%, 40%); 110 | margin: 16px 0 10px 0; 111 | font-size: 12px; 112 | `; 113 | 114 | export const Item = styled.div` 115 | display: flex; 116 | font-size: 14px; 117 | `; 118 | 119 | export const EmptyPanel = styled.div` 120 | flex: 1; 121 | display: flex; 122 | justify-content: center; 123 | align-items: center; 124 | color: hsl(0, 0%, 60%); 125 | font-size: 14px; 126 | `; 127 | 128 | export const NotSelectedPanel = styled.div` 129 | flex: 1; 130 | display: flex; 131 | justify-content: center; 132 | align-items: center; 133 | background-color: hsl(0, 100%, 100%); 134 | box-shadow: hsla(0, 0%, 0%, 0.08) 0 2px 4px 0; 135 | color: hsl(0, 0%, 60%); 136 | border-radius: 2px; 137 | `; 138 | 139 | export const DictContent: FunctionComponent<{ 140 | dict?: Dict; 141 | label: string; 142 | }> = ({dict, label}) => { 143 | let entries = dict && Object.entries(dict); 144 | 145 | if (!entries || !entries.length) { 146 | return <>>; 147 | } 148 | 149 | return ( 150 | <> 151 | {label} 152 | {entries.map(([key, value]) => ( 153 | 154 | {key}: {String(value)} 155 | 156 | ))} 157 | > 158 | ); 159 | }; 160 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@scripts/@output-panel.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import {observer} from 'mobx-react'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | const Label = styled.div` 7 | position: absolute; 8 | line-height: 20px; 9 | padding: 0 10px; 10 | border-radius: 10px; 11 | top: -10px; 12 | left: 10px; 13 | color: white; 14 | `; 15 | 16 | const Output = styled.pre` 17 | padding: 20px; 18 | margin: 0; 19 | `; 20 | 21 | const Wrapper = styled.div` 22 | position: relative; 23 | margin-top: 20px; 24 | border-radius: 5px; 25 | 26 | &.info { 27 | background-color: hsl(101, 51%, 97%); 28 | border: 1px solid hsl(101, 51%, 58%); 29 | 30 | ${Label} { 31 | background-color: hsl(101, 51%, 58%); 32 | } 33 | } 34 | 35 | &.error { 36 | background-color: hsl(11, 77%, 97%); 37 | border: 1px solid hsl(11, 77%, 58%); 38 | 39 | ${Label} { 40 | background-color: hsl(11, 77%, 58%); 41 | } 42 | } 43 | `; 44 | 45 | export type OutputPanelType = 'info' | 'error'; 46 | 47 | export interface OutputPanelProps { 48 | type: OutputPanelType; 49 | label: string; 50 | output: string; 51 | } 52 | 53 | @observer 54 | export class OutputPanel extends Component { 55 | render(): ReactNode { 56 | let {type, label, output} = this.props; 57 | 58 | return ( 59 | 65 | {label} 66 | {output} 67 | 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@scripts/@running-record-viewer-view.tsx: -------------------------------------------------------------------------------- 1 | import {CaretRightFilled, RedoOutlined} from '@ant-design/icons'; 2 | import {Input, Modal, Tooltip, message} from 'antd'; 3 | import {RouteComponentProps} from 'boring-router-react'; 4 | import {computed, observable} from 'mobx'; 5 | import {Observer, observer} from 'mobx-react'; 6 | import React, {Component, ReactNode} from 'react'; 7 | import styled from 'styled-components'; 8 | 9 | import {RunningRecord} from '../../../program/types'; 10 | import {ENTRANCES} from '../../@constants'; 11 | import {Router} from '../../@routes'; 12 | 13 | import {DictContent, ExecuteButton, Item, Label, Title} from './@common'; 14 | import {OutputPanel} from './@output-panel'; 15 | 16 | const TOOLTIP_MOUSE_ENTER_DELAY = 0.5; 17 | 18 | // TODO: Cannot import `OUTPUT_CLEAR_CHARACTER` from '@makeflow/makescript-agent' ? 19 | const OUTPUT_CLEAR_CHARACTER = '\x1Bc'; 20 | const SHOWABLE_CLEAR_CHARACTER = '\n\n-- clear --\n\n'; 21 | 22 | const SUCCESS_MESSAGE = '执行成功'; 23 | const FAILED_MESSAGE = '未知错误'; 24 | 25 | type RecordIdMatch = Router['scripts']['records']['recordId']; 26 | 27 | const Wrapper = styled.div` 28 | flex: 1; 29 | position: relative; 30 | border-radius: 2px; 31 | background-color: #fff; 32 | box-shadow: hsla(0, 0%, 0%, 0.08) 0 2px 4px 0; 33 | overflow: hidden; 34 | `; 35 | 36 | const Content = styled.div` 37 | height: 100%; 38 | padding: 60px; 39 | overflow: auto; 40 | `; 41 | 42 | const PasswordInput = styled(Input)` 43 | margin-top: 10px; 44 | `; 45 | 46 | export interface RunningRecordViewerViewProps 47 | extends RouteComponentProps {} 48 | 49 | @observer 50 | export class RunningRecordViewerView extends Component< 51 | RunningRecordViewerViewProps 52 | > { 53 | @computed 54 | private get runningRecord(): RunningRecord | undefined { 55 | let { 56 | match: { 57 | $params: {recordId}, 58 | }, 59 | } = this.props; 60 | 61 | return ENTRANCES.scriptService.getRunningRecord(recordId); 62 | } 63 | 64 | @computed 65 | private get runningOutputRendering(): ReactNode { 66 | let record = this.runningRecord; 67 | 68 | if (!record || !record.output) { 69 | return undefined; 70 | } 71 | 72 | let {output, error} = record.output; 73 | 74 | output = output?.trim(); 75 | error = error?.trim(); 76 | 77 | if (!output && !error) { 78 | return undefined; 79 | } 80 | 81 | return ( 82 | <> 83 | 脚本输出 84 | {error && ( 85 | 90 | )} 91 | {output && ( 92 | 97 | )} 98 | > 99 | ); 100 | } 101 | 102 | @computed 103 | private get runningResultRendering(): ReactNode { 104 | let record = this.runningRecord; 105 | 106 | if (!record || !record.result) { 107 | return; 108 | } 109 | 110 | let message: string; 111 | 112 | if (record.result.ok) { 113 | message = record.result.message || SUCCESS_MESSAGE; 114 | } else { 115 | message = record.result.message || FAILED_MESSAGE; 116 | } 117 | 118 | return ( 119 | <> 120 | 执行结果 121 | 122 | {message} 123 | 124 | > 125 | ); 126 | } 127 | 128 | @computed 129 | private get makeflowInfoRendering(): ReactNode { 130 | let record = this.runningRecord; 131 | 132 | if (!record || !record.makeflow) { 133 | return undefined; 134 | } 135 | 136 | return ( 137 | <> 138 | 触发用户(Makeflow) 139 | {record.makeflow.assignee.displayName} 140 | 任务链接(Makeflow) 141 | 142 | 143 | #{record.makeflow.numericId}: {record.makeflow.brief} 144 | 145 | 146 | > 147 | ); 148 | } 149 | 150 | render(): ReactNode { 151 | let record = this.runningRecord; 152 | 153 | if (!record) { 154 | return 脚本未找到; 155 | } 156 | 157 | let executionButtonTitle = record.ranAt ? '重新执行该脚本' : '执行该脚本'; 158 | let icon = record.ranAt ? : ; 159 | 160 | return ( 161 | 162 | 163 | 164 | {record.namespace} : {record.name} 165 | 166 | {this.makeflowInfoRendering} 167 | 触发时间 168 | {new Date(record.createdAt).toLocaleString()} 169 | 使用 Token 170 | {record.triggerTokenLabel ?? '未知 Token'} 171 | 执行时间 172 | 173 | {record.ranAt 174 | ? new Date(record.ranAt).toLocaleString() 175 | : '尚未执行'} 176 | 177 | 178 | 179 | {this.runningOutputRendering} 180 | {this.runningResultRendering} 181 | 182 | 183 | 187 | 188 | {icon} 189 | 190 | 191 | 192 | ); 193 | } 194 | 195 | private onExecuteButtonClick = async (): Promise => { 196 | let record = this.runningRecord; 197 | 198 | if (!record) { 199 | return; 200 | } 201 | 202 | let confirmationMessage: string; 203 | 204 | if (record.ranAt) { 205 | confirmationMessage = '是否再次以相同参数执行该脚本?'; 206 | } else { 207 | confirmationMessage = '请确保已检查脚本参数'; 208 | } 209 | 210 | let { 211 | scriptsMap, 212 | } = await ENTRANCES.scriptService.fetchScriptDefinitionsMap(); 213 | 214 | let definition = scriptsMap 215 | .get(record.namespace) 216 | ?.find(definition => definition.name === record?.name); 217 | 218 | if (!definition) { 219 | void message.error( 220 | '未找到该记录所对应的脚本定义。请检查节点注册信息及对应节点的脚本列表。', 221 | ); 222 | return; 223 | } 224 | 225 | let inputValueObservable = observable.box(undefined); 226 | 227 | let modalContent: ReactNode; 228 | 229 | if (definition.needsPassword) { 230 | modalContent = ( 231 | 232 | {() => { 233 | return ( 234 | 235 | 执行该脚本需要提供一个密码: 236 | { 240 | inputValueObservable.set(value); 241 | }} 242 | /> 243 | 244 | ); 245 | }} 246 | 247 | ); 248 | } else { 249 | modalContent = confirmationMessage; 250 | } 251 | 252 | Modal.confirm({ 253 | title: '确认执行', 254 | content: modalContent, 255 | okText: '执行', 256 | cancelText: '取消', 257 | onOk: async () => { 258 | try { 259 | await ENTRANCES.scriptService.runScriptFromRecords( 260 | record!.id, 261 | inputValueObservable.get(), 262 | ); 263 | void message.success('执行成功'); 264 | } catch (error) { 265 | void message.error('执行失败'); 266 | } 267 | }, 268 | }); 269 | }; 270 | } 271 | 272 | function replaceClearCharacter(text: string): string { 273 | return text.replace(OUTPUT_CLEAR_CHARACTER, SHOWABLE_CLEAR_CHARACTER); 274 | } 275 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@scripts/@script-definition-viewer.tsx: -------------------------------------------------------------------------------- 1 | import {CaretRightFilled} from '@ant-design/icons'; 2 | import {BriefScriptDefinition} from '@makeflow/makescript-agent'; 3 | import {Input, Modal, Table, Tooltip, message} from 'antd'; 4 | import ClipboardJS from 'clipboard'; 5 | import {computed} from 'mobx'; 6 | import {observer} from 'mobx-react'; 7 | import React, {Component, ReactNode} from 'react'; 8 | import styled from 'styled-components'; 9 | import {Dict} from 'tslang'; 10 | 11 | import {ENTRANCES} from '../../@constants'; 12 | 13 | import {ExecuteButton, Item, Label, Title} from './@common'; 14 | 15 | const TOOLTIP_MOUSE_ENTER_DELAY = 0.5; 16 | 17 | const JSON_INDENTATION = 2; 18 | 19 | const RUNNING_LINK_ID = 'running-link'; 20 | 21 | const Wrapper = styled.div` 22 | flex: 1; 23 | background-color: #fff; 24 | box-shadow: hsla(0, 0%, 0%, 0.08) 0 2px 4px 0; 25 | border-radius: 2px; 26 | position: relative; 27 | overflow: hidden; 28 | 29 | .ant-table-cell { 30 | padding: 5px 10px !important; 31 | 32 | pre { 33 | margin: 0; 34 | } 35 | } 36 | `; 37 | 38 | const Content = styled.div` 39 | height: 100%; 40 | width: 100%; 41 | padding: 60px; 42 | overflow: auto; 43 | `; 44 | 45 | const RequiredTip = styled.span` 46 | color: red; 47 | `; 48 | 49 | const ParametersTable = styled.table` 50 | width: 100%; 51 | 52 | tr, 53 | td { 54 | padding: 5px; 55 | } 56 | `; 57 | 58 | export interface ScriptDefinitionViewerProps { 59 | baseURL: string; 60 | namespace: string; 61 | scriptDefinition: BriefScriptDefinition; 62 | } 63 | 64 | @observer 65 | export class ScriptDefinitionViewer extends Component< 66 | ScriptDefinitionViewerProps 67 | > { 68 | private clipboardJS: ClipboardJS | undefined; 69 | 70 | @computed 71 | private get parametersRendering(): ReactNode { 72 | let { 73 | scriptDefinition: {parameters}, 74 | } = this.props; 75 | 76 | if (!parameters || !Object.entries(parameters ?? {}).length) { 77 | return ( 78 | <> 79 | 脚本参数 80 | 该脚本不接受参数 81 | > 82 | ); 83 | } 84 | 85 | return ( 86 | <> 87 | 脚本参数 88 | { 91 | return { 92 | key: name, 93 | name, 94 | definition: ( 95 | 96 | {JSON.stringify(definition, undefined, JSON_INDENTATION)} 97 | 98 | ), 99 | }; 100 | })} 101 | columns={[ 102 | {title: '参数名', dataIndex: 'name', key: 'name'}, 103 | {title: '参数定义', dataIndex: 'definition', key: 'definition'}, 104 | ]} 105 | /> 106 | > 107 | ); 108 | } 109 | 110 | render(): ReactNode { 111 | let {namespace, baseURL, scriptDefinition} = this.props; 112 | 113 | if (!scriptDefinition) { 114 | return 脚本未找到; 115 | } 116 | 117 | let {type, name, manual} = scriptDefinition; 118 | 119 | return ( 120 | 121 | 122 | 123 | {type}: {name} 124 | 125 | 执行链接 126 | 127 | {`${baseURL}/api/script/${namespace}/${name}/enqueue`} 130 | 131 | 需手动执行 132 | {manual ? '是' : '否'} 133 | {this.parametersRendering} 134 | 135 | 139 | 140 | 141 | 142 | 143 | 144 | ); 145 | } 146 | 147 | componentDidMount(): void { 148 | let clipboard = new ClipboardJS(`#${RUNNING_LINK_ID}`, { 149 | target: element => element, 150 | }); 151 | 152 | clipboard.on('success', () => { 153 | void message.success('已成功复制剪切板'); 154 | }); 155 | 156 | clipboard.on('error', async () => { 157 | void message.error(`操作失败,请手动复制`); 158 | }); 159 | 160 | this.clipboardJS = clipboard; 161 | } 162 | 163 | componentWillUnmount(): void { 164 | this.clipboardJS?.destroy(); 165 | } 166 | 167 | private onExecuteButtonClick = (): void => { 168 | let {scriptDefinition, namespace} = this.props; 169 | 170 | if ( 171 | !Object.entries(scriptDefinition.parameters ?? {}).length && 172 | !scriptDefinition.needsPassword 173 | ) { 174 | Modal.confirm({ 175 | title: '手动执行脚本', 176 | content: `即使是需要手动执行的脚本在触发后也会立即执行,确定要手动触发并执行 "${scriptDefinition.name}" 脚本吗?`, 177 | onOk: async () => { 178 | await ENTRANCES.scriptService.runScriptDirectly({ 179 | namespace, 180 | name: scriptDefinition.name, 181 | parameters: {}, 182 | password: undefined, 183 | }); 184 | void message.success('执行成功'); 185 | }, 186 | }); 187 | } else { 188 | let requiredParameterNames = Object.entries(scriptDefinition.parameters) 189 | .filter( 190 | ([, definition]) => 191 | typeof definition === 'object' && definition.required, 192 | ) 193 | .map(([name]) => name); 194 | let parameterResult: Dict = {}; 195 | let password: string | undefined; 196 | 197 | Modal.confirm({ 198 | title: '手动执行脚本', 199 | content: ( 200 | 201 | 202 | {Object.entries(scriptDefinition.parameters).map( 203 | ([name, definition]) => { 204 | let displayName = 205 | typeof definition === 'object' 206 | ? definition.displayName ?? name 207 | : name; 208 | 209 | return ( 210 | 211 | 212 | {displayName} 213 | {requiredParameterNames.includes(name) ? ( 214 | * 215 | ) : undefined} 216 | 217 | 218 | { 220 | parameterResult[name] = value; 221 | }} 222 | /> 223 | 224 | 225 | ); 226 | }, 227 | )} 228 | {scriptDefinition.needsPassword ? ( 229 | 230 | 231 | 执行密码* 232 | 233 | 234 | { 237 | password = value; 238 | }} 239 | /> 240 | 241 | 242 | ) : undefined} 243 | 244 | 245 | ), 246 | onOk: async () => { 247 | if ( 248 | requiredParameterNames.every(name => !!parameterResult[name]) && 249 | (!scriptDefinition.needsPassword || password) 250 | ) { 251 | await ENTRANCES.scriptService.runScriptDirectly({ 252 | namespace, 253 | name: scriptDefinition.name, 254 | parameters: parameterResult, 255 | password, 256 | }); 257 | void message.success('执行成功'); 258 | } else { 259 | void message.error('必填的参数不能为空'); 260 | // Do not close the modal 261 | throw new Error(); 262 | } 263 | }, 264 | }); 265 | } 266 | }; 267 | } 268 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@scripts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './scripts-management-view'; 2 | export * from './running-records-view'; 3 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@scripts/running-records-view.tsx: -------------------------------------------------------------------------------- 1 | import {ArrowLeftOutlined, SettingOutlined} from '@ant-design/icons'; 2 | import {Empty, Tooltip} from 'antd'; 3 | import {Route, RouteComponentProps} from 'boring-router-react'; 4 | import classNames from 'classnames'; 5 | import memorize from 'memorize-decorator'; 6 | import {computed} from 'mobx'; 7 | import {observer} from 'mobx-react'; 8 | import React, {Component, MouseEventHandler, ReactNode} from 'react'; 9 | import styled from 'styled-components'; 10 | 11 | import {ENTRANCES} from '../../@constants'; 12 | import {Router, route} from '../../@routes'; 13 | 14 | import { 15 | EmptyPanel, 16 | NotSelectedPanel, 17 | ScriptBriefItem, 18 | ScriptList, 19 | ScriptListContent, 20 | ScriptListLabel, 21 | Wrapper, 22 | } from './@common'; 23 | import {RunningRecordViewerView} from './@running-record-viewer-view'; 24 | 25 | const TOOLTIP_MOUSE_ENTER_DELAY = 0.5; 26 | 27 | type CommandsHistoryMatch = Router['scripts']['records']; 28 | 29 | const ActionButton = styled.div` 30 | cursor: pointer; 31 | `; 32 | 33 | const ScriptBriefItemNamespace = styled.div` 34 | color: #888; 35 | `; 36 | 37 | const ScriptBriefItemName = styled.div` 38 | overflow: hidden; 39 | text-overflow: ellipsis; 40 | white-space: nowrap; 41 | width: 100%; 42 | `; 43 | 44 | export interface RunningRecordsViewProps 45 | extends RouteComponentProps {} 46 | 47 | @observer 48 | export class RunningRecordsView extends Component { 49 | @computed 50 | private get activeRecordId(): string | undefined { 51 | let { 52 | match: {recordId: recordIdMatch}, 53 | } = this.props; 54 | 55 | if (!recordIdMatch.$matched) { 56 | return undefined; 57 | } 58 | 59 | return recordIdMatch.$params.recordId; 60 | } 61 | 62 | @computed 63 | private get toShowNamespaceInList(): boolean { 64 | return ( 65 | new Set( 66 | ENTRANCES.scriptService.runningRecords.map(record => record.namespace), 67 | ).size > 1 68 | ); 69 | } 70 | 71 | @computed 72 | private get recordsRendering(): ReactNode { 73 | let runningRecords = ENTRANCES.scriptService.runningRecords; 74 | 75 | if (!runningRecords.length) { 76 | return ( 77 | 78 | 79 | 80 | ); 81 | } 82 | 83 | let activeId = this.activeRecordId; 84 | 85 | return ( 86 | 87 | {runningRecords.map(({id, namespace, name, ranAt, output}) => { 88 | let notExecuted = !ranAt; 89 | let hasError = !!output?.error; 90 | 91 | let tooltipMessage = ''; 92 | 93 | if (notExecuted) { 94 | tooltipMessage = '该脚本暂未执行'; 95 | } 96 | 97 | if (hasError) { 98 | tooltipMessage = '执行结果有错误'; 99 | } 100 | 101 | return ( 102 | 108 | 116 | {this.toShowNamespaceInList ? ( 117 | 118 | {namespace} 119 | 120 | ) : undefined} 121 | {/* TODO: Display name ? */} 122 | {name} 123 | 124 | 125 | ); 126 | })} 127 | 128 | ); 129 | } 130 | 131 | @computed 132 | private get notSelectedPanelRendering(): ReactNode { 133 | let recordSelected = this.props.match.recordId.$matched; 134 | 135 | if (recordSelected) { 136 | return undefined; 137 | } 138 | 139 | return ( 140 | 141 | 145 | 146 | ); 147 | } 148 | 149 | render(): ReactNode { 150 | let {match} = this.props; 151 | 152 | return ( 153 | 154 | 155 | 156 | 160 | 161 | 162 | 脚本执行记录 163 | 167 | 168 | 169 | 170 | 171 | 172 | {this.recordsRendering} 173 | 174 | 175 | {this.notSelectedPanelRendering} 176 | 177 | ); 178 | } 179 | 180 | private onBackButtonClick = (): void => { 181 | route.$push(); 182 | }; 183 | 184 | private onManageButtonClick = (): void => { 185 | route.scripts.management.$push(); 186 | }; 187 | 188 | @memorize() 189 | private getOnRunningRecordClick(id: string): MouseEventHandler { 190 | return () => { 191 | let {match} = this.props; 192 | 193 | match.recordId.$push({recordId: id}); 194 | }; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@scripts/scripts-management-view.tsx: -------------------------------------------------------------------------------- 1 | import {ArrowLeftOutlined} from '@ant-design/icons'; 2 | import {BriefScriptDefinition} from '@makeflow/makescript-agent'; 3 | import {Empty, Menu} from 'antd'; 4 | import SubMenu from 'antd/lib/menu/SubMenu'; 5 | import {Route, RouteComponentProps} from 'boring-router-react'; 6 | import {computed, observable} from 'mobx'; 7 | import {observer} from 'mobx-react'; 8 | import React, {Component, ReactElement, ReactNode} from 'react'; 9 | import styled from 'styled-components'; 10 | 11 | import {ENTRANCES} from '../../@constants'; 12 | import {Router, route} from '../../@routes'; 13 | 14 | import { 15 | EmptyPanel, 16 | NotSelectedPanel, 17 | ScriptList, 18 | ScriptListLabel, 19 | Wrapper, 20 | } from './@common'; 21 | import {ScriptDefinitionViewer} from './@script-definition-viewer'; 22 | 23 | type ScriptsManagementMatch = Router['scripts']['management']; 24 | 25 | const BackButton = styled.div` 26 | cursor: pointer; 27 | `; 28 | 29 | const ViewerPanel = styled.div` 30 | flex: 1; 31 | display: flex; 32 | flex-direction: column; 33 | `; 34 | 35 | const ScriptsWrapper = styled.div` 36 | overflow-y: hidden; 37 | 38 | &:hover { 39 | overflow-y: auto; 40 | } 41 | 42 | .ant-menu-item { 43 | padding-left: 30px !important; 44 | } 45 | `; 46 | 47 | export interface ScriptsManagementViewProps 48 | extends RouteComponentProps {} 49 | 50 | @observer 51 | export class ScriptsManagementView extends Component< 52 | ScriptsManagementViewProps 53 | > { 54 | @observable 55 | private scriptDefinitionsMap: 56 | | Map 57 | | undefined; 58 | 59 | @observable 60 | private baseURL: string | undefined; 61 | 62 | @computed 63 | private get activeScriptName(): [string, string] | undefined { 64 | let { 65 | match: { 66 | namespace: {scriptName: scriptNameMatch}, 67 | }, 68 | } = this.props; 69 | 70 | if (!scriptNameMatch.$matched) { 71 | return undefined; 72 | } 73 | 74 | return [ 75 | scriptNameMatch.$params.namespace, 76 | scriptNameMatch.$params.scriptName, 77 | ]; 78 | } 79 | 80 | @computed 81 | private get activeScriptDefinition(): BriefScriptDefinition | undefined { 82 | let activeScriptName = this.activeScriptName; 83 | 84 | if (!activeScriptName) { 85 | return undefined; 86 | } 87 | 88 | return this.scriptDefinitionsMap 89 | ?.get(activeScriptName[0]) 90 | ?.find( 91 | scriptDefinition => scriptDefinition.name === activeScriptName![1], 92 | ); 93 | } 94 | 95 | private scriptDefinitionViewerView = observer( 96 | (): ReactElement => { 97 | let activeScriptDefinition = this.activeScriptDefinition; 98 | let activeScriptName = this.activeScriptName; 99 | let baseURL = this.baseURL; 100 | 101 | if (!activeScriptName || !activeScriptDefinition || !baseURL) { 102 | return <>>; 103 | } 104 | 105 | return ( 106 | 111 | ); 112 | }, 113 | ); 114 | 115 | @computed 116 | private get scriptsListRendering(): ReactNode { 117 | let scriptDefinitionsMap = this.scriptDefinitionsMap; 118 | 119 | if (!scriptDefinitionsMap) { 120 | return ( 121 | 122 | 123 | 124 | ); 125 | } 126 | 127 | if (!scriptDefinitionsMap.size) { 128 | return ( 129 | 130 | 131 | 132 | ); 133 | } 134 | 135 | let [activeNamespace, activeScriptName] = this.activeScriptName ?? []; 136 | 137 | return ( 138 | 139 | { 144 | let {match} = this.props; 145 | 146 | let [namespace, scriptName] = String(key).split(':'); 147 | 148 | match.namespace.scriptName.$push({ 149 | namespace, 150 | scriptName, 151 | }); 152 | }} 153 | > 154 | {Array.from(scriptDefinitionsMap).map( 155 | ([namespace, scriptDefinitions]) => ( 156 | 157 | {scriptDefinitions.map(({name, displayName}) => ( 158 | 159 | {displayName} 160 | 161 | ))} 162 | 163 | ), 164 | )} 165 | 166 | 167 | ); 168 | } 169 | 170 | @computed 171 | private get notSelectedPanelRendering(): ReactNode { 172 | let commandSelected = this.props.match.namespace.scriptName.$matched; 173 | 174 | if (commandSelected) { 175 | return undefined; 176 | } 177 | 178 | return ( 179 | 180 | 184 | 185 | ); 186 | } 187 | 188 | render(): ReactNode { 189 | let {match} = this.props; 190 | 191 | return ( 192 | 193 | 194 | 195 | 199 | 200 | 201 | 脚本列表 202 | 203 | {this.scriptsListRendering} 204 | 205 | 206 | 210 | {this.notSelectedPanelRendering} 211 | 212 | 213 | ); 214 | } 215 | 216 | componentDidMount(): void { 217 | ENTRANCES.scriptService 218 | .fetchScriptDefinitionsMap() 219 | .then(({scriptsMap, baseURL}) => { 220 | this.scriptDefinitionsMap = scriptsMap; 221 | this.baseURL = baseURL; 222 | }) 223 | .catch(console.error); 224 | } 225 | 226 | private onBackButtonClick = (): void => { 227 | route.scripts.records.$push(); 228 | }; 229 | } 230 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@status/index.ts: -------------------------------------------------------------------------------- 1 | export * from './status-view'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@status/status-view.tsx: -------------------------------------------------------------------------------- 1 | import {Empty, Tooltip, message} from 'antd'; 2 | import {Link, RouteComponentProps} from 'boring-router-react'; 3 | import ClipboardJS from 'clipboard'; 4 | import {observable} from 'mobx'; 5 | import {observer} from 'mobx-react'; 6 | import React, {Component, ReactNode} from 'react'; 7 | import styled from 'styled-components'; 8 | 9 | import {Button, Card} from '../../@components'; 10 | import {ENTRANCES} from '../../@constants'; 11 | import {Router, route} from '../../@routes'; 12 | import {AgentsStatus} from '../../@services'; 13 | 14 | const JOIN_LINK_ID = 'join-link'; 15 | const JOIN_COMMAND_ID = 'join-command'; 16 | 17 | type StatusMatch = Router['status']; 18 | 19 | const Wrapper = styled.div` 20 | display: flex; 21 | width: 100vw; 22 | min-height: 100vh; 23 | justify-content: center; 24 | align-items: center; 25 | 26 | ${Card.Wrapper} { 27 | display: flex; 28 | flex-direction: column; 29 | width: 500px; 30 | } 31 | `; 32 | 33 | export const Label = styled.div` 34 | line-height: 16px; 35 | color: hsl(0, 0%, 40%); 36 | margin: 16px 0 10px 0; 37 | font-size: 12px; 38 | `; 39 | 40 | export const Item = styled.div` 41 | display: flex; 42 | font-size: 14px; 43 | `; 44 | 45 | const RegisteredAgentWrapper = styled.div` 46 | display: flex; 47 | background-color: hsl(101, 51%, 58%); 48 | color: #fff; 49 | padding: 10px; 50 | margin: 10px; 51 | border-radius: 10px; 52 | `; 53 | 54 | const RegisteredAgentNamespace = styled.div` 55 | flex-grow: 1; 56 | `; 57 | 58 | const RegisteredAgentScriptQuantity = styled.div``; 59 | 60 | const JoinLink = styled.div` 61 | cursor: pointer; 62 | `; 63 | 64 | const BackButton = styled(Button)` 65 | width: 100%; 66 | margin-top: 20px; 67 | `; 68 | 69 | export interface StatusProps extends RouteComponentProps {} 70 | 71 | @observer 72 | export class StatusView extends Component { 73 | @observable 74 | private status: AgentsStatus | undefined; 75 | 76 | private disposes: (() => void)[] = []; 77 | 78 | render(): ReactNode { 79 | let status = this.status; 80 | 81 | return ( 82 | 83 | 84 | {status ? ( 85 | <> 86 | 节点加入命令 87 | 88 | 89 | 90 | makescript-agent --server-url {status.joinLink} 91 | 92 | 93 | 94 | 节点加入链接 95 | 96 | 97 | {status.joinLink} 98 | 99 | 100 | 已注册节点 101 | {status.registeredAgents.length ? ( 102 | status.registeredAgents.map(registeredAgent => ( 103 | 107 | 108 | 109 | {registeredAgent.namespace} 110 | 111 | 112 | {registeredAgent.scriptQuantity} 113 | 114 | 115 | 116 | )) 117 | ) : ( 118 | 122 | 没有已注册的节点,请到{' '} 123 | 127 | GitHub 128 | {' '} 129 | 查看如何使用节点。 130 | > 131 | } 132 | /> 133 | )} 134 | > 135 | ) : ( 136 | 137 | )} 138 | 139 | 140 | 返回 141 | 142 | 143 | 144 | ); 145 | } 146 | 147 | componentDidMount(): void { 148 | ENTRANCES.scriptService 149 | .fetchStatus() 150 | .then(status => { 151 | this.status = status; 152 | 153 | setTimeout(() => { 154 | let joinLinkClipboard = new ClipboardJS(`#${JOIN_LINK_ID}`, { 155 | target: element => element, 156 | }); 157 | 158 | let joinCommandClipboard = new ClipboardJS(`#${JOIN_COMMAND_ID}`, { 159 | target: element => element, 160 | }); 161 | 162 | let successHandler = (): void => { 163 | void message.success('已成功复制剪切板'); 164 | }; 165 | 166 | let errorHandler = (): void => { 167 | void message.error(`操作失败,请手动复制`); 168 | }; 169 | 170 | joinLinkClipboard.on('success', successHandler); 171 | joinLinkClipboard.on('error', errorHandler); 172 | 173 | joinCommandClipboard.on('success', successHandler); 174 | joinCommandClipboard.on('error', errorHandler); 175 | 176 | this.disposes.push(() => joinLinkClipboard.destroy()); 177 | this.disposes.push(() => joinCommandClipboard.destroy()); 178 | }); 179 | }) 180 | .catch(console.error); 181 | } 182 | 183 | componentWillUnmount(): void { 184 | for (let dispose of this.disposes) { 185 | dispose(); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@tokens/@generate-modal.tsx: -------------------------------------------------------------------------------- 1 | import {Input, Modal, message} from 'antd'; 2 | import ClipboardJS from 'clipboard'; 3 | import {action, observable, runInAction} from 'mobx'; 4 | import {observer} from 'mobx-react'; 5 | import React, {ChangeEvent, Component, ReactNode} from 'react'; 6 | 7 | import {ENTRANCES} from '../../@constants'; 8 | 9 | export interface GenerateModalProps { 10 | visible: boolean; 11 | onCancel(): void; 12 | } 13 | 14 | @observer 15 | export class GenerateModal extends Component { 16 | @observable 17 | private label = ''; 18 | 19 | render(): ReactNode { 20 | let {visible, onCancel} = this.props; 21 | 22 | return ( 23 | 34 | 39 | 40 | ); 41 | } 42 | 43 | private onLabelInputChange = ({ 44 | currentTarget: {value}, 45 | }: ChangeEvent): void => { 46 | this.setLabel(value); 47 | }; 48 | 49 | private onGenerateToken = async (): Promise => { 50 | try { 51 | let label = this.label; 52 | 53 | if (!label) { 54 | void message.error('请输入备注'); 55 | return; 56 | } 57 | 58 | let token = await ENTRANCES.tokenService.generateToken(label); 59 | 60 | runInAction(() => { 61 | this.label = ''; 62 | }); 63 | 64 | Modal.confirm({ 65 | icon: <>>, 66 | title: '生成成功', 67 | content: ( 68 | <> 69 | 已成功生成新 Token: 70 | {token} 71 | > 72 | ), 73 | cancelText: '知道了', 74 | okText: '复制到剪切板', 75 | // '_' is to prevent closing 76 | onOk: _ => {}, 77 | okButtonProps: { 78 | id: 'copy-button', 79 | }, 80 | }); 81 | 82 | let clipboard = new ClipboardJS('#copy-button', { 83 | target: () => document.querySelector('#token-content-to-copy')!, 84 | }); 85 | 86 | clipboard.on('success', async () => { 87 | void message.success('已复制到剪切板'); 88 | }); 89 | clipboard.on('error', async () => { 90 | void message.error('复制失败, 请手动复制'); 91 | }); 92 | } catch (error) { 93 | void message.error(`操作失败${error.message}`); 94 | } 95 | 96 | let {onCancel} = this.props; 97 | 98 | onCancel(); 99 | }; 100 | 101 | @action 102 | private setLabel(label: string): void { 103 | this.label = label; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@tokens/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tokens-view'; 2 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/@tokens/tokens-view.tsx: -------------------------------------------------------------------------------- 1 | import {CloseOutlined} from '@ant-design/icons'; 2 | import {List, Modal, Tooltip, message} from 'antd'; 3 | import {RouteComponentProps} from 'boring-router-react'; 4 | import memorize from 'memorize-decorator'; 5 | import {action, computed, observable} from 'mobx'; 6 | import {observer} from 'mobx-react'; 7 | import React, {Component, MouseEventHandler, ReactNode} from 'react'; 8 | import styled from 'styled-components'; 9 | 10 | import {ActiveToken} from '../../../program/types'; 11 | import {Button, Card} from '../../@components'; 12 | import {ENTRANCES} from '../../@constants'; 13 | import {Router, route} from '../../@routes'; 14 | 15 | import {GenerateModal} from './@generate-modal'; 16 | 17 | const TOOLTIP_MOUSE_ENTER_DELAY = 0.5; 18 | 19 | const Wrapper = styled.div` 20 | box-sizing: border-box; 21 | width: 100vw; 22 | min-height: 100vh; 23 | padding: 50px 0; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | 28 | ${Card.Wrapper} { 29 | width: 420px; 30 | } 31 | 32 | ${Button}, 33 | .token-list { 34 | margin-top: 20px; 35 | } 36 | `; 37 | 38 | const DeactivateButton = styled.div` 39 | width: 40px; 40 | height: 40px; 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | color: hsl(0, 0%, 40%); 45 | cursor: pointer; 46 | `; 47 | 48 | const TokenItem = styled.div` 49 | height: 40px; 50 | padding-left: 15px; 51 | display: flex; 52 | align-items: center; 53 | justify-content: space-between; 54 | border-radius: 2px; 55 | color: hsl(0, 0%, 20%); 56 | 57 | ${DeactivateButton} { 58 | display: none; 59 | } 60 | 61 | &:hover { 62 | background-color: hsl(221, 100%, 97%); 63 | 64 | ${DeactivateButton} { 65 | display: flex; 66 | } 67 | } 68 | `; 69 | 70 | const TokenList = styled(List)` 71 | max-height: 230px; 72 | overflow-y: auto; 73 | `; 74 | 75 | export interface TokensViewProps 76 | extends RouteComponentProps {} 77 | 78 | @observer 79 | export class TokensView extends Component { 80 | @observable 81 | private generateModalVisible = false; 82 | 83 | @computed 84 | private get tokens(): ActiveToken[] { 85 | return ENTRANCES.tokenService.tokens.sort( 86 | (a, b) => b.createdAt - a.createdAt, 87 | ); 88 | } 89 | 90 | @computed 91 | private get tokensRendering(): ReactNode { 92 | return ( 93 | ( 98 | 99 | {token.label} 100 | 105 | 108 | 109 | 110 | 111 | 112 | )} 113 | /> 114 | ); 115 | } 116 | 117 | render(): ReactNode { 118 | return ( 119 | 120 | 121 | {this.tokensRendering} 122 | 新建 123 | 返回 124 | 125 | 129 | 130 | ); 131 | } 132 | 133 | private onGenerateButtonClick = async (): Promise => { 134 | this.setGenerateModalVisible(true); 135 | }; 136 | 137 | private onGenerateModalCancel = (): void => { 138 | this.setGenerateModalVisible(false); 139 | }; 140 | 141 | private onBackButtonClick = (): void => { 142 | route.$push(); 143 | }; 144 | 145 | @memorize() 146 | private getOnDeactivateTokenButtonClick({ 147 | id, 148 | label, 149 | }: ActiveToken): MouseEventHandler { 150 | return async () => { 151 | Modal.confirm({ 152 | title: '停用 Token', 153 | content: `是否确定停用 ${label} ?`, 154 | onOk: async () => { 155 | try { 156 | await ENTRANCES.tokenService.disableToken(id); 157 | void message.success('停用成功'); 158 | } catch (error) { 159 | void message.error('停用失败'); 160 | } 161 | }, 162 | }); 163 | }; 164 | } 165 | 166 | @action 167 | private setGenerateModalVisible(visible: boolean): void { 168 | this.generateModalVisible = visible; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /packages/makescript/src/web/@views/app.tsx: -------------------------------------------------------------------------------- 1 | import {Route} from 'boring-router-react'; 2 | import {observer} from 'mobx-react'; 3 | import React, {Component, ReactNode} from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | import {route} from '../@routes'; 7 | 8 | import {HomeView} from './@home'; 9 | import {InitializeView} from './@initialize'; 10 | import {LoginView} from './@login'; 11 | import {MakeflowLoginView, MakeflowView} from './@makeflow'; 12 | import {RunningRecordsView, ScriptsManagementView} from './@scripts'; 13 | import {StatusView} from './@status'; 14 | import {TokensView} from './@tokens'; 15 | 16 | const Wrapper = styled.div` 17 | background-color: hsl(221, 55%, 97%); 18 | `; 19 | 20 | @observer 21 | export class App extends Component { 22 | render(): ReactNode { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/makescript/src/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MakeScript 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/makescript/src/web/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | color: #333; 8 | } 9 | 10 | .ant-modal-wrapper .ant-modal-header { 11 | border: 0; 12 | } 13 | 14 | .ant-modal-wrapper .ant-modal-footer { 15 | border: 0; 16 | } 17 | -------------------------------------------------------------------------------- /packages/makescript/src/web/main.tsx: -------------------------------------------------------------------------------- 1 | import 'antd/dist/antd.css'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import './main.css'; 7 | 8 | import {ENTRANCES} from './@constants'; 9 | import {App} from './@views/app'; 10 | 11 | main().catch(console.error); 12 | 13 | async function main(): Promise { 14 | await ENTRANCES.ready; 15 | 16 | await ENTRANCES.authorizationService.check(); 17 | 18 | ReactDOM.render(, document.getElementById('app')); 19 | } 20 | -------------------------------------------------------------------------------- /packages/makescript/src/web/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'highlight.js/lib/highlight'; 2 | 3 | declare module 'highlight.js/lib/languages/json'; 4 | -------------------------------------------------------------------------------- /packages/makescript/src/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@mufan/code/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "../../.bld-cache/web", 6 | 7 | "experimentalDecorators": true, 8 | "target": "es5", 9 | "lib": ["esnext", "dom", "dom.iterable"], 10 | "jsx": "react" 11 | }, 12 | "references": [{"path": "../program"}] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "packages/makescript-agent/src/program" 5 | }, 6 | { 7 | "path": "packages/makescript/src/program" 8 | }, 9 | { 10 | "path": "packages/makescript/src/web" 11 | } 12 | ], 13 | "files": [] 14 | } 15 | --------------------------------------------------------------------------------
96 | {JSON.stringify(definition, undefined, JSON_INDENTATION)} 97 |