@ai-outfitter/outfitter 0.10.0 → 1.0.1

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