fitout 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Josh Nichols
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # Fitout
2
+
3
+ [![CI](https://github.com/technicalpickles/fitout/actions/workflows/ci.yml/badge.svg)](https://github.com/technicalpickles/fitout/actions/workflows/ci.yml)
4
+
5
+ Context-aware plugin manager for Claude Code.
6
+
7
+ ## The Problem
8
+
9
+ Managing Claude Code plugins across projects is painful:
10
+ - Config files *look* correct but don't reflect what's actually installed
11
+ - This mismatch leads to broken sessions and missing capabilities
12
+ - Manually syncing plugins across projects is tedious and error-prone
13
+
14
+ ## The Solution
15
+
16
+ Fitout ensures your actual runtime state matches your declared configuration.
17
+
18
+ 1. Declare desired plugins in `.claude/fitout.toml`
19
+ 2. Run `fitout status` to see the diff
20
+ 3. Run `fitout apply` to sync
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ # Install globally
26
+ npm install -g fitout
27
+
28
+ # Set up Claude integration
29
+ fitout init
30
+ ```
31
+
32
+ This adds a SessionStart hook to Claude Code that automatically installs missing plugins when you start a session.
33
+
34
+ ### Non-interactive setup
35
+
36
+ ```bash
37
+ fitout init --yes # Use defaults (creates default profile)
38
+ fitout init --hook-only # Only add hook, no profile
39
+ ```
40
+
41
+ Requires [Claude Code CLI](https://claude.ai/docs/claude-code) to be installed.
42
+
43
+ ## Quick Start
44
+
45
+ Create `.claude/fitout.toml` in your project:
46
+
47
+ ```toml
48
+ plugins = [
49
+ "superpowers@superpowers-marketplace",
50
+ "ci-cd-tools@pickled-claude-plugins",
51
+ ]
52
+ ```
53
+
54
+ Check status:
55
+
56
+ ```bash
57
+ fitout status
58
+ ```
59
+
60
+ Output:
61
+
62
+ ```
63
+ Context: /path/to/project
64
+
65
+ ✗ superpowers@superpowers-marketplace (missing)
66
+ ✗ ci-cd-tools@pickled-claude-plugins (missing)
67
+
68
+ 0 present, 2 missing
69
+ ```
70
+
71
+ Install missing plugins:
72
+
73
+ ```bash
74
+ fitout apply
75
+ ```
76
+
77
+ ## Commands
78
+
79
+ ### `fitout status`
80
+
81
+ Shows the diff between desired and installed plugins.
82
+
83
+ - `✓` - Plugin is installed
84
+ - `✗` - Plugin is missing
85
+ - `?` - Plugin is installed but not in config
86
+
87
+ Exit code is `1` if any plugins are missing, `0` otherwise.
88
+
89
+ ### `fitout apply`
90
+
91
+ Installs missing plugins to sync with config.
92
+
93
+ ```bash
94
+ fitout apply # Install missing plugins
95
+ fitout apply --dry-run # Preview what would be installed
96
+ ```
97
+
98
+ ## Profiles
99
+
100
+ Share plugin sets across projects using profiles.
101
+
102
+ ### User Profiles
103
+
104
+ Create profiles at `~/.config/fitout/profiles/`:
105
+
106
+ ```toml
107
+ # ~/.config/fitout/profiles/default.toml
108
+ # Auto-included in every project (silent if missing)
109
+ plugins = [
110
+ "superpowers@superpowers-marketplace",
111
+ ]
112
+ ```
113
+
114
+ ```toml
115
+ # ~/.config/fitout/profiles/backend.toml
116
+ plugins = [
117
+ "database-tools@some-registry",
118
+ "api-helpers@some-registry",
119
+ ]
120
+ ```
121
+
122
+ ### Using Profiles
123
+
124
+ Reference profiles in your project config:
125
+
126
+ ```toml
127
+ # .claude/fitout.toml
128
+ profiles = ["backend"]
129
+ plugins = [
130
+ "project-specific@registry",
131
+ ]
132
+ ```
133
+
134
+ Plugins merge additively. The `default` profile auto-includes if present.
135
+
136
+ ### Provenance
137
+
138
+ Status output shows where each plugin comes from:
139
+
140
+ ```
141
+ Context: /path/to/project
142
+
143
+ ✓ superpowers@superpowers-marketplace (from: default)
144
+ ✓ database-tools@some-registry (from: backend)
145
+ ✓ project-specific@registry
146
+
147
+ 3 present
148
+ ```
149
+
150
+ ## Configuration Reference
151
+
152
+ ### Project Config (`.claude/fitout.toml`)
153
+
154
+ ```toml
155
+ # Optional: explicit profiles to include
156
+ profiles = ["backend", "testing"]
157
+
158
+ # Required: plugins for this project
159
+ plugins = [
160
+ "plugin-name@registry",
161
+ ]
162
+ ```
163
+
164
+ ### Profile Config (`~/.config/fitout/profiles/<name>.toml`)
165
+
166
+ ```toml
167
+ # Plugins provided by this profile
168
+ plugins = [
169
+ "plugin-name@registry",
170
+ ]
171
+ ```
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ npm install # Install dependencies
177
+ npm test # Run tests
178
+ npm run dev -- status # Run in dev mode
179
+ npm run build # Build to dist/
180
+ ```
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,10 @@
1
+ export interface InstalledPlugin {
2
+ id: string;
3
+ version: string;
4
+ scope: 'local' | 'user' | 'global';
5
+ enabled: boolean;
6
+ projectPath?: string;
7
+ }
8
+ export declare function parsePluginList(jsonOutput: string): InstalledPlugin[];
9
+ export declare function listPlugins(): InstalledPlugin[];
10
+ export declare function installPlugin(pluginId: string): void;
package/dist/claude.js ADDED
@@ -0,0 +1,20 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ export function parsePluginList(jsonOutput) {
3
+ const parsed = JSON.parse(jsonOutput);
4
+ if (!Array.isArray(parsed)) {
5
+ return [];
6
+ }
7
+ return parsed;
8
+ }
9
+ export function listPlugins() {
10
+ const output = execFileSync('claude', ['plugin', 'list', '--json'], {
11
+ encoding: 'utf-8',
12
+ });
13
+ return parsePluginList(output);
14
+ }
15
+ export function installPlugin(pluginId) {
16
+ execFileSync('claude', ['plugin', 'install', pluginId, '--scope', 'local'], {
17
+ encoding: 'utf-8',
18
+ stdio: 'inherit',
19
+ });
20
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { runStatus } from './status.js';
4
+ import { runInstall } from './install.js';
5
+ import { runInit, getProjectConfigPath, readClaudeSettings, getFitoutHookStatus, hasFitoutSkill, hasDefaultProfile, hasProjectConfig, getProjectConfigContent, getDefaultProfilePath, } from './init.js';
6
+ import { getClaudeSettingsPath, getFitoutSkillPath } from './paths.js';
7
+ import { getProfilesDir, resolveProjectRoot } from './context.js';
8
+ import { confirm, input } from './prompt.js';
9
+ import { colors, symbols, formatPath } from './colors.js';
10
+ import { hasGlobalConfig, createGlobalConfig, getGlobalConfigPath, getGlobalConfigContent } from './globalConfig.js';
11
+ import { refreshMarketplaces, listAvailablePlugins } from './marketplace.js';
12
+ import { runUpdate, updatePlugin } from './update.js';
13
+ import { listPlugins } from './claude.js';
14
+ import { handleCompletion, installCompletion, uninstallCompletion } from './completion.js';
15
+ // Handle shell completion requests before command parsing
16
+ if (handleCompletion()) {
17
+ process.exit(0);
18
+ }
19
+ program
20
+ .name('fitout')
21
+ .description('Context-aware plugin manager for Claude Code')
22
+ .version('0.1.0');
23
+ program
24
+ .command('status')
25
+ .description('Show desired vs actual plugin state')
26
+ .option('--refresh', 'Refresh marketplace data before checking')
27
+ .action((options) => {
28
+ const { output, exitCode } = runStatus(process.cwd(), {
29
+ refresh: options.refresh,
30
+ });
31
+ console.log(output);
32
+ process.exit(exitCode);
33
+ });
34
+ // Helper for install action (used by both `install` command and default)
35
+ function doInstall(options = {}) {
36
+ const { output, exitCode } = runInstall(process.cwd(), {
37
+ dryRun: options.dryRun,
38
+ hook: options.hook,
39
+ });
40
+ if (output) {
41
+ console.log(output);
42
+ }
43
+ process.exit(exitCode);
44
+ }
45
+ program
46
+ .command('install', { isDefault: true })
47
+ .description('Install missing plugins to sync desired state')
48
+ .option('--dry-run', 'Show what would change without installing')
49
+ .option('--hook', 'Hook mode: silent on no-op, minimal output for Claude context')
50
+ .action((options) => doInstall(options));
51
+ program
52
+ .command('update [plugins...]')
53
+ .description('Update outdated plugins (all if no plugins specified)')
54
+ .option('--offline', 'Skip marketplace refresh, use cached data')
55
+ .option('--dry-run', 'Show what would be updated without applying')
56
+ .action((plugins, options) => {
57
+ const projectRoot = resolveProjectRoot(process.cwd());
58
+ // Refresh by default, unless --offline
59
+ if (!options.offline) {
60
+ console.log('Refreshing marketplaces...');
61
+ refreshMarketplaces();
62
+ }
63
+ const installed = listPlugins();
64
+ const available = listAvailablePlugins();
65
+ const result = runUpdate(projectRoot, installed, available, plugins, {
66
+ dryRun: options.dryRun,
67
+ });
68
+ if (result.output) {
69
+ console.log(result.output);
70
+ }
71
+ if (result.exitCode !== 0) {
72
+ process.exit(result.exitCode);
73
+ }
74
+ // Perform actual updates
75
+ if (result.pluginsToUpdate.length > 0) {
76
+ for (const plugin of result.pluginsToUpdate) {
77
+ console.log(` ${symbols.outdated} ${plugin.id} v${plugin.installedVersion} → v${plugin.availableVersion}`);
78
+ updatePlugin(plugin.id);
79
+ }
80
+ console.log('');
81
+ console.log(`Updated ${result.pluginsToUpdate.length} plugin${result.pluginsToUpdate.length > 1 ? 's' : ''}. Restart Claude to apply changes.`);
82
+ }
83
+ process.exit(0);
84
+ });
85
+ // Marketplace subcommands
86
+ const marketplace = program
87
+ .command('marketplace')
88
+ .description('Manage Claude Code marketplaces');
89
+ marketplace
90
+ .command('refresh')
91
+ .description('Update marketplace data from sources')
92
+ .action(() => {
93
+ console.log('Refreshing marketplaces...');
94
+ refreshMarketplaces();
95
+ console.log(`${symbols.present} Marketplaces updated`);
96
+ process.exit(0);
97
+ });
98
+ program
99
+ .command('init')
100
+ .description('Set up Fitout integration with Claude Code')
101
+ .option('-y, --yes', 'Skip prompts, use defaults')
102
+ .option('--hook-only', 'Only add the hook, do not create profile or project config')
103
+ .action(async (options) => {
104
+ const settingsPath = getClaudeSettingsPath();
105
+ const profilesDir = getProfilesDir();
106
+ const projectRoot = resolveProjectRoot(process.cwd());
107
+ const projectConfigPath = getProjectConfigPath(projectRoot);
108
+ const skillPath = getFitoutSkillPath();
109
+ // Check current state
110
+ const settings = readClaudeSettings(settingsPath);
111
+ const hookStatus = getFitoutHookStatus(settings);
112
+ const hookExists = hookStatus !== 'none';
113
+ const hookOutdated = hookStatus === 'outdated';
114
+ const skillExists = hasFitoutSkill();
115
+ const profileExists = hasDefaultProfile(profilesDir);
116
+ const configExists = hasProjectConfig(projectRoot);
117
+ // Handle --hook-only mode
118
+ if (options.hookOnly) {
119
+ if (hookStatus === 'current') {
120
+ console.log(`${symbols.present} Hook already installed`);
121
+ process.exit(0);
122
+ }
123
+ const result = runInit({
124
+ settingsPath,
125
+ profilesDir,
126
+ createProfile: false,
127
+ createSkill: false,
128
+ });
129
+ if (result.hookUpgraded) {
130
+ console.log(`${symbols.present} SessionStart hook upgraded in ${formatPath(settingsPath)}`);
131
+ }
132
+ else {
133
+ console.log(`${symbols.present} SessionStart hook added to ${formatPath(settingsPath)}`);
134
+ }
135
+ console.log(colors.dim(' Restart Claude to activate'));
136
+ process.exit(0);
137
+ }
138
+ // Handle --yes mode (non-interactive)
139
+ if (options.yes) {
140
+ const result = runInit({
141
+ settingsPath,
142
+ profilesDir,
143
+ createProfile: true,
144
+ profileName: 'default',
145
+ projectRoot,
146
+ createProjectConfig: true,
147
+ createSkill: true,
148
+ });
149
+ const created = [];
150
+ const upgraded = [];
151
+ if (result.hookAdded)
152
+ created.push('hook');
153
+ if (result.hookUpgraded)
154
+ upgraded.push('hook');
155
+ if (result.skillCreated)
156
+ created.push('skill');
157
+ if (result.profileCreated)
158
+ created.push('profile');
159
+ if (result.projectConfigCreated)
160
+ created.push('project config');
161
+ if (created.length === 0 && upgraded.length === 0) {
162
+ console.log(`${symbols.present} Already initialized`);
163
+ }
164
+ else {
165
+ const parts = [];
166
+ if (created.length > 0)
167
+ parts.push(`Created: ${created.join(', ')}`);
168
+ if (upgraded.length > 0)
169
+ parts.push(`Upgraded: ${upgraded.join(', ')}`);
170
+ console.log(`${symbols.present} ${parts.join('. ')}`);
171
+ console.log(colors.dim(' Restart Claude to activate'));
172
+ }
173
+ process.exit(0);
174
+ }
175
+ // Interactive phased mode
176
+ console.log('Checking Fitout setup...\n');
177
+ // Phase 1: Global setup
178
+ console.log(colors.header('Global:'));
179
+ let needsGlobalSetup = false;
180
+ if (hookStatus === 'current') {
181
+ console.log(` ${symbols.present} SessionStart hook`);
182
+ }
183
+ else if (hookStatus === 'outdated') {
184
+ console.log(` ${symbols.outdated} SessionStart hook ${colors.dim('(outdated)')}`);
185
+ needsGlobalSetup = true;
186
+ }
187
+ else {
188
+ console.log(` ${symbols.missing} SessionStart hook ${colors.dim('(missing)')}`);
189
+ needsGlobalSetup = true;
190
+ }
191
+ if (skillExists) {
192
+ console.log(` ${symbols.present} Diagnostic skill`);
193
+ }
194
+ else {
195
+ console.log(` ${symbols.missing} Diagnostic skill ${colors.dim('(missing)')}`);
196
+ needsGlobalSetup = true;
197
+ }
198
+ const globalConfigExists = hasGlobalConfig();
199
+ if (globalConfigExists) {
200
+ console.log(` ${symbols.present} Global config`);
201
+ }
202
+ else {
203
+ console.log(` ${symbols.missing} Global config ${colors.dim('(optional)')}`);
204
+ }
205
+ // Set up global components if needed
206
+ let hookAdded = false;
207
+ let hookUpgraded = false;
208
+ let skillCreated = false;
209
+ let globalConfigCreated = false;
210
+ if (needsGlobalSetup) {
211
+ console.log('');
212
+ const promptText = hookOutdated
213
+ ? 'Set up missing/outdated global components?'
214
+ : 'Set up missing global components?';
215
+ const setupGlobal = await confirm(promptText);
216
+ if (setupGlobal) {
217
+ const result = runInit({
218
+ settingsPath,
219
+ profilesDir,
220
+ createProfile: false,
221
+ createSkill: !skillExists,
222
+ });
223
+ hookAdded = result.hookAdded;
224
+ hookUpgraded = result.hookUpgraded;
225
+ skillCreated = result.skillCreated;
226
+ if (hookAdded)
227
+ console.log(` ${symbols.present} Hook installed`);
228
+ if (hookUpgraded)
229
+ console.log(` ${symbols.present} Hook upgraded`);
230
+ if (skillCreated)
231
+ console.log(` ${symbols.present} Skill installed`);
232
+ }
233
+ }
234
+ // Offer to create global config if missing
235
+ if (!globalConfigExists) {
236
+ console.log('');
237
+ const createConfig = await confirm('Create global config for marketplaces?');
238
+ if (createConfig) {
239
+ // Show preview
240
+ const configContent = getGlobalConfigContent();
241
+ console.log(`\nReady to create ${formatPath(getGlobalConfigPath())}:`);
242
+ console.log(colors.dim(' ' + configContent.split('\n').join('\n ')));
243
+ const confirmCreate = await confirm('Create?');
244
+ if (confirmCreate) {
245
+ globalConfigCreated = createGlobalConfig();
246
+ if (globalConfigCreated) {
247
+ console.log(` ${symbols.present} Created ${formatPath(getGlobalConfigPath())}`);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ // Phase 2: Default profile
253
+ console.log('');
254
+ console.log(colors.header('Profile:'));
255
+ let profileCreated = false;
256
+ let profileName = 'default';
257
+ if (profileExists) {
258
+ console.log(` ${symbols.present} Default profile`);
259
+ }
260
+ else {
261
+ console.log(` ${symbols.missing} Default profile ${colors.dim('(missing)')}`);
262
+ console.log('');
263
+ const createProfile = await confirm('Create a default profile?');
264
+ if (createProfile) {
265
+ profileName = await input('Profile name', 'default');
266
+ const result = runInit({
267
+ settingsPath,
268
+ profilesDir,
269
+ createProfile: true,
270
+ profileName,
271
+ createSkill: false,
272
+ });
273
+ profileCreated = result.profileCreated;
274
+ if (profileCreated) {
275
+ console.log(` ${symbols.present} Created ${formatPath(getDefaultProfilePath(profilesDir, profileName))}`);
276
+ }
277
+ }
278
+ }
279
+ // Phase 3: Project config
280
+ console.log('');
281
+ console.log(colors.header('Project:'));
282
+ let projectConfigCreated = false;
283
+ if (configExists) {
284
+ console.log(` ${symbols.present} Project config`);
285
+ }
286
+ else {
287
+ console.log(` ${symbols.missing} Project config ${colors.dim('(missing)')}`);
288
+ console.log('');
289
+ // Show preview
290
+ const configContent = getProjectConfigContent(profileCreated || profileExists ? profileName : undefined);
291
+ console.log(`Ready to create ${formatPath(projectConfigPath)}:`);
292
+ console.log(colors.dim(' ' + configContent.split('\n').join('\n ')));
293
+ const createConfig = await confirm('Create project config?');
294
+ if (createConfig) {
295
+ const result = runInit({
296
+ settingsPath,
297
+ profilesDir,
298
+ createProfile: false,
299
+ projectRoot,
300
+ createProjectConfig: true,
301
+ createSkill: false,
302
+ });
303
+ projectConfigCreated = result.projectConfigCreated;
304
+ if (projectConfigCreated) {
305
+ console.log(` ${symbols.present} Created ${formatPath(projectConfigPath)}`);
306
+ }
307
+ }
308
+ }
309
+ // Summary
310
+ console.log('');
311
+ const anythingCreated = hookAdded || hookUpgraded || skillCreated || globalConfigCreated || profileCreated || projectConfigCreated;
312
+ if (!anythingCreated && hookExists && skillExists && profileExists && configExists) {
313
+ console.log(`${symbols.present} ${colors.success('Already initialized')}`);
314
+ }
315
+ else if (anythingCreated) {
316
+ console.log(colors.dim('Restart Claude to activate changes.'));
317
+ }
318
+ process.exit(0);
319
+ });
320
+ // Completion subcommands
321
+ const completion = program
322
+ .command('completion')
323
+ .description('Manage shell completions');
324
+ completion
325
+ .command('install')
326
+ .description('Install shell completions')
327
+ .argument('[shell]', 'Shell type: bash, zsh, fish, or pwsh (prompts if not specified)')
328
+ .action(async (shell) => {
329
+ try {
330
+ await installCompletion(shell);
331
+ console.log(`${symbols.present} Shell completions installed`);
332
+ console.log(colors.dim(' Restart your shell or source your config to activate'));
333
+ process.exit(0);
334
+ }
335
+ catch (err) {
336
+ console.error(`Failed to install completions: ${err}`);
337
+ process.exit(1);
338
+ }
339
+ });
340
+ completion
341
+ .command('uninstall')
342
+ .description('Remove shell completions')
343
+ .action(async () => {
344
+ try {
345
+ await uninstallCompletion();
346
+ console.log(`${symbols.present} Shell completions removed`);
347
+ process.exit(0);
348
+ }
349
+ catch (err) {
350
+ console.error(`Failed to uninstall completions: ${err}`);
351
+ process.exit(1);
352
+ }
353
+ });
354
+ program.parse();
@@ -0,0 +1,27 @@
1
+ export declare const colors: {
2
+ success: import("chalk").ChalkInstance;
3
+ error: import("chalk").ChalkInstance;
4
+ warning: import("chalk").ChalkInstance;
5
+ action: import("chalk").ChalkInstance;
6
+ header: import("chalk").ChalkInstance;
7
+ dim: import("chalk").ChalkInstance;
8
+ sourceDefault: import("chalk").ChalkInstance;
9
+ sourceProject: import("chalk").ChalkInstance;
10
+ sourceOther: import("chalk").ChalkInstance;
11
+ };
12
+ export declare const symbols: {
13
+ present: string;
14
+ missing: string;
15
+ extra: string;
16
+ install: string;
17
+ outdated: string;
18
+ };
19
+ export declare function provenanceColor(source: string): (text: string) => string;
20
+ /**
21
+ * Format a path for display, replacing $HOME with ~
22
+ */
23
+ export declare function formatPath(path: string): string;
24
+ /**
25
+ * Format context line, only showing if different from cwd
26
+ */
27
+ export declare function formatContextLine(projectRoot: string, cwd: string): string;
package/dist/colors.js ADDED
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import { homedir } from 'node:os';
3
+ export const colors = {
4
+ // Semantic
5
+ success: chalk.green,
6
+ error: chalk.red,
7
+ warning: chalk.yellow,
8
+ action: chalk.cyan,
9
+ // Structural
10
+ header: chalk.bold.white,
11
+ dim: chalk.dim,
12
+ // Provenance (dim versions)
13
+ sourceDefault: chalk.dim.blue,
14
+ sourceProject: chalk.dim.magenta,
15
+ sourceOther: chalk.dim.cyan,
16
+ };
17
+ export const symbols = {
18
+ present: colors.success('✓'),
19
+ missing: colors.error('✗'),
20
+ extra: colors.warning('?'),
21
+ install: colors.action('+'),
22
+ outdated: colors.warning('↑'),
23
+ };
24
+ export function provenanceColor(source) {
25
+ switch (source) {
26
+ case 'default':
27
+ return colors.sourceDefault;
28
+ case 'project':
29
+ return colors.sourceProject;
30
+ default:
31
+ return colors.sourceOther;
32
+ }
33
+ }
34
+ /**
35
+ * Format a path for display, replacing $HOME with ~
36
+ */
37
+ export function formatPath(path) {
38
+ const home = homedir();
39
+ return path.startsWith(home) ? path.replace(home, '~') : path;
40
+ }
41
+ /**
42
+ * Format context line, only showing if different from cwd
43
+ */
44
+ export function formatContextLine(projectRoot, cwd) {
45
+ if (projectRoot === cwd) {
46
+ return '';
47
+ }
48
+ return `${colors.header('Context:')} ${formatPath(projectRoot)}\n\n`;
49
+ }
@@ -0,0 +1,14 @@
1
+ import { type SupportedShell } from '@pnpm/tabtab';
2
+ /**
3
+ * Handle shell completion requests.
4
+ * Call this early in CLI startup - returns true if we handled a completion request.
5
+ */
6
+ export declare function handleCompletion(): boolean;
7
+ /**
8
+ * Install shell completions
9
+ */
10
+ export declare function installCompletion(shell?: SupportedShell): Promise<void>;
11
+ /**
12
+ * Uninstall shell completions
13
+ */
14
+ export declare function uninstallCompletion(): Promise<void>;