diff --git a/packages/cli/package.json b/packages/cli/package.json index 3bb2e63..30cf751 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "agentloom", - "version": "0.1.9", + "version": "0.1.10", "description": "Unified agent and MCP sync CLI for multi-provider AI tooling", "type": "module", "bin": { diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 996e542..ac1d887 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -97,25 +97,41 @@ async function runEntityAwareUpdate(options: { return; } + console.log( + `Checking ${entries.length} lock entr${entries.length === 1 ? "y" : "ies"} for updates...`, + ); + let updated = 0; let skipped = 0; - for (const entry of entries) { + for (const [index, entry] of entries.entries()) { + const progressPrefix = formatUpdateProgressPrefix(index, entries.length); + const entryLabel = formatLockEntryLabel(entry); + if (!entryIncludesTarget(entry, options.target)) { + console.log( + `${progressPrefix} Skipping ${entryLabel} (does not track ${options.target}).`, + ); skipped += 1; continue; } + console.log(`${progressPrefix} Checking ${entryLabel}...`); + const probe = prepareSource({ source: entry.source, ref: entry.requestedRef, subdir: entry.subdir, }); - const hasNewCommit = probe.resolvedCommit !== entry.resolvedCommit; + const latestCommit = probe.resolvedCommit; + const hasNewCommit = latestCommit !== entry.resolvedCommit; probe.cleanup(); if (!hasNewCommit) { + console.log( + `${progressPrefix} Up to date at ${formatShortCommit(entry.resolvedCommit)}.`, + ); skipped += 1; continue; } @@ -128,10 +144,17 @@ async function runEntityAwareUpdate(options: { !updatePlan.importRules && !updatePlan.importSkills ) { + console.log( + `${progressPrefix} Skipping ${entryLabel} (no tracked entities selected for update).`, + ); skipped += 1; continue; } + console.log( + `${progressPrefix} Updating ${entryLabel} (${formatShortCommit(entry.resolvedCommit)} -> ${formatShortCommit(latestCommit)})...`, + ); + try { const importOptions: Parameters[0] = { source: entry.source, @@ -194,6 +217,9 @@ async function runEntityAwareUpdate(options: { rawSource: entry.source, summary, }); + console.log( + `${progressPrefix} Updated ${entryLabel} to ${formatShortCommit(summary.resolvedCommit)}.`, + ); updated += 1; } catch (err) { if (err instanceof NonInteractiveConflictError) { @@ -217,6 +243,29 @@ async function runEntityAwareUpdate(options: { } } +function formatUpdateProgressPrefix(index: number, total: number): string { + return `[${index + 1}/${total}]`; +} + +function formatLockEntryLabel(entry: LockEntry): string { + const base = entry.source; + const refSuffix = + typeof entry.requestedRef === "string" && entry.requestedRef.length > 0 + ? `@${entry.requestedRef}` + : ""; + const subdirSuffix = + typeof entry.subdir === "string" && entry.subdir.length > 0 + ? ` (${entry.subdir})` + : ""; + return `${base}${refSuffix}${subdirSuffix}`; +} + +function formatShortCommit(commit: string): string { + const normalized = commit.trim(); + if (normalized.length <= 12) return normalized; + return normalized.slice(0, 12); +} + interface EntryUpdatePlan { importAgents: boolean; importCommands: boolean; diff --git a/packages/cli/tests/unit/update-command.test.ts b/packages/cli/tests/unit/update-command.test.ts index dfc1787..ea0b60c 100644 --- a/packages/cli/tests/unit/update-command.test.ts +++ b/packages/cli/tests/unit/update-command.test.ts @@ -171,10 +171,54 @@ describe("runUpdateCommand", () => { }); const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toContain("Checking 1 lock entry for updates..."); + expect(output).toContain( + "[1/1] Checking farnoodma/agents@main (packages/agents)...", + ); + expect(output).toContain( + "[1/1] Updating farnoodma/agents@main (packages/agents) (old-commit -> new-commit)...", + ); + expect(output).toContain( + "[1/1] Updated farnoodma/agents@main (packages/agents) to new-commit.", + ); expect(output).toContain("Updated entries: 1"); expect(output).toContain("Unchanged entries: 0"); }); + it("prints per-entry progress when a lock entry is already current", async () => { + const paths = createScopePaths(); + const cleanup = vi.fn(); + + commandMocks.resolveScope.mockResolvedValue(paths); + commandMocks.readLockfile.mockReturnValue(createLockfile()); + commandMocks.prepareSource.mockReturnValue({ + spec: { source: "farnoodma/agents", type: "github" as const }, + rootPath: "/tmp/source", + importRoot: "/tmp/source/packages/agents", + resolvedCommit: "old-commit", + cleanup, + }); + + const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); + + await runUpdateCommand( + { _: ["update"], "no-sync": true } as ParsedArgs, + "/workspace", + ); + + expect(commandMocks.importSource).not.toHaveBeenCalled(); + expect(cleanup).toHaveBeenCalledTimes(1); + + const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toContain("Checking 1 lock entry for updates..."); + expect(output).toContain( + "[1/1] Checking farnoodma/agents@main (packages/agents)...", + ); + expect(output).toContain("[1/1] Up to date at old-commit."); + expect(output).toContain("Updated entries: 0"); + expect(output).toContain("Unchanged entries: 1"); + }); + it("skips mcp and skills updates when lock selectors are explicitly empty", async () => { const nonInteractive = !(process.stdin.isTTY && process.stdout.isTTY); const paths = createScopePaths();