@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.
- package/README.md +205 -0
- package/extensions/catalog.ts +7 -0
- package/package.json +42 -0
- package/src/catalog/crud.ts +143 -0
- package/src/catalog/install.ts +181 -0
- package/src/catalog/ratings.ts +32 -0
- package/src/catalog/reconcile.ts +339 -0
- package/src/catalog/source.ts +173 -0
- package/src/commands/add.ts +135 -0
- package/src/commands/definitions.ts +78 -0
- package/src/commands/diff.ts +158 -0
- package/src/commands/dispatch.ts +102 -0
- package/src/commands/init.ts +127 -0
- package/src/commands/login.ts +105 -0
- package/src/commands/profiles.ts +147 -0
- package/src/commands/remove.ts +90 -0
- package/src/commands/status.ts +142 -0
- package/src/commands/sync.ts +406 -0
- package/src/commands/toggle.ts +147 -0
- package/src/commands/types.ts +38 -0
- package/src/commands/verify.ts +107 -0
- package/src/config/io.ts +82 -0
- package/src/config/paths.ts +44 -0
- package/src/config/schema.ts +87 -0
- package/src/index.ts +94 -0
- package/src/profiles/manager.ts +159 -0
- package/src/register.ts +285 -0
- package/src/sync/auth.ts +109 -0
- package/src/sync/cache.ts +40 -0
- package/src/sync/gist.ts +253 -0
- package/src/sync/pull.ts +76 -0
- package/src/sync/push.ts +78 -0
- package/src/update/pi-update.ts +60 -0
- package/src/update/registry.ts +27 -0
- package/src/update/self-update.ts +60 -0
- package/src/update/semver.ts +38 -0
- package/src/update/types.ts +21 -0
- package/src/update/update-cache.ts +54 -0
- package/src/util/errors.ts +144 -0
- package/src/util/exec.ts +160 -0
|
@@ -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
|
+
}
|