@foundation0/api 1.1.2 → 1.1.4

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/agents.ts CHANGED
@@ -39,7 +39,7 @@ interface ActiveConfigInput {
39
39
  required: boolean
40
40
  }
41
41
 
42
- const CLI_NAME = 'example'
42
+ const CLI_NAME = 'f0'
43
43
  const AGENT_INITIAL_VERSION = 'v0.0.1'
44
44
  const DEFAULT_SKILL_NAME = 'coding-standards'
45
45
  const VERSION_RE = 'v?\\d+(?:\\.\\d+){2,}'
@@ -58,7 +58,7 @@ export function usage(): string {
58
58
  `Use --latest to resolve /file-name to the latest version.\n` +
59
59
  `The active file created is [file].active.<ext>.\n` +
60
60
  `Use "run" to start codex with developer_instructions from system/prompt.ts.\n` +
61
- `Set EXAMPLE_CODEX_BIN to pin a specific codex binary.\n`
61
+ `Set F0_CODEX_BIN (or EXAMPLE_CODEX_BIN) to pin a specific codex binary.\n`
62
62
  }
63
63
 
64
64
  export function resolveAgentsRoot(processRoot: string = process.cwd()): string {
@@ -460,9 +460,9 @@ export function setActiveLink(sourceFile: string, activeFile: string): 'symlink'
460
460
 
461
461
  fs.mkdirSync(path.dirname(activeFile), { recursive: true })
462
462
 
463
- if (fs.existsSync(activeFile)) {
464
- fs.rmSync(activeFile, { force: true })
465
- }
463
+ // Remove the active pointer first. Using rmSync directly (instead of existsSync)
464
+ // ensures we also clear broken symlinks (existsSync returns false for them).
465
+ fs.rmSync(activeFile, { force: true })
466
466
 
467
467
  const linkTarget = path.relative(path.dirname(activeFile), sourceFile)
468
468
 
@@ -643,7 +643,7 @@ type SpawnCodexResult = {
643
643
  type SpawnCodexFn = (args: string[]) => SpawnCodexResult
644
644
 
645
645
  function getCodexCommand(): string {
646
- const override = process.env.EXAMPLE_CODEX_BIN?.trim()
646
+ const override = process.env.F0_CODEX_BIN?.trim() ?? process.env.EXAMPLE_CODEX_BIN?.trim()
647
647
  if (override) {
648
648
  return override
649
649
  }
@@ -666,6 +666,7 @@ const defaultSpawnCodex: SpawnCodexFn = (args) => {
666
666
  const err = primary.error as NodeJS.ErrnoException
667
667
  if (
668
668
  process.platform === 'win32'
669
+ && !process.env.F0_CODEX_BIN
669
670
  && !process.env.EXAMPLE_CODEX_BIN
670
671
  && primaryCommand === 'codex.cmd'
671
672
  && err.code === 'ENOENT'
package/git.ts CHANGED
@@ -5,7 +5,7 @@ export type {
5
5
  GitServiceApi,
6
6
  GitServiceApiExecutionResult,
7
7
  GitServiceApiMethod,
8
- } from '../git/packages/git/src/index.ts'
8
+ } from '@foundation0/git'
9
9
 
10
10
  export {
11
11
  attachGitLabelManagementApi,
@@ -17,4 +17,4 @@ export {
17
17
  extractDependencyIssueNumbers,
18
18
  resolveProjectRepoIdentity,
19
19
  syncIssueDependencies,
20
- } from '../git/packages/git/src/index.ts'
20
+ } from '@foundation0/git'
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { curl } from './curl'
3
+
4
+ describe('api/libs/curl', () => {
5
+ const withServer = async <T>(fn: (baseUrl: string) => Promise<T>): Promise<T> => {
6
+ const server = Bun.serve({
7
+ port: 0,
8
+ fetch: async (req) => {
9
+ const url = new URL(req.url)
10
+
11
+ if (url.pathname === '/hello') {
12
+ return new Response('hello', { headers: { 'x-test': '1' } })
13
+ }
14
+
15
+ if (url.pathname === '/echo') {
16
+ const body = await req.text()
17
+ return Response.json({
18
+ method: req.method,
19
+ body,
20
+ contentType: req.headers.get('content-type'),
21
+ ua: req.headers.get('user-agent'),
22
+ })
23
+ }
24
+
25
+ if (url.pathname === '/redirect') {
26
+ return new Response('nope', { status: 302, headers: { location: '/hello' } })
27
+ }
28
+
29
+ return new Response('not found', { status: 404 })
30
+ },
31
+ })
32
+
33
+ try {
34
+ return await fn(`http://127.0.0.1:${server.port}`)
35
+ } finally {
36
+ server.stop(true)
37
+ }
38
+ }
39
+
40
+ it('fetches a URL and returns body on stdout', async () => {
41
+ await withServer(async (baseUrl) => {
42
+ const result = await curl(`${baseUrl}/hello`)
43
+ expect(result.exitCode).toBe(0)
44
+ expect(result.timedOut).toBe(false)
45
+ expect(result.stdout).toBe('hello')
46
+ expect(result.results?.[0]?.httpCode).toBe(200)
47
+ })
48
+ })
49
+
50
+ it('defaults scheme-less host URLs to http://', async () => {
51
+ await withServer(async (baseUrl) => {
52
+ const url = baseUrl.replace('http://', '')
53
+ const result = await curl(`${url}/hello`)
54
+ expect(result.exitCode).toBe(0)
55
+ expect(result.stdout).toBe('hello')
56
+ })
57
+ })
58
+
59
+ it('supports -i to include response headers', async () => {
60
+ await withServer(async (baseUrl) => {
61
+ const result = await curl('-i', `${baseUrl}/hello`)
62
+ expect(result.exitCode).toBe(0)
63
+ expect(result.stdout).toContain('HTTP/1.1 200')
64
+ expect(result.stdout.toLowerCase()).toContain('x-test: 1')
65
+ expect(result.stdout).toContain('hello')
66
+ })
67
+ })
68
+
69
+ it('supports -d for POST body and defaults method to POST', async () => {
70
+ await withServer(async (baseUrl) => {
71
+ const result = await curl('-d', 'a=1', `${baseUrl}/echo`)
72
+ expect(result.exitCode).toBe(0)
73
+ const payload = JSON.parse(result.stdout)
74
+ expect(payload.method).toBe('POST')
75
+ expect(payload.body).toBe('a=1')
76
+ expect(payload.contentType).toContain('application/x-www-form-urlencoded')
77
+ })
78
+ })
79
+
80
+ it('supports -G with -d to apply data as query params', async () => {
81
+ await withServer(async (baseUrl) => {
82
+ const result = await curl('-G', '-d', 'a=1', `${baseUrl}/hello`)
83
+ expect(result.exitCode).toBe(0)
84
+ expect(result.stdout).toBe('hello')
85
+ })
86
+ })
87
+
88
+ it('supports -L to follow redirects', async () => {
89
+ await withServer(async (baseUrl) => {
90
+ const result = await curl('-L', `${baseUrl}/redirect`)
91
+ expect(result.exitCode).toBe(0)
92
+ expect(result.results?.[0]?.redirectCount).toBe(1)
93
+ expect(result.results?.[0]?.urlEffective).toContain('/hello')
94
+ expect(result.stdout).toBe('hello')
95
+ })
96
+ })
97
+
98
+ it('supports -f to fail on HTTP >= 400 and suppress body', async () => {
99
+ await withServer(async (baseUrl) => {
100
+ const result = await curl('-f', `${baseUrl}/missing`)
101
+ expect(result.exitCode).toBe(22)
102
+ expect(result.stdout).toBe('')
103
+ expect(result.stderr).toContain('returned error')
104
+ })
105
+ })
106
+
107
+ it('supports -w to write out %{http_code}', async () => {
108
+ await withServer(async (baseUrl) => {
109
+ const result = await curl('-w', '%{http_code}', `${baseUrl}/hello`)
110
+ expect(result.exitCode).toBe(0)
111
+ expect(result.stdout).toBe('hello200')
112
+ })
113
+ })
114
+
115
+ it('accepts trailing tool options object without turning it into a curl arg', async () => {
116
+ await withServer(async (baseUrl) => {
117
+ const result = await curl(`${baseUrl}/hello`, { timeoutMs: 10_000 })
118
+ expect(result.exitCode).toBe(0)
119
+ expect(result.args).toEqual([`${baseUrl}/hello`])
120
+ })
121
+ })
122
+
123
+ it('returns usage exit code for unsupported flags', async () => {
124
+ await withServer(async (baseUrl) => {
125
+ const result = await curl('--version', `${baseUrl}/hello`)
126
+ expect(result.exitCode).toBe(2)
127
+ expect(result.stderr).toContain('Unsupported curl option')
128
+ })
129
+ })
130
+ })