Skip to content

feat(cli): auto-upgrade skill and clean legacy hooks on start/restart - #15

Merged
Marvae merged 2 commits into
mainfrom
feat/upgrade-migrations
Apr 14, 2026
Merged

feat(cli): auto-upgrade skill and clean legacy hooks on start/restart#15
Marvae merged 2 commits into
mainfrom
feat/upgrade-migrations

Conversation

@Marvae

@Marvae Marvae commented Apr 14, 2026

Copy link
Copy Markdown
Owner
  • Add runUpgradeMigrations() called from start/restart commands so npm update + teams-bot restart completes an upgrade automatically
  • syncInstalledSkill: version-stamped sync — only replaces the installed skill directory when the package version changes (.version marker)
  • cleanupLegacyHooks: removes stale SessionStart hook configs from settings.json that referenced the now-deleted session-start.sh
  • Move skill template from .claude/skills/ to skills/ so it won't be auto-discovered by Claude Code as a live skill
  • Remove stale .claude/hooks/ from package.json files array
  • Remove scope choice from install-skill (always installs globally)
  • Fix scope "2" destination path (was using projectDir instead of cwd)
  • Deduplicate cleanupLegacyHooks from uninstallSkill into shared function

- Add runUpgradeMigrations() called from start/restart commands so
  `npm update` + `teams-bot restart` completes an upgrade automatically
- syncInstalledSkill: version-stamped sync — only replaces the installed
  skill directory when the package version changes (.version marker)
- cleanupLegacyHooks: removes stale SessionStart hook configs from
  settings.json that referenced the now-deleted session-start.sh
- Move skill template from .claude/skills/ to skills/ so it won't be
  auto-discovered by Claude Code as a live skill
- Remove stale .claude/hooks/ from package.json files array
- Remove scope choice from install-skill (always installs globally)
- Fix scope "2" destination path (was using projectDir instead of cwd)
- Deduplicate cleanupLegacyHooks from uninstallSkill into shared function

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds automatic upgrade behavior to the CLI so bot start/restart can migrate user config and keep the bundled /handoff skill in sync after package updates.

Changes:

  • Add runUpgradeMigrations() and invoke it from start/restart to perform upgrade-time maintenance.
  • Implement migrations to (1) remove legacy SessionStart hook configs referencing session-start.sh and (2) version-stamp + sync the installed /handoff skill directory only when the package version changes.
  • Move the packaged skill template to skills/handoff/ and update package.json publish contents accordingly; add Vitest coverage for migrations.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/upgrade-migrations.test.ts Adds test coverage for legacy hook cleanup and version-stamped skill sync behavior.
src/cli/skill.ts Implements migrations (cleanupLegacyHooks, syncInstalledSkill) and updates install logic to use the new skills/ template location.
src/cli/commands.ts Calls migrations on start and restart to make upgrades automatic.
skills/handoff/SKILL.md Introduces the new packaged skill template location/content.
package.json Updates published files list to include skills/ and remove legacy .claude/... entries.
Comments suppressed due to low confidence (1)

src/cli/commands.ts:187

  • runUpgradeMigrations() is executed before the healthz probe in startCommand(). If a migration throws (e.g., due to unexpected settings.json shape or filesystem permissions), the CLI will fail even when the bot is already running. Consider making migrations non-fatal (catch/log) so teams-bot start remains reliable.
export async function startCommand(): Promise<void> {
  const platform = detectPlatform();
  runUpgradeMigrations();

  // Check if already running
  if (await probe("http://127.0.0.1:3978/healthz", 2000)) {
    console.log("Bot is already running.");

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +103 to +116
it("preserves non-legacy hooks", () => {
writeSettings(fakeHome, {
hooks: {
SessionStart: [
{
hooks: [{ type: "command", command: "some-other-hook.sh" }],
},
{
hooks: [
{ type: "command", command: "/path/session-start.sh" },
],
},
],
},

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case name is misleading: the second SessionStart group uses a command containing "session-start.sh", which the migration intentionally removes. Consider renaming the test to reflect that it preserves hooks not matching the legacy session-start.sh pattern, rather than implying session-start.sh is non-legacy.

Copilot uses AI. Check for mistakes.
Comment thread src/cli/skill.ts
Comment on lines +72 to +73
if (!hooks?.SessionStart) continue;
const groups = hooks.SessionStart as Array<Record<string, unknown>>;

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleanupLegacyHooks assumes hooks.SessionStart is an array and immediately calls .filter() on it. If a user’s settings.json has SessionStart set to a non-array value (e.g., an object/string), this will throw and can break start/restart (since migrations now run automatically). Add an Array.isArray(hooks.SessionStart) guard and skip (or normalize) when the shape is unexpected.

Suggested change
if (!hooks?.SessionStart) continue;
const groups = hooks.SessionStart as Array<Record<string, unknown>>;
const sessionStart = hooks?.SessionStart;
if (!Array.isArray(sessionStart)) continue;
const groups = sessionStart as Array<Record<string, unknown>>;

Copilot uses AI. Check for mistakes.
Comment thread src/cli/skill.ts Outdated
Comment on lines +106 to +107
if (!entry.isFile()) continue;
fs.copyFileSync(path.join(srcDir, entry.name), path.join(destDir, entry.name));

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyDirSync is documented as replacing the destination directory contents with the source directory contents, but the implementation only copies top-level files and skips subdirectories. Either make this a recursive directory copy (so future skill templates with nested files work) or update the doc/comment + name to reflect that only flat files are copied.

Suggested change
if (!entry.isFile()) continue;
fs.copyFileSync(path.join(srcDir, entry.name), path.join(destDir, entry.name));
const srcPath = path.join(srcDir, entry.name);
const destPath = path.join(destDir, entry.name);
if (entry.isDirectory()) {
copyDirSync(srcPath, destPath);
continue;
}
if (!entry.isFile()) continue;
fs.copyFileSync(srcPath, destPath);

Copilot uses AI. Check for mistakes.
Comment thread src/cli/commands.ts Outdated

export async function restartCommand(): Promise<void> {
const platform = detectPlatform();
runUpgradeMigrations();

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runUpgradeMigrations() now runs as part of restartCommand(). Since it performs filesystem reads/writes, any thrown error will abort the restart flow before stopService/startService runs. Consider wrapping migrations in a try/catch here (or inside runUpgradeMigrations) and logging a warning so restart remains best-effort.

Suggested change
runUpgradeMigrations();
try {
await Promise.resolve(runUpgradeMigrations());
} catch (error) {
console.warn(
"Warning: upgrade migrations failed during restart; continuing with service restart.",
error,
);
}

Copilot uses AI. Check for mistakes.
- Wrap runUpgradeMigrations() in try/catch in start/restart so
  migration errors don't block the bot from running
- Add Array.isArray guard for SessionStart hooks
- Make copyDirSync recursive for future-proofing
@Marvae
Marvae merged commit 17b7194 into main Apr 14, 2026
7 checks passed
@Marvae
Marvae deleted the feat/upgrade-migrations branch April 14, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants