@pi-stef/catalog 0.2.2

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,147 @@
1
+ /**
2
+ * `ct profiles` and `ct profile` subcommand implementations.
3
+ *
4
+ * `ct profiles` lists all profiles with an active indicator.
5
+ * `ct profile <name>` switches the active profile.
6
+ * `ct profile <name> --create` creates a new empty profile.
7
+ * `ct profile <name> --delete` deletes a profile (with confirmation).
8
+ */
9
+
10
+ import type { CommandArgs, CommandCtx } from "./types.js";
11
+ import { readCatalog, writeCatalog } from "../config/io.js";
12
+ import {
13
+ createProfile,
14
+ switchProfile,
15
+ deleteProfile,
16
+ DEFAULT_PROFILE,
17
+ } from "../profiles/manager.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /** Context for profile commands. Extends base with `confirm` for deletion. */
24
+ export interface ProfilesCtx extends CommandCtx {
25
+ ui: CommandCtx["ui"] & {
26
+ confirm?: (message: string) => Promise<boolean>;
27
+ };
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // profilesCommand
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Execute the `ct profiles` subcommand.
36
+ *
37
+ * Lists all available profiles, marking the active one with `*`.
38
+ * The default profile is always listed.
39
+ */
40
+ export async function profilesCommand(
41
+ _args: CommandArgs,
42
+ ctx: ProfilesCtx,
43
+ ): Promise<void> {
44
+ const catalog = readCatalog(ctx.home);
45
+ const active = catalog.meta.activeProfile ?? DEFAULT_PROFILE;
46
+ const profileNames = Object.keys(catalog.profiles ?? {});
47
+
48
+ const lines: string[] = ["Profiles:"];
49
+
50
+ // Default profile is always available
51
+ const defaultMarker = active === DEFAULT_PROFILE ? " *" : "";
52
+ lines.push(` ${DEFAULT_PROFILE}${defaultMarker}`);
53
+
54
+ // Named profiles
55
+ for (const name of profileNames.sort()) {
56
+ const marker = active === name ? " *" : "";
57
+ lines.push(` ${name}${marker}`);
58
+ }
59
+
60
+ ctx.ui.notify(lines.join("\n"), "info");
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // profileCommand
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Execute the `ct profile` subcommand.
69
+ *
70
+ * Modes:
71
+ * - No positional args: show current profile
72
+ * - `ct profile <name>`: switch to the named profile
73
+ * - `ct profile <name> --create`: create a new profile
74
+ * - `ct profile <name> --delete`: delete a profile (prompts for confirmation)
75
+ */
76
+ export async function profileCommand(
77
+ args: CommandArgs,
78
+ ctx: ProfilesCtx,
79
+ ): Promise<void> {
80
+ const { positional, flags } = args;
81
+ const name = positional[0];
82
+
83
+ // --- Show current profile ---
84
+ if (!name) {
85
+ const catalog = readCatalog(ctx.home);
86
+ const active = catalog.meta.activeProfile ?? DEFAULT_PROFILE;
87
+ ctx.ui.notify(`Current profile: ${active}`, "info");
88
+ return;
89
+ }
90
+
91
+ const catalog = readCatalog(ctx.home);
92
+
93
+ // --- Create mode ---
94
+ if (flags["create"]) {
95
+ try {
96
+ const updated = createProfile(catalog, name);
97
+ writeCatalog(updated, ctx.home);
98
+ ctx.ui.notify(`Created profile "${name}"`, "info");
99
+ } catch (err: unknown) {
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ ctx.ui.notify(message, "error");
102
+ }
103
+ return;
104
+ }
105
+
106
+ // --- Delete mode ---
107
+ if (flags["delete"]) {
108
+ try {
109
+ // Pre-validate to catch errors before prompting
110
+ if (name === DEFAULT_PROFILE) {
111
+ throw new Error(`Cannot delete the "${DEFAULT_PROFILE}" profile`);
112
+ }
113
+ if (!catalog.profiles?.[name]) {
114
+ throw new Error(`Profile "${name}" not found`);
115
+ }
116
+
117
+ // Confirm deletion
118
+ if (ctx.ui.confirm) {
119
+ const confirmed = await ctx.ui.confirm(
120
+ `Delete profile "${name}"? This cannot be undone.`,
121
+ );
122
+ if (!confirmed) {
123
+ ctx.ui.notify("Cancelled", "info");
124
+ return;
125
+ }
126
+ }
127
+
128
+ const updated = deleteProfile(catalog, name);
129
+ writeCatalog(updated, ctx.home);
130
+ ctx.ui.notify(`Deleted profile "${name}"`, "info");
131
+ } catch (err: unknown) {
132
+ const message = err instanceof Error ? err.message : String(err);
133
+ ctx.ui.notify(message, "error");
134
+ }
135
+ return;
136
+ }
137
+
138
+ // --- Switch mode ---
139
+ try {
140
+ const updated = switchProfile(catalog, name);
141
+ writeCatalog(updated, ctx.home);
142
+ ctx.ui.notify(`Switched to profile "${name}"`, "info");
143
+ } catch (err: unknown) {
144
+ const message = err instanceof Error ? err.message : String(err);
145
+ ctx.ui.notify(message, "error");
146
+ }
147
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `ct remove` subcommand implementation.
3
+ *
4
+ * Removes a package from the catalog. Supports:
5
+ * - `ct remove <name>` — prompts for confirmation, then removes
6
+ * - `ct remove <name> --yes` — skips confirmation prompt
7
+ * - After removing from catalog, runs `pi uninstall <name>`
8
+ *
9
+ * Uses `removePackage` from `crud.ts` for catalog mutation,
10
+ * and `writeCatalog` / `readCatalog` for persistence.
11
+ */
12
+
13
+ import type { CommandArgs, CommandCtx } from "./types.js";
14
+ import { removePackage } from "../catalog/crud.js";
15
+ import { readCatalog, writeCatalog } from "../config/io.js";
16
+ import { piUninstall } from "../util/exec.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** Context for `removeCommand`, extending the base with `confirm`. */
23
+ export interface RemoveCtx extends CommandCtx {
24
+ ui: CommandCtx["ui"] & {
25
+ confirm?: (message: string) => Promise<boolean>;
26
+ };
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // removeCommand
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Execute the `ct remove` subcommand.
35
+ *
36
+ * Reads the catalog, confirms removal, removes the package,
37
+ * writes the catalog, and runs `pi uninstall`.
38
+ */
39
+ export async function removeCommand(
40
+ args: CommandArgs,
41
+ ctx: RemoveCtx,
42
+ ): Promise<void> {
43
+ const { positional, flags } = args;
44
+ const name = positional[0];
45
+
46
+ // --- Validate required args -----------------------------------------------
47
+ if (!name) {
48
+ ctx.ui.notify("Usage: ct remove <name> [--yes]", "error");
49
+ return;
50
+ }
51
+
52
+ // --- Read catalog ---------------------------------------------------------
53
+ const catalog = readCatalog(ctx.home);
54
+
55
+ // --- Validate package exists ----------------------------------------------
56
+ if (!catalog.packages[name]) {
57
+ ctx.ui.notify(`Package "${name}" not found`, "error");
58
+ return;
59
+ }
60
+
61
+ // --- Confirmation (skip with --yes / -y) ----------------------------------
62
+ const skipConfirm = "yes" in flags || "y" in flags;
63
+ if (!skipConfirm) {
64
+ if (ctx.ui.confirm) {
65
+ const confirmed = await ctx.ui.confirm(
66
+ `Remove package "${name}" from catalog?`,
67
+ );
68
+ if (!confirmed) {
69
+ ctx.ui.notify("Removal cancelled", "info");
70
+ return;
71
+ }
72
+ }
73
+ }
74
+
75
+ // --- Remove package -------------------------------------------------------
76
+ const updated = removePackage(catalog, name);
77
+ writeCatalog(updated, ctx.home);
78
+
79
+ ctx.ui.notify(`Removed "${name}" from catalog`, "info");
80
+
81
+ // --- Run pi uninstall -----------------------------------------------------
82
+ try {
83
+ await piUninstall(name);
84
+ } catch {
85
+ ctx.ui.notify(
86
+ `Warning: package "${name}" removed from catalog but uninstall failed`,
87
+ "warning",
88
+ );
89
+ }
90
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * `ct status` subcommand implementation.
3
+ *
4
+ * Shows catalog status: profile, package counts by rating,
5
+ * installed/missing/orphan counts, gist URL, and last sync time.
6
+ */
7
+
8
+ import type { CommandArgs, CommandCtx } from "./types.js";
9
+ import { readCatalog, readLock } from "../config/io.js";
10
+ import { scanInstalled } from "../catalog/install.js";
11
+ import { readCachedGistId } from "../sync/cache.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Context for `statusCommand`. Uses the base `CommandCtx`. */
18
+ export type StatusCtx = CommandCtx;
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ interface RatingCounts {
25
+ core: number;
26
+ useful: number;
27
+ debatable: number;
28
+ disabled: number;
29
+ }
30
+
31
+ function countByRating(
32
+ packages: Record<string, { rating: string; enabled?: boolean }>,
33
+ ): RatingCounts {
34
+ const counts: RatingCounts = { core: 0, useful: 0, debatable: 0, disabled: 0 };
35
+ for (const pkg of Object.values(packages)) {
36
+ const r = pkg.rating as keyof RatingCounts;
37
+ if (r in counts) {
38
+ counts[r]++;
39
+ }
40
+ }
41
+ return counts;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // statusCommand
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Execute the `ct status` subcommand.
50
+ *
51
+ * Reads catalog, lock, gist cache, and installed packages to build
52
+ * a comprehensive status summary.
53
+ */
54
+ export async function statusCommand(
55
+ args: CommandArgs,
56
+ ctx: StatusCtx,
57
+ ): Promise<void> {
58
+ const { flags } = args;
59
+ const profile =
60
+ typeof flags["profile"] === "string" ? flags["profile"] : "default";
61
+
62
+ const catalog = readCatalog(ctx.home);
63
+ const lock = readLock(ctx.home);
64
+ const installed = scanInstalled(ctx.home);
65
+ const gistId = readCachedGistId(ctx.home);
66
+
67
+ const packages = catalog.packages;
68
+ const totalPackages = Object.keys(packages).length;
69
+
70
+ // --- Package counts by rating ---
71
+ const ratingCounts = countByRating(packages);
72
+
73
+ // --- Installed / missing / orphan ---
74
+ const catalogSources = new Set<string>();
75
+ for (const pkg of Object.values(packages)) {
76
+ catalogSources.add(pkg.source);
77
+ }
78
+
79
+ const installedSources = new Set<string>();
80
+ for (const inst of Object.values(installed)) {
81
+ installedSources.add(inst.source);
82
+ }
83
+
84
+ // Count how many catalog packages are actually installed
85
+ let installedCount = 0;
86
+ for (const pkg of Object.values(packages)) {
87
+ if (installedSources.has(pkg.source)) {
88
+ installedCount++;
89
+ }
90
+ }
91
+
92
+ // Missing = catalog packages not installed
93
+ const missingCount = totalPackages - installedCount;
94
+
95
+ // Orphans = installed packages not in catalog
96
+ let orphanCount = 0;
97
+ for (const inst of Object.values(installed)) {
98
+ if (!catalogSources.has(inst.source)) {
99
+ orphanCount++;
100
+ }
101
+ }
102
+
103
+ // --- Last sync time ---
104
+ let lastSync: string | undefined;
105
+ for (const lockPkg of Object.values(lock.packages)) {
106
+ if (!lastSync || lockPkg.installedAt > lastSync) {
107
+ lastSync = lockPkg.installedAt;
108
+ }
109
+ }
110
+
111
+ // --- Build status message ---
112
+ const lines: string[] = [];
113
+
114
+ // Profile
115
+ lines.push(`Profile: ${profile}`);
116
+
117
+ // Package counts
118
+ lines.push(
119
+ `Packages: ${totalPackages} total (core: ${ratingCounts.core}, useful: ${ratingCounts.useful}, debatable: ${ratingCounts.debatable}, disabled: ${ratingCounts.disabled})`,
120
+ );
121
+
122
+ // Installed/missing/orphan
123
+ lines.push(
124
+ `Installed: ${installedCount}, Missing: ${missingCount}, Orphans: ${orphanCount}`,
125
+ );
126
+
127
+ // Gist URL
128
+ if (gistId) {
129
+ lines.push(`Gist: https://gist.github.com/${gistId}`);
130
+ } else {
131
+ lines.push("No remote gist configured");
132
+ }
133
+
134
+ // Last sync
135
+ if (lastSync) {
136
+ lines.push(`Last sync: ${lastSync}`);
137
+ } else {
138
+ lines.push("Last sync: never synced");
139
+ }
140
+
141
+ ctx.ui.notify(lines.join("\n"), "info");
142
+ }