@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.
Files changed (308) hide show
  1. package/.outfitter/skills/outfitter/SKILL.md +67 -37
  2. package/README.md +63 -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 +353 -553
  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 +93 -334
  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 -369
  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,103 +103,231 @@ 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 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 === "number" && width > 0 ? width : 120;
328
+ const maxWidth = typeof width === 'number' && width > 0 ? width : 120;
291
329
  if (cachedLines === undefined || cachedWidth !== maxWidth) {
292
- cachedLines = createStartupHeaderLines(theme, firstRun, maxWidth);
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, "shift+tab")) return undefined;
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("tool_call", async (event) => {
320
- if (mode !== "plan" || event.toolName !== "bash") return;
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: "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 ?? ''),
325
357
  };
326
358
  });
327
359
 
328
- pi.on("context", async (event) => {
329
- const messages = event.messages.filter((message) => message.customType !== "outfitter-mode-context");
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: "custom",
340
- customType: "outfitter-mode-context",
367
+ role: 'custom',
368
+ customType: 'outfitter-mode-context',
341
369
  content:
342
- "[OUTFITTER PLAN MODE ACTIVE]\n" +
343
- "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.',
344
372
  display: false,
345
373
  },
346
374
  ],
@@ -348,285 +376,97 @@ export default function outfitter(pi) {
348
376
  });
349
377
  }
350
378
 
351
- // Every emitted line must fit maxWidth: pi-tui throws on over-wide render output,
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
- const installTarget = await questionUi.selectInstallTarget(paths);
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
- if (privateCatalogAccepted) {
514
- privateCatalogOnboarding.writePrivateProfileCatalogsEnabled(fs, paths.homeSettingsPath);
515
- }
516
-
517
- const privateCatalogsEnabled = privateCatalogsAlreadyEnabled || privateCatalogAccepted;
518
- fs.mkdirSync(fs.dirname(installTarget.settingsPath), { recursive: true });
519
- fs.writeFileSync(installTarget.settingsPath, createRemoteSettingsContent(github, ref, settingsPath, privateCatalogsEnabled && installTarget.settingsPath === paths.homeSettingsPath));
520
- questionUi.notify(
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
- const runProvidedSourceOnboarding = async (fs, paths, questionUi, sourceUri) => {
532
- const installTarget = await questionUi.selectInstallTarget(paths);
533
- if (installTarget === undefined) {
534
- questionUi.notify("Outfitter setup cancelled; no settings were changed.", "warning");
535
- return;
536
- }
537
-
538
- fs.mkdirSync(fs.dirname(installTarget.settingsPath), { recursive: true });
539
- fs.writeFileSync(installTarget.settingsPath, createProvidedSourceSettingsContent(sourceUri));
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
- "Outfitter saved setup source to " + installTarget.settingsPath + ".",
543
- "Run 'outfitter sync' or restart Outfitter after the source is reachable.",
544
- ].join("\n"),
545
- "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',
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 normalizeInputValue = (value) => typeof value === "string" ? value.trim() : undefined;
550
-
551
- const createProvidedSourceSettingsContent = (sourceUri) =>
552
- ["remote_settings:", " - uri: " + JSON.stringify(sourceUri), " path: settings.yml", ""].join("\n");
553
-
554
- const readCurrentDefaultProfile = (settingsPath, existsSync, readFileSync) => {
555
- if (!existsSync(settingsPath)) return undefined;
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
- entries = fs.readdirSync(sourcePath).sort();
582
- } catch {
583
- return [];
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 readProfileYaml = (content, fallbackId) => ({
606
- id: readYamlString(content, "id") ?? fallbackId,
607
- label: readYamlString(content, "label"),
608
- description: readYamlString(content, "description"),
609
- template: readYamlString(content, "template") === "true",
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
- const readYamlString = (content, key) => {
613
- const match = new RegExp("^" + key + ":\\s*([^\\n#]+)", "mu").exec(content);
614
- 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;
615
448
  };
616
449
 
617
- const compareProfiles = (left, right, currentDefault) => {
618
- if (currentDefault !== undefined) {
619
- if (left.id === currentDefault) return -1;
620
- if (right.id === currentDefault) return 1;
621
- }
622
- if (left.id === "founder") return -1;
623
- if (right.id === "founder") return 1;
624
- 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;
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(0, items.findIndex((item) => item.value === initialValue));
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 === "number" && width > 0 ? width : 120;
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("muted", description);
499
+ const descriptionText = theme.fg('muted', description);
665
500
  if (inlineDescriptionWidth >= 30) {
666
- const [firstLine = "", ...remainingLines] = wrapTextWithAnsi(descriptionText, inlineDescriptionWidth);
667
- add(baseLine + " " + firstLine);
668
- 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));
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("accent", "".repeat(maxWidth)));
678
- titleLines.forEach((line, index) => addWrapped(index === 0 ? theme.fg("text", " " + line) : theme.fg("dim", " " + line)));
679
- lines.push("");
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("accent", "") : " ";
518
+ const prefix = selected ? theme.fg('accent', '') : ' ';
684
519
  const paddedLabel = item.label.padEnd(labelWidth);
685
- const label = selected ? theme.fg("accent", paddedLabel) : paddedLabel;
520
+ const label = selected ? theme.fg('accent', paddedLabel) : paddedLabel;
686
521
  renderSelectedItem(prefix, label, selected ? item.description : undefined);
687
522
  });
688
-
689
- lines.push("");
690
- add(theme.fg("dim", "↑↓ navigate enter select escape/ctrl+c cancel"));
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("c"))) finish(undefined);
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 ? " (current)" : "";
713
- const recommended = currentDefault === undefined && profile.id === "founder" ? " (Recommended)" : "";
714
- 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 : '';
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");