├── _config.yml ├── .gitignore ├── examples └── inventor-thumbnail │ ├── Clutch_Gear_20t.ipt │ ├── ThumbnailPlugin.bundle.zip │ ├── setup-pipeline.sh │ ├── setup-pipeline.ps1 │ ├── README.md │ ├── run-pipeline.sh │ ├── run-pipeline.ps1 │ ├── run-pipeline.js │ └── setup-pipeline.js ├── tests └── forge-dm.bats ├── .travis.yml ├── tools └── autocomplete-bash.sh ├── src ├── common.js ├── helpers │ └── derivativePersistence.js ├── forge-md.js ├── forge-dm.js └── forge-da.js ├── package.json └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | node_modules/ 3 | .vscode/ 4 | output/ 5 | *.log 6 | .DS_Store 7 | Thumbs.db 8 | -------------------------------------------------------------------------------- /examples/inventor-thumbnail/Clutch_Gear_20t.ipt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk-Forge/forge-cli-utils/HEAD/examples/inventor-thumbnail/Clutch_Gear_20t.ipt -------------------------------------------------------------------------------- /examples/inventor-thumbnail/ThumbnailPlugin.bundle.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Autodesk-Forge/forge-cli-utils/HEAD/examples/inventor-thumbnail/ThumbnailPlugin.bundle.zip -------------------------------------------------------------------------------- /tests/forge-dm.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "Forge credentials available" { 4 | [ "$FORGE_CLIENT_ID" != "" ] 5 | [ "$FORGE_CLIENT_SECRET" != "" ] 6 | } 7 | 8 | @test "Show help" { 9 | result1="$(forge-dm)" 10 | [ "$result1" != "" ] 11 | result2="$(forge-dm -h)" 12 | [ "$result1" == "$result2" ] 13 | } 14 | 15 | @test "List buckets" { 16 | result1="$(forge-dm list-buckets | jq -r '.[] | .bucketKey')" 17 | [ "$result1" != "" ] 18 | result2="$(forge-dm lb --short)" 19 | [ "$result1" == "$result2" ] 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | 5 | before_install: 6 | - sudo add-apt-repository ppa:duggan/bats -y 7 | - sudo apt-get update -y 8 | - sudo apt-get install bats jq -y 9 | 10 | script: 11 | - npm run build 12 | - npm link 13 | - bats tests 14 | 15 | deploy: 16 | - provider: npm 17 | email: $NPM_EMAIL 18 | api_key: $NPM_AUTH_TOKEN 19 | on: 20 | tags: true 21 | - provider: releases 22 | api_key: $GITHUB_TOKEN 23 | file_glob: true 24 | file: bin/*.zip 25 | skip_cleanup: true 26 | on: 27 | tags: true 28 | -------------------------------------------------------------------------------- /tools/autocomplete-bash.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | configure () { 4 | echo "Configuring bash auto-completion for $1" 5 | DIR=$(dirname $BASH_SOURCE) 6 | COMMANDS=$(node $DIR/../src/$1.js -h | sed -e '1,/Commands/d' | awk -F '|' '{ gsub(/ +/, "", $1); print $1 }' | tr '\n' ' ') 7 | complete -W "$COMMANDS" $1 8 | } 9 | 10 | if [[ -z "$FORGE_CLIENT_ID" ]]; then 11 | FORGE_CLIENT_ID=foo 12 | fi 13 | if [[ -z "$FORGE_CLIENT_SECRET" ]]; then 14 | FORGE_CLIENT_SECRET=bar 15 | fi 16 | 17 | configure "forge-da" 18 | configure "forge-dm" 19 | configure "forge-md" 20 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | function log(result) { 2 | switch (typeof result) { 3 | case 'object': 4 | console.log(JSON.stringify(result, null, 4)); 5 | break; 6 | default: 7 | console.log(result); 8 | break; 9 | } 10 | } 11 | 12 | function warn(result) { 13 | switch (typeof result) { 14 | case 'object': 15 | console.warn(JSON.stringify(result, null, 4)); 16 | break; 17 | default: 18 | console.warn(result); 19 | break; 20 | } 21 | } 22 | 23 | function error(err) { 24 | if (err instanceof Error) { 25 | console.error(err.message); 26 | } else { 27 | console.error(err); 28 | } 29 | process.exit(1); 30 | } 31 | 32 | module.exports = { 33 | log, 34 | warn, 35 | error 36 | }; 37 | -------------------------------------------------------------------------------- /examples/inventor-thumbnail/setup-pipeline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, and FORGE_BUCKET must be set before running this script. 4 | 5 | set -e # Exit on any error 6 | 7 | FORGE_DA_SCRIPT="node ../../src/forge-da.js" 8 | 9 | APPBUNDLE_NAME=TestBundle 10 | APPBUNDLE_ALIAS=dev 11 | APPBUNDLE_FILE=./ThumbnailPlugin.bundle.zip 12 | APPBUNDLE_ENGINE=Autodesk.Inventor+24 13 | 14 | ACTIVITY_NAME=TestActivity 15 | ACTIVITY_ALIAS=dev 16 | 17 | # Create or update an appbundle 18 | echo "Creating/updating appbundle" 19 | $FORGE_DA_SCRIPT create-appbundle $APPBUNDLE_NAME $APPBUNDLE_FILE $APPBUNDLE_ENGINE "Bundle for testing Forge CLI tool" --update 20 | 21 | # Create or update an appbundle alias 22 | APPBUNDLE_VERSION=$($FORGE_DA_SCRIPT list-appbundle-versions $APPBUNDLE_NAME --short | tail -n 1) 23 | echo "Creating/updating appbundle alias" 24 | $FORGE_DA_SCRIPT create-appbundle-alias $APPBUNDLE_ALIAS $APPBUNDLE_NAME $APPBUNDLE_VERSION --update 25 | 26 | # Create or update an activity 27 | echo "Creating/updating activity" 28 | $FORGE_DA_SCRIPT create-activity $ACTIVITY_NAME $APPBUNDLE_NAME $APPBUNDLE_ALIAS $APPBUNDLE_ENGINE --input PartFile --output Thumbnail --output-local-name thumbnail.bmp --update 29 | 30 | # Create or update an activity alias 31 | ACTIVITY_VERSION=$($FORGE_DA_SCRIPT list-activity-versions $ACTIVITY_NAME --short | tail -n 1) 32 | echo "Creating/updating activity alias" 33 | $FORGE_DA_SCRIPT create-activity-alias $ACTIVITY_ALIAS $ACTIVITY_NAME $ACTIVITY_VERSION --update 34 | -------------------------------------------------------------------------------- /examples/inventor-thumbnail/setup-pipeline.ps1: -------------------------------------------------------------------------------- 1 | # FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, and FORGE_BUCKET must be set before running this script. 2 | 3 | function Request-DA { 4 | $command = $args[0] 5 | $result = Invoke-Expression "node ..\..\src\forge-da.js $command" 6 | if ($LASTEXITCODE -ne 0) { 7 | Write-Error "forge-da command failed: $command" -ErrorAction "Stop" 8 | } 9 | return $result 10 | } 11 | 12 | $appbundle_name = "MyTestBundle" 13 | $appbundle_alias = "dev" 14 | $appbundle_file = ".\ThumbnailPlugin.bundle.zip" 15 | $appbundle_engine = "Autodesk.Inventor+24" 16 | 17 | $activity_name = "MyTestActivity" 18 | $activity_alias = "dev" 19 | 20 | # Create or update an appbundle 21 | Write-Host "Creating/updating an appbundle $appbundle_name" 22 | Request-DA "create-appbundle $appbundle_name $appbundle_file $appbundle_engine --update" 23 | 24 | # Create or update an appbundle alias 25 | Write-Host "Creating/updating an appbundle alias $appbundle_alias" 26 | $appbundle_version = $result | Select-Object -Last 1 27 | Request-DA "create-appbundle-alias $appbundle_alias $appbundle_name $appbundle_version --update" 28 | 29 | # Create or update an activity 30 | Write-Host "Creating/updating an activity $activity_name" 31 | Request-DA "create-activity $activity_name $appbundle_name $appbundle_alias $appbundle_engine --input PartFile --output Thumbnail --output-local-name thumbnail.bmp --update" 32 | 33 | # Create or update an activity alias 34 | Write-Host "Creating/updating an activity alias $activity_alias" 35 | $activity_version = $result | Select-Object -Last 1 36 | Request-DA "create-activity-alias $activity_alias $activity_name $activity_version --update" 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "forge-cli-utils", 3 | "version": "1.3.1", 4 | "description": "Command line tools for Autodesk Forge services.", 5 | "author": "Petr Broz ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "autodesk", 9 | "forge", 10 | "cli" 11 | ], 12 | "bin": { 13 | "forge-dm": "src/forge-dm.js", 14 | "forge-da": "src/forge-da.js", 15 | "forge-md": "src/forge-md.js" 16 | }, 17 | "scripts": { 18 | "build": "npm run build:win && npm run build:macos && npm run build:linux", 19 | "build:win": "export FORGE_CLI_ARCH=node10-win-x64 && npm run pack && zip -jr bin/forge-cli-utils.$FORGE_CLI_ARCH.zip bin/$FORGE_CLI_ARCH/* && rm -rf bin/$FORGE_CLI_ARCH", 20 | "build:macos": "export FORGE_CLI_ARCH=node10-macos-x64 && npm run pack && zip -jr bin/forge-cli-utils.$FORGE_CLI_ARCH.zip bin/$FORGE_CLI_ARCH/* && rm -rf bin/$FORGE_CLI_ARCH", 21 | "build:linux": "export FORGE_CLI_ARCH=node10-linux-x64 && npm run pack && zip -jr bin/forge-cli-utils.$FORGE_CLI_ARCH.zip bin/$FORGE_CLI_ARCH/* && rm -rf bin/$FORGE_CLI_ARCH", 22 | "pack": "npm run pack:dm && npm run pack:md && npm run pack:da", 23 | "pack:dm": "pkg src/forge-dm.js --targets $FORGE_CLI_ARCH --out-path bin/$FORGE_CLI_ARCH", 24 | "pack:md": "pkg src/forge-md.js --targets $FORGE_CLI_ARCH --out-path bin/$FORGE_CLI_ARCH", 25 | "pack:da": "pkg src/forge-da.js --targets $FORGE_CLI_ARCH --out-path bin/$FORGE_CLI_ARCH" 26 | }, 27 | "dependencies": { 28 | "commander": "^2.20.3", 29 | "forge-server-utils": "^8.2.2", 30 | "form-data": "^2.5.1", 31 | "fs-extra": "^8.1.0", 32 | "inquirer": "^6.5.2", 33 | "jszip": "^3.4.0", 34 | "ora": "^4.0.4" 35 | }, 36 | "devDependencies": { 37 | "pkg": "^4.4.8" 38 | }, 39 | "engines": { 40 | "node": ">=10.15.3" 41 | }, 42 | "homepage": "https://petrbroz.github.io/forge-cli-utils/", 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/petrbroz/forge-cli-utils" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/petrbroz/forge-cli-utils/issues" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/inventor-thumbnail/README.md: -------------------------------------------------------------------------------- 1 | # inventor-thumbnail 2 | 3 | Sample scripts for creating and running a Forge Design Automation 4 | pipeline that generates a thumbnail from an Autodesk Inventor file. 5 | 6 | The _setup-pipeline_ script: 7 | - creates (or updates) an app bundle for Autodesk Inventor engine, 8 | using a pre-packaged Inventor plugin _ThumbnailPlugin.bundle.zip_ 9 | - creates (or updates) an alias pointing to the latest version of the app bundle 10 | - creates (or updates) an activity with the Inventor plugin that 11 | takes an Inventor file as its input, and generates an image file 12 | on its output 13 | - creates (or updates) an alias pointing to the latest version of the activity 14 | 15 | The _run-pipeline_ script: 16 | - uploads an example Inventor file included with this sample 17 | - creates signed URLs for the input Inventor file and the output thumbnail 18 | - creates a work item for the activity defined during the setup 19 | - waits for the work item to complete 20 | - downloads the thumbnail image to _output_ subfolder 21 | 22 | ## Running 23 | 24 | ### On macOS/linux 25 | 26 | #### Bash script 27 | 28 | ```bash 29 | export FORGE_CLIENT_ID= 30 | export FORGE_CLIENT_SECRET= 31 | export FORGE_BUCKET= 32 | ./setup-pipeline.sh 33 | ./run-pipeline.sh 34 | ``` 35 | 36 | #### Node.js script 37 | 38 | ```bash 39 | export FORGE_CLIENT_ID= 40 | export FORGE_CLIENT_SECRET= 41 | export FORGE_BUCKET= 42 | node setup-pipeline.js 43 | node run-pipeline.js 44 | ``` 45 | 46 | ### On Windows 47 | 48 | #### PowerShell script 49 | 50 | ```powershell 51 | $env:FORGE_CLIENT_ID = "" 52 | $env:FORGE_CLIENT_SECRET = "" 53 | $env:FORGE_BUCKET = "" 54 | .\setup-pipeline.ps1 55 | .\run-pipeline.ps1 56 | ``` 57 | 58 | #### Node.js script 59 | 60 | ```powershell 61 | $env:FORGE_CLIENT_ID = "" 62 | $env:FORGE_CLIENT_SECRET = "" 63 | $env:FORGE_BUCKET = "" 64 | node setup-pipeline.js 65 | node run-pipeline.js 66 | ``` 67 | -------------------------------------------------------------------------------- /examples/inventor-thumbnail/run-pipeline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, and FORGE_BUCKET must be set before running this script. 4 | 5 | set -e # Exit on any error 6 | 7 | FORGE_DM_SCRIPT="node ../../src/forge-dm.js" 8 | FORGE_DA_SCRIPT="node ../../src/forge-da.js" 9 | 10 | ACTIVITY_NAME=TestActivity 11 | ACTIVITY_ALIAS=dev 12 | 13 | INPUT_FILE_PATH=./Clutch_Gear_20t.ipt 14 | INPUT_OBJECT_KEY=input.ipt 15 | THUMBNAIL_OBJECT_KEY=thumbnail.bmp 16 | 17 | # If it does not exist, create a data bucket 18 | if [ $($FORGE_DM_SCRIPT list-buckets --short | grep $FORGE_BUCKET | wc -l) -eq "0" ] # TODO: use better matching 19 | then 20 | echo "Creating a bucket $FORGE_BUCKET" 21 | $FORGE_DM_SCRIPT create-bucket $FORGE_BUCKET 22 | fi 23 | 24 | # Upload Inventor file and create a placeholder for the output thumbnail 25 | echo "Preparing input/output files" 26 | $FORGE_DM_SCRIPT upload-object $INPUT_FILE_PATH application/octet-stream $FORGE_BUCKET $INPUT_OBJECT_KEY 27 | mkdir -p output 28 | touch output/thumbnail.bmp 29 | $FORGE_DM_SCRIPT upload-object output/thumbnail.bmp image/bmp $FORGE_BUCKET $THUMBNAIL_OBJECT_KEY 30 | 31 | # Generate signed URLs for all input and output files 32 | echo "Creating signed URLs" 33 | INPUT_FILE_SIGNED_URL=$($FORGE_DM_SCRIPT create-signed-url $FORGE_BUCKET $INPUT_OBJECT_KEY --access read --short) 34 | THUMBNAIL_SIGNED_URL=$($FORGE_DM_SCRIPT create-signed-url $FORGE_BUCKET $THUMBNAIL_OBJECT_KEY --access readwrite --short) 35 | 36 | # Create work item and poll the results 37 | echo "Creating work item" 38 | WORKITEM_ID=$($FORGE_DA_SCRIPT create-workitem $ACTIVITY_NAME $ACTIVITY_ALIAS --input PartFile --input-url $INPUT_FILE_SIGNED_URL --output Thumbnail --output-url $THUMBNAIL_SIGNED_URL --short) 39 | echo "Waiting for work item $WORKITEM_ID to complete" 40 | WORKITEM_STATUS="inprogress" 41 | while [ $WORKITEM_STATUS == "inprogress" ] 42 | do 43 | sleep 5 44 | WORKITEM_STATUS=$($FORGE_DA_SCRIPT get-workitem $WORKITEM_ID --short) 45 | echo $WORKITEM_STATUS 46 | done 47 | 48 | # Download the results 49 | echo "Downloading results to output/thumbnail.bmp" 50 | $FORGE_DM_SCRIPT download-object $FORGE_BUCKET $THUMBNAIL_OBJECT_KEY output/thumbnail.bmp 51 | 52 | echo "Process complete. See the thumbnail in the output folder, or download it from $THUMBNAIL_SIGNED_URL" 53 | -------------------------------------------------------------------------------- /examples/inventor-thumbnail/run-pipeline.ps1: -------------------------------------------------------------------------------- 1 | # FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, and FORGE_BUCKET must be set before running this script. 2 | 3 | function Request-DM { 4 | $command = $args[0] 5 | $result = Invoke-Expression "node ..\..\src\forge-dm.js $command" 6 | if ($LASTEXITCODE -ne 0) { 7 | Write-Error "forge-dm command failed: $command" -ErrorAction "Stop" 8 | } 9 | return $result 10 | } 11 | 12 | function Request-DA { 13 | $command = $args[0] 14 | $result = Invoke-Expression "node ..\..\src\forge-da.js $command" 15 | if ($LASTEXITCODE -ne 0) { 16 | Write-Error "forge-da command failed: $command" -ErrorAction "Stop" 17 | } 18 | return $result 19 | } 20 | 21 | $activity_name = "MyTestActivity" 22 | $activity_alias = "dev" 23 | 24 | $input_file_path = ".\Clutch_Gear_20t.ipt" 25 | $input_object_key = "input.ipt" 26 | $thumbnail_object_key = "thumbnail.bmp" 27 | 28 | # If it does not exist, create a data bucket 29 | $result = Request-DM "list-buckets --short" 30 | $result = $result | Select-String -Pattern $env:FORGE_BUCKET | Measure-Object -Line 31 | if ($result.Lines -eq 0) { 32 | Write-Host "Creating a bucket $env:FORGE_BUCKET" 33 | Request-DM "create-bucket $env:FORGE_BUCKET" 34 | } 35 | 36 | # Upload Inventor file and create a placeholder for the output thumbnail 37 | Write-Host "Preparing input/output files" 38 | Request-DM "upload-object $input_file_path application/octet-stream $env:FORGE_BUCKET $input_object_key" 39 | New-Item -Name "output" -Path "." -ItemType "directory" 40 | New-Item -Name "thumbnail.bmp" -Path ".\output" -ItemType "file" 41 | Request-DM "upload-object .\output\thumbnail.bmp image/bmp $env:FORGE_BUCKET $thumbnail_object_key" 42 | 43 | # Generate signed URLs for all input and output files 44 | Write-Host "Creating signed URLs" 45 | $input_file_signed_url = Request-DM "create-signed-url $env:FORGE_BUCKET $input_object_key --access read --short" 46 | $thumbnail_signed_url = Request-DM "create-signed-url $env:FORGE_BUCKET $thumbnail_object_key --access readwrite --short" 47 | 48 | # Create work item and poll the results 49 | Write-Host "Creating work item" 50 | $workitem_id = Request-DA "create-workitem $activity_name $activity_alias --input PartFile --input-url $input_file_signed_url --output Thumbnail --output-url $thumbnail_signed_url --short" 51 | Write-Host "Waiting for work item $workitem_id to complete" 52 | $workitem_status = "inprogress" 53 | while ($workitem_status -eq "inprogress") { 54 | Start-Sleep -s 5 55 | $workitem_status = Request-DA "get-workitem $workitem_id --short" 56 | Write-Host $workitem_status 57 | } 58 | 59 | # Download the results 60 | Write-Host "Downloading results to output/thumbnail.bmp" 61 | Request-DM "download-object $env:FORGE_BUCKET $thumbnail_object_key output\thumbnail.bmp" 62 | Write-Host "Process complete. See the thumbnail in the output folder, or download it from $thumbnail_signed_url" 63 | -------------------------------------------------------------------------------- /examples/inventor-thumbnail/run-pipeline.js: -------------------------------------------------------------------------------- 1 | #!/bin/env node 2 | 3 | const fs = require('fs'); 4 | const { DataManagementClient, DesignAutomationClient } = require('forge-server-utils'); 5 | 6 | const BUCKET = process.env.FORGE_BUCKET; 7 | 8 | const ACTIVITY_NAME = 'TestActivity'; 9 | const ACTIVITY_ALIAS = 'dev'; 10 | 11 | const INPUT_FILE_PATH = './Clutch_Gear_20t.ipt'; 12 | const INPUT_OBJECT_KEY = 'input.ipt'; 13 | const THUMBNAIL_OBJECT_KEY = 'thumbnail.bmp'; 14 | 15 | let credentials = { 16 | client_id: process.env.FORGE_CLIENT_ID, 17 | client_secret: process.env.FORGE_CLIENT_SECRET 18 | }; 19 | let dm = new DataManagementClient(credentials); 20 | let da = new DesignAutomationClient(credentials); 21 | 22 | function sleep(ms) { 23 | return new Promise(function(resolve, reject) { 24 | setTimeout(function() { resolve(); }, ms); 25 | }); 26 | } 27 | 28 | async function run() { 29 | // Create bucket if it doesn't exist 30 | const allBuckets = await dm.listBuckets(); 31 | const matchingBuckets = allBuckets.filter(item => item.bucketKey === BUCKET); 32 | if (matchingBuckets.length === 0) { 33 | try { 34 | await dm.createBucket(BUCKET, 'persistent'); 35 | } catch(err) { 36 | console.error('Could not create bucket', err); 37 | process.exit(1); 38 | } 39 | } 40 | 41 | // Upload Inventor file and create a placeholder for the output thumbnail 42 | const inputObjectBuff = fs.readFileSync(INPUT_FILE_PATH); 43 | try { 44 | await dm.uploadObject(BUCKET, INPUT_OBJECT_KEY, 'application/octet-stream', inputObjectBuff); 45 | } catch(err) { 46 | console.error('Could not upload input file', err); 47 | process.exit(1); 48 | } 49 | 50 | // Generate signed URLs for all input and output files 51 | let inputFileSignedUrl; 52 | let thumbnailSignedUrl; 53 | try { 54 | inputFileSignedUrl = await dm.createSignedUrl(BUCKET, INPUT_OBJECT_KEY, 'read'); 55 | thumbnailSignedUrl = await dm.createSignedUrl(BUCKET, THUMBNAIL_OBJECT_KEY, 'readwrite'); 56 | } catch(err) { 57 | console.error('Could not generate signed URLs', err); 58 | process.exit(1); 59 | } 60 | 61 | // Create work item and poll the results 62 | const activityId = credentials.client_id + '.' + ACTIVITY_NAME + '+' + ACTIVITY_ALIAS; 63 | const workitemInputs = [ 64 | { name: 'PartFile', url: inputFileSignedUrl.signedUrl } 65 | ]; 66 | const workitemOutputs = [ 67 | { name: 'Thumbnail', url: thumbnailSignedUrl.signedUrl } 68 | ]; 69 | let workitem; 70 | try { 71 | workitem = await da.createWorkItem(activityId, workitemInputs, workitemOutputs); 72 | console.log('Workitem', workitem); 73 | while (workitem.status === 'inprogress' || workitem.status === 'pending') { 74 | await sleep(5000); 75 | workitem = await da.workItemDetails(workitem.id); 76 | console.log(workitem.status); 77 | } 78 | } catch(err) { 79 | console.error('Could not run work item', err); 80 | process.exit(1); 81 | } 82 | 83 | console.log('Results', workitem); 84 | console.log('Result thumbnail url:', thumbnailSignedUrl.signedUrl); 85 | } 86 | 87 | run(); 88 | -------------------------------------------------------------------------------- /examples/inventor-thumbnail/setup-pipeline.js: -------------------------------------------------------------------------------- 1 | #!/bin/env node 2 | 3 | const fs = require('fs'); 4 | const FormData = require('form-data'); 5 | const { DataManagementClient, DesignAutomationClient } = require('forge-server-utils'); 6 | 7 | const APPBUNDLE_NAME = 'TestBundle'; 8 | const APPBUNDLE_DESCRIPTIION = 'TestBundle description'; 9 | const APPBUNDLE_ALIAS = 'dev'; 10 | const APPBUNDLE_FILE = './ThumbnailPlugin.bundle.zip'; 11 | const APPBUNDLE_ENGINE = 'Autodesk.Inventor+24'; 12 | 13 | const ACTIVITY_NAME = 'TestActivity'; 14 | const ACTIVITY_DESCRIPTION = 'Activity description'; 15 | const ACTIVITY_ALIAS = 'dev'; 16 | 17 | let credentials = { 18 | client_id: process.env.FORGE_CLIENT_ID, 19 | client_secret: process.env.FORGE_CLIENT_SECRET 20 | }; 21 | let dm = new DataManagementClient(credentials); 22 | let da = new DesignAutomationClient(credentials); 23 | 24 | function uploadAppBundleFile(appBundle, appBundleFilename) { 25 | const uploadParameters = appBundle.uploadParameters.formData; 26 | const form = new FormData(); 27 | form.append('key', uploadParameters['key']); 28 | form.append('policy', uploadParameters['policy']); 29 | form.append('content-type', uploadParameters['content-type']); 30 | form.append('success_action_status', uploadParameters['success_action_status']); 31 | form.append('success_action_redirect', uploadParameters['success_action_redirect']); 32 | form.append('x-amz-signature', uploadParameters['x-amz-signature']); 33 | form.append('x-amz-credential', uploadParameters['x-amz-credential']); 34 | form.append('x-amz-algorithm', uploadParameters['x-amz-algorithm']); 35 | form.append('x-amz-date', uploadParameters['x-amz-date']); 36 | form.append('x-amz-server-side-encryption', uploadParameters['x-amz-server-side-encryption']); 37 | form.append('x-amz-security-token', uploadParameters['x-amz-security-token']); 38 | form.append('file', fs.createReadStream(appBundleFilename)); 39 | return new Promise(function(resolve, reject) { 40 | form.submit(appBundle.uploadParameters.endpointURL, function(err, res) { 41 | if (err) { 42 | reject(err); 43 | } else { 44 | resolve(res); 45 | } 46 | }); 47 | }); 48 | } 49 | 50 | async function setup() { 51 | // Create or update an appbundle 52 | const allAppBundles = await da.listAppBundles(); 53 | const matchingAppBundles = allAppBundles.filter(item => item.indexOf(APPBUNDLE_NAME) !== -1); 54 | let appBundle; 55 | try { 56 | if (matchingAppBundles.length === 0) { 57 | appBundle = await da.createAppBundle(APPBUNDLE_NAME, APPBUNDLE_ENGINE, APPBUNDLE_DESCRIPTIION); 58 | } else { 59 | appBundle = await da.updateAppBundle(APPBUNDLE_NAME, APPBUNDLE_ENGINE, APPBUNDLE_DESCRIPTIION); 60 | } 61 | } catch(err) { 62 | console.error('Could not create or update appbundle', err); 63 | process.exit(1); 64 | } 65 | console.log('AppBundle', appBundle); 66 | 67 | // Upload appbundle zip file 68 | try { 69 | await uploadAppBundleFile(appBundle, APPBUNDLE_FILE); 70 | } catch(err) { 71 | console.error('Could not upload appbundle file', err); 72 | process.exit(1); 73 | } 74 | 75 | // Create or update an appbundle alias 76 | const allAppBundleAliases = await da.listAppBundleAliases(APPBUNDLE_NAME); 77 | const matchingAppBundleAliases = allAppBundleAliases.filter(item => item.id === APPBUNDLE_ALIAS); 78 | let appBundleAlias; 79 | try { 80 | if (matchingAppBundleAliases.length === 0) { 81 | appBundleAlias = await da.createAppBundleAlias(APPBUNDLE_NAME, APPBUNDLE_ALIAS, appBundle.version); 82 | } else { 83 | appBundleAlias = await da.updateAppBundleAlias(APPBUNDLE_NAME, APPBUNDLE_ALIAS, appBundle.version); 84 | } 85 | } catch(err) { 86 | console.error('Could not create or update appbundle alias', err); 87 | process.exit(1); 88 | } 89 | console.log('AppBundle alias', appBundleAlias); 90 | 91 | // Create or update an activity 92 | const allActivities = await da.listActivities(); 93 | const matchingActivities = allActivities.filter(item => item.indexOf('.' + ACTIVITY_NAME + '+') !== -1); 94 | const activityInputs = [ 95 | { name: 'PartFile', description: 'Input Inventor part file.' } 96 | ]; 97 | const activityOutputs = [ 98 | { name: 'Thumbnail', description: 'Output thumbnail bitmap file.', localName: 'thumbnail.bmp' } 99 | ]; 100 | let activity; 101 | try { 102 | if (matchingActivities.length === 0) { 103 | activity = await da.createActivity(ACTIVITY_NAME, ACTIVITY_DESCRIPTION, APPBUNDLE_NAME, APPBUNDLE_ALIAS, APPBUNDLE_ENGINE, activityInputs, activityOutputs); 104 | } else { 105 | activity = await da.updateActivity(ACTIVITY_NAME, ACTIVITY_DESCRIPTION, APPBUNDLE_NAME, APPBUNDLE_ALIAS, APPBUNDLE_ENGINE, activityInputs, activityOutputs); 106 | } 107 | } catch(err) { 108 | console.error('Could not create or update activity', err); 109 | process.exit(1); 110 | } 111 | console.log('Activity', activity); 112 | 113 | // Create or update an activity alias 114 | const allActivityAliases = await da.listActivityAliases(ACTIVITY_NAME); 115 | const matchingActivityAliases = allActivityAliases.filter(item => item.id === ACTIVITY_ALIAS); 116 | let activityAlias; 117 | try { 118 | if (matchingActivityAliases.length === 0) { 119 | activityAlias = await da.createActivityAlias(ACTIVITY_NAME, ACTIVITY_ALIAS, activity.version); 120 | } else { 121 | activityAlias = await da.updateActivityAlias(ACTIVITY_NAME, ACTIVITY_ALIAS, activity.version); 122 | } 123 | } catch(err) { 124 | console.error('Could not create or update activity alias', err); 125 | process.exit(1); 126 | } 127 | console.log('Activity alias', activityAlias); 128 | } 129 | 130 | setup(); 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # forge-cli-utils 2 | 3 | [![build status](https://travis-ci.com/petrbroz/forge-cli-utils.svg?branch=master)](https://travis-ci.com/petrbroz/forge-cli-utils) 4 | [![npm version](https://badge.fury.io/js/forge-cli-utils.svg)](https://badge.fury.io/js/forge-cli-utils) 5 | ![node](https://img.shields.io/node/v/forge-cli-utils.svg) 6 | ![npm downloads](https://img.shields.io/npm/dw/forge-cli-utils.svg) 7 | ![platforms](https://img.shields.io/badge/platform-windows%20%7C%20osx%20%7C%20linux-lightgray.svg) 8 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) 9 | 10 | Command line tools for Autodesk Forge services. 11 | 12 | [![asciicast](https://asciinema.org/a/244057.svg)](https://asciinema.org/a/244057) 13 | 14 | ## Installation 15 | 16 | ### Using npm 17 | 18 | Install the `forge-cli-utils` library, either in your own npm project 19 | (`npm install --save forge-cli-utils`), or globally (`npm install --global forge-cli-utils`). 20 | 21 | ### Self-contained binaries 22 | 23 | Scripts in this library are also packaged into self-contained binaries for various platforms 24 | using the [pkg](https://www.npmjs.com/package/pkg) module. You can download the binaries from 25 | [release](https://github.com/petrbroz/forge-cli-utils/releases) pages. 26 | 27 | ## Usage 28 | 29 | ### Providing Forge credentials 30 | 31 | The CLI tools require Forge app credentials to be provided as env. variables. 32 | 33 | > If you don't have a Forge app yet, check out this tutorial: https://forge.autodesk.com/en/docs/oauth/v2/tutorials/create-app/. 34 | 35 | On macOS and linux: 36 | ```bash 37 | export FORGE_CLIENT_ID= 38 | export FORGE_CLIENT_SECRET= 39 | ``` 40 | 41 | On Windows, using _cmd.exe_: 42 | ``` 43 | set FORGE_CLIENT_ID= 44 | set FORGE_CLIENT_SECRET= 45 | ``` 46 | 47 | On Windows, using PowerShell: 48 | ```powershell 49 | $env:FORGE_CLIENT_ID = "" 50 | $env:FORGE_CLIENT_SECRET = "" 51 | ``` 52 | 53 | ### Scripts 54 | 55 | Use the following scripts for different Forge services: 56 | - `forge-dm` - [Forge Data Management](https://forge.autodesk.com/en/docs/data/v2) service 57 | - `forge-md` - [Forge Model Derivative](https://forge.autodesk.com/en/docs/model-derivative/v2) service 58 | - `forge-da` - [Forge Design Automation](https://forge.autodesk.com/en/docs/design-automation/v3) service 59 | 60 | Each script expects a _subcommand_ similar to `git`. To get a list of all available commands, 61 | run the script with `-h` or `--help`. 62 | 63 | > When using bash, use the _tools/autocomplete-bash.sh_ script to setup a simple auto-completion 64 | > for the basic commands of each script: `source tools/autocomplete-bash.sh`. 65 | 66 | Most commands output raw JSON output from Forge services by default, but in many cases 67 | you can use `-s` or `--short` flag to output a more concise version of the results. 68 | The raw JSON output can also be combined with tools like [jq](https://stedolan.github.io/jq) 69 | to extract just the pieces of information that you need: 70 | 71 | ```bash 72 | # Listing buckets as full JSON 73 | forge-dm list-buckets 74 | 75 | # Listing bucket keys 76 | forge-dm list-buckets --short 77 | 78 | # List creation dates of all buckets 79 | forge-dm list-buckets | jq '.[] | .createdDate' 80 | ``` 81 | 82 | ### Examples 83 | 84 | #### Data Management 85 | 86 | ```bash 87 | # Listing buckets as full JSON 88 | forge-dm list-buckets 89 | 90 | # Listing object IDs of specific bucket 91 | forge-dm list-objects my-test-bucket --short 92 | 93 | # Listing object IDs without specifying a bucket (will show an interactive prompt with list of buckets to choose from) 94 | forge-dm list-objects --short 95 | 96 | # Getting an URN of an object 97 | forge-dm object-urn my-bucket-key my-object-key 98 | ``` 99 | 100 | #### Design Automation 101 | 102 | ```bash 103 | # Creating a new app bundle 104 | forge-da create-appbundle BundleName path/to/bundle/zipfile Autodesk.Inventor+23 "Bundle description here." 105 | 106 | # Updating existing activity 107 | forge-da update-activity ActivityName BundleName BundleAlias Autodesk.Inventor+23 --input PartFile --output Thumbnail --output-local-name thumbnail.bmp 108 | 109 | # Creating work item 110 | forge-da create-workitem ActivityName ActivityAlias --input PartFile --input-url https://some.url --output Thumbnail --output-url https://another.url --short 111 | ``` 112 | 113 | > When specifying inputs and outputs for an activity or work item, `--input-*` and `--output-*` arguments 114 | > are always applied to the last input/output ID. For example, consider the following sequence of arguments: 115 | > `--input InputA --input-local-name house.rvt --input InputB --input InputC --input-url https://foobar.com`. 116 | > Such a sequence will define three inputs: _InputA_ with local name _house.rvt_, _InputB_ (with no additional 117 | > properties), and _InputC_ with URL _https://foobar.com_. 118 | > For more details, see https://github.com/petrbroz/forge-cli-utils/wiki/Design-Automation-Inputs-and-Outputs 119 | 120 | #### Model Derivative 121 | 122 | ```bash 123 | # Translating a model based on its URN 124 | forge-md translate dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6cG9jLWJvdXlndWVzLWltbW9iaWxpZXIvaW5wdXQucnZ0 125 | 126 | # Showing an interactive prompt with all viewables in an URN, and then getting properties of the selected viewable 127 | forge-md get-viewable-props dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6cG9jLWJvdXlndWVzLWltbW9iaWxpZXIvaW5wdXQucnZ0 128 | 129 | # Download the derivatives once translation is completed, -u --guid , -c --directory 130 | forge-md download-derivatives dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6cG9jLWJvdXlndWVzLWltbW9iaWxpZXIvaW5wdXQucnZ0 -c '/path/to/output/optional' -u 'cdcf63c6-6a67-ffd2-2a8e-1e31397052f7' 131 | ``` 132 | 133 | > For additional examples, check out the _examples_ subfolder. 134 | 135 | ## Additional Resources 136 | 137 | - blog post on auto-deploying Design Automation plugins from Visual Studio: https://forge.autodesk.com/blog/deploying-design-automation-visual-studio -------------------------------------------------------------------------------- /src/helpers/derivativePersistence.js: -------------------------------------------------------------------------------- 1 | const zlib = require('zlib') 2 | const jsZip = require('jszip'); 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | const { log, warn, error } = require('../common'); 6 | const ora = require('ora'); 7 | 8 | function getSanitizedStringArray (array) { 9 | return array && (array instanceof Array ? array : array.split(',')).map(e => e.trim()) 10 | } 11 | 12 | module.exports = class DerivativePersistence { 13 | 14 | constructor (modelDerivative, urn) { 15 | this.modelDerivative = modelDerivative; 16 | this.urn = urn; 17 | this.persistPromises = {} 18 | this.excludedRoles = ['Autodesk.CloudPlatform.PropertyDatabase']; 19 | this.saved = 0; 20 | this.toSave = 0; 21 | this.spinner = ora(`Processing: ${this.saved} of ${this.toSave} derivatives saved ...`).start() 22 | } 23 | 24 | setExcludeRoles (excludedRoles) { 25 | this.excludedRoles = getSanitizedStringArray(excludedRoles) 26 | } 27 | 28 | setOutputDirectory (directory) { 29 | this.directory = directory 30 | } 31 | 32 | addURNToPersist (derivativeUrn) { 33 | this.persistPromises[derivativeUrn] || (this.persistPromises[derivativeUrn] = this.persistDerivative(derivativeUrn)) 34 | } 35 | 36 | getDerivativeURN (derivativeUrn, fileName) { 37 | return derivativeUrn.split('/')[0] + '/' + path.join(derivativeUrn.split('/').slice(1,-1).join('/'), fileName).replace(/\\/g, '/') 38 | } 39 | 40 | persistAssetsFromManifest (manifest, derivativeUrn) { 41 | return manifest.assets.forEach(e => { 42 | if(![...e.URI].some(c => [':', '?', '*', '<', '>', '|'].includes(c))) this.addURNToPersist(this.getDerivativeURN(derivativeUrn, e.URI)) 43 | }) 44 | } 45 | 46 | persistAssetsFromSVFPath (svfPath, derivativeUrn) { 47 | return new Promise((resolve, reject) => { 48 | try { 49 | fs.readFile(svfPath, (err, data) => { 50 | if (err) reject(err); 51 | jsZip.loadAsync(data).then(contents => { 52 | contents.files['manifest.json'].async('string').then(data => { 53 | this.persistAssetsFromManifest(JSON.parse(data), derivativeUrn); 54 | resolve() 55 | }) 56 | }) 57 | }) 58 | } catch (err) { 59 | reject(err) 60 | } 61 | }) 62 | } 63 | 64 | persistAssetsFromManifestStream (stream, derivativeUrn) { 65 | return new Promise((resolve, reject) => { 66 | try { 67 | let data = ''; 68 | const gstream = stream.pipe(zlib.createGunzip()); 69 | gstream.on('data', chunk => data += chunk); 70 | gstream.on('finish', () => { 71 | this.persistAssetsFromManifest(JSON.parse(data), derivativeUrn); 72 | resolve() 73 | }) 74 | } catch (err) { 75 | reject(err) 76 | } 77 | }) 78 | } 79 | 80 | persistDerivative (derivativeUrn) { 81 | return new Promise(async (resolve, reject) => { 82 | try { 83 | log('Fetching: ' + derivativeUrn); 84 | this.toSave++; 85 | this.spinner.text = `Processing: ${this.saved} of ${this.toSave} derivatives saved ...`; 86 | const filePath = path.join(this.directory || '', derivativeUrn.split('/').splice(1).join('/')); 87 | await fs.ensureFile(filePath); 88 | const derivativeReadStream = await this.modelDerivative.getDerivativeStream(this.urn, derivativeUrn); 89 | const fileWriteStream = fs.createWriteStream(filePath); 90 | derivativeReadStream.on('error', err => reject(err)); 91 | derivativeReadStream.on('finish', () => log('Finish reading: ' + derivativeUrn)); 92 | fileWriteStream.on('error', err => reject(err)); 93 | fileWriteStream.on('finish', async () => { 94 | log('Saved to: ' + filePath); 95 | this.spinner.text = `Processing: ${this.saved} of ${this.toSave} derivatives saved ...`; 96 | this.saved++; 97 | switch (path.extname(filePath)) { 98 | case '.svf': 99 | await this.persistAssetsFromSVFPath(filePath, derivativeUrn); 100 | break; 101 | case '.f2d': 102 | case '.f3d': 103 | await this.persistAssetsFromManifestStream(await this.modelDerivative.getDerivativeStream(this.urn, this.getDerivativeURN(derivativeUrn, 'manifest.json.gz')), derivativeUrn) 104 | } 105 | resolve() 106 | }); 107 | derivativeReadStream.pipe(fileWriteStream); 108 | } catch(err) { 109 | error('Error fetching:' + derivativeUrn) 110 | reject(err) 111 | } 112 | }).finally(() => delete this.persistPromises[derivativeUrn]); 113 | } 114 | 115 | persistDerivatives (children, force) { 116 | children.forEach(e => { 117 | const shouldForce = this.targetAssets && this.targetAssets.includes(e.guid); 118 | if (!e.guid || force || (!this.targetAssets || this.targetAssets.includes(e.guid)) && !this.excludedRoles.includes(e.role)) { 119 | if (e.children instanceof Array) this.persistDerivatives(e.children, shouldForce); 120 | if (typeof e.urn == 'string') this.addURNToPersist(e.urn) 121 | } 122 | }); 123 | } 124 | 125 | async fetch (assets, targetAssets) { 126 | this.targetAssets = getSanitizedStringArray(targetAssets); 127 | this.persistDerivatives(assets); 128 | try { 129 | while (Object.keys(this.persistPromises).length) 130 | await Promise.all(Object.values(this.persistPromises)); 131 | this.spinner.succeed(`Done: ${this.saved} of ${this.toSave} derivatives saved ...`) 132 | } catch(err) { 133 | this.spinner.fail(err.toString()) 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /src/forge-md.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const { prompt } = require('inquirer'); 5 | const { ModelDerivativeClient } = require('forge-server-utils'); 6 | const DerivativePersistence = require('./helpers/derivativePersistence'); 7 | const package = require('../package.json'); 8 | const { log, warn, error } = require('./common'); 9 | 10 | const { FORGE_CLIENT_ID, FORGE_CLIENT_SECRET } = process.env; 11 | if (!FORGE_CLIENT_ID || !FORGE_CLIENT_SECRET) { 12 | warn('Provide FORGE_CLIENT_ID and FORGE_CLIENT_SECRET as env. variables.'); 13 | return; 14 | } 15 | const modelDerivative = new ModelDerivativeClient({ client_id: FORGE_CLIENT_ID, client_secret: FORGE_CLIENT_SECRET }); 16 | 17 | async function promptViewable(urn) { 18 | const metadata = await modelDerivative.getMetadata(urn); 19 | const viewables = metadata.data.metadata; 20 | const answer = await prompt({ type: 'list', name: 'viewable', choices: viewables.map(viewable => viewable.guid + ' (' + viewable.name + ')') }); 21 | return answer.viewable.substr(0, answer.viewable.indexOf(' ')); 22 | } 23 | 24 | function sleep(ms) { 25 | return new Promise(function(resolve) { 26 | setTimeout(resolve, ms); 27 | }); 28 | } 29 | 30 | program 31 | .version(package.version) 32 | .description('Command-line tool for accessing Autodesk Forge Model Derivative service.'); 33 | 34 | program 35 | .command('list-formats [input-type]') 36 | .alias('lf') 37 | .description('List supported output formats for specific input type or for all input types.') 38 | .action(async function(inputType) { 39 | try { 40 | const formats = await modelDerivative.formats(); 41 | if (inputType) { 42 | log(Object.keys(formats).filter(key => formats[key].includes(inputType))); 43 | } else { 44 | log(formats); 45 | } 46 | } catch(err) { 47 | error(err); 48 | } 49 | }); 50 | 51 | program 52 | .command('translate ') 53 | .alias('t') 54 | .description('Start translation job.') 55 | .option('-t, --type ', 'Output type ("svf" by default). See `forge-md list-formats` for all possible output types.', 'svf') 56 | .option('-v, --views ', 'Comma-separated list of requested views ("2d,3d" by default)', '2d,3d') 57 | .option('-w, --wait', 'Wait for the translation to complete.', false) 58 | .action(async function(urn, command) { 59 | try { 60 | const outputs = [{ type: command.type, views: command.views.split(',') }]; 61 | await modelDerivative.submitJob(urn, outputs); 62 | 63 | if (command.wait) { 64 | let manifest = await modelDerivative.getManifest(urn); 65 | while (manifest.status === 'inprogress') { 66 | await sleep(5000); 67 | } 68 | } 69 | } catch(err) { 70 | error(err); 71 | } 72 | }); 73 | 74 | program 75 | .command('get-manifest ') 76 | .alias('gm') 77 | .description('Get manifest of derivative.') 78 | .option('-s, --short', 'Return status of manifest instead of the entire JSON.') 79 | .action(async function(urn, command) { 80 | try { 81 | const manifest = await modelDerivative.getManifest(urn); 82 | log(command.short ? manifest.status : manifest); 83 | } catch(err) { 84 | error(err); 85 | } 86 | }); 87 | 88 | program 89 | .command('get-metadata ') 90 | .alias('gx') 91 | .description('Get metadata of derivative.') 92 | .option('-s, --short', 'Return GUIDs of viewables instead of the entire JSON.') 93 | .action(async function(urn, command) { 94 | try { 95 | const metadata = await modelDerivative.getMetadata(urn); 96 | if (command.short) { 97 | metadata.data.metadata.forEach(viewable => log(viewable.guid)); 98 | } else { 99 | log(metadata); 100 | } 101 | } catch(err) { 102 | error(err); 103 | } 104 | }); 105 | 106 | program 107 | .command('get-viewable-tree [guid]') 108 | .alias('gvt') 109 | .description('Get object tree of specific viewable.') 110 | .action(async function(urn, guid, command) { 111 | try { 112 | if (!guid) { 113 | guid = await promptViewable(urn); 114 | } 115 | 116 | const tree = await modelDerivative.getViewableTree(urn, guid); 117 | log(tree); 118 | } catch (err) { 119 | error(err); 120 | } 121 | }); 122 | 123 | program 124 | .command('get-viewable-props [guid]') 125 | .alias('gvp') 126 | .description('Get properties of specific viewable.') 127 | .action(async function(urn, guid, command) { 128 | try { 129 | if (!guid) { 130 | guid = await promptViewable(urn); 131 | } 132 | 133 | const props = await modelDerivative.getViewableProperties(urn, guid); 134 | log(props); 135 | } catch (err) { 136 | error(err); 137 | } 138 | }); 139 | 140 | program 141 | .command('download-derivatives ') 142 | .alias('dd') 143 | .description('Download derivatives .') 144 | .option('-c, --directory ', 'Specifies the output directory.') 145 | .option('-u, --guid ', 'Specifies GUIDs of the derivatives to download, separated by comma e.g. `-u "a0798102-7662-0a66-e0d2-cf982c29eb9a, 593a30e5-12b8-41b3-a3c1-d02fc80dad24-0018d776"`') 146 | .action(async function(urn, command) { 147 | // TODO: see if we could reuse forge-convert-utils here 148 | try { 149 | const manifest = await modelDerivative.getManifest(urn); 150 | if (manifest.progress == 'complete' && manifest.status == 'success' && manifest.derivatives instanceof Array) 151 | { 152 | const derivativePersistenceClient = new DerivativePersistence(modelDerivative, urn, command.directory); 153 | derivativePersistenceClient.setOutputDirectory(command.directory); 154 | await derivativePersistenceClient.fetch(manifest.derivatives, command.guid); 155 | } 156 | else error('Job not completed or failed - check manifest for details.') 157 | } catch (err) { 158 | error(err); 159 | } 160 | }); 161 | 162 | program.parse(process.argv); 163 | if (!program.args.length) { 164 | program.help(); 165 | } 166 | -------------------------------------------------------------------------------- /src/forge-dm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const crypto = require('crypto'); 6 | const program = require('commander'); 7 | const { prompt } = require('inquirer'); 8 | const { DataManagementClient, urnify } = require('forge-server-utils'); 9 | 10 | const package = require('../package.json'); 11 | const { log, warn, error } = require('./common'); 12 | 13 | const { FORGE_CLIENT_ID, FORGE_CLIENT_SECRET } = process.env; 14 | if (!FORGE_CLIENT_ID || !FORGE_CLIENT_SECRET) { 15 | warn('Provide FORGE_CLIENT_ID and FORGE_CLIENT_SECRET as env. variables.'); 16 | return; 17 | } 18 | const data = new DataManagementClient({ client_id: FORGE_CLIENT_ID, client_secret: FORGE_CLIENT_SECRET }); 19 | 20 | async function promptBucket() { 21 | const buckets = await data.listBuckets(); 22 | const answer = await prompt({ type: 'list', name: 'bucket', choices: buckets.map(bucket => bucket.bucketKey) }); 23 | return answer.bucket; 24 | } 25 | 26 | async function promptObject(bucket) { 27 | const objects = await data.listObjects(bucket); 28 | const answer = await prompt({ type: 'list', name: 'object', choices: objects.map(object => object.objectKey) }); 29 | return answer.object; 30 | } 31 | 32 | function computeFileHash(filename) { 33 | return new Promise(function(resolve, reject) { 34 | const stream = fs.createReadStream(filename); 35 | let hash = crypto.createHash('md5'); 36 | stream.on('data', function(chunk) { 37 | hash.update(chunk); 38 | }); 39 | stream.on('end', function() { 40 | resolve(hash.digest('hex')); 41 | }); 42 | stream.on('error', function(err) { 43 | reject(err); 44 | }); 45 | }); 46 | } 47 | 48 | program 49 | .version(package.version) 50 | .description('Command-line tool for accessing Autodesk Forge Data Management service.'); 51 | 52 | program 53 | .command('list-buckets') 54 | .alias('lb') 55 | .description('List buckets.') 56 | .option('-s, --short', 'Output bucket keys instead of the entire JSON.') 57 | .action(async function(command) { 58 | try { 59 | if (command.short) { 60 | for await (const buckets of data.iterateBuckets()) { 61 | buckets.forEach(bucket => log(bucket.bucketKey)); 62 | } 63 | } else { 64 | log(await data.listBuckets()); 65 | } 66 | } catch(err) { 67 | error(err); 68 | } 69 | }); 70 | 71 | program 72 | .command('bucket-details [bucket]') 73 | .alias('bd') 74 | .description('Retrieve bucket details.') 75 | .action(async function(bucket) { 76 | try { 77 | if (!bucket) { 78 | bucket = await promptBucket(); 79 | } 80 | 81 | const details = await data.getBucketDetails(bucket); 82 | log(details); 83 | } catch(err) { 84 | error(err); 85 | } 86 | }); 87 | 88 | program 89 | .command('create-bucket ') 90 | .alias('cb') 91 | .description('Create new bucket.') 92 | .option('-r, --retention ', 'Data retention policy. One of "transient" (default), "temporary", or "permanent".') 93 | .action(async function(bucket, command) { 94 | try { 95 | const retention = command.retention || 'transient'; 96 | const response = await data.createBucket(bucket, retention); 97 | log(response); 98 | } catch(err) { 99 | error(err); 100 | } 101 | }); 102 | 103 | program 104 | .command('list-objects [bucket]') 105 | .alias('lo') 106 | .description('List objects in bucket.') 107 | .option('-s, --short', 'Output object IDs instead of the entire JSON.') 108 | .option('-p, --prefix ', 'Limit the output only to objects with given prefix in their key.') 109 | .action(async function(bucket, command) { 110 | try { 111 | if (!bucket) { 112 | bucket = await promptBucket(); 113 | } 114 | 115 | if (command.short) { 116 | for await (const objects of data.iterateObjects(bucket, 16, command.prefix)) { 117 | objects.forEach(object => log(object.objectId)); 118 | } 119 | } else { 120 | log(await data.listObjects(bucket, command.prefix)); 121 | } 122 | } catch(err) { 123 | error(err); 124 | } 125 | }); 126 | 127 | program 128 | .command('upload-object [bucket] [name]') 129 | .alias('uo') 130 | .description('Upload file to bucket.') 131 | .option('-s, --short', 'Output object ID instead of the entire JSON.') 132 | .option('-r, --resumable', 'Upload file in chunks using the resumable capabilities. If the upload is interrupted or fails, simply run the command again to continue.') 133 | .option('-rp, --resumable-page ', 'Optional max. size of each chunk during resumable upload (in MB; must be greater or equal to 2; by default 5).', 5) 134 | .option('-rs, --resumable-session ', 'Optional session ID during the resumable upload (if omitted, an MD5 hash of the file content is used).') 135 | .action(async function(filename, contentType, bucket, name, command) { 136 | try { 137 | if (!bucket) { 138 | bucket = await promptBucket(); 139 | } 140 | 141 | if (!name) { 142 | const answer = await prompt({ type: 'input', name: 'name', default: path.basename(filename) }); 143 | name = answer.name; 144 | } 145 | 146 | if (command.resumable) { 147 | /* 148 | * If the --resumable option is used, collect the list of ranges of data that has already been 149 | * uploaded in a specific session (either an MD5 hash of the file contents or a custom ID), 150 | * and start uploading missing chunks. 151 | */ 152 | const sessionId = command.resumableSession || (await computeFileHash(filename)); 153 | let ranges = null; 154 | try { 155 | ranges = await data.getResumableUploadStatus(bucket, name, sessionId); 156 | console.log('ranges', ranges); 157 | } catch(err) { 158 | ranges = []; 159 | } 160 | 161 | const maxChunkSize = command.resumablePage << 20; 162 | const totalFileSize = fs.statSync(filename).size; 163 | let lastByte = 0; 164 | let fd = fs.openSync(filename, 'r'); 165 | let buff = Buffer.alloc(maxChunkSize); 166 | // Upload potential missing data before each successfully uploaded range 167 | for (const range of ranges) { 168 | while (lastByte < range.start) { 169 | const chunkSize = Math.min(range.start - lastByte, maxChunkSize); 170 | fs.readSync(fd, buff, 0, chunkSize, lastByte); 171 | await data.uploadObjectResumable(bucket, name, buff.slice(0, chunkSize), lastByte, totalFileSize, sessionId, contentType); 172 | lastByte += chunkSize; 173 | } 174 | lastByte = range.end + 1; 175 | } 176 | // Upload potential missing data after the last successfully uploaded range 177 | while (lastByte < totalFileSize - 1) { 178 | const chunkSize = Math.min(totalFileSize - lastByte, maxChunkSize); 179 | fs.readSync(fd, buff, 0, chunkSize, lastByte); 180 | await data.uploadObjectResumable(bucket, name, buff.slice(0, chunkSize), lastByte, totalFileSize, sessionId, contentType); 181 | lastByte += chunkSize; 182 | } 183 | fs.closeSync(fd); 184 | } else { 185 | const buffer = fs.readFileSync(filename); 186 | const uploaded = await data.uploadObject(bucket, name, contentType, buffer); 187 | log(command.short ? uploaded.objectId : uploaded); 188 | } 189 | } catch(err) { 190 | error(err); 191 | } 192 | }); 193 | 194 | program 195 | .command('download-object [bucket] [object] [filename]') 196 | .alias('do') 197 | .description('Download file from bucket.') 198 | .action(async function(bucket, object, filename, command) { 199 | try { 200 | if (!bucket) { 201 | bucket = await promptBucket(); 202 | } 203 | if (!object) { 204 | object = await promptObject(bucket); 205 | } 206 | if (!filename) { 207 | filename = object; 208 | } 209 | 210 | const arrayBuffer = await data.downloadObject(bucket, object); 211 | // TODO: add support for streaming data directly to disk instead of getting entire file into memory first 212 | fs.writeFileSync(filename, Buffer.from(arrayBuffer), { encoding: 'binary' }); 213 | } catch(err) { 214 | error(err); 215 | } 216 | }); 217 | 218 | program 219 | .command('object-urn [bucket] [object]') 220 | .alias('ou') 221 | .description('Get an URN (used in Model Derivative service) of specific bucket/object.') 222 | .action(async function(bucket, object, command) { 223 | try { 224 | if (!bucket) { 225 | bucket = await promptBucket(); 226 | } 227 | if (!object) { 228 | object = await promptObject(bucket); 229 | } 230 | 231 | const details = await data.getObjectDetails(bucket, object); 232 | log(urnify(details.objectId)); 233 | } catch(err) { 234 | error(err); 235 | } 236 | }); 237 | 238 | program 239 | .command('create-signed-url [bucket] [object]') 240 | .alias('csu') 241 | .description('Creates signed URL for specific bucket and object key.') 242 | .option('-s, --short', 'Output signed URL instead of the entire JSON.') 243 | .option('-a, --access ', 'Allowed access types for the created URL ("read", "write", or the default "readwrite").', 'readwrite') 244 | .action(async function(bucket, object, command) { 245 | try { 246 | if (!bucket) { 247 | bucket = await promptBucket(); 248 | } 249 | if (!object) { 250 | object = await promptObject(bucket); 251 | } 252 | 253 | const info = await data.createSignedUrl(bucket, object, command.access); 254 | log(command.short ? info.signedUrl : info); 255 | } catch(err) { 256 | error(err); 257 | } 258 | }); 259 | 260 | program.parse(process.argv); 261 | if (!program.args.length) { 262 | program.help(); 263 | } 264 | -------------------------------------------------------------------------------- /src/forge-da.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | 5 | const program = require('commander'); 6 | const { prompt } = require('inquirer'); 7 | const FormData = require('form-data'); 8 | const { DesignAutomationClient, DesignAutomationID } = require('forge-server-utils'); 9 | 10 | const package = require('../package.json'); 11 | const { log, warn, error } = require('./common'); 12 | 13 | const { FORGE_CLIENT_ID, FORGE_CLIENT_SECRET } = process.env; 14 | if (!FORGE_CLIENT_ID || !FORGE_CLIENT_SECRET) { 15 | warn('Provide FORGE_CLIENT_ID and FORGE_CLIENT_SECRET as env. variables.'); 16 | return; 17 | } 18 | 19 | const designAutomation = new DesignAutomationClient({ client_id: FORGE_CLIENT_ID, client_secret: FORGE_CLIENT_SECRET }); 20 | 21 | function isQualifiedID(qualifiedId) { 22 | return DesignAutomationID.parse(qualifiedId) !== null; 23 | } 24 | 25 | function decomposeQualifiedID(qualifiedId) { 26 | return DesignAutomationID.parse(qualifiedId); 27 | } 28 | 29 | async function promptEngine() { 30 | const engines = await designAutomation.listEngines(); 31 | const answer = await prompt({ type: 'list', name: 'engine', choices: engines }); 32 | return answer.engine; 33 | } 34 | 35 | async function promptAppBundle() { 36 | const bundles = await designAutomation.listAppBundles(); 37 | const uniqueBundleNames = new Set(bundles.map(DesignAutomationID.parse).filter(item => item !== null).map(item => item.id)); 38 | const answer = await prompt({ type: 'list', name: 'bundle', choices: Array.from(uniqueBundleNames.values()) }); 39 | return answer.bundle; 40 | } 41 | 42 | async function promptAppBundleVersion(appbundle) { 43 | const versions = await designAutomation.listAppBundleVersions(appbundle); 44 | const answer = await prompt({ type: 'list', name: 'version', choices: versions }); 45 | return answer.version; 46 | } 47 | 48 | async function promptAppBundleAlias(appbundle) { 49 | const aliases = await designAutomation.listAppBundleAliases(appbundle); 50 | const answer = await prompt({ type: 'list', name: 'alias', choices: aliases.map(item => item.id).filter(id => id !== '$LATEST') }); 51 | return answer.alias; 52 | } 53 | 54 | async function promptActivity(nameOnly = true) { 55 | const activities = await designAutomation.listActivities(); 56 | if (nameOnly) { 57 | const uniqueActivityNames = new Set(activities.map(DesignAutomationID.parse).filter(item => item !== null).map(item => item.id)); 58 | const answer = await prompt({ type: 'list', name: 'activity', choices: Array.from(uniqueActivityNames.values()) }); 59 | return answer.activity; 60 | } else { 61 | const answer = await prompt({ type: 'list', name: 'activity', choices: activities }); 62 | return answer.activity; 63 | } 64 | } 65 | 66 | async function promptActivityVersion(activity) { 67 | const versions = await designAutomation.listActivityVersions(activity); 68 | const answer = await prompt({ type: 'list', name: 'version', choices: versions }); 69 | return answer.version; 70 | } 71 | 72 | async function promptActivityAlias(activity) { 73 | const aliases = await designAutomation.listActivityAliases(activity); 74 | const answer = await prompt({ type: 'list', name: 'alias', choices: aliases.map(item => item.id).filter(id => id !== '$LATEST') }); 75 | return answer.alias; 76 | } 77 | 78 | function uploadAppBundleFile(appBundle, appBundleFilename) { 79 | const uploadParameters = appBundle.uploadParameters.formData; 80 | const form = new FormData(); 81 | form.append('key', uploadParameters['key']); 82 | form.append('policy', uploadParameters['policy']); 83 | form.append('content-type', uploadParameters['content-type']); 84 | form.append('success_action_status', uploadParameters['success_action_status']); 85 | form.append('success_action_redirect', uploadParameters['success_action_redirect']); 86 | form.append('x-amz-signature', uploadParameters['x-amz-signature']); 87 | form.append('x-amz-credential', uploadParameters['x-amz-credential']); 88 | form.append('x-amz-algorithm', uploadParameters['x-amz-algorithm']); 89 | form.append('x-amz-date', uploadParameters['x-amz-date']); 90 | form.append('x-amz-server-side-encryption', uploadParameters['x-amz-server-side-encryption']); 91 | form.append('x-amz-security-token', uploadParameters['x-amz-security-token']); 92 | form.append('file', fs.createReadStream(appBundleFilename)); 93 | return new Promise(function(resolve, reject) { 94 | form.submit(appBundle.uploadParameters.endpointURL, function(err, res) { 95 | if (err) { 96 | reject(err); 97 | } else { 98 | resolve(res); 99 | } 100 | }); 101 | }); 102 | } 103 | 104 | program 105 | .version(package.version) 106 | .description('Command-line tool for accessing Autodesk Forge Design Automation service.'); 107 | 108 | program 109 | .command('list-engines') 110 | .alias('le') 111 | .description('List engines.') 112 | .option('-s, --short', 'Output engine IDs instead of the entire JSON.') 113 | .action(async function(command) { 114 | try { 115 | if (command.short) { 116 | for await (const engines of designAutomation.iterateEngines()) { 117 | engines.forEach(engine => log(engine)); 118 | } 119 | } else { 120 | log(await designAutomation.listEngines()); 121 | } 122 | } catch(err) { 123 | error(err); 124 | } 125 | }); 126 | 127 | program 128 | .command('get-engine [engine-full-id]') 129 | .alias('ge') 130 | .description('Get engine details.') 131 | .action(async function(engineFullId, command) { 132 | try { 133 | if (!engineFullId) { 134 | engineFullId = await promptEngine(); 135 | } 136 | 137 | if (!isQualifiedID(engineFullId)) { 138 | throw new Error('Engine ID must be fully qualified (".+").'); 139 | } 140 | 141 | const engine = await designAutomation.getEngine(engineFullId); 142 | log(engine); 143 | } catch(err) { 144 | error(err); 145 | } 146 | }); 147 | 148 | program 149 | .command('list-appbundles') 150 | .alias('lb') 151 | .description('List app bundles.') 152 | .option('-s, --short', 'Output app bundle IDs instead of the entire JSON.') 153 | .action(async function(command) { 154 | try { 155 | if (command.short) { 156 | for await (const bundles of designAutomation.iterateAppBundles()) { 157 | bundles.forEach(bundle => log(bundle)); 158 | } 159 | } else { 160 | log(await designAutomation.listAppBundles()); 161 | } 162 | } catch(err) { 163 | error(err); 164 | } 165 | }); 166 | 167 | program 168 | .command('get-appbundle [bundle-short-id] [bundle-alias]') 169 | .alias('gb') 170 | .description('Get appbundle details.') 171 | .action(async function(bundleShortId, bundleAlias, command) { 172 | try { 173 | if (!bundleShortId) { 174 | bundleShortId = await promptAppBundle(); 175 | } 176 | if (!bundleAlias) { 177 | bundleAlias = await promptAppBundleAlias(bundleShortId); 178 | } 179 | 180 | const bundleFullId = new DesignAutomationID(designAutomation.auth.client_id, bundleShortId, bundleAlias); 181 | const appbundle = await designAutomation.getAppBundle(bundleFullId.toString()); 182 | log(appbundle); 183 | } catch(err) { 184 | error(err); 185 | } 186 | }); 187 | 188 | async function appBundleExists(bundleShortId) { 189 | const appBundleIDs = await designAutomation.listAppBundles(); 190 | const match = appBundleIDs.map(decomposeQualifiedID).find(item => item.id === bundleShortId); 191 | return !!match; 192 | } 193 | 194 | program 195 | .command('create-appbundle [engine-full-id] [description]') 196 | .alias('cb') 197 | .description('Create new app bundle.') 198 | .option('-s, --short', 'Output app bundle ID instead of the entire JSON.') 199 | .option('-u, --update', 'If app bundle already exists, update it.') 200 | .action(async function(bundleShortId, filename, engineFullId, description, command) { 201 | try { 202 | if (!engineFullId) { 203 | engineFullId = await promptEngine(); 204 | } 205 | if (!description) { 206 | description = `${bundleShortId} created via Forge CLI Utils.`; 207 | } 208 | 209 | let exists = false; 210 | if (command.update) { 211 | exists = await appBundleExists(bundleShortId); 212 | } 213 | 214 | let appBundle = exists 215 | ? await designAutomation.updateAppBundle(bundleShortId, engineFullId, null, description) 216 | : await designAutomation.createAppBundle(bundleShortId, engineFullId, null, description); 217 | await uploadAppBundleFile(appBundle, filename); 218 | if (command.short) { 219 | log(appBundle.id); 220 | } else { 221 | log(appBundle); 222 | } 223 | } catch(err) { 224 | error(err); 225 | } 226 | }); 227 | 228 | program 229 | .command('update-appbundle [engine-full-id] [description]') 230 | .alias('ub') 231 | .description('Update existing app bundle.') 232 | .option('-s, --short', 'Output app bundle ID instead of the entire JSON.') 233 | .option('-c, --create', 'If app bundle does not exists, create it.') 234 | .action(async function(bundleShortId, filename, engineFullId, description, command) { 235 | try { 236 | if (!engineFullId) { 237 | engineFullId = await promptEngine(); 238 | } 239 | let exists = true; 240 | if (command.create) { 241 | exists = await appBundleExists(bundleShortId); 242 | } 243 | 244 | let appBundle = exists 245 | ? await designAutomation.updateAppBundle(bundleShortId, engineFullId, null, description) 246 | : await designAutomation.createAppBundle(bundleShortId, engineFullId, null, description); 247 | await uploadAppBundleFile(appBundle, filename); 248 | if (command.short) { 249 | log(appBundle.id); 250 | } else { 251 | log(appBundle); 252 | } 253 | } catch(err) { 254 | console.log(err.response); 255 | error(err); 256 | } 257 | }); 258 | 259 | program 260 | .command('list-appbundle-versions [bundle-short-id]') 261 | .alias('lbv') 262 | .description('List app bundle versions.') 263 | .option('-s, --short', 'Output version numbers instead of the entire JSON.') 264 | .action(async function(bundleShortId, command) { 265 | try { 266 | if (!bundleShortId) { 267 | bundleShortId = await promptAppBundle(); 268 | } 269 | 270 | if (command.short) { 271 | for await (const versions of designAutomation.iterateAppBundleVersions(bundleShortId)) { 272 | versions.forEach(version => log(version)); 273 | } 274 | } else { 275 | log(await designAutomation.listAppBundleVersions(bundleShortId)); 276 | } 277 | } catch(err) { 278 | error(err); 279 | } 280 | }); 281 | 282 | program 283 | .command('list-appbundle-aliases [bundle-short-id]') 284 | .alias('lba') 285 | .description('List app bundle aliases.') 286 | .option('-s, --short', 'Output app bundle aliases instead of the entire JSON.') 287 | .action(async function(bundleShortId, command) { 288 | try { 289 | if (!bundleShortId) { 290 | bundleShortId = await promptAppBundle(); 291 | } 292 | 293 | if (command.short) { 294 | for await (const aliases of designAutomation.iterateAppBundleAliases(bundleShortId)) { 295 | aliases.forEach(alias => log(alias.id)); 296 | } 297 | } else { 298 | log(await designAutomation.listAppBundleAliases(bundleShortId)); 299 | } 300 | } catch(err) { 301 | error(err); 302 | } 303 | }); 304 | 305 | async function appBundleAliasExists(bundleShortId, bundleAlias) { 306 | const appBundleAliases = await designAutomation.listAppBundleAliases(bundleShortId); 307 | const match = appBundleAliases.find(item => item.id === bundleAlias); 308 | return !!match; 309 | } 310 | 311 | program 312 | .command('create-appbundle-alias [bundle-short-id] [bundle-version]') 313 | .alias('cba') 314 | .description('Create new app bundle alias.') 315 | .option('-s, --short', 'Output alias name instead of the entire JSON.') 316 | .option('-u, --update', 'If app bundle alias exists, update it.') 317 | .action(async function(bundleAlias, bundleShortId, bundleVersion, command) { 318 | try { 319 | if (!bundleShortId) { 320 | bundleShortId = await promptAppBundle(); 321 | } 322 | if (!bundleVersion) { 323 | bundleVersion = await promptAppBundleVersion(bundleShortId); 324 | } 325 | 326 | let exists = false; 327 | if (command.update) { 328 | exists = await appBundleAliasExists(bundleShortId, bundleAlias); 329 | } 330 | 331 | let aliasObject = exists 332 | ? await designAutomation.updateAppBundleAlias(bundleShortId, bundleAlias, parseInt(bundleVersion)) 333 | : await designAutomation.createAppBundleAlias(bundleShortId, bundleAlias, parseInt(bundleVersion)); 334 | if (command.short) { 335 | log(aliasObject.id); 336 | } else { 337 | log(aliasObject); 338 | } 339 | } catch(err) { 340 | error(err); 341 | } 342 | }); 343 | 344 | program 345 | .command('update-appbundle-alias [bundle-short-id] [bundle-version]') 346 | .alias('uba') 347 | .description('Update existing app bundle alias.') 348 | .option('-s, --short', 'Output alias name instead of the entire JSON.') 349 | .option('-c, --create', 'If app bundle alias does not exist, create it.') 350 | .action(async function(bundleAlias, bundleShortId, bundleVersion, command) { 351 | try { 352 | if (!bundleShortId) { 353 | bundleShortId = await promptAppBundle(); 354 | } 355 | if (!bundleVersion) { 356 | bundleVersion = await promptAppBundleVersion(bundleShortId); 357 | } 358 | 359 | let exists = true; 360 | if (command.create) { 361 | exists = await appBundleAliasExists(bundleShortId, bundleAlias); 362 | } 363 | 364 | let aliasObject = exists 365 | ? await designAutomation.updateAppBundleAlias(bundleShortId, bundleAlias, parseInt(bundleVersion)) 366 | : await designAutomation.createAppBundleAlias(bundleShortId, bundleAlias, parseInt(bundleVersion)); 367 | if (command.short) { 368 | log(aliasObject.id); 369 | } else { 370 | log(aliasObject); 371 | } 372 | } catch(err) { 373 | error(err); 374 | } 375 | }); 376 | 377 | program 378 | .command('delete-appbundle [bundle-short-id]') 379 | .alias('db') 380 | .description('Delete app bundle with all its aliases and versions.') 381 | .action(async function(bundleShortId, command) { 382 | try { 383 | if (!bundleShortId) { 384 | bundleShortId = await promptAppBundle(); 385 | } 386 | await designAutomation.deleteAppBundle(bundleShortId); 387 | } catch(err) { 388 | error(err); 389 | } 390 | }); 391 | 392 | program 393 | .command('delete-appbundle-alias [bundle-short-id] [alias]') 394 | .alias('dba') 395 | .description('Delete app bundle alias.') 396 | .action(async function(bundleShortId, alias, command) { 397 | try { 398 | if (!bundleShortId) { 399 | bundleShortId = await promptAppBundle(); 400 | } 401 | if (!alias) { 402 | alias = await promptAppBundleAlias(bundleShortId); 403 | } 404 | await designAutomation.deleteAppBundleAlias(bundleShortId, alias); 405 | } catch(err) { 406 | error(err); 407 | } 408 | }); 409 | 410 | program 411 | .command('delete-appbundle-version [bundle-short-id] [version]') 412 | .alias('dbv') 413 | .description('Delete app bundle version.') 414 | .action(async function(bundleShortId, version, command) { 415 | try { 416 | if (!bundleShortId) { 417 | bundleShortId = await promptAppBundle(); 418 | } 419 | if (!version) { 420 | version = await promptAppBundleVersion(bundleShortId); 421 | } 422 | await designAutomation.deleteAppBundleVersion(bundleShortId, parseInt(version)); 423 | } catch(err) { 424 | error(err); 425 | } 426 | }); 427 | 428 | program 429 | .command('list-activities') 430 | .alias('la') 431 | .description('List activities.') 432 | .option('-s, --short', 'Output activity IDs instead of the entire JSON.') 433 | .action(async function(command) { 434 | try { 435 | if (command.short) { 436 | for await (const bundles of designAutomation.iterateActivities()) { 437 | bundles.forEach(bundle => log(bundle)); 438 | } 439 | } else { 440 | log(await designAutomation.listActivities()); 441 | } 442 | } catch(err) { 443 | error(err); 444 | } 445 | }); 446 | 447 | program 448 | .command('get-activity [activity-short-id] [activity-alias]') 449 | .alias('ga') 450 | .description('Get activity details.') 451 | .action(async function(activityShortId, activityAlias, command) { 452 | // TODO: handle situation when no alias is available 453 | try { 454 | if (!activityShortId) { 455 | activityShortId = await promptActivity(true); 456 | } 457 | if (!activityAlias) { 458 | activityAlias = await promptActivityAlias(activityShortId); 459 | } 460 | 461 | const activityId = new DesignAutomationID(designAutomation.auth.client_id, activityShortId, activityAlias); 462 | const workitem = await designAutomation.getActivity(activityId.toString()); 463 | log(workitem); 464 | } catch(err) { 465 | error(err); 466 | } 467 | }); 468 | 469 | let _activityInputs = []; 470 | let _activityOutputs = []; 471 | 472 | function _collectActivityInputs(val) { 473 | _activityInputs.push({ name: val }); 474 | } 475 | 476 | function _collectActivityInputProps(propName, transform = (val) => val) { 477 | return function(val) { 478 | if (_activityInputs.length === 0) { 479 | throw new Error(`Cannot assign property "${propName}" when no --input was provided. See https://github.com/petrbroz/forge-cli-utils/wiki/Design-Automation-Inputs-and-Outputs.`); 480 | } 481 | _activityInputs[_activityInputs.length - 1][propName] = transform(val); 482 | }; 483 | } 484 | 485 | function _collectActivityOutputs(val) { 486 | _activityOutputs.push({ name: val }); 487 | } 488 | 489 | function _collectActivityOutputProps(propName, transform = (val) => val) { 490 | return function(val) { 491 | if (_activityOutputs.length === 0) { 492 | throw new Error(`Cannot assign property "${propName}" when no --output was provided. See https://github.com/petrbroz/forge-cli-utils/wiki/Design-Automation-Inputs-and-Outputs.`); 493 | } 494 | _activityOutputs[_activityOutputs.length - 1][propName] = transform(val); 495 | }; 496 | } 497 | 498 | async function activityExists(activityId) { 499 | const activityIds = await designAutomation.listActivities(); 500 | const match = activityIds.map(decomposeQualifiedID).find(item => item.id === activityId); 501 | return !!match; 502 | } 503 | 504 | function _inventorActivityConfig(activityId, description, ownerId, bundleName, bundleAlias, engine, inputs, outputs) { 505 | const config = { 506 | commandLine: [`$(engine.path)\\InventorCoreConsole.exe /al \"$(appbundles[${bundleName}].path)\"`], 507 | parameters: {}, 508 | description: description, 509 | engine: engine, 510 | appbundles: [`${ownerId}.${bundleName}+${bundleAlias}`] 511 | }; 512 | if (activityId) { 513 | config.id = activityId; 514 | } 515 | if (inputs.length > 0 && Array.isArray(config.commandLine)) { 516 | config.commandLine[0] += ' /i'; 517 | for (const input of inputs) { 518 | config.commandLine[0] += ` \"$(args[${input.name}].path)\"`; 519 | const param = config.parameters[input.name] = { verb: input.verb || 'get' }; 520 | for (const prop of Object.keys(input)) { 521 | if (input.hasOwnProperty(prop) && prop !== 'name') { 522 | param[prop] = input[prop]; 523 | } 524 | } 525 | } 526 | } 527 | for (const output of outputs) { 528 | const param = config.parameters[output.name] = { verb: output.verb || 'put' }; 529 | for (const prop of Object.keys(output)) { 530 | if (output.hasOwnProperty(prop) && prop !== 'name') { 531 | param[prop] = output[prop]; 532 | } 533 | } 534 | } 535 | return config; 536 | } 537 | 538 | function _revitActivityConfig(activityId, description, ownerId, bundleName, bundleAlias, engine, inputs, outputs) { 539 | const config = { 540 | commandLine: [`$(engine.path)\\revitcoreconsole.exe /al \"$(appbundles[${bundleName}].path)\"`], 541 | parameters: {}, 542 | description: description, 543 | engine: engine, 544 | appbundles: [`${ownerId}.${bundleName}+${bundleAlias}`] 545 | }; 546 | if (activityId) { 547 | config.id = activityId; 548 | } 549 | if (inputs.length > 0 && Array.isArray(config.commandLine)) { 550 | config.commandLine[0] += ' /i'; 551 | for (const input of inputs) { 552 | config.commandLine[0] += ` \"$(args[${input.name}].path)\"`; 553 | const param = config.parameters[input.name] = { verb: input.verb || 'get' }; 554 | for (const prop of Object.keys(input)) { 555 | if (input.hasOwnProperty(prop) && prop !== 'name') { 556 | param[prop] = input[prop]; 557 | } 558 | } 559 | } 560 | } 561 | for (const output of outputs) { 562 | const param = config.parameters[output.name] = { verb: output.verb || 'put' }; 563 | for (const prop of Object.keys(output)) { 564 | if (output.hasOwnProperty(prop) && prop !== 'name') { 565 | param[prop] = output[prop]; 566 | } 567 | } 568 | } 569 | return config; 570 | } 571 | 572 | function _autocadActivityConfig(activityId, description, ownerId, bundleName, bundleAlias, engine, inputs, outputs, script) { 573 | const config = { 574 | commandLine: [`$(engine.path)\\accoreconsole.exe /al \"$(appbundles[${bundleName}].path)\"`], 575 | parameters: {}, 576 | description: description, 577 | engine: engine, 578 | appbundles: [`${ownerId}.${bundleName}+${bundleAlias}`] 579 | }; 580 | if (activityId) { 581 | config.id = activityId; 582 | } 583 | if (inputs.length > 0 && Array.isArray(config.commandLine)) { 584 | config.commandLine[0] += ' /i'; 585 | for (const input of inputs) { 586 | config.commandLine[0] += ` \"$(args[${input.name}].path)\"`; 587 | const param = config.parameters[input.name] = { verb: input.verb || 'get' }; 588 | for (const prop of Object.keys(input)) { 589 | if (input.hasOwnProperty(prop) && prop !== 'name') { 590 | param[prop] = input[prop]; 591 | } 592 | } 593 | } 594 | } 595 | for (const output of outputs) { 596 | const param = config.parameters[output.name] = { verb: output.verb || 'put' }; 597 | for (const prop of Object.keys(output)) { 598 | if (output.hasOwnProperty(prop) && prop !== 'name') { 599 | param[prop] = output[prop]; 600 | } 601 | } 602 | } 603 | if (script && Array.isArray(config.commandLine)) { 604 | config.settings = { 605 | script: script 606 | }; 607 | config.commandLine[0] += ' /s \"$(settings[script].path)\"'; 608 | } 609 | return config; 610 | } 611 | 612 | function _3dsmaxActivityConfig(activityId, description, ownerId, bundleName, bundleAlias, engine, inputs, outputs, script) { 613 | const config = { 614 | commandLine: `$(engine.path)\\3dsmaxbatch.exe`, 615 | parameters: {}, 616 | description: description, 617 | engine: engine, 618 | appbundles: [`${ownerId}.${bundleName}+${bundleAlias}`] 619 | }; 620 | if (activityId) { 621 | config.id = activityId; 622 | } 623 | if (inputs.length > 1) { 624 | throw new Error('3dsMax engine only supports single input file.') 625 | } else if (inputs.length > 0) { 626 | const input = inputs[0]; 627 | config.commandLine += ` -sceneFile \"$(args[${input.name}].path)\"`; 628 | const param = config.parameters[input.name] = { verb: input.verb || 'get' }; 629 | for (const prop of Object.keys(input)) { 630 | if (input.hasOwnProperty(prop) && prop !== 'name') { 631 | param[prop] = input[prop]; 632 | } 633 | } 634 | } 635 | for (const output of outputs) { 636 | const param = config.parameters[output.name] = { verb: output.verb || 'put' }; 637 | for (const prop of Object.keys(output)) { 638 | if (output.hasOwnProperty(prop) && prop !== 'name') { 639 | param[prop] = output[prop]; 640 | } 641 | } 642 | } 643 | if (script) { 644 | config.settings = { 645 | script: script 646 | }; 647 | config.commandLine += ' \"$(settings[script].path)\"'; 648 | } 649 | return config; 650 | } 651 | 652 | program 653 | .command('create-activity [bundle-short-id] [bundle-alias] [engine-full-id]') 654 | .alias('ca') 655 | .description('Create new activity.') 656 | .option('-s, --short', 'Output app bundle ID instead of the entire JSON.') 657 | .option('-u, --update', 'If activity already exists, update it.') 658 | .option('-d, --description ', 'Optional activity description.') 659 | .option('--script', 'Optional engine-specific script to pass to activity.') 660 | .option('-i, --input ', 'Activity input ID (can be used multiple times).', _collectActivityInputs) 661 | .option('-iv, --input-verb ', 'Optional HTTP verb for the last activity input ("get" by default; can be used multiple times).', _collectActivityInputProps('verb')) 662 | .option('-iz, --input-zip ', 'Optional zip flag for the last activity input (can be used multiple times).', _collectActivityInputProps('zip', (val) => val.toLowerCase() === 'true')) 663 | .option('-ir, --input-required ', 'Optional required flag for the last activity input (can be used multiple times).', _collectActivityInputProps('required', (val) => val.toLowerCase() === 'true')) 664 | .option('-iod, --input-on-demand', 'Optional ondemand flag for the last activity input (can be used multiple times).', _collectActivityInputProps('ondemand', (val) => val.toLowerCase() === 'true')) 665 | .option('-id, --input-description ', 'Optional description for the last activity input (can be used multiple times).', _collectActivityInputProps('description')) 666 | .option('-iln, --input-local-name ', 'Optional local name for the last activity input (can be used multiple times).', _collectActivityInputProps('localName')) 667 | .option('-o, --output ', 'Activity output ID (can be used multiple times).', _collectActivityOutputs) 668 | .option('-ov, --output-verb ', 'Optional HTTP verb for the last activity output ("put" by default; can be used multiple times).', _collectActivityOutputProps('verb')) 669 | .option('-oz, --output-zip ', 'Optional zip flag for the last activity output (can be used multiple times).', _collectActivityOutputProps('zip', (val) => val.toLowerCase() === 'true')) 670 | .option('-or, --output-required ', 'Optional required flag for the last activity output (can be used multiple times).', _collectActivityOutputProps('required', (val) => val.toLowerCase() === 'true')) 671 | .option('-ood, --output-on-demand', 'Optional ondemand flag for the last activity output (can be used multiple times).', _collectActivityOutputProps('ondemand', (val) => val.toLowerCase() === 'true')) 672 | // TODO: flags like "-or" or "-ood" are causing issues (they are recognized as "-o r" or "-o od"); resolve that issue or get rid of them 673 | .option('-od, --output-description ', 'Optional description for the last activity output (can be used multiple times).', _collectActivityOutputProps('description')) 674 | .option('-oln, --output-local-name ', 'Optional local name for the last activity output (can be used multiple times).', _collectActivityOutputProps('localName')) 675 | .action(async function(activityShortId, bundleShortId, bundleAlias, engineFullId, command) { 676 | try { 677 | if (!bundleShortId) { 678 | bundleShortId = await promptAppBundle(); 679 | } 680 | if (!bundleAlias) { 681 | bundleAlias = await promptAppBundleAlias(bundleShortId); 682 | } 683 | if (!engineFullId) { 684 | engineFullId = await promptEngine(); 685 | } 686 | let description = command.description; 687 | if (!description) { 688 | description = `${activityShortId} created via Forge CLI Utils.`; 689 | } 690 | 691 | let exists = false; 692 | if (command.update) { 693 | exists = await activityExists(activityShortId); 694 | } 695 | 696 | let config = null; 697 | const engineId = DesignAutomationID.parse(engineFullId); 698 | switch (engineId.id) { 699 | case 'AutoCAD': 700 | config = _autocadActivityConfig(activityShortId, description, FORGE_CLIENT_ID, bundleShortId, bundleAlias, engineFullId, _activityInputs, _activityOutputs, command.script); 701 | break; 702 | case '3dsMax': 703 | config = _3dsmaxActivityConfig(activityShortId, description, FORGE_CLIENT_ID, bundleShortId, bundleAlias, engineFullId, _activityInputs, _activityOutputs, command.script) 704 | break; 705 | case 'Revit': 706 | config = _revitActivityConfig(activityShortId, description, FORGE_CLIENT_ID, bundleShortId, bundleAlias, engineFullId, _activityInputs, _activityOutputs); 707 | break; 708 | case 'Inventor': 709 | config = _inventorActivityConfig(activityShortId, description, FORGE_CLIENT_ID, bundleShortId, bundleAlias, engineFullId, _activityInputs, _activityOutputs); 710 | break; 711 | } 712 | 713 | const bundleFullId = new DesignAutomationID(FORGE_CLIENT_ID, bundleShortId, bundleAlias).toString(); 714 | let activity = exists 715 | ? await designAutomation.updateActivity(activityShortId, engineFullId, config.commandLine, bundleFullId, config.parameters, config.settings, description) 716 | : await designAutomation.createActivity(activityShortId, engineFullId, config.commandLine, bundleFullId, config.parameters, config.settings, description); 717 | if (command.short) { 718 | log(activity.id); 719 | } else { 720 | log(activity); 721 | } 722 | } catch(err) { 723 | error(err); 724 | } 725 | }); 726 | 727 | program 728 | .command('update-activity [bundle-short-id] [bundle-alias] [engine-full-id]') 729 | .alias('ua') 730 | .description('Update existing activity.') 731 | .option('-s, --short', 'Output app bundle ID instead of the entire JSON.') 732 | .option('-c, --create', 'If activity does not exist, create it.') 733 | .option('-d, --description ', 'Optional activity description.') 734 | .option('--script', 'Optional engine-specific script to pass to activity.') 735 | .option('-i, --input ', 'Activity input ID (can be used multiple times).', _collectActivityInputs) 736 | .option('-iv, --input-verb ', 'Optional HTTP verb for the last activity input ("get" by default; can be used multiple times).', _collectActivityInputProps('verb')) 737 | .option('-iz, --input-zip ', 'Optional zip flag for the last activity input (can be used multiple times).', _collectActivityInputProps('zip', (val) => val.toLowerCase() === 'true')) 738 | .option('-ir, --input-required ', 'Optional required flag for the last activity input (can be used multiple times).', _collectActivityInputProps('required', (val) => val.toLowerCase() === 'true')) 739 | .option('-iod, --input-on-demand', 'Optional ondemand flag for the last activity input (can be used multiple times).', _collectActivityInputProps('ondemand', (val) => val.toLowerCase() === 'true')) 740 | .option('-id, --input-description ', 'Optional description for the last activity input (can be used multiple times).', _collectActivityInputProps('description')) 741 | .option('-iln, --input-local-name ', 'Optional local name for the last activity input (can be used multiple times).', _collectActivityInputProps('localName')) 742 | .option('-o, --output ', 'Activity output ID (can be used multiple times).', _collectActivityOutputs) 743 | .option('-ov, --output-verb ', 'Optional HTTP verb for the last activity output ("put" by default; can be used multiple times).', _collectActivityOutputProps('verb')) 744 | .option('-oz, --output-zip ', 'Optional zip flag for the last activity output (can be used multiple times).', _collectActivityOutputProps('zip', (val) => val.toLowerCase() === 'true')) 745 | .option('-or, --output-required ', 'Optional required flag for the last activity output (can be used multiple times).', _collectActivityOutputProps('required', (val) => val.toLowerCase() === 'true')) 746 | .option('-ood, --output-on-demand', 'Optional ondemand flag for the last activity output (can be used multiple times).', _collectActivityOutputProps('ondemand', (val) => val.toLowerCase() === 'true')) 747 | .option('-od, --output-description ', 'Optional description for the last activity output (can be used multiple times).', _collectActivityOutputProps('description')) 748 | .option('-oln, --output-local-name ', 'Optional local name for the last activity output (can be used multiple times).', _collectActivityOutputProps('localName')) 749 | .action(async function(activityShortId, bundleShortId, bundleAlias, engineFullId, command) { 750 | try { 751 | if (!bundleShortId) { 752 | bundleShortId = await promptAppBundle(); 753 | } 754 | if (!bundleAlias) { 755 | bundleAlias = await promptAppBundleAlias(bundleShortId); 756 | } 757 | if (!engineFullId) { 758 | engineFullId = await promptEngine(); 759 | } 760 | let description = command.description; 761 | if (!description) { 762 | description = `${activityShortId} created via Forge CLI Utils.`; 763 | } 764 | 765 | let exists = true; 766 | if (command.create) { 767 | exists = await activityExists(activityShortId); 768 | } 769 | 770 | let config = null; 771 | const engineId = DesignAutomationID.parse(engineFullId); 772 | switch (engineId.id) { 773 | case 'AutoCAD': 774 | config = _autocadActivityConfig(id, description, FORGE_CLIENT_ID, bundleShortId, bundleAlias, engineFullId, _activityInputs, _activityOutputs, command.script); 775 | break; 776 | case '3dsMax': 777 | config = _3dsmaxActivityConfig(id, description, FORGE_CLIENT_ID, bundleShortId, bundleAlias, engineFullId, _activityInputs, _activityOutputs, command.script) 778 | break; 779 | case 'Revit': 780 | config = _revitActivityConfig(id, description, FORGE_CLIENT_ID, bundleShortId, bundleAlias, engineFullId, _activityInputs, _activityOutputs); 781 | break; 782 | case 'Inventor': 783 | config = _inventorActivityConfig(id, description, FORGE_CLIENT_ID, bundleShortId, bundleAlias, engineFullId, _activityInputs, _activityOutputs); 784 | break; 785 | } 786 | 787 | let activity = exists 788 | ? await designAutomation.updateActivity(activityShortId, engineFullId, config.commandLine, bundleFullId, config.parameters, config.settings, description) 789 | : await designAutomation.createActivity(activityShortId, engineFullId, config.commandLine, bundleFullId, config.parameters, config.settings, description); 790 | if (command.short) { 791 | log(activity.id); 792 | } else { 793 | log(activity); 794 | } 795 | } catch(err) { 796 | error(err); 797 | } 798 | }); 799 | 800 | program 801 | .command('list-activity-versions [activity-short-id]') 802 | .alias('lav') 803 | .description('List activity versions.') 804 | .option('-s, --short', 'Output version numbers instead of the entire JSON.') 805 | .action(async function(activityShortId, command) { 806 | try { 807 | if (!activityShortId) { 808 | activityShortId = await promptActivity(); 809 | } 810 | 811 | if (command.short) { 812 | for await (const versions of designAutomation.iterateActivityVersions(activityShortId)) { 813 | versions.forEach(version => log(version)); 814 | } 815 | } else { 816 | log(await designAutomation.listActivityVersions(activityShortId)); 817 | } 818 | } catch(err) { 819 | error(err); 820 | } 821 | }); 822 | 823 | program 824 | .command('list-activity-aliases [activity-short-id]') 825 | .alias('laa') 826 | .description('List activity aliases.') 827 | .option('-s, --short', 'Output activity aliases instead of the entire JSON.') 828 | .action(async function(activityShortId, command) { 829 | try { 830 | if (!activityShortId) { 831 | activityShortId = await promptActivity(); 832 | } 833 | 834 | if (command.short) { 835 | for await (const aliases of designAutomation.iterateActivityAliases(activityShortId)) { 836 | aliases.forEach(alias => log(alias.id)); 837 | } 838 | } else { 839 | log(await designAutomation.listActivityAliases(activityShortId)); 840 | } 841 | } catch(err) { 842 | error(err); 843 | } 844 | }); 845 | 846 | async function activityAliasExists(activityShortId, activityAlias) { 847 | const activityAliases = await designAutomation.listActivityAliases(activityShortId); 848 | const match = activityAliases.find(item => item.id === activityAlias); 849 | return !!match; 850 | } 851 | 852 | program 853 | .command('create-activity-alias [activity-short-id] [activity-version]') 854 | .alias('caa') 855 | .description('Create new activity alias.') 856 | .option('-s, --short', 'Output alias name instead of the entire JSON.') 857 | .option('-u, --update', 'If activity alias already exists, update it.') 858 | .action(async function(activityAlias, activityShortId, activityVersion, command) { 859 | try { 860 | if (!activityShortId) { 861 | activityShortId = await promptActivity(); 862 | } 863 | if (!activityVersion) { 864 | activityVersion = await promptActivityVersion(activityShortId); 865 | } 866 | 867 | let exists = false; 868 | if (command.update) { 869 | exists = await activityAliasExists(activityShortId, activityAlias); 870 | } 871 | 872 | let aliasObject = exists 873 | ? await designAutomation.updateActivityAlias(activityShortId, activityAlias, parseInt(activityVersion)) 874 | : await designAutomation.createActivityAlias(activityShortId, activityAlias, parseInt(activityVersion)); 875 | if (command.short) { 876 | log(aliasObject.id); 877 | } else { 878 | log(aliasObject); 879 | } 880 | } catch(err) { 881 | error(err); 882 | } 883 | }); 884 | 885 | program 886 | .command('update-activity-alias [activity-short-id] [activity-version]') 887 | .alias('uaa') 888 | .description('Update existing activity alias.') 889 | .option('-s, --short', 'Output alias name instead of the entire JSON.') 890 | .option('-c, --create', 'If activity alias does not exist, create it.') 891 | .action(async function(activityAlias, activityShortId, activityVersion, command) { 892 | try { 893 | if (!activityShortId) { 894 | activityShortId = await promptActivity(); 895 | } 896 | if (!activityVersion) { 897 | activityVersion = await promptActivityVersion(activityShortId); 898 | } 899 | 900 | let exists = true; 901 | if (command.create) { 902 | exists = await activityAliasExists(activityShortId, activityAlias); 903 | } 904 | 905 | let aliasObject = exists 906 | ? await designAutomation.updateActivityAlias(activityShortId, activityAlias, parseInt(activityVersion)) 907 | : await designAutomation.createActivityAlias(activityShortId, activityAlias, parseInt(activityVersion)); 908 | if (command.short) { 909 | log(aliasObject.id); 910 | } else { 911 | log(aliasObject); 912 | } 913 | } catch(err) { 914 | error(err); 915 | } 916 | }); 917 | 918 | program 919 | .command('delete-activity [activity-short-id]') 920 | .alias('da') 921 | .description('Delete activity with all its aliases and versions.') 922 | .action(async function(activityShortId, command) { 923 | try { 924 | if (!activityShortId) { 925 | activityShortId = await promptActivity(); 926 | } 927 | await designAutomation.deleteActivity(activityShortId); 928 | } catch(err) { 929 | error(err); 930 | } 931 | }); 932 | 933 | program 934 | .command('delete-activity-alias [activity-short-id] [alias]') 935 | .alias('daa') 936 | .description('Delete activity alias.') 937 | .action(async function(activityShortId, alias, command) { 938 | try { 939 | if (!activityShortId) { 940 | activityShortId = await promptActivity(); 941 | } 942 | if (!alias) { 943 | alias = await promptActivityAlias(activityShortId); 944 | } 945 | await designAutomation.deleteActivityAlias(activityShortId, alias); 946 | } catch(err) { 947 | error(err); 948 | } 949 | }); 950 | 951 | program 952 | .command('delete-activity-version [activity-short-id] [version]') 953 | .alias('dav') 954 | .description('Delete activity version.') 955 | .action(async function(activityShortId, version, command) { 956 | try { 957 | if (!activityShortId) { 958 | activityShortId = await promptActivity(); 959 | } 960 | if (!version) { 961 | version = await promptActivityVersion(activityShortId); 962 | } 963 | await designAutomation.deleteActivityVersion(activityShortId, parseInt(version)); 964 | } catch(err) { 965 | error(err); 966 | } 967 | }); 968 | 969 | let _workitemInputs = []; 970 | let _workitemOutputs = []; 971 | 972 | function _collectWorkitemInputs(val) { 973 | _workitemInputs.push({ name: val }); 974 | } 975 | 976 | function _collectWorkitemInputProps(propName, transform = (val) => val) { 977 | return function(val) { 978 | if (_workitemInputs.length === 0) { 979 | throw new Error(`Cannot assign property "${propName}" when no --input was provided. See https://github.com/petrbroz/forge-cli-utils/wiki/Design-Automation-Inputs-and-Outputs.`); 980 | } 981 | _workitemInputs[_workitemInputs.length - 1][propName] = transform(val); 982 | }; 983 | } 984 | 985 | function _collectWorkitemInputHeaders(val) { 986 | if (_workitemInputs.length === 0) { 987 | throw new Error('Cannot assign header property when no --input was provided. See https://github.com/petrbroz/forge-cli-utils/wiki/Design-Automation-Inputs-and-Outputs.'); 988 | } 989 | if (!_workitemInputs[_workitemInputs.length - 1].headers) { 990 | _workitemInputs[_workitemInputs.length - 1].headers = {}; 991 | } 992 | const tokens = val.split(':'); 993 | const name = tokens[0].trim(); 994 | const value = tokens[1].trim(); 995 | _workitemInputs[_workitemInputs.length - 1].headers[name] = value; 996 | } 997 | 998 | function _collectWorkitemOutputs(val) { 999 | _workitemOutputs.push({ name: val }); 1000 | } 1001 | 1002 | function _collectWorkitemOutputProps(propName, transform = (val) => val) { 1003 | return function(val) { 1004 | if (_workitemOutputs.length === 0) { 1005 | throw new Error(`Cannot assign property "${propName}" when no --output was provided. See https://github.com/petrbroz/forge-cli-utils/wiki/Design-Automation-Inputs-and-Outputs.`); 1006 | } 1007 | _workitemOutputs[_workitemOutputs.length - 1][propName] = transform(val); 1008 | }; 1009 | } 1010 | 1011 | function _collectWorkitemOutputHeaders(val) { 1012 | if (_workitemOutputs.length === 0) { 1013 | throw new Error('Cannot assign header property when no --output was provided. See https://github.com/petrbroz/forge-cli-utils/wiki/Design-Automation-Inputs-and-Outputs.'); 1014 | } 1015 | if (!_workitemOutputs[_workitemOutputs.length - 1].headers) { 1016 | _workitemOutputs[_workitemOutputs.length - 1].headers = {}; 1017 | } 1018 | const tokens = val.split(':'); 1019 | const name = tokens[0].trim(); 1020 | const value = tokens[1].trim(); 1021 | _workitemOutputs[_workitemOutputs.length - 1].headers[name] = value; 1022 | } 1023 | 1024 | program 1025 | .command('create-workitem [activity-short-id] [activity-alias]') 1026 | .alias('cw') 1027 | .description('Create new work item.') 1028 | .option('-s, --short', 'Output work item ID instead of the entire JSON.') 1029 | .option('-i, --input ', 'Work item input ID (can be used multiple times).', _collectWorkitemInputs) 1030 | .option('-iu, --input-url ', 'URL of the last work item input (can be used multiple times).', _collectWorkitemInputProps('url')) 1031 | .option('-iv, --input-verb ', 'Optional HTTP verb for the last work item input ("get" by default; can be used multiple times).', _collectWorkitemInputProps('verb')) 1032 | .option('-iln, --input-local-name ', 'Optional local name of the last work item input (can be used multiple times).', _collectWorkitemInputProps('localName')) 1033 | .option('-ih, --input-header ', 'Optional HTTP request header for the last work item input (can be used multiple times).', _collectWorkitemInputHeaders) 1034 | .option('-o, --output ', 'Work item output ID (can be used multiple times).', _collectWorkitemOutputs) 1035 | .option('-ou, --output-url ', 'URL of the last work item output (can be used multiple times).', _collectWorkitemOutputProps('url')) 1036 | .option('-ov, --output-verb ', 'Optional HTTP verb for the last work item output ("put" by default; can be used multiple times).', _collectWorkitemOutputProps('verb')) 1037 | .option('-oln, --output-local-name ', 'Optional local name of the last work item output (can be used multiple times).', _collectWorkitemOutputProps('localName')) 1038 | .option('-oh, --output-header ', 'Optional HTTP request header for the last work item output (can be used multiple times).', _collectWorkitemOutputHeaders) 1039 | .action(async function(activityShortId, activityAlias, command) { 1040 | try { 1041 | if (!activityShortId) { 1042 | activityShortId = await promptActivity(true); 1043 | } 1044 | if (!activityAlias) { 1045 | activityAlias = await promptActivityAlias(activityShortId); 1046 | } 1047 | 1048 | const args = {}; 1049 | for (const input of _workitemInputs) { 1050 | const arg = args[input.name] = { verb: input.verb || 'get' }; 1051 | for (const prop of Object.keys(input)) { 1052 | if (input.hasOwnProperty(prop) && prop !== 'name') { 1053 | arg[prop] = input[prop]; 1054 | } 1055 | } 1056 | } 1057 | for (const output of _workitemOutputs) { 1058 | const arg = args[output.name] = { verb: output.verb || 'put' }; 1059 | for (const prop of Object.keys(output)) { 1060 | if (output.hasOwnProperty(prop) && prop !== 'name') { 1061 | arg[prop] = output[prop]; 1062 | } 1063 | } 1064 | } 1065 | 1066 | const activityId = new DesignAutomationID(FORGE_CLIENT_ID, activityShortId, activityAlias); 1067 | const workitem = await designAutomation.createWorkItem(activityId.toString(), args); 1068 | if (command.short) { 1069 | log(workitem.id); 1070 | } else { 1071 | log(workitem); 1072 | } 1073 | } catch(err) { 1074 | console.error(err.response); 1075 | error(err); 1076 | } 1077 | }); 1078 | 1079 | program 1080 | .command('get-workitem ') 1081 | .alias('gw') 1082 | .description('Get work item details.') 1083 | .option('-s, --short', 'Output work item status instead of the entire JSON.') 1084 | .action(async function(workitemId, command) { 1085 | try { 1086 | const workitem = await designAutomation.getWorkItem(workitemId); 1087 | if (command.short) { 1088 | log(workitem.status); 1089 | } else { 1090 | log(workitem); 1091 | } 1092 | } catch(err) { 1093 | error(err); 1094 | } 1095 | }); 1096 | 1097 | program 1098 | .command('delete-workitem ') 1099 | .alias('dw') 1100 | .description('Delete work item.') 1101 | .action(async function(workitemId, command) { 1102 | try { 1103 | await designAutomation.deleteWorkItem(workitemId); 1104 | } catch(err) { 1105 | error(err); 1106 | } 1107 | }); 1108 | 1109 | program.parse(process.argv); 1110 | if (!program.args.length) { 1111 | program.help(); 1112 | } 1113 | --------------------------------------------------------------------------------