@pi-stef/catalog 0.3.5 → 0.5.0
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.
- package/README.md +34 -39
- package/package.json +1 -1
- package/src/catalog/crud.ts +9 -18
- package/src/catalog/install.ts +7 -0
- package/src/catalog/migrate.ts +59 -0
- package/src/catalog/packages.ts +56 -0
- package/src/catalog/ratings.ts +0 -16
- package/src/catalog/setup.ts +169 -0
- package/src/commands/add.ts +113 -28
- package/src/commands/definitions.ts +3 -1
- package/src/commands/init.ts +3 -2
- package/src/commands/remove.ts +76 -0
- package/src/commands/reset.ts +136 -0
- package/src/commands/status.ts +71 -36
- package/src/commands/sync.ts +39 -2
- package/src/commands/toggle.ts +11 -16
- package/src/commands/types.ts +2 -0
- package/src/commands/update.ts +113 -0
- package/src/config/io.ts +3 -0
- package/src/config/schema.ts +0 -8
- package/src/register.ts +80 -12
- package/src/sync/pull.ts +2 -0
- package/src/util/exec.ts +12 -0
package/src/commands/add.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `ct add` subcommand implementation.
|
|
3
3
|
*
|
|
4
4
|
* Adds a new package to the catalog. Supports:
|
|
5
|
-
* - Full args: `ct add <
|
|
5
|
+
* - Full args: `ct add <source> [--type <t>]`
|
|
6
6
|
* - Git source without `--type`: prompts for type via `ctx.ui.select()`
|
|
7
7
|
* - After adding, runs `pi install` to install the package
|
|
8
8
|
*
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* and `writeCatalog` / `readCatalog` for persistence.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { RatingValue } from "../catalog/ratings.js";
|
|
14
13
|
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
15
14
|
import { addPackage } from "../catalog/crud.js";
|
|
15
|
+
import { sourceToKey } from "../catalog/source.js";
|
|
16
|
+
import { checkSetupForSource, formatSetupStatus } from "../catalog/setup.js";
|
|
17
|
+
import { PI_STEF_PACKAGES } from "../catalog/packages.js";
|
|
16
18
|
import { readCatalog, writeCatalog } from "../config/io.js";
|
|
17
19
|
import { piInstall } from "../util/exec.js";
|
|
18
20
|
|
|
@@ -34,25 +36,6 @@ export interface AddCtx extends CommandCtx {
|
|
|
34
36
|
// Helpers
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
36
38
|
|
|
37
|
-
const VALID_RATINGS: RatingValue[] = ["core", "useful", "debatable"];
|
|
38
|
-
|
|
39
|
-
function isValidRating(value: string): value is RatingValue {
|
|
40
|
-
return VALID_RATINGS.includes(value as RatingValue);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function resolveRating(flags: Record<string, true | string>): RatingValue {
|
|
44
|
-
const raw =
|
|
45
|
-
"r" in flags
|
|
46
|
-
? flags["r"]
|
|
47
|
-
: "rating" in flags
|
|
48
|
-
? flags["rating"]
|
|
49
|
-
: undefined;
|
|
50
|
-
|
|
51
|
-
if (raw === true || raw === undefined) return "core";
|
|
52
|
-
if (typeof raw === "string" && isValidRating(raw)) return raw;
|
|
53
|
-
return "core";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
39
|
function resolveType(
|
|
57
40
|
flags: Record<string, true | string>,
|
|
58
41
|
): "skill" | "pi-native" | undefined {
|
|
@@ -75,24 +58,115 @@ function resolveType(
|
|
|
75
58
|
/**
|
|
76
59
|
* Execute the `ct add` subcommand.
|
|
77
60
|
*
|
|
61
|
+
* New syntax (preferred): `ct add <source> [--type ...]`
|
|
62
|
+
* — name is auto-derived from source via `sourceToKey()`.
|
|
63
|
+
*
|
|
64
|
+
* Legacy syntax (deprecated): `ct add <name> <source> [--type ...]`
|
|
65
|
+
* — still accepted but emits a deprecation warning.
|
|
66
|
+
*
|
|
78
67
|
* Reads the catalog, validates inputs, prompts for type if needed,
|
|
79
68
|
* adds the package, writes the catalog, and runs `pi install`.
|
|
80
69
|
*/
|
|
81
70
|
export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void> {
|
|
82
71
|
const { positional, flags } = args;
|
|
83
|
-
const name = positional[0];
|
|
84
|
-
const source = positional[1];
|
|
85
72
|
|
|
86
|
-
// ---
|
|
87
|
-
if (
|
|
73
|
+
// --- Handle --scope batch mode ---------------------------------------------
|
|
74
|
+
if ("scope" in flags) {
|
|
75
|
+
const scope = flags["scope"];
|
|
76
|
+
if (scope !== "@pi-stef") {
|
|
77
|
+
ctx.ui.notify(`Unsupported scope: "${scope}". Use --scope @pi-stef.`, "error");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const catalog = readCatalog(ctx.home);
|
|
82
|
+
let added = 0;
|
|
83
|
+
let skipped = 0;
|
|
84
|
+
let currentCatalog = catalog;
|
|
85
|
+
|
|
86
|
+
for (const pkg of PI_STEF_PACKAGES) {
|
|
87
|
+
const npmSource = `npm:${pkg}`;
|
|
88
|
+
|
|
89
|
+
// Skip if already in catalog
|
|
90
|
+
if (currentCatalog.packages[pkg]) {
|
|
91
|
+
skipped++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
currentCatalog = addPackage(currentCatalog, pkg, npmSource);
|
|
97
|
+
added++;
|
|
98
|
+
} catch (err: unknown) {
|
|
99
|
+
// Unexpected validation error — warn but continue
|
|
100
|
+
ctx.ui.notify(
|
|
101
|
+
`Warning: failed to add "${pkg}": ${err instanceof Error ? err.message : String(err)}`,
|
|
102
|
+
"warning",
|
|
103
|
+
);
|
|
104
|
+
skipped++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (added > 0) {
|
|
109
|
+
writeCatalog(currentCatalog, ctx.home);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Install all added packages
|
|
113
|
+
const setupWarnings: string[] = [];
|
|
114
|
+
if (added > 0) {
|
|
115
|
+
for (const pkg of PI_STEF_PACKAGES) {
|
|
116
|
+
if (currentCatalog.packages[pkg]?.source === `npm:${pkg}`) {
|
|
117
|
+
ctx.ui.setWorkingMessage?.(`Installing ${pkg}...`);
|
|
118
|
+
try {
|
|
119
|
+
await piInstall(`npm:${pkg}`);
|
|
120
|
+
|
|
121
|
+
// Check setup after successful install
|
|
122
|
+
const setup = checkSetupForSource(`npm:${pkg}`, ctx.home);
|
|
123
|
+
if (setup && !setup.ok) {
|
|
124
|
+
setupWarnings.push(`${pkg}: ${formatSetupStatus(setup)}`);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
ctx.ui.notify(`Warning: install of "${pkg}" failed`, "warning");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
ctx.ui.setWorkingMessage?.();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const parts: string[] = [
|
|
135
|
+
`Scope @pi-stef: added ${added}, skipped ${skipped} (already in catalog)`,
|
|
136
|
+
];
|
|
137
|
+
if (setupWarnings.length > 0) {
|
|
138
|
+
parts.push(`Setup incomplete:\n ${setupWarnings.join("\n ")}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ctx.ui.notify(
|
|
142
|
+
parts.join("\n"),
|
|
143
|
+
setupWarnings.length > 0 ? "warning" : "info",
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Handle legacy 2-arg syntax: ct add <name> <source> -------------------
|
|
149
|
+
let name: string;
|
|
150
|
+
let source: string;
|
|
151
|
+
|
|
152
|
+
if (positional.length >= 2) {
|
|
153
|
+
name = positional[0];
|
|
154
|
+
source = positional[1];
|
|
88
155
|
ctx.ui.notify(
|
|
89
|
-
"
|
|
156
|
+
`"ct add <name> <source>" is legacy. Use "ct add <source>" — name is auto-derived.`,
|
|
157
|
+
"warning",
|
|
158
|
+
);
|
|
159
|
+
} else if (positional.length === 1) {
|
|
160
|
+
source = positional[0];
|
|
161
|
+
name = sourceToKey(source);
|
|
162
|
+
} else {
|
|
163
|
+
ctx.ui.notify(
|
|
164
|
+
"Usage: ct add <source> [--type <skill|pi-native>]",
|
|
90
165
|
"error",
|
|
91
166
|
);
|
|
92
167
|
return;
|
|
93
168
|
}
|
|
94
169
|
|
|
95
|
-
const rating = resolveRating(flags);
|
|
96
170
|
let type = resolveType(flags);
|
|
97
171
|
|
|
98
172
|
// --- Read catalog ---------------------------------------------------------
|
|
@@ -113,7 +187,7 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
113
187
|
|
|
114
188
|
// --- Add package ----------------------------------------------------------
|
|
115
189
|
try {
|
|
116
|
-
const updated = addPackage(catalog, name, source,
|
|
190
|
+
const updated = addPackage(catalog, name, source, type);
|
|
117
191
|
writeCatalog(updated, ctx.home);
|
|
118
192
|
} catch (err: unknown) {
|
|
119
193
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -124,6 +198,7 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
124
198
|
ctx.ui.notify(`Added "${name}" to catalog`, "info");
|
|
125
199
|
|
|
126
200
|
// --- Run pi install -------------------------------------------------------
|
|
201
|
+
ctx.ui.setWorkingMessage?.(`Installing ${name}...`);
|
|
127
202
|
try {
|
|
128
203
|
await piInstall(source);
|
|
129
204
|
} catch {
|
|
@@ -132,4 +207,14 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
132
207
|
"warning",
|
|
133
208
|
);
|
|
134
209
|
}
|
|
210
|
+
ctx.ui.setWorkingMessage?.();
|
|
211
|
+
|
|
212
|
+
// --- Check setup requirements ---------------------------------------------
|
|
213
|
+
const setup = checkSetupForSource(source, ctx.home);
|
|
214
|
+
if (setup && !setup.ok) {
|
|
215
|
+
ctx.ui.notify(
|
|
216
|
+
`Setup incomplete for "${name}": ${formatSetupStatus(setup)}`,
|
|
217
|
+
"warning",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
135
220
|
}
|
|
@@ -30,7 +30,8 @@ export const SUBCOMMAND_DEFS: readonly SubcommandDef[] = [
|
|
|
30
30
|
{ name: "init", description: "Initialize a new catalog" },
|
|
31
31
|
{ name: "add", aliases: ["a"], description: "Add a package to the catalog" },
|
|
32
32
|
{ name: "remove", aliases: ["rm"], description: "Remove a package from the catalog" },
|
|
33
|
-
{ name: "toggle", description: "Toggle a package's
|
|
33
|
+
{ name: "toggle", description: "Toggle a package's enabled state" },
|
|
34
|
+
{ name: "update", aliases: ["up"], description: "Update packages to latest versions" },
|
|
34
35
|
{ name: "disable", description: "Disable a package" },
|
|
35
36
|
{ name: "enable", description: "Enable a package" },
|
|
36
37
|
{ name: "push", description: "Push catalog to remote gist" },
|
|
@@ -41,6 +42,7 @@ export const SUBCOMMAND_DEFS: readonly SubcommandDef[] = [
|
|
|
41
42
|
{ name: "verify", description: "Verify catalog integrity" },
|
|
42
43
|
{ name: "profiles", description: "List available profiles" },
|
|
43
44
|
{ name: "profile", description: "Show or switch active profile" },
|
|
45
|
+
{ name: "reset", description: "Reset catalog: uninstall all @pi-stef packages and delete config" },
|
|
44
46
|
] as const;
|
|
45
47
|
|
|
46
48
|
// ---------------------------------------------------------------------------
|
package/src/commands/init.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
9
|
|
|
10
10
|
import { scanInstalled } from "../catalog/install.js";
|
|
11
|
+
import { migrateRatingToEnabledRaw } from "../catalog/migrate.js";
|
|
11
12
|
import { CatalogYamlSchema } from "../config/schema.js";
|
|
12
13
|
import type { CatalogYaml } from "../config/schema.js";
|
|
13
14
|
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
@@ -29,7 +30,7 @@ export type InitContext = CommandCtx;
|
|
|
29
30
|
* Initialize a new catalog.
|
|
30
31
|
*
|
|
31
32
|
* - Without flags: scans installed packages and generates a catalog with
|
|
32
|
-
*
|
|
33
|
+
* every discovered package enabled.
|
|
33
34
|
* - With `--from-gist=<id>`: fetches the gist, reads its `cat.yaml` file,
|
|
34
35
|
* validates it, and writes it as the local catalog.
|
|
35
36
|
*/
|
|
@@ -70,7 +71,6 @@ function initFromScan(ctx: InitContext): void {
|
|
|
70
71
|
for (const [name, pkg] of Object.entries(installed)) {
|
|
71
72
|
packages[name] = {
|
|
72
73
|
source: pkg.source,
|
|
73
|
-
rating: "core",
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -115,6 +115,7 @@ async function initFromGist(gistId: string, ctx: InitContext): Promise<void> {
|
|
|
115
115
|
|
|
116
116
|
// Validate and write
|
|
117
117
|
const parsed = yaml.load(gistContent);
|
|
118
|
+
migrateRatingToEnabledRaw(parsed);
|
|
118
119
|
const catalog = CatalogYamlSchema.parse(parsed);
|
|
119
120
|
|
|
120
121
|
writeCatalog(catalog, ctx.home);
|
package/src/commands/remove.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
14
14
|
import { removePackage } from "../catalog/crud.js";
|
|
15
|
+
import { isPiStefSource } from "../catalog/packages.js";
|
|
15
16
|
import { readCatalog, writeCatalog, readLock, writeLock } from "../config/io.js";
|
|
16
17
|
import { piUninstall } from "../util/exec.js";
|
|
17
18
|
|
|
@@ -41,6 +42,79 @@ export async function removeCommand(
|
|
|
41
42
|
ctx: RemoveCtx,
|
|
42
43
|
): Promise<void> {
|
|
43
44
|
const { positional, flags } = args;
|
|
45
|
+
|
|
46
|
+
// --- Handle --scope batch mode ---------------------------------------------
|
|
47
|
+
if ("scope" in flags) {
|
|
48
|
+
const scope = flags["scope"];
|
|
49
|
+
if (scope !== "@pi-stef") {
|
|
50
|
+
ctx.ui.notify(`Unsupported scope: "${scope}". Use --scope @pi-stef.`, "error");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const catalog = readCatalog(ctx.home);
|
|
55
|
+
const lock = readLock(ctx.home);
|
|
56
|
+
|
|
57
|
+
// isPiStefSource returns false for @pi-stef/catalog by design (see packages.ts)
|
|
58
|
+
const piStefNames = Object.keys(catalog.packages).filter(
|
|
59
|
+
(name) => isPiStefSource(catalog.packages[name].source),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (piStefNames.length === 0) {
|
|
63
|
+
ctx.ui.notify("No @pi-stef packages found in catalog", "info");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Confirmation
|
|
68
|
+
const skipConfirm = "yes" in flags || "y" in flags;
|
|
69
|
+
if (!skipConfirm && ctx.ui.confirm) {
|
|
70
|
+
const confirmed = await ctx.ui.confirm(
|
|
71
|
+
`Remove ${piStefNames.length} @pi-stef packages from catalog?`,
|
|
72
|
+
);
|
|
73
|
+
if (!confirmed) {
|
|
74
|
+
ctx.ui.notify("Removal cancelled", "info");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Capture sources before removing
|
|
80
|
+
const sources: Record<string, string> = {};
|
|
81
|
+
for (const name of piStefNames) {
|
|
82
|
+
sources[name] = catalog.packages[name].source;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Remove all from catalog and lock file
|
|
86
|
+
for (const name of piStefNames) {
|
|
87
|
+
delete catalog.packages[name];
|
|
88
|
+
if (lock.packages[name]) {
|
|
89
|
+
delete lock.packages[name];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
writeCatalog(catalog, ctx.home);
|
|
94
|
+
writeLock(lock, ctx.home);
|
|
95
|
+
|
|
96
|
+
// Uninstall all
|
|
97
|
+
let uninstalled = 0;
|
|
98
|
+
let failed = 0;
|
|
99
|
+
for (const name of piStefNames) {
|
|
100
|
+
ctx.ui.setWorkingMessage?.(`Uninstalling ${name} (${uninstalled + 1}/${piStefNames.length})...`);
|
|
101
|
+
try {
|
|
102
|
+
await piUninstall(sources[name]);
|
|
103
|
+
uninstalled++;
|
|
104
|
+
} catch {
|
|
105
|
+
ctx.ui.notify(`Warning: uninstall of "${name}" failed`, "warning");
|
|
106
|
+
failed++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
ctx.ui.setWorkingMessage?.();
|
|
110
|
+
|
|
111
|
+
ctx.ui.notify(
|
|
112
|
+
`Scope @pi-stef: removed ${piStefNames.length}, uninstalled ${uninstalled}${failed > 0 ? ` (${failed} uninstall failed)` : ""}`,
|
|
113
|
+
failed > 0 ? "warning" : "info",
|
|
114
|
+
);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
44
118
|
const name = positional[0];
|
|
45
119
|
|
|
46
120
|
// --- Validate required args -----------------------------------------------
|
|
@@ -91,6 +165,7 @@ export async function removeCommand(
|
|
|
91
165
|
ctx.ui.notify(`Removed "${name}" from catalog`, "info");
|
|
92
166
|
|
|
93
167
|
// --- Run pi uninstall -----------------------------------------------------
|
|
168
|
+
ctx.ui.setWorkingMessage?.(`Uninstalling ${name}...`);
|
|
94
169
|
try {
|
|
95
170
|
await piUninstall(source);
|
|
96
171
|
} catch {
|
|
@@ -99,4 +174,5 @@ export async function removeCommand(
|
|
|
99
174
|
"warning",
|
|
100
175
|
);
|
|
101
176
|
}
|
|
177
|
+
ctx.ui.setWorkingMessage?.();
|
|
102
178
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ct reset` subcommand implementation.
|
|
3
|
+
*
|
|
4
|
+
* Full nuke: uninstalls all @pi-stef packages, deletes gist refs,
|
|
5
|
+
* deletes config files (cat.yaml, catalog.lock.json, .gist),
|
|
6
|
+
* and removes the empty catalog directory.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* - `ct reset` — prompts for confirmation
|
|
10
|
+
* - `ct reset --yes` — skips confirmation
|
|
11
|
+
*
|
|
12
|
+
* This is a destructive operation. The catalog can be re-initialized
|
|
13
|
+
* with `ct init` after resetting.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
18
|
+
import { isPiStefSource } from "../catalog/packages.js";
|
|
19
|
+
import { catalogDir, catalogFile, lockFile } from "../config/paths.js";
|
|
20
|
+
import { piUninstall } from "../util/exec.js";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Context for `resetCommand`, extending the base with `confirm`. */
|
|
27
|
+
export interface ResetCtx extends CommandCtx {
|
|
28
|
+
ui: CommandCtx["ui"] & {
|
|
29
|
+
confirm?: (message: string) => Promise<boolean>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// resetCommand
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Execute the `ct reset` subcommand.
|
|
39
|
+
*
|
|
40
|
+
* 1. Check cat.yaml exists
|
|
41
|
+
* 2. Confirm (skippable with --yes)
|
|
42
|
+
* 3. Find @pi-stef packages (isPiStefSource excludes catalog)
|
|
43
|
+
* 4. pi uninstall each
|
|
44
|
+
* 5. Delete config files with fs.rmSync({ recursive: true, force: true })
|
|
45
|
+
* 6. Remove empty catalog directory
|
|
46
|
+
*/
|
|
47
|
+
export async function resetCommand(
|
|
48
|
+
args: CommandArgs,
|
|
49
|
+
ctx: ResetCtx,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const { flags } = args;
|
|
52
|
+
|
|
53
|
+
// --- Check cat.yaml exists ------------------------------------------------
|
|
54
|
+
const catPath = catalogFile(ctx.home);
|
|
55
|
+
if (!fs.existsSync(catPath)) {
|
|
56
|
+
ctx.ui.notify("No catalog found. Run `ct init` first.", "error");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- Confirmation ----------------------------------------------------------
|
|
61
|
+
const skipConfirm = "yes" in flags || "y" in flags;
|
|
62
|
+
if (!skipConfirm && ctx.ui.confirm) {
|
|
63
|
+
const confirmed = await ctx.ui.confirm(
|
|
64
|
+
"This will uninstall all @pi-stef packages and delete all catalog config. Continue?",
|
|
65
|
+
);
|
|
66
|
+
if (!confirmed) {
|
|
67
|
+
ctx.ui.notify("Reset cancelled", "info");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- Find @pi-stef packages -----------------------------------------------
|
|
73
|
+
// Read catalog directly (raw YAML) to avoid schema validation on corrupt files
|
|
74
|
+
let packages: Record<string, { source: string }> = {};
|
|
75
|
+
try {
|
|
76
|
+
const yaml = await import("js-yaml");
|
|
77
|
+
const content = fs.readFileSync(catPath, "utf8");
|
|
78
|
+
const parsed = yaml.load(content) as { packages?: Record<string, { source: string }> };
|
|
79
|
+
packages = parsed?.packages ?? {};
|
|
80
|
+
} catch {
|
|
81
|
+
// If YAML is corrupt, we still want to delete config files
|
|
82
|
+
ctx.ui.notify("Warning: could not parse cat.yaml — skipping uninstall step", "warning");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const piStefNames = Object.keys(packages).filter(
|
|
86
|
+
(name) => isPiStefSource(packages[name].source),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// --- Uninstall @pi-stef packages ------------------------------------------
|
|
90
|
+
let uninstalled = 0;
|
|
91
|
+
let failed = 0;
|
|
92
|
+
|
|
93
|
+
for (const name of piStefNames) {
|
|
94
|
+
ctx.ui.setWorkingMessage?.(`Uninstalling ${name} (${uninstalled + 1}/${piStefNames.length})...`);
|
|
95
|
+
try {
|
|
96
|
+
await piUninstall(packages[name].source);
|
|
97
|
+
uninstalled++;
|
|
98
|
+
} catch {
|
|
99
|
+
ctx.ui.notify(`Warning: uninstall of "${name}" failed`, "warning");
|
|
100
|
+
failed++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
ctx.ui.setWorkingMessage?.();
|
|
104
|
+
|
|
105
|
+
// --- Delete config files --------------------------------------------------
|
|
106
|
+
const dir = catalogDir(ctx.home);
|
|
107
|
+
const gistPath = `${dir}/.gist`;
|
|
108
|
+
|
|
109
|
+
// Delete individual files first, then the directory
|
|
110
|
+
for (const filePath of [catPath, lockFile(ctx.home), gistPath]) {
|
|
111
|
+
try {
|
|
112
|
+
fs.rmSync(filePath, { force: true });
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore errors — file may not exist
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Remove the catalog directory itself
|
|
119
|
+
try {
|
|
120
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
} catch {
|
|
122
|
+
// Ignore errors — directory may not exist or be non-empty
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Report ----------------------------------------------------------------
|
|
126
|
+
const parts: string[] = [];
|
|
127
|
+
if (piStefNames.length > 0) {
|
|
128
|
+
parts.push(`uninstalled ${uninstalled}/${piStefNames.length} packages`);
|
|
129
|
+
}
|
|
130
|
+
parts.push("deleted config files");
|
|
131
|
+
|
|
132
|
+
ctx.ui.notify(
|
|
133
|
+
`Reset complete: ${parts.join(", ")}${failed > 0 ? ` (${failed} uninstall failed)` : ""}`,
|
|
134
|
+
failed > 0 ? "warning" : "info",
|
|
135
|
+
);
|
|
136
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ct status` subcommand implementation.
|
|
3
3
|
*
|
|
4
|
-
* Shows catalog status: profile, package counts
|
|
5
|
-
* installed/missing/orphan counts, gist URL,
|
|
4
|
+
* Shows catalog status: profile, package counts (enabled/disabled),
|
|
5
|
+
* installed/missing/orphan counts, gist URL, last sync time,
|
|
6
|
+
* and individual package listing with setup status.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
9
10
|
import { readCatalog, readLock } from "../config/io.js";
|
|
10
11
|
import { scanInstalled } from "../catalog/install.js";
|
|
11
12
|
import { readCachedGistId } from "../sync/cache.js";
|
|
13
|
+
import { checkSetupForSource } from "../catalog/setup.js";
|
|
12
14
|
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
14
16
|
// Types
|
|
@@ -17,30 +19,6 @@ import { readCachedGistId } from "../sync/cache.js";
|
|
|
17
19
|
/** Context for `statusCommand`. Uses the base `CommandCtx`. */
|
|
18
20
|
export type StatusCtx = CommandCtx;
|
|
19
21
|
|
|
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
22
|
// ---------------------------------------------------------------------------
|
|
45
23
|
// statusCommand
|
|
46
24
|
// ---------------------------------------------------------------------------
|
|
@@ -49,7 +27,7 @@ function countByRating(
|
|
|
49
27
|
* Execute the `ct status` subcommand.
|
|
50
28
|
*
|
|
51
29
|
* Reads catalog, lock, gist cache, and installed packages to build
|
|
52
|
-
* a comprehensive status summary.
|
|
30
|
+
* a comprehensive status summary with individual package listing.
|
|
53
31
|
*/
|
|
54
32
|
export async function statusCommand(
|
|
55
33
|
args: CommandArgs,
|
|
@@ -67,24 +45,33 @@ export async function statusCommand(
|
|
|
67
45
|
const packages = catalog.packages;
|
|
68
46
|
const totalPackages = Object.keys(packages).length;
|
|
69
47
|
|
|
70
|
-
// ---
|
|
71
|
-
|
|
48
|
+
// --- Enabled / disabled counts ---
|
|
49
|
+
let enabledCount = 0;
|
|
50
|
+
let disabledCount = 0;
|
|
51
|
+
for (const pkg of Object.values(packages)) {
|
|
52
|
+
if (pkg.enabled === false) {
|
|
53
|
+
disabledCount++;
|
|
54
|
+
} else {
|
|
55
|
+
enabledCount++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
72
58
|
|
|
73
59
|
// --- Installed / missing / orphan ---
|
|
60
|
+
// Build a lookup map for installed packages by source
|
|
61
|
+
const installedBySource = new Map<string, { name: string; version?: string }>();
|
|
62
|
+
for (const inst of Object.values(installed)) {
|
|
63
|
+
installedBySource.set(inst.source, { name: inst.name, version: inst.version });
|
|
64
|
+
}
|
|
65
|
+
|
|
74
66
|
const catalogSources = new Set<string>();
|
|
75
67
|
for (const pkg of Object.values(packages)) {
|
|
76
68
|
catalogSources.add(pkg.source);
|
|
77
69
|
}
|
|
78
70
|
|
|
79
|
-
const installedSources = new Set<string>();
|
|
80
|
-
for (const inst of Object.values(installed)) {
|
|
81
|
-
installedSources.add(inst.source);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
71
|
// Count how many catalog packages are actually installed
|
|
85
72
|
let installedCount = 0;
|
|
86
73
|
for (const pkg of Object.values(packages)) {
|
|
87
|
-
if (
|
|
74
|
+
if (installedBySource.has(pkg.source)) {
|
|
88
75
|
installedCount++;
|
|
89
76
|
}
|
|
90
77
|
}
|
|
@@ -116,7 +103,7 @@ export async function statusCommand(
|
|
|
116
103
|
|
|
117
104
|
// Package counts
|
|
118
105
|
lines.push(
|
|
119
|
-
`Packages: ${totalPackages} total (
|
|
106
|
+
`Packages: ${totalPackages} total (${enabledCount} enabled, ${disabledCount} disabled)`,
|
|
120
107
|
);
|
|
121
108
|
|
|
122
109
|
// Installed/missing/orphan
|
|
@@ -138,5 +125,53 @@ export async function statusCommand(
|
|
|
138
125
|
lines.push("Last sync: never synced");
|
|
139
126
|
}
|
|
140
127
|
|
|
128
|
+
// --- Individual package listing ---
|
|
129
|
+
if (totalPackages > 0) {
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push("Packages:");
|
|
132
|
+
|
|
133
|
+
for (const [name, pkg] of Object.entries(packages)) {
|
|
134
|
+
const isDisabled = pkg.enabled === false;
|
|
135
|
+
const isInstalled = installedBySource.has(pkg.source);
|
|
136
|
+
const inst = installedBySource.get(pkg.source);
|
|
137
|
+
|
|
138
|
+
// Status indicator
|
|
139
|
+
let status: string;
|
|
140
|
+
if (isDisabled) {
|
|
141
|
+
status = "disabled";
|
|
142
|
+
} else if (isInstalled) {
|
|
143
|
+
status = "installed";
|
|
144
|
+
} else {
|
|
145
|
+
status = "missing";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Version info
|
|
149
|
+
const versionStr = inst?.version ? ` v${inst.version}` : "";
|
|
150
|
+
|
|
151
|
+
// Setup status
|
|
152
|
+
let setupStr = "";
|
|
153
|
+
if (isInstalled && !isDisabled) {
|
|
154
|
+
const setup = checkSetupForSource(pkg.source, ctx.home);
|
|
155
|
+
if (setup && !setup.ok) {
|
|
156
|
+
setupStr = " ⚠ setup incomplete";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lines.push(` ${name} [${status}]${versionStr}${setupStr}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Orphans ---
|
|
165
|
+
if (orphanCount > 0) {
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("Orphans:");
|
|
168
|
+
for (const inst of Object.values(installed)) {
|
|
169
|
+
if (!catalogSources.has(inst.source)) {
|
|
170
|
+
const versionStr = inst.version ? ` v${inst.version}` : "";
|
|
171
|
+
lines.push(` ${inst.name} [orphan]${versionStr}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
141
176
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
142
177
|
}
|