swarmkit 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +33 -0
  5. package/dist/commands/add.d.ts +2 -0
  6. package/dist/commands/add.js +98 -0
  7. package/dist/commands/doctor.d.ts +2 -0
  8. package/dist/commands/doctor.js +100 -0
  9. package/dist/commands/hive.d.ts +2 -0
  10. package/dist/commands/hive.js +248 -0
  11. package/dist/commands/init/phases/configure.d.ts +2 -0
  12. package/dist/commands/init/phases/configure.js +85 -0
  13. package/dist/commands/init/phases/global-setup.d.ts +2 -0
  14. package/dist/commands/init/phases/global-setup.js +81 -0
  15. package/dist/commands/init/phases/packages.d.ts +2 -0
  16. package/dist/commands/init/phases/packages.js +30 -0
  17. package/dist/commands/init/phases/project.d.ts +2 -0
  18. package/dist/commands/init/phases/project.js +56 -0
  19. package/dist/commands/init/phases/use-case.d.ts +2 -0
  20. package/dist/commands/init/phases/use-case.js +41 -0
  21. package/dist/commands/init/state.d.ts +13 -0
  22. package/dist/commands/init/state.js +9 -0
  23. package/dist/commands/init/state.test.d.ts +1 -0
  24. package/dist/commands/init/state.test.js +21 -0
  25. package/dist/commands/init/wizard.d.ts +4 -0
  26. package/dist/commands/init/wizard.js +108 -0
  27. package/dist/commands/init.d.ts +2 -0
  28. package/dist/commands/init.js +11 -0
  29. package/dist/commands/login.d.ts +2 -0
  30. package/dist/commands/login.js +91 -0
  31. package/dist/commands/logout.d.ts +2 -0
  32. package/dist/commands/logout.js +19 -0
  33. package/dist/commands/remove.d.ts +2 -0
  34. package/dist/commands/remove.js +55 -0
  35. package/dist/commands/status.d.ts +2 -0
  36. package/dist/commands/status.js +87 -0
  37. package/dist/commands/update.d.ts +2 -0
  38. package/dist/commands/update.js +54 -0
  39. package/dist/commands/whoami.d.ts +2 -0
  40. package/dist/commands/whoami.js +40 -0
  41. package/dist/config/global.d.ts +26 -0
  42. package/dist/config/global.js +71 -0
  43. package/dist/config/global.test.d.ts +1 -0
  44. package/dist/config/global.test.js +167 -0
  45. package/dist/config/keys.d.ts +10 -0
  46. package/dist/config/keys.js +47 -0
  47. package/dist/config/keys.test.d.ts +1 -0
  48. package/dist/config/keys.test.js +87 -0
  49. package/dist/doctor/checks.d.ts +31 -0
  50. package/dist/doctor/checks.js +226 -0
  51. package/dist/doctor/checks.test.d.ts +1 -0
  52. package/dist/doctor/checks.test.js +301 -0
  53. package/dist/doctor/types.d.ts +29 -0
  54. package/dist/doctor/types.js +1 -0
  55. package/dist/hub/auth-flow.d.ts +16 -0
  56. package/dist/hub/auth-flow.js +118 -0
  57. package/dist/hub/auth-flow.test.d.ts +1 -0
  58. package/dist/hub/auth-flow.test.js +98 -0
  59. package/dist/hub/client.d.ts +51 -0
  60. package/dist/hub/client.js +107 -0
  61. package/dist/hub/client.test.d.ts +1 -0
  62. package/dist/hub/client.test.js +177 -0
  63. package/dist/hub/credentials.d.ts +14 -0
  64. package/dist/hub/credentials.js +41 -0
  65. package/dist/hub/credentials.test.d.ts +1 -0
  66. package/dist/hub/credentials.test.js +102 -0
  67. package/dist/index.d.ts +17 -1
  68. package/dist/index.js +10 -2
  69. package/dist/packages/installer.d.ts +42 -0
  70. package/dist/packages/installer.js +158 -0
  71. package/dist/packages/installer.test.d.ts +1 -0
  72. package/dist/packages/installer.test.js +283 -0
  73. package/dist/packages/plugin.d.ts +13 -0
  74. package/dist/packages/plugin.js +33 -0
  75. package/dist/packages/plugin.test.d.ts +1 -0
  76. package/dist/packages/plugin.test.js +99 -0
  77. package/dist/packages/registry.d.ts +37 -0
  78. package/dist/packages/registry.js +154 -0
  79. package/dist/packages/registry.test.d.ts +1 -0
  80. package/dist/packages/registry.test.js +188 -0
  81. package/dist/packages/setup.d.ts +55 -0
  82. package/dist/packages/setup.js +414 -0
  83. package/dist/packages/setup.test.d.ts +1 -0
  84. package/dist/packages/setup.test.js +808 -0
  85. package/dist/utils/ui.d.ts +10 -0
  86. package/dist/utils/ui.js +47 -0
  87. package/dist/utils/ui.test.d.ts +1 -0
  88. package/dist/utils/ui.test.js +102 -0
  89. package/package.json +29 -6
@@ -0,0 +1,85 @@
1
+ import { select, password } from "@inquirer/prompts";
2
+ import chalk from "chalk";
3
+ import { readConfig, writeConfig } from "../../../config/global.js";
4
+ import { writeKey } from "../../../config/keys.js";
5
+ import * as ui from "../../../utils/ui.js";
6
+ /** Packages that use embeddings */
7
+ const EMBEDDING_CONSUMERS = ["minimem", "cognitive-core", "skill-tree"];
8
+ export async function configureKeys(state) {
9
+ const updated = { ...state };
10
+ // Check if any installed package uses embeddings
11
+ const needsEmbeddings = state.selectedPackages.some((p) => EMBEDDING_CONSUMERS.includes(p));
12
+ if (needsEmbeddings) {
13
+ console.log();
14
+ const embeddingChoice = await select({
15
+ message: "Embedding provider (used by " +
16
+ state.selectedPackages
17
+ .filter((p) => EMBEDDING_CONSUMERS.includes(p))
18
+ .join(", ") +
19
+ "):",
20
+ choices: [
21
+ { name: "OpenAI (requires OPENAI_API_KEY)", value: "openai" },
22
+ {
23
+ name: "Gemini (free tier available)",
24
+ value: "gemini",
25
+ },
26
+ { name: "Local (no API key, uses local models)", value: "local" },
27
+ { name: "Skip (configure later)", value: null },
28
+ ],
29
+ });
30
+ if (embeddingChoice) {
31
+ updated.embeddingProvider = embeddingChoice;
32
+ // Collect embedding API key if needed
33
+ if (embeddingChoice === "openai") {
34
+ const key = await password({
35
+ message: "OpenAI API key:",
36
+ mask: "*",
37
+ });
38
+ if (key) {
39
+ updated.apiKeys["openai"] = key;
40
+ writeKey("openai", key);
41
+ ui.success("OpenAI API key stored");
42
+ }
43
+ }
44
+ else if (embeddingChoice === "gemini") {
45
+ const key = await password({
46
+ message: "Gemini API key:",
47
+ mask: "*",
48
+ });
49
+ if (key) {
50
+ updated.apiKeys["gemini"] = key;
51
+ writeKey("gemini", key);
52
+ ui.success("Gemini API key stored");
53
+ }
54
+ }
55
+ // Save embedding preference
56
+ const config = readConfig();
57
+ config.embeddingProvider = embeddingChoice;
58
+ if (embeddingChoice === "openai") {
59
+ config.embeddingModel = "text-embedding-3-small";
60
+ }
61
+ writeConfig(config);
62
+ }
63
+ }
64
+ // Always ask for Anthropic key (needed for agents)
65
+ console.log();
66
+ const hasAnthropicKey = !!process.env["ANTHROPIC_API_KEY"];
67
+ if (hasAnthropicKey) {
68
+ ui.info(`Anthropic API key detected from environment ${chalk.dim("(ANTHROPIC_API_KEY)")}`);
69
+ }
70
+ else {
71
+ const anthropicKey = await password({
72
+ message: "Anthropic API key (for running agents):",
73
+ mask: "*",
74
+ });
75
+ if (anthropicKey) {
76
+ updated.apiKeys["anthropic"] = anthropicKey;
77
+ writeKey("anthropic", anthropicKey);
78
+ ui.success("Anthropic API key stored");
79
+ }
80
+ else {
81
+ ui.info("Skipped — you can set ANTHROPIC_API_KEY later.");
82
+ }
83
+ }
84
+ return updated;
85
+ }
@@ -0,0 +1,2 @@
1
+ import type { WizardState } from "../state.js";
2
+ export declare function initGlobal(state: WizardState): Promise<void>;
@@ -0,0 +1,81 @@
1
+ import { input, select } from "@inquirer/prompts";
2
+ import chalk from "chalk";
3
+ import { GLOBAL_CONFIG_DIRS, isGlobalInit, initGlobalPackage, } from "../../../packages/setup.js";
4
+ import * as ui from "../../../utils/ui.js";
5
+ /** Global packages in setup order */
6
+ const GLOBAL_SETUP_ORDER = ["skill-tree", "openhive"];
7
+ export async function initGlobal(state) {
8
+ const globalPackages = GLOBAL_SETUP_ORDER.filter((pkg) => state.selectedPackages.includes(pkg));
9
+ if (globalPackages.length === 0)
10
+ return;
11
+ const uninitialized = globalPackages.filter((pkg) => !isGlobalInit(pkg));
12
+ if (uninitialized.length === 0) {
13
+ ui.blank();
14
+ ui.info("All global packages already configured.");
15
+ return;
16
+ }
17
+ ui.heading(" Global package setup");
18
+ const ctx = {
19
+ packages: state.selectedPackages,
20
+ embeddingProvider: state.embeddingProvider,
21
+ apiKeys: state.apiKeys,
22
+ };
23
+ for (const pkg of uninitialized) {
24
+ let openhiveOpts;
25
+ // Openhive: intercept into the wizard for inline configuration
26
+ if (pkg === "openhive") {
27
+ console.log();
28
+ ui.info("Configuring OpenHive...");
29
+ console.log();
30
+ const name = await input({
31
+ message: "OpenHive instance name:",
32
+ default: "OpenHive",
33
+ });
34
+ const port = await input({
35
+ message: "OpenHive port:",
36
+ default: "3000",
37
+ });
38
+ const verification = await select({
39
+ message: "Registration mode:",
40
+ choices: [
41
+ { name: "Open — anyone can register", value: "open" },
42
+ {
43
+ name: "Invite — require an invite code",
44
+ value: "invite",
45
+ },
46
+ {
47
+ name: "Manual — admin approves each",
48
+ value: "manual",
49
+ },
50
+ ],
51
+ });
52
+ const authMode = await select({
53
+ message: "Auth mode:",
54
+ choices: [
55
+ { name: "Local — no login, single-user", value: "local" },
56
+ {
57
+ name: "Token — email/password registration",
58
+ value: "token",
59
+ },
60
+ ],
61
+ });
62
+ openhiveOpts = {
63
+ name,
64
+ port: parseInt(port, 10) || 3000,
65
+ authMode,
66
+ verification,
67
+ };
68
+ }
69
+ const result = await initGlobalPackage(pkg, ctx, openhiveOpts);
70
+ const configDir = GLOBAL_CONFIG_DIRS[pkg] ?? "";
71
+ const suffix = result.message
72
+ ? ` ${chalk.dim(`(${result.message})`)}`
73
+ : "";
74
+ if (result.success) {
75
+ ui.success(`${pkg} ${chalk.dim("→")} ~/${configDir}/${suffix}`);
76
+ }
77
+ else {
78
+ ui.fail(`${pkg}: ${result.message ?? "setup failed"}`);
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,2 @@
1
+ import type { WizardState } from "../state.js";
2
+ export declare function installSelectedPackages(state: WizardState): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import chalk from "chalk";
2
+ import { installPackages } from "../../../packages/installer.js";
3
+ import { addInstalledPackages } from "../../../config/global.js";
4
+ import * as ui from "../../../utils/ui.js";
5
+ export async function installSelectedPackages(state) {
6
+ const { selectedPackages } = state;
7
+ console.log();
8
+ console.log(` Installing ${chalk.bold(selectedPackages.length.toString())} packages...`);
9
+ console.log();
10
+ const results = await installPackages(selectedPackages);
11
+ let failures = 0;
12
+ for (const result of results) {
13
+ if (result.success) {
14
+ ui.success(`${result.package} ${result.version ? chalk.dim("v" + result.version) : ""}`);
15
+ }
16
+ else {
17
+ ui.fail(`${result.package} — ${result.error}`);
18
+ failures++;
19
+ }
20
+ }
21
+ // Record successful installations
22
+ const installed = results.filter((r) => r.success).map((r) => r.package);
23
+ if (installed.length > 0) {
24
+ addInstalledPackages(installed);
25
+ }
26
+ if (failures > 0) {
27
+ ui.blank();
28
+ ui.warn(`${failures} package${failures === 1 ? "" : "s"} failed to install. Run \`swarmkit doctor\` to diagnose.`);
29
+ }
30
+ }
@@ -0,0 +1,2 @@
1
+ import type { WizardState } from "../state.js";
2
+ export declare function initProject(state: WizardState): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { confirm } from "@inquirer/prompts";
4
+ import chalk from "chalk";
5
+ import { PACKAGES } from "../../../packages/registry.js";
6
+ import { PROJECT_INIT_ORDER, projectConfigDirs, isProjectInit, initProjectPackage, } from "../../../packages/setup.js";
7
+ import * as ui from "../../../utils/ui.js";
8
+ export async function initProject(state) {
9
+ const cwd = process.cwd();
10
+ // Only offer project init if we're in a directory that looks like a project
11
+ const isProject = existsSync(join(cwd, ".git")) || existsSync(join(cwd, "package.json"));
12
+ if (!isProject) {
13
+ ui.blank();
14
+ ui.info("Run `swarmkit init` inside a project directory to initialize per-project configs.");
15
+ return;
16
+ }
17
+ const configDirs = projectConfigDirs(state.usePrefix);
18
+ // Filter to packages that have project-level config, in init order
19
+ const projectPackages = PROJECT_INIT_ORDER.filter((pkg) => state.selectedPackages.includes(pkg) &&
20
+ !PACKAGES[pkg]?.globalOnly &&
21
+ pkg in configDirs);
22
+ if (projectPackages.length === 0)
23
+ return;
24
+ // Check which are already initialized
25
+ const uninitialized = projectPackages.filter((pkg) => !isProjectInit(cwd, pkg));
26
+ if (uninitialized.length === 0) {
27
+ ui.blank();
28
+ ui.info("All packages already initialized in this project.");
29
+ return;
30
+ }
31
+ console.log();
32
+ const shouldInit = await confirm({
33
+ message: `Initialize ${uninitialized.length} package${uninitialized.length === 1 ? "" : "s"} in ${chalk.dim(cwd)}?`,
34
+ default: true,
35
+ });
36
+ if (!shouldInit)
37
+ return;
38
+ const ctx = {
39
+ cwd,
40
+ packages: state.selectedPackages,
41
+ embeddingProvider: state.embeddingProvider,
42
+ apiKeys: state.apiKeys,
43
+ usePrefix: state.usePrefix,
44
+ };
45
+ ui.blank();
46
+ for (const pkg of uninitialized) {
47
+ const result = await initProjectPackage(pkg, ctx);
48
+ const configDir = configDirs[pkg] ?? "";
49
+ if (result.success) {
50
+ ui.success(`${pkg} ${chalk.dim("→")} ${configDir}/`);
51
+ }
52
+ else {
53
+ ui.fail(`${pkg}: ${result.message ?? "initialization failed"}`);
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,2 @@
1
+ import type { WizardState } from "../state.js";
2
+ export declare function selectUseCase(state: WizardState): Promise<WizardState>;
@@ -0,0 +1,41 @@
1
+ import { select, checkbox } from "@inquirer/prompts";
2
+ import { BUNDLES, PACKAGES, getAllPackageNames } from "../../../packages/registry.js";
3
+ export async function selectUseCase(state) {
4
+ const bundleChoice = await select({
5
+ message: "What are you building?",
6
+ choices: [
7
+ ...Object.values(BUNDLES).map((b) => ({
8
+ name: `${b.label} (${b.name} bundle — ${b.packages.length} packages)`,
9
+ value: b.name,
10
+ })),
11
+ {
12
+ name: "Let me pick individually",
13
+ value: "custom",
14
+ },
15
+ ],
16
+ });
17
+ if (bundleChoice === "custom") {
18
+ const selected = await checkbox({
19
+ message: "Select packages to install:",
20
+ choices: getAllPackageNames().map((name) => ({
21
+ name: `${name} ${PACKAGES[name].description}`,
22
+ value: name,
23
+ })),
24
+ });
25
+ if (selected.length === 0) {
26
+ console.log("\n No packages selected.\n");
27
+ process.exit(0);
28
+ }
29
+ return {
30
+ ...state,
31
+ bundle: "custom",
32
+ selectedPackages: selected,
33
+ };
34
+ }
35
+ const bundle = BUNDLES[bundleChoice];
36
+ return {
37
+ ...state,
38
+ bundle: bundleChoice,
39
+ selectedPackages: [...bundle.packages],
40
+ };
41
+ }
@@ -0,0 +1,13 @@
1
+ export interface WizardState {
2
+ /** Selected bundle name, or "custom" */
3
+ bundle: string;
4
+ /** Packages to install */
5
+ selectedPackages: string[];
6
+ /** Embedding provider choice */
7
+ embeddingProvider: "openai" | "gemini" | "local" | null;
8
+ /** Collected API keys (provider -> key) */
9
+ apiKeys: Record<string, string>;
10
+ /** Whether to nest project configs under .swarm/ (default true) */
11
+ usePrefix: boolean;
12
+ }
13
+ export declare function createEmptyState(): WizardState;
@@ -0,0 +1,9 @@
1
+ export function createEmptyState() {
2
+ return {
3
+ bundle: "",
4
+ selectedPackages: [],
5
+ embeddingProvider: null,
6
+ apiKeys: {},
7
+ usePrefix: true,
8
+ };
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createEmptyState } from "./state.js";
3
+ describe("WizardState", () => {
4
+ describe("createEmptyState", () => {
5
+ it("returns a state with empty defaults", () => {
6
+ const state = createEmptyState();
7
+ expect(state.bundle).toBe("");
8
+ expect(state.selectedPackages).toEqual([]);
9
+ expect(state.embeddingProvider).toBeNull();
10
+ expect(state.apiKeys).toEqual({});
11
+ expect(state.usePrefix).toBe(true);
12
+ });
13
+ it("returns a new object each time", () => {
14
+ const a = createEmptyState();
15
+ const b = createEmptyState();
16
+ expect(a).not.toBe(b);
17
+ expect(a.selectedPackages).not.toBe(b.selectedPackages);
18
+ expect(a.apiKeys).not.toBe(b.apiKeys);
19
+ });
20
+ });
21
+ });
@@ -0,0 +1,4 @@
1
+ export interface WizardOptions {
2
+ noPrefix?: boolean;
3
+ }
4
+ export declare function runWizard(opts?: WizardOptions): Promise<void>;
@@ -0,0 +1,108 @@
1
+ import chalk from "chalk";
2
+ import { select } from "@inquirer/prompts";
3
+ import { isFirstRun, readConfig, writeConfig, ensureConfigDir } from "../../config/global.js";
4
+ import * as ui from "../../utils/ui.js";
5
+ import { getActiveIntegrations } from "../../packages/registry.js";
6
+ import { isClaudeCliAvailable } from "../../packages/installer.js";
7
+ import { isInstalledPlugin, registerPlugin } from "../../packages/plugin.js";
8
+ import { createEmptyState } from "./state.js";
9
+ import { selectUseCase } from "./phases/use-case.js";
10
+ import { installSelectedPackages } from "./phases/packages.js";
11
+ import { configureKeys } from "./phases/configure.js";
12
+ import { initProject } from "./phases/project.js";
13
+ import { initGlobal } from "./phases/global-setup.js";
14
+ export async function runWizard(opts) {
15
+ console.log();
16
+ console.log(` ${chalk.bold("swarmkit")} ${chalk.dim("— multi-agent infrastructure toolkit")}`);
17
+ console.log();
18
+ if (isFirstRun()) {
19
+ await runFirstTimeSetup(opts);
20
+ }
21
+ else {
22
+ await runProjectSetup(opts);
23
+ }
24
+ }
25
+ async function runFirstTimeSetup(opts) {
26
+ ensureConfigDir();
27
+ // Step 1: Use case / bundle selection
28
+ let state = await selectUseCase(createEmptyState());
29
+ if (opts?.noPrefix) {
30
+ state.usePrefix = false;
31
+ }
32
+ // Step 2: Install packages
33
+ await installSelectedPackages(state);
34
+ // Step 2.5: Plugin activation (for any Claude Code plugins)
35
+ await activatePlugins(state);
36
+ // Step 3: Configure (embedding provider, API keys)
37
+ state = await configureKeys(state);
38
+ // Step 4: Global package setup (skill-tree, openhive)
39
+ await initGlobal(state);
40
+ // Step 5: Project init (if in a project directory)
41
+ await initProject(state);
42
+ // Persist prefix preference
43
+ const config = readConfig();
44
+ config.usePrefix = state.usePrefix;
45
+ writeConfig(config);
46
+ // Summary
47
+ printSummary(state);
48
+ }
49
+ async function runProjectSetup(opts) {
50
+ const config = readConfig();
51
+ ui.info(`Global config found. ${config.installedPackages.length} packages installed.`);
52
+ const state = createEmptyState();
53
+ state.selectedPackages = [...config.installedPackages];
54
+ state.usePrefix = opts?.noPrefix === true ? false : (config.usePrefix ?? true);
55
+ await initProject(state);
56
+ }
57
+ async function activatePlugins(state) {
58
+ if (!(await isClaudeCliAvailable()))
59
+ return;
60
+ for (const pkg of state.selectedPackages) {
61
+ if (!(await isInstalledPlugin(pkg)))
62
+ continue;
63
+ console.log();
64
+ ui.info(`${pkg} is a Claude Code plugin.`);
65
+ const choice = await select({
66
+ message: `Activate ${pkg} as a Claude Code plugin?`,
67
+ choices: [
68
+ {
69
+ name: "Project (this repository only)",
70
+ value: "project",
71
+ },
72
+ {
73
+ name: "User (all projects)",
74
+ value: "user",
75
+ },
76
+ {
77
+ name: "Skip (activate later)",
78
+ value: "skip",
79
+ },
80
+ ],
81
+ });
82
+ if (choice === "skip") {
83
+ ui.info("Skipped. Activate later with: claude plugin add <path> --scope project|user");
84
+ continue;
85
+ }
86
+ try {
87
+ await registerPlugin(pkg, choice);
88
+ ui.success(`Plugin activated (${choice} scope).`);
89
+ }
90
+ catch (err) {
91
+ const msg = err instanceof Error ? err.message : String(err);
92
+ ui.fail(`Plugin activation failed: ${msg}`);
93
+ }
94
+ }
95
+ }
96
+ function printSummary(state) {
97
+ const integrations = getActiveIntegrations(state.selectedPackages);
98
+ if (integrations.length > 0) {
99
+ ui.heading(" Active integrations:");
100
+ for (const integration of integrations) {
101
+ const [a, b] = integration.packages;
102
+ ui.bullet(` ${a} ${chalk.dim("↔")} ${b} ${chalk.dim(integration.description)}`);
103
+ }
104
+ }
105
+ ui.blank();
106
+ console.log(` ${chalk.green("Done.")} Run ${chalk.bold("`swarmkit status`")} to see your setup.`);
107
+ ui.blank();
108
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerInitCommand(program: Command): void;
@@ -0,0 +1,11 @@
1
+ export function registerInitCommand(program) {
2
+ program
3
+ .command("init")
4
+ .description("Interactive setup wizard")
5
+ .option("--no-prefix", "Use flat layout (e.g. .opentasks/) instead of nesting under .swarm/")
6
+ .action(async (opts) => {
7
+ // Dynamic import to avoid loading @inquirer/prompts for other commands
8
+ const { runWizard } = await import("./init/wizard.js");
9
+ await runWizard({ noPrefix: opts.prefix === false });
10
+ });
11
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerLoginCommand(program: Command): void;
@@ -0,0 +1,91 @@
1
+ import chalk from "chalk";
2
+ import { isLoggedIn } from "../hub/credentials.js";
3
+ import { getMe, HubApiError } from "../hub/client.js";
4
+ import { findAvailablePort, getLoginUrl, runLoginFlow, } from "../hub/auth-flow.js";
5
+ import * as ui from "../utils/ui.js";
6
+ export function registerLoginCommand(program) {
7
+ program
8
+ .command("login")
9
+ .description("Log in to SwarmHub")
10
+ .option("--hub <url>", "SwarmHub URL (default: https://hub.swarmkit.ai)")
11
+ .action(async (opts) => {
12
+ // Set env var so getHubUrl() picks it up for this session
13
+ if (opts.hub) {
14
+ process.env.SWARMKIT_HUB_URL = opts.hub;
15
+ }
16
+ if (isLoggedIn()) {
17
+ try {
18
+ const user = await getMe();
19
+ ui.blank();
20
+ ui.info(`Already logged in as ${chalk.bold(user.name)} (${user.email})`);
21
+ ui.info("Run `swarmkit logout` first to switch accounts.");
22
+ ui.blank();
23
+ return;
24
+ }
25
+ catch {
26
+ // Token expired or invalid — proceed with login
27
+ }
28
+ }
29
+ let port;
30
+ try {
31
+ port = await findAvailablePort();
32
+ }
33
+ catch {
34
+ ui.blank();
35
+ ui.fail("Could not find an available port for the callback server.");
36
+ ui.blank();
37
+ return;
38
+ }
39
+ const loginUrl = getLoginUrl(port);
40
+ ui.blank();
41
+ ui.info("Opening browser to authenticate with SwarmHub...");
42
+ const opened = await openBrowser(loginUrl);
43
+ if (!opened) {
44
+ ui.blank();
45
+ ui.info("Open this URL in your browser to log in:");
46
+ ui.blank();
47
+ console.log(` ${loginUrl}`);
48
+ }
49
+ ui.blank();
50
+ ui.info("Waiting for authentication...");
51
+ try {
52
+ await runLoginFlow(port);
53
+ const user = await getMe();
54
+ ui.blank();
55
+ ui.success(`Logged in as ${chalk.bold(user.name)} (${user.email})`);
56
+ ui.blank();
57
+ }
58
+ catch (err) {
59
+ ui.blank();
60
+ if (err instanceof HubApiError) {
61
+ ui.fail(`Login failed: ${err.message}`);
62
+ }
63
+ else if (err instanceof Error) {
64
+ ui.fail(err.message);
65
+ }
66
+ else {
67
+ ui.fail("Login failed.");
68
+ }
69
+ ui.blank();
70
+ }
71
+ });
72
+ }
73
+ async function openBrowser(url) {
74
+ try {
75
+ const { exec } = await import("node:child_process");
76
+ const { platform } = await import("node:os");
77
+ const cmd = platform() === "darwin"
78
+ ? "open"
79
+ : platform() === "win32"
80
+ ? "start"
81
+ : "xdg-open";
82
+ return new Promise((resolve) => {
83
+ exec(`${cmd} ${JSON.stringify(url)}`, (err) => {
84
+ resolve(!err);
85
+ });
86
+ });
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerLogoutCommand(program: Command): void;
@@ -0,0 +1,19 @@
1
+ import { isLoggedIn, deleteCredentials } from "../hub/credentials.js";
2
+ import * as ui from "../utils/ui.js";
3
+ export function registerLogoutCommand(program) {
4
+ program
5
+ .command("logout")
6
+ .description("Log out of SwarmHub")
7
+ .action(() => {
8
+ if (!isLoggedIn()) {
9
+ ui.blank();
10
+ ui.info("Not currently logged in.");
11
+ ui.blank();
12
+ return;
13
+ }
14
+ deleteCredentials();
15
+ ui.blank();
16
+ ui.success("Logged out of SwarmHub.");
17
+ ui.blank();
18
+ });
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerRemoveCommand(program: Command): void;