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
138 changes: 69 additions & 69 deletions src/Capacitor.Cli/Commands/AgentCommand.cs

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions src/Capacitor.Cli/Commands/CleanupCommand.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
namespace Capacitor.Cli.Commands;

sealed class CleanupCommand(WatcherManager watchers) {
sealed class CleanupCommand(WatcherManager watchers, StandardStreams console) {
Comment thread
realtonyyoung marked this conversation as resolved.
Resolved

public async Task<int> HandleCleanup() {
// The directory WatcherPaths resolves, not the config default: a spawn writes its pid
// files wherever KCAP_WATCHER_DIR points, and cleanup has to look in that same place.
var watcherDir = watchers.GetWatcherDir();

if (!Directory.Exists(watcherDir)) {
await Console.Out.WriteLineAsync("No watchers directory found.");
await console.Out.WriteLineAsync("No watchers directory found.");

return 0;
}
Expand All @@ -21,16 +21,16 @@ public async Task<int> HandleCleanup() {

foreach (var pidFile in pidFiles) {
var key = Path.GetFileNameWithoutExtension(pidFile);
var wasRunning = await watchers.KillWatcher(key);
var wasRunning = await watchers.KillWatcher(key, console.Error);

if (wasRunning) {
await Console.Out.WriteLineAsync($"Killed watcher {key}");
await console.Out.WriteLineAsync($"Killed watcher {key}");
killed++;
} else if (File.Exists(pidFile)) {
await Console.Out.WriteLineAsync($"Could not stop watcher {key}; kept its PID file");
await console.Out.WriteLineAsync($"Could not stop watcher {key}; kept its PID file");
failed++;
} else {
await Console.Out.WriteLineAsync($"Cleaned up stale PID file for {key}");
await console.Out.WriteLineAsync($"Cleaned up stale PID file for {key}");
cleaned++;
}
}
Expand All @@ -41,11 +41,11 @@ public async Task<int> HandleCleanup() {
// them, and this also mops up orphans whose .pid was already gone.
var purged = watchers.PurgeAuxiliaryFiles();

await Console.Out.WriteLineAsync(
await console.Out.WriteLineAsync(
$"Done. Killed {killed} watcher(s), cleaned {cleaned} stale PID file(s), purged {purged} auxiliary file(s).");

if (failed > 0) {
await Console.Out.WriteLineAsync($"{failed} watcher(s) could not be stopped; run kcap cleanup again.");
await console.Out.WriteLineAsync($"{failed} watcher(s) could not be stopped; run kcap cleanup again.");

return 1;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Capacitor.Cli/Commands/CommandServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public static IServiceCollection AddCapacitorCli(
sp.GetRequiredService<UserHome>(), sp.GetRequiredService<BinaryProbe>()));
services.AddSingleton(sp => LegacySkillsRoots.FromEnvironment(sp.GetRequiredService<UserHome>()));
services.AddSingleton(sp => AccountStore.Beside(sp.GetRequiredService<DaemonStore>()));
// Transient, so each command reads the console as it stands when it is built.
services.AddTransient(_ => StandardStreams.FromProcess());
services.AddSingleton(sp => PluginEnvironment.FromProcess(
sp.GetRequiredService<ProfileContext>().Snapshot,
sp.GetRequiredService<UserHome>(),
Expand Down
180 changes: 90 additions & 90 deletions src/Capacitor.Cli/Commands/DaemonCommands.cs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/Capacitor.Cli/Commands/DaemonServiceCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ internal async Task<int> Uninstall() {
return 1;
}

if (!ended) DaemonCommands.MarkLedgerInterrupted(store, id, time);
if (!ended) DaemonCommands.MarkLedgerInterrupted(store, id, time, Console.Error);

await Console.Out.WriteLineAsync($"Service '{id}' uninstalled ({manager.Describe()}).");
return 0;
Expand Down Expand Up @@ -643,7 +643,7 @@ internal async Task<int> UninstallUnder(string dir) {
await Console.Error.WriteLineAsync($"Could not uninstall service '{unit}': {error}");
failed = true;
} else {
if (!ended) DaemonCommands.MarkLedgerInterrupted(store, unit, time);
if (!ended) DaemonCommands.MarkLedgerInterrupted(store, unit, time, Console.Error);
await Console.Out.WriteLineAsync($"Service '{unit}' uninstalled ({manager.Describe()}).");
}
}
Expand Down Expand Up @@ -750,7 +750,7 @@ internal async Task<int> Stop(string[] args) {
return 1;
}

if (!exited) DaemonCommands.MarkLedgerInterrupted(store, id, time, DaemonCommands.KeepsSavedResumes(request, outcome));
if (!exited) DaemonCommands.MarkLedgerInterrupted(store, id, time, Console.Error, DaemonCommands.KeepsSavedResumes(request, outcome));

await Console.Out.WriteLineAsync($"Service '{id}' stopped (still installed).");
return 0;
Expand Down
6 changes: 3 additions & 3 deletions src/Capacitor.Cli/Commands/LocalCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ async Task<int> UninstallAsync(LocalStack stack, bool purge, bool force, bool ye
LocalUninstallOutcome outcome;

try {
outcome = await stack.RunAsync(new SyncProgress<LocalInstallEvent>(RenderRecovery),
outcome = await stack.RunAsync(new SyncProgress<LocalInstallEvent>(e => RenderRecovery(Console.Out, e)),
(lease, state, ct) => uninstall.RunAsync(lease, state, purge, force, config.Directory, ct), CancellationToken.None, force);
} catch (Exception ex) when (ex is LocalLifecycleException or LocalInstallFailure or LocalUpgradeFailure) {
return await FailAsync(ex.Message);
Expand Down Expand Up @@ -203,7 +203,7 @@ async Task DescribeUninstallAsync(LocalInstallation installation, string root, b
await Console.Out.WriteLineAsync($" • Retire {installation.ServerUrl}: nothing is sent to it again, and a new installation gets a new port");
}

internal static void RenderRecovery(LocalInstallEvent e) {
internal static void RenderRecovery(TextWriter output, LocalInstallEvent e) {
var line = e switch {
LocalInstallEvent.Recovering r => $"Finishing an interrupted {r.Operation} (stopped at {r.Phase})…",
LocalInstallEvent.Recovered r => $"Finished the interrupted {r.Operation}",
Expand All @@ -212,7 +212,7 @@ internal static void RenderRecovery(LocalInstallEvent e) {
_ => LocalSetupTerminal.Line(e, CapacitorVersion.CurrentDisplay()),
};

if (line is not null) Console.Out.WriteLine($" {line}");
if (line is not null) output.WriteLine($" {line}");
}

static async Task<int> FailAsync(string message) {
Expand Down
15 changes: 8 additions & 7 deletions src/Capacitor.Cli/Commands/OutgoingRecorders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ namespace Capacitor.Cli.Commands;

sealed class OutgoingRecorders(
DaemonStore store, ConfigRoot config, ProfileContext profiles, UserHome home,
HarnessRegistry harnesses, BinaryProbe binaries, WatcherManager watchers, TimeProvider time) : IOutgoingRecorders {
HarnessRegistry harnesses, BinaryProbe binaries, WatcherManager watchers, TimeProvider time,
StandardStreams console) : IOutgoingRecorders {
static readonly TimeSpan ProbeBudget = TimeSpan.FromSeconds(5);

/// <summary>A launch still starting is counted too: it is not in the agent table until its runtime answers. A
Expand Down Expand Up @@ -43,24 +44,24 @@ public async Task<bool> StopAsync() {
var ended = await ServiceRemoval.EndAgentsAsync(store, id, time);

if (services.Uninstall(id, out var error)) {
if (!ended) DaemonCommands.MarkLedgerInterrupted(store, id, time);
await Console.Out.WriteLineAsync($" • Removed daemon service '{id}' ({services.Describe()})");
if (!ended) DaemonCommands.MarkLedgerInterrupted(store, id, time, console.Error);
await console.Out.WriteLineAsync($" • Removed daemon service '{id}' ({services.Describe()})");
} else {
await Console.Error.WriteLineAsync($" • Could not remove daemon service '{id}': {error}");
await console.Error.WriteLineAsync($" • Could not remove daemon service '{id}': {error}");
stopped = false;
}
}
} catch (PlatformNotSupportedException) {
// No service backend on this OS.
}

if (await new DaemonCommands(store, config, profiles, home, harnesses, binaries, time).HandleAsync(["daemon", "stop", "--yes"]) != 0)
if (await new DaemonCommands(store, config, profiles, home, harnesses, binaries, time, console).HandleAsync(["daemon", "stop", "--yes"]) != 0)
stopped = false;

if (await new CleanupCommand(watchers).HandleCleanup() != 0) stopped = false;
if (await new CleanupCommand(watchers, console).HandleCleanup() != 0) stopped = false;

foreach (var survivor in Live()) {
await Console.Error.WriteLineAsync($" • {survivor} is still running.");
await console.Error.WriteLineAsync($" • {survivor} is still running.");
stopped = false;
}

Expand Down
10 changes: 5 additions & 5 deletions src/Capacitor.Cli/Commands/PluginCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1840,7 +1840,7 @@ await env.Stderr.WriteLineAsync(
var hooksFailed = false;
var hooksCurrent = refreshOnly && File.Exists(agentPath) && KiroHooksInstaller.ReadMarker(agentPath) == CapacitorVersion.Current();
if (!hooksCurrent) {
if (InstallKiroHooks(agentPath, env.Harnesses)) {
if (InstallKiroHooks(agentPath, env.Harnesses, env.Stderr)) {
var clonedFrom = KiroHooksInstaller.ReadPreviousDefault(agentPath) ?? "your default agent";
await env.Stdout.WriteLineAsync(
refreshOnly
Expand Down Expand Up @@ -2059,7 +2059,7 @@ await env.Stdout.WriteLineAsync(
/// <c>chat.defaultAgent</c> to kcap. The replaced default is recorded in the
/// marker for <c>plugin remove --kiro</c> to restore. Idempotent.
/// </summary>
public static bool InstallKiroHooks(string agentJsonPath, HarnessRegistry harnesses) {
public static bool InstallKiroHooks(string agentJsonPath, HarnessRegistry harnesses, TextWriter stderr) {
try {
var settingsPath = KiroSettingsPathFor(agentJsonPath);
var currentDefault = KiroSettings.ReadDefaultAgent(settingsPath) ?? "kiro_default";
Expand Down Expand Up @@ -2106,7 +2106,7 @@ public static bool InstallKiroHooks(string agentJsonPath, HarnessRegistry harnes
// previous default + paths so the user can restore it by hand,
// rather than returning the same opaque false as "kiro-cli missing".
if (!KiroSettings.SetDefaultAgent(settingsPath, recordedDefault)) {
Console.Error.WriteLine(
stderr.WriteLine(
$"[kcap] Kiro install could not be completed OR rolled back. "
+ $"chat.defaultAgent may still be '{KiroAgentName}' with no install marker. "
+ $"Your previous default agent was '{recordedDefault}' — restore it manually "
Expand Down Expand Up @@ -2771,8 +2771,8 @@ public static bool RemoveGeminiHooks(string settingsPath) {
return idx >= 0 && idx + 1 < args.Length ? args[idx + 1] : null;
}

static int PrintUsage() {
Console.Error.WriteLine(
int PrintUsage() {
env.Stderr.WriteLine(
"Usage: kcap plugin <install|remove> [--project] [--codex|--cursor|--copilot|--gemini|--kiro|--pi|--opencode|--antigravity|--skills] [--tools-only] [--if-installed]"
);

Expand Down
3 changes: 2 additions & 1 deletion src/Capacitor.Cli/Commands/SetupCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,8 @@ browserAgents is null
CapacitorOnPath: () => binaries.Finds("kcap"),
InstallAgentSkills: AgentsSkillsInstaller.Install,
CleanLegacyCodexSkills: legacyDir => AgentsSkillsInstaller.CleanLegacyCodexSkills(legacyDir).RemovedAny,
InstallKiroHooks: agentPath => PluginCommand.InstallKiroHooks(agentPath, harnesses),
// Read when it runs: a reported run swaps stderr for the writer its failure line comes from.
InstallKiroHooks: agentPath => PluginCommand.InstallKiroHooks(agentPath, harnesses, Console.Error),
InstallPiExtension: PiExtensionInstaller.Install,
InstallOpenCodeExtension: OpenCodeExtensionInstaller.Install,
InstallAntigravityHooks: PluginCommand.InstallAntigravityHooks,
Expand Down
9 changes: 9 additions & 0 deletions src/Capacitor.Cli/Commands/StandardStreams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Capacitor.Cli.Commands;

/// <summary>
/// The console a command talks to, supplied rather than read from <see cref="Console"/> so a test can
/// hand each run its own writers and run beside other tests.
/// </summary>
public sealed record StandardStreams(TextWriter Out, TextWriter Error, TextReader In) {
public static StandardStreams FromProcess() => new(Console.Out, Console.Error, Console.In);
}
Loading
Loading