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.
@@ -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;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function confirm(question: string, defaultValue?: boolean): Promise<boolean>;
2
+ export declare function input(question: string, defaultValue?: string): Promise<string>;
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
+ }
@@ -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,8 @@
1
+ export interface TestContext {
2
+ claudeHome: string;
3
+ fitoutHome: string;
4
+ baseDir: string;
5
+ cleanup: () => void;
6
+ }
7
+ export declare function setupTestEnv(): TestContext;
8
+ export declare function cleanTestTmp(): void;
@@ -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
+ }
@@ -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
+ }