@agfpd/iapeer 0.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.
Files changed (63) hide show
  1. package/bin/iapeer +25 -0
  2. package/package.json +37 -0
  3. package/src/cli/cli.test.ts +130 -0
  4. package/src/cli/index.ts +608 -0
  5. package/src/cli/listTui.test.ts +70 -0
  6. package/src/cli/listTui.ts +165 -0
  7. package/src/codec/codec.test.ts +271 -0
  8. package/src/codec/index.ts +217 -0
  9. package/src/core/constants.test.ts +21 -0
  10. package/src/core/constants.ts +180 -0
  11. package/src/core/errors.ts +20 -0
  12. package/src/core/index.ts +3 -0
  13. package/src/core/normalize.test.ts +98 -0
  14. package/src/core/normalize.ts +89 -0
  15. package/src/core/socket.ts +63 -0
  16. package/src/create/create.test.ts +143 -0
  17. package/src/create/index.ts +178 -0
  18. package/src/daemon/daemon-http.test.ts +114 -0
  19. package/src/daemon/daemon.test.ts +103 -0
  20. package/src/daemon/index.ts +439 -0
  21. package/src/daemon/main.test.ts +194 -0
  22. package/src/daemon/main.ts +230 -0
  23. package/src/enable/enable.test.ts +92 -0
  24. package/src/enable/index.ts +381 -0
  25. package/src/identity/identity.test.ts +262 -0
  26. package/src/identity/index.ts +603 -0
  27. package/src/index.ts +27 -0
  28. package/src/init/index.ts +408 -0
  29. package/src/init/init.test.ts +171 -0
  30. package/src/init/runtime-resolve.test.ts +49 -0
  31. package/src/install/index.ts +84 -0
  32. package/src/install/install.test.ts +31 -0
  33. package/src/launch/adapters/claude.ts +250 -0
  34. package/src/launch/adapters/codex.ts +329 -0
  35. package/src/launch/adapters/notifier.ts +90 -0
  36. package/src/launch/adapters/telegram.ts +130 -0
  37. package/src/launch/bootstrap.test.ts +56 -0
  38. package/src/launch/composeSystemPrompt.layers.test.ts +319 -0
  39. package/src/launch/composeSystemPrompt.test.ts +98 -0
  40. package/src/launch/composeSystemPrompt.ts +261 -0
  41. package/src/launch/index.ts +253 -0
  42. package/src/launch/launch.test.ts +233 -0
  43. package/src/launch/launchd.test.ts +363 -0
  44. package/src/launch/launchd.ts +375 -0
  45. package/src/launch/launchdRun.ts +168 -0
  46. package/src/launch/sockdir.test.ts +70 -0
  47. package/src/launch/types.ts +300 -0
  48. package/src/lifecycle/index.ts +840 -0
  49. package/src/lifecycle/lifecycle.test.ts +496 -0
  50. package/src/onboard/index.ts +135 -0
  51. package/src/onboard/onboard.test.ts +39 -0
  52. package/src/provision/index.ts +170 -0
  53. package/src/provision/provision.test.ts +104 -0
  54. package/src/registry/index.ts +453 -0
  55. package/src/registry/registry.test.ts +400 -0
  56. package/src/runtime/deploy.ts +230 -0
  57. package/src/runtime/index.ts +191 -0
  58. package/src/runtime/runtime.test.ts +226 -0
  59. package/src/storage/index.ts +331 -0
  60. package/src/storage/peers-home.test.ts +34 -0
  61. package/src/storage/storage.test.ts +65 -0
  62. package/src/transport/index.ts +522 -0
  63. package/tsconfig.json +17 -0
@@ -0,0 +1,170 @@
1
+ // Provision — the one-call peer-creation entrypoint. Composes the two foundation
2
+ // writes a new peer needs: identity.ensurePeerProfile (local <cwd>/.iapeer/
3
+ // peer-profile.json + scaffold + — for an INFRA runtime — the always-on launchd
4
+ // plist) and registry.upsertPeer (the global peers-profiles.json entry the daemon
5
+ // reads for its tool-list, caller resolution, and wake-on-miss findPeer).
6
+ //
7
+ // INFRA gotcha closed at this layer: a notifier/telegram always-on plist runs its
8
+ // launcher under launchd's MINIMAL PATH (no ~/.local/bin, ~/.bun/bin). So before
9
+ // writing the plist we RESOLVE the runtime launcher to an absolute path against the
10
+ // rich provisioning PATH and bake it in (NOTIFIER_RUNTIME_BIN / TELEGRAM_RUNTIME_BIN
11
+ // via installAlwaysOnPlist). If it does not resolve to an executable, we REFUSE to
12
+ // provision rather than create a peer whose always-on session crash-loops.
13
+
14
+ import { basename, resolve } from 'path'
15
+ import {
16
+ INFRA_RUNTIME_DEFAULT_BIN,
17
+ isInfraRuntime,
18
+ isRuntime,
19
+ normalizeNameCandidate,
20
+ type Intelligence,
21
+ type Runtime,
22
+ } from '../core/constants.ts'
23
+ import { IapError } from '../core/errors.ts'
24
+ import { peerProfilePath } from '../storage/index.ts'
25
+ import { ensurePeerProfile } from '../identity/index.ts'
26
+ import { readPeersIndex, upsertPeer } from '../registry/index.ts'
27
+ import { launchdPlistPath, resolveExecutable } from '../launch/launchd.ts'
28
+
29
+ export interface ProvisionPeerOptions {
30
+ /** The peer's working directory. personality defaults to normalized basename. */
31
+ cwd: string
32
+ runtime: Runtime
33
+ /** Explicit personality override (default: normalized basename(cwd)). */
34
+ personality?: string
35
+ description?: string
36
+ intelligence?: Intelligence
37
+ /** Infra runtime launcher (abs path or PATH name); resolved to an abs path. */
38
+ runtimeBin?: string
39
+ env?: NodeJS.ProcessEnv
40
+ warn?: (message: string) => void
41
+ }
42
+
43
+ export interface ProvisionResult {
44
+ personality: string
45
+ runtime: Runtime
46
+ cwd: string
47
+ profilePath: string
48
+ intelligence: Intelligence
49
+ /** For an infra runtime: the installed always-on plist path. */
50
+ plistPath?: string
51
+ /** For an infra runtime: the absolute launcher path baked into the plist. */
52
+ runtimeBin?: string
53
+ }
54
+
55
+ /**
56
+ * Provision a new peer in one call: local profile + scaffold (+ infra always-on
57
+ * plist with a pinned launcher) then the registry entry. Idempotent-ish: if the
58
+ * cwd already has a peer-profile.json, ensurePeerProfile returns it unchanged (no
59
+ * second plist install) and the registry is upserted (merge-with-existing).
60
+ */
61
+ export async function provisionPeer(opts: ProvisionPeerOptions): Promise<ProvisionResult> {
62
+ const env = opts.env ?? process.env
63
+ if (!isRuntime(opts.runtime)) {
64
+ throw new IapError(`invalid runtime "${opts.runtime}" — must match /^[a-z][a-z0-9]{0,31}$/`)
65
+ }
66
+ const cwd = resolve(opts.cwd)
67
+
68
+ // INFRA: resolve the launcher to an abs path NOW (rich provisioning PATH) and
69
+ // refuse to provision a crash-looper. Warm-on-demand runtimes need no plist.
70
+ let runtimeBin: string | undefined
71
+ if (isInfraRuntime(opts.runtime)) {
72
+ const want = opts.runtimeBin ?? INFRA_RUNTIME_DEFAULT_BIN[opts.runtime] ?? opts.runtime
73
+ runtimeBin = resolveExecutable(want, env)
74
+ if (!runtimeBin) {
75
+ throw new IapError(
76
+ `cannot provision infra peer "${opts.personality ?? normalizeNameCandidate(basename(cwd))}": ` +
77
+ `runtime launcher "${want}" not found on PATH or not executable. Install ${opts.runtime}-runtime ` +
78
+ `(or pass an absolute --bin) so the always-on plist resolves it under launchd's minimal PATH.`,
79
+ )
80
+ }
81
+ }
82
+
83
+ const peers = readPeersIndex({ env }).peers
84
+ // 1) local profile + scaffold + (infra) always-on plist with the pinned launcher.
85
+ const profile = ensurePeerProfile({
86
+ cwd,
87
+ runtime: opts.runtime,
88
+ env,
89
+ peers,
90
+ personality: opts.personality,
91
+ runtimeBin,
92
+ warn: opts.warn,
93
+ })
94
+ // 2) registry entry — without it the daemon does not see the peer (tool-list,
95
+ // caller resolution, wake-on-miss findPeer all read peers-profiles.json).
96
+ await upsertPeer(
97
+ {
98
+ personality: profile.personality,
99
+ runtime: opts.runtime,
100
+ cwd,
101
+ intelligence: opts.intelligence ?? profile.intelligence,
102
+ description: opts.description,
103
+ },
104
+ { env, warn: opts.warn },
105
+ )
106
+
107
+ return {
108
+ personality: profile.personality,
109
+ runtime: opts.runtime,
110
+ cwd,
111
+ profilePath: peerProfilePath(cwd),
112
+ intelligence: opts.intelligence ?? profile.intelligence,
113
+ plistPath: isInfraRuntime(opts.runtime) ? launchdPlistPath(profile.personality, env) : undefined,
114
+ runtimeBin,
115
+ }
116
+ }
117
+
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+ // CLI — `bun src/provision/index.ts <cwd> <runtime> [--personality p] [--bin path]
120
+ // [--description d] [--intelligence i]`
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+
123
+ function parseFlags(argv: string[]): { positionals: string[]; flags: Record<string, string> } {
124
+ const positionals: string[] = []
125
+ const flags: Record<string, string> = {}
126
+ for (let i = 0; i < argv.length; i++) {
127
+ const a = argv[i]
128
+ if (a.startsWith('--')) {
129
+ flags[a.slice(2)] = argv[++i] ?? ''
130
+ } else {
131
+ positionals.push(a)
132
+ }
133
+ }
134
+ return { positionals, flags }
135
+ }
136
+
137
+ if (import.meta.main) {
138
+ const { positionals, flags } = parseFlags(process.argv.slice(2))
139
+ const [cwd, runtime] = positionals
140
+ if (!cwd || !runtime) {
141
+ process.stderr.write(
142
+ 'usage: provision-peer <cwd> <runtime> [--personality <p>] [--bin <abs-launcher>] ' +
143
+ '[--description <d>] [--intelligence artificial|natural|absent]\n',
144
+ )
145
+ process.exit(2)
146
+ }
147
+ provisionPeer({
148
+ cwd,
149
+ runtime,
150
+ personality: flags.personality,
151
+ description: flags.description,
152
+ intelligence: flags.intelligence as Intelligence | undefined,
153
+ runtimeBin: flags.bin,
154
+ warn: m => process.stderr.write(`warn: ${m}\n`),
155
+ })
156
+ .then(r => {
157
+ process.stdout.write(
158
+ `provisioned peer "${r.personality}" (${r.runtime}, ${r.intelligence})\n` +
159
+ ` profile: ${r.profilePath}\n` +
160
+ ` registry: peers-profiles.json updated\n` +
161
+ (r.plistPath ? ` plist: ${r.plistPath}\n launcher: ${r.runtimeBin}\n` : '') +
162
+ (r.plistPath ? ` (plist written, NOT loaded — run: launchctl bootstrap gui/$(id -u) ${r.plistPath})\n` : ''),
163
+ )
164
+ process.exit(0)
165
+ })
166
+ .catch(e => {
167
+ process.stderr.write(`provision failed: ${e instanceof Error ? e.message : String(e)}\n`)
168
+ process.exit(1)
169
+ })
170
+ }
@@ -0,0 +1,104 @@
1
+ // provisionPeer — one-call peer creation: profile + registry (+ infra plist with a
2
+ // PINNED launcher). All registry/plist writes go under IAPEER_ROOT /
3
+ // IAPEER_LAUNCHAGENTS_DIR temp dirs so the suite never touches the live fleet.
4
+
5
+ import { afterEach, describe, expect, test } from 'bun:test'
6
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
7
+ import { tmpdir } from 'os'
8
+ import { join } from 'path'
9
+ import { provisionPeer } from './index.ts'
10
+ import { peerProfilePath } from '../storage/index.ts'
11
+ import { readPeerProfile } from '../identity/index.ts'
12
+ import { findPeer, readPeersIndex } from '../registry/index.ts'
13
+ import { isFoundationOwnedPlist, launchdPlistPath } from '../launch/index.ts'
14
+
15
+ const roots: string[] = []
16
+ function mkTmp(): string {
17
+ const d = mkdtempSync(join(tmpdir(), 'iapeer-prov-'))
18
+ roots.push(d)
19
+ return d
20
+ }
21
+ afterEach(() => {
22
+ while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
23
+ })
24
+
25
+ function fakeBinDir(name = 'notifier-runtime'): { dir: string; bin: string } {
26
+ const dir = mkTmp()
27
+ const bin = join(dir, name)
28
+ writeFileSync(bin, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
29
+ return { dir, bin }
30
+ }
31
+ function envFor(root: string, path?: string): NodeJS.ProcessEnv {
32
+ return {
33
+ IAPEER_ROOT: join(root, 'iapeer'),
34
+ IAPEER_LAUNCHAGENTS_DIR: join(root, 'LA'),
35
+ HOME: root,
36
+ ...(path ? { PATH: path } : {}),
37
+ } as NodeJS.ProcessEnv
38
+ }
39
+
40
+ describe('provisionPeer', () => {
41
+ test('infra (notifier): writes profile + registry + plist with a pinned abs launcher', async () => {
42
+ const root = mkTmp()
43
+ const { dir: bindir, bin } = fakeBinDir()
44
+ const env = envFor(root, bindir)
45
+ const cwd = join(root, 'timer') // basename → personality 'timer'
46
+
47
+ const r = await provisionPeer({ cwd, runtime: 'notifier', env })
48
+
49
+ expect(r.personality).toBe('timer')
50
+ expect(r.intelligence).toBe('absent') // notifier zone default
51
+ expect(r.runtimeBin).toBe(bin)
52
+ // local profile
53
+ expect(existsSync(peerProfilePath(cwd))).toBe(true)
54
+ expect(readPeerProfile(cwd)!.runtime).toBe('notifier')
55
+ // registry entry (daemon reads this for tool-list / findPeer / wake)
56
+ expect(findPeer(readPeersIndex({ env }), 'timer')?.cwd).toBe(cwd)
57
+ // plist pins the launcher to an abs path (launchd minimal PATH safe)
58
+ const plist = launchdPlistPath('timer', env)
59
+ expect(existsSync(plist)).toBe(true)
60
+ expect(isFoundationOwnedPlist(plist)).toBe(true)
61
+ const xml = readFileSync(plist, 'utf8')
62
+ expect(xml).toContain('<key>NOTIFIER_RUNTIME_BIN</key>')
63
+ expect(xml).toContain(`<string>${bin}</string>`)
64
+ })
65
+
66
+ test('infra with unresolvable launcher → REFUSED (no profile, no registry, no plist)', async () => {
67
+ const root = mkTmp()
68
+ const env = envFor(root, join(root, 'empty')) // PATH dir without notifier-runtime
69
+ const cwd = join(root, 'timer')
70
+
71
+ await expect(provisionPeer({ cwd, runtime: 'notifier', env })).rejects.toThrow(/not found on PATH|launcher/i)
72
+
73
+ expect(existsSync(peerProfilePath(cwd))).toBe(false)
74
+ expect(findPeer(readPeersIndex({ env }), 'timer')).toBeNull()
75
+ expect(existsSync(launchdPlistPath('timer', env))).toBe(false)
76
+ })
77
+
78
+ test('warm-on-demand (claude): profile + registry, NO plist (no launcher needed)', async () => {
79
+ const root = mkTmp()
80
+ const env = envFor(root)
81
+ const cwd = join(root, 'worker')
82
+
83
+ const r = await provisionPeer({ cwd, runtime: 'claude', env })
84
+
85
+ expect(r.plistPath).toBeUndefined()
86
+ expect(r.runtimeBin).toBeUndefined()
87
+ expect(existsSync(peerProfilePath(cwd))).toBe(true)
88
+ expect(findPeer(readPeersIndex({ env }), 'worker')?.runtime).toBe('claude')
89
+ expect(existsSync(launchdPlistPath('worker', env))).toBe(false)
90
+ })
91
+
92
+ test('explicit personality override (not basename) provisions under that name', async () => {
93
+ const root = mkTmp()
94
+ const { dir: bindir } = fakeBinDir()
95
+ const env = envFor(root, bindir)
96
+ const cwd = join(root, 'some-dir')
97
+
98
+ const r = await provisionPeer({ cwd, runtime: 'notifier', personality: 'timer', env })
99
+
100
+ expect(r.personality).toBe('timer')
101
+ expect(existsSync(launchdPlistPath('timer', env))).toBe(true)
102
+ expect(findPeer(readPeersIndex({ env }), 'timer')?.cwd).toBe(cwd)
103
+ })
104
+ })