@@ -46,7 +46,12 @@ check_prereqs() {
4646 local missing=()
4747 command -v git > /dev/null 2>&1 || missing+=(" git" )
4848 command -v bun > /dev/null 2>&1 || missing+=(" bun (https://bun.sh)" )
49- command -v aws > /dev/null 2>&1 || missing+=(" aws-cli v2 (https://aws.amazon.com/cli/)" )
49+ if command -v aws > /dev/null 2>&1 ; then
50+ aws --version 2>&1 | grep -q ' ^aws-cli/2\.' \
51+ || missing+=(" aws-cli v2 (found v1 — upgrade: https://aws.amazon.com/cli/)" )
52+ else
53+ missing+=(" aws-cli v2 (https://aws.amazon.com/cli/)" )
54+ fi
5055 if [ ${# missing[@]} -gt 0 ]; then
5156 die " Missing prerequisites:$( printf ' \n • %s' " ${missing[@]} " ) "
5257 fi
@@ -62,36 +67,62 @@ gather_inputs() {
6267 printf ' Clone directory [%s]: ' " $HOME /opencode" > /dev/tty
6368 IFS= read -r CLONE_DIR < /dev/tty
6469 CLONE_DIR=" ${CLONE_DIR:- $HOME / opencode} "
70+ # Expand leading ~ to $HOME before validation and use
71+ CLONE_DIR=" ${CLONE_DIR/# \~ / $HOME } "
72+ # Reject shell metacharacters that could corrupt the rc file
73+ case " $CLONE_DIR " in
74+ * [' |;&$`<>(){}!^*?' \\ ]* )
75+ die " Invalid clone directory — shell metacharacters are not allowed" ;;
76+ esac
77+ # Reject control characters (especially newlines)
78+ if [[ " $CLONE_DIR " =~ [[:cntrl:]] ]]; then
79+ die " Invalid clone directory — control characters are not allowed"
80+ fi
6581
6682 while true ; do
6783 printf ' AWS account ID: ' > /dev/tty
6884 IFS= read -r ACCOUNT_ID < /dev/tty
69- [ -n " ${ACCOUNT_ID:- } " ] && break
70- warn " AWS account ID is required."
85+ [ -n " ${ACCOUNT_ID:- } " ] || { warn " AWS account ID is required." ; continue ; }
86+ # AWS account IDs are exactly 12 digits
87+ [[ " $ACCOUNT_ID " =~ ^[0-9]{12}$ ]] || { warn " AWS account ID must be exactly 12 digits." ; continue ; }
88+ break
7189 done
7290
7391 printf ' Preferred AWS region [us-east-1]: ' > /dev/tty
7492 IFS= read -r AWS_REGION < /dev/tty
7593 AWS_REGION=" ${AWS_REGION:- us-east-1} "
94+ # AWS regions match the pattern: two-or-more-letters, dash, letters, dash, digit(s)
95+ [[ " $AWS_REGION " =~ ^[a-z]{2,}-[a-z]+-[0-9]+$ ]] \
96+ || die " Invalid AWS region format (expected e.g. us-east-1, eu-west-2)"
7697 echo
7798}
7899
79100# ── step 3: clone & build ─────────────────────────────────────────────────────
80101clone_and_build () {
81102 if [ -d " $CLONE_DIR /.git" ]; then
82103 warn " Directory $CLONE_DIR already exists — skipping clone"
83- info " Fetching and checking out $BRANCH branch..."
104+ # Refuse to proceed over uncommitted changes to avoid silent data loss.
105+ if ! git -C " $CLONE_DIR " diff --quiet || ! git -C " $CLONE_DIR " diff --cached --quiet; then
106+ die " $CLONE_DIR has uncommitted changes — commit or stash them and re-run"
107+ fi
108+ local current
109+ current=$( git -C " $CLONE_DIR " rev-parse --abbrev-ref HEAD)
110+ if [ " $current " != " $BRANCH " ]; then
111+ die " $CLONE_DIR is on branch '$current ', not '$BRANCH ' — switch manually and re-run"
112+ fi
113+ info " Fetching latest $BRANCH branch..."
84114 git -C " $CLONE_DIR " fetch origin
85- git -C " $CLONE_DIR " checkout " $BRANCH "
115+ git -C " $CLONE_DIR " merge --ff-only " origin/ $BRANCH "
86116 else
87117 info " Cloning $REPO_URL → $CLONE_DIR ..."
88- git clone --branch " $BRANCH " " $REPO_URL " " $CLONE_DIR "
118+ git clone --depth 1 -- branch " $BRANCH " " $REPO_URL " " $CLONE_DIR "
89119 fi
90120
91121 info " Installing dependencies..."
92122 # BUN_CONFIG_REGISTRY override prevents private registry redirects (e.g. CMS Artifactory)
93123 # from breaking public package resolution.
94- (cd " $CLONE_DIR " && BUN_CONFIG_REGISTRY=https://registry.npmjs.org bun install)
124+ # --frozen-lockfile ensures bun.lock is respected and no unexpected version upgrades occur.
125+ (cd " $CLONE_DIR " && BUN_CONFIG_REGISTRY=https://registry.npmjs.org bun install --frozen-lockfile)
95126
96127 info " Building opencode binary (this takes about a minute)..."
97128 # OPENCODE_CHANNEL=flex ensures every Flexion fork build — regardless of which git
@@ -107,23 +138,20 @@ clone_and_build() {
107138# ── step 4: AWS SSO config ────────────────────────────────────────────────────
108139write_aws_config () {
109140 mkdir -p " $HOME /.aws"
110- local config=" $HOME /.aws/config"
111-
112- if grep -q " \[profile $AWS_PROFILE \]" " $config " 2> /dev/null; then
113- warn " AWS profile [$AWS_PROFILE ] already in $config — skipping"
141+ # Use aws configure set for atomic, structured writes — avoids raw cat >> which can
142+ # corrupt ~/.aws/config if a concurrent write leaves an unterminated section header.
143+ # aws configure get doubles as an idempotency check without relying on fragile grep.
144+ if aws configure get sso_start_url --profile " $AWS_PROFILE " > /dev/null 2>&1 ; then
145+ warn " AWS profile [$AWS_PROFILE ] already configured — skipping"
114146 return
115147 fi
116148
117- info " Writing AWS SSO profile to $config ..."
118- cat >> " $config " << EOF
119-
120- [profile $AWS_PROFILE ]
121- sso_start_url = $SSO_START_URL
122- sso_region = $SSO_REGION
123- sso_account_id = $ACCOUNT_ID
124- sso_role_name = $SSO_ROLE_NAME
125- region = $AWS_REGION
126- EOF
149+ info " Writing AWS SSO profile via aws configure set..."
150+ aws configure set sso_start_url " $SSO_START_URL " --profile " $AWS_PROFILE "
151+ aws configure set sso_region " $SSO_REGION " --profile " $AWS_PROFILE "
152+ aws configure set sso_account_id " $ACCOUNT_ID " --profile " $AWS_PROFILE "
153+ aws configure set sso_role_name " $SSO_ROLE_NAME " --profile " $AWS_PROFILE "
154+ aws configure set region " $AWS_REGION " --profile " $AWS_PROFILE "
127155 success " AWS profile [$AWS_PROFILE ] written"
128156}
129157
@@ -191,6 +219,7 @@ write_opencode_config() {
191219 }
192220}
193221EOF
222+ chmod 600 " $config_file "
194223 success " opencode config written"
195224}
196225
@@ -202,35 +231,46 @@ write_shell_alias() {
202231 * ) rc_file=" $HOME /.bashrc" ;;
203232 esac
204233
205- if grep -q " opencode-work()" " $rc_file " 2> /dev/null; then
234+ # Sentinel comment is the idempotency guard — more reliable than matching the
235+ # function signature, which a user might remove while leaving a stale comment.
236+ if grep -qF " ── Flexion opencode launcher ──" " $rc_file " 2> /dev/null; then
206237 warn " opencode-work() already defined in $rc_file — skipping"
207238 return
208239 fi
209240
210241 info " Appending opencode-work() to $rc_file ..."
211242
212- # Single-quoted heredoc prevents variable expansion inside the function body.
213- # CLONE_DIR_PLACEHOLDER is substituted after writing via perl.
214- cat >> " $rc_file " << 'SHELL_EOF '
243+ # Use printf %q to shell-quote the clone path at install time.
244+ # This eliminates the previous Perl s|...|..| substitution which was vulnerable
245+ # to injection if CLONE_DIR contained a | character.
246+ local clone_dir_q
247+ clone_dir_q=$( printf ' %q' " $CLONE_DIR " )
248+
249+ # Double-quoted heredoc: $clone_dir_q expands now (install time);
250+ # all other $ references are \-escaped so they expand at shell run time.
251+ cat >> " $rc_file " << SHELL_EOF
215252
216253# ── Flexion opencode launcher ─────────────────────────────────────────────────
217254opencode-work() {
218255 local profile="ClaudeCodeAccess"
256+ local clone_dir=$clone_dir_q
219257 local arch os
220- case "$(uname -m)" in arm64|aarch64) arch="arm64" ;; *) arch="x64" ;; esac
221- case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; *)
222- echo "Unsupported OS: $(uname -s)" && return 1 ;; esac
223- echo "Logging in to AWS SSO ($profile)..."
224- aws sso login --profile "$profile" || return 1
225- eval "$(aws configure export-credentials --profile "$profile" --format env)"
226- "CLONE_DIR_PLACEHOLDER/packages/opencode/dist/opencode-${os}-${arch}/bin/opencode" "$@"
258+ case "\$ (uname -m)" in arm64|aarch64) arch="arm64" ;; *) arch="x64" ;; esac
259+ case "\$ (uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; *)
260+ echo "Unsupported OS: \$ (uname -s)" && return 1 ;; esac
261+ echo "Logging in to AWS SSO (\$ profile)..."
262+ aws sso login --profile "\$ profile" || return 1
263+ # Subshell confines exported credentials to the opencode process — they do not
264+ # persist in the interactive shell environment after opencode exits.
265+ (
266+ eval "\$ (aws configure export-credentials --profile "\$ profile" --format env)"
267+ export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
268+ "\$ {clone_dir}/packages/opencode/dist/opencode-\$ {os}-\$ {arch}/bin/opencode" "\$ @"
269+ )
227270}
228271# ─────────────────────────────────────────────────────────────────────────────
229272SHELL_EOF
230273
231- # Substitute actual clone path. Uses | as delimiter to safely handle / in paths.
232- perl -i -pe " s|CLONE_DIR_PLACEHOLDER|${CLONE_DIR} |g" " $rc_file "
233-
234274 success " opencode-work() added to $rc_file "
235275}
236276
@@ -245,8 +285,8 @@ main() {
245285
246286 local rc_file
247287 case " $( basename " ${SHELL:- bash} " ) " in
248- zsh) rc_file=" ~ /.zshrc" ;;
249- * ) rc_file=" ~ /.bashrc" ;;
288+ zsh) rc_file=" $HOME /.zshrc" ;;
289+ * ) rc_file=" $HOME /.bashrc" ;;
250290 esac
251291
252292 printf ' \n%s%sInstallation complete!%s\n' " $GREEN " " $BOLD " " $NC "
0 commit comments