
Starter Kit Upgrade
- 255 installs
- 685 repo stars
- Updated July 23, 2026
- laravel/agent-skills
Upgrade a Laravel agent starter kit to newer framework versions, dependencies, and conventions without breaking auth, queues, or API scaffolding.
About
Assists upgrading Laravel agent starter kits across framework and package versions, updating dependencies, configuration, middleware, and project conventions while preserving authentication, queue, and API scaffolding needed for agent-powered SaaS backends.
- Laravel starter kit version migrations
- Dependency and config alignment
- Preserves auth and API scaffolding
- Agent-ready project bootstrap updates
- Framework convention compliance checks
Starter Kit Upgrade by the numbers
- 255 all-time installs (skills.sh)
- +15 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #34 of 65 PHP & Laravel skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/laravel/agent-skills --skill starter-kit-upgradeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 255 |
|---|---|
| repo stars | ★ 685 |
| Last updated | July 23, 2026 |
| Repository | laravel/agent-skills ↗ |
What it does
Upgrade a Laravel agent starter kit to newer framework versions, dependencies, and conventions without breaking auth, queues, or API scaffolding.
Files
Laravel Starter Kit Upgrade
- Users bootstrap from
laravel/vue-starter-kit,react-starter-kit,svelte-starter-kit, orlivewire-starter-kit, then customize. They own the code. - We pick specific features from upstream (e.g. "toast notifications", "2FA autofocus fix"), not "version upgrades."
- The user's git history is unrelated to the kit's. There is no common ancestor. We compare user-now vs upstream-now, byte by byte.
- We never auto-merge a customized file. Customizations are surfaced; the user decides.
- Behavior preservation is the contract: the user's currently-passing tests/typecheck/build must still pass after.
Safety contract: non-negotiable
Read these to the user before any side effects, and live by them throughout:
1. Working tree must be clean. If git status --porcelain is non-empty, refuse and tell the user to commit or stash. Do not "stash for them." 2. All work happens on a dedicated branch (starter-kit-upgrade/<short-id>). The user's current branch is never modified. 3. Each applied feature is its own commit. That is how revertability works. 4. Never auto-resolve conflicts. A change touching customized code is surfaced; default action is to skip the file. 5. Never silently overwrite manifests or lockfiles (composer.json, package.json, *-lock.*). Show diffs; let the user decide. 6. Verify behavior preservation. Re-run the user's tests/typecheck/build after applying. A previously-passing check that now fails is a regression. Stop, surface, recommend revert. 7. Detect from unambiguous signals; ask when ambiguous. Concrete evidence (e.g. config/fortify.php exists) is fine. Picking a likely answer when signals are mixed or absent is not.
If any of these is violated, abort with a clear message about what went wrong and how to recover.
Required tools
git(in the user's project)gh(authenticated;gh auth statusreturns OK)jq(used byrun_tests.sh)bash(for the bundled scripts)
If any is missing, stop in Phase 4 and tell the user how to install.
Gotchas
Environment-specific behavior the agent will get wrong without being told. Read these before starting the workflow and apply throughout.
- Parallel implementations. When a feature has
newfiles plusdiffersto call sites, the user may already have an in-house equivalent (their own toast helper, validation rule, etc.). Surface as a whole; don't apply thenewfiles in isolation as if they're "safe." Default action is to skip the entire feature; the user can opt to adopt upstream's version and remove theirs later.
- Renamed paths. If a
newpath's basename or class name already exists elsewhere in the user's repo, the user has likely renamed/moved it. Surface, don't auto-apply, or you'll create a duplicate. Show them the upstream change and let them apply it to their renamed file by hand or wait for user confirmation.
- Later upstream edits. Copying upstream HEAD pulls in _every_ commit since the feature, not just the feature's own changes. Always run the Phase 5 step 2 check before applying. When later edits exist, scope to
<sha>:<path>instead ofHEAD:<path>.
- Transitive imports. New files often
importfrom helpers that are NOT in the same feature commit (Vue/React/Svelte:@/lib/...,@/components/...; Livewire:@include,<x-...>,<livewire:...>). Phase 5 step 4 covers the scan; never declare a feature applied without it. Uncovered imports show up as runtime/compile errors.
- Lockfile drift. Manifests are user-curated. Never overwrite. Walk the user through the upstream diff, let them merge, then regenerate lockfiles via the package manager (Phase 6).
- Stale node_modules after major bumps. After Vite v7 → v8, React 18 → 19, etc.,
npm installoften fails withERESOLVE. Clean and reinstall (Phase 6).
- New migrations. When upstream adds migrations (e.g. "Catch migrations up to Skeleton"), surface them separately. Recommend
php artisan migrate:statusfirst; applying a new migration on a populated DB can fail loudly.
- Major framework bumps as features. Things like Laravel 12 → 13, Livewire 3 → 4, or Inertia v2 → v3 are too large and too breaking for the feature-by-feature flow. Do not attempt them through this skill. Instead, prompt the user to run the corresponding Laravel Boost MCP slash command first, then come back and re-run this skill against the resulting (clean-tree) repo. If Boost is not yet installed:
composer require laravel/boost --dev && php artisan boost:install(requires Boost^2.0). Slash commands: - Laravel 12 → 13:
/upgrade-laravel-v13 - Livewire 3 → 4:
/upgrade-livewire-v4 - Inertia v2 → v3:
/upgrade-inertia-v3
- Already-present features. If Phase 2's pre-filter missed it and Phase 5's classifier reports every file as
already-present, skip the feature with a note: "every file matches upstream's current; moving on." Don't commit an empty commit.
- More than ~50 `differs`. The per-file walkthrough is too tedious to be useful at that scale. Stop, recommend manual upgrade for that feature.
Workflow
Eight phases, in order. Each phase establishes invariants the next relies on.
Phase 1: Identify the kit and branch variant
Inspect the user's project:
| vue | react | svelte | livewire | |
|---|---|---|---|---|
| Cue | .vue files in resources/js/components/ui/ | .tsx files in resources/js/components/ui/ | .svelte files in resources/js/components/ui/ | no resources/js/components/ui/ dir |
package.json has | "vue" + "@inertiajs/vue3" | "react" + "@inertiajs/react" | "svelte" + "@inertiajs/svelte" | n/a |
composer.json has | n/a | n/a | n/a | "livewire/livewire" + "livewire/flux" |
State the detected kit out loud. If only one column matches, proceed. If two columns partially match (e.g. both .vue and .tsx present, or package.json lists vue and react), stop and ask.
Then determine the branch variant. There are four branches per kit, formed by two independent axes:
- Auth axis (read
composer.json): - Fortify if
composer.jsonhaslaravel/fortify, orconfig/fortify.phpexists, orapp/Actions/Fortify/exists, orapp/Providers/FortifyServiceProvider.phpexists. - WorkOS if
composer.jsonhaslaravel/workosand none of the Fortify markers are present. - Teams axis (check whether team scaffolding is present):
- Teams if
app/Models/Team.phpexists (usually accompanied byMembership.php,TeamInvitation.php, and a..._create_teams_table.phpmigration). - Non-teams otherwise.
Combine the two axes to get the branch name:
| Auth | Teams | Branch |
|---|---|---|
| Fortify | no | main |
| Fortify | yes | teams |
| WorkOS | no | workos |
| WorkOS | yes | workos-teams |
State the detected branch out loud. Only ask if signals are contradictory (e.g. Fortify markers present _and_ laravel/workos in composer, or a Team.php model with no teams migration); that means user customization you can't safely guess at.
Phase 2: Enumerate available upstream features
The user can't tell you "what version they're on" reliably (and we don't try). Inspect upstream as it exists today and present a feature catalog.
Fetch raw data. The default window is the last 100 commits / merged PRs; tell the user that up front so they know features older than that won't appear in the catalog. If they bootstrapped well before that window, walk back with &page=2, &page=3, etc. or raise --limit.
gh api "repos/laravel/<kit>/commits?sha=<branch>&per_page=100" \
-q '.[] | {sha: .sha[0:7], date: .commit.author.date[0:10], msg: .commit.message | split("\n")[0]}'
gh pr list --repo "laravel/<kit>" --state merged --base "<branch>" --limit 100 \
--json number,title,mergeCommit,mergedAtCluster commits/PRs into user-facing features. Examples a user would recognize:
- "Toast notifications across all kits" (1 commit, several files)
- "Password visibility toggle in auth forms" (1 commit, 3 files)
- "2FA autofocus fix" (1 commit, 1 file)
- "Teams support" (1 PR, many files; flag as large)
- "Inertia 3 upgrade" (lockfile-heavy; flag as needing review)
- "Maintenance: formatting / lint config" (bucket of small commits)
Bucket internal/refactor commits as a single "Maintenance" entry. The user usually skips it.
Pre-filter: for each candidate feature, run scripts/classify_feature.sh against its commit. If every file is already-present, mark [!] Already present and skip by default.
Phase 3: Present the catalog and get explicit selection
Available upstream features (vue-starter-kit, branch: main):
[ ] Toast notifications · PR #142, 4 files, 1 lockfile
[ ] Password visibility toggle · PR #131, 3 files
[ ] 2FA autofocus fix · commit 78fda0c, 1 file
[ ] Teams support · PR #98, 23 files (LARGE)
[~] Inertia 3 upgrade · PR #110, lockfile-heavy (review carefully)
[!] Already present: Vite font plugin
Which would you like to pull in?Wait for the selection. Recap the picks and the affected file counts. Ask one final time before any side effects.
Phase 4: Preflight, baseline, and workspace setup
Run preflight:
scripts/preflight.sh <user_repo>It checks the repo is a git repo, the tree is clean, and that gh (authenticated) and jq are available. If it exits non-zero, surface the message verbatim and stop.
Record a verification baseline so Phase 7 can distinguish regressions from pre-existing failures. Use mktemp so concurrent runs don't clobber each other:
baseline=$(mktemp -t skup-baseline.XXXXXX.json)
scripts/run_tests.sh <user_repo> --baseline "$baseline"Hold onto $baseline; Phase 7 needs it.
Fetch the upstream kit and capture its path:
kit_dir=$(scripts/fetch_kit.sh <kit> <branch>)Hold onto $kit_dir; Phase 5 needs it. The script is idempotent: re-running with the same args fetches the latest branch tip rather than re-cloning.
Create the upgrade branch:
git -C <user_repo> checkout -b "starter-kit-upgrade/$(date +%Y%m%d-%H%M)-<first-slug>"If the user is already on a starter-kit-upgrade/... branch (a previous run that didn't get cleaned up), checkout -b will refuse if the new name collides. Don't auto-resolve: ask whether they want to resume on that branch (skip the checkout -b, keep going from where they were), start fresh (the new timestamped name will already differ by minute, so just retry — or bump to +%Y%m%d-%H%M%S if it's the same minute), or abort so they can clean up manually. Never delete the existing branch on their behalf.
From this point on, every write goes to this branch.
Phase 5: Apply each selected feature
For each selected feature, in order:
1. Classify. Run scripts/classify_feature.sh <kit_dir> <sha> <user_repo>. Statuses:
new: file does not exist in user repo, exists at upstream HEAD. Safe to add.already-present: user's file is byte-identical to upstream HEAD. Skip.differs: user has the file and bytes differ from upstream HEAD. Surface.deleted-upstream: upstream HEAD lacks the file but the user has it. Surface; default is keep theirs.lockfile: manifest or lock file. Surface; never auto-merge.
The classifier compares only against upstream HEAD. The user's git history doesn't trace back to the kit's, so there's no "before-image" baseline to merge against; we don't try. The feature commit just enumerates which paths to look at.
2. Later-edits check. Find which feature paths _later_ upstream commits also modified:
scripts/later_edits.sh <kit_dir> <sha> <user_repo>Each path the script prints is a path where copying upstream HEAD's content pulls _later_ changes in too. Diff <sha>:<path> against HEAD:<path>; if a non-whitespace hunk differs, scope to the feature commit (git -C <kit_dir> show <sha>:<path>) and note it in the report.
3. Apply `new` files. The script writes upstream HEAD's content for each new path and stages it; everything else is left for steps 4–5:
scripts/apply_new_files.sh <kit_dir> <sha> <user_repo>It prints applied <path> for each file written so you can collect the list for the feature's commit message and the report.
Before letting the script run, check for the rename gotcha (see Gotchas → "Renamed paths"). If a new path's basename already exists at a different location in the user's repo, surface to the user before applying.
4. Transitive-imports check. New files often import helpers that aren't in the same feature commit. The script picks the right regex for the kit (Vue/React/Svelte handle TS/JS imports; Livewire handles Blade includes / x- components / livewire: tags):
scripts/scan_transitive_imports.sh <kit> <new_files...>Output is <file>:<line>:<match> per import. For each match, verify the corresponding helper file exists in the user's repo. If not, the new files won't compile/render; flag the missing target as a follow-up dependency the user needs to fetch (same walkthrough as differs).
5. Walk the user through `differs`, `deleted-upstream`, and `lockfile`. One file at a time:
- Show what upstream has:
git -C <kit_dir> show HEAD:<path>(or<sha>:<path>iflater_edits.shflagged this path). - Show their current file.
- Show the diff between the two.
- Ask the user to pick: take upstream wholesale (lossy; confirm first), keep theirs, or merge by hand (you produce a unified diff for reference; they write the result).
- If they're unsure, ask once more with the diff in front of them. Still unsure → keep theirs and move on. Don't pick silently.
- Stage whatever they chose:
git -C <user_repo> add <path>.
For lockfile: never overwrite the manifest. Show the upstream diff for composer.json / package.json, walk them through the relevant change, let them edit the manifest. Lockfile regeneration happens in Phase 6.
6. Commit the feature as one revertable unit:
git -C <user_repo> commit -m "starter-kit-upgrade: <feature name>
Upstream: laravel/<kit>@<sha>
Files added: <list>
Files updated (took upstream): <list>
Files updated (manual merge): <list>
Files kept as-is: <list>"If the user wants to bail out at any point, leave the branch as-is. They can drop it with git branch -D.
Phase 6: Reconcile manifests if needed
If any feature touched a manifest, lockfiles are out of sync. After the user agrees, run:
scripts/reconcile_manifests.sh <user_repo>The script runs composer install (when composer.json + composer.lock are both present), auto-detects the JS package manager from the existing lockfile, runs <pm> install, and on failure (typically ERESOLVE after a major bump like Vite v7 → v8 or React 18 → 19) wipes node_modules + the lockfile and retries once.
Commit lockfile updates as a separate starter-kit-upgrade: dependency lockfiles commit so they can be reverted independently.
Phase 7: Verify behavior preservation
Compare against the baseline:
scripts/run_tests.sh <user_repo> --compare "$baseline"Compare mode runs PHP tests, JS typecheck, JS build (whichever exist) and reports only checks that were passing in the baseline and now fail. Pre-existing failures are not the upgrade's fault and don't block.
If a regression is reported:
- Show the failing output from the per-check log file the script points to.
- Recommend
git revert HEADfirst; if that doesn't fix it, revert again. - For multi-feature uncertainty, suggest
git bisect start <upgrade-branch> <previous-branch>. - Do not edit code to make the failing check pass; that violates the behavior contract.
If the project has no discoverable verification commands, say so explicitly in the report. Don't pretend verification happened.
Phase 8: Write the report
Write to /tmp/starter-kit-upgrade-report-<id>.md (where <id> matches the upgrade branch's starter-kit-upgrade/<id>) first; never silently into the user's repo. Stamping the id keeps concurrent runs and re-runs from clobbering each other. Show the path and ask whether they want it copied in as STARTER_KIT_UPGRADE.md or kept out of tree.
# Starter Kit Upgrade Report
- Date: <date>
- Kit: laravel/<kit>
- Branch tracked: <branch>
- Upgrade branch: starter-kit-upgrade/<id>
## Features applied
- <feature name> · laravel/<kit>@<sha> · <N files>
- Applied: <list>
- Skipped: <list with reasons>
- Manual decisions: <if any, with reasoning>
- Later-edit drift avoided: <if any, with paths scoped manually>
## Lockfile updates
<which lock files were regenerated and how>
## Verification
- Baseline: <path or summary>
- Result: <PASS / REGRESSED:<list> / NO-CHECKS>
- Output: <relevant snippet>
## How to revert
- Drop a single feature: `git revert <commit-sha>`
- Discard everything: `git checkout <previous-branch> && git branch -D starter-kit-upgrade/<id>`Out of scope
- Detecting which kit "version" the user started from. There is no reliable way; we don't pretend.
- Reconciling dep version constraints automatically. We show; the user decides.
- Forks of the starter kits. If the repo's structure isn't recognizable as one of the three official kits, refuse and explain.
- Cross-kit migration (e.g. Vue → React).
- Running linters / formatters on applied files. The user runs their own tooling.
#!/usr/bin/env bash
# apply_new_files.sh — write upstream HEAD's content for every file the
# classifier reports as `new`, and stage each.
#
# Usage: apply_new_files.sh <kit_dir> <sha> <user_repo>
#
# Files with any other classifier status (already-present, differs,
# deleted-upstream, lockfile) are left alone for the agent to handle
# interactively per Phase 5 step 5.
#
# Prints "applied <path>" for each file written so the caller can include
# the list in the feature commit message and the report.
set -euo pipefail
usage() {
echo "Usage: $0 <kit_dir> <sha> <user_repo>" >&2
exit 2
}
[[ $# -lt 3 ]] && usage
kit_dir="$1"
sha="$2"
user_repo="$3"
script_dir="$(cd "$(dirname "$0")" && pwd)"
"$script_dir/classify_feature.sh" "$kit_dir" "$sha" "$user_repo" \
| awk -F'\t' '$1=="new"{print $2}' \
| while IFS= read -r path; do
mkdir -p "$user_repo/$(dirname "$path")"
git -C "$kit_dir" show "HEAD:$path" > "$user_repo/$path"
git -C "$user_repo" add -- "$path"
echo "applied $path"
done
#!/usr/bin/env bash
# classify_feature.sh — classify each file touched by an upstream feature commit
# against the user's repo, comparing only against upstream's current branch tip.
#
# Usage: classify_feature.sh <kit_dir> <sha> <user_repo>
#
# The SHA enumerates which files the feature touches. Comparison is always
# against upstream HEAD (already checked out in <kit_dir>). The user's git
# history is unrelated to the kit's, so "what was the file before the change"
# is not meaningful on their side.
#
# Per file, prints "<status>\t<path>". Statuses:
# new absent in user repo, exists at upstream HEAD
# already-present user's bytes equal upstream HEAD's bytes
# differs user has the file but bytes differ from upstream HEAD
# deleted-upstream upstream HEAD lacks the file but the user still has it
# lockfile composer/package manifest or lockfile (always user-mediated)
#
# All comparisons are byte-exact via diff(1).
set -euo pipefail
kit_dir="$1"
sha="$2"
user_repo="$3"
classify() {
local path="$1"
local user_file="$user_repo/$path"
case "$path" in
composer.json|composer.lock|package.json|package-lock.json|pnpm-lock.yaml|yarn.lock|bun.lockb|bun.lock)
printf "lockfile\t%s\n" "$path"
return
;;
esac
local upstream
upstream=$(mktemp)
# Single call: succeeds iff upstream has the file. No separate existence check.
if git -C "$kit_dir" show "HEAD:$path" >"$upstream" 2>/dev/null; then
if [[ -e "$user_file" ]]; then
if diff -q "$upstream" "$user_file" >/dev/null 2>&1; then
printf "already-present\t%s\n" "$path"
else
printf "differs\t%s\n" "$path"
fi
else
printf "new\t%s\n" "$path"
fi
else
# Upstream HEAD doesn't have this path — only meaningful if the user still does.
if [[ -e "$user_file" ]]; then
printf "deleted-upstream\t%s\n" "$path"
else
printf "already-present\t%s\n" "$path"
fi
fi
rm -f "$upstream"
}
# diff-tree gives us the paths the feature touched; classification is against HEAD.
git -C "$kit_dir" diff-tree --no-commit-id --name-only --no-renames -r "$sha" \
| while IFS= read -r path; do
[[ -z "${path:-}" ]] && continue
classify "$path"
done
#!/usr/bin/env bash
# fetch_kit.sh — clone (or update) a Laravel starter kit repo into a local
# cache so other scripts can read upstream state.
#
# Usage: fetch_kit.sh <kit> <branch> [dest]
# <kit> one of: vue-starter-kit, react-starter-kit, svelte-starter-kit, livewire-starter-kit
# <branch> one of: main, teams, workos, workos-teams
# [dest] destination dir (default: /tmp/starter-kit-<kit>)
#
# Idempotent: if dest already has a clone of the same repo, fetches the
# branch and resets to upstream rather than re-cloning. Prints the
# destination path on stdout so the caller can capture it (e.g.
# `kit_dir=$(scripts/fetch_kit.sh vue-starter-kit main)`).
set -euo pipefail
usage() {
echo "Usage: $0 <kit> <branch> [dest]" >&2
exit 2
}
[[ $# -lt 2 ]] && usage
kit="$1"
branch="$2"
dest="${3:-/tmp/starter-kit-$kit}"
case "$kit" in
vue-starter-kit|react-starter-kit|svelte-starter-kit|livewire-starter-kit) ;;
*)
echo "ERROR: unknown kit '$kit'" >&2
echo "Supported: vue-starter-kit, react-starter-kit, svelte-starter-kit, livewire-starter-kit" >&2
exit 3
;;
esac
url="https://github.com/laravel/$kit.git"
if [[ -d "$dest/.git" ]]; then
# Match the exact repo (HTTPS or SSH, with or without .git suffix) so
# forks like 'vue-starter-kit-fork' don't pass the check.
existing=$(git -C "$dest" remote get-url origin 2>/dev/null || true)
if [[ ! "$existing" =~ ^(https://github\.com/|git@github\.com:)laravel/${kit}(\.git)?$ ]]; then
echo "ERROR: '$dest' exists but is not a clone of laravel/$kit (origin: ${existing:-none})" >&2
exit 4
fi
git -C "$dest" fetch --quiet --depth 500 origin "$branch"
git -C "$dest" checkout --quiet "origin/$branch"
else
git clone --quiet --depth 500 --branch "$branch" "$url" "$dest"
fi
echo "$dest"
#!/usr/bin/env bash
# later_edits.sh — for the files a feature commit touches, list which
# of those files have ALSO been modified by later upstream commits.
#
# Usage: later_edits.sh <kit_dir> <sha> <user_repo>
#
# Output: one path per line. If the list is non-empty, copying upstream
# HEAD's content for those paths will pull in changes beyond the feature.
# In that case, scope to <sha>:<path> instead of HEAD:<path> when applying.
set -euo pipefail
usage() {
echo "Usage: $0 <kit_dir> <sha> <user_repo>" >&2
exit 2
}
[[ $# -lt 3 ]] && usage
kit_dir="$1"
sha="$2"
user_repo="$3"
script_dir="$(cd "$(dirname "$0")" && pwd)"
# Read paths as an array so filenames with spaces/globs survive intact when
# passed to `git log -- <paths>`.
paths=()
while IFS= read -r path; do
[[ -n "$path" ]] && paths+=("$path")
done < <("$script_dir/classify_feature.sh" "$kit_dir" "$sha" "$user_repo" | cut -f2)
[[ ${#paths[@]} -eq 0 ]] && exit 0
git -C "$kit_dir" log --name-only --pretty=format: "${sha}..HEAD" -- "${paths[@]}" \
| sort -u \
| sed '/^$/d'
#!/usr/bin/env bash
# preflight.sh — verify the user's repo is in a state where the skill can
# safely operate. Run before any side effects.
#
# Usage: preflight.sh <user_repo>
#
# Checks (in order):
# 1. Path is a git repo
# 2. Working tree is clean (no uncommitted changes)
# 3. `gh` is available and authenticated
# 4. `jq` is available
#
# Exits non-zero with a clear message about which check failed and how to fix.
# Exit codes are stable so the agent can branch on them if needed:
# 2 = not a git repo
# 3 = dirty working tree
# 4 = gh missing
# 5 = gh not authenticated
# 6 = jq missing
set -euo pipefail
usage() {
echo "Usage: $0 <user_repo>" >&2
exit 2
}
[[ $# -lt 1 ]] && usage
repo="$1"
fail() {
local code="$1" message="$2" hint="$3"
echo "FAIL: $message" >&2
echo "$hint" >&2
exit "$code"
}
check_git_repo() {
git -C "$repo" rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|| fail 2 "'$repo' is not a git repository." \
"Run 'git init' (or pass a path inside an existing repo)."
}
check_clean_tree() {
[[ -z "$(git -C "$repo" status --porcelain)" ]] && return
echo "FAIL: '$repo' has uncommitted changes." >&2
echo "Commit or stash them before running this skill." >&2
git -C "$repo" status --short >&2
exit 3
}
check_gh_installed() {
command -v gh >/dev/null 2>&1 \
|| fail 4 "'gh' CLI is not installed." \
"Install: https://cli.github.com/"
}
check_gh_authenticated() {
gh auth status >/dev/null 2>&1 \
|| fail 5 "'gh' is not authenticated." \
"Run: gh auth login"
}
check_jq_installed() {
command -v jq >/dev/null 2>&1 \
|| fail 6 "'jq' is not installed." \
"Install via your package manager (e.g. 'brew install jq')."
}
check_git_repo
check_clean_tree
check_gh_installed
check_gh_authenticated
check_jq_installed
echo "preflight: OK"
#!/usr/bin/env bash
# reconcile_manifests.sh — regenerate composer.lock and the JS lockfile
# after the user has merged manifest changes from upstream.
#
# Usage: reconcile_manifests.sh <user_repo>
#
# - Runs `composer install` if composer.json + composer.lock both exist.
# - Auto-detects the JS package manager from the existing lockfile and runs
# `<pm> install`. On install failure (often ERESOLVE after a major bump),
# removes node_modules and the lockfile, then reinstalls. The recovery is
# announced on stderr so the agent can include it in the report.
#
# The user must have already agreed to run this — the script doesn't ask.
set -euo pipefail
usage() {
echo "Usage: $0 <user_repo>" >&2
exit 2
}
[[ $# -lt 1 ]] && usage
repo="$1"
cd "$repo"
# Mirror run_tests.sh's auto-detection. Both helpers return non-zero (via
# the `[[ -f package.json ]] || return 1` guard) when the project has no
# package.json, so callers must guard their use behind that check.
js_package_manager() {
[[ -f package.json ]] || return 1
if [[ -f pnpm-lock.yaml ]]; then echo pnpm
elif [[ -f bun.lockb || -f bun.lock ]]; then echo bun
elif [[ -f yarn.lock ]]; then echo yarn
else echo npm
fi
}
js_lockfile() {
[[ -f package.json ]] || return 1
if [[ -f pnpm-lock.yaml ]]; then echo pnpm-lock.yaml
elif [[ -f bun.lockb ]]; then echo bun.lockb
elif [[ -f bun.lock ]]; then echo bun.lock
elif [[ -f yarn.lock ]]; then echo yarn.lock
else echo package-lock.json
fi
}
if [[ -f composer.json && -f composer.lock ]]; then
echo "==> composer install --no-interaction" >&2
composer install --no-interaction
fi
if [[ -f package.json ]]; then
pm=$(js_package_manager)
lock=$(js_lockfile)
echo "==> $pm install" >&2
if ! "$pm" install; then
# Common cause: stale node_modules after a major bump (ERESOLVE).
# Try the standard recovery once before giving up.
echo "==> '$pm install' failed; removing node_modules + $lock and retrying" >&2
rm -rf node_modules "$lock"
"$pm" install
fi
fi
#!/usr/bin/env bash
# run_tests.sh — discover and run the user's verification commands, optionally
# comparing pre-upgrade and post-upgrade results so we don't blame the upgrade
# for failures that already existed.
#
# Usage:
# run_tests.sh <user_repo> # run, print summary, exit 0/1
# run_tests.sh <user_repo> --baseline <out.json> # record results to out.json
# run_tests.sh <user_repo> --compare <baseline.json> # compare to baseline
#
# Discovery (per category, first match wins):
# php_tests: composer.json scripts.test → vendor/bin/pest → vendor/bin/phpunit → php artisan test
# js_typecheck: package.json scripts.types → scripts.typecheck → scripts.tsc
# js_build: package.json scripts.build
#
# All three checks run in parallel. Each check's combined stdout+stderr is
# captured to /tmp/skup-tests-<label>.log so failures can be surfaced.
#
# Results JSON shape (built and parsed with jq — never with grep/sed):
# {
# "php_tests": {"ran": true, "command": "vendor/bin/pest", "passed": true, "exit": 0, "log": "/tmp/..."},
# "js_typecheck": {"ran": false, "reason": "no command discovered"},
# "js_build": {"ran": true, "command": "pnpm run build", "passed": false, "exit": 1, "log": "/tmp/..."}
# }
#
# Compare mode: prints regressions only — labels that passed in baseline and now fail.
set -uo pipefail
usage() {
echo "Usage: $0 <user_repo> [--baseline <out.json> | --compare <baseline.json>]" >&2
exit 2
}
[[ $# -lt 1 ]] && usage
command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is required" >&2; exit 3; }
repo="$1"; shift
mode="run"
ref=""
case "${1:-}" in
"") mode="run" ;;
--baseline) mode="baseline"; ref="${2:-}"; [[ -z "$ref" ]] && usage ;;
--compare) mode="compare"; ref="${2:-}"; [[ -z "$ref" ]] && usage ;;
*) usage ;;
esac
cd "$repo"
# Pick the JS package manager; only meaningful when package.json exists.
js_package_manager() {
[[ -f package.json ]] || return 1
if [[ -f pnpm-lock.yaml ]]; then echo pnpm
elif [[ -f bun.lockb || -f bun.lock ]]; then echo bun
elif [[ -f yarn.lock ]]; then echo yarn
else echo npm
fi
}
has_npm_script() {
[[ -f package.json ]] || return 1
jq -e --arg s "$1" '.scripts[$s] // empty' package.json >/dev/null 2>&1
}
# Each discover_* prints the discovered command (one line, command + args)
# or nothing if no command exists for that category.
discover_php_tests() {
if [[ -f composer.json ]] && jq -e '.scripts.test // empty' composer.json >/dev/null 2>&1; then
echo "composer test"
elif [[ -x vendor/bin/pest ]]; then echo "vendor/bin/pest"
elif [[ -x vendor/bin/phpunit ]]; then echo "vendor/bin/phpunit"
elif [[ -f artisan ]]; then echo "php artisan test"
fi
}
discover_js_typecheck() {
local pm; pm=$(js_package_manager) || return 0
for s in types typecheck tsc; do
if has_npm_script "$s"; then echo "$pm run $s"; return; fi
done
}
discover_js_build() {
local pm; pm=$(js_package_manager) || return 0
has_npm_script build && echo "$pm run build"
}
# Run a single check in the background. Writes one JSON object to <out_json>.
# stdout+stderr go to /tmp/skup-tests-<label>.log for later surfacing.
run_check() {
local label="$1" cmd="$2" out_json="$3"
local logfile="/tmp/skup-tests-${label}.log"
if [[ -z "$cmd" ]]; then
jq -nc --arg l "$label" '{($l): {ran: false, reason: "no command discovered"}}' > "$out_json"
return
fi
: > "$logfile"
bash -c "$cmd" >"$logfile" 2>&1
local ec=$?
jq -nc --arg l "$label" --arg c "$cmd" --arg log "$logfile" --argjson ec "$ec" \
'{($l): {ran: true, command: $c, passed: ($ec == 0), exit: $ec, log: $log}}' > "$out_json"
}
# Run all three checks in parallel and merge their result fragments.
build_results() {
local php js_t js_b
php=$(discover_php_tests)
js_t=$(discover_js_typecheck)
js_b=$(discover_js_build)
local f1 f2 f3
f1=$(mktemp); f2=$(mktemp); f3=$(mktemp)
run_check php_tests "$php" "$f1" &
run_check js_typecheck "$js_t" "$f2" &
run_check js_build "$js_b" "$f3" &
wait
jq -s 'add' "$f1" "$f2" "$f3"
rm -f "$f1" "$f2" "$f3"
}
case "$mode" in
run)
results=$(build_results)
echo "$results"
# Exit 1 if anything that ran failed.
if [[ $(echo "$results" | jq '[.. | objects | select(.passed == false)] | length') -gt 0 ]]; then
exit 1
fi
;;
baseline)
build_results > "$ref"
echo "baseline recorded: $ref" >&2
;;
compare)
[[ -f "$ref" ]] || { echo "ERROR: baseline '$ref' not found" >&2; exit 2; }
post=$(build_results)
regressed=0
for label in php_tests js_typecheck js_build; do
was_pass=$(jq -r --arg l "$label" '.[$l].passed == true' "$ref")
is_fail=$(echo "$post" | jq -r --arg l "$label" '.[$l].passed == false')
if [[ "$was_pass" == "true" ]] && [[ "$is_fail" == "true" ]]; then
logfile=$(echo "$post" | jq -r --arg l "$label" '.[$l].log // ""')
echo "REGRESSION: $label was passing, now fails (log: $logfile)" >&2
regressed=1
fi
done
echo "$post"
exit $regressed
;;
esac
#!/usr/bin/env bash
# scan_transitive_imports.sh — scan applied `new` files for imports that
# may point at helpers absent in the user's repo.
#
# Usage: scan_transitive_imports.sh <kit> <new_files...>
# <kit> one of: vue-starter-kit, react-starter-kit, svelte-starter-kit, livewire-starter-kit
#
# Vue / React / Svelte: TS/JS imports with @, ~, ./, ../ aliases.
# Livewire: Blade includes, x-components, livewire tags.
#
# Output mirrors `grep -EHn` so the agent sees `<file>:<line>:<match>` for
# every import. Empty output means no transitive imports to chase. Always
# exits 0 — finding nothing is not an error.
set -euo pipefail
usage() {
echo "Usage: $0 <kit> <new_files...>" >&2
exit 2
}
[[ $# -lt 2 ]] && usage
kit="$1"; shift
# Pick the import-style regex for the kit. Same DRY shape as run_tests.sh's
# discover_* helpers: each kit maps to one canonical pattern.
case "$kit" in
vue-starter-kit|react-starter-kit|svelte-starter-kit)
pattern="from ['\"](@/|~/|\\./|\\.\\./)"
;;
livewire-starter-kit)
pattern="@(include|extends|component|livewire)\\(|<x-|<livewire:"
;;
*)
echo "ERROR: unknown kit '$kit'" >&2
exit 3
;;
esac
grep -EHn "$pattern" "$@" 2>/dev/null || true