magus-cli 7.4.0 → 7.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 +9 -8
- package/package.json +4 -4
- package/src/__tests__/blind/c19-paste-routing.test.ts +252 -0
- package/src/__tests__/blind/c20-registry-and-file-safety.test.ts +560 -0
- package/src/__tests__/install-command.test.ts +4 -3
- package/src/__tests__/models-cli.test.ts +4 -7
- package/src/__tests__/models-manager.test.ts +52 -40
- package/src/__tests__/profile-absorb.test.ts +1138 -0
- package/src/__tests__/profile-command.test.ts +90 -32
- package/src/__tests__/profile-migration-offer.test.ts +18 -15
- package/src/__tests__/profile-migration.test.ts +246 -117
- package/src/__tests__/profile-render.test.ts +141 -0
- package/src/__tests__/profile-skills.test.ts +425 -0
- package/src/__tests__/profile-startup.test.ts +44 -14
- package/src/__tests__/profile-state.test.ts +795 -0
- package/src/__tests__/settings-file.test.ts +5 -7
- package/src/__tests__/stale-load-repair.test.ts +5 -4
- package/src/__tests__/styles-manager.test.ts +47 -74
- package/src/__tests__/styles-screen-state.test.ts +11 -10
- package/src/cli/bootstrap.ts +87 -27
- package/src/cli/doctor.ts +152 -44
- package/src/cli/install.ts +92 -127
- package/src/cli/models.ts +21 -1
- package/src/cli/profile.ts +48 -77
- package/src/cli/router.ts +32 -6
- package/src/cli/update.ts +43 -6
- package/src/data/gitignore-defaults.ts +3 -3
- package/src/services/active-closure.ts +3 -3
- package/src/services/claude-settings.ts +283 -125
- package/src/services/cli-tool-commands.ts +30 -0
- package/src/services/conventions-manager.ts +88 -0
- package/src/services/git-worktree.ts +21 -0
- package/src/services/gitignore-service.ts +39 -22
- package/src/services/manifest.ts +4 -5
- package/src/services/models-manager.ts +36 -73
- package/src/services/profile-absorb.ts +1532 -0
- package/src/services/profile-activate.ts +348 -96
- package/src/services/profile-live.ts +825 -0
- package/src/services/profile-render.ts +115 -0
- package/src/services/profile-skills.ts +462 -0
- package/src/services/profile-startup.ts +88 -16
- package/src/services/profile-state.ts +622 -0
- package/src/services/profiles.ts +20 -32
- package/src/services/resolver.ts +6 -1
- package/src/services/settings-file.ts +14 -8
- package/src/services/skills-manager.ts +14 -8
- package/src/services/styles-manager.ts +45 -28
- package/src/services/tool-probe.ts +48 -3
- package/src/types/index.ts +15 -9
- package/src/ui/App.tsx +25 -2
- package/src/ui/components/modals/ModalContainer.tsx +9 -1
- package/src/ui/hooks/useProfileStartupModal.ts +20 -15
- package/src/ui/input/KeyRouter.tsx +147 -23
- package/src/ui/input/key-dispatch.ts +48 -0
- package/src/ui/keymap.ts +8 -1
- package/src/ui/renderers/cliToolRenderers.tsx +65 -27
- package/src/ui/screens/CliToolsScreen.tsx +74 -13
- package/src/ui/screens/PluginsScreen.tsx +6 -2
- package/src/ui/screens/ProfilesScreen.tsx +75 -27
- package/src/ui/screens/StylesScreen.tsx +2 -1
- package/src/ui/state/types.ts +1 -1
- package/src/__tests__/profile-materializer.test.ts +0 -238
- package/src/__tests__/profile-sync.test.ts +0 -154
- package/src/__tests__/symlink-manager.test.ts +0 -180
- package/src/services/profile-materializer.ts +0 -133
- package/src/services/profile-sync.ts +0 -158
- package/src/services/symlink-manager.ts +0 -343
package/src/cli/profile.ts
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `magus profile <list|show|switch|
|
|
2
|
+
* `magus profile <list|show|switch|init> [name]`
|
|
3
3
|
*
|
|
4
|
-
* Inspect and switch between team profiles
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Inspect and switch between the team profiles in `.claude/profiles.json`.
|
|
5
|
+
* Switching renders the chosen profile's settings.json, .mcp.json and
|
|
6
|
+
* models.json and installs its skills; anything Claude Code wrote into the
|
|
7
|
+
* live config first goes into the profile being left.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import fs from "fs-extra";
|
|
12
|
-
import { ensureGitignoreEntries } from "../services/conventions-manager.js";
|
|
13
10
|
import { checkBinaries, missingBinaries } from "../services/doctor-bins.js";
|
|
14
11
|
import { summarizeClosure } from "../services/install-plan.js";
|
|
15
12
|
import { getManifestPath, readManifest } from "../services/manifest.js";
|
|
16
13
|
import { reportDisplaced } from "../services/profile-activate.js";
|
|
17
|
-
import {
|
|
14
|
+
import { activeProfile, namedActiveProfile } from "../services/profile-live.js";
|
|
15
|
+
import type { SkillInstaller } from "../services/profile-skills.js";
|
|
16
|
+
import {
|
|
17
|
+
suggestedProfile,
|
|
18
|
+
switchHint,
|
|
19
|
+
switchToProfile,
|
|
20
|
+
} from "../services/profile-state.js";
|
|
18
21
|
import { resolveProfile } from "../services/resolver.js";
|
|
19
22
|
import { SettingsParseError } from "../services/settings-file.js";
|
|
20
23
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from "
|
|
26
|
-
import { ensureManifest, reportUnparseable } from "./bootstrap.js";
|
|
24
|
+
ensureManifest,
|
|
25
|
+
printActivationNotes,
|
|
26
|
+
reportUnparseable,
|
|
27
|
+
} from "./bootstrap.js";
|
|
28
|
+
import { note as noteLine } from "./update-view.js";
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Every subcommand reads `.claude/profiles.json`, and one that does not parse
|
|
@@ -58,11 +60,9 @@ async function dispatchProfileCommand(args: string[]): Promise<number> {
|
|
|
58
60
|
args.includes("--yes") || args.includes("-y"),
|
|
59
61
|
projectPath,
|
|
60
62
|
);
|
|
61
|
-
case "sync":
|
|
62
|
-
return syncActiveProfile(rest[0], projectPath);
|
|
63
63
|
default:
|
|
64
64
|
console.error(
|
|
65
|
-
`Unknown profile command "${sub}". Use: list | show <name> | switch <name> | init
|
|
65
|
+
`Unknown profile command "${sub}". Use: list | show <name> | switch <name> | init`,
|
|
66
66
|
);
|
|
67
67
|
return 1;
|
|
68
68
|
}
|
|
@@ -83,7 +83,9 @@ export async function initProfile(
|
|
|
83
83
|
console.error(
|
|
84
84
|
`${getManifestPath(projectPath)} already defines: ${Object.keys(existing.profiles).join(", ")}`,
|
|
85
85
|
);
|
|
86
|
-
console.error(
|
|
86
|
+
console.error(
|
|
87
|
+
"Changes to the live config are recorded in the active profile automatically.",
|
|
88
|
+
);
|
|
87
89
|
return 1;
|
|
88
90
|
}
|
|
89
91
|
// `profile init` exists to create the manifest, so it is the one caller that
|
|
@@ -103,12 +105,18 @@ export async function listProfiles(projectPath: string): Promise<number> {
|
|
|
103
105
|
for (const id of ids) {
|
|
104
106
|
const entry = manifest.profiles[id];
|
|
105
107
|
const marker = id === active ? "●" : " ";
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
console.log(
|
|
108
|
+
console.log(` ${marker} ${id} — ${entry.name}`);
|
|
109
|
+
}
|
|
110
|
+
if (active) {
|
|
111
|
+
console.log(`\nActive: ${active}`);
|
|
112
|
+
} else {
|
|
113
|
+
const named = await namedActiveProfile(projectPath);
|
|
114
|
+
if (named) {
|
|
115
|
+
console.log(
|
|
116
|
+
`\nThe active profile is recorded as "${named.profile}", which is not defined above. Run: ${switchHint(ids, suggestedProfile(manifest))}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
|
-
if (active) console.log(`\nActive: ${active}`);
|
|
112
120
|
return 0;
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -137,42 +145,32 @@ export async function showProfile(
|
|
|
137
145
|
export async function switchProfile(
|
|
138
146
|
name: string | undefined,
|
|
139
147
|
projectPath: string,
|
|
148
|
+
/** Skill installer seam; the real one fetches from GitHub. */
|
|
149
|
+
opts: { installSkill?: SkillInstaller } = {},
|
|
140
150
|
): Promise<number> {
|
|
141
151
|
if (!name) {
|
|
142
152
|
console.error("Usage: magus profile switch <name>");
|
|
143
153
|
return 1;
|
|
144
154
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
155
|
+
|
|
156
|
+
// The one switch path, shared with the TUI's Profiles tab: edits go into
|
|
157
|
+
// the profile being LEFT (or, if it no longer exists, into this one), then
|
|
158
|
+
// this profile is rendered. A no-op absorb when the router's gate ran first.
|
|
159
|
+
const result = await switchToProfile(projectPath, name, {
|
|
160
|
+
onDisplace: reportDisplaced(projectPath),
|
|
161
|
+
installSkill: opts.installSkill,
|
|
162
|
+
});
|
|
163
|
+
for (const line of result.lines) console.log(noteLine(line));
|
|
164
|
+
if (result.problems.length > 0 || !result.activation) {
|
|
165
|
+
for (const problem of result.problems) console.error(problem);
|
|
156
166
|
return 1;
|
|
157
167
|
}
|
|
158
|
-
|
|
159
|
-
// In profile mode the source of truth is .claude/profiles.json; the
|
|
160
|
-
// generated dir AND the per-developer symlinks must stay out of git so
|
|
161
|
-
// switching profiles causes no churn.
|
|
162
|
-
await ensureGitignoreEntries(
|
|
163
|
-
path.join(projectPath, ".gitignore"),
|
|
164
|
-
PROFILE_GITIGNORE_ENTRIES,
|
|
165
|
-
"# magus: generated profile build output + active-profile symlinks",
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// Switching between two already-active profiles displaces nothing — the paths
|
|
169
|
-
// are links magus owns. The FIRST switch in an unmanaged project displaces
|
|
170
|
-
// real files, and that is the run that must say so.
|
|
171
|
-
await activateProfile(name, projectPath, reportDisplaced(projectPath));
|
|
172
168
|
console.log(`✓ Switched to profile: ${name}`);
|
|
169
|
+
printActivationNotes(result.activation);
|
|
170
|
+
const manifest = await readManifest(projectPath);
|
|
173
171
|
|
|
174
172
|
// Verify the profile's binary deps are actually usable — the switch itself
|
|
175
|
-
// succeeds (
|
|
173
|
+
// succeeds (the config is in place), but warn so "switched" is not misleading.
|
|
176
174
|
try {
|
|
177
175
|
const closure = await resolveProfile(manifest, name);
|
|
178
176
|
const missing = missingBinaries(await checkBinaries(closure.bins));
|
|
@@ -189,30 +187,3 @@ export async function switchProfile(
|
|
|
189
187
|
}
|
|
190
188
|
return 0;
|
|
191
189
|
}
|
|
192
|
-
|
|
193
|
-
export async function syncActiveProfile(
|
|
194
|
-
nameArg: string | undefined,
|
|
195
|
-
projectPath: string,
|
|
196
|
-
): Promise<number> {
|
|
197
|
-
const name = nameArg ?? (await activeProfile(projectPath));
|
|
198
|
-
if (!name) {
|
|
199
|
-
console.error(
|
|
200
|
-
"No active profile to sync. Pass a name: magus profile sync <name>",
|
|
201
|
-
);
|
|
202
|
-
return 1;
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
const { changes, wrote } = await syncProfile(name, projectPath);
|
|
206
|
-
if (!wrote) {
|
|
207
|
-
console.log(`Profile "${name}" is already in sync with the manifest.`);
|
|
208
|
-
return 0;
|
|
209
|
-
}
|
|
210
|
-
console.log(`Synced "${name}" → .claude/profiles.json:`);
|
|
211
|
-
for (const c of changes) console.log(` ${c.kind}: ${c.detail}`);
|
|
212
|
-
console.log("\nReview the diff and commit .claude/profiles.json.");
|
|
213
|
-
return 0;
|
|
214
|
-
} catch (e) {
|
|
215
|
-
console.error((e as Error).message);
|
|
216
|
-
return 1;
|
|
217
|
-
}
|
|
218
|
-
}
|
package/src/cli/router.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { ThemeMode } from "@opentui/core";
|
|
|
10
10
|
import { SettingsParseError } from "../services/settings-file.js";
|
|
11
11
|
import { checkForUpdates } from "../services/version-check.js";
|
|
12
12
|
import { type ThemeEnv, parseThemeWord } from "../ui/theme-resolve.js";
|
|
13
|
-
import { reportUnparseable } from "./bootstrap.js";
|
|
13
|
+
import { profileGate, reportUnparseable } from "./bootstrap.js";
|
|
14
14
|
import { runDoctorCommand } from "./doctor.js";
|
|
15
15
|
import { runInstallCommand } from "./install.js";
|
|
16
16
|
import { runModelsCommand } from "./models.js";
|
|
@@ -102,6 +102,9 @@ const SUBCOMMANDS = new Set([
|
|
|
102
102
|
"doctor",
|
|
103
103
|
]);
|
|
104
104
|
|
|
105
|
+
/** Subcommands that read or write profile state, and so pass the gate first. */
|
|
106
|
+
const PROFILE_GATED = new Set(["install", "update", "profile", "models"]);
|
|
107
|
+
|
|
105
108
|
export async function route(
|
|
106
109
|
argv: string[],
|
|
107
110
|
version: string,
|
|
@@ -173,6 +176,25 @@ export async function route(
|
|
|
173
176
|
setThemeMode(mode);
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
// Every command that reads or writes profile state passes the same gate
|
|
180
|
+
// first: restore a lost marker, convert an older layout, absorb what Claude
|
|
181
|
+
// Code wrote into the live config, regenerate from a changed profiles.json.
|
|
182
|
+
// Read-only runs (`--check`, `--dry-run`) promise to write nothing, so they
|
|
183
|
+
// skip it. `doctor` runs it itself, to report what it did.
|
|
184
|
+
if (
|
|
185
|
+
command !== undefined &&
|
|
186
|
+
PROFILE_GATED.has(command) &&
|
|
187
|
+
!rest.includes("--check") &&
|
|
188
|
+
!rest.includes("--dry-run")
|
|
189
|
+
) {
|
|
190
|
+
const ok = await profileGate(process.cwd(), {
|
|
191
|
+
// Both of these fix a marker naming a missing profile by activating
|
|
192
|
+
// one that exists.
|
|
193
|
+
allowUnknown: command === "install" || command === "profile",
|
|
194
|
+
});
|
|
195
|
+
if (!ok) return HANDLED(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
176
198
|
try {
|
|
177
199
|
switch (command) {
|
|
178
200
|
case "update":
|
|
@@ -227,9 +249,9 @@ without one is offered adoption on first run. See docs/team-configuration.md.
|
|
|
227
249
|
install [profile] Install the profile's plugins, binaries, skills and env at
|
|
228
250
|
the manifest's PINNED versions, then activate it.
|
|
229
251
|
No argument installs every profile.
|
|
230
|
-
--check Report
|
|
252
|
+
--check Report version mismatches only, write nothing
|
|
253
|
+
(exits 1 in strict mode)
|
|
231
254
|
--yes, -y Skip the confirmation prompt
|
|
232
|
-
--force Discard unsynced local edits instead of refusing
|
|
233
255
|
update [profile] Keep the ACTIVE profile current: install whatever it
|
|
234
256
|
declares but this machine lacks, and advance anything
|
|
235
257
|
behind the version its marketplace publishes. Only the
|
|
@@ -244,10 +266,14 @@ without one is offered adoption on first run. See docs/team-configuration.md.
|
|
|
244
266
|
missing or known to be behind — the CI gate.
|
|
245
267
|
profile list Show every profile; ● marks the active one
|
|
246
268
|
profile show <n> Print a profile's fully resolved closure
|
|
247
|
-
profile switch <n>
|
|
269
|
+
profile switch <n> Make <n> the active profile: render its settings.json,
|
|
270
|
+
.mcp.json and models.json, install its skills
|
|
248
271
|
profile init Write .claude/profiles.json from this project's setup
|
|
249
|
-
|
|
250
|
-
|
|
272
|
+
|
|
273
|
+
Changes Claude Code makes to .claude/settings.json or .mcp.json (a plugin
|
|
274
|
+
installed, a setting changed) are changes to the active profile: every command
|
|
275
|
+
above records them in .claude/profiles.json before it acts.
|
|
276
|
+
doctor Check binary deps, the active profile, and conventions
|
|
251
277
|
--fix Apply the repairs it can make itself
|
|
252
278
|
|
|
253
279
|
Per-subagent model routing — give each subagent its own model and effort. The
|
package/src/cli/update.ts
CHANGED
|
@@ -48,9 +48,20 @@ import {
|
|
|
48
48
|
clearMarketplaceCache,
|
|
49
49
|
getAvailablePlugins,
|
|
50
50
|
} from "../services/plugin-manager.js";
|
|
51
|
+
import {
|
|
52
|
+
recordRefetchedSkills,
|
|
53
|
+
trackFetchedSkills,
|
|
54
|
+
updatableSkills,
|
|
55
|
+
} from "../services/profile-activate.js";
|
|
56
|
+
import { activeProfile } from "../services/profile-live.js";
|
|
57
|
+
import { skillFolder } from "../services/profile-skills.js";
|
|
58
|
+
import {
|
|
59
|
+
suggestedProfile,
|
|
60
|
+
switchHint,
|
|
61
|
+
withCliBookkeeping,
|
|
62
|
+
} from "../services/profile-state.js";
|
|
51
63
|
import { resolveProfile } from "../services/resolver.js";
|
|
52
64
|
import { getInstalledSkillNames } from "../services/skills-manager.js";
|
|
53
|
-
import { activeProfile } from "../services/symlink-manager.js";
|
|
54
65
|
import { REAL_TOOL_PROBE_IO, probeTools } from "../services/tool-probe.js";
|
|
55
66
|
// The apply half lives in services/ so the TUI runs the SAME code — see the
|
|
56
67
|
// header of update-engine.ts for what drifted while it did not.
|
|
@@ -197,7 +208,7 @@ export async function runUpdateCommand(
|
|
|
197
208
|
//
|
|
198
209
|
// BOTH `--check` and `--dry-run` are read-only here. An earlier version let
|
|
199
210
|
// `--dry-run` activate, reasoning that a preview needs a real profile to
|
|
200
|
-
// report against — but activating writes
|
|
211
|
+
// report against — but activating writes config files and edits a TRACKED
|
|
201
212
|
// `.gitignore`, and `--dry-run` promises to write nothing and exit 0. A
|
|
202
213
|
// preview that mutates the repo is not a preview.
|
|
203
214
|
//
|
|
@@ -207,7 +218,7 @@ export async function runUpdateCommand(
|
|
|
207
218
|
// preview result rather than an error.
|
|
208
219
|
//
|
|
209
220
|
// NAMING A PROFILE SKIPS THE GATE. `magus update lean` says which profile to
|
|
210
|
-
// update, and updating it needs the manifest, not the
|
|
221
|
+
// update, and updating it needs the manifest, not the marker — installs go
|
|
211
222
|
// to project scope either way. Gating it would refuse a question the user
|
|
212
223
|
// already answered on the command line, and would break `magus update lean`
|
|
213
224
|
// and `--check lean` in CI, both of which worked before this gate existed.
|
|
@@ -254,7 +265,7 @@ export async function runUpdateCommand(
|
|
|
254
265
|
if (!resolvedId) {
|
|
255
266
|
console.error(
|
|
256
267
|
failLine(
|
|
257
|
-
`No active profile. Run
|
|
268
|
+
`No active profile. Run ${switchHint(profileIds, suggestedProfile(current))}.`,
|
|
258
269
|
),
|
|
259
270
|
);
|
|
260
271
|
return 1;
|
|
@@ -414,11 +425,18 @@ export async function runUpdateCommand(
|
|
|
414
425
|
|
|
415
426
|
// ---- plan ----------------------------------------------------------------
|
|
416
427
|
|
|
428
|
+
// Skills go into the live `.claude/skills/`, so only the ACTIVE profile's,
|
|
429
|
+
// and never over a custom skill folder.
|
|
430
|
+
const skillRefs = await updatableSkills(
|
|
431
|
+
projectPath,
|
|
432
|
+
targetId,
|
|
433
|
+
closure.skills,
|
|
434
|
+
);
|
|
417
435
|
const plan: UpdatePlan = {
|
|
418
436
|
profileId: targetId,
|
|
419
437
|
plugins: planPluginUpdates(closure.plugins, catalog),
|
|
420
438
|
bins: planBinUpdates(closure.bins, toolFacts),
|
|
421
|
-
skills: planSkillUpdates(
|
|
439
|
+
skills: planSkillUpdates(skillRefs, installedSkills),
|
|
422
440
|
binSpecs: new Map(closure.bins.map((b) => [b.name, b])),
|
|
423
441
|
};
|
|
424
442
|
|
|
@@ -575,9 +593,28 @@ export async function runUpdateCommand(
|
|
|
575
593
|
console.log("");
|
|
576
594
|
applyLive.start((tick) => applyFrame(state, tick, Date.now()));
|
|
577
595
|
|
|
578
|
-
|
|
596
|
+
// `claude plugin install --scope project` records each install in the live
|
|
597
|
+
// settings.json — for a profile that may not be the active one. The marker
|
|
598
|
+
// is flagged while that runs; in `finally` the re-fetched skill folders are
|
|
599
|
+
// recorded with their new contents, then the live config is reconciled:
|
|
600
|
+
// the user's own edits in the meantime are kept, the install's records are
|
|
601
|
+
// discarded, and an interrupted run leaves the flag for the next command.
|
|
602
|
+
const reconciledLines: string[] = [];
|
|
603
|
+
// Only the skills this run actually fetched get new hashes: one it did
|
|
604
|
+
// not fetch and someone edited meanwhile must still read as edited.
|
|
605
|
+
const skills = trackFetchedSkills(skillRefs, reporter);
|
|
606
|
+
const outcome = await withCliBookkeeping(
|
|
607
|
+
projectPath,
|
|
608
|
+
() => applyPlan(plan, projectPath, { report: skills.report }),
|
|
609
|
+
{
|
|
610
|
+
beforeReconcile: () =>
|
|
611
|
+
recordRefetchedSkills(projectPath, skills.fetched()),
|
|
612
|
+
report: (line) => reconciledLines.push(line),
|
|
613
|
+
},
|
|
614
|
+
);
|
|
579
615
|
|
|
580
616
|
applyLive.stop();
|
|
617
|
+
for (const line of reconciledLines) console.log(noteLine(line));
|
|
581
618
|
|
|
582
619
|
const { failed } = outcome;
|
|
583
620
|
|
|
@@ -12,9 +12,9 @@ export const BUILTIN_DEFAULTS: GitignoreManifest = {
|
|
|
12
12
|
".claude/scheduled_tasks.lock",
|
|
13
13
|
".claude/cache/",
|
|
14
14
|
".claude/.statusline-worktree-*",
|
|
15
|
-
//
|
|
16
|
-
".claude/
|
|
17
|
-
// Generated from profiles.json (
|
|
15
|
+
// The per-developer active-profile marker.
|
|
16
|
+
".claude/profiles/",
|
|
17
|
+
// Generated from profiles.json (rendered from the active profile when one
|
|
18
18
|
// is active). The routing itself IS committed — in profiles.json, under
|
|
19
19
|
// the profile that declares it.
|
|
20
20
|
".claude/models.json",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* pick the profile in different ways — the CLI inline, the TUI not at all (it
|
|
7
7
|
* judged everything against "latest") — so an exact pin could never agree.
|
|
8
8
|
* Both now choose through {@link selectProfileId} over the same manifest and
|
|
9
|
-
* the same active-profile
|
|
9
|
+
* the same active-profile marker.
|
|
10
10
|
*
|
|
11
11
|
* Read-only. Unlike the CLI's `ensureManifest`, nothing here bootstraps a
|
|
12
12
|
* manifest: a screen opening must never author a file a team commits.
|
|
@@ -20,14 +20,14 @@ import type {
|
|
|
20
20
|
ResolvedClosure,
|
|
21
21
|
} from "../types/index.js";
|
|
22
22
|
import { readManifest, validateManifest } from "./manifest.js";
|
|
23
|
+
import { activeProfile } from "./profile-live.js";
|
|
23
24
|
import { resolveProfile } from "./resolver.js";
|
|
24
|
-
import { activeProfile } from "./symlink-manager.js";
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* The profile to use: the explicit one, else the active one, else the only one.
|
|
28
28
|
*
|
|
29
29
|
* Each candidate counts only if the manifest defines it. A stale active-profile
|
|
30
|
-
*
|
|
30
|
+
* marker naming a profile that was since removed would otherwise be chosen and
|
|
31
31
|
* then fail to resolve, even when the manifest has exactly one profile and the
|
|
32
32
|
* answer is unambiguous.
|
|
33
33
|
*/
|