Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,18 @@ jobs:
set -e
echo "status=${status}" >> "${GITHUB_OUTPUT}"

- name: Finalize Zsh Syntax Result
- name: Run Zsh Regression Tests
id: zsh_tests
if: steps.zsh_syntax.outputs.status == '0'
run: zsh tests/installomator-label-parser.zsh

Comment on lines +272 to +276
- name: Finalize Zsh Validation Result
if: success() || failure()
shell: bash
run: |
target_count="${{ steps.collect_targets.outputs.count }}"
syntax_status="${{ steps.zsh_syntax.outputs.status }}"
tests_outcome="${{ steps.zsh_tests.outcome }}"

if [[ "${target_count}" == "0" ]]; then
{
Expand All @@ -286,19 +292,20 @@ jobs:
fi

{
echo "### Zsh Syntax"
echo "### Zsh Validation"
echo
echo "- Files scanned: ${target_count}"
echo "- Regression tests: ${tests_outcome}"
} >> "${GITHUB_STEP_SUMMARY}"

if [[ "${syntax_status}" == "0" ]]; then
if [[ "${syntax_status}" == "0" && "${tests_outcome}" == "success" ]]; then
echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "✅ All zsh files passed \`zsh -n\`" >> "${GITHUB_STEP_SUMMARY}"
echo "✅ Zsh syntax and regression tests passed" >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi

echo "" >> "${GITHUB_STEP_SUMMARY}"
echo "❌ zsh syntax errors found" >> "${GITHUB_STEP_SUMMARY}"
echo "❌ Zsh validation failed" >> "${GITHUB_STEP_SUMMARY}"
exit 1

shellcheck:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to this project are documented in this file.

## 1.2.0 - 19-Aug-2026
- Fixed validation of Installomator labels declared in multiline alias arms (Bug Report #16)
- Added `selectionDialogDefaultChecked` to configure default selection for interactive-mode items (while keeping already-installed items disabled and unchecked; thanks for FR #14, @jeffmw777!)
- Updated `codex` Validation Path

## 1.1.0 - 04-Aug-2026
- Normalize surrounding straight and smart quotes in silent-mode CSV item IDs before lookup (thanks for the heads-up, @applegurutim!)
- Clarify that Silent Mode Parameter 5 expects configured item identifiers, not Jamf command strings.
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
![GitHub release (latest by date)](https://img.shields.io/github/v/release/Setup-Your-Mac/SYM-Lite?display_name=tag) ![GitHub issues](https://img.shields.io/github/issues-raw/Setup-Your-Mac/SYM-Lite) ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/Setup-Your-Mac/SYM-Lite) ![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/Setup-Your-Mac/SYM-Lite) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Setup-Your-Mac/SYM-Lite) [![swiftDialog](https://img.shields.io/badge/swiftDialog-Enabled-blue)](https://swiftdialog.app) [![Semgrep Security Scan](https://img.shields.io/badge/security%20scanned%20by-Semgrep-00C7B7?style=flat&logo=semgrep&logoColor=white)](https://semgrep.dev)

# SYM-Lite (1.1.0)
# SYM-Lite (1.2.0)

> **SYM-Lite** is a lean, purpose-built script for executing MDM-agnostic [Installomator labels](https://github.com/Installomator/Installomator/tree/main/fragments/labels) and [Homebrew](https://brew.sh) casks / formulas, as well as Jamf Pro-specific [policy triggers](https://learn.jamf.com/r/en-US/jamf-pro-documentation-current/Triggers_for_Policies), all through a unified [swiftDialog](https://swiftdialog.app) selection and reporting interface.

Expand Down Expand Up @@ -71,7 +71,7 @@ installomatorLabels=(
)
```

At runtime, SYM-Lite validates each configured label against `organizationInstallomatorFile` before building the picker or accepting silent-mode CSV input. If a label is missing from that Installomator file, or if the Installomator file is unavailable or unreadable, SYM-Lite logs a warning or error and removes Installomator labels from the current run while leaving other item types available.
At runtime, SYM-Lite validates each configured label against single-line and continued top-level alias arms in `organizationInstallomatorFile` before building the picker or accepting silent-mode CSV input. If a label is missing from that Installomator file, or if the Installomator file is unavailable, unreadable, or cannot be parsed, SYM-Lite logs a warning or error and removes Installomator labels from the current run while leaving other item types available.

### Adding Homebrew Items

Expand Down Expand Up @@ -155,12 +155,14 @@ sudo ~/Downloads/SYM-Lite.zsh
```

**User experience:**
1. Selection dialog appears with all configured items
1. Selection dialog appears with all configured items; selectable items start checked when `selectionDialogDefaultChecked="true"`
2. User selects one or more items using checkboxes
3. Inspect Mode dialog launches showing real-time progress
4. Completion report shows one row per selected item
5. Optional restart prompt

`selectionDialogDefaultChecked` affects interactive mode only. Users can deselect prechecked items before continuing, and already-installed items disabled by status sublabels remain unchecked. Silent mode continues to select items exclusively from `operationsCSV`.

If the user clicks `Cancel` in the selection dialog, interactive mode exits cleanly without launching Inspect Mode. If `selectionDialogStatusSublabelsEnabled="true"` and every remaining valid item is already installed, interactive mode shows an informational dialog and exits without launching Inspect Mode. If no valid items remain after configuration validation, interactive mode exits cleanly with a generic unavailable-items message.

**Interactive mode requirements:**
Expand Down Expand Up @@ -330,6 +332,7 @@ swiftDialog's [Inspect Mode](https://swiftdialog.app/advanced/inspect-mode/) use
| `organizationOverlayiconURL` | swiftDialog logo | Overlay icon URL |
| `mainDialogIcon` | GitHub raw `SYM_icon.png` URL | Main dialog icon |
| `fontSize` | `"14"` | Dialog message font size |
| `selectionDialogDefaultChecked` | `"true"` | Start selectable interactive-mode items checked |
| `selectionDialogStatusSublabelsEnabled` | `"true"` | Show install-state sublabels, disable already-installed items, and exit cleanly if no selectable items remain |
| `restartPromptEnabled` | `"true"` | Show restart prompt after completion |
| `scriptLog` | `/var/log/...log` | Client-side log path |
Expand All @@ -338,6 +341,6 @@ swiftDialog's [Inspect Mode](https://swiftdialog.app/advanced/inspect-mode/) use

(The rest of the document — Logging, Troubleshooting, Testing Checklist, Next Steps, and Support — remains unchanged as the reordering was already applied where relevant.)

**Version:** 1.1.0
**Date:** 04-Aug-2026
**Version:** 1.2.0
**Date:** 19-Aug-2026
**Author:** Dan K. Snelson (@dan-snelson)
62 changes: 54 additions & 8 deletions SYM-Lite.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@
#
# HISTORY
#
# Version 1.1.0, 04-Aug-2026, Dan K. Snelson (@dan-snelson)
# - Normalize surrounding straight and smart quotes in silent-mode CSV item IDs before lookup (thanks for the heads-up, @applegurutim!)
# - Clarify that Silent Mode Parameter 5 expects configured item identifiers, not Jamf command strings.
# - Fix silent-mode Parameter 5 parsing when Jamf passes multiple comma-separated item IDs wrapped in one quoted CSV string (thanks for another heads-up, @applegurutim!)
# - Updates for OpenAI renaming "Codex.app" to "ChatGPT.app"
# Version 1.2.0, 19-Aug-2026, Dan K. Snelson (@dan-snelson)
# - Fixed validation of Installomator labels declared in multiline alias arms (Bug Report #16)
# - Added `selectionDialogDefaultChecked` to configure default selection for interactive-mode items (while keeping already-installed items disabled and unchecked; thanks for FR #14, @jeffmw777!)
# - Updated `codex` Validation Path
#
####################################################################################################

Expand All @@ -37,7 +36,7 @@ setopt NONOMATCH
setopt TYPESET_SILENT

# Script Version
scriptVersion="1.1.0"
scriptVersion="1.2.0"

# Script Human-readable Name
humanReadableScriptName="Setup Your Mac Lite: Developer Edition"
Expand Down Expand Up @@ -105,6 +104,7 @@ mainDialogIcon="https://raw.githubusercontent.com/setup-your-mac/Setup-Your-Mac/

# Dialog presentation defaults
fontSize="14"
selectionDialogDefaultChecked="true"
selectionDialogStatusSublabelsEnabled="true"

# Restart prompt behavior
Expand All @@ -123,7 +123,7 @@ installomatorLabels=(
"awsvpnclient | AWS VPN Client | /Applications/AWS VPN Client/AWS VPN Client.app | https://usw2.ics.services.jamfcloud.com/icon/hash_1d1bef5523d9f7eca5a45f2db9a63732e85edb5f914220807ca740ba7c4881b9"
"bruno | Bruno | /Applications/Bruno.app | https://usw2.ics.services.jamfcloud.com/icon/hash_48501630ad2f5dd5de3e055d6acdda07682895440cad366ee7befac71cab1399"
"charles | Charles Proxy | /Applications/Charles.app | https://use2.ics.services.jamfcloud.com/icon/hash_59b395ca81889a6d83deda8e6babc5ae4bc5931d36a72b738fe30b84d027593d"
"codex | OpenAI ChatGPT Codex | /Applications/ChatGPT.app | https://usw2.ics.services.jamfcloud.com/icon/hash_be9d2917e81980484f875d9056e5e4aa45d59dffa7b03c20f8dbb5137e96ee26"
"codex | OpenAI ChatGPT Codex | /Applications/ChatGPT.localized/ChatGPT.app | https://usw2.ics.services.jamfcloud.com/icon/hash_be9d2917e81980484f875d9056e5e4aa45d59dffa7b03c20f8dbb5137e96ee26"
"docker | Docker | /Applications/Docker.app | https://usw2.ics.services.jamfcloud.com/icon/hash_a344dca5fdc0e86822e8f21ec91088e6591b1e292bdcebdee1281fbd794c2724"
"jetbrainsintellijidea | IntelliJ IDEA | /Applications/IntelliJ IDEA.app | https://usw2.ics.services.jamfcloud.com/icon/hash_f669d73acc06297e1fc2f65245cfbdace03263f81aebf95444a8360a101b239d"
"pique | Pique | /Applications/Pique.app | https://usw2.ics.services.jamfcloud.com/icon/hash_7d2539860cca6ec5ea5a71cba2aee7d93b9534e4267c16f73c7035f3dc025b9c"
Expand Down Expand Up @@ -426,6 +426,7 @@ function getSelectionDialogCheckboxesJSON() {
local checkboxLabel=""
local escapedCheckboxLabel=""
local escapedIconURL=""
local checkboxChecked="false"
local checkboxDisabled="false"
local itemID=""
local existingLabel=""
Expand Down Expand Up @@ -482,10 +483,16 @@ function getSelectionDialogCheckboxesJSON() {
selectionDialogOptionRecords+=("${itemID}")
fi

if [[ "${selectionDialogDefaultChecked:l}" == "true" ]] && [[ "${checkboxDisabled}" == "false" ]]; then
checkboxChecked="true"
else
checkboxChecked="false"
fi

escapedCheckboxLabel=$(escapeJSONString "${checkboxLabel}")
escapedItemID=$(escapeJSONString "${itemID}")
escapedIconURL=$(escapeJSONString "${itemIconURL}")
checkboxItemsJSON="${checkboxItemsJSON}${separator}{\"label\":\"${escapedCheckboxLabel}\",\"name\":\"${escapedItemID}\",\"checked\":false,\"disabled\":${checkboxDisabled},\"icon\":\"${escapedIconURL}\"}"
checkboxItemsJSON="${checkboxItemsJSON}${separator}{\"label\":\"${escapedCheckboxLabel}\",\"name\":\"${escapedItemID}\",\"checked\":${checkboxChecked},\"disabled\":${checkboxDisabled},\"icon\":\"${escapedIconURL}\"}"
separator=","
done

Expand All @@ -510,6 +517,7 @@ function getAvailableInstallomatorLabels() {
BEGIN {
inLabelCase = 0
caseDepth = 0
continuedArm = ""
}

/^[[:space:]]*case[[:space:]]+\$label[[:space:]]+in[[:space:]]*$/ {
Expand All @@ -519,6 +527,31 @@ function getAvailableInstallomatorLabels() {
}

inLabelCase {
if (continuedArm != "") {
if (caseDepth != 1) {
exit 2
}

if ($0 ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_|-]*\|\\[[:space:]]*$/) {
labelFragment = $0
sub(/^[[:space:]]*/, "", labelFragment)
sub(/\\[[:space:]]*$/, "", labelFragment)
continuedArm = continuedArm labelFragment
next
}

if ($0 ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_|-]*\)[[:space:]]*$/) {
labelFragment = $0
sub(/^[[:space:]]*/, "", labelFragment)
sub(/\)[[:space:]]*$/, "", labelFragment)
print continuedArm labelFragment
continuedArm = ""
next
}

exit 2
}

if ($0 ~ /^[[:space:]]*case[[:space:]].*[[:space:]]+in[[:space:]]*$/) {
caseDepth++
next
Expand All @@ -532,13 +565,26 @@ function getAvailableInstallomatorLabels() {
next
}

if (caseDepth == 1 && $0 ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_|-]*\|\\[[:space:]]*$/) {
continuedArm = $0
sub(/^[[:space:]]*/, "", continuedArm)
sub(/\\[[:space:]]*$/, "", continuedArm)
next
}

if (caseDepth == 1 && $0 ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_|-]*\)[[:space:]]*$/) {
labelArm = $0
sub(/^[[:space:]]*/, "", labelArm)
sub(/\)[[:space:]]*$/, "", labelArm)
print labelArm
}
}

END {
if (continuedArm != "") {
exit 2
}
}
' "${organizationInstallomatorFile}"); then
return 1
fi
Expand Down
136 changes: 136 additions & 0 deletions tests/installomator-label-parser.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/bin/zsh --no-rcs

setopt ERR_EXIT
setopt NO_UNSET
setopt PIPE_FAIL

repoRoot="${0:A:h:h}"
scriptPath="${repoRoot}/SYM-Lite.zsh"
temporaryDirectory="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/sym-lite-parser-tests.XXXXXX")"
validFixture="${temporaryDirectory}/Installomator-valid.zsh"
incompleteFixture="${temporaryDirectory}/Installomator-incomplete.zsh"

function cleanup() {
/bin/rm -rf -- "${temporaryDirectory}"
}
trap cleanup EXIT

function fail() {
print -u2 -r -- "FAIL: $1"
exit 1
}

function extractFunction() {
local functionName="$1"

/usr/bin/awk -v functionName="${functionName}" '
$0 == "function " functionName "() {" {
capture = 1
}

capture {
print
}

capture && /^}$/ {
exit
}
' "${scriptPath}"
}

function loadProductionFunction() {
local functionName="$1"
local functionSource=""

functionSource="$(extractFunction "${functionName}")"
[[ -n "${functionSource}" ]] || fail "Unable to extract ${functionName} from SYM-Lite.zsh"
eval "${functionSource}"
}

function assertOutputContains() {
local output="$1"
local expected="$2"

print -r -- "${output}" | /usr/bin/grep -Fqx -- "${expected}" \
|| fail "Expected parsed label '${expected}'"
}

function assertOutputExcludes() {
local output="$1"
local unexpected="$2"

if print -r -- "${output}" | /usr/bin/grep -Fqx -- "${unexpected}"; then
fail "Unexpected parsed label '${unexpected}'"
fi
}

loadProductionFunction "parseInstallomatorItem"
loadProductionFunction "getAvailableInstallomatorLabels"
loadProductionFunction "normalizeInstallomatorLabels"

function preFlight() { :; }
function warning() { :; }
function errorOut() { :; }

/bin/cat > "${validFixture}" <<'FIXTURE'
case $label in
singlelabel|singlealias)
;;
multilinefirst|\
multilinesecond|\
multilinethird)
;;
parentlabel)
case "${nestedValue}" in
nestedalias)
;;
esac
;;
longversion)
;;
valuesfromarguments)
;;
*)
;;
esac
FIXTURE
/bin/chmod +x "${validFixture}"

organizationInstallomatorFile="${validFixture}"
parsedLabels="$(getAvailableInstallomatorLabels)" \
|| fail "Valid fixture returned a parse failure"

assertOutputContains "${parsedLabels}" "singlelabel"
assertOutputContains "${parsedLabels}" "singlealias"
assertOutputContains "${parsedLabels}" "multilinefirst"
assertOutputContains "${parsedLabels}" "multilinesecond"
assertOutputContains "${parsedLabels}" "multilinethird"
assertOutputContains "${parsedLabels}" "parentlabel"
assertOutputExcludes "${parsedLabels}" "nestedalias"
assertOutputExcludes "${parsedLabels}" "longversion"
assertOutputExcludes "${parsedLabels}" "valuesfromarguments"
assertOutputExcludes "${parsedLabels}" "*"

configuredInstallomatorLabels=(
"multilinefirst | Multiline Label | /Applications/Multiline.app | https://example.invalid/icon.png"
)
installomatorLabels=("${configuredInstallomatorLabels[@]}")
normalizeInstallomatorLabels

[[ ${#installomatorLabels[@]} -eq 1 ]] \
|| fail "Normalization removed a configured multiline alias"
[[ "${installomatorLabels[1]}" == "${configuredInstallomatorLabels[1]}" ]] \
|| fail "Normalization changed the configured multiline alias"

/bin/cat > "${incompleteFixture}" <<'FIXTURE'
case $label in
incompletealias|\
FIXTURE
/bin/chmod +x "${incompleteFixture}"

organizationInstallomatorFile="${incompleteFixture}"
if getAvailableInstallomatorLabels >/dev/null 2>&1; then
fail "Incomplete continuation arm did not return a parse failure"
fi

print -r -- "PASS: Installomator label parser regression tests"