@sweny-ai/core 0.1.65 → 0.1.67

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.
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { shouldSkip, readCache, writeCache, isCacheFresh, formatNudge, defaultCachePath, maybeNudge, CACHE_TTL_MS, } from "../version-check.js";
6
+ function captureStream() {
7
+ const chunks = [];
8
+ const stream = {
9
+ write: (chunk) => {
10
+ chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
11
+ return true;
12
+ },
13
+ };
14
+ return {
15
+ stream,
16
+ get text() {
17
+ return chunks.join("");
18
+ },
19
+ };
20
+ }
21
+ describe("shouldSkip", () => {
22
+ it("skips in CI", () => {
23
+ expect(shouldSkip({ CI: "1" }, true, "run")).toBe(true);
24
+ });
25
+ it("skips when SWENY_NO_UPDATE_CHECK is set", () => {
26
+ expect(shouldSkip({ SWENY_NO_UPDATE_CHECK: "1" }, true, "run")).toBe(true);
27
+ });
28
+ it("skips when SWENY_OFFLINE is set", () => {
29
+ expect(shouldSkip({ SWENY_OFFLINE: "1" }, true, "run")).toBe(true);
30
+ });
31
+ it("skips non-TTY invocations (pipes, scripts)", () => {
32
+ expect(shouldSkip({}, false, "run")).toBe(true);
33
+ });
34
+ it("skips the upgrade command itself to avoid double-printing", () => {
35
+ expect(shouldSkip({}, true, "upgrade")).toBe(true);
36
+ expect(shouldSkip({}, true, "update")).toBe(true);
37
+ });
38
+ it("allows the nudge in an interactive TTY with no opt-out", () => {
39
+ expect(shouldSkip({}, true, "run")).toBe(false);
40
+ });
41
+ });
42
+ describe("cache I/O", () => {
43
+ let tmpdir;
44
+ let cachePath;
45
+ beforeEach(() => {
46
+ tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-version-check-"));
47
+ cachePath = path.join(tmpdir, "sub", "version-check.json");
48
+ });
49
+ afterEach(() => {
50
+ fs.rmSync(tmpdir, { recursive: true, force: true });
51
+ });
52
+ it("round-trips a valid cache entry", () => {
53
+ writeCache(cachePath, { latest: "0.1.66", checkedAt: 12345 });
54
+ expect(readCache(cachePath)).toEqual({ latest: "0.1.66", checkedAt: 12345 });
55
+ });
56
+ it("creates parent directories as needed", () => {
57
+ writeCache(cachePath, { latest: "0.1.66", checkedAt: 12345 });
58
+ expect(fs.existsSync(cachePath)).toBe(true);
59
+ });
60
+ it("returns null for a missing cache file", () => {
61
+ expect(readCache(cachePath)).toBeNull();
62
+ });
63
+ it("returns null when the cache is malformed JSON", () => {
64
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
65
+ fs.writeFileSync(cachePath, "{{{not json");
66
+ expect(readCache(cachePath)).toBeNull();
67
+ });
68
+ it("returns null when required fields are missing or wrong type", () => {
69
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
70
+ fs.writeFileSync(cachePath, JSON.stringify({ latest: "0.1.66" }));
71
+ expect(readCache(cachePath)).toBeNull();
72
+ fs.writeFileSync(cachePath, JSON.stringify({ checkedAt: 1 }));
73
+ expect(readCache(cachePath)).toBeNull();
74
+ fs.writeFileSync(cachePath, JSON.stringify({ latest: "", checkedAt: 1 }));
75
+ expect(readCache(cachePath)).toBeNull();
76
+ });
77
+ it("never throws on unwritable cache directories (degrades silently)", () => {
78
+ // Point at a path where the parent is a file — mkdirSync will throw,
79
+ // and writeCache must swallow it.
80
+ const blocker = path.join(tmpdir, "blocker");
81
+ fs.writeFileSync(blocker, "im-a-file");
82
+ expect(() => writeCache(path.join(blocker, "cache.json"), { latest: "0.1.66", checkedAt: 1 })).not.toThrow();
83
+ });
84
+ });
85
+ describe("isCacheFresh", () => {
86
+ it("returns true within TTL", () => {
87
+ expect(isCacheFresh({ latest: "x", checkedAt: 1000 }, 1000 + CACHE_TTL_MS - 1)).toBe(true);
88
+ });
89
+ it("returns false at or past TTL", () => {
90
+ expect(isCacheFresh({ latest: "x", checkedAt: 1000 }, 1000 + CACHE_TTL_MS)).toBe(false);
91
+ expect(isCacheFresh({ latest: "x", checkedAt: 1000 }, 1000 + CACHE_TTL_MS + 1)).toBe(false);
92
+ });
93
+ });
94
+ describe("formatNudge", () => {
95
+ it("includes both versions and the upgrade command", () => {
96
+ const out = formatNudge("0.1.65", "0.1.66");
97
+ expect(out).toContain("0.1.65");
98
+ expect(out).toContain("0.1.66");
99
+ expect(out).toContain("sweny upgrade");
100
+ });
101
+ });
102
+ describe("defaultCachePath", () => {
103
+ it("honors XDG_CACHE_HOME when set", () => {
104
+ const p = defaultCachePath({ XDG_CACHE_HOME: "/custom/cache" }, "/home/x");
105
+ expect(p).toBe(path.join("/custom/cache", "sweny", "version-check.json"));
106
+ });
107
+ it("falls back to ~/.cache when XDG_CACHE_HOME is unset", () => {
108
+ expect(defaultCachePath({}, "/home/x")).toBe(path.join("/home/x/.cache", "sweny", "version-check.json"));
109
+ });
110
+ it("treats empty XDG_CACHE_HOME as unset (defensive)", () => {
111
+ expect(defaultCachePath({ XDG_CACHE_HOME: "" }, "/home/x")).toBe(path.join("/home/x/.cache", "sweny", "version-check.json"));
112
+ });
113
+ });
114
+ describe("maybeNudge", () => {
115
+ let tmpdir;
116
+ let cachePath;
117
+ let stderr;
118
+ const fetchOk = vi.fn(async () => "9.9.9");
119
+ beforeEach(() => {
120
+ tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-nudge-"));
121
+ cachePath = path.join(tmpdir, "version-check.json");
122
+ stderr = captureStream();
123
+ fetchOk.mockClear();
124
+ });
125
+ afterEach(() => {
126
+ fs.rmSync(tmpdir, { recursive: true, force: true });
127
+ });
128
+ function baseDeps(over = {}) {
129
+ return {
130
+ currentVersion: "0.1.65",
131
+ cachePath,
132
+ now: 1_000_000,
133
+ env: {},
134
+ isTty: true,
135
+ commandName: "run",
136
+ stderr: stderr.stream,
137
+ fetchLatestVersion: fetchOk,
138
+ ...over,
139
+ };
140
+ }
141
+ it("prints nudge when cache shows a newer release", async () => {
142
+ writeCache(cachePath, { latest: "0.1.66", checkedAt: 1_000_000 });
143
+ await maybeNudge(baseDeps());
144
+ expect(stderr.text).toMatch(/0\.1\.66 is available/);
145
+ expect(stderr.text).toMatch(/sweny upgrade/);
146
+ });
147
+ it("stays silent when cache is equal or lower than current", async () => {
148
+ writeCache(cachePath, { latest: "0.1.65", checkedAt: 1_000_000 });
149
+ await maybeNudge(baseDeps());
150
+ expect(stderr.text).toBe("");
151
+ });
152
+ it("refreshes the cache when stale and writes the fetched version", async () => {
153
+ writeCache(cachePath, { latest: "0.1.64", checkedAt: 0 });
154
+ await maybeNudge(baseDeps({ now: CACHE_TTL_MS + 10 }));
155
+ expect(fetchOk).toHaveBeenCalledWith("latest");
156
+ const after = readCache(cachePath);
157
+ expect(after?.latest).toBe("9.9.9");
158
+ expect(after?.checkedAt).toBe(CACHE_TTL_MS + 10);
159
+ });
160
+ it("refreshes when cache is missing and does not crash when fetch fails", async () => {
161
+ const fetchFail = vi.fn(async () => {
162
+ throw new Error("ENETDOWN");
163
+ });
164
+ await maybeNudge(baseDeps({ fetchLatestVersion: fetchFail }));
165
+ expect(fetchFail).toHaveBeenCalled();
166
+ expect(readCache(cachePath)).toBeNull(); // fetch failed, nothing written
167
+ expect(stderr.text).toBe(""); // silent on error
168
+ });
169
+ it("does not refresh when cache is fresh (saves bandwidth)", async () => {
170
+ writeCache(cachePath, { latest: "0.1.65", checkedAt: 1_000_000 });
171
+ await maybeNudge(baseDeps({ now: 1_000_000 + 1 }));
172
+ expect(fetchOk).not.toHaveBeenCalled();
173
+ });
174
+ it("skips entirely in CI — no nudge, no fetch", async () => {
175
+ writeCache(cachePath, { latest: "0.1.66", checkedAt: 1_000_000 });
176
+ await maybeNudge(baseDeps({ env: { CI: "1" } }));
177
+ expect(stderr.text).toBe("");
178
+ expect(fetchOk).not.toHaveBeenCalled();
179
+ });
180
+ it("skips entirely on non-TTY stderr", async () => {
181
+ writeCache(cachePath, { latest: "0.1.66", checkedAt: 1_000_000 });
182
+ await maybeNudge(baseDeps({ isTty: false }));
183
+ expect(stderr.text).toBe("");
184
+ expect(fetchOk).not.toHaveBeenCalled();
185
+ });
186
+ it("suppresses the nudge from within `sweny upgrade` itself", async () => {
187
+ writeCache(cachePath, { latest: "0.1.66", checkedAt: 1_000_000 });
188
+ await maybeNudge(baseDeps({ commandName: "upgrade" }));
189
+ expect(stderr.text).toBe("");
190
+ expect(fetchOk).not.toHaveBeenCalled();
191
+ });
192
+ });
@@ -0,0 +1,30 @@
1
+ import type { Command } from "commander";
2
+ import type { Workflow } from "../types.js";
3
+ export interface DiagramOptions {
4
+ direction?: string;
5
+ title?: string;
6
+ block?: boolean;
7
+ output?: string;
8
+ }
9
+ export interface DiagramDeps {
10
+ /** Load a workflow file (YAML or JSON). Injected so tests can stub. */
11
+ loadWorkflowFile: (file: string) => Workflow;
12
+ /** Commander instance used to detect whether --block was set explicitly. */
13
+ command?: Command;
14
+ /** Stream for success/error messages. Defaults to process.stderr. */
15
+ stderr?: NodeJS.WritableStream;
16
+ /** Stream for diagram output when no --output is set. Defaults to process.stdout. */
17
+ stdout?: NodeJS.WritableStream;
18
+ /** Exit handler. Defaults to process.exit. Tests can throw instead. */
19
+ exit?: (code: number) => void;
20
+ }
21
+ /**
22
+ * Render a workflow as a Mermaid diagram and write to stdout or a file.
23
+ *
24
+ * Block-default rules (in order of precedence):
25
+ * 1. Explicit `--block` / `--no-block` — always wins (detected via the
26
+ * Command's option-value source when provided)
27
+ * 2. When `-o` is set, infer from extension: `.mmd` → raw, `.md` → fenced
28
+ * 3. Otherwise default to fenced (matches prior stdout behavior)
29
+ */
30
+ export declare function runWorkflowDiagram(file: string, options: DiagramOptions, deps: DiagramDeps): void;
@@ -0,0 +1,74 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import chalk from "chalk";
4
+ import { toMermaid, toMermaidBlock } from "../mermaid.js";
5
+ import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workflows/index.js";
6
+ /**
7
+ * Render a workflow as a Mermaid diagram and write to stdout or a file.
8
+ *
9
+ * Block-default rules (in order of precedence):
10
+ * 1. Explicit `--block` / `--no-block` — always wins (detected via the
11
+ * Command's option-value source when provided)
12
+ * 2. When `-o` is set, infer from extension: `.mmd` → raw, `.md` → fenced
13
+ * 3. Otherwise default to fenced (matches prior stdout behavior)
14
+ */
15
+ export function runWorkflowDiagram(file, options, deps) {
16
+ const stderr = deps.stderr ?? process.stderr;
17
+ const stdout = deps.stdout ?? process.stdout;
18
+ const exit = deps.exit ?? ((code) => process.exit(code));
19
+ let workflow;
20
+ // Builtin workflow names
21
+ if (file === "triage") {
22
+ workflow = triageWorkflow;
23
+ }
24
+ else if (file === "implement") {
25
+ workflow = implementWorkflow;
26
+ }
27
+ else if (file === "seed-content") {
28
+ workflow = seedContentWorkflow;
29
+ }
30
+ else {
31
+ try {
32
+ workflow = deps.loadWorkflowFile(file);
33
+ }
34
+ catch (err) {
35
+ stderr.write(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`) + "\n");
36
+ exit(1);
37
+ return;
38
+ }
39
+ }
40
+ const direction = (options.direction === "LR" ? "LR" : "TB");
41
+ const title = options.title ?? workflow.name;
42
+ // Explicit --block / --no-block always wins. When commander's source is
43
+ // "cli" or "env" the user (or env var) set it; otherwise we may infer.
44
+ const blockSource = deps.command?.getOptionValueSource("block");
45
+ const blockExplicit = blockSource === "cli" || blockSource === "env";
46
+ let useBlock = options.block !== false;
47
+ if (!blockExplicit && options.output) {
48
+ const ext = path.extname(options.output).toLowerCase();
49
+ if (ext === ".mmd")
50
+ useBlock = false;
51
+ else if (ext === ".md")
52
+ useBlock = true;
53
+ }
54
+ const render = useBlock ? toMermaidBlock : toMermaid;
55
+ const output = render(workflow, { direction, title }) + "\n";
56
+ if (options.output) {
57
+ try {
58
+ // Create parent dirs so `-o some/nested/diagram.mmd` works without a
59
+ // separate mkdir — matches `pandoc -o`, `tsc --outFile`, etc.
60
+ const dir = path.dirname(options.output);
61
+ if (dir && dir !== "." && !fs.existsSync(dir)) {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ }
64
+ fs.writeFileSync(options.output, output, "utf8");
65
+ stderr.write(chalk.green(` ✓ Wrote diagram to ${options.output}`) + "\n");
66
+ }
67
+ catch (err) {
68
+ stderr.write(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`) + "\n");
69
+ exit(1);
70
+ }
71
+ return;
72
+ }
73
+ stdout.write(output);
74
+ }
package/dist/cli/main.js CHANGED
@@ -17,7 +17,8 @@ import { loadAdditionalContext } from "../templates.js";
17
17
  import { validateWorkflow as validateWorkflowSchema } from "../schema.js";
18
18
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
19
19
  import { buildWorkflow, refineWorkflow } from "../workflow-builder.js";
20
- import { toMermaid, toMermaidBlock } from "../mermaid.js";
20
+ import { toMermaidBlock } from "../mermaid.js";
21
+ import { runWorkflowDiagram } from "./diagram.js";
21
22
  import { DagRenderer } from "./renderer.js";
22
23
  import * as readline from "node:readline";
23
24
  import { loadDotenv, loadConfigFile } from "./config-file.js";
@@ -30,6 +31,9 @@ import { checkProviderConnectivity } from "./check.js";
30
31
  import { registerSetupCommand } from "./setup.js";
31
32
  import { registerPublishCommand } from "./publish.js";
32
33
  import { reportToCloud } from "./cloud-report.js";
34
+ import { runUpgrade, fetchLatestFromNpm } from "./upgrade.js";
35
+ import { maybeNudge, defaultCachePath } from "./version-check.js";
36
+ import { spawnSync } from "node:child_process";
33
37
  // ── Stream observer (NDJSON) ────────────────────────────────────────
34
38
  /**
35
39
  * Create an observer that writes NDJSON ExecutionEvents to stdout.
@@ -801,57 +805,8 @@ workflowCmd
801
805
  .option("--block", "Wrap in ```mermaid fenced code block (default)", true)
802
806
  .option("--no-block", "Output raw Mermaid without code fence")
803
807
  .option("-o, --output <path>", "Write the diagram to a file instead of stdout")
804
- .action((file, options) => {
805
- let workflow;
806
- // Support builtin workflow names
807
- if (file === "triage") {
808
- workflow = triageWorkflow;
809
- }
810
- else if (file === "implement") {
811
- workflow = implementWorkflow;
812
- }
813
- else if (file === "seed-content") {
814
- workflow = seedContentWorkflow;
815
- }
816
- else {
817
- try {
818
- workflow = loadWorkflowFile(file);
819
- }
820
- catch (err) {
821
- console.error(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
822
- process.exit(1);
823
- return;
824
- }
825
- }
826
- const direction = (options.direction === "LR" ? "LR" : "TB");
827
- const title = options.title ?? workflow.name;
828
- // Block default: when writing to a file, infer from extension.
829
- // `.mmd` is raw Mermaid (what mmdc / Mermaid Live expect); `.md` is
830
- // fenced markdown. Stdout and unknown extensions keep the prior default
831
- // (fenced). An explicit --block / --no-block always wins.
832
- const blockExplicit = process.argv.includes("--block") || process.argv.includes("--no-block");
833
- let useBlock = options.block !== false;
834
- if (!blockExplicit && options.output) {
835
- const ext = path.extname(options.output).toLowerCase();
836
- if (ext === ".mmd")
837
- useBlock = false;
838
- else if (ext === ".md")
839
- useBlock = true;
840
- }
841
- const render = useBlock ? toMermaidBlock : toMermaid;
842
- const output = render(workflow, { direction, title }) + "\n";
843
- if (options.output) {
844
- try {
845
- fs.writeFileSync(options.output, output, "utf8");
846
- console.error(chalk.green(` ✓ Wrote diagram to ${options.output}`));
847
- }
848
- catch (err) {
849
- console.error(chalk.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
850
- process.exit(1);
851
- }
852
- return;
853
- }
854
- process.stdout.write(output);
808
+ .action(function diagramAction(file, options) {
809
+ runWorkflowDiagram(file, options, { loadWorkflowFile, command: this });
855
810
  });
856
811
  workflowCmd
857
812
  .command("export <name>")
@@ -1009,4 +964,65 @@ workflowCmd
1009
964
  }
1010
965
  console.log();
1011
966
  });
1012
- program.parse();
967
+ // ── sweny upgrade / update ────────────────────────────────────────────
968
+ // Self-update the globally-installed @sweny-ai/core. Mirrors the UX of
969
+ // `bun upgrade`, `deno upgrade`, `rustup update`, etc.
970
+ const upgradeCmd = program
971
+ .command("upgrade")
972
+ .alias("update")
973
+ .description("Upgrade sweny to the latest published version")
974
+ .option("--check", "Report what would be installed without running the installer")
975
+ .option("--force", "Reinstall even if the current version is already latest")
976
+ .option("--tag <tag>", "npm dist-tag to install (default: latest)", "latest")
977
+ .action(async (options) => {
978
+ // process.argv[1] is the CLI entrypoint — resolve through any symlinks
979
+ // (nvm, homebrew, etc. symlink the bin into a PATH dir) so PM detection
980
+ // looks at the real install location.
981
+ const argv1 = process.argv[1] ?? "";
982
+ let installPath = argv1;
983
+ try {
984
+ installPath = fs.realpathSync(argv1);
985
+ }
986
+ catch {
987
+ // Fall through with the unresolved path; detection degrades gracefully.
988
+ }
989
+ await runUpgrade(options, {
990
+ currentVersion: version,
991
+ installPath,
992
+ fetchLatestVersion: fetchLatestFromNpm,
993
+ runInstall: (cmd, args) => {
994
+ const res = spawnSync(cmd, args, { stdio: "inherit" });
995
+ if (res.error) {
996
+ process.stderr.write(chalk.red(` Error: couldn't run ${cmd} — ${res.error.message}`) + "\n");
997
+ return 127;
998
+ }
999
+ return typeof res.status === "number" ? res.status : 1;
1000
+ },
1001
+ canWrite: (dir) => {
1002
+ try {
1003
+ fs.accessSync(dir, fs.constants.W_OK);
1004
+ return true;
1005
+ }
1006
+ catch {
1007
+ return false;
1008
+ }
1009
+ },
1010
+ });
1011
+ });
1012
+ // Touch upgradeCmd so TS/ESLint don't flag it unused if someone tightens rules.
1013
+ void upgradeCmd;
1014
+ // ── Passive "new version available" nudge ─────────────────────────────
1015
+ // Runs after every command. Non-blocking, bounded to 1.5s, silent on any
1016
+ // failure, skipped in CI / non-TTY / opt-out environments.
1017
+ program.hook("postAction", async (_thisCommand, actionCommand) => {
1018
+ const topLevel = actionCommand.parent?.name() === "sweny" ? actionCommand.name() : actionCommand.parent?.name();
1019
+ await maybeNudge({
1020
+ currentVersion: version,
1021
+ cachePath: defaultCachePath(),
1022
+ now: Date.now(),
1023
+ env: process.env,
1024
+ isTty: Boolean(process.stderr.isTTY),
1025
+ commandName: topLevel,
1026
+ });
1027
+ });
1028
+ await program.parseAsync();
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `sweny upgrade` — self-update the globally-installed `@sweny-ai/core` CLI.
3
+ *
4
+ * The hard parts are detection (which package manager owns this install?) and
5
+ * permissions (global node_modules often require sudo). We keep the runtime
6
+ * surface tiny via dependency injection so the whole flow is testable.
7
+ */
8
+ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun" | "volta" | "brew" | "unknown";
9
+ export interface UpgradeOptions {
10
+ /** Skip the "already latest" short-circuit. */
11
+ force?: boolean;
12
+ /** Don't install — just report what would happen. */
13
+ check?: boolean;
14
+ /** Override the tag pulled from the registry. Defaults to "latest". */
15
+ tag?: string;
16
+ }
17
+ export interface UpgradeDeps {
18
+ /** Version currently running (from `package.json`). */
19
+ currentVersion: string;
20
+ /** Absolute path to the current CLI entrypoint — used for PM detection. */
21
+ installPath: string;
22
+ /** Fetch latest version for `@sweny-ai/core@<tag>`. */
23
+ fetchLatestVersion: (tag: string) => Promise<string>;
24
+ /** Run the resolved install command. Returns the process exit code. */
25
+ runInstall: (cmd: string, args: string[]) => number;
26
+ /** Writable-dir probe for EACCES detection. */
27
+ canWrite: (dir: string) => boolean;
28
+ stdout?: NodeJS.WritableStream;
29
+ stderr?: NodeJS.WritableStream;
30
+ exit?: (code: number) => void;
31
+ }
32
+ /**
33
+ * Detect the package manager that owns this install by walking the parent
34
+ * chain of the CLI entrypoint. The rules are ordered by specificity — more
35
+ * distinctive roots (bun, volta, pnpm, homebrew) beat the generic
36
+ * `lib/node_modules` npm layout.
37
+ */
38
+ export declare function detectPackageManager(installPath: string): PackageManager;
39
+ /**
40
+ * Resolve the install-root directory for writability checks. For npm-style
41
+ * layouts, walk up until we find `node_modules/@sweny-ai/core` and return
42
+ * the parent of `node_modules` (the writable prefix). Returns null if the
43
+ * install path doesn't look like a package-manager install.
44
+ */
45
+ export declare function resolveInstallRoot(installPath: string): string | null;
46
+ /** Build the (cmd, args) pair for a given package manager + tag. */
47
+ export declare function installCommandFor(pm: PackageManager, tag: string): {
48
+ cmd: string;
49
+ args: string[];
50
+ } | null;
51
+ /**
52
+ * Compare two semver-ish version strings. Returns:
53
+ * - `>0` when `a > b`
54
+ * - `<0` when `a < b`
55
+ * - `0` when equal
56
+ *
57
+ * Handles numeric core (`major.minor.patch`) and pre-release strings
58
+ * conservatively: any pre-release (`-alpha.1`) is treated as *lower* than
59
+ * the same core without one, matching npm's semver ordering at the level
60
+ * of precision we need for "is the registry newer than me."
61
+ */
62
+ export declare function compareVersions(a: string, b: string): number;
63
+ /**
64
+ * Fetch the latest published version of `@sweny-ai/core` from the npm
65
+ * registry. Uses the dist-tag endpoint which returns a small, cacheable JSON
66
+ * document — no tarball download.
67
+ */
68
+ export declare function fetchLatestFromNpm(tag: string): Promise<string>;
69
+ /**
70
+ * Main `sweny upgrade` entrypoint. Resolves the latest version, decides
71
+ * whether an install is needed, dispatches to the right package manager,
72
+ * and surfaces permission problems as actionable copy-paste commands.
73
+ */
74
+ export declare function runUpgrade(options: UpgradeOptions, deps: UpgradeDeps): Promise<void>;