magus-cli 7.12.2 → 7.14.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 +21 -0
- package/package.json +4 -4
- package/src/__tests__/market-bases.test.ts +112 -0
- package/src/__tests__/market-config.test.ts +84 -0
- package/src/__tests__/market-conflicts.test.ts +175 -0
- package/src/__tests__/market-consent.test.ts +185 -0
- package/src/__tests__/market-content.test.ts +183 -0
- package/src/__tests__/market-delivery.test.ts +308 -0
- package/src/__tests__/market-fixture.ts +1167 -0
- package/src/__tests__/market-git.test.ts +130 -0
- package/src/__tests__/market-identity.test.ts +127 -0
- package/src/__tests__/market-install.test.ts +143 -0
- package/src/__tests__/market-layout.test.ts +206 -0
- package/src/__tests__/market-publish.test.ts +263 -0
- package/src/__tests__/market-r2-adapters.test.ts +190 -0
- package/src/__tests__/market-r2-company-cli.test.ts +350 -0
- package/src/__tests__/market-r2-company-unit.test.ts +224 -0
- package/src/__tests__/market-r2-local-items.test.ts +197 -0
- package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
- package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
- package/src/__tests__/market-r2-routing.test.ts +127 -0
- package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
- package/src/__tests__/market-r2b-display-path.test.ts +180 -0
- package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
- package/src/__tests__/market-r2b-local-items.test.ts +253 -0
- package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
- package/src/__tests__/market-readonly.test.ts +162 -0
- package/src/__tests__/market-repositories.test.ts +263 -0
- package/src/__tests__/market-results.test.ts +198 -0
- package/src/__tests__/market-router.test.ts +65 -0
- package/src/__tests__/market-safety.test.ts +357 -0
- package/src/__tests__/market-teammates.test.ts +304 -0
- package/src/cli/install.ts +64 -5
- package/src/cli/market-view.ts +329 -0
- package/src/cli/market.ts +664 -0
- package/src/cli/router.ts +69 -3
- package/src/cli/update-view.ts +56 -0
- package/src/cli/update.ts +111 -9
- package/src/data/marketplaces.ts +8 -2
- package/src/main.tsx +20 -2
- package/src/market-tui.tsx +21 -0
- package/src/services/active-closure.ts +71 -9
- package/src/services/claude-cli.ts +140 -10
- package/src/services/claude-settings.ts +19 -25
- package/src/services/doctor.ts +5 -5
- package/src/services/git-write.ts +615 -0
- package/src/services/github-cli.ts +309 -0
- package/src/services/local-marketplace.ts +112 -50
- package/src/services/market-bases.ts +191 -0
- package/src/services/market-change.ts +242 -0
- package/src/services/market-clone.ts +572 -0
- package/src/services/market-fs.ts +138 -0
- package/src/services/market-install.ts +172 -0
- package/src/services/market-layout.ts +311 -0
- package/src/services/market-local.ts +93 -0
- package/src/services/market-marker.ts +49 -0
- package/src/services/market-ops-content.ts +277 -0
- package/src/services/market-ops-plugin.ts +655 -0
- package/src/services/market-ops-publish.ts +1227 -0
- package/src/services/market-ops-repo.ts +916 -0
- package/src/services/market-ops-skill.ts +788 -0
- package/src/services/market-ops.ts +686 -0
- package/src/services/market-publish.ts +468 -0
- package/src/services/market-registry.ts +382 -0
- package/src/services/market-types.ts +268 -0
- package/src/services/marketplace-conflicts.ts +127 -0
- package/src/services/marketplace-refresh.ts +33 -3
- package/src/services/marketplace-sync.ts +253 -22
- package/src/services/plugin-dependencies.ts +17 -0
- package/src/services/plugin-manager.ts +1 -1
- package/src/services/plugin-mcp-config.ts +6 -9
- package/src/services/plugin-requires.ts +20 -8
- package/src/services/profile-absorb.ts +88 -3
- package/src/services/profile-adopt.ts +10 -1
- package/src/services/profiles.ts +64 -1
- package/src/services/resolver.ts +17 -5
- package/src/services/settings-manager.ts +2 -2
- package/src/services/skills-manager.ts +2 -2
- package/src/services/update-plan.ts +67 -8
- package/src/tui-runtime.tsx +142 -0
- package/src/tui.tsx +7 -124
- package/src/types/index.ts +7 -0
- package/src/ui/adapters/marketAdapter.ts +366 -0
- package/src/ui/adapters/pluginsAdapter.ts +45 -3
- package/src/ui/components/layout/ScreenLayout.tsx +43 -17
- package/src/ui/components/modals/ConfirmModal.tsx +145 -8
- package/src/ui/components/modals/HelpOverlay.tsx +10 -3
- package/src/ui/components/modals/ModalContainer.tsx +47 -5
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/components/primitives/ScopeBadge.tsx +28 -0
- package/src/ui/components/primitives/ScopeVersionLine.tsx +27 -0
- package/src/ui/components/primitives/SelectableRow.tsx +14 -1
- package/src/ui/components/primitives/ToneLine.tsx +25 -0
- package/src/ui/components/primitives/index.ts +5 -0
- package/src/ui/keymap.ts +21 -5
- package/src/ui/market/MarketApp.tsx +59 -0
- package/src/ui/market/MarketplaceScreen.tsx +280 -0
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +161 -0
- package/src/ui/market/MarketplacesScreen.tsx +264 -0
- package/src/ui/market/PluginDetail.snowflake.tsx +61 -0
- package/src/ui/market/SkillDetail.snowflake.tsx +71 -0
- package/src/ui/market/marketKeymap.ts +243 -0
- package/src/ui/market/useMarketState.ts +694 -0
- package/src/ui/renderers/pluginRenderers.tsx +31 -3
- package/src/ui/renderers/pluginStatusText.ts +6 -0
- package/src/ui/screens/PluginsScreen.tsx +49 -5
- package/src/ui/screens/SkillsScreen.tsx +2 -2
- package/src/ui/state/AppContext.tsx +14 -2
- package/src/ui/state/types.ts +9 -2
- package/src/utils/config-dir.ts +29 -9
- package/src/utils/display-path.ts +33 -0
|
@@ -132,7 +132,9 @@ function categoryDetail(
|
|
|
132
132
|
<text fg={isEnabled ? theme.colors.success : theme.colors.muted}>
|
|
133
133
|
{isEnabled ? "● Added" : "○ Not added"}
|
|
134
134
|
</text>
|
|
135
|
-
|
|
135
|
+
{mp.source.repo ? (
|
|
136
|
+
<text fg={theme.colors.info}>github.com/{mp.source.repo}</text>
|
|
137
|
+
) : null}
|
|
136
138
|
<text fg={theme.colors.text}>Plugins: {item.pluginCount}</text>
|
|
137
139
|
<ActionHints
|
|
138
140
|
hints={[
|
|
@@ -328,7 +330,12 @@ function StatusLine({
|
|
|
328
330
|
? `→ v${status.target}`
|
|
329
331
|
: "";
|
|
330
332
|
// Moved or still required: kept on purpose, which is known — never UNKNOWN.
|
|
331
|
-
|
|
333
|
+
// Left out of updates for its marketplace: known too.
|
|
334
|
+
const label = isKeptDeprecated(status)
|
|
335
|
+
? "KEPT"
|
|
336
|
+
: status.reason === "marketplace-conflict"
|
|
337
|
+
? "LEFT OUT"
|
|
338
|
+
: ACTION_LABEL[status.action];
|
|
332
339
|
return (
|
|
333
340
|
<KeyValueLine
|
|
334
341
|
label="Status"
|
|
@@ -571,7 +578,28 @@ function pluginDetail(
|
|
|
571
578
|
|
|
572
579
|
{/* Metadata */}
|
|
573
580
|
<StatusLine status={status} />
|
|
574
|
-
{
|
|
581
|
+
{/*
|
|
582
|
+
* Left out: the version this catalog offers is the other repository's,
|
|
583
|
+
* so it is not shown as one to install. What is installed here, and
|
|
584
|
+
* that updates leave it alone, is.
|
|
585
|
+
*/}
|
|
586
|
+
{status?.reason === "marketplace-conflict" ? (
|
|
587
|
+
<KeyValueLine
|
|
588
|
+
label="Version"
|
|
589
|
+
value={
|
|
590
|
+
<span>
|
|
591
|
+
{status.installed ? (
|
|
592
|
+
<span fg={theme.colors.info}>
|
|
593
|
+
v{status.installed} installed
|
|
594
|
+
</span>
|
|
595
|
+
) : (
|
|
596
|
+
<span fg={theme.colors.muted}>not installed</span>
|
|
597
|
+
)}
|
|
598
|
+
<span fg={theme.colors.warning}> · left out of updates</span>
|
|
599
|
+
</span>
|
|
600
|
+
}
|
|
601
|
+
/>
|
|
602
|
+
) : showVersion ? (
|
|
575
603
|
<KeyValueLine
|
|
576
604
|
label="Version"
|
|
577
605
|
value={
|
|
@@ -40,6 +40,12 @@ export function pluginStatusText(
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Left out of every update: its marketplace here is not the profile's. A
|
|
44
|
+
// fact that needs a look, so amber, and never the "?" of "cannot tell".
|
|
45
|
+
if (status.reason === "marketplace-conflict") {
|
|
46
|
+
return { suffix: ` left out — ${status.note ?? ""}`, tone: "warning" };
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
switch (status.action) {
|
|
44
50
|
case "unknown":
|
|
45
51
|
// Amber, not red: "cannot tell" is not a failure.
|
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
} from "../../services/claude-cli.js";
|
|
13
13
|
import { readSettings, setMcpEnvVar } from "../../services/claude-settings.js";
|
|
14
14
|
import { clearContentDriftCache } from "../../services/content-drift.js";
|
|
15
|
+
import {
|
|
16
|
+
type MarketplaceCheck,
|
|
17
|
+
NOTHING_LEFT_OUT,
|
|
18
|
+
leftOutWhy,
|
|
19
|
+
} from "../../services/marketplace-conflicts.js";
|
|
20
|
+
import { conflictFix } from "../../services/marketplace-sync.js";
|
|
15
21
|
import { missingPluginEnvVars } from "../../services/plugin-env-needs.js";
|
|
16
22
|
import {
|
|
17
23
|
type MarketplaceFetchFailure,
|
|
@@ -179,6 +185,13 @@ export function PluginsScreen() {
|
|
|
179
185
|
const [listedByProfiles, setListedByProfiles] = useState<readonly string[]>(
|
|
180
186
|
[],
|
|
181
187
|
);
|
|
188
|
+
/**
|
|
189
|
+
* The profile's marketplaces whose plugins `magus update` leaves out here,
|
|
190
|
+
* found without changing anything: their rows say so, and no update or
|
|
191
|
+
* install is planned for them.
|
|
192
|
+
*/
|
|
193
|
+
const [marketplaceCheck, setMarketplaceCheck] =
|
|
194
|
+
useState<MarketplaceCheck>(NOTHING_LEFT_OUT);
|
|
182
195
|
|
|
183
196
|
// Fetch data (always fetches all scopes). Only the most recently started
|
|
184
197
|
// load may touch the screen.
|
|
@@ -213,6 +226,7 @@ export function PluginsScreen() {
|
|
|
213
226
|
setActiveProfileId(active?.profileId ?? null);
|
|
214
227
|
setRequiredBy(required);
|
|
215
228
|
setListedByProfiles(active?.listedByProfiles ?? []);
|
|
229
|
+
setMarketplaceCheck(active?.marketplaces ?? NOTHING_LEFT_OUT);
|
|
216
230
|
|
|
217
231
|
// Which catalogs answered, and which only appeared to. Read straight
|
|
218
232
|
// after the fetch, while the record still describes this attempt.
|
|
@@ -309,9 +323,10 @@ export function PluginsScreen() {
|
|
|
309
323
|
pins ?? {},
|
|
310
324
|
requiredBy,
|
|
311
325
|
listedByProfiles,
|
|
326
|
+
(pluginId) => leftOutWhy(pluginId, marketplaceCheck),
|
|
312
327
|
)
|
|
313
328
|
: new Map(),
|
|
314
|
-
[shownPlugins, pins, requiredBy, listedByProfiles],
|
|
329
|
+
[shownPlugins, pins, requiredBy, listedByProfiles, marketplaceCheck],
|
|
315
330
|
);
|
|
316
331
|
|
|
317
332
|
/**
|
|
@@ -754,7 +769,7 @@ export function PluginsScreen() {
|
|
|
754
769
|
if (choice === null) return;
|
|
755
770
|
if (choice) {
|
|
756
771
|
try {
|
|
757
|
-
await cliAddMarketplace(choice);
|
|
772
|
+
await cliAddMarketplace(choice, "user");
|
|
758
773
|
await handleRefresh();
|
|
759
774
|
} catch (e) {
|
|
760
775
|
await modal.failures(addMarketplaceFailure(choice, e));
|
|
@@ -909,6 +924,25 @@ export function PluginsScreen() {
|
|
|
909
924
|
}
|
|
910
925
|
};
|
|
911
926
|
|
|
927
|
+
/**
|
|
928
|
+
* A row `magus update` leaves out — its marketplace here is not the one the
|
|
929
|
+
* profile names, or could not be compared — takes no install, update,
|
|
930
|
+
* removal or scope change from any key: what this machine would install is
|
|
931
|
+
* the other repository's plugin. The one popup says so; no claude command
|
|
932
|
+
* runs. True when the key was refused.
|
|
933
|
+
*/
|
|
934
|
+
const refuseLeftOut = async (plugin: PluginInfo): Promise<boolean> => {
|
|
935
|
+
const status = statuses.get(plugin.id);
|
|
936
|
+
if (status?.reason !== "marketplace-conflict") return false;
|
|
937
|
+
const conflicted = marketplaceCheck.conflicted.has(plugin.marketplace);
|
|
938
|
+
await modal.failures({
|
|
939
|
+
headline: `${plugin.name} is left out — nothing was changed`,
|
|
940
|
+
rows: [{ subject: plugin.name, reason: status.note ?? "left out" }],
|
|
941
|
+
fix: conflicted ? conflictFix(plugin.marketplace) : null,
|
|
942
|
+
});
|
|
943
|
+
return true;
|
|
944
|
+
};
|
|
945
|
+
|
|
912
946
|
const handleSelect = async () => {
|
|
913
947
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
914
948
|
if (!item) return;
|
|
@@ -940,7 +974,7 @@ export function PluginsScreen() {
|
|
|
940
974
|
);
|
|
941
975
|
if (wantAdd) {
|
|
942
976
|
try {
|
|
943
|
-
await cliAddMarketplace(repo);
|
|
977
|
+
await cliAddMarketplace(repo, "user");
|
|
944
978
|
await handleRefresh();
|
|
945
979
|
} catch (e) {
|
|
946
980
|
await modal.failures(addMarketplaceFailure(repo, e));
|
|
@@ -949,6 +983,7 @@ export function PluginsScreen() {
|
|
|
949
983
|
}
|
|
950
984
|
} else if (item.kind === "plugin") {
|
|
951
985
|
const plugin = item.plugin;
|
|
986
|
+
if (await refuseLeftOut(plugin)) return;
|
|
952
987
|
const status = statuses.get(plugin.id);
|
|
953
988
|
|
|
954
989
|
// The label's arrow and what the key then does are ONE rule —
|
|
@@ -1088,6 +1123,7 @@ export function PluginsScreen() {
|
|
|
1088
1123
|
const handleUpdate = async () => {
|
|
1089
1124
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
1090
1125
|
if (!item || item.kind !== "plugin") return;
|
|
1126
|
+
if (await refuseLeftOut(item.plugin)) return;
|
|
1091
1127
|
// The row said "update", "stale — reinstall" or "BROKEN"; nothing else is
|
|
1092
1128
|
// work for this key. `install` stays out — that is the scope keys' and the
|
|
1093
1129
|
// profile's business, and the detail panel never offers U for it.
|
|
@@ -1180,6 +1216,7 @@ export function PluginsScreen() {
|
|
|
1180
1216
|
const handleScopeToggle = async (scope: "user" | "project" | "local") => {
|
|
1181
1217
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
1182
1218
|
if (!item || item.kind !== "plugin") return;
|
|
1219
|
+
if (await refuseLeftOut(item.plugin)) return;
|
|
1183
1220
|
|
|
1184
1221
|
const plugin = item.plugin;
|
|
1185
1222
|
const scopeLabel =
|
|
@@ -1253,6 +1290,7 @@ export function PluginsScreen() {
|
|
|
1253
1290
|
const handleRemoveDeprecated = async () => {
|
|
1254
1291
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
1255
1292
|
if (!item || item.kind !== "plugin" || !item.plugin.isOrphaned) return;
|
|
1293
|
+
if (await refuseLeftOut(item.plugin)) return;
|
|
1256
1294
|
|
|
1257
1295
|
const plugin = item.plugin;
|
|
1258
1296
|
|
|
@@ -1425,8 +1463,14 @@ export function PluginsScreen() {
|
|
|
1425
1463
|
// "cannot tell rendered as fine" bug.
|
|
1426
1464
|
// Moved or still-required plugins are `unknown` to the planner but known to
|
|
1427
1465
|
// the user — kept on purpose — so they are not counted as "unverified".
|
|
1466
|
+
// Left out for their marketplace: known too, and counted in their own words.
|
|
1467
|
+
const leftOutCount = [...statuses.values()].filter(
|
|
1468
|
+
(s) => s.reason === "marketplace-conflict",
|
|
1469
|
+
).length;
|
|
1428
1470
|
const unknownCount =
|
|
1429
|
-
counts.unknown -
|
|
1471
|
+
counts.unknown -
|
|
1472
|
+
[...statuses.values()].filter(isKeptDeprecated).length -
|
|
1473
|
+
leftOutCount;
|
|
1430
1474
|
const installCount = counts.install;
|
|
1431
1475
|
// Broken: installed at the right version, loading an older one. Its own
|
|
1432
1476
|
// number, never folded into `updateCount` or into silence — a clean header
|
|
@@ -1439,7 +1483,7 @@ export function PluginsScreen() {
|
|
|
1439
1483
|
// No longer published, and `a` removes them. Counted where the rows can be
|
|
1440
1484
|
// seen to need work; they used to sit inside "unverified".
|
|
1441
1485
|
const deprecatedCount = counts.remove;
|
|
1442
|
-
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}${installCount > 0 ? ` │ ${installCount} to install` : ""}${brokenCount > 0 ? ` │ ${brokenCount} broken` : ""}${deprecatedCount > 0 ? ` │ ${deprecatedCount} deprecated` : ""}${unknownCount > 0 ? ` │ ${unknownCount} unverified` : ""}`;
|
|
1486
|
+
const subtitle = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} updates` : ""}${installCount > 0 ? ` │ ${installCount} to install` : ""}${brokenCount > 0 ? ` │ ${brokenCount} broken` : ""}${deprecatedCount > 0 ? ` │ ${deprecatedCount} deprecated` : ""}${leftOutCount > 0 ? ` │ ${leftOutCount} left out` : ""}${unknownCount > 0 ? ` │ ${unknownCount} unverified` : ""}`;
|
|
1443
1487
|
const searchPlaceholder = `${scopeLabel} │ ${installedCount} installed${updateCount > 0 ? ` │ ${updateCount} ⬆` : ""}${installCount > 0 ? ` │ ${installCount} +` : ""}${brokenCount + deprecatedCount > 0 ? ` │ ${brokenCount + deprecatedCount} ✗` : ""}${unknownCount > 0 ? ` │ ${unknownCount} ?` : ""} │ / to search`;
|
|
1444
1488
|
|
|
1445
1489
|
const catalogNotice = buildCatalogNotice({
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import fs from "fs-extra";
|
|
4
3
|
import React, {
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
fetchAvailableSkills,
|
|
28
27
|
fetchSkillFrontmatter,
|
|
29
28
|
fetchSkillSetSkills,
|
|
29
|
+
getUserSkillsDir,
|
|
30
30
|
installSkill,
|
|
31
31
|
readLocalSkillFrontmatter,
|
|
32
32
|
uninstallSkill,
|
|
@@ -213,7 +213,7 @@ export function SkillsScreen() {
|
|
|
213
213
|
const user = new Map<string, SkillFrontmatter | null>();
|
|
214
214
|
const project = new Map<string, SkillFrontmatter | null>();
|
|
215
215
|
const unreadable: FailureRow[] = [];
|
|
216
|
-
const userDir =
|
|
216
|
+
const userDir = getUserSkillsDir();
|
|
217
217
|
const projDir = path.join(
|
|
218
218
|
state.projectPath || process.cwd(),
|
|
219
219
|
".claude",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useContext,
|
|
7
7
|
useReducer,
|
|
8
8
|
} from "react";
|
|
9
|
+
import type { HelpSection } from "../keymap.js";
|
|
9
10
|
import { appReducer, initialState } from "./reducer.js";
|
|
10
11
|
import type {
|
|
11
12
|
AppAction,
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
ProgressState,
|
|
16
17
|
Route,
|
|
17
18
|
Screen,
|
|
19
|
+
SelectOption,
|
|
18
20
|
} from "./types.js";
|
|
19
21
|
|
|
20
22
|
// ============================================================================
|
|
@@ -144,7 +146,11 @@ export function useModal() {
|
|
|
144
146
|
dispatch({ type: "HIDE_MODAL" });
|
|
145
147
|
},
|
|
146
148
|
|
|
147
|
-
confirm: (
|
|
149
|
+
confirm: (
|
|
150
|
+
title: string,
|
|
151
|
+
message: string,
|
|
152
|
+
details?: readonly string[],
|
|
153
|
+
): Promise<boolean> => {
|
|
148
154
|
return new Promise((resolve) => {
|
|
149
155
|
dispatch({
|
|
150
156
|
type: "SHOW_MODAL",
|
|
@@ -152,6 +158,7 @@ export function useModal() {
|
|
|
152
158
|
type: "confirm",
|
|
153
159
|
title,
|
|
154
160
|
message,
|
|
161
|
+
...(details ? { details } : {}),
|
|
155
162
|
onConfirm: () => {
|
|
156
163
|
dispatch({ type: "HIDE_MODAL" });
|
|
157
164
|
resolve(true);
|
|
@@ -194,7 +201,7 @@ export function useModal() {
|
|
|
194
201
|
select: (
|
|
195
202
|
title: string,
|
|
196
203
|
message: string,
|
|
197
|
-
options:
|
|
204
|
+
options: SelectOption[],
|
|
198
205
|
defaultIndex?: number,
|
|
199
206
|
): Promise<string | null> => {
|
|
200
207
|
return new Promise((resolve) => {
|
|
@@ -275,6 +282,11 @@ export function useModal() {
|
|
|
275
282
|
help: (screen: Screen) => {
|
|
276
283
|
dispatch({ type: "SHOW_MODAL", modal: { type: "help", screen } });
|
|
277
284
|
},
|
|
285
|
+
|
|
286
|
+
/** The key help overlay of an app with its own keymap: exactly `sections`. */
|
|
287
|
+
helpSections: (sections: readonly HelpSection[]) => {
|
|
288
|
+
dispatch({ type: "SHOW_MODAL", modal: { type: "help", sections } });
|
|
289
|
+
},
|
|
278
290
|
};
|
|
279
291
|
}
|
|
280
292
|
|
package/src/ui/state/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ProfileEntry,
|
|
13
13
|
SkillInfo,
|
|
14
14
|
} from "../../types/index.js";
|
|
15
|
+
import type { HelpSection } from "../keymap.js";
|
|
15
16
|
import type { ReportBadgeKind } from "../theme-mode.js";
|
|
16
17
|
|
|
17
18
|
// ============================================================================
|
|
@@ -120,6 +121,8 @@ export type ModalState =
|
|
|
120
121
|
type: "confirm";
|
|
121
122
|
title: string;
|
|
122
123
|
message: string;
|
|
124
|
+
/** What confirming does, one line each — a plan, drawn as a list. */
|
|
125
|
+
details?: readonly string[];
|
|
123
126
|
onConfirm: () => void;
|
|
124
127
|
onCancel: () => void;
|
|
125
128
|
}
|
|
@@ -173,9 +176,13 @@ export type ModalState =
|
|
|
173
176
|
message: string;
|
|
174
177
|
}
|
|
175
178
|
| {
|
|
176
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* The key help overlay, opened with `?`: `screen` first, from the main
|
|
181
|
+
* keymap — or, in an app with its own keymap, exactly `sections`.
|
|
182
|
+
*/
|
|
177
183
|
type: "help";
|
|
178
|
-
screen
|
|
184
|
+
screen?: Screen;
|
|
185
|
+
sections?: readonly HelpSection[];
|
|
179
186
|
};
|
|
180
187
|
|
|
181
188
|
// ============================================================================
|
package/src/utils/config-dir.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* config-dir.ts — where Claude Code's config lives, with a test guard.
|
|
3
3
|
*
|
|
4
|
-
* `CLAUDE_CONFIG_DIR` is Claude Code's own override and must be honoured
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `CLAUDE_CONFIG_DIR` is Claude Code's own override and must be honoured: Claude
|
|
5
|
+
* Code keeps everything under it — `settings.json`, `skills/`, and `plugins/`
|
|
6
|
+
* with the registry, the marketplace clones and the cache. Reading any of those
|
|
7
|
+
* from one root and the rest from another makes installed flags and installed
|
|
8
|
+
* versions disagree, so every reader resolves through this file.
|
|
9
|
+
*
|
|
10
|
+
* Resolved per call rather than captured at import time, because a
|
|
11
|
+
* module-level path bakes in the operator's real directory and cannot be
|
|
12
|
+
* overridden afterwards.
|
|
13
|
+
*
|
|
14
|
+
* Without `CLAUDE_CONFIG_DIR` the root is `<HOME>/.claude`, with `HOME` read at
|
|
15
|
+
* call time and `os.homedir()` only as the fallback: under Bun `os.homedir()`
|
|
16
|
+
* answers the home the process started with and ignores a later
|
|
17
|
+
* `process.env.HOME` (measured on Bun 1.4; Node follows the change).
|
|
8
18
|
*
|
|
9
19
|
* The test guard
|
|
10
20
|
* --------------
|
|
11
|
-
* Under `bun test`, a missing `CLAUDE_CONFIG_DIR`
|
|
12
|
-
* `~/.claude`. Callers treat null as "no cache, no
|
|
21
|
+
* Under `bun test`, a missing `CLAUDE_CONFIG_DIR` makes `claudeConfigDirOrNull`
|
|
22
|
+
* return null instead of `~/.claude`. Callers treat null as "no cache, no
|
|
23
|
+
* clone" — an empty, inert state.
|
|
13
24
|
*
|
|
14
25
|
* This is not defensive decoration. Once the catalog cache and the rate-limit
|
|
15
26
|
* cooldown moved to disk, and the catalog resolver gained a `git fetch` fallback,
|
|
@@ -25,6 +36,11 @@
|
|
|
25
36
|
import os from "node:os";
|
|
26
37
|
import path from "node:path";
|
|
27
38
|
|
|
39
|
+
/** `HOME` at call time, else the home the process started with. */
|
|
40
|
+
function homeDir(): string {
|
|
41
|
+
return process.env.HOME || os.homedir();
|
|
42
|
+
}
|
|
43
|
+
|
|
28
44
|
/**
|
|
29
45
|
* The Claude config directory, or null when a test has not chosen one.
|
|
30
46
|
*
|
|
@@ -36,10 +52,14 @@ export function claudeConfigDirOrNull(): string | null {
|
|
|
36
52
|
const explicit = process.env.CLAUDE_CONFIG_DIR;
|
|
37
53
|
if (explicit) return explicit;
|
|
38
54
|
if (process.env.NODE_ENV === "test") return null;
|
|
39
|
-
return path.join(
|
|
55
|
+
return path.join(homeDir(), ".claude");
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* The Claude config directory: `CLAUDE_CONFIG_DIR`, else `<HOME>/.claude`,
|
|
60
|
+
* even under test. The one root for every Claude Code file magus reads: plugin
|
|
61
|
+
* state, USER `settings.json` and USER skills.
|
|
62
|
+
*/
|
|
43
63
|
export function requireClaudeConfigDir(): string {
|
|
44
|
-
return process.env.CLAUDE_CONFIG_DIR || path.join(
|
|
64
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(homeDir(), ".claude");
|
|
45
65
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* display-path.ts — a folder as a person reads it: relative to the project
|
|
3
|
+
* when it is inside it, `~/…` when it is under the home folder, else as is.
|
|
4
|
+
* For display only; JSON and every file operation keep the absolute path.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
/** `child` is `parent` itself or somewhere under it. */
|
|
11
|
+
function within(parent: string, child: string): string | null {
|
|
12
|
+
const rel = path.relative(parent, child);
|
|
13
|
+
if (rel === "") return "";
|
|
14
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
15
|
+
return rel;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `abs` shortened for display: `.claude/skills/greet` inside `cwd`,
|
|
20
|
+
* `~/.claude/skills/team-tools` under the home folder, otherwise unchanged.
|
|
21
|
+
* The home folder is `$HOME` when set, as Claude Code and the shell read it.
|
|
22
|
+
*/
|
|
23
|
+
export function displayPath(
|
|
24
|
+
abs: string,
|
|
25
|
+
cwd: string,
|
|
26
|
+
home: string = process.env.HOME || os.homedir(),
|
|
27
|
+
): string {
|
|
28
|
+
const inProject = within(path.resolve(cwd), path.resolve(abs));
|
|
29
|
+
if (inProject !== null) return inProject === "" ? "." : inProject;
|
|
30
|
+
const inHome = home ? within(path.resolve(home), path.resolve(abs)) : null;
|
|
31
|
+
if (inHome !== null) return inHome === "" ? "~" : `~/${inHome}`;
|
|
32
|
+
return abs;
|
|
33
|
+
}
|