Skip to content

Commit c35b42b

Browse files
fix: make runtime examples self-contained (#71)
Use managed temporary directories and wait for asynchronous spec handoffs so examples run reliably with current CLI behavior. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent d960f18 commit c35b42b

4 files changed

Lines changed: 594 additions & 19 deletions

File tree

examples/daemon-multi-session.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
/**
22
* Daemon: multiple concurrent sessions.
33
*
4-
* Connects to a local daemon, creates two sessions in separate /tmp
5-
* directories, and runs them concurrently over a single WebSocket
6-
* connection.
4+
* Connects to a local daemon, creates two sessions in separate temporary
5+
* directories, and runs them concurrently over a single WebSocket connection.
76
*
87
* Usage:
98
* npx tsx examples/daemon-multi-session.ts
@@ -13,7 +12,31 @@
1312
* example skips itself when the env var is unset.
1413
*/
1514

16-
import { connectDaemon, DroidMessageType } from '@factory/droid-sdk';
15+
import { mkdtemp, mkdir, readFile, rm } from 'node:fs/promises';
16+
import { tmpdir } from 'node:os';
17+
import { join } from 'node:path';
18+
19+
import {
20+
connectDaemon,
21+
DroidMessageType,
22+
ToolConfirmationOutcome,
23+
ToolConfirmationType,
24+
type ToolConfirmationDetails,
25+
} from '@factory/droid-sdk';
26+
27+
function canWriteFile(
28+
filePath: string,
29+
details: ToolConfirmationDetails
30+
): boolean {
31+
switch (details.type) {
32+
case ToolConfirmationType.Create:
33+
case ToolConfirmationType.Edit:
34+
case ToolConfirmationType.ApplyPatch:
35+
return details.filePath === filePath;
36+
default:
37+
return false;
38+
}
39+
}
1740

1841
async function main(): Promise<void> {
1942
if (!process.env.FACTORY_API_KEY) {
@@ -27,13 +50,36 @@ async function main(): Promise<void> {
2750
console.log('Connecting to local daemon...\n');
2851
const daemon = await connectDaemon({ apiKey: process.env.FACTORY_API_KEY });
2952
console.log('Connected!\n');
53+
const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-daemon-'));
54+
const frontendDir = join(tempDir, 'frontend');
55+
const backendDir = join(tempDir, 'backend');
56+
const frontendFile = join(frontendDir, 'hello.md');
57+
const backendFile = join(backendDir, 'notes.md');
58+
await Promise.all([
59+
mkdir(frontendDir, { recursive: true }),
60+
mkdir(backendDir, { recursive: true }),
61+
]);
3062

3163
try {
3264
const frontend = await daemon.createSession({
33-
cwd: '/tmp/daemon-test-frontend',
65+
cwd: frontendDir,
66+
permissionHandler(params) {
67+
return params.toolUses.every((item) =>
68+
canWriteFile(frontendFile, item.details)
69+
)
70+
? ToolConfirmationOutcome.ProceedOnce
71+
: ToolConfirmationOutcome.Cancel;
72+
},
3473
});
3574
const backend = await daemon.createSession({
36-
cwd: '/tmp/daemon-test-backend',
75+
cwd: backendDir,
76+
permissionHandler(params) {
77+
return params.toolUses.every((item) =>
78+
canWriteFile(backendFile, item.details)
79+
)
80+
? ToolConfirmationOutcome.ProceedOnce
81+
: ToolConfirmationOutcome.Cancel;
82+
},
3783
});
3884

3985
console.log('Two sessions created. Running concurrently...\n');
@@ -76,11 +122,17 @@ async function main(): Promise<void> {
76122
]);
77123
} finally {
78124
await daemon.close();
125+
try {
126+
const [greeting, notes] = await Promise.all([
127+
readFile(frontendFile, 'utf8'),
128+
readFile(backendFile, 'utf8'),
129+
]);
130+
console.log(`\n=== hello.md ===\n${greeting.trim()}`);
131+
console.log(`\n=== notes.md ===\n${notes.trim()}`);
132+
} finally {
133+
await rm(tempDir, { recursive: true, force: true });
134+
}
79135
}
80-
81-
console.log(
82-
'\nDone. Check /tmp/daemon-test-frontend/hello.md and /tmp/daemon-test-backend/notes.md'
83-
);
84136
}
85137

86138
main().catch((err: unknown) => {

examples/spec-mode-new-session.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
*/
1414

1515
import { mkdtemp, readFile, rm } from 'node:fs/promises';
16-
import { tmpdir } from 'node:os';
1716
import { join } from 'node:path';
1817

1918
import {
@@ -24,9 +23,30 @@ import {
2423
createSession,
2524
} from '@factory/droid-sdk';
2625

27-
const tempDir = await mkdtemp(join(tmpdir(), 'droid-sdk-spec-'));
26+
const tempDir = await mkdtemp(join(process.cwd(), '.droid-sdk-spec-'));
2827
const outputPath = join(tempDir, 'hello.txt');
2928

29+
async function waitForFile(path: string, timeoutMs = 120_000): Promise<string> {
30+
const deadline = Date.now() + timeoutMs;
31+
32+
while (Date.now() < deadline) {
33+
try {
34+
return await readFile(path, 'utf8');
35+
} catch (error) {
36+
if (
37+
!(error instanceof Error) ||
38+
!('code' in error) ||
39+
error.code !== 'ENOENT'
40+
) {
41+
throw error;
42+
}
43+
await new Promise<void>((resolve) => setTimeout(resolve, 500));
44+
}
45+
}
46+
47+
throw new Error(`Timed out waiting for ${path}`);
48+
}
49+
3050
try {
3151
const session = await createSession({
3252
apiKey: process.env.FACTORY_API_KEY!,
@@ -58,18 +78,16 @@ try {
5878
)) {
5979
// Consume the stream until the handoff implementation finishes.
6080
}
61-
} finally {
62-
await session.close();
63-
}
6481

65-
try {
66-
console.log(await readFile(outputPath, 'utf8'));
67-
} catch {
82+
console.log(await waitForFile(outputPath));
83+
} catch (error) {
6884
console.error(
6985
`Expected ${outputPath} to exist after the turn, but it was not ` +
70-
'created.'
86+
`created: ${error instanceof Error ? error.message : String(error)}`
7187
);
7288
process.exitCode = 1;
89+
} finally {
90+
await session.close();
7391
}
7492
} finally {
7593
await rm(tempDir, { recursive: true, force: true });

0 commit comments

Comments
 (0)