@ai-outfitter/outfitter 0.11.0 → 1.0.2
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/.outfitter/skills/outfitter/SKILL.md +67 -37
- package/README.md +63 -35
- package/code/enterprise/cli/privateCatalogSettings.cjs +2 -2
- package/code/enterprise/pi-extension/privateCatalogOnboarding.js +5 -8
- package/code/enterprise/shared/privateCatalogPolicy.cjs +5 -5
- package/code/pi-extension/src/outfitter-extension.js +353 -553
- package/code/pi-extension/src/outfitter-runtime-extension.js +160 -0
- package/dist/agents/AgentLaunch.d.ts +9 -1
- package/dist/agents/AgentLaunch.js +19 -0
- package/dist/agents/AgentLaunch.js.map +1 -1
- package/dist/agents/PiCredentialPersistence.d.ts +6 -0
- package/dist/agents/PiCredentialPersistence.js +33 -0
- package/dist/agents/PiCredentialPersistence.js.map +1 -0
- package/dist/cli/OutfitterCli.js +10 -18
- package/dist/cli/OutfitterCli.js.map +1 -1
- package/dist/cli/commands/CommandObject.d.ts +0 -5
- package/dist/cli/commands/CommandObject.js +1 -4
- package/dist/cli/commands/CommandObject.js.map +1 -1
- package/dist/cli/commands/DumpCommand.d.ts +19 -0
- package/dist/cli/commands/DumpCommand.js +54 -0
- package/dist/cli/commands/DumpCommand.js.map +1 -0
- package/dist/cli/commands/ListCommand.d.ts +17 -0
- package/dist/cli/commands/ListCommand.js +77 -0
- package/dist/cli/commands/ListCommand.js.map +1 -0
- package/dist/cli/commands/PiRuntimeLaunch.d.ts +8 -0
- package/dist/cli/commands/PiRuntimeLaunch.js +47 -0
- package/dist/cli/commands/PiRuntimeLaunch.js.map +1 -0
- package/dist/cli/commands/ProcessDefaults.d.ts +2 -0
- package/dist/cli/commands/ProcessDefaults.js +5 -0
- package/dist/cli/commands/ProcessDefaults.js.map +1 -0
- package/dist/cli/commands/RunAgentCommand.d.ts +46 -0
- package/dist/cli/commands/RunAgentCommand.js +176 -0
- package/dist/cli/commands/RunAgentCommand.js.map +1 -0
- package/dist/cli/commands/SetupCommand.d.ts +38 -6
- package/dist/cli/commands/SetupCommand.js +194 -232
- package/dist/cli/commands/SetupCommand.js.map +1 -1
- package/dist/cli/commands/ValidateCommand.d.ts +20 -0
- package/dist/cli/commands/ValidateCommand.js +54 -0
- package/dist/cli/commands/ValidateCommand.js.map +1 -0
- package/dist/composer/Composer.d.ts +11 -0
- package/dist/composer/Composer.js +72 -0
- package/dist/composer/Composer.js.map +1 -0
- package/dist/composer/Composition.d.ts +33 -0
- package/dist/composer/Composition.js +2 -0
- package/dist/composer/Composition.js.map +1 -0
- package/dist/dump/Containment.d.ts +8 -0
- package/dist/dump/Containment.js +22 -0
- package/dist/dump/Containment.js.map +1 -0
- package/dist/dump/Dump.d.ts +8 -0
- package/dist/dump/Dump.js +182 -0
- package/dist/dump/Dump.js.map +1 -0
- package/dist/extensions/PiExtensionCache.d.ts +30 -0
- package/dist/extensions/PiExtensionCache.js +92 -0
- package/dist/extensions/PiExtensionCache.js.map +1 -0
- package/dist/fs/TypeConflict.d.ts +6 -0
- package/dist/fs/TypeConflict.js +21 -0
- package/dist/fs/TypeConflict.js.map +1 -0
- package/dist/paths/OutfitterCache.d.ts +6 -0
- package/dist/paths/OutfitterCache.js +17 -0
- package/dist/paths/OutfitterCache.js.map +1 -0
- package/dist/projection/Materialize.d.ts +24 -0
- package/dist/projection/Materialize.js +83 -0
- package/dist/projection/Materialize.js.map +1 -0
- package/dist/projection/ProjectHarness.d.ts +5 -0
- package/dist/projection/ProjectHarness.js +77 -0
- package/dist/projection/ProjectHarness.js.map +1 -0
- package/dist/projection/Projection.d.ts +23 -0
- package/dist/projection/Projection.js +2 -0
- package/dist/projection/Projection.js.map +1 -0
- package/dist/resolver/AgentDefinition.d.ts +30 -0
- package/dist/resolver/AgentDefinition.js +123 -0
- package/dist/resolver/AgentDefinition.js.map +1 -0
- package/dist/resolver/Layer.d.ts +12 -0
- package/dist/resolver/Layer.js +30 -0
- package/dist/resolver/Layer.js.map +1 -0
- package/dist/resolver/Resolver.d.ts +3 -0
- package/dist/resolver/Resolver.js +163 -0
- package/dist/resolver/Resolver.js.map +1 -0
- package/dist/resolver/ResolverContext.d.ts +15 -0
- package/dist/resolver/ResolverContext.js +11 -0
- package/dist/resolver/ResolverContext.js.map +1 -0
- package/dist/resolver/ResolverValidation.d.ts +11 -0
- package/dist/resolver/ResolverValidation.js +112 -0
- package/dist/resolver/ResolverValidation.js.map +1 -0
- package/dist/resolver/Resource.d.ts +88 -0
- package/dist/resolver/Resource.js +34 -0
- package/dist/resolver/Resource.js.map +1 -0
- package/dist/schemas/agent.schema.json +48 -0
- package/dist/schemas/settings.schema.json +32 -9
- package/dist/settings/Settings.d.ts +34 -5
- package/dist/settings/Settings.js +3 -1
- package/dist/settings/Settings.js.map +1 -1
- package/dist/settings/SettingsLoader.d.ts +1 -1
- package/dist/settings/SettingsLoader.js +23 -22
- package/dist/settings/SettingsLoader.js.map +1 -1
- package/dist/settings/SettingsMerger.js +12 -10
- package/dist/settings/SettingsMerger.js.map +1 -1
- package/dist/setup/DefaultCatalog.d.ts +20 -0
- package/dist/setup/DefaultCatalog.js +89 -0
- package/dist/setup/DefaultCatalog.js.map +1 -0
- package/dist/setup/Setup.d.ts +43 -0
- package/dist/setup/Setup.js +261 -0
- package/dist/setup/Setup.js.map +1 -0
- package/dist/skills/SkillDocument.d.ts +6 -1
- package/dist/skills/SkillDocument.js.map +1 -1
- package/dist/sources/SourceCache.d.ts +19 -0
- package/dist/{profiles/ProfileCache.js → sources/SourceCache.js} +22 -18
- package/dist/sources/SourceCache.js.map +1 -0
- package/dist/validation/SchemaValidator.d.ts +1 -1
- package/dist/validation/SchemaValidator.js +4 -13
- package/dist/validation/SchemaValidator.js.map +1 -1
- package/docs/architecture/state_writeback_strategy.md +54 -122
- package/docs/documentation/README.md +33 -12
- package/docs/documentation/actions.md +35 -52
- package/docs/documentation/agents.md +109 -0
- package/docs/documentation/best-practices.md +25 -63
- package/docs/documentation/catalogs.md +126 -0
- package/docs/documentation/cli.md +39 -41
- package/docs/documentation/concepts.md +66 -23
- package/docs/documentation/dump-and-bake.md +30 -0
- package/docs/documentation/first-time-cli-agent-users.md +8 -8
- package/docs/documentation/getting-started.md +28 -8
- package/docs/documentation/hooks.md +20 -0
- package/docs/documentation/iterating-on-profiles.md +56 -70
- package/docs/documentation/local-development.md +84 -0
- package/docs/documentation/migration.md +39 -0
- package/docs/documentation/personas.md +41 -0
- package/docs/documentation/porting-claude.md +54 -0
- package/docs/documentation/profiles.md +16 -169
- package/docs/documentation/settings.md +61 -0
- package/docs/documentation/skills.md +93 -334
- package/docs/documentation/state.md +24 -62
- package/docs/documentation/subagents.md +37 -0
- package/docs/documentation/support-matrix.md +40 -35
- package/docs/documentation/switching-to-outfitter.md +75 -81
- package/docs/documentation/tasks.md +13 -0
- package/docs/documentation/usecases/engineering.md +67 -84
- package/docs/documentation/usecases/organization-profile-catalog.md +83 -111
- package/docs/documentation/usecases/persona-reviews.md +133 -139
- package/docs/philosophy.md +2 -2
- package/package.json +3 -3
- package/src/schemas/agent.schema.json +48 -0
- package/src/schemas/settings.schema.json +32 -9
- package/dist/agents/AdapterProfileControls.d.ts +0 -21
- package/dist/agents/AdapterProfileControls.js +0 -76
- package/dist/agents/AdapterProfileControls.js.map +0 -1
- package/dist/agents/AdapterStatePaths.d.ts +0 -12
- package/dist/agents/AdapterStatePaths.js +0 -46
- package/dist/agents/AdapterStatePaths.js.map +0 -1
- package/dist/agents/AgentAdapter.d.ts +0 -44
- package/dist/agents/AgentAdapter.js +0 -2
- package/dist/agents/AgentAdapter.js.map +0 -1
- package/dist/agents/AgentRegistry.d.ts +0 -6
- package/dist/agents/AgentRegistry.js +0 -17
- package/dist/agents/AgentRegistry.js.map +0 -1
- package/dist/agents/LaunchResources.d.ts +0 -17
- package/dist/agents/LaunchResources.js +0 -61
- package/dist/agents/LaunchResources.js.map +0 -1
- package/dist/agents/OutfitterSkill.d.ts +0 -11
- package/dist/agents/OutfitterSkill.js +0 -128
- package/dist/agents/OutfitterSkill.js.map +0 -1
- package/dist/agents/ResourceIdentity.d.ts +0 -2
- package/dist/agents/ResourceIdentity.js +0 -51
- package/dist/agents/ResourceIdentity.js.map +0 -1
- package/dist/agents/claude/ClaudeAdapter.d.ts +0 -2
- package/dist/agents/claude/ClaudeAdapter.js +0 -148
- package/dist/agents/claude/ClaudeAdapter.js.map +0 -1
- package/dist/agents/claude/ClaudeCompositeProfileWriter.d.ts +0 -5
- package/dist/agents/claude/ClaudeCompositeProfileWriter.js +0 -7
- package/dist/agents/claude/ClaudeCompositeProfileWriter.js.map +0 -1
- package/dist/agents/pi/PiAdapter.d.ts +0 -2
- package/dist/agents/pi/PiAdapter.js +0 -363
- package/dist/agents/pi/PiAdapter.js.map +0 -1
- package/dist/agents/pi/PiArgs.d.ts +0 -2
- package/dist/agents/pi/PiArgs.js +0 -15
- package/dist/agents/pi/PiArgs.js.map +0 -1
- package/dist/agents/pi/PiCompositeProfileWriter.d.ts +0 -5
- package/dist/agents/pi/PiCompositeProfileWriter.js +0 -7
- package/dist/agents/pi/PiCompositeProfileWriter.js.map +0 -1
- package/dist/agents/pi/PiExtensionCache.d.ts +0 -12
- package/dist/agents/pi/PiExtensionCache.js +0 -195
- package/dist/agents/pi/PiExtensionCache.js.map +0 -1
- package/dist/agents/pi/PiMcpConfig.d.ts +0 -2
- package/dist/agents/pi/PiMcpConfig.js +0 -114
- package/dist/agents/pi/PiMcpConfig.js.map +0 -1
- package/dist/agents/pi/PiSettingsMergePolicy.d.ts +0 -17
- package/dist/agents/pi/PiSettingsMergePolicy.js +0 -59
- package/dist/agents/pi/PiSettingsMergePolicy.js.map +0 -1
- package/dist/agents/pi/PiSkillSources.d.ts +0 -8
- package/dist/agents/pi/PiSkillSources.js +0 -73
- package/dist/agents/pi/PiSkillSources.js.map +0 -1
- package/dist/cli/commands/FirstRunWelcomeProfile.d.ts +0 -11
- package/dist/cli/commands/FirstRunWelcomeProfile.js +0 -110
- package/dist/cli/commands/FirstRunWelcomeProfile.js.map +0 -1
- package/dist/cli/commands/PiLoginLaunch.d.ts +0 -22
- package/dist/cli/commands/PiLoginLaunch.js +0 -170
- package/dist/cli/commands/PiLoginLaunch.js.map +0 -1
- package/dist/cli/commands/RunCommand.d.ts +0 -36
- package/dist/cli/commands/RunCommand.js +0 -345
- package/dist/cli/commands/RunCommand.js.map +0 -1
- package/dist/cli/commands/SyncCommand.d.ts +0 -46
- package/dist/cli/commands/SyncCommand.js +0 -244
- package/dist/cli/commands/SyncCommand.js.map +0 -1
- package/dist/cli/commands/WelcomeCommand.d.ts +0 -56
- package/dist/cli/commands/WelcomeCommand.js +0 -224
- package/dist/cli/commands/WelcomeCommand.js.map +0 -1
- package/dist/cli/commands/assets/outfitter-ascii.txt +0 -5
- package/dist/cli/commands/profile/Command.d.ts +0 -7
- package/dist/cli/commands/profile/Command.js +0 -24
- package/dist/cli/commands/profile/Command.js.map +0 -1
- package/dist/cli/commands/profile/CreateCommand.d.ts +0 -19
- package/dist/cli/commands/profile/CreateCommand.js +0 -115
- package/dist/cli/commands/profile/CreateCommand.js.map +0 -1
- package/dist/cli/commands/profile/LintCommand.d.ts +0 -19
- package/dist/cli/commands/profile/LintCommand.js +0 -155
- package/dist/cli/commands/profile/LintCommand.js.map +0 -1
- package/dist/cli/commands/profile/ListCommand.d.ts +0 -19
- package/dist/cli/commands/profile/ListCommand.js +0 -91
- package/dist/cli/commands/profile/ListCommand.js.map +0 -1
- package/dist/cli/commands/profile/Shared.d.ts +0 -9
- package/dist/cli/commands/profile/Shared.js +0 -10
- package/dist/cli/commands/profile/Shared.js.map +0 -1
- package/dist/cli/commands/run/RunFirstRunOnboarding.d.ts +0 -7
- package/dist/cli/commands/run/RunFirstRunOnboarding.js +0 -52
- package/dist/cli/commands/run/RunFirstRunOnboarding.js.map +0 -1
- package/dist/cli/commands/run/RunLaunchSummary.d.ts +0 -2
- package/dist/cli/commands/run/RunLaunchSummary.js +0 -35
- package/dist/cli/commands/run/RunLaunchSummary.js.map +0 -1
- package/dist/cli/commands/run/RunProfileResolution.d.ts +0 -39
- package/dist/cli/commands/run/RunProfileResolution.js +0 -128
- package/dist/cli/commands/run/RunProfileResolution.js.map +0 -1
- package/dist/cli/commands/run/RunStateWritePrompt.d.ts +0 -2
- package/dist/cli/commands/run/RunStateWritePrompt.js +0 -29
- package/dist/cli/commands/run/RunStateWritePrompt.js.map +0 -1
- package/dist/cli/commands/setup/SetupPrompts.d.ts +0 -14
- package/dist/cli/commands/setup/SetupPrompts.js +0 -296
- package/dist/cli/commands/setup/SetupPrompts.js.map +0 -1
- package/dist/cli/commands/setup/SetupSourceImport.d.ts +0 -5
- package/dist/cli/commands/setup/SetupSourceImport.js +0 -177
- package/dist/cli/commands/setup/SetupSourceImport.js.map +0 -1
- package/dist/cli/commands/setup/SetupSourceLaunch.d.ts +0 -4
- package/dist/cli/commands/setup/SetupSourceLaunch.js +0 -65
- package/dist/cli/commands/setup/SetupSourceLaunch.js.map +0 -1
- package/dist/cli/commands/setup/SetupStarterSource.d.ts +0 -21
- package/dist/cli/commands/setup/SetupStarterSource.js +0 -133
- package/dist/cli/commands/setup/SetupStarterSource.js.map +0 -1
- package/dist/cli/commands/setup/SetupTypes.d.ts +0 -91
- package/dist/cli/commands/setup/SetupTypes.js +0 -26
- package/dist/cli/commands/setup/SetupTypes.js.map +0 -1
- package/dist/compositeProfile/CompositeProfile.d.ts +0 -8
- package/dist/compositeProfile/CompositeProfile.js +0 -6
- package/dist/compositeProfile/CompositeProfile.js.map +0 -1
- package/dist/compositeProfile/CompositeProfileAssembler.d.ts +0 -12
- package/dist/compositeProfile/CompositeProfileAssembler.js +0 -32
- package/dist/compositeProfile/CompositeProfileAssembler.js.map +0 -1
- package/dist/compositeProfile/CompositeProfileCleanup.d.ts +0 -9
- package/dist/compositeProfile/CompositeProfileCleanup.js +0 -87
- package/dist/compositeProfile/CompositeProfileCleanup.js.map +0 -1
- package/dist/compositeProfile/CompositeProfileFile.d.ts +0 -16
- package/dist/compositeProfile/CompositeProfileFile.js +0 -16
- package/dist/compositeProfile/CompositeProfileFile.js.map +0 -1
- package/dist/compositeProfile/CompositeProfileTemplate.d.ts +0 -15
- package/dist/compositeProfile/CompositeProfileTemplate.js +0 -65
- package/dist/compositeProfile/CompositeProfileTemplate.js.map +0 -1
- package/dist/compositeProfile/CompositeProfileWatcher.d.ts +0 -18
- package/dist/compositeProfile/CompositeProfileWatcher.js +0 -46
- package/dist/compositeProfile/CompositeProfileWatcher.js.map +0 -1
- package/dist/compositeProfile/StatePersistence.d.ts +0 -39
- package/dist/compositeProfile/StatePersistence.js +0 -249
- package/dist/compositeProfile/StatePersistence.js.map +0 -1
- package/dist/fs/SafeSymlink.d.ts +0 -13
- package/dist/fs/SafeSymlink.js +0 -50
- package/dist/fs/SafeSymlink.js.map +0 -1
- package/dist/profiles/Profile.d.ts +0 -60
- package/dist/profiles/Profile.js +0 -7
- package/dist/profiles/Profile.js.map +0 -1
- package/dist/profiles/ProfileCache.d.ts +0 -8
- package/dist/profiles/ProfileCache.js.map +0 -1
- package/dist/profiles/ProfileLoader.d.ts +0 -28
- package/dist/profiles/ProfileLoader.js +0 -299
- package/dist/profiles/ProfileLoader.js.map +0 -1
- package/dist/profiles/ProfileMerger.d.ts +0 -19
- package/dist/profiles/ProfileMerger.js +0 -112
- package/dist/profiles/ProfileMerger.js.map +0 -1
- package/dist/profiles/ProfileSource.d.ts +0 -35
- package/dist/profiles/ProfileSource.js +0 -13
- package/dist/profiles/ProfileSource.js.map +0 -1
- package/dist/profiles/PromptIncludes.d.ts +0 -32
- package/dist/profiles/PromptIncludes.js +0 -147
- package/dist/profiles/PromptIncludes.js.map +0 -1
- package/dist/prompts/SystemPromptExport.d.ts +0 -16
- package/dist/prompts/SystemPromptExport.js +0 -81
- package/dist/prompts/SystemPromptExport.js.map +0 -1
- package/dist/schemas/profile-source.schema.json +0 -29
- package/dist/schemas/profile.schema.json +0 -200
- package/dist/skills/ProfileSkillResolution.d.ts +0 -21
- package/dist/skills/ProfileSkillResolution.js +0 -88
- package/dist/skills/ProfileSkillResolution.js.map +0 -1
- package/dist/skills/SkillCatalog.d.ts +0 -41
- package/dist/skills/SkillCatalog.js +0 -119
- package/dist/skills/SkillCatalog.js.map +0 -1
- package/dist/skills/SkillResolution.d.ts +0 -34
- package/dist/skills/SkillResolution.js +0 -369
- package/dist/skills/SkillResolution.js.map +0 -1
- package/docs/documentation/profile-repository.md +0 -179
- package/src/schemas/SchemaDocument.ts +0 -20
- package/src/schemas/profile-source.schema.json +0 -29
- package/src/schemas/profile.schema.json +0 -200
|
@@ -1,190 +1,100 @@
|
|
|
1
|
-
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const OUTFITTER_ASCII_GRADIENT = [
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
|
2
|
+
|
|
3
|
+
// The CLI stamps these placeholders into an isolated copy before Pi loads it.
|
|
4
|
+
const OUTFITTER_HOME = '__OUTFITTER_HOME__';
|
|
5
|
+
const OUTFITTER_PROJECT = '__OUTFITTER_PROJECT__';
|
|
6
|
+
const OUTFITTER_SETUP_RESULT_PATH = '__OUTFITTER_SETUP_RESULT_PATH__';
|
|
7
|
+
const OUTFITTER_AGENT_CHOICES = '__OUTFITTER_AGENT_CHOICES__';
|
|
8
|
+
const OUTFITTER_CURRENT_DEFAULT = '__OUTFITTER_CURRENT_DEFAULT__';
|
|
9
|
+
const OUTFITTER_SETUP_SOURCE_URI = '__OUTFITTER_SETUP_SOURCE_URI__';
|
|
10
|
+
const OUTFITTER_AUTO_OPEN = '__OUTFITTER_AUTO_OPEN__';
|
|
11
|
+
const OUTFITTER_ASCII_ART = [
|
|
12
|
+
'▄▄▄▄▄▄▄ ▄▄▄ ▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄',
|
|
13
|
+
'███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ▀▀▀ ███ ███',
|
|
14
|
+
'███ ███ ███ ███ ███ ███▄▄ ███ ███ ███ ███▀ ███▄██▀',
|
|
15
|
+
'███▄███ ███▄███ ███ ███ ▄▄███▄▄ ███ ███ ███▄███ ███ ███',
|
|
16
|
+
'▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀ ▀▀▀ ▀▀▀▀▀▀▀ ▀▀▀ ▀▀▀',
|
|
17
|
+
].join('\n');
|
|
18
|
+
const OUTFITTER_ASCII_GRADIENT = ['success', 'accent', 'text', 'muted', 'dim'];
|
|
19
|
+
// Must match the CLI's agent-slug rule (Setup.ts `agentSlugPattern`) exactly, so any id the
|
|
20
|
+
// walkthrough accepts also passes `applySetupSelection`'s validation instead of crashing after setup.
|
|
21
|
+
const OUTFITTER_PROFILE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
22
|
+
const OUTFITTER_PLAN_TOOLS = ['read', 'grep', 'find', 'ls'];
|
|
23
|
+
const OUTFITTER_DEFAULT_TOOLS = ['read', 'bash', 'edit', 'write'];
|
|
24
|
+
const loadPrivateCatalogOnboarding = () => import('./pi-extension/privateCatalogOnboarding.js');
|
|
21
25
|
|
|
22
26
|
export default function outfitter(pi) {
|
|
23
|
-
let
|
|
27
|
+
let onboardingRunning = false;
|
|
28
|
+
let mode = 'build';
|
|
24
29
|
let buildModeTools;
|
|
25
|
-
let loginSubmitted = false;
|
|
26
30
|
|
|
27
31
|
const updateModeStatus = (ctx) => {
|
|
28
|
-
const color = mode ===
|
|
29
|
-
ctx.ui.setStatus(
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const updateProfileStatus = (ctx) => {
|
|
33
|
-
// Stays a string when the placeholder is unstamped (file loaded outside an
|
|
34
|
-
// Outfitter launch) and `undefined` when the launch had no resolved profile.
|
|
35
|
-
if (typeof OUTFITTER_ACTIVE_PROFILE !== "object" || OUTFITTER_ACTIVE_PROFILE === null) return;
|
|
36
|
-
const name = OUTFITTER_ACTIVE_PROFILE.label ?? OUTFITTER_ACTIVE_PROFILE.id;
|
|
37
|
-
if (!name) return;
|
|
38
|
-
ctx.ui.setStatus("outfitter-profile", ctx.ui.theme.fg("muted", "profile: " + name));
|
|
32
|
+
const color = mode === 'plan' ? 'warning' : 'muted';
|
|
33
|
+
ctx.ui.setStatus('outfitter-mode', ctx.ui.theme.fg(color, 'mode: ' + mode));
|
|
39
34
|
};
|
|
40
35
|
|
|
41
36
|
const enterPlanMode = (ctx) => {
|
|
42
|
-
if (mode !==
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
mode = "plan";
|
|
37
|
+
if (mode !== 'plan') buildModeTools = pi.getActiveTools();
|
|
38
|
+
mode = 'plan';
|
|
46
39
|
const availableTools = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
47
40
|
const planTools = OUTFITTER_PLAN_TOOLS.filter((toolName) => availableTools.has(toolName));
|
|
48
41
|
pi.setActiveTools(planTools.length > 0 ? planTools : OUTFITTER_PLAN_TOOLS);
|
|
49
42
|
updateModeStatus(ctx);
|
|
50
|
-
ctx.ui.notify(
|
|
43
|
+
ctx.ui.notify('Outfitter mode: plan (read-only tools; Shift+Tab to switch back)', 'info');
|
|
51
44
|
};
|
|
52
45
|
|
|
53
46
|
const enterBuildMode = (ctx) => {
|
|
54
|
-
mode =
|
|
47
|
+
mode = 'build';
|
|
55
48
|
pi.setActiveTools(buildModeTools ?? OUTFITTER_DEFAULT_TOOLS);
|
|
56
49
|
buildModeTools = undefined;
|
|
57
50
|
updateModeStatus(ctx);
|
|
58
|
-
ctx.ui.notify(
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const cycleOutfitterMode = (ctx) => {
|
|
62
|
-
if (mode === "plan") {
|
|
63
|
-
enterBuildMode(ctx);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
enterPlanMode(ctx);
|
|
51
|
+
ctx.ui.notify('Outfitter mode: build (normal tools; Shift+Tab for plan mode)', 'info');
|
|
68
52
|
};
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
if (!OUTFITTER_AUTO_OPEN || event.cwd !== OUTFITTER_PROJECT) {
|
|
72
|
-
return { trusted: "undecided" };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { trusted: "yes", remember: true };
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const exportRuntimeSystemPrompt = async (ctx) => {
|
|
79
|
-
const outputPath = typeof process === "undefined" ? undefined : process.env.OUTFITTER_SYSTEM_PROMPT_EXPORT_PATH;
|
|
80
|
-
if (!outputPath || typeof ctx.getSystemPrompt !== "function") return;
|
|
54
|
+
const cycleOutfitterMode = (ctx) => (mode === 'plan' ? enterBuildMode(ctx) : enterPlanMode(ctx));
|
|
81
55
|
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
const [{ mkdirSync, writeFileSync }, { dirname }] = await Promise.all([import("node:fs"), import("node:path")]);
|
|
86
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
87
|
-
writeFileSync(
|
|
88
|
-
outputPath,
|
|
89
|
-
[
|
|
90
|
-
"<!-- Generated by Outfitter from Pi runtime ctx.getSystemPrompt(). Safe to review or git-ignore. Do not edit by hand. -->",
|
|
91
|
-
"# Generated Pi runtime system prompt",
|
|
92
|
-
"",
|
|
93
|
-
systemPrompt,
|
|
94
|
-
"",
|
|
95
|
-
].join("\n"),
|
|
96
|
-
);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const submitSlashCommand = async (ctx, command, notification) => {
|
|
100
|
-
if (ctx.mode !== "tui") return false;
|
|
56
|
+
const submitSlashCommand = async (ctx, command) => {
|
|
57
|
+
if (ctx.mode !== 'tui') return false;
|
|
101
58
|
ctx.ui.setEditorText(command);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
invalidate: () => undefined,
|
|
112
|
-
};
|
|
113
|
-
}, { overlay: true, overlayOptions: { nonCapturing: true, visible: () => false } });
|
|
114
|
-
return true;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const getAvailableModelCount = async (ctx) => {
|
|
118
|
-
if (ctx.modelRegistry === undefined || typeof ctx.modelRegistry.getAvailable !== "function") {
|
|
119
|
-
return ctx.model === undefined ? 0 : 1;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const available = await ctx.modelRegistry.getAvailable();
|
|
124
|
-
return Array.isArray(available) ? available.length : 0;
|
|
125
|
-
} catch {
|
|
126
|
-
return ctx.model === undefined ? 0 : 1;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const confirmModelProviderConnection = async (ctx) => {
|
|
131
|
-
if (typeof ctx.ui.custom !== "function") return true;
|
|
132
|
-
const selected = await selectDescribedOption(
|
|
133
|
-
ctx,
|
|
134
|
-
[
|
|
135
|
-
"Pi does not have a model provider connected yet.",
|
|
136
|
-
"Connect one now so Outfitter can use Pi.",
|
|
137
|
-
"Credentials stay inside Pi.",
|
|
138
|
-
],
|
|
139
|
-
[{ value: "connect", label: "Connect a model provider" }],
|
|
140
|
-
"connect",
|
|
59
|
+
await ctx.ui.custom(
|
|
60
|
+
(tui, _theme, _keybindings, done) => {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
tui.focusedComponent?.handleInput?.('\r');
|
|
63
|
+
done(true);
|
|
64
|
+
}, 25);
|
|
65
|
+
return { render: () => [], invalidate: () => undefined };
|
|
66
|
+
},
|
|
67
|
+
{ overlay: true, overlayOptions: { nonCapturing: true, visible: () => false } },
|
|
141
68
|
);
|
|
142
|
-
return
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const openLoginIfNoModels = async (ctx) => {
|
|
146
|
-
if (loginSubmitted || ctx.mode !== "tui") return;
|
|
147
|
-
const availableModelCount = await getAvailableModelCount(ctx);
|
|
148
|
-
if (availableModelCount > 0) return;
|
|
149
|
-
if (!(await confirmModelProviderConnection(ctx))) return;
|
|
150
|
-
loginSubmitted = await submitSlashCommand(ctx, "/login");
|
|
69
|
+
return true;
|
|
151
70
|
};
|
|
152
71
|
|
|
153
72
|
const createQuestionUi = (ctx) => ({
|
|
154
73
|
async selectSetupMode() {
|
|
155
74
|
const options = [
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
75
|
+
'Use the default Outfitter profile catalog',
|
|
76
|
+
'Create your own profile',
|
|
77
|
+
'Provide a different catalog to import',
|
|
159
78
|
];
|
|
160
|
-
const selected = await ctx.ui.select(
|
|
79
|
+
const selected = await ctx.ui.select('How would you like to set up Outfitter?', options);
|
|
161
80
|
if (selected === undefined) return undefined;
|
|
162
|
-
return options.indexOf(selected) === 1 ?
|
|
81
|
+
return options.indexOf(selected) === 1 ? 'create' : options.indexOf(selected) === 2 ? 'catalog' : 'default';
|
|
163
82
|
},
|
|
164
|
-
async selectInstallTarget(
|
|
83
|
+
async selectInstallTarget() {
|
|
165
84
|
const items = [
|
|
166
85
|
{
|
|
167
|
-
value:
|
|
168
|
-
label:
|
|
169
|
-
description:
|
|
86
|
+
value: 'home',
|
|
87
|
+
label: 'Home folder (~/.agents)',
|
|
88
|
+
description: 'These profiles will be available anywhere you start outfitter.',
|
|
170
89
|
},
|
|
171
90
|
{
|
|
172
|
-
value:
|
|
173
|
-
label:
|
|
174
|
-
description:
|
|
91
|
+
value: 'project',
|
|
92
|
+
label: 'Current project directory (.agents)',
|
|
93
|
+
description:
|
|
94
|
+
'These profiles will only be available in the current project directory and will compose the profiles of the same name in the home folder.',
|
|
175
95
|
},
|
|
176
96
|
];
|
|
177
|
-
|
|
178
|
-
const selected = typeof ctx.ui.custom === "function"
|
|
179
|
-
? await selectDescribedOption(ctx, title, items, "home")
|
|
180
|
-
: await ctx.ui.select(title.join("\n"), items.map((item) => item.label));
|
|
181
|
-
if (selected === undefined) return undefined;
|
|
182
|
-
const selectedValue = items.some((item) => item.value === selected)
|
|
183
|
-
? selected
|
|
184
|
-
: items.find((item) => item.label === selected)?.value;
|
|
185
|
-
return selectedValue === "project"
|
|
186
|
-
? { id: "project", settingsPath: paths.projectSettingsPath, profilesPath: paths.projectProfilesPath }
|
|
187
|
-
: { id: "home", settingsPath: paths.homeSettingsPath, profilesPath: paths.homeProfilesPath };
|
|
97
|
+
return selectFromItems(ctx, ['Where should Outfitter install these settings?'], items, 'home');
|
|
188
98
|
},
|
|
189
99
|
async selectProfile(profiles, currentDefault) {
|
|
190
100
|
const items = profiles.map((profile) => ({
|
|
@@ -193,103 +103,231 @@ export default function outfitter(pi) {
|
|
|
193
103
|
description: profile.description,
|
|
194
104
|
}));
|
|
195
105
|
const title = [
|
|
196
|
-
|
|
197
|
-
|
|
106
|
+
'Outfitter profile setup',
|
|
107
|
+
'',
|
|
198
108
|
"Choose the default profile from the selected catalog for future 'outfitter' launches.",
|
|
199
|
-
|
|
109
|
+
'The current Pi process keeps the profile it started with; this setting applies on the next launch.',
|
|
200
110
|
];
|
|
201
|
-
const initialProfileId =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
: await ctx.ui.select(title.join("\n"), items.map((item) => item.label));
|
|
111
|
+
const initialProfileId =
|
|
112
|
+
currentDefault ?? (profiles.some((profile) => profile.id === 'founder') ? 'founder' : profiles[0]?.id);
|
|
113
|
+
const selectedId = await selectFromItems(ctx, title, items, initialProfileId);
|
|
205
114
|
if (selectedId === undefined) return undefined;
|
|
206
|
-
return profiles.find((profile) => profile.id === selectedId)
|
|
115
|
+
return profiles.find((profile) => profile.id === selectedId);
|
|
116
|
+
},
|
|
117
|
+
async selectCliAgent() {
|
|
118
|
+
const items = [
|
|
119
|
+
{
|
|
120
|
+
value: 'pi',
|
|
121
|
+
label: 'Pi / Outfitter (Recommended)',
|
|
122
|
+
description: 'Use the coding agent bundled with Outfitter.',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
value: 'claude',
|
|
126
|
+
label: 'Claude Code',
|
|
127
|
+
description: "Use Anthropic's separately installed Claude Code CLI.",
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
return selectFromItems(ctx, ['Which CLI agent should Outfitter use by default?'], items, 'pi');
|
|
207
131
|
},
|
|
208
132
|
async input(message, defaultValue) {
|
|
209
|
-
if (typeof ctx.ui.input ===
|
|
133
|
+
if (typeof ctx.ui.input === 'function') {
|
|
210
134
|
return ctx.ui.input(message, defaultValue === undefined ? undefined : { defaultValue });
|
|
211
135
|
}
|
|
212
|
-
const suffix = defaultValue === undefined ?
|
|
213
|
-
|
|
214
|
-
return selected;
|
|
136
|
+
const suffix = defaultValue === undefined ? '' : ' [' + defaultValue + ']';
|
|
137
|
+
return ctx.ui.select(message + suffix, [defaultValue ?? '']);
|
|
215
138
|
},
|
|
216
139
|
async confirmPrivateCatalog(repository) {
|
|
217
140
|
const { confirmPrivateCatalog } = await loadPrivateCatalogOnboarding();
|
|
218
141
|
return confirmPrivateCatalog(ctx, selectDescribedOption, repository);
|
|
219
142
|
},
|
|
220
|
-
notify: (message, type =
|
|
143
|
+
notify: (message, type = 'info') => ctx.ui.notify(message, type),
|
|
221
144
|
});
|
|
222
145
|
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const questionUi = createQuestionUi(ctx);
|
|
229
|
-
|
|
230
|
-
if (OUTFITTER_SETUP_SOURCE_URI !== undefined) {
|
|
231
|
-
await runProvidedSourceOnboarding({ mkdirSync, writeFileSync, dirname }, paths, questionUi, OUTFITTER_SETUP_SOURCE_URI);
|
|
232
|
-
await openLoginIfNoModels(ctx);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
146
|
+
const finish = async (ctx, selection, message) => {
|
|
147
|
+
await writeSetupResult(selection);
|
|
148
|
+
ctx.ui.notify(message, 'info');
|
|
149
|
+
ctx.shutdown();
|
|
150
|
+
};
|
|
235
151
|
|
|
236
|
-
|
|
152
|
+
const cancel = (ctx) => {
|
|
153
|
+
ctx.ui.notify('Outfitter setup cancelled; no settings were changed.', 'warning');
|
|
154
|
+
ctx.shutdown();
|
|
155
|
+
};
|
|
237
156
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
157
|
+
const runDefaultCatalogOnboarding = async (ctx, questionUi) => {
|
|
158
|
+
if (OUTFITTER_AGENT_CHOICES.length === 0) {
|
|
159
|
+
questionUi.notify(
|
|
160
|
+
'No profiles were found in the default Outfitter profile catalog. Fix the catalog sync or provide a different catalog.',
|
|
161
|
+
'error',
|
|
162
|
+
);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const selectedProfile = await questionUi.selectProfile(OUTFITTER_AGENT_CHOICES, OUTFITTER_CURRENT_DEFAULT);
|
|
166
|
+
if (selectedProfile === undefined) return cancel(ctx);
|
|
167
|
+
if (!OUTFITTER_PROFILE_ID_PATTERN.test(selectedProfile.id)) {
|
|
168
|
+
questionUi.notify('Selected profile id is not filesystem-safe; no settings were changed.', 'error');
|
|
241
169
|
return;
|
|
242
170
|
}
|
|
171
|
+
const choice = await askTargetAndHarness(questionUi);
|
|
172
|
+
if (choice === undefined) return cancel(ctx);
|
|
173
|
+
const { target, harness } = choice;
|
|
174
|
+
const settingsPath = installRoot(target) + '/.agents/settings.yml';
|
|
175
|
+
await finish(
|
|
176
|
+
ctx,
|
|
177
|
+
{ setupMode: 'default', agentId: selectedProfile.id, harness, target },
|
|
178
|
+
[
|
|
179
|
+
"Outfitter saved default profile '" + selectedProfile.id + "' to " + settingsPath + '.',
|
|
180
|
+
'Profile choices were loaded from the default Outfitter profile catalog, not generated locally.',
|
|
181
|
+
"It applies on the next 'outfitter' launch; restart Outfitter to load the selected profile.",
|
|
182
|
+
].join('\n'),
|
|
183
|
+
);
|
|
184
|
+
};
|
|
243
185
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
186
|
+
const runCreateProfileOnboarding = async (ctx, questionUi) => {
|
|
187
|
+
const enteredProfileId = normalizeInputValue(await questionUi.input('Profile id', 'my_profile'));
|
|
188
|
+
const profileId = enteredProfileId?.replaceAll('_', '-');
|
|
189
|
+
if (!profileId || !OUTFITTER_PROFILE_ID_PATTERN.test(profileId)) {
|
|
190
|
+
questionUi.notify('Profile id is not filesystem-safe; no settings were changed.', 'error');
|
|
247
191
|
return;
|
|
248
192
|
}
|
|
193
|
+
const label = normalizeInputValue(await questionUi.input('Profile label', profileId));
|
|
194
|
+
const choice = await askTargetAndHarness(questionUi);
|
|
195
|
+
if (choice === undefined) return cancel(ctx);
|
|
196
|
+
const { target, harness } = choice;
|
|
197
|
+
const root = installRoot(target);
|
|
198
|
+
const profilePath = root + '/.agents/agents/' + profileId + '/agent.md';
|
|
199
|
+
const settingsPath = root + '/.agents/settings.yml';
|
|
200
|
+
await finish(
|
|
201
|
+
ctx,
|
|
202
|
+
{ setupMode: 'create', agentId: profileId, agentLabel: label, harness, target },
|
|
203
|
+
[
|
|
204
|
+
"Outfitter created profile '" + profileId + "' at " + profilePath + '.',
|
|
205
|
+
'Outfitter saved settings to ' + settingsPath + '.',
|
|
206
|
+
"It applies on the next 'outfitter' launch; restart Outfitter to load the selected profile.",
|
|
207
|
+
].join('\n'),
|
|
208
|
+
);
|
|
209
|
+
};
|
|
249
210
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
await
|
|
211
|
+
const runRemoteSettingsOnboarding = async (ctx, questionUi) => {
|
|
212
|
+
const github = normalizeInputValue(
|
|
213
|
+
await questionUi.input('GitHub catalog repo (owner/repo)', 'my_account/outfitter_config'),
|
|
214
|
+
);
|
|
215
|
+
const ref = normalizeInputValue(await questionUi.input('Catalog ref', 'main')) || 'main';
|
|
216
|
+
const settingsPath =
|
|
217
|
+
normalizeInputValue(await questionUi.input('Catalog settings path', 'settings.yml')) || 'settings.yml';
|
|
218
|
+
if (!github || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(github)) {
|
|
219
|
+
questionUi.notify('Catalog repo must use owner/repo syntax; no settings were changed.', 'error');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (settingsPath.startsWith('/') || settingsPath.includes('..')) {
|
|
223
|
+
questionUi.notify('Catalog settings path must stay inside the repository; no settings were changed.', 'error');
|
|
253
224
|
return;
|
|
254
225
|
}
|
|
226
|
+
const { existsSync, readFileSync } = await import('node:fs');
|
|
227
|
+
const homeSettingsPath = OUTFITTER_HOME + '/.agents/settings.yml';
|
|
228
|
+
const privateCatalogOnboarding = await loadPrivateCatalogOnboarding();
|
|
229
|
+
const privateCatalogsAlreadyEnabled = privateCatalogOnboarding.readPrivateProfileCatalogsEnabled(
|
|
230
|
+
{ existsSync, readFileSync },
|
|
231
|
+
homeSettingsPath,
|
|
232
|
+
);
|
|
233
|
+
let privateCatalogAccepted = false;
|
|
234
|
+
if (
|
|
235
|
+
!privateCatalogsAlreadyEnabled &&
|
|
236
|
+
(await privateCatalogOnboarding.classifyGitHubRepositoryVisibility(github)) === 'private'
|
|
237
|
+
) {
|
|
238
|
+
privateCatalogAccepted = await questionUi.confirmPrivateCatalog(github);
|
|
239
|
+
if (!privateCatalogAccepted) {
|
|
240
|
+
questionUi.notify('Private catalog setup was cancelled; no settings were changed.', 'warning');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const choice = await askTargetAndHarness(questionUi);
|
|
245
|
+
if (choice === undefined) return cancel(ctx);
|
|
246
|
+
const { target, harness } = choice;
|
|
247
|
+
const destination = installRoot(target) + '/.agents/settings.yml';
|
|
248
|
+
if (!(await confirmSettingsReplacement(ctx, questionUi, destination))) return;
|
|
249
|
+
await finish(
|
|
250
|
+
ctx,
|
|
251
|
+
{
|
|
252
|
+
setupMode: 'catalog',
|
|
253
|
+
github,
|
|
254
|
+
ref,
|
|
255
|
+
settingsPath,
|
|
256
|
+
harness,
|
|
257
|
+
privateCatalogsEnabled: privateCatalogsAlreadyEnabled || privateCatalogAccepted || undefined,
|
|
258
|
+
privateCatalogAccepted: privateCatalogAccepted || undefined,
|
|
259
|
+
target,
|
|
260
|
+
},
|
|
261
|
+
privateCatalogAccepted
|
|
262
|
+
? 'Outfitter enabled private profile catalogs in ~/.agents/settings.yml and saved this catalog.'
|
|
263
|
+
: [
|
|
264
|
+
'Outfitter saved remote settings catalog to ' + destination + '.',
|
|
265
|
+
"Run 'outfitter sync' or restart Outfitter after the catalog is reachable.",
|
|
266
|
+
].join('\n'),
|
|
267
|
+
);
|
|
268
|
+
};
|
|
255
269
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
const runProvidedSourceOnboarding = async (ctx, questionUi, sourceUri) => {
|
|
271
|
+
const choice = await askTargetAndHarness(questionUi);
|
|
272
|
+
if (choice === undefined) return cancel(ctx);
|
|
273
|
+
const { target, harness } = choice;
|
|
274
|
+
const destination = installRoot(target) + '/.agents/settings.yml';
|
|
275
|
+
if (!(await confirmSettingsReplacement(ctx, questionUi, destination))) return;
|
|
276
|
+
await finish(
|
|
277
|
+
ctx,
|
|
278
|
+
{ setupMode: 'source', sourceUri, harness, target },
|
|
279
|
+
[
|
|
280
|
+
'Outfitter saved setup source to ' + destination + '.',
|
|
281
|
+
"Run 'outfitter sync' or restart Outfitter after the source is reachable.",
|
|
282
|
+
].join('\n'),
|
|
260
283
|
);
|
|
261
|
-
await openLoginIfNoModels(ctx);
|
|
262
284
|
};
|
|
263
285
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
286
|
+
const runOutfitterOnboarding = async (ctx) => {
|
|
287
|
+
if (!ctx.hasUI || onboardingRunning) return;
|
|
288
|
+
onboardingRunning = true;
|
|
289
|
+
try {
|
|
290
|
+
const questionUi = createQuestionUi(ctx);
|
|
291
|
+
if (OUTFITTER_SETUP_SOURCE_URI !== undefined) {
|
|
292
|
+
return await runProvidedSourceOnboarding(ctx, questionUi, OUTFITTER_SETUP_SOURCE_URI);
|
|
293
|
+
}
|
|
294
|
+
const setupMode = await questionUi.selectSetupMode();
|
|
295
|
+
if (setupMode === undefined) return cancel(ctx);
|
|
296
|
+
if (setupMode === 'catalog') return await runRemoteSettingsOnboarding(ctx, questionUi);
|
|
297
|
+
if (setupMode === 'create') return await runCreateProfileOnboarding(ctx, questionUi);
|
|
298
|
+
return await runDefaultCatalogOnboarding(ctx, questionUi);
|
|
299
|
+
} finally {
|
|
300
|
+
onboardingRunning = false;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
pi.registerCommand('outfitter', {
|
|
305
|
+
description: 'Configure Outfitter profile onboarding',
|
|
306
|
+
handler: async (_args, ctx) => runOutfitterOnboarding(ctx),
|
|
269
307
|
});
|
|
270
308
|
|
|
271
|
-
pi.registerCommand(
|
|
272
|
-
description:
|
|
309
|
+
pi.registerCommand('mode', {
|
|
310
|
+
description: 'Toggle Outfitter build/plan mode',
|
|
273
311
|
handler: async (_args, ctx) => {
|
|
274
|
-
if (ctx.mode
|
|
275
|
-
cycleOutfitterMode(ctx);
|
|
312
|
+
if (ctx.mode === 'tui') cycleOutfitterMode(ctx);
|
|
276
313
|
},
|
|
277
314
|
});
|
|
278
315
|
|
|
279
|
-
pi.on(
|
|
280
|
-
|
|
281
|
-
|
|
316
|
+
pi.on('project_trust', async (event) => {
|
|
317
|
+
if (OUTFITTER_AUTO_OPEN && event.cwd === OUTFITTER_PROJECT) return { trusted: 'yes', remember: true };
|
|
318
|
+
return { trusted: 'undecided' };
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
pi.on('session_start', async (event, ctx) => {
|
|
322
|
+
if (ctx.mode !== 'tui') return;
|
|
282
323
|
ctx.ui.setHeader((_tui, theme) => {
|
|
283
|
-
const firstRun = event.reason === "startup" && OUTFITTER_AUTO_OPEN;
|
|
284
324
|
let cachedWidth;
|
|
285
325
|
let cachedLines;
|
|
286
326
|
return {
|
|
287
|
-
// Pi-tui rejects render output wider than the terminal, so the header
|
|
288
|
-
// re-fits its lines to every width pi hands it instead of rendering once.
|
|
289
327
|
render: (width) => {
|
|
290
|
-
const maxWidth = typeof width ===
|
|
328
|
+
const maxWidth = typeof width === 'number' && width > 0 ? width : 120;
|
|
291
329
|
if (cachedLines === undefined || cachedWidth !== maxWidth) {
|
|
292
|
-
cachedLines = createStartupHeaderLines(theme,
|
|
330
|
+
cachedLines = createStartupHeaderLines(theme, maxWidth);
|
|
293
331
|
cachedWidth = maxWidth;
|
|
294
332
|
}
|
|
295
333
|
return cachedLines;
|
|
@@ -301,46 +339,36 @@ export default function outfitter(pi) {
|
|
|
301
339
|
};
|
|
302
340
|
});
|
|
303
341
|
updateModeStatus(ctx);
|
|
304
|
-
updateProfileStatus(ctx);
|
|
305
342
|
ctx.ui.onTerminalInput((data) => {
|
|
306
|
-
if (!matchesKey(data,
|
|
343
|
+
if (!matchesKey(data, 'shift+tab')) return undefined;
|
|
307
344
|
cycleOutfitterMode(ctx);
|
|
308
345
|
return { consume: true };
|
|
309
346
|
});
|
|
310
|
-
|
|
311
|
-
if (event.reason === "startup" && OUTFITTER_AUTO_OPEN) {
|
|
312
|
-
await submitSlashCommand(ctx, "/outfitter");
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
await openLoginIfNoModels(ctx);
|
|
347
|
+
if (event.reason === 'startup' && OUTFITTER_AUTO_OPEN) await submitSlashCommand(ctx, '/outfitter');
|
|
317
348
|
});
|
|
318
349
|
|
|
319
|
-
pi.on(
|
|
320
|
-
if (mode !==
|
|
321
|
-
|
|
350
|
+
pi.on('tool_call', async (event) => {
|
|
351
|
+
if (mode !== 'plan' || event.toolName !== 'bash') return;
|
|
322
352
|
return {
|
|
323
353
|
block: true,
|
|
324
|
-
reason:
|
|
354
|
+
reason:
|
|
355
|
+
'Outfitter plan mode blocks Bash commands. Press Shift+Tab to return to build mode. Command: ' +
|
|
356
|
+
String(event.input?.command ?? ''),
|
|
325
357
|
};
|
|
326
358
|
});
|
|
327
359
|
|
|
328
|
-
pi.on(
|
|
329
|
-
const messages = event.messages.filter((message) => message.customType !==
|
|
330
|
-
|
|
331
|
-
if (mode !== "plan") {
|
|
332
|
-
return { messages };
|
|
333
|
-
}
|
|
334
|
-
|
|
360
|
+
pi.on('context', async (event) => {
|
|
361
|
+
const messages = event.messages.filter((message) => message.customType !== 'outfitter-mode-context');
|
|
362
|
+
if (mode !== 'plan') return { messages };
|
|
335
363
|
return {
|
|
336
364
|
messages: [
|
|
337
365
|
...messages,
|
|
338
366
|
{
|
|
339
|
-
role:
|
|
340
|
-
customType:
|
|
367
|
+
role: 'custom',
|
|
368
|
+
customType: 'outfitter-mode-context',
|
|
341
369
|
content:
|
|
342
|
-
|
|
343
|
-
|
|
370
|
+
'[OUTFITTER PLAN MODE ACTIVE]\n' +
|
|
371
|
+
'You are in read-only planning mode. Inspect files and explain the implementation plan, but do not modify files, run Bash commands, or claim changes are done. Ask before leaving planning mode.',
|
|
344
372
|
display: false,
|
|
345
373
|
},
|
|
346
374
|
],
|
|
@@ -348,285 +376,97 @@ export default function outfitter(pi) {
|
|
|
348
376
|
});
|
|
349
377
|
}
|
|
350
378
|
|
|
351
|
-
|
|
352
|
-
// which crashed startup in narrow (e.g. 80x24 tmux) terminals. Prose wraps so no
|
|
353
|
-
// words are lost; ASCII art and the brand line truncate because wrapping them is noise.
|
|
354
|
-
const createStartupHeaderLines = (theme, firstRun, maxWidth) => {
|
|
355
|
-
const lines = [];
|
|
356
|
-
const addTruncated = (line) => lines.push(visibleWidth(line) > maxWidth ? truncateToWidth(line, maxWidth) : line);
|
|
357
|
-
const addWrapped = (line) => {
|
|
358
|
-
for (const wrappedLine of wrapTextWithAnsi(line, Math.max(1, maxWidth))) addTruncated(wrappedLine);
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
if (OUTFITTER_STARTUP_ASCII_ART) {
|
|
362
|
-
OUTFITTER_ASCII_ART.split("\n").forEach((line, index) =>
|
|
363
|
-
addTruncated(theme.fg(OUTFITTER_ASCII_GRADIENT[index] ?? "accent", line)),
|
|
364
|
-
);
|
|
365
|
-
lines.push("");
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
addTruncated(theme.bold(theme.fg("accent", "Outfitter")) + theme.fg("dim", " + pi"));
|
|
369
|
-
addWrapped(theme.fg("muted", "/ commands · ! bash · shift+tab mode · ctrl+shift+t thinking · ctrl+o more"));
|
|
370
|
-
|
|
371
|
-
if (firstRun) {
|
|
372
|
-
lines.push("");
|
|
373
|
-
addWrapped(theme.fg("dim", "Outfitter turns Pi into a configured working environment:"));
|
|
374
|
-
addWrapped(theme.fg("dim", "• profiles define model, tools, prompts, skills, and extensions"));
|
|
375
|
-
addWrapped(theme.fg("dim", "• settings can live in your home folder or this project"));
|
|
376
|
-
addWrapped(theme.fg("dim", "• catalogs let teams share setups through GitHub"));
|
|
377
|
-
return lines;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
lines.push("");
|
|
381
|
-
addWrapped(
|
|
382
|
-
theme.fg(
|
|
383
|
-
"dim",
|
|
384
|
-
"Outfitter + Pi can explain its own features and look up its docs. Ask it how to use or extend Pi or outfitter profiles.",
|
|
385
|
-
),
|
|
386
|
-
);
|
|
387
|
-
return lines;
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
const createOutfitterPaths = (join) => ({
|
|
391
|
-
homeSettingsPath: join(OUTFITTER_HOME, ".outfitter", "settings.yml"),
|
|
392
|
-
homeProfilesPath: join(OUTFITTER_HOME, ".outfitter", "profiles"),
|
|
393
|
-
projectSettingsPath: join(OUTFITTER_PROJECT, ".outfitter", "settings.yml"),
|
|
394
|
-
projectProfilesPath: join(OUTFITTER_PROJECT, ".outfitter", "profiles"),
|
|
395
|
-
defaultProfilesPath: OUTFITTER_DEFAULT_PROFILES_PATH,
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const runDefaultCatalogOnboarding = async (fs, paths, questionUi) => {
|
|
399
|
-
const currentDefault = readCurrentDefaultProfile(paths.homeSettingsPath, fs.existsSync, fs.readFileSync);
|
|
400
|
-
const profiles = discoverProfileChoices(fs, paths, currentDefault);
|
|
401
|
-
if (profiles.length === 0) {
|
|
402
|
-
questionUi.notify(
|
|
403
|
-
"No profiles were found in the default Outfitter profile catalog. Fix the catalog sync or provide a different catalog.",
|
|
404
|
-
"error",
|
|
405
|
-
);
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const selectedProfile = await questionUi.selectProfile(profiles, currentDefault);
|
|
410
|
-
if (selectedProfile === undefined) {
|
|
411
|
-
questionUi.notify("Outfitter setup cancelled; no settings were changed.", "warning");
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (!OUTFITTER_PROFILE_ID_PATTERN.test(selectedProfile.id)) {
|
|
416
|
-
questionUi.notify("Selected profile id is not filesystem-safe; no settings were changed.", "error");
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const installTarget = await questionUi.selectInstallTarget(paths);
|
|
421
|
-
if (installTarget === undefined) {
|
|
422
|
-
questionUi.notify("Outfitter setup cancelled; no settings were changed.", "warning");
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
fs.mkdirSync(fs.dirname(installTarget.settingsPath), { recursive: true });
|
|
427
|
-
fs.mkdirSync(installTarget.profilesPath, { recursive: true });
|
|
428
|
-
const settingsExisted = fs.existsSync(installTarget.settingsPath);
|
|
429
|
-
if (settingsExisted) {
|
|
430
|
-
updateExistingSettingsDefaultProfile(installTarget.settingsPath, selectedProfile.id, fs.readFileSync, fs.writeFileSync);
|
|
431
|
-
} else {
|
|
432
|
-
fs.writeFileSync(installTarget.settingsPath, createDefaultSettingsContent(selectedProfile.id));
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
questionUi.notify(
|
|
436
|
-
[
|
|
437
|
-
"Outfitter saved default profile '" + selectedProfile.id + "' to " + installTarget.settingsPath + ".",
|
|
438
|
-
"Profile choices were loaded from the default Outfitter profile catalog, not generated locally.",
|
|
439
|
-
"It applies on the next 'outfitter' launch; restart Outfitter to load the selected profile.",
|
|
440
|
-
].join("\n"),
|
|
441
|
-
"info",
|
|
442
|
-
);
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
const runCreateProfileOnboarding = async (fs, paths, questionUi) => {
|
|
446
|
-
const profileId = normalizeInputValue(await questionUi.input("Profile id", "my_profile"));
|
|
447
|
-
if (!profileId || !OUTFITTER_PROFILE_ID_PATTERN.test(profileId)) {
|
|
448
|
-
questionUi.notify("Profile id is not filesystem-safe; no settings were changed.", "error");
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
const label = normalizeInputValue(await questionUi.input("Profile label", profileId));
|
|
452
|
-
const installTarget = await questionUi.selectInstallTarget(paths);
|
|
453
|
-
if (installTarget === undefined) {
|
|
454
|
-
questionUi.notify("Outfitter setup cancelled; no settings were changed.", "warning");
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
fs.mkdirSync(fs.dirname(installTarget.settingsPath), { recursive: true });
|
|
459
|
-
if (fs.existsSync(installTarget.settingsPath)) {
|
|
460
|
-
updateExistingSettingsDefaultProfile(installTarget.settingsPath, profileId, fs.readFileSync, fs.writeFileSync);
|
|
461
|
-
} else {
|
|
462
|
-
fs.writeFileSync(installTarget.settingsPath, createLocalProfileSettingsContent(profileId));
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const profilePath = fs.join(installTarget.profilesPath, profileId, "profile.yml");
|
|
466
|
-
if (!fs.existsSync(profilePath)) {
|
|
467
|
-
fs.mkdirSync(fs.dirname(profilePath), { recursive: true });
|
|
468
|
-
fs.writeFileSync(profilePath, createUserProfileContent(profileId, label));
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
questionUi.notify(
|
|
472
|
-
[
|
|
473
|
-
"Outfitter created profile '" + profileId + "' at " + profilePath + ".",
|
|
474
|
-
"Outfitter saved settings to " + installTarget.settingsPath + ".",
|
|
475
|
-
"It applies on the next 'outfitter' launch; restart Outfitter to load the selected profile.",
|
|
476
|
-
].join("\n"),
|
|
477
|
-
"info",
|
|
478
|
-
);
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
const runRemoteSettingsOnboarding = async (fs, paths, questionUi) => {
|
|
482
|
-
const github = normalizeInputValue(await questionUi.input("GitHub catalog repo (owner/repo)", "my_account/outfitter_config"));
|
|
483
|
-
const ref = normalizeInputValue(await questionUi.input("Catalog ref", "main")) || "main";
|
|
484
|
-
const settingsPath = normalizeInputValue(await questionUi.input("Catalog settings path", "settings.yml")) || "settings.yml";
|
|
485
|
-
if (!github || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(github)) {
|
|
486
|
-
questionUi.notify("Catalog repo must use owner/repo syntax; no settings were changed.", "error");
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
if (settingsPath.startsWith("/") || settingsPath.includes("..")) {
|
|
490
|
-
questionUi.notify("Catalog settings path must stay inside the repository; no settings were changed.", "error");
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const privateCatalogOnboarding = await loadPrivateCatalogOnboarding();
|
|
495
|
-
const privateCatalogsAlreadyEnabled = privateCatalogOnboarding.readPrivateProfileCatalogsEnabled(fs, paths.homeSettingsPath);
|
|
496
|
-
let privateCatalogAccepted = false;
|
|
497
|
-
if (!privateCatalogsAlreadyEnabled && await privateCatalogOnboarding.classifyGitHubRepositoryVisibility(github) === "private") {
|
|
498
|
-
const accepted = await questionUi.confirmPrivateCatalog(github);
|
|
499
|
-
if (!accepted) {
|
|
500
|
-
questionUi.notify("Private catalog setup was cancelled; no settings were changed.", "warning");
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
privateCatalogAccepted = true;
|
|
505
|
-
}
|
|
379
|
+
const normalizeInputValue = (value) => (typeof value === 'string' ? value.trim() : undefined);
|
|
506
380
|
|
|
507
|
-
|
|
508
|
-
if (installTarget === undefined) {
|
|
509
|
-
questionUi.notify("Outfitter setup cancelled; no settings were changed.", "warning");
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
381
|
+
const installRoot = (target) => (target === 'project' ? OUTFITTER_PROJECT : OUTFITTER_HOME);
|
|
512
382
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
privateCatalogAccepted
|
|
522
|
-
? "Outfitter enabled private profile catalogs in ~/.outfitter/settings.yml and saved this catalog."
|
|
523
|
-
: [
|
|
524
|
-
"Outfitter saved remote settings catalog to " + installTarget.settingsPath + ".",
|
|
525
|
-
"Run 'outfitter sync' or restart Outfitter after the catalog is reachable.",
|
|
526
|
-
].join("\n"),
|
|
527
|
-
"info",
|
|
528
|
-
);
|
|
383
|
+
// Every onboarding branch ends by asking for the install target then the CLI agent, cancelling on
|
|
384
|
+
// either escape. Returns undefined when the user cancels so the caller can `return cancel(ctx)`.
|
|
385
|
+
const askTargetAndHarness = async (questionUi) => {
|
|
386
|
+
const target = await questionUi.selectInstallTarget();
|
|
387
|
+
if (target === undefined) return undefined;
|
|
388
|
+
const harness = await questionUi.selectCliAgent();
|
|
389
|
+
if (harness === undefined) return undefined;
|
|
390
|
+
return { target, harness };
|
|
529
391
|
};
|
|
530
392
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
questionUi.notify(
|
|
393
|
+
// Catalog/source setup writes a fresh settings file at `destination`, replacing whatever is there.
|
|
394
|
+
// Never clobber existing settings without an explicit confirmation; returns true only when the file
|
|
395
|
+
// is absent or the user chose to replace it, and notifies+shuts down otherwise so nothing is written.
|
|
396
|
+
const confirmSettingsReplacement = async (ctx, questionUi, destination) => {
|
|
397
|
+
const { existsSync } = await import('node:fs');
|
|
398
|
+
if (!existsSync(destination)) return true;
|
|
399
|
+
const decision = await selectFromItems(
|
|
400
|
+
ctx,
|
|
401
|
+
['Outfitter settings already exist at ' + destination + '.', 'Replace them with your selections?'],
|
|
541
402
|
[
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
]
|
|
545
|
-
|
|
403
|
+
{ value: 'keep', label: 'Keep my current settings', description: 'Leave the existing file unchanged and exit.' },
|
|
404
|
+
{ value: 'replace', label: 'Replace them', description: 'Overwrite the existing file with these selections.' },
|
|
405
|
+
],
|
|
406
|
+
'keep',
|
|
546
407
|
);
|
|
408
|
+
if (decision === 'replace') return true;
|
|
409
|
+
questionUi.notify('Kept your existing Outfitter settings; no changes were made.', 'warning');
|
|
410
|
+
ctx.shutdown();
|
|
411
|
+
return false;
|
|
547
412
|
};
|
|
548
413
|
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
const match = /^default_profile:\s*([^\n#]+)/mu.exec(readFileSync(settingsPath, "utf8"));
|
|
557
|
-
return match?.[1]?.trim().replace(/^['"]|['"]$/gu, "");
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const discoverProfileChoices = (fs, paths, currentDefault) => {
|
|
561
|
-
const discovered = new Map();
|
|
562
|
-
const addProfile = (profile) => {
|
|
563
|
-
if (!profile?.id || !OUTFITTER_PROFILE_ID_PATTERN.test(profile.id)) return;
|
|
564
|
-
const existing = discovered.get(profile.id);
|
|
565
|
-
discovered.set(profile.id, {
|
|
566
|
-
id: profile.id,
|
|
567
|
-
label: profile.label ?? existing?.label,
|
|
568
|
-
description: profile.description ?? existing?.description,
|
|
569
|
-
});
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
for (const profile of readProfilesFromSource(fs, paths.defaultProfilesPath)) addProfile(profile);
|
|
573
|
-
|
|
574
|
-
return [...discovered.values()].sort((left, right) => compareProfiles(left, right, currentDefault));
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
const readProfilesFromSource = (fs, sourcePath) => {
|
|
578
|
-
if (!sourcePath || !fs.existsSync(sourcePath)) return [];
|
|
579
|
-
let entries;
|
|
414
|
+
const writeSetupResult = async (selection) => {
|
|
415
|
+
const [{ mkdirSync, renameSync, rmSync, writeFileSync }, { dirname }] = await Promise.all([
|
|
416
|
+
import('node:fs'),
|
|
417
|
+
import('node:path'),
|
|
418
|
+
]);
|
|
419
|
+
const temporaryPath = OUTFITTER_SETUP_RESULT_PATH + '.tmp';
|
|
420
|
+
mkdirSync(dirname(OUTFITTER_SETUP_RESULT_PATH), { recursive: true });
|
|
580
421
|
try {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
422
|
+
writeFileSync(temporaryPath, JSON.stringify(selection) + '\n', { flag: 'wx' });
|
|
423
|
+
renameSync(temporaryPath, OUTFITTER_SETUP_RESULT_PATH);
|
|
424
|
+
} finally {
|
|
425
|
+
rmSync(temporaryPath, { force: true });
|
|
584
426
|
}
|
|
585
|
-
|
|
586
|
-
return entries.flatMap((entryName) => {
|
|
587
|
-
const entryPath = fs.join(sourcePath, entryName);
|
|
588
|
-
let entryStat;
|
|
589
|
-
try {
|
|
590
|
-
entryStat = fs.statSync(entryPath);
|
|
591
|
-
} catch {
|
|
592
|
-
return [];
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (entryStat.isDirectory()) {
|
|
596
|
-
const profilePath = fs.join(entryPath, "profile.yml");
|
|
597
|
-
return fs.existsSync(profilePath) ? [readProfileYaml(fs.readFileSync(profilePath, "utf8"), entryName)] : [];
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (!entryStat.isFile() || !/\.ya?ml$/u.test(entryName) || entryName === "profile.yml") return [];
|
|
601
|
-
return [readProfileYaml(fs.readFileSync(entryPath, "utf8"), entryName.replace(/\.ya?ml$/u, ""))];
|
|
602
|
-
}).filter((profile) => profile.template !== true);
|
|
603
427
|
};
|
|
604
428
|
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
429
|
+
const createStartupHeaderLines = (theme, maxWidth) => {
|
|
430
|
+
const lines = [];
|
|
431
|
+
const add = (line) => lines.push(visibleWidth(line) > maxWidth ? truncateToWidth(line, maxWidth) : line);
|
|
432
|
+
const addWrapped = (line) => {
|
|
433
|
+
for (const wrapped of wrapTextWithAnsi(line, Math.max(1, maxWidth))) add(wrapped);
|
|
434
|
+
};
|
|
611
435
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
436
|
+
OUTFITTER_ASCII_ART.split('\n').forEach((line, index) =>
|
|
437
|
+
add(theme.fg(OUTFITTER_ASCII_GRADIENT[index] ?? 'accent', line)),
|
|
438
|
+
);
|
|
439
|
+
lines.push('');
|
|
440
|
+
add(theme.bold(theme.fg('accent', 'Outfitter')) + theme.fg('dim', ' + pi'));
|
|
441
|
+
addWrapped(theme.fg('muted', '/ commands · ! bash · shift+tab mode · ctrl+shift+t thinking · ctrl+o more'));
|
|
442
|
+
lines.push('');
|
|
443
|
+
addWrapped(theme.fg('dim', 'Outfitter turns Pi into a configured working environment:'));
|
|
444
|
+
addWrapped(theme.fg('dim', '• profiles define model, tools, prompts, skills, and extensions'));
|
|
445
|
+
addWrapped(theme.fg('dim', '• settings can live in your home folder or this project'));
|
|
446
|
+
addWrapped(theme.fg('dim', '• catalogs let teams share setups through GitHub'));
|
|
447
|
+
return lines;
|
|
615
448
|
};
|
|
616
449
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
450
|
+
// Runs the described-option picker when Pi supports custom UI, otherwise falls back to a plain
|
|
451
|
+
// label list, then resolves whichever form's answer back to the chosen item's `value`.
|
|
452
|
+
const selectFromItems = async (ctx, titleLines, items, initialValue) => {
|
|
453
|
+
const selected =
|
|
454
|
+
typeof ctx.ui.custom === 'function'
|
|
455
|
+
? await selectDescribedOption(ctx, titleLines, items, initialValue)
|
|
456
|
+
: await ctx.ui.select(
|
|
457
|
+
titleLines.join('\n'),
|
|
458
|
+
items.map((item) => item.label),
|
|
459
|
+
);
|
|
460
|
+
if (selected === undefined) return undefined;
|
|
461
|
+
return items.some((item) => item.value === selected) ? selected : items.find((item) => item.label === selected)?.value;
|
|
625
462
|
};
|
|
626
463
|
|
|
627
464
|
const selectDescribedOption = (ctx, titleLines, items, initialValue) =>
|
|
628
465
|
ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
629
|
-
let selectedIndex = Math.max(
|
|
466
|
+
let selectedIndex = Math.max(
|
|
467
|
+
0,
|
|
468
|
+
items.findIndex((item) => item.value === initialValue),
|
|
469
|
+
);
|
|
630
470
|
const labelWidth = Math.max(...items.map((item) => item.label.length));
|
|
631
471
|
let cachedWidth;
|
|
632
472
|
let cachedLines;
|
|
@@ -641,17 +481,13 @@ const selectDescribedOption = (ctx, titleLines, items, initialValue) =>
|
|
|
641
481
|
selectedIndex = Math.max(0, Math.min(items.length - 1, selectedIndex + delta));
|
|
642
482
|
refresh();
|
|
643
483
|
};
|
|
644
|
-
|
|
645
484
|
const render = (width) => {
|
|
646
|
-
const maxWidth = typeof width ===
|
|
485
|
+
const maxWidth = typeof width === 'number' && width > 0 ? width : 120;
|
|
647
486
|
if (cachedLines && cachedWidth === maxWidth) return cachedLines;
|
|
648
|
-
|
|
649
487
|
const lines = [];
|
|
650
488
|
const add = (line) => lines.push(visibleWidth(line) > maxWidth ? truncateToWidth(line, maxWidth) : line);
|
|
651
|
-
const addWrapped = (line, widthForWrap = maxWidth, prefix =
|
|
652
|
-
for (const wrappedLine of wrapTextWithAnsi(line, Math.max(1, widthForWrap)))
|
|
653
|
-
add(prefix + wrappedLine);
|
|
654
|
-
}
|
|
489
|
+
const addWrapped = (line, widthForWrap = maxWidth, prefix = '') => {
|
|
490
|
+
for (const wrappedLine of wrapTextWithAnsi(line, Math.max(1, widthForWrap))) add(prefix + wrappedLine);
|
|
655
491
|
};
|
|
656
492
|
const renderSelectedItem = (prefix, label, description) => {
|
|
657
493
|
const baseLine = prefix + label;
|
|
@@ -659,37 +495,34 @@ const selectDescribedOption = (ctx, titleLines, items, initialValue) =>
|
|
|
659
495
|
add(baseLine);
|
|
660
496
|
return;
|
|
661
497
|
}
|
|
662
|
-
|
|
663
498
|
const inlineDescriptionWidth = maxWidth - visibleWidth(baseLine) - 2;
|
|
664
|
-
const descriptionText = theme.fg(
|
|
499
|
+
const descriptionText = theme.fg('muted', description);
|
|
665
500
|
if (inlineDescriptionWidth >= 30) {
|
|
666
|
-
const [firstLine =
|
|
667
|
-
add(baseLine +
|
|
668
|
-
const continuationPrefix =
|
|
501
|
+
const [firstLine = '', ...remainingLines] = wrapTextWithAnsi(descriptionText, inlineDescriptionWidth);
|
|
502
|
+
add(baseLine + ' ' + firstLine);
|
|
503
|
+
const continuationPrefix = ' '.repeat(Math.min(maxWidth, visibleWidth(baseLine) + 2));
|
|
669
504
|
for (const line of remainingLines) add(continuationPrefix + line);
|
|
670
505
|
return;
|
|
671
506
|
}
|
|
672
|
-
|
|
673
507
|
add(baseLine);
|
|
674
|
-
addWrapped(descriptionText, maxWidth - 2,
|
|
508
|
+
addWrapped(descriptionText, maxWidth - 2, ' ');
|
|
675
509
|
};
|
|
676
510
|
|
|
677
|
-
add(theme.fg(
|
|
678
|
-
titleLines.forEach((line, index) =>
|
|
679
|
-
|
|
680
|
-
|
|
511
|
+
add(theme.fg('accent', '─'.repeat(maxWidth)));
|
|
512
|
+
titleLines.forEach((line, index) =>
|
|
513
|
+
addWrapped(index === 0 ? theme.fg('text', ' ' + line) : theme.fg('dim', ' ' + line)),
|
|
514
|
+
);
|
|
515
|
+
lines.push('');
|
|
681
516
|
items.forEach((item, index) => {
|
|
682
517
|
const selected = index === selectedIndex;
|
|
683
|
-
const prefix = selected ? theme.fg(
|
|
518
|
+
const prefix = selected ? theme.fg('accent', '→ ') : ' ';
|
|
684
519
|
const paddedLabel = item.label.padEnd(labelWidth);
|
|
685
|
-
const label = selected ? theme.fg(
|
|
520
|
+
const label = selected ? theme.fg('accent', paddedLabel) : paddedLabel;
|
|
686
521
|
renderSelectedItem(prefix, label, selected ? item.description : undefined);
|
|
687
522
|
});
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
add(theme.fg(
|
|
691
|
-
add(theme.fg("accent", "─".repeat(maxWidth)));
|
|
692
|
-
|
|
523
|
+
lines.push('');
|
|
524
|
+
add(theme.fg('dim', '↑↓ navigate enter select escape/ctrl+c cancel'));
|
|
525
|
+
add(theme.fg('accent', '─'.repeat(maxWidth)));
|
|
693
526
|
cachedWidth = maxWidth;
|
|
694
527
|
cachedLines = lines;
|
|
695
528
|
return lines;
|
|
@@ -703,47 +536,14 @@ const selectDescribedOption = (ctx, titleLines, items, initialValue) =>
|
|
|
703
536
|
if (matchesKey(data, Key.up)) move(-1);
|
|
704
537
|
else if (matchesKey(data, Key.down)) move(1);
|
|
705
538
|
else if (matchesKey(data, Key.enter)) finish(items[selectedIndex]?.value);
|
|
706
|
-
else if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl(
|
|
539
|
+
else if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) finish(undefined);
|
|
707
540
|
},
|
|
708
541
|
};
|
|
709
542
|
});
|
|
710
543
|
|
|
711
544
|
const formatProfileLabel = (profile, currentDefault) => {
|
|
712
|
-
const current = profile.id === currentDefault ?
|
|
713
|
-
const recommended = currentDefault === undefined && profile.id ===
|
|
714
|
-
const label = profile.label ?
|
|
545
|
+
const current = profile.id === currentDefault ? ' (current)' : '';
|
|
546
|
+
const recommended = currentDefault === undefined && profile.id === 'founder' ? ' (Recommended)' : '';
|
|
547
|
+
const label = profile.label ? ' — ' + profile.label : '';
|
|
715
548
|
return profile.id + label + current + recommended;
|
|
716
549
|
};
|
|
717
|
-
|
|
718
|
-
const createDefaultSettingsContent = (profileId) =>
|
|
719
|
-
OUTFITTER_DEFAULT_SETTINGS_TEMPLATE.replace("__OUTFITTER_PROFILE_ID__", profileId);
|
|
720
|
-
|
|
721
|
-
const createLocalProfileSettingsContent = (profileId) =>
|
|
722
|
-
["default_profile: " + profileId, "profile_sources:", " - path: ./profiles", ""].join("\n");
|
|
723
|
-
|
|
724
|
-
const createRemoteSettingsContent = (github, ref, path, privateCatalogsEnabled = false) =>
|
|
725
|
-
[
|
|
726
|
-
...(privateCatalogsEnabled ? ["enterprise:", " private_profile_catalogs: true"] : []),
|
|
727
|
-
"remote_settings:",
|
|
728
|
-
" - github: " + github,
|
|
729
|
-
" ref: " + ref,
|
|
730
|
-
" path: " + path,
|
|
731
|
-
"",
|
|
732
|
-
].join("\n");
|
|
733
|
-
|
|
734
|
-
const updateExistingSettingsDefaultProfile = (settingsPath, profileId, readFileSync, writeFileSync) => {
|
|
735
|
-
const content = readFileSync(settingsPath, "utf8");
|
|
736
|
-
const nextContent = /^default_profile:.*$/mu.test(content)
|
|
737
|
-
? content.replace(/^default_profile:.*$/gmu, "default_profile: " + profileId)
|
|
738
|
-
: content.replace(/\s*$/u, "\n") + "default_profile: " + profileId + "\n";
|
|
739
|
-
writeFileSync(settingsPath, nextContent);
|
|
740
|
-
};
|
|
741
|
-
|
|
742
|
-
const createUserProfileContent = (profileId, label) =>
|
|
743
|
-
[
|
|
744
|
-
"id: " + profileId,
|
|
745
|
-
"label: " + (label || profileId),
|
|
746
|
-
"description: User-created Outfitter profile.",
|
|
747
|
-
"controls: {}",
|
|
748
|
-
"",
|
|
749
|
-
].join("\n");
|