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 +21 -0
- package/README.md +184 -0
- package/dist/claude.d.ts +10 -0
- package/dist/claude.js +20 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +354 -0
- package/dist/colors.d.ts +27 -0
- package/dist/colors.js +49 -0
- package/dist/completion.d.ts +14 -0
- package/dist/completion.js +129 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +11 -0
- package/dist/constraint.d.ts +42 -0
- package/dist/constraint.js +111 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.js +27 -0
- package/dist/diff.d.ts +23 -0
- package/dist/diff.js +25 -0
- package/dist/globalConfig.d.ts +10 -0
- package/dist/globalConfig.js +60 -0
- package/dist/hookError.d.ts +2 -0
- package/dist/hookError.js +7 -0
- package/dist/init.d.ts +50 -0
- package/dist/init.js +227 -0
- package/dist/install.d.ts +25 -0
- package/dist/install.js +257 -0
- package/dist/marketplace.d.ts +32 -0
- package/dist/marketplace.js +100 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +36 -0
- package/dist/profiles.d.ts +19 -0
- package/dist/profiles.js +89 -0
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +52 -0
- package/dist/status.d.ts +22 -0
- package/dist/status.js +179 -0
- package/dist/test-utils.d.ts +8 -0
- package/dist/test-utils.js +33 -0
- package/dist/update.d.ts +28 -0
- package/dist/update.js +108 -0
- package/package.json +46 -0
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
|
+
[](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
|
package/dist/claude.d.ts
ADDED
|
@@ -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
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();
|
package/dist/colors.d.ts
ADDED
|
@@ -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>;
|