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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FettleConfig } from './config.js';
|
|
2
|
+
export interface ResolvedPlugin {
|
|
3
|
+
id: string;
|
|
4
|
+
source: string;
|
|
5
|
+
constraint: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface ConstraintOverride {
|
|
8
|
+
pluginId: string;
|
|
9
|
+
projectConstraint: string;
|
|
10
|
+
winningConstraint: string;
|
|
11
|
+
winningSource: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ProfileResolutionResult {
|
|
14
|
+
plugins: ResolvedPlugin[];
|
|
15
|
+
errors: string[];
|
|
16
|
+
constraintOverrides: ConstraintOverride[];
|
|
17
|
+
}
|
|
18
|
+
export declare function loadProfile(profilesDir: string, name: string): string[] | null;
|
|
19
|
+
export declare function resolveProfiles(profilesDir: string, config: FettleConfig): ProfileResolutionResult;
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse } from 'smol-toml';
|
|
4
|
+
import { parsePluginList, mergeConstraints } from './constraint.js';
|
|
5
|
+
export function loadProfile(profilesDir, name) {
|
|
6
|
+
const profilePath = join(profilesDir, `${name}.toml`);
|
|
7
|
+
if (!existsSync(profilePath)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const content = readFileSync(profilePath, 'utf-8');
|
|
11
|
+
const parsed = parse(content);
|
|
12
|
+
const plugins = Array.isArray(parsed.plugins)
|
|
13
|
+
? parsed.plugins.filter((p) => typeof p === 'string')
|
|
14
|
+
: [];
|
|
15
|
+
return plugins;
|
|
16
|
+
}
|
|
17
|
+
export function resolveProfiles(profilesDir, config) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const constraintOverrides = [];
|
|
20
|
+
const pluginMap = new Map();
|
|
21
|
+
// Track project constraints for override detection
|
|
22
|
+
const projectConstraints = new Map();
|
|
23
|
+
// Helper to add plugins with constraint merging
|
|
24
|
+
const addPlugins = (pluginStrings, source) => {
|
|
25
|
+
const parseResult = parsePluginList(pluginStrings);
|
|
26
|
+
// Collect parse errors
|
|
27
|
+
for (const error of parseResult.errors) {
|
|
28
|
+
errors.push(`${error.input}: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
for (const parsed of parseResult.plugins) {
|
|
31
|
+
const existing = pluginMap.get(parsed.id);
|
|
32
|
+
if (existing) {
|
|
33
|
+
// Plugin exists - merge constraints (higher wins)
|
|
34
|
+
const merged = mergeConstraints(existing.constraint, parsed.constraint);
|
|
35
|
+
// Track if project constraint was overridden
|
|
36
|
+
if (source === 'project' && parsed.constraint !== null) {
|
|
37
|
+
projectConstraints.set(parsed.id, parsed.constraint);
|
|
38
|
+
}
|
|
39
|
+
// Check if project's constraint is being overridden by profile
|
|
40
|
+
const projectConstraint = projectConstraints.get(parsed.id);
|
|
41
|
+
if (projectConstraint &&
|
|
42
|
+
existing.constraint !== null &&
|
|
43
|
+
merged === existing.constraint &&
|
|
44
|
+
merged !== projectConstraint) {
|
|
45
|
+
constraintOverrides.push({
|
|
46
|
+
pluginId: parsed.id,
|
|
47
|
+
projectConstraint,
|
|
48
|
+
winningConstraint: merged,
|
|
49
|
+
winningSource: existing.source,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
existing.constraint = merged;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
pluginMap.set(parsed.id, {
|
|
56
|
+
id: parsed.id,
|
|
57
|
+
source,
|
|
58
|
+
constraint: parsed.constraint,
|
|
59
|
+
});
|
|
60
|
+
// Track project constraints
|
|
61
|
+
if (source === 'project' && parsed.constraint !== null) {
|
|
62
|
+
projectConstraints.set(parsed.id, parsed.constraint);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// 1. Auto-include default if exists
|
|
68
|
+
const defaultPlugins = loadProfile(profilesDir, 'default');
|
|
69
|
+
if (defaultPlugins !== null) {
|
|
70
|
+
addPlugins(defaultPlugins, 'default');
|
|
71
|
+
}
|
|
72
|
+
// 2. Load explicit profiles
|
|
73
|
+
for (const profileName of config.profiles) {
|
|
74
|
+
const profilePlugins = loadProfile(profilesDir, profileName);
|
|
75
|
+
if (profilePlugins === null) {
|
|
76
|
+
errors.push(`Profile not found: ${profileName}`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
addPlugins(profilePlugins, profileName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 3. Add project plugins
|
|
83
|
+
addPlugins(config.plugins, 'project');
|
|
84
|
+
return {
|
|
85
|
+
plugins: Array.from(pluginMap.values()),
|
|
86
|
+
errors,
|
|
87
|
+
constraintOverrides,
|
|
88
|
+
};
|
|
89
|
+
}
|
package/dist/prompt.d.ts
ADDED
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/prompt.ts
|
|
2
|
+
import * as readline from 'node:readline';
|
|
3
|
+
export async function confirm(question, defaultValue = true) {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout,
|
|
7
|
+
});
|
|
8
|
+
const hint = defaultValue ? '(Y/n)' : '(y/N)';
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
let answered = false;
|
|
11
|
+
rl.on('close', () => {
|
|
12
|
+
if (!answered) {
|
|
13
|
+
// Ctrl+C pressed - exit cleanly
|
|
14
|
+
console.log('');
|
|
15
|
+
process.exit(130);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
rl.question(`${question} ${hint} `, (answer) => {
|
|
19
|
+
answered = true;
|
|
20
|
+
rl.close();
|
|
21
|
+
const trimmed = answer.trim().toLowerCase();
|
|
22
|
+
if (trimmed === '') {
|
|
23
|
+
resolve(defaultValue);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
resolve(trimmed === 'y' || trimmed === 'yes');
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function input(question, defaultValue = '') {
|
|
32
|
+
const rl = readline.createInterface({
|
|
33
|
+
input: process.stdin,
|
|
34
|
+
output: process.stdout,
|
|
35
|
+
});
|
|
36
|
+
const hint = defaultValue ? ` [${defaultValue}]` : '';
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
let answered = false;
|
|
39
|
+
rl.on('close', () => {
|
|
40
|
+
if (!answered) {
|
|
41
|
+
// Ctrl+C pressed - exit cleanly
|
|
42
|
+
console.log('');
|
|
43
|
+
process.exit(130);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
rl.question(`${question}${hint}: `, (answer) => {
|
|
47
|
+
answered = true;
|
|
48
|
+
rl.close();
|
|
49
|
+
resolve(answer.trim() || defaultValue);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PluginDiff, PluginDiffResolved } from './diff.js';
|
|
2
|
+
import { ConstraintOverride } from './profiles.js';
|
|
3
|
+
import { OutdatedPlugin } from './update.js';
|
|
4
|
+
import { HookStatus } from './init.js';
|
|
5
|
+
export interface StatusOptions {
|
|
6
|
+
refresh?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface GlobalStatus {
|
|
9
|
+
hookStatus: HookStatus;
|
|
10
|
+
skillInstalled: boolean;
|
|
11
|
+
profiles: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function formatGlobalStatus(status: GlobalStatus): string;
|
|
14
|
+
export interface StatusDiff extends PluginDiffResolved {
|
|
15
|
+
outdated: OutdatedPlugin[];
|
|
16
|
+
}
|
|
17
|
+
export declare function formatStatusResolved(diff: StatusDiff, showRefreshTip: boolean, constraintOverrides?: ConstraintOverride[]): string;
|
|
18
|
+
export declare function formatStatus(diff: PluginDiff): string;
|
|
19
|
+
export declare function runStatus(cwd: string, options?: StatusOptions): {
|
|
20
|
+
output: string;
|
|
21
|
+
exitCode: number;
|
|
22
|
+
};
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { findConfigPath, resolveProjectRoot, getProfilesDir } from './context.js';
|
|
3
|
+
import { parseConfig } from './config.js';
|
|
4
|
+
import { listPlugins } from './claude.js';
|
|
5
|
+
import { diffPluginsResolved } from './diff.js';
|
|
6
|
+
import { resolveProfiles } from './profiles.js';
|
|
7
|
+
import { colors, symbols, provenanceColor, formatContextLine } from './colors.js';
|
|
8
|
+
import { listAvailablePlugins, refreshMarketplaces } from './marketplace.js';
|
|
9
|
+
import { findOutdatedPlugins } from './update.js';
|
|
10
|
+
import { readClaudeSettings, getFitoutHookStatus, hasFitoutSkill } from './init.js';
|
|
11
|
+
import { getClaudeSettingsPath } from './paths.js';
|
|
12
|
+
export function formatGlobalStatus(status) {
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push(colors.header('Global:'));
|
|
15
|
+
if (status.hookStatus === 'current') {
|
|
16
|
+
lines.push(` ${symbols.present} Hook installed`);
|
|
17
|
+
}
|
|
18
|
+
else if (status.hookStatus === 'outdated') {
|
|
19
|
+
lines.push(` ${symbols.outdated} Hook outdated ${colors.dim('(run `fitout init` to upgrade)')}`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
lines.push(` ${symbols.missing} Hook ${colors.dim('(run `fitout init`)')}`);
|
|
23
|
+
}
|
|
24
|
+
if (status.skillInstalled) {
|
|
25
|
+
lines.push(` ${symbols.present} Skill installed`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
lines.push(` ${symbols.missing} Skill ${colors.dim('(run `fitout init`)')}`);
|
|
29
|
+
}
|
|
30
|
+
if (status.profiles.length > 0) {
|
|
31
|
+
const profileList = status.profiles.map(p => provenanceColor(p)(p)).join(', ');
|
|
32
|
+
lines.push(` ${symbols.present} Profiles: ${profileList}`);
|
|
33
|
+
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
function formatProvenance(source) {
|
|
37
|
+
if (source === 'project')
|
|
38
|
+
return '';
|
|
39
|
+
const colorFn = provenanceColor(source);
|
|
40
|
+
return ' ' + colorFn(`(from: ${source})`);
|
|
41
|
+
}
|
|
42
|
+
export function formatStatusResolved(diff, showRefreshTip, constraintOverrides = []) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
const outdatedIds = new Set(diff.outdated.map((p) => p.id));
|
|
45
|
+
for (const plugin of diff.present) {
|
|
46
|
+
const outdated = diff.outdated.find((o) => o.id === plugin.id);
|
|
47
|
+
const constraintStr = plugin.constraint ? ` >= ${plugin.constraint}` : '';
|
|
48
|
+
if (outdated) {
|
|
49
|
+
lines.push(`${symbols.outdated} ${plugin.id} ${colors.warning(`v${outdated.installedVersion} → v${outdated.availableVersion}`)}${constraintStr}${formatProvenance(plugin.source)} ${colors.warning('(outdated)')}`);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const versionStr = plugin.version ? ` ${plugin.version}` : '';
|
|
53
|
+
lines.push(`${symbols.present} ${plugin.id}${versionStr}${constraintStr}${formatProvenance(plugin.source)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const plugin of diff.missing) {
|
|
57
|
+
lines.push(`${symbols.missing} ${plugin.id}${formatProvenance(plugin.source)} ${colors.error('(missing)')}`);
|
|
58
|
+
}
|
|
59
|
+
for (const plugin of diff.extra) {
|
|
60
|
+
lines.push(`${symbols.extra} ${plugin.id} ${colors.warning('(not in config)')}`);
|
|
61
|
+
}
|
|
62
|
+
const upToDateCount = diff.present.length - diff.outdated.length;
|
|
63
|
+
const summary = [
|
|
64
|
+
upToDateCount > 0 ? colors.success(`${upToDateCount} present`) : null,
|
|
65
|
+
diff.outdated.length > 0 ? colors.warning(`${diff.outdated.length} outdated`) : null,
|
|
66
|
+
diff.missing.length > 0 ? colors.error(`${diff.missing.length} missing`) : null,
|
|
67
|
+
diff.extra.length > 0 ? colors.warning(`${diff.extra.length} extra`) : null,
|
|
68
|
+
].filter(Boolean).join(', ');
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(summary || 'No plugins configured');
|
|
71
|
+
// Add tips when there are outdated plugins
|
|
72
|
+
if (diff.outdated.length > 0) {
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(colors.dim(`Tip: Run \`fitout update\` to update outdated plugins.`));
|
|
75
|
+
if (showRefreshTip) {
|
|
76
|
+
lines.push(colors.dim(` Run \`fitout status --refresh\` to check for newer versions.`));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Add constraint override warnings
|
|
80
|
+
if (constraintOverrides.length > 0) {
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(colors.header('Warnings:'));
|
|
83
|
+
for (const override of constraintOverrides) {
|
|
84
|
+
lines.push(` ${override.pluginId}: constraint >= ${override.projectConstraint} (project) overridden by >= ${override.winningConstraint} (${override.winningSource})`);
|
|
85
|
+
lines.push(` To fix: update .claude/fitout.toml to ">= ${override.winningConstraint}" or remove the constraint`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
export function formatStatus(diff) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
for (const plugin of diff.present) {
|
|
93
|
+
lines.push(`${symbols.present} ${plugin.id}`);
|
|
94
|
+
}
|
|
95
|
+
for (const id of diff.missing) {
|
|
96
|
+
lines.push(`${symbols.missing} ${id} ${colors.error('(missing)')}`);
|
|
97
|
+
}
|
|
98
|
+
for (const plugin of diff.extra) {
|
|
99
|
+
lines.push(`${symbols.extra} ${plugin.id} ${colors.warning('(not in config)')}`);
|
|
100
|
+
}
|
|
101
|
+
const summary = [
|
|
102
|
+
diff.present.length > 0 ? colors.success(`${diff.present.length} present`) : null,
|
|
103
|
+
diff.missing.length > 0 ? colors.error(`${diff.missing.length} missing`) : null,
|
|
104
|
+
diff.extra.length > 0 ? colors.warning(`${diff.extra.length} extra`) : null,
|
|
105
|
+
].filter(Boolean).join(', ');
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push(summary || 'No plugins configured');
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
export function runStatus(cwd, options = {}) {
|
|
111
|
+
// Check global status
|
|
112
|
+
const settingsPath = getClaudeSettingsPath();
|
|
113
|
+
const settings = readClaudeSettings(settingsPath);
|
|
114
|
+
const hookStatus = getFitoutHookStatus(settings);
|
|
115
|
+
const skillInstalled = hasFitoutSkill();
|
|
116
|
+
const configPath = findConfigPath(cwd);
|
|
117
|
+
if (!configPath) {
|
|
118
|
+
// No project config - show global status and hint
|
|
119
|
+
const globalStatus = formatGlobalStatus({
|
|
120
|
+
hookStatus,
|
|
121
|
+
skillInstalled,
|
|
122
|
+
profiles: [],
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
output: `${globalStatus}\n\nNo project config. Run \`fitout init\` to create one.`,
|
|
126
|
+
exitCode: 1,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
130
|
+
// Refresh marketplaces if requested
|
|
131
|
+
if (options.refresh) {
|
|
132
|
+
console.log('Refreshing marketplaces...');
|
|
133
|
+
refreshMarketplaces();
|
|
134
|
+
}
|
|
135
|
+
let configContent;
|
|
136
|
+
let config;
|
|
137
|
+
try {
|
|
138
|
+
configContent = readFileSync(configPath, 'utf-8');
|
|
139
|
+
config = parseConfig(configContent);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return {
|
|
143
|
+
output: `Failed to read config: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
|
144
|
+
exitCode: 1,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Resolve profiles
|
|
148
|
+
const profilesDir = getProfilesDir();
|
|
149
|
+
const resolution = resolveProfiles(profilesDir, config);
|
|
150
|
+
if (resolution.errors.length > 0) {
|
|
151
|
+
return {
|
|
152
|
+
output: `${colors.header('Profile errors:')}\n${resolution.errors.map((e) => ` ${symbols.missing} ${e}`).join('\n')}`,
|
|
153
|
+
exitCode: 1,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Get list of profiles being used
|
|
157
|
+
const profiles = config.profiles || [];
|
|
158
|
+
const installed = listPlugins();
|
|
159
|
+
const available = listAvailablePlugins();
|
|
160
|
+
const diff = diffPluginsResolved(resolution.plugins, installed, projectRoot);
|
|
161
|
+
const outdated = findOutdatedPlugins(installed, available, projectRoot);
|
|
162
|
+
const statusDiff = {
|
|
163
|
+
...diff,
|
|
164
|
+
outdated,
|
|
165
|
+
};
|
|
166
|
+
const showRefreshTip = !options.refresh;
|
|
167
|
+
const contextLine = formatContextLine(projectRoot, cwd);
|
|
168
|
+
// Format global status
|
|
169
|
+
const globalStatus = formatGlobalStatus({
|
|
170
|
+
hookStatus,
|
|
171
|
+
skillInstalled,
|
|
172
|
+
profiles,
|
|
173
|
+
});
|
|
174
|
+
const formatted = formatStatusResolved(statusDiff, showRefreshTip, resolution.constraintOverrides);
|
|
175
|
+
return {
|
|
176
|
+
output: `${globalStatus}\n\n${contextLine}${colors.header('Plugins:')}\n${formatted}`,
|
|
177
|
+
exitCode: diff.missing.length > 0 ? 1 : 0,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/test-utils.ts
|
|
2
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { vi } from 'vitest';
|
|
5
|
+
// Project-local temp directory (gitignored)
|
|
6
|
+
const TEST_TMP_ROOT = join(import.meta.dirname, '..', '.test-tmp');
|
|
7
|
+
let testCounter = 0;
|
|
8
|
+
export function setupTestEnv() {
|
|
9
|
+
const testId = `${Date.now()}-${++testCounter}`;
|
|
10
|
+
const baseDir = join(TEST_TMP_ROOT, testId);
|
|
11
|
+
const claudeHome = join(baseDir, '.claude');
|
|
12
|
+
const fitoutHome = join(baseDir, '.config', 'fitout');
|
|
13
|
+
mkdirSync(claudeHome, { recursive: true });
|
|
14
|
+
mkdirSync(fitoutHome, { recursive: true });
|
|
15
|
+
vi.stubEnv('CLAUDE_CONFIG_DIR', claudeHome);
|
|
16
|
+
vi.stubEnv('FITOUT_CONFIG_HOME', fitoutHome);
|
|
17
|
+
return {
|
|
18
|
+
claudeHome,
|
|
19
|
+
fitoutHome,
|
|
20
|
+
baseDir,
|
|
21
|
+
cleanup: () => {
|
|
22
|
+
vi.unstubAllEnvs();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function cleanTestTmp() {
|
|
27
|
+
try {
|
|
28
|
+
rmSync(TEST_TMP_ROOT, { recursive: true, force: true, maxRetries: 3 });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Ignore errors - directory might be in use by parallel tests
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { InstalledPlugin } from './claude.js';
|
|
2
|
+
import { AvailablePlugin } from './marketplace.js';
|
|
3
|
+
export interface OutdatedPlugin {
|
|
4
|
+
id: string;
|
|
5
|
+
installedVersion: string;
|
|
6
|
+
availableVersion: string;
|
|
7
|
+
scope: 'local' | 'user' | 'global';
|
|
8
|
+
projectPath?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compare semver versions. Returns:
|
|
12
|
+
* -1 if a < b
|
|
13
|
+
* 0 if a == b
|
|
14
|
+
* 1 if a > b
|
|
15
|
+
*/
|
|
16
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
17
|
+
export declare function findOutdatedPlugins(installed: InstalledPlugin[], available: AvailablePlugin[], projectPath: string): OutdatedPlugin[];
|
|
18
|
+
export declare function updatePlugin(pluginId: string, scope?: 'local' | 'user'): void;
|
|
19
|
+
export interface UpdateOptions {
|
|
20
|
+
refresh?: boolean;
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface UpdateResult {
|
|
24
|
+
output: string;
|
|
25
|
+
exitCode: number;
|
|
26
|
+
pluginsToUpdate: OutdatedPlugin[];
|
|
27
|
+
}
|
|
28
|
+
export declare function runUpdate(projectPath: string, installed: InstalledPlugin[], available: AvailablePlugin[], pluginIds: string[], options?: UpdateOptions): UpdateResult;
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Compare semver versions. Returns:
|
|
4
|
+
* -1 if a < b
|
|
5
|
+
* 0 if a == b
|
|
6
|
+
* 1 if a > b
|
|
7
|
+
*/
|
|
8
|
+
export function compareVersions(a, b) {
|
|
9
|
+
const parseVersion = (v) => {
|
|
10
|
+
return v.split('.').map((n) => parseInt(n, 10) || 0);
|
|
11
|
+
};
|
|
12
|
+
const aParts = parseVersion(a);
|
|
13
|
+
const bParts = parseVersion(b);
|
|
14
|
+
const maxLen = Math.max(aParts.length, bParts.length);
|
|
15
|
+
for (let i = 0; i < maxLen; i++) {
|
|
16
|
+
const aVal = aParts[i] || 0;
|
|
17
|
+
const bVal = bParts[i] || 0;
|
|
18
|
+
if (aVal < bVal)
|
|
19
|
+
return -1;
|
|
20
|
+
if (aVal > bVal)
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
export function findOutdatedPlugins(installed, available, projectPath) {
|
|
26
|
+
const availableMap = new Map(available.map((p) => [p.id, p]));
|
|
27
|
+
const outdated = [];
|
|
28
|
+
// Filter to local plugins for this project
|
|
29
|
+
const localPlugins = installed.filter((p) => p.scope === 'local' && p.projectPath === projectPath);
|
|
30
|
+
for (const plugin of localPlugins) {
|
|
31
|
+
const avail = availableMap.get(plugin.id);
|
|
32
|
+
if (!avail)
|
|
33
|
+
continue;
|
|
34
|
+
// Skip if either version is missing
|
|
35
|
+
if (!plugin.version || !avail.version)
|
|
36
|
+
continue;
|
|
37
|
+
if (compareVersions(plugin.version, avail.version) < 0) {
|
|
38
|
+
outdated.push({
|
|
39
|
+
id: plugin.id,
|
|
40
|
+
installedVersion: plugin.version,
|
|
41
|
+
availableVersion: avail.version,
|
|
42
|
+
scope: plugin.scope,
|
|
43
|
+
projectPath: plugin.projectPath,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return outdated;
|
|
48
|
+
}
|
|
49
|
+
export function updatePlugin(pluginId, scope = 'local') {
|
|
50
|
+
execFileSync('claude', ['plugin', 'update', pluginId, '--scope', scope], {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function runUpdate(projectPath, installed, available, pluginIds, options = {}) {
|
|
56
|
+
const outdated = findOutdatedPlugins(installed, available, projectPath);
|
|
57
|
+
// Filter to specific plugins if provided
|
|
58
|
+
const toUpdate = pluginIds.length > 0
|
|
59
|
+
? outdated.filter((p) => pluginIds.includes(p.id))
|
|
60
|
+
: outdated;
|
|
61
|
+
// Check if any specified plugins weren't found
|
|
62
|
+
if (pluginIds.length > 0) {
|
|
63
|
+
const outdatedIds = new Set(outdated.map((p) => p.id));
|
|
64
|
+
const installedIds = new Set(installed
|
|
65
|
+
.filter((p) => p.scope === 'local' && p.projectPath === projectPath)
|
|
66
|
+
.map((p) => p.id));
|
|
67
|
+
for (const id of pluginIds) {
|
|
68
|
+
if (!installedIds.has(id)) {
|
|
69
|
+
return {
|
|
70
|
+
output: `Error: Plugin "${id}" not installed`,
|
|
71
|
+
exitCode: 1,
|
|
72
|
+
pluginsToUpdate: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!outdatedIds.has(id)) {
|
|
76
|
+
const plugin = installed.find((p) => p.id === id && p.scope === 'local' && p.projectPath === projectPath);
|
|
77
|
+
return {
|
|
78
|
+
output: `✓ ${id} is already up-to-date (v${plugin?.version})`,
|
|
79
|
+
exitCode: 0,
|
|
80
|
+
pluginsToUpdate: [],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (toUpdate.length === 0) {
|
|
86
|
+
return {
|
|
87
|
+
output: 'All plugins are up-to-date.',
|
|
88
|
+
exitCode: 0,
|
|
89
|
+
pluginsToUpdate: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (options.dryRun) {
|
|
93
|
+
const lines = [`Would update ${toUpdate.length} plugin${toUpdate.length > 1 ? 's' : ''}:`];
|
|
94
|
+
for (const plugin of toUpdate) {
|
|
95
|
+
lines.push(` ↑ ${plugin.id} v${plugin.installedVersion} → v${plugin.availableVersion}`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
output: lines.join('\n'),
|
|
99
|
+
exitCode: 0,
|
|
100
|
+
pluginsToUpdate: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
output: `Updating ${toUpdate.length} outdated plugin${toUpdate.length > 1 ? 's' : ''}...`,
|
|
105
|
+
exitCode: 0,
|
|
106
|
+
pluginsToUpdate: toUpdate,
|
|
107
|
+
};
|
|
108
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fitout",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Context-aware plugin manager for Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fitout": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"dev": "tsx src/cli.ts"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"claude",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"plugins",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"author": "Josh Nichols",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/technicalpickles/fitout.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/technicalpickles/fitout#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/technicalpickles/fitout/issues"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@pnpm/tabtab": "^0.5.4",
|
|
36
|
+
"chalk": "^5.6.2",
|
|
37
|
+
"commander": "^14.0.2",
|
|
38
|
+
"smol-toml": "^1.6.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.1.0",
|
|
42
|
+
"tsx": "^4.21.0",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
45
|
+
}
|
|
46
|
+
}
|