@homer0/pi-profiles 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Leonardo Apiwan (@homer0)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -1,3 +1,201 @@
1
- # Temporary Holding Version
1
+ # 🎭 Pi Profiles
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Profiles for [Pi](https://pi.dev): turn sets of skills, extensions, and context on and off, per machine or per project.
4
+
5
+ You may run Pi in multiple environments, with different requirements, different _styles_. This package helps you manage those things as profiles, grouping related things, and enabling or disabling based on whatever rules you want.
6
+
7
+ ## 🍿 Usage
8
+
9
+ > ⚠️ **This package is for Pi.** It registers a Pi extension and reads Pi's directories; it's not a general-purpose profile manager.
10
+
11
+ - 📦 [Installation](#-installation)
12
+ - [As an extension](#as-an-extension)
13
+ - [As a library](#as-a-library)
14
+ - 📁 [Profiles](#-profiles)
15
+ - [Structure](#structure)
16
+ - [Configuration](#configuration)
17
+ - [Enabling profiles](#enabling-profiles)
18
+ - [Conditional profiles](#conditional-profiles)
19
+ - [Packages](#packages)
20
+ - 🧰 [Commands](#-commands)
21
+ - [/profiles-status](#profiles-status)
22
+ - ⚙️ [Examples](#%EF%B8%8F-examples)
23
+ - 🤘 [Development](#-development)
24
+ - 📝 [Motivation](#-motivation)
25
+
26
+ ### 📦 Installation
27
+
28
+ You can use this package as an extension, which loads your enabled profiles in every session, or as a library, to find out which profiles are enabled from your own extension, tool, or skill.
29
+
30
+ #### As an extension
31
+
32
+ ```bash
33
+ pi install npm:@homer0/pi-profiles
34
+ ```
35
+
36
+ This will make Pi do the following in every session, for the profiles that are enabled:
37
+
38
+ | When | What it does |
39
+ | ------------------- | ------------------------------------------------------------------------------------------------- |
40
+ | Session start | Shows the enabled profiles above the editor, and loads the extensions of their packages. |
41
+ | Resources discovery | Adds the skills of the profiles, and the skills, prompts, and themes of their packages. |
42
+ | Before the agent | Appends the `AGENTS.md` of each profile to the system prompt, under a `# Profile Context` header. |
43
+
44
+ It also adds the [`/profiles-status`](#profiles-status) command.
45
+
46
+ #### As a library
47
+
48
+ ```bash
49
+ pnpm add @homer0/pi-profiles
50
+ ```
51
+
52
+ > `@earendil-works/pi-coding-agent` is a peer dependency: Pi provides it to your extension, so don't bundle it.
53
+
54
+ Installing it as a dependency will give you access to an ESM module with the following things:
55
+
56
+ | Export | What it is |
57
+ | ------------------------ | -------------------------------------------------------------- |
58
+ | `resolveEnabledProfiles` | A function that returns the names of the enabled profiles. |
59
+ | `profilesConfigSchema` | The [Zod](https://zod.dev) schema of the `pi.profiles` config. |
60
+ | `ProfilesConfig` | The type of the `pi.profiles` config. |
61
+ | `ProfileLibOptions` | The type of the options `resolveEnabledProfiles` takes. |
62
+
63
+ The config is read with `@homer0/pi-config`, which is a dependency of this package. Installing this package doesn't install the extension of that one (the `get_config` tool and the `/config` command); for that, install it directly.
64
+
65
+ ### 📁 Profiles
66
+
67
+ #### Structure
68
+
69
+ Each profile is a folder inside the `profiles` folder of the Pi agent directory (`<...>/agent/profiles/<name>/`). Everything in it is optional:
70
+
71
+ ```
72
+ profiles/
73
+ └── work/
74
+ ├── AGENTS.md # Extra instructions for the agent
75
+ ├── packages.json # The packages to load
76
+ └── skills/ # Skills that are only available with this profile
77
+ ```
78
+
79
+ #### Configuration
80
+
81
+ This package uses [`@homer0/pi-config`](https://www.npmjs.com/package/@homer0/pi-config), which is a config manager for Pi, and it allows you to put config files in JSON or YAML in your agent directory, and/or your project directory.
82
+
83
+ This README will cover a basic usage of `@homer0/pi-config`, but I strongly recommend reading its own README for a complete understanding of the possibilities it offers.
84
+
85
+ If you are not using `@homer0/pi-config`, assume that all the configurations described below go into a `config.yaml` file in your agent directory.
86
+
87
+ If the `pi.profiles` config is invalid, no profile is enabled, and you get a message with the reason.
88
+
89
+ #### Enabling profiles
90
+
91
+ Profiles are enabled with the `pi.profiles.enabled` config:
92
+
93
+ ```yaml
94
+ pi:
95
+ profiles:
96
+ enabled:
97
+ - work
98
+ - dev
99
+ ```
100
+
101
+ Just a list with the names of the profiles you want, and they have to match the directory names inside the `profiles` folder.
102
+
103
+ #### Conditional profiles
104
+
105
+ Profiles that are enabled under special conditions are defined in `pi.profiles.enabledIf`.
106
+
107
+ ```yaml
108
+ pi:
109
+ profiles:
110
+ enabledIf:
111
+ work: scripts/is-work-laptop.sh
112
+ dev: false
113
+ ```
114
+
115
+ This alternative complements `pi.profiles.enabled`, so the values you can give each profile key are just a script that determines whether the profile should be enabled, or `false`. You can't just set the value to `true`; for that, use `pi.profiles.enabled` instead.
116
+
117
+ Regarding the script,
118
+
119
+ 1. It will be executed from the Pi directory.
120
+ 2. The script can be `.sh` to run with `bash`, `.ts` to run with `jiti`, or any other extension that runs with `node`.
121
+ 3. The path can be relative or absolute. A relative path is looked up in the project directory, then in the agent directory, and then in the Pi directory. The first one that has it wins.
122
+ 3.a. If the script is not found in any of these locations, the profile is not enabled, and a warning is issued.
123
+ 4. The script should return the string `true` to enable the profile.
124
+
125
+ Based on the example above, the `is-work-laptop.sh` could look like this:
126
+
127
+ ```sh
128
+ #!/bin/bash
129
+
130
+ # Check if the current machine is a work laptop
131
+ if [ "$(hostname)" = "work-laptop" ]; then
132
+ echo "true"
133
+ else
134
+ echo "false"
135
+ fi
136
+ ```
137
+
138
+ #### Packages
139
+
140
+ A profile can have a `packages.json` file, which is like the profile version of the Pi `settings.json`'s `packages` key... but with a small extra feature.
141
+
142
+ Just like in `settings.json`, you have a `packages` array with the paths to the packages you want to load. You may already see the difference, which is that the paths have the same support as the profiles, meaning they can be relative or absolute, and can have an `enabledIf` condition.
143
+
144
+ ```json
145
+ {
146
+ "packages": [
147
+ "./extensions/my-extension",
148
+ "/absolute/path/to/another-package",
149
+ { "path": "./extensions/only-at-work", "enabledIf": "scripts/is-work-laptop.sh" }
150
+ ]
151
+ }
152
+ ```
153
+
154
+ Packages are loaded the same way Pi loads the ones defined in the main `settings.json` file. So, yes, they support extensions, skills, prompts, and themes.
155
+
156
+ ### 🧰 Commands
157
+
158
+ #### /profiles-status
159
+
160
+ Shows the profiles that were loaded in the current session: `Profiles loaded: work, dev`, or `No profiles loaded`.
161
+
162
+ ### ⚙️ Examples
163
+
164
+ This section will focus on the examples as a library.
165
+
166
+ #### Finding out which profiles are enabled
167
+
168
+ ```ts
169
+ import { resolveEnabledProfiles } from '@homer0/pi-profiles';
170
+
171
+ const profiles = await resolveEnabledProfiles({
172
+ cwd: ctx.cwd,
173
+ // Called with the reason when something non-fatal goes wrong. If you don't send it, the
174
+ // issues are logged with `console.warn`.
175
+ onError: (message) => ctx.ui.notify(message, 'warning'),
176
+ });
177
+ ```
178
+
179
+ ⚠️ The profiles are not stored in the session, so this function basically does the same resolution the agent did when the session started.
180
+
181
+ ### 🤘 Development
182
+
183
+ As this project is part of a monorepo, some of the tooling, like `lint-staged` and `husky`, is installed in the root's `package.json`.
184
+
185
+ #### Tasks
186
+
187
+ | Task | Description |
188
+ | ------------- | ------------------------------- |
189
+ | `build` | Builds the package. |
190
+ | `lint` | Lints the package. |
191
+ | `lint:file` | Lints the files you send to it. |
192
+ | `test` | Runs the unit tests. |
193
+ | `types:check` | Validates the TypeScript types. |
194
+
195
+ ## 📝 Motivation
196
+
197
+ I spent half of my day with my work laptop, and then the other half with my personal one. I was starting to play with Pi and I knew out of the gate that I wanted to use the agent on both laptops, but that I'd have tools and settings that were specific to each environment; like, I'd never have a Jira configuration on my personal laptop xD.
198
+
199
+ So I built a simple version of this (at the same time as `@homer0/pi-config`) to manage my skills and extensions for work and never enable them on my personal laptop.
200
+
201
+ Then I realized that this approach could be generalized for any environment-specific configuration, not just my work and personal laptops; for example: now I have profiles for the meta harnesses that run Pi.
@@ -0,0 +1,2 @@
1
+ let e;const t=()=>(e??=import(`../handlers-CDMu6gs8.js`).catch(t=>{throw e=void 0,t}),e),n=e=>{let n={pi:e,state:{sessionProfiles:[]}};e.on(`session_start`,async(e,r)=>{n.state.sessionProfiles=[];let{handleSessionStart:i}=await t();await i({runtime:n,event:e,ctx:r})}),e.on(`resources_discover`,async(e,n)=>{let{handleResourcesDiscover:r}=await t();return r({ctx:n})}),e.on(`before_agent_start`,async(e,n)=>{let{handleBeforeAgentStart:r}=await t();return r({event:e,ctx:n})}),e.registerCommand(`profiles-status`,{description:`Show currently loaded profiles`,handler:async(e,r)=>{let{handleProfilesStatus:i}=await t();i({runtime:n,ctx:r})}})};export{n as default};
2
+ //# sourceMappingURL=profiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiles.js","names":["handlersModule","loadHandlers","error","profiles","runtime","pi","handleSessionStart","handleResourcesDiscover","handleBeforeAgentStart","handleProfilesStatus"],"sources":["../../src/extensions/profiles.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { ProfilesRuntime } from '../types.js';\n\n/** Module shape of the lazily loaded handlers. */\ntype HandlersModule = typeof import('../handlers.js');\n\n/** The promise of the handlers module, shared by every event once it's been requested. */\nlet handlersModule: Promise<HandlersModule> | undefined;\n\n/**\n * Lazy-loads all the handlers through a single shared `import()`. Pi fires several events\n * at the start of a session, and jiti evaluates lazily loaded modules asynchronously,\n * handing partial exports to any `import()` of a module (or of a dependency shared by\n * several handlers) that is still evaluating. Loading everything through one memoized\n * promise makes overlapping events wait for the same evaluation. A failed load is not\n * cached, so the next event retries.\n *\n * @returns The module with all the handlers.\n */\nconst loadHandlers = (): Promise<HandlersModule> => {\n handlersModule ??= import('../handlers.js').catch((error: unknown) => {\n handlersModule = undefined;\n throw error;\n });\n return handlersModule;\n};\n\n/**\n * Registers the profile resource and context handling with Pi. The callbacks only\n * lazy-load their implementation, through the shared handlers loader.\n *\n * @param pi The Pi extension API.\n */\nconst profiles = (pi: ExtensionAPI): void => {\n const runtime: ProfilesRuntime = { pi, state: { sessionProfiles: [] } };\n\n pi.on('session_start', async (event, ctx) => {\n runtime.state.sessionProfiles = [];\n const { handleSessionStart } = await loadHandlers();\n await handleSessionStart({ runtime, event, ctx });\n });\n\n pi.on('resources_discover', async (_event, ctx) => {\n const { handleResourcesDiscover } = await loadHandlers();\n return handleResourcesDiscover({ ctx });\n });\n\n pi.on('before_agent_start', async (event, ctx) => {\n const { handleBeforeAgentStart } = await loadHandlers();\n return handleBeforeAgentStart({ event, ctx });\n });\n\n pi.registerCommand('profiles-status', {\n description: 'Show currently loaded profiles',\n handler: async (_args, ctx) => {\n const { handleProfilesStatus } = await loadHandlers();\n handleProfilesStatus({ runtime, ctx });\n },\n });\n};\n\nexport default profiles;\n"],"mappings":"AAOA,IAAIA,EAYJ,MAAMC,OACJ,IAAmB,OAAO,0BAAiB,CAAC,MAAO,GAAmB,CAEpE,KADA,GAAiB,IAAA,GACXC,CACR,CAAC,EACMF,GASHG,EAAY,GAA2B,CAC3C,IAAMC,EAA2B,CAAE,KAAI,MAAO,CAAE,gBAAiB,CAAC,CAAE,CAAE,EAEtEC,EAAG,GAAG,gBAAiB,MAAO,EAAO,IAAQ,CAC3C,EAAQ,MAAM,gBAAkB,CAAC,EACjC,GAAM,CAAE,sBAAuB,MAAMJ,EAAa,EAClD,MAAMK,EAAmB,CAAE,UAAS,QAAO,KAAI,CAAC,CAClD,CAAC,EAEDD,EAAG,GAAG,qBAAsB,MAAO,EAAQ,IAAQ,CACjD,GAAM,CAAE,2BAA4B,MAAMJ,EAAa,EACvD,OAAOM,EAAwB,CAAE,KAAI,CAAC,CACxC,CAAC,EAEDF,EAAG,GAAG,qBAAsB,MAAO,EAAO,IAAQ,CAChD,GAAM,CAAE,0BAA2B,MAAMJ,EAAa,EACtD,OAAOO,EAAuB,CAAE,QAAO,KAAI,CAAC,CAC9C,CAAC,EAEDH,EAAG,gBAAgB,kBAAmB,CACpC,YAAa,iCACb,QAAS,MAAO,EAAO,IAAQ,CAC7B,GAAM,CAAE,wBAAyB,MAAMJ,EAAa,EACpDQ,EAAqB,CAAE,UAAS,KAAI,CAAC,CACvC,CACF,CAAC,CACH,E"}
@@ -0,0 +1,4 @@
1
+ import{a as e,c as t,i as n,l as r,n as i,o as a,r as o,s,t as c}from"./utils-IUrXjb0h.js";import{DefaultPackageManager as l,SettingsManager as u,getAgentDir as d}from"@earendil-works/pi-coding-agent";import{existsSync as f,statSync as p}from"node:fs";import{dirname as m,isAbsolute as h,join as g}from"node:path";import{readFile as _}from"node:fs/promises";const v=d(),y=o(`profiles`),b=e=>{try{return p(e).isDirectory()}catch{return!1}},x=async(n,r)=>{if(typeof n==`string`)return n;let{path:i,enabledIf:o}=n;if(o===!1||o===`false`)return;let s=e(o,r);if(s===void 0)t(`Could not resolve package enabledIf script for "${i}": ${o}`);else return await a(s)?i:void 0},S=async(e,t)=>{if(!f(e))return[];let n=m(e);try{let t=JSON.parse(await _(e,`utf8`)),{packages:i}=r.parse(t),a=[];for(let e of i){let t=await x(e,n);t!==void 0&&a.push(t)}return a}catch(n){return t(`Invalid profile packages file ${e}; skipped. ${i(n)}`),[]}},C=async(e={})=>{let r=e.onError??(e=>t(e)),a=await n(e),o={names:a,skillPaths:[],agentsFiles:[],packages:[]};if(a.length===0)return o;for(let e of a){let t=g(y,e),n=g(t,`skills`),a=g(t,`AGENTS.md`),s=g(t,`packages.json`);if(b(n)&&o.skillPaths.push(n),f(a))try{o.agentsFiles.push({path:a,content:await _(a,`utf8`)})}catch(e){r(`Could not read profile context ${a}; skipped. ${i(e)}`)}let c=await S(s,r);o.packages.push(...c.map(e=>h(e)?e:g(t,e)))}return o},w=({pi:e,sessionStartEvent:t,ctx:n})=>{let r=[],i=((i,a)=>{let o=e.on(i,a);return i===`session_start`&&r.push(async()=>{await a(t,n)}),o});return{pi:{...e,on:i},queue:r}},T=async(e,n)=>{let{packages:r,sessionStartEvent:a,ctx:o}=n;if(r.length===0)return;let s=n.cwd??process.cwd(),c=n.onError??(e=>t(e));try{let t=u.create(s,v),n=await new l({cwd:s,agentDir:v,settingsManager:t}).resolveExtensionSources(r,{temporary:!0});for(let t of n.extensions)if(t.enabled&&(t.path.endsWith(`.ts`)||t.path.endsWith(`.js`)))try{let n=(await import(t.path))?.default;if(typeof n==`function`){let{pi:r,queue:s}=w({pi:e,sessionStartEvent:a,ctx:o});await n(r);for(let e of s)try{await e()}catch(e){c(`Profile extension ${t.path} session_start handler failed. ${i(e)}`)}}}catch(e){c(`Could not load profile extension ${t.path}; skipped. ${i(e)}`)}}catch(e){c(`Could not load profile extensions; skipped. ${i(e)}`)}},E=async e=>{let{packages:n}=e;if(n.length===0)return{skillPaths:[],promptPaths:[],themePaths:[]};let r=e.cwd??process.cwd(),a=e.onError??(e=>t(e));try{let e=u.create(r,v),t=await new l({cwd:r,agentDir:v,settingsManager:e}).resolveExtensionSources(n,{temporary:!0}),i=e=>e.filter(e=>e.enabled).map(e=>e.path);return{skillPaths:i(t.skills),promptPaths:i(t.prompts),themePaths:i(t.themes)}}catch(e){return a(`Could not load profile packages; skipped. ${i(e)}`),{skillPaths:[],promptPaths:[],themePaths:[]}}},D=c(`pi-profiles`),O=e=>({cwd:e.cwd,onError:t=>D(e,t)}),k=async({event:e,ctx:t})=>{let n=await C(O(t));if(n.agentsFiles.length===0)return;let r=n.agentsFiles.map(e=>`## ${e.path}\n\n${e.content.trim()}`).join(`
2
+
3
+ `);return{systemPrompt:`${e.systemPrompt}\n\n# Profile Context\n\n${r}`}},A=async({ctx:e})=>{let t=await C(O(e));if(t.names.length===0)return;let n=await E({packages:t.packages,...O(e)});return{skillPaths:[...t.skillPaths,...n.skillPaths],promptPaths:n.promptPaths,themePaths:n.themePaths}},j=async(e,t)=>{let r=await n(O(t));r.length!==0&&s({pi:e,ctx:t,key:`profiles-announcement`,label:`Profiles`,value:r.join(`, `)})},M=async({runtime:e,event:t,ctx:n})=>{let{pi:r,state:i}=e;await j(r,n);let a=await C(O(n));i.sessionProfiles=a.names,a.packages.length>0&&await T(r,{packages:a.packages,sessionStartEvent:t,ctx:n,...O(n)})},N=({runtime:e,ctx:t})=>{let{sessionProfiles:n}=e.state,r=n.length===0?`No profiles loaded`:`Profiles loaded: ${n.join(`, `)}`;D(t,r)};export{O as forSession,k as handleBeforeAgentStart,N as handleProfilesStatus,A as handleResourcesDiscover,M as handleSessionStart,D as outputMessage};
4
+ //# sourceMappingURL=handlers-CDMu6gs8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers-CDMu6gs8.js","names":["resolveScriptPath","warnProfilesIssue","resolveEnabledProfiles","profilePackagesSchema","ensureErrorMessage","runScript","getAgentPath","createOutputMessage","AGENT_DIR","getAgentDir","PROFILES_DIR","isDirectory","statSync","path","resolvePackageEntry","entry","pkgPath","enabledIf","resolved","profileDir","readProfilePackages","existsSync","packagesPath","dirname","parsed","readFile","results","packages","error","onError","discoverProfileResources","options","message","names","resources","name","join","skillsPath","agentsPath","profilePackages","isAbsolute","pkg","wrapPiForReplay","queue","on","unsubscribe","pi","eventType","handler","sessionStartEvent","ctx","loadProfileExtensions","cwd","settingsManager","SettingsManager","DefaultPackageManager","ext","factory","replayPi","replay","resolveProfilePackageResources","enabledPaths","resource","outputMessage","forSession","handleBeforeAgentStart","profileContext","file","event","handleResourcesDiscover","packageResources","announceConfiguredProfiles","showAnnouncementWidget","handleSessionStart","runtime","handleProfilesStatus","sessionProfiles"],"sources":["../src/resources.ts","../src/handlers.ts"],"sourcesContent":["import {\n DefaultPackageManager,\n SettingsManager,\n getAgentDir,\n type ExtensionAPI,\n type ExtensionContext,\n type ExtensionHandler,\n type SessionStartEvent,\n} from '@earendil-works/pi-coding-agent';\nimport { existsSync, statSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { profilePackagesSchema, type PackageEntry } from './schemas.js';\nimport type {\n LoadProfileExtensionsOptions,\n PackageResourcePaths,\n ProfileLibOptions,\n ProfileResources,\n ResolveProfilePackageResourcesOptions,\n} from './types.js';\nimport {\n ensureErrorMessage,\n getAgentPath,\n resolveEnabledProfiles,\n resolveScriptPath,\n runScript,\n warnProfilesIssue,\n} from './utils.js';\n\nconst AGENT_DIR = getAgentDir();\nconst PROFILES_DIR = getAgentPath('profiles');\nconst PACKAGES_FILENAME = 'packages.json';\nconst AGENTS_FILENAME = 'AGENTS.md';\nconst SKILLS_DIRNAME = 'skills';\n\n/** Factory function signature exported by a pi extension module. */\ntype ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;\n\n/**\n * Checks if a path is an existing directory.\n *\n * @param path The path to check.\n * @returns Whether it's a directory.\n */\nconst isDirectory = (path: string): boolean => {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n};\n\n/**\n * Resolves a single package entry to a path, evaluating its `enabledIf` condition if it\n * has one.\n *\n * @param entry The entry from the profile's `packages.json`.\n * @param profileDir The directory of the profile, to resolve the script against.\n * @returns The path of the package, or `undefined` if it's turned off.\n */\nconst resolvePackageEntry = async (\n entry: PackageEntry,\n profileDir: string,\n): Promise<string | undefined> => {\n if (typeof entry === 'string') return entry;\n const { path: pkgPath, enabledIf } = entry;\n if (enabledIf === false || enabledIf === 'false') return undefined;\n const resolved = resolveScriptPath(enabledIf, profileDir);\n if (typeof resolved === 'undefined') {\n warnProfilesIssue(\n `Could not resolve package enabledIf script for \"${pkgPath}\": ${enabledIf}`,\n );\n return undefined;\n }\n return (await runScript(resolved)) ? pkgPath : undefined;\n};\n\n/**\n * Reads the packages a profile declares, evaluating the `enabledIf` condition of each. If\n * the file is invalid, it reports it through `onError` and the profile has no packages.\n *\n * @param packagesPath The path of the profile's `packages.json`.\n * @param onError Where to report non-fatal issues.\n * @returns The paths of the packages, as declared.\n */\nconst readProfilePackages = async (\n packagesPath: string,\n onError: (message: string) => void,\n): Promise<string[]> => {\n if (!existsSync(packagesPath)) return [];\n const profileDir = dirname(packagesPath);\n\n try {\n const parsed = JSON.parse(await readFile(packagesPath, 'utf8')) as unknown;\n const { packages } = profilePackagesSchema.parse(parsed);\n const results: string[] = [];\n for (const entry of packages) {\n const pkgPath = await resolvePackageEntry(entry, profileDir);\n if (typeof pkgPath !== 'undefined') results.push(pkgPath);\n }\n return results;\n } catch (error) {\n onError(\n `Invalid profile packages file ${packagesPath}; skipped. ${ensureErrorMessage(error)}`,\n );\n return [];\n }\n};\n\n/**\n * Discovers what every enabled profile exposes: its `skills` directory, its `AGENTS.md`\n * context file, and its declared packages. Whatever can't be read is reported through\n * `onError` and skipped.\n *\n * @param options The directory to resolve from, and where to report non-fatal issues.\n * @returns The names of the profiles and what they expose.\n */\nexport const discoverProfileResources = async (\n options: ProfileLibOptions = {},\n): Promise<ProfileResources> => {\n const onError = options.onError ?? ((message: string) => warnProfilesIssue(message));\n const names = await resolveEnabledProfiles(options);\n const resources: ProfileResources = {\n names,\n skillPaths: [],\n agentsFiles: [],\n packages: [],\n };\n\n if (names.length === 0) return resources;\n\n for (const name of names) {\n const profileDir = join(PROFILES_DIR, name);\n const skillsPath = join(profileDir, SKILLS_DIRNAME);\n const agentsPath = join(profileDir, AGENTS_FILENAME);\n const packagesPath = join(profileDir, PACKAGES_FILENAME);\n\n if (isDirectory(skillsPath)) resources.skillPaths.push(skillsPath);\n\n if (existsSync(agentsPath)) {\n try {\n resources.agentsFiles.push({\n path: agentsPath,\n content: await readFile(agentsPath, 'utf8'),\n });\n } catch (error) {\n onError(\n `Could not read profile context ${agentsPath}; skipped. ${ensureErrorMessage(error)}`,\n );\n }\n }\n\n const profilePackages = await readProfilePackages(packagesPath, onError);\n resources.packages.push(\n ...profilePackages.map((pkg) => (isAbsolute(pkg) ? pkg : join(profileDir, pkg))),\n );\n }\n\n return resources;\n};\n\n/**\n * One queued `session_start` handler registration, captured by {@link wrapPiForReplay} so\n * it can be replayed after its factory returns.\n */\ntype QueuedSessionStartReplay = () => Promise<void>;\n\n/** Options for {@link wrapPiForReplay}. */\ntype WrapPiForReplayOptions = {\n /** The Pi extension API to wrap. */\n pi: ExtensionAPI;\n /** The in-flight `session_start` event. */\n sessionStartEvent: SessionStartEvent;\n /** The context of the in-flight `session_start`. */\n ctx: ExtensionContext;\n};\n\n/** Return value of {@link wrapPiForReplay}: the wrapped `pi` plus its replay queue. */\ntype WrapPiForReplayResult = { pi: ExtensionAPI; queue: QueuedSessionStartReplay[] };\n\n/**\n * Wraps `pi` for one dynamically-loaded factory call so any `pi.on('session_start', ...)`\n * it makes still registers normally, but is also queued for replay. Pi snapshots\n * registered handlers before dispatching an event, so a handler registered by a factory\n * invoked from inside an in-flight `session_start` handler would otherwise never run for\n * the event that triggered this load.\n *\n * @param options The API to wrap, and the event and context to replay.\n * @returns The wrapped `pi`, and the replay queue in registration order.\n */\nconst wrapPiForReplay = ({\n pi,\n sessionStartEvent,\n ctx,\n}: WrapPiForReplayOptions): WrapPiForReplayResult => {\n const queue: QueuedSessionStartReplay[] = [];\n // `ExtensionAPI.on` is a large overload union; a single forwarding implementation can't\n // match every overload, so the event/handler pair is cast through `never` at the\n // forwarding call and the finished function is cast once to the real overloaded type.\n const on = ((eventType: string, handler: ExtensionHandler<SessionStartEvent>) => {\n const unsubscribe = pi.on(eventType as never, handler as never);\n if (eventType === 'session_start') {\n queue.push(async () => {\n await handler(sessionStartEvent, ctx);\n });\n }\n return unsubscribe;\n }) as ExtensionAPI['on'];\n\n return { pi: { ...pi, on }, queue };\n};\n\n/**\n * Imports and runs the extension factories of the profile packages. It's meant to be\n * called from a `session_start` handler: the `session_start` handlers each factory\n * registers are replayed right after, with the same event, because Pi's own dispatch for\n * it has already taken its snapshot of the handlers. A handler that fails is reported and\n * doesn't stop the others.\n *\n * @param pi The Pi extension API to give to the factories.\n * @param options The packages to load, and the event and context to replay.\n */\nexport const loadProfileExtensions = async (\n pi: ExtensionAPI,\n options: LoadProfileExtensionsOptions,\n): Promise<void> => {\n const { packages, sessionStartEvent, ctx } = options;\n if (packages.length === 0) return;\n const cwd = options.cwd ?? process.cwd();\n const onError = options.onError ?? ((message: string) => warnProfilesIssue(message));\n\n try {\n // Resolve the pi.extensions entry points declared by each profile package.\n const settingsManager = SettingsManager.create(cwd, AGENT_DIR);\n const packageManager = new DefaultPackageManager({\n cwd,\n agentDir: AGENT_DIR,\n settingsManager,\n });\n const resolved = await packageManager.resolveExtensionSources(packages, {\n temporary: true,\n });\n\n // Import each extension file and call its factory with a replay-wrapped pi API, then\n // replay any session_start registrations it made, in registration order, with\n // per-handler error isolation matching pi's own emit() behavior.\n for (const ext of resolved.extensions) {\n // Guard against the directory-path fallback that resolveExtensionSources emits\n // when a package has no pi.extensions manifest and no index.ts/index.js.\n if (ext.enabled && (ext.path.endsWith('.ts') || ext.path.endsWith('.js'))) {\n try {\n const imported = await import(ext.path);\n const factory = imported?.default;\n if (typeof factory === 'function') {\n const { pi: replayPi, queue } = wrapPiForReplay({\n pi,\n sessionStartEvent,\n ctx,\n });\n await (factory as ExtensionFactory)(replayPi);\n for (const replay of queue) {\n try {\n await replay();\n } catch (error) {\n onError(\n `Profile extension ${ext.path} session_start handler failed. ${ensureErrorMessage(error)}`,\n );\n }\n }\n }\n } catch (error) {\n onError(\n `Could not load profile extension ${ext.path}; skipped. ${ensureErrorMessage(error)}`,\n );\n }\n }\n }\n } catch (error) {\n onError(`Could not load profile extensions; skipped. ${ensureErrorMessage(error)}`);\n }\n};\n\n/**\n * Resolves the skills, prompt templates and themes the profile packages expose. If they\n * can't be resolved, it reports it through `onError` and there are none.\n *\n * @param options The packages to resolve, the directory to resolve from, and where to\n * report non-fatal issues.\n * @returns The paths of the enabled resources, by kind.\n */\nexport const resolveProfilePackageResources = async (\n options: ResolveProfilePackageResourcesOptions,\n): Promise<PackageResourcePaths> => {\n const { packages } = options;\n if (packages.length === 0) {\n return { skillPaths: [], promptPaths: [], themePaths: [] };\n }\n const cwd = options.cwd ?? process.cwd();\n const onError = options.onError ?? ((message: string) => warnProfilesIssue(message));\n\n try {\n const settingsManager = SettingsManager.create(cwd, AGENT_DIR);\n const packageManager = new DefaultPackageManager({\n cwd,\n agentDir: AGENT_DIR,\n settingsManager,\n });\n const resolved = await packageManager.resolveExtensionSources(packages, {\n temporary: true,\n });\n const enabledPaths = (resources: typeof resolved.skills): string[] =>\n resources.filter((resource) => resource.enabled).map((resource) => resource.path);\n\n return {\n skillPaths: enabledPaths(resolved.skills),\n promptPaths: enabledPaths(resolved.prompts),\n themePaths: enabledPaths(resolved.themes),\n };\n } catch (error) {\n onError(`Could not load profile packages; skipped. ${ensureErrorMessage(error)}`);\n return { skillPaths: [], promptPaths: [], themePaths: [] };\n }\n};\n","import type {\n BeforeAgentStartEvent,\n BeforeAgentStartEventResult,\n ExtensionCommandContext,\n ExtensionContext,\n ExtensionAPI,\n ResourcesDiscoverResult,\n SessionStartEvent,\n} from '@earendil-works/pi-coding-agent';\nimport {\n discoverProfileResources,\n loadProfileExtensions,\n resolveProfilePackageResources,\n} from './resources.js';\nimport type { ProfileLibOptions, ProfilesRuntime } from './types.js';\nimport {\n createOutputMessage,\n resolveEnabledProfiles,\n showAnnouncementWidget,\n} from './utils.js';\n\n/** Emits a visible status message when UI is available, otherwise logs to stdout. */\nexport const outputMessage = createOutputMessage('pi-profiles');\n\n/**\n * Builds the options that make the profile functions use the directory of the session and\n * report their non-fatal issues through its output.\n *\n * @param ctx The context of the session.\n * @returns The options for the profile functions.\n */\nexport const forSession = (ctx: ExtensionContext): ProfileLibOptions => ({\n cwd: ctx.cwd,\n onError: (message) => outputMessage(ctx, message),\n});\n\n/** Options for {@link handleBeforeAgentStart}. */\ntype HandleBeforeAgentStartOptions = {\n /** The `before_agent_start` event. */\n event: BeforeAgentStartEvent;\n /** The context of the session. */\n ctx: ExtensionContext;\n};\n\n/**\n * Handler for `before_agent_start`: appends the `AGENTS.md` content of the enabled\n * profiles to the system prompt.\n *\n * @param options The event and the context.\n * @returns The new system prompt, or `undefined` if there's nothing to append.\n */\nexport const handleBeforeAgentStart = async ({\n event,\n ctx,\n}: HandleBeforeAgentStartOptions): Promise<BeforeAgentStartEventResult | undefined> => {\n const resources = await discoverProfileResources(forSession(ctx));\n if (resources.agentsFiles.length === 0) return undefined;\n\n const profileContext = resources.agentsFiles\n .map((file) => `## ${file.path}\\n\\n${file.content.trim()}`)\n .join('\\n\\n');\n\n return {\n systemPrompt: `${event.systemPrompt}\\n\\n# Profile Context\\n\\n${profileContext}`,\n };\n};\n\n/** Options for {@link handleResourcesDiscover}. */\ntype HandleResourcesDiscoverOptions = {\n /** The context of the session. */\n ctx: ExtensionContext;\n};\n\n/**\n * Handler for `resources_discover`: contributes the skills, prompt templates and themes\n * of the enabled profiles and of their packages.\n *\n * @param options The context.\n * @returns The resource paths, or `undefined` if no profile is enabled.\n */\nexport const handleResourcesDiscover = async ({\n ctx,\n}: HandleResourcesDiscoverOptions): Promise<ResourcesDiscoverResult | undefined> => {\n const resources = await discoverProfileResources(forSession(ctx));\n if (resources.names.length === 0) return undefined;\n\n const packageResources = await resolveProfilePackageResources({\n packages: resources.packages,\n ...forSession(ctx),\n });\n\n return {\n skillPaths: [...resources.skillPaths, ...packageResources.skillPaths],\n promptPaths: packageResources.promptPaths,\n themePaths: packageResources.themePaths,\n };\n};\n\nconst WIDGET_KEY = 'profiles-announcement';\n\n/**\n * Announces the enabled profiles with a widget, if there are any.\n *\n * @param pi The Pi extension API.\n * @param ctx The context of the session.\n */\nconst announceConfiguredProfiles = async (\n pi: ExtensionAPI,\n ctx: ExtensionContext,\n): Promise<void> => {\n const names = await resolveEnabledProfiles(forSession(ctx));\n if (names.length === 0) return;\n\n showAnnouncementWidget({\n pi,\n ctx,\n key: WIDGET_KEY,\n label: 'Profiles',\n value: names.join(', '),\n });\n};\n\n/** Options for {@link handleSessionStart}. */\ntype HandleSessionStartOptions = {\n /** The runtime of the extension. */\n runtime: ProfilesRuntime;\n /** The `session_start` event. */\n event: SessionStartEvent;\n /** The context of the session. */\n ctx: ExtensionContext;\n};\n\n/**\n * Handler for `session_start`: announces the enabled profiles, records the discovered\n * ones in `runtime.state`, and loads the extensions of their packages.\n *\n * @param options The runtime, the event, and the context.\n */\nexport const handleSessionStart = async ({\n runtime,\n event,\n ctx,\n}: HandleSessionStartOptions): Promise<void> => {\n const { pi, state } = runtime;\n await announceConfiguredProfiles(pi, ctx);\n const resources = await discoverProfileResources(forSession(ctx));\n state.sessionProfiles = resources.names;\n if (resources.packages.length > 0) {\n await loadProfileExtensions(pi, {\n packages: resources.packages,\n sessionStartEvent: event,\n ctx,\n ...forSession(ctx),\n });\n }\n};\n\n/** Options for {@link handleProfilesStatus}. */\ntype HandleProfilesStatusOptions = {\n /** The runtime of the extension. */\n runtime: ProfilesRuntime;\n /** The context of the command. */\n ctx: ExtensionCommandContext;\n};\n\n/**\n * Handles `/profiles-status`: shows the profiles loaded in this session.\n *\n * @param options The runtime and the context.\n */\nexport const handleProfilesStatus = ({\n runtime,\n ctx,\n}: HandleProfilesStatusOptions): void => {\n const { sessionProfiles } = runtime.state;\n const message =\n sessionProfiles.length === 0\n ? 'No profiles loaded'\n : `Profiles loaded: ${sessionProfiles.join(', ')}`;\n outputMessage(ctx, message);\n};\n"],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export{u as profilesConfigSchema,i as resolveEnabledProfiles}from"./utils-IUrXjb0h.js";
@@ -0,0 +1,2 @@
1
+ import{createRequire as e}from"node:module";import{z as t}from"zod";import{CONFIG_DIR_NAME as n,getAgentDir as r}from"@earendil-works/pi-coding-agent";import{getConfigValue as i}from"@homer0/pi-config";import{execFile as a}from"node:child_process";import{existsSync as o}from"node:fs";import{homedir as s}from"node:os";import{extname as c,isAbsolute as l,join as u,resolve as d}from"node:path";import{promisify as f}from"node:util";const p=t.object({enabled:t.array(t.string().min(1)).default([]),enabledIf:t.record(t.string().min(1),t.union([t.literal(!1),t.string().min(1)])).default({})}),m=t.union([t.string().min(1),t.object({path:t.string().min(1),enabledIf:t.union([t.literal(!1),t.string().min(1)])})]),h=t.object({packages:t.array(m).default([])}),g=(...e)=>u(r(),...e),_=(()=>{try{let t=e(import.meta.url).resolve(`jiti/package.json`),n=t.split(`/node_modules/`)[0];if(n)return u(n,`node_modules/.bin/jiti`);if(t.includes(`.pnpm`))return`pnpm exec jiti`}catch{}return`npx jiti`})(),v=e=>e instanceof t.ZodError?e.issues.map(e=>e.message).join(`; `):e instanceof Error?e.message:String(e),y=e=>{let{pi:t,ctx:n,key:r,label:i,value:a}=e;n.ui.setWidget(r,(e,t)=>({render:()=>[t.fg(`customMessageLabel`,`${i}: `)+t.fg(`customMessageText`,a)],invalidate:()=>{},dispose:()=>{}})),t.on(`input`,(e,t)=>{t.ui.setWidget(r,void 0)})},b=e=>(t,n)=>{t.hasUI?t.ui.notify(n,`info`):console.info(`[${e}] ${n}`)},x=`pi.profiles`,S=s(),C=u(S,n),w=r(),T=f(a),E=(e,t)=>{t===void 0?console.warn(`[pi-profiles] ${e}`):console.warn(`[pi-profiles] ${e}`,t)},D=(e,t)=>{if(l(e))return o(e)?e:void 0;for(let n of[t,w,C]){let t=d(n,e);if(o(t))return t}},O=async e=>{let t=c(e),n;n=t===`.sh`?[e]:t===`.ts`?[`-c`,`${_} "${e}"`]:[`-c`,`node "${e}"`];try{let{stdout:e}=await T(`bash`,n,{cwd:C});return e.trim()===`true`}catch(t){return E(`Script failed: ${e}`,t),!1}},k=async(e,t)=>{let n=[];for(let[r,i]of Object.entries(e))if(i&&i!==`false`){let e=D(i,t);e===void 0?E(`Could not resolve enabledIf script for "${r}": ${i}`):await O(e)&&n.push(r)}return n},A=async(e={})=>{let t=e.cwd??process.cwd(),n=e.onError??(e=>E(e));try{let e=await i(x,{cwd:t,schema:p});if(!e)return[];let{enabled:n,enabledIf:r}=e,a=await k(r,t);return Array.from(/* @__PURE__ */ new Set([...n,...a]))}catch(e){return n(`Invalid ${x} config; profiles skipped. ${v(e)}`),[]}};export{D as a,E as c,A as i,h as l,v as n,O as o,g as r,y as s,b as t,p as u};
2
+ //# sourceMappingURL=utils-IUrXjb0h.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils-IUrXjb0h.js","names":["profilesConfigSchema","z","packageEntrySchema","profilePackagesSchema","getAgentPath","segments","join","getAgentDir","JITI_BIN","pkgPath","createRequire","workspaceRoot","ensureErrorMessage","error","issue","showAnnouncementWidget","options","ctx","key","theme","label","value","pi","inputCtx","createOutputMessage","message","subsystem","CONFIG_PATH","HOME_DIR","homedir","REPO_DIR","CONFIG_DIR_NAME","AGENT_DIR","execFileAsync","promisify","execFile","warnProfilesIssue","resolveScriptPath","isAbsolute","scriptPath","existsSync","base","cwd","candidate","resolve","runScript","ext","extname","args","stdout","evaluateEnabledIf","results","name","enabledIf","resolved","resolveEnabledProfiles","onError","config","getConfigValue","conditional","enabled"],"sources":["../src/schemas.ts","../src/utils.ts"],"sourcesContent":["import { z } from 'zod';\n\n/** Runtime schema for `pi.profiles` in the Pi config. */\nexport const profilesConfigSchema = z.object({\n /** The names of the profiles that are always enabled. */\n enabled: z.array(z.string().min(1)).default([]),\n /**\n * The conditional profiles: each key is a profile name, and each value is the path of a\n * script that prints `true` when the profile should be enabled, or `false` to turn the\n * entry off.\n */\n enabledIf: z\n .record(z.string().min(1), z.union([z.literal(false), z.string().min(1)]))\n .default({}),\n});\n\n/** The resolved `pi.profiles` config. */\nexport type ProfilesConfig = z.infer<typeof profilesConfigSchema>;\n\n/**\n * Runtime schema for one entry in a profile's `packages.json`: a path, or an object with\n * the path and a condition.\n */\nexport const packageEntrySchema = z.union([\n z.string().min(1),\n z.object({\n /** The path of the package, absolute or relative to the profile directory. */\n path: z.string().min(1),\n /**\n * The path of a script that prints `true` when the package should be loaded, or\n * `false` to turn the entry off.\n */\n enabledIf: z.union([z.literal(false), z.string().min(1)]),\n }),\n]);\n\n/** One entry in a profile's `packages.json`. */\nexport type PackageEntry = z.infer<typeof packageEntrySchema>;\n\n/** Runtime schema for a profile's `packages.json` file. */\nexport const profilePackagesSchema = z.object({\n /** The packages the profile loads. */\n packages: z.array(packageEntrySchema).default([]),\n});\n","import {\n CONFIG_DIR_NAME,\n getAgentDir,\n type ExtensionAPI,\n type ExtensionContext,\n} from '@earendil-works/pi-coding-agent';\nimport { getConfigValue } from '@homer0/pi-config';\nimport { execFile } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { homedir } from 'node:os';\nimport { extname, isAbsolute, join, resolve } from 'node:path';\nimport { promisify } from 'node:util';\nimport { z } from 'zod';\nimport { profilesConfigSchema } from './schemas.js';\nimport type { ProfileLibOptions } from './types.js';\n\n/**\n * Resolves a path relative to the Pi agent directory.\n *\n * @param segments The segments of the path, relative to the agent directory.\n * @returns The absolute path.\n */\nexport const getAgentPath = (...segments: string[]): string =>\n join(getAgentDir(), ...segments);\n\n/**\n * Resolves the absolute path to the jiti binary. In a pnpm workspace, the binary is\n * always at `<workspace-root>/node_modules/.bin/jiti`, derived by splitting the resolved\n * jiti package path on `/node_modules/`. Falls back to `npx jiti` when the package cannot\n * be resolved.\n *\n * @returns The command that runs jiti.\n */\nexport const resolveJitiBin = (): string => {\n try {\n const requireFromHere = createRequire(import.meta.url);\n const pkgPath = requireFromHere.resolve('jiti/package.json');\n const workspaceRoot = pkgPath.split('/node_modules/')[0];\n if (workspaceRoot) return join(workspaceRoot, 'node_modules/.bin/jiti');\n // pnpm detected but workspace root could not be derived — use pnpm exec as a safe fallback\n if (pkgPath.includes('.pnpm')) return 'pnpm exec jiti';\n } catch {\n // fall through to npx fallback\n }\n return 'npx jiti';\n};\n\n/**\n * The resolved jiti binary for this workspace. Computed once at module load so callers\n * can use it directly in shell commands or spawn arguments.\n */\nexport const JITI_BIN = resolveJitiBin();\n\n/**\n * Returns the error message string from any thrown value. Prettifies zod validation\n * errors instead of surfacing their raw JSON `.message`.\n *\n * @param error The thrown value.\n * @returns The message of the error, or the value as a string if it's not an error.\n */\nexport const ensureErrorMessage = (error: unknown): string => {\n if (error instanceof z.ZodError) {\n return error.issues.map((issue) => issue.message).join('; ');\n }\n return error instanceof Error ? error.message : String(error);\n};\n\n/** Options for {@link showAnnouncementWidget}. */\ntype AnnouncementWidgetOptions = {\n /** The Pi extension API, to listen for the user input. */\n pi: ExtensionAPI;\n /** The context of the session. */\n ctx: ExtensionContext;\n /** The key that identifies the widget. */\n key: string;\n /** The label of the announcement. */\n label: string;\n /** The value of the announcement. */\n value: string;\n};\n\n/**\n * Shows a labelled announcement widget above the editor and removes it on the first user\n * input. Intended for session-start status messages that should not persist or appear in\n * LLM context.\n *\n * @param options What to show, and where.\n */\nexport const showAnnouncementWidget = (options: AnnouncementWidgetOptions): void => {\n const { pi, ctx, key, label, value } = options;\n ctx.ui.setWidget(key, (_tui, theme) => ({\n render: () => [\n theme.fg('customMessageLabel', `${label}: `) + theme.fg('customMessageText', value),\n ],\n invalidate: () => {},\n dispose: () => {},\n }));\n\n pi.on('input', (_event, inputCtx) => {\n inputCtx.ui.setWidget(key, undefined);\n });\n};\n\n/**\n * Creates a function that outputs a message for one subsystem. When there is a UI, it\n * notifies through it; otherwise, it logs to stdout, with the subsystem as the prefix.\n *\n * @param subsystem The name that identifies the messages.\n * @returns The function that outputs the messages.\n */\nexport const createOutputMessage =\n (subsystem: string) =>\n (ctx: ExtensionContext, message: string): void => {\n if (ctx.hasUI) {\n ctx.ui.notify(message, 'info');\n return;\n }\n\n // eslint-disable-next-line no-console -- Non-interactive sessions still need visible extension output.\n console.info(`[${subsystem}] ${message}`);\n };\n\nconst CONFIG_PATH = 'pi.profiles';\nconst HOME_DIR = homedir();\nconst REPO_DIR = join(HOME_DIR, CONFIG_DIR_NAME);\nconst AGENT_DIR = getAgentDir();\nconst execFileAsync = promisify(execFile);\n\n/**\n * Reports a non-fatal profile-resolution issue without an active UI session.\n *\n * @param message The description of the issue.\n * @param error The error behind the issue, if there is one.\n */\nexport const warnProfilesIssue = (message: string, error?: unknown): void => {\n if (typeof error === 'undefined') {\n // eslint-disable-next-line no-console -- Profile resolution issues should be visible without an active Pi session.\n console.warn(`[pi-profiles] ${message}`);\n } else {\n // eslint-disable-next-line no-console -- Profile resolution issues should be visible without an active Pi session.\n console.warn(`[pi-profiles] ${message}`, error);\n }\n};\n\n/**\n * Resolves a script path against the cwd, the agent directory, and the Pi directory, in\n * that order. An absolute path is only checked for existence.\n *\n * @param scriptPath The path of the script.\n * @param cwd The directory to resolve relative paths against first.\n * @returns The path of the script, or `undefined` if it doesn't exist anywhere.\n */\nexport const resolveScriptPath = (\n scriptPath: string,\n cwd: string,\n): string | undefined => {\n if (isAbsolute(scriptPath)) return existsSync(scriptPath) ? scriptPath : undefined;\n for (const base of [cwd, AGENT_DIR, REPO_DIR]) {\n const candidate = resolve(base, scriptPath);\n if (existsSync(candidate)) return candidate;\n }\n return undefined;\n};\n\n/**\n * Runs a script from the Pi directory: `.sh` with bash, `.ts` with jiti, and anything\n * else with node. If it fails, it warns and counts as `false`.\n *\n * @param scriptPath The absolute path of the script.\n * @returns Whether the script printed `true`.\n */\nexport const runScript = async (scriptPath: string): Promise<boolean> => {\n const ext = extname(scriptPath);\n let args: string[];\n if (ext === '.sh') {\n args = [scriptPath];\n } else if (ext === '.ts') {\n args = ['-c', `${JITI_BIN} \"${scriptPath}\"`];\n } else {\n args = ['-c', `node \"${scriptPath}\"`];\n }\n try {\n const { stdout } = await execFileAsync('bash', args, { cwd: REPO_DIR });\n return stdout.trim() === 'true';\n } catch (error) {\n warnProfilesIssue(`Script failed: ${scriptPath}`, error);\n return false;\n }\n};\n\n/**\n * Runs the `enabledIf` scripts and collects the profiles whose script printed `true`.\n * Entries set to `false`, and scripts that can't be found, are skipped.\n *\n * @param enabledIf The `enabledIf` entries, from profile name to script path.\n * @param cwd The directory to resolve relative script paths against first.\n * @returns The names of the profiles that passed.\n */\nexport const evaluateEnabledIf = async (\n enabledIf: Record<string, false | string>,\n cwd: string,\n): Promise<string[]> => {\n const results: string[] = [];\n for (const [name, scriptPath] of Object.entries(enabledIf)) {\n if (scriptPath && scriptPath !== 'false') {\n const resolved = resolveScriptPath(scriptPath, cwd);\n if (typeof resolved === 'undefined') {\n warnProfilesIssue(\n `Could not resolve enabledIf script for \"${name}\": ${scriptPath}`,\n );\n } else {\n const passed = await runScript(resolved);\n if (passed) results.push(name);\n }\n }\n }\n return results;\n};\n\n/**\n * Resolves the names of the enabled profiles, from the `enabled` list and the `enabledIf`\n * scripts of the `pi.profiles` config. If the config is invalid, it reports it through\n * `onError` and no profile is enabled.\n *\n * This is the single source of truth for which profiles are active: the extension and any\n * other extension that needs to know them call this same function.\n *\n * @param options The directory to resolve from, and where to report non-fatal issues.\n * @returns The names of the enabled profiles, without duplicates.\n */\nexport const resolveEnabledProfiles = async (\n options: ProfileLibOptions = {},\n): Promise<string[]> => {\n const cwd = options.cwd ?? process.cwd();\n const onError = options.onError ?? ((message: string) => warnProfilesIssue(message));\n\n try {\n const config = await getConfigValue(CONFIG_PATH, {\n cwd,\n schema: profilesConfigSchema,\n });\n if (!config) return [];\n const { enabled, enabledIf } = config;\n const conditional = await evaluateEnabledIf(enabledIf, cwd);\n return Array.from(new Set([...enabled, ...conditional]));\n } catch (error) {\n onError(\n `Invalid ${CONFIG_PATH} config; profiles skipped. ${ensureErrorMessage(error)}`,\n );\n return [];\n }\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "@homer0/pi-profiles",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "description": "Profiles for Pi: toggle sets of skills, extensions, and context per project or machine",
6
+ "author": "Leonardo Apiwan (@homer0) <me@homer0.com>",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi",
11
+ "pi-extension",
12
+ "profiles"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/homer0/packages.git",
17
+ "directory": "packages/public/pi-profiles"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "provenance": true
22
+ },
23
+ "main": "./dist/index.js",
24
+ "types": "./src/index.ts",
25
+ "files": [
26
+ "dist/",
27
+ "src/",
28
+ "!src/**/*.test.ts"
29
+ ],
30
+ "exports": {
31
+ ".": {
32
+ "types": "./src/index.ts",
33
+ "import": "./dist/index.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "engines": {
38
+ "node": ">=24"
39
+ },
40
+ "pi": {
41
+ "extensions": [
42
+ "./dist/extensions/profiles.js"
43
+ ]
44
+ },
45
+ "dependencies": {
46
+ "@homer0/pi-config": "^1.1.0",
47
+ "jiti": "2.7.0",
48
+ "zod": "4.6.7"
49
+ },
50
+ "peerDependencies": {
51
+ "@earendil-works/pi-coding-agent": ">1.0.0"
52
+ },
53
+ "devDependencies": {
54
+ "@earendil-works/pi-coding-agent": "catalog:",
55
+ "@homer0/oxlint-config": "3.0.0",
56
+ "@homer0/tsconfig": "6.0.0",
57
+ "@types/node": "catalog:",
58
+ "@vitest/coverage-v8": "catalog:",
59
+ "oxlint": "catalog:",
60
+ "tsdown": "catalog:",
61
+ "typescript": "catalog:",
62
+ "vitest": "catalog:"
63
+ },
64
+ "scripts": {
65
+ "lint": "oxlint .",
66
+ "lint:all": "oxlint .",
67
+ "lint:file": "oxlint $@",
68
+ "test": "vitest run $@",
69
+ "types:check": "tsc --noEmit",
70
+ "build": "tsdown",
71
+ "prepublishOnly": "pnpm run build"
72
+ },
73
+ "gitHead": "cc45fa7b2a70d060ac154c13d4dcaed6ca69334f"
74
+ }
@@ -0,0 +1,62 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import type { ProfilesRuntime } from '../types.js';
3
+
4
+ /** Module shape of the lazily loaded handlers. */
5
+ type HandlersModule = typeof import('../handlers.js');
6
+
7
+ /** The promise of the handlers module, shared by every event once it's been requested. */
8
+ let handlersModule: Promise<HandlersModule> | undefined;
9
+
10
+ /**
11
+ * Lazy-loads all the handlers through a single shared `import()`. Pi fires several events
12
+ * at the start of a session, and jiti evaluates lazily loaded modules asynchronously,
13
+ * handing partial exports to any `import()` of a module (or of a dependency shared by
14
+ * several handlers) that is still evaluating. Loading everything through one memoized
15
+ * promise makes overlapping events wait for the same evaluation. A failed load is not
16
+ * cached, so the next event retries.
17
+ *
18
+ * @returns The module with all the handlers.
19
+ */
20
+ const loadHandlers = (): Promise<HandlersModule> => {
21
+ handlersModule ??= import('../handlers.js').catch((error: unknown) => {
22
+ handlersModule = undefined;
23
+ throw error;
24
+ });
25
+ return handlersModule;
26
+ };
27
+
28
+ /**
29
+ * Registers the profile resource and context handling with Pi. The callbacks only
30
+ * lazy-load their implementation, through the shared handlers loader.
31
+ *
32
+ * @param pi The Pi extension API.
33
+ */
34
+ const profiles = (pi: ExtensionAPI): void => {
35
+ const runtime: ProfilesRuntime = { pi, state: { sessionProfiles: [] } };
36
+
37
+ pi.on('session_start', async (event, ctx) => {
38
+ runtime.state.sessionProfiles = [];
39
+ const { handleSessionStart } = await loadHandlers();
40
+ await handleSessionStart({ runtime, event, ctx });
41
+ });
42
+
43
+ pi.on('resources_discover', async (_event, ctx) => {
44
+ const { handleResourcesDiscover } = await loadHandlers();
45
+ return handleResourcesDiscover({ ctx });
46
+ });
47
+
48
+ pi.on('before_agent_start', async (event, ctx) => {
49
+ const { handleBeforeAgentStart } = await loadHandlers();
50
+ return handleBeforeAgentStart({ event, ctx });
51
+ });
52
+
53
+ pi.registerCommand('profiles-status', {
54
+ description: 'Show currently loaded profiles',
55
+ handler: async (_args, ctx) => {
56
+ const { handleProfilesStatus } = await loadHandlers();
57
+ handleProfilesStatus({ runtime, ctx });
58
+ },
59
+ });
60
+ };
61
+
62
+ export default profiles;