Run this file with Mask,
a CLI task runner defined by a simple markdown file.
These tasks are for bootstrapping new projects;
hence, should be run with the --maskfile option
from a different directory.
One-time, global home directory setup
Setup shell initialization
Saves the old shell initialization script with .old suffix.
Copies the standard profile.sh to the right script for the shell.
In effect, this moves all shell initialization to the scripts in profile.d.
mkdir -p "$HOME/profile.d"
cp "$MASKFILE_DIR/src/home-init/shell/profile.d/*" "$HOME/profile.d"
case "$SHELL" in
*/bash)
SHELL_PROFILE_INTERACTIVE=".bash_profile"
;;
*/zsh)
SHELL_PROFILE_INTERACTIVE=".zshrc"
;;
*)
echo "Unsupported shell: $SHELL"
echo "Manually copy the profile.sh to the appropriate shell profile"
;;
esac
if [[ -f "$HOME/$SHELL_PROFILE_INTERACTIVE" ]]; then
BACKUP_SUFFIX=".backup.$(date +%Y%m%d_%H%M%S)"
cp "$HOME/$SHELL_PROFILE_INTERACTIVE" "$HOME/$SHELL_PROFILE_INTERACTIVE.$BACKUP_SUFFIX"
if [[ -f "$HOME/$SHELL_PROFILE_INTERACTIVE.$BACKUP_SUFFIX" ]]; then
cp "$MASKFILE_DIR/src/home-init/shell/profile.sh" "$HOME/$SHELL_PROFILE_INTERACTIVE"
fi
fiSetup home directory for managing global npm packages
Better installs than npm install -g
because "global" here persists across managed node versions.
The libraries we use are:
@forge/clicorepack: Zero-runtime-dependency package acting as bridge between Node projects and their package managersknip: find & remove unused npm libraries from repospromptfoo: test/evaluate promptssort-package-json: sort keys in the package.jsontsx: run TypeScript code without configuration or compilationturbo: the build system for JavaScript and TypeScript codebases (esp monorepos)vskill: a package manager & builder for securing the AI skills supply chainyarn: a package manager used by many Atlassian repos
Note: yarn is used in many Atlassian internal repos and even in some public examples
(out of habit).
To install yarn, use yarn set version stable via corepack.
Then use yarn init -2 to setup a new repo.
We prefer npm for customer-facing repos
because it's 1 less thing for new Node developers to learn.
eval "$(fnm env --use-on-cd)"
fnm install --lts
fnm use default
# Path set in 10-env-bin.sh
mkdir "$HOME/npm-global"
cd "$HOME/npm-global"
node --version > .nvmrc
npm init --yes
tmp=$(mktemp) && \
jq \
'.name |= "npm-global" | .version |= "0.0.0" | .license |= "Apache-2.0" | .private |= true' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
tmp=$(mktemp) && \
jq \
'.description |= "A better way to manage global package that supports node version switching"' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
tmp=$(mktemp) && \
jq \
'.scripts = {}' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
tmp=$(mktemp) && \
jq \
'.dependencies += {
"@forge/cli":"*",
"corepack":"*",
"knip":"*",
"promptfoo":"*",
"sort-package-json":"*",
"tsx":"*",
"turbo":"*",
"vskill":"*"
}' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
npm installSetup home directory path for user-defined scripts
# Path set in 10-env-bin.sh
mkdir "$HOME/bin"Install shell beautification tools via
brew
These are simply cosmetic
and aren't strictly required for Forge development.
Requires brew.
Beautification adds:
brew_packages=(
macchina
starship
)
brew install ${brew_packages[@]}Install
bunvia the official install script
Uses the official curl-to-shell install method documented at
bun.sh/docs/installation.
curl -fsSL https://bun.sh/install | bashUpdate global home configuration
Default post-creation configuration for Forge Rovo agents
echo "home-update brew"
$MASK home-update brew
echo "home-update node-lts"
$MASK home-update node-lts
echo "home-update npm-global"
$MASK home-update npm-global
echo "home-update bun"
$MASK home-update bunInstall required tools via
brewand Node LTS versions viafnm
Requires brew,
which in turn requires curl, file, and git.
The packages we use are:
fnm: install, maintain, and switch between different versions of node.jsgit-cliff: generate changelogs from git commits using conventional commitsjq: command-line JSON processing used in these scriptsyq: command-line YAML processing used in these scripts
brew_packages=(
fnm
git-cliff
jq
yq
)
brew install ${brew_packages[@]}Update versions of brew packages with
brew update.
brew upgradeUpdate Node LTS versions via
fnm
Manages LTS versions of Node.js corresponding to what's available from Forge.
node_lts=(
iron
jod
krypton
latest
)
for lts in "${node_lts[@]}"; do
fnm install lts/$lts
done
fnm default lts/krypton
fnm use defaultUpdate libraries and versions of npm global packages in npm-global
The libraries we use are:
@forge/clicorepack: Zero-runtime-dependency package acting as bridge between Node projects and their package managersknip: find & remove unused npm libraries from repospromptfoo: test/evaluate promptssort-package-json: sort keys in the package.jsontsx: run TypeScript code without configuration or compilationturbo: the build system for JavaScript and TypeScript codebases (esp monorepos)vskill: a package manager & builder for securing the AI skills supply chainyarn: a package manager used by many Atlassian repos
Note: yarn is used in many Atlassian internal repos and even in some public examples
(out of habit).
To install yarn, use yarn set version stable via corepack.
Then use yarn init -2 to setup a new repo.
We prefer npm for customer-facing repos
because it's 1 less thing for new Node developers to learn.
cd "$HOME/npm-global"
npm_globals=(
@forge/cli
corepack
knip
promptfoo
sort-package-json
tsx
turbo
vskill
)
for lib in "${npm_globals[@]}"; do
tmp=$(mktemp) && \
jq \
--arg lib "$lib" \
'.dependencies += { $lib:"*" }' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
done
npm updateUpdate
bunto the latest version
Uses bun upgrade to update an existing install to the latest version.
See home-init bun for the initial install.
bun upgradeConfigure Rovo Dev CLI with bash commands for Forge dev
commands=(
'tree'
'jq'
'yq'
'npm run'
'npm test'
'forge --help'
'forge environments'
'forge lint'
'forge logs'
'forge version'
'forge whoami'
'knip'
'sort-package-json'
)
for cmd in "${commands[@]}"; do
export cmd
yq \
--inplace \
--prettyPrint \
'.toolPermissions.bash.commands += [{"command": (strenv(cmd) + "(\s.*)?"), "permission": "allow"}]' \
"$HOME/.rovodev/config.yml"
doneCreate new projects
Create a new Node project
npm init --yes
tmp=$(mktemp) && \
jq \
--arg node_version ">=`node --version | cut -c2-`" \
--arg npm_version ">=`npm --version`" \
'.engines = { "node":$node_version, "npm":$npm_version }' \
package.json \
> "$tmp" && \
mv "$tmp" package.jsonCreate a new Forge project
forge createOne-time, post-creation configuration of new projects
These subcommands are expected to be run only once because they are not idempotent. Multiple executions may be distructive, or duplicative.
Run all post-creation configuration for Forge Rovo agents
echo "## git options"
echo "repo-init gitignore"
$MASK repo-init gitignore
echo "repo-init oss"
$MASK repo-init oss
echo "repo-init changelog"
$MASK repo-init changelog
echo "## node.js options"
rm -Rf node_modules
rm package-lock.json
echo "repo-init package"
$MASK repo-init package
echo "repo-init typescript"
$MASK repo-init typescript
echo "repo-init biome"
$MASK repo-init biome
echo "## forge options"
echo "repo-init aidev"
$MASK repo-init aidev
echo "repo-init dev-trigger"
$MASK repo-init dev-trigger
echo "repo-init lifecycle-trigger"
$MASK repo-init lifecycle-trigger
echo "repo-init rovo"
$MASK repo-init rovo
echo "repo-init test"
$MASK repo-init test
echo "repo-init promptfoo"
$MASK repo-init promptfoo
echo "repo-update format-forge"
$MASK repo-update format-forge
echo "repo-update pin-node-version"
$MASK repo-update pin-node-versionDefault post-creation configuration for typical Forge apps
echo "## git options"
echo "repo-init gitignore"
$MASK repo-init gitignore
echo "repo-init oss"
$MASK repo-init oss
echo "repo-init changelog"
$MASK repo-init changelog
echo "## node.js options"
rm -Rf node_modules
rm package-lock.json
echo "repo-init package"
$MASK repo-init package
echo "repo-init typescript"
$MASK repo-init typescript
echo "## forge options"
echo "repo-init aidev"
$MASK repo-init aidev
echo "repo-init test"
$MASK repo-init test
echo "repo-update format-forge"
$MASK repo-update format-forge
echo "repo-update pin-node-version"
$MASK repo-update pin-node-versionInitialize repo with skills, agents instructions, etc used by AI coding agents.
cp $MASKFILE_DIR/src/repo-init/aidev/AGENTS.md .
cp -R $MASKFILE_DIR/src/repo-init/aidev/.agents .Initialize linting & formatting with Biome
npm install --save-dev --save-exact @biomejs/biome
npx @biomejs/biome init
tmp=$(mktemp) && \
jq \
'.vcs.enabled |= true | .vcs.useIgnoreFile |= true | .vcs.defaultBranch = "main"' \
biome.json \
> "$tmp" && \
mv "$tmp" biome.json
tmp=$(mktemp) && \
jq \
'.formatter.indentStyle |= "space"' \
biome.json \
> "$tmp" && \
mv "$tmp" biome.json
tmp=$(mktemp) && \
jq \
'.scripts += {
"format":"biome format --write",
"format:check":"biome format",
"lint:check":"biome lint",
"lint:fix":"biome lint --write"
}' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
cp $MASKFILE_DIR/src/repo-init/format/.editorconfig .Initialize changelog with
git-cliff
git cliff --init github
tmp=$(mktemp) && \
jq \
'.scripts += { "changelog":"git cliff" }' \
package.json \
> "$tmp" && \
mv "$tmp" package.jsonInitialize a
.gitignorefile.
gitignore=(
visualstudiocode
linux
macos
windows
node
turbo
yarn
)
query=$(IFS=, ; echo "${gitignore[*]}")
curl \
--silent \
--location \
https://www.gitignore.io/api/$query \
> .gitignoreInitialize the project with Atlassian OSS scaffolding
files=(
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE
)
for file in "${files[@]}"; do
cp $MASKFILE_DIR/src/repo-init/oss/$file .
done
touch README.md
cp -R $MASKFILE_DIR/src/repo-init/oss/.atlassian .
tmp=$(mktemp) && \
jq \
'.license = "Apache-2.0"' \
package.json \
> "$tmp" && \
mv "$tmp" package.jsonInitialize default values for
package.json
tmp=$(mktemp) &&
jq \
'.version |= "0.0.0" | .license |= "Apache-2.0" | .private |= true' \
package.json \
>"$tmp" &&
mv "$tmp" package.json
tmp=$(mktemp) &&
jq \
--arg name "$(basename "$(pwd)")" \
'.name = $name' \
package.json \
>"$tmp" &&
mv "$tmp" package.json
common_scripts=(
build
changelog
check
clean
dev
forge:deploy
forge:install
forge:uninstall
forge:upgrade
format
format:check
generate
generate:openapi
generate:rovo
lint
lint:check
lint:fix
test
test:coverage
test:watch
prepare
preview
start
todo
typecheck
)
for script in "${common_scripts[@]}"; do
tmp=$(mktemp) &&
jq \
--arg script "$script" \
--arg not_implemented "echo 'Not implemented'" \
'.scripts += { ($script):$not_implemented }' \
package.json \
>"$tmp" &&
mv "$tmp" package.json
done
tmp=$(mktemp) &&
jq \
--arg name "$(basename "$(pwd)")" \
'.name = $name' \
package.json \
>"$tmp" &&
mv "$tmp" package.json
tmp=$(mktemp) &&
jq \
--arg todo "'TODO'" \
--arg package_json "'package.json'" \
--arg message "'No TODOs found!'" \
'.scripts += {
"check":"npm run lint && npm run format:check && npm run typecheck",
"clean":"rm -rf ./dist",
"forge:deploy": "source .env && forge deploy --environment development",
"forge:install": "source .env && forge install --site \"$SITENAME.atlassian.net\" --product $PRODUCT --environment development --non-interactive",
"forge:uninstall": "source .env && forge uninstall --site \"$SITENAME.atlassian.net\" --product $PRODUCT",
"forge:upgrade": "source .env && forge install --upgrade --site \"$SITENAME.atlassian.net\" --product $PRODUCT --environment development --non-interactive",
"generate":"npm run generate:openapi && npm run generate:rovo",
"lint":"npm run lint:check && forge lint",
"todo":"grep -rn \($todo) --exclude=\($package_json) . --color=auto || echo \($message)",
}' \
package.json \
>"$tmp" &&
mv "$tmp" package.json
sort-package-jsonInitialize Node project with standard
promptfooconfiguration
This is compatible with the Rovo configuration (repo-init rovo).
$MASK repo-init test
npm install --save-dev promptfoo
npx promptfoo init
cp $MASKFILE_DIR/src/repo-init/promptfoo/promptfooconfig.yaml .
cp -R $MASKFILE_DIR/src/repo-init/promptfoo/tests .
mkdir -p prompts
touch prompts/agent-instructions.md
tmp=$(mktemp) && \
jq \
'.scripts += { "eval":"promptfoo eval", "view":"promptfoo view" }' \
package.json \
> "$tmp" && \
mv "$tmp" package.jsonInitialize Forge project with standard prompt configuration
$MASK repo-init typescript
npm install --save-dev yaml
yq \
--inplace \
--prettyPrint \
'.modules["rovo:agent"] = (.modules["rovo:agent"] // [{"key": "rovo-agent", "name": "Rovo Agent"}])' \
manifest.yml
mkdir -p prompts
touch prompts/agent-instructions.md
yq \
--inplace \
--prettyPrint \
'.resources += [{ "key":"agent-prompts", "path":"prompts" }]' \
manifest.yml
yq \
--inplace \
--prettyPrint \
'.modules["rovo:agent"][].prompt = "resource:agent-prompts;agent-instructions.md"' \
manifest.yml
tmp=$(mktemp) && \
jq \
'.scripts += { "generate:rovo":"tsx ./scripts/actiontypes.ts && npm run format" }' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
mkdir -p src/rovo
cp $MASKFILE_DIR/src/rovo/src/rovo/action.ts src/rovo
mkdir -p scripts
cp $MASKFILE_DIR/src/rovo/scripts/actiontypes.ts scriptsInitialize TypeScript for a Node project
- TypeScript is JavaScript with syntax for types.
tsxruns TypeScript code without configuration or compilation.- openapi-typescript is a library for converting OpenAPI 3.0/3.1 schemas to TypeScript types and type-safe fetching.
modules=(
openapi-typescript
tsx
typescript@5
@types/node
)
npm install --save-dev ${modules[@]}
tmp=$(mktemp) && \
jq \
'.scripts += {
"build":"tsc",
"generate:openapi":"openapi-typescript",
"typecheck":"tsc --noEmit",
}' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
cp $MASKFILE_DIR/src/repo-init/typescript/tsconfig.json .
cp $MASKFILE_DIR/src/repo-init/typescript/redocly.yaml .
for file in $(find src -name "*.jsx"); do mv "$file" "${file%.jsx}.tsx"; done
for file in $(find src -name "*.js"); do mv "$file" "${file%.js}.ts"; doneInitialize testing with Vitest, ArchUnitTS, and initial Forge tests
$MASK repo-init typescript
modules=(
vitest
archunit
)
npm install --save-dev ${modules[@]}
tmp=$(mktemp) && \
jq \
'.scripts += {
"test": "vitest run",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage",
}' \
package.json \
> "$tmp" && \
mv "$tmp" package.json
cat << 'EOF' > vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
},
});
EOF
cp -R $MASKFILE_DIR/src/repo-init/tests .Initialize Forge project with webtrigger configuration for testing
$MASK repo-init typescript
yq \
--inplace \
--prettyPrint \
'.modules.webtrigger += [{ "key":"dev-trigger", "function":"trigger", "response":{ "type":"dynamic" }}]' \
manifest.yml
yq \
--inplace \
--prettyPrint \
'.modules.function += [{ "key":"trigger", "handler":"index.trigger" }]' \
manifest.yml
mkdir -p src/forge
cp $MASKFILE_DIR/src/forge/events.ts src/forge
cp $MASKFILE_DIR/src/forge/trigger.ts src/forge
echo 'export { trigger } from "./forge/trigger";' >> src/index.tsInitialize Forge project with lifecycle for observability
$MASK repo-init typescript
yq \
--inplace \
--prettyPrint \
'.modules.trigger += [{ "key":"trigger-lifecycle", "function":"lifecycle-handler", "events":[ "avi:forge:installed:app", "avi:forge:upgraded:app"] }]' \
manifest.yml
yq \
--inplace \
--prettyPrint \
'.modules.function += [{ "key":"lifecycle-handler", "handler":"index.lifecycle" }]' \
manifest.yml
mkdir -p src/forge
cp $MASKFILE_DIR/src/forge/events.ts src/forge
cp $MASKFILE_DIR/src/forge/lifecycle.ts src/forge
echo 'export { lifecycle } from "./forge/lifecycle";' >> src/index.tsInitialize Node project with standard env var configuration
Not yet implemented.
Update existing project configurations
These subcommands are expected to be run at regular intervals because they are idempotent. These commands should not destroy any existing code or configuration.
Format the
manifest.ymlfor a freshly created Forge project
modules_pick='.modules |= pick( (["rovo:agent", "action"] + keys - ["function"]) | unique + ["function"])'
submodules_pick='.modules[][] |= pick( (["key", "name", "title", "description"] + keys) | unique)'
inputs_pick='.modules.action[].inputs[] |= pick( (["key", "name", "title", "description"] + keys) | unique)'
yq \
--inplace \
--prettyPrint \
"sort_keys(..) | $modules_pick | $submodules_pick | $inputs_pick" \
manifest.yml
yq \
--inplace \
--prettyPrint \
"del(.. | select(length == 0))" \
manifest.ymlPin the project's Node version to what is currently available
node --version > .nvmrc
tmp=$(mktemp) && \
jq \
--arg node_version "$(node --version | cut -c2-)" \
'.engines = { "node":$node_version }' \
package.json \
> "$tmp" && \
mv "$tmp" package.json