zooid 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { installPiExtension, resolvePiAgentDir } from './pi-extension-install.js'
6
+
7
+ const dirs: string[] = []
8
+ const scratch = () => { const dir = mkdtempSync(join(tmpdir(), 'zooid-pi-')); dirs.push(dir); return dir }
9
+ afterEach(() => dirs.splice(0).forEach((dir) => rmSync(dir, { recursive: true, force: true })))
10
+
11
+ describe('resolvePiAgentDir', () => {
12
+ it('resolves a relative PI_CODING_AGENT_DIR against each agent workdir', () => {
13
+ expect(
14
+ resolvePiAgentDir({
15
+ agentWorkdir: '/proj/agents/alice',
16
+ daemonHome: '/home/op',
17
+ env: { PI_CODING_AGENT_DIR: '.pi-agent' },
18
+ }),
19
+ ).toEqual({ dir: '/proj/agents/alice/.pi-agent', scope: 'project' })
20
+ })
21
+
22
+ it('honours an absolute override and falls back to the operator home', () => {
23
+ expect(
24
+ resolvePiAgentDir({
25
+ agentWorkdir: '/proj/agents/alice',
26
+ daemonHome: '/home/op',
27
+ env: { PI_CODING_AGENT_DIR: '/shared/pi' },
28
+ }),
29
+ ).toEqual({ dir: '/shared/pi', scope: 'project' })
30
+ expect(
31
+ resolvePiAgentDir({ agentWorkdir: '/proj/agents/alice', daemonHome: '/home/op', env: {} }),
32
+ ).toEqual({ dir: '/home/op/.pi/agent', scope: 'home' })
33
+ })
34
+ })
35
+
36
+ describe('installPiExtension', () => {
37
+ it('only installs into an existing Pi home and preserves other extensions', () => {
38
+ const home = scratch(); const source = join(scratch(), 'bundle.js')
39
+ writeFileSync(source, '// bundle')
40
+ const agentDir = join(home, '.pi', 'agent')
41
+ expect(installPiExtension({ agentDir, bundlePath: source })).toMatchObject({ status: 'skipped' })
42
+ expect(existsSync(join(home, '.pi'))).toBe(false)
43
+ const extensions = join(agentDir, 'extensions')
44
+ mkdirSync(extensions, { recursive: true }); writeFileSync(join(extensions, 'user.js'), '// user')
45
+ expect(installPiExtension({ agentDir, bundlePath: source })).toMatchObject({ status: 'installed' })
46
+ expect(readFileSync(join(extensions, 'zooid-tasks.js'), 'utf8')).toBe('// bundle')
47
+ expect(readFileSync(join(extensions, 'user.js'), 'utf8')).toBe('// user')
48
+ expect(installPiExtension({ agentDir, bundlePath: source })).toMatchObject({ status: 'unchanged' })
49
+ })
50
+
51
+ it('creates a project agent dir that does not exist yet', () => {
52
+ const workdir = scratch(); const source = join(scratch(), 'bundle.js')
53
+ writeFileSync(source, '// bundle')
54
+ const { dir } = resolvePiAgentDir({
55
+ agentWorkdir: workdir,
56
+ daemonHome: '/home/op',
57
+ env: { PI_CODING_AGENT_DIR: '.pi-agent' },
58
+ })
59
+ expect(installPiExtension({ agentDir: dir, bundlePath: source, createMissing: true })).toMatchObject({
60
+ status: 'installed',
61
+ target: join(workdir, '.pi-agent', 'extensions', 'zooid-tasks.js'),
62
+ })
63
+ expect(readFileSync(join(workdir, '.pi-agent', 'extensions', 'zooid-tasks.js'), 'utf8')).toBe('// bundle')
64
+ })
65
+ })
@@ -0,0 +1,56 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, isAbsolute, join, resolve } from 'node:path'
3
+
4
+ export interface InstallPiExtensionResult {
5
+ status: 'installed' | 'unchanged' | 'skipped'
6
+ reason?: string
7
+ target?: string
8
+ }
9
+
10
+ export interface PiAgentDir {
11
+ dir: string
12
+ /** `project` dirs are ours to create; `home` is the operator's ~/.pi. */
13
+ scope: 'project' | 'home'
14
+ }
15
+
16
+ /**
17
+ * Resolve pi's agent dir the way pi does: PI_CODING_AGENT_DIR when set,
18
+ * otherwise ~/.pi/agent. `zooid init` writes that variable as a *relative*
19
+ * path on purpose (ZOD075) so one value is correct under both runtimes — it
20
+ * resolves against the agent's cwd, and `agents/<name>` locally and
21
+ * `/workspace` in a container are the same directory. We always resolve
22
+ * against the host workdir, which is the host side of that same mount.
23
+ */
24
+ export function resolvePiAgentDir(opts: {
25
+ agentWorkdir: string
26
+ daemonHome: string
27
+ env?: { PI_CODING_AGENT_DIR?: string | undefined }
28
+ }): PiAgentDir {
29
+ const override = opts.env?.PI_CODING_AGENT_DIR
30
+ if (override) {
31
+ return {
32
+ dir: isAbsolute(override) ? override : resolve(opts.agentWorkdir, override),
33
+ scope: 'project',
34
+ }
35
+ }
36
+ return { dir: join(opts.daemonHome, '.pi', 'agent'), scope: 'home' }
37
+ }
38
+
39
+ /** Install our bundle without replacing an operator's existing Pi extensions. */
40
+ export function installPiExtension(opts: {
41
+ agentDir: string
42
+ bundlePath: string
43
+ /** Project dirs belong to the workspace, so we may create them. ~/.pi is the
44
+ * operator's: install into it only if they already use pi. */
45
+ createMissing?: boolean
46
+ }): InstallPiExtensionResult {
47
+ if (!opts.createMissing && !existsSync(opts.agentDir)) {
48
+ return { status: 'skipped', reason: 'no Pi home' }
49
+ }
50
+ const target = join(opts.agentDir, 'extensions', 'zooid-tasks.js')
51
+ mkdirSync(dirname(target), { recursive: true })
52
+ const source = readFileSync(opts.bundlePath)
53
+ if (existsSync(target) && readFileSync(target).equals(source)) return { status: 'unchanged', target }
54
+ writeFileSync(target, source)
55
+ return { status: 'installed', target }
56
+ }