@factory/cli 0.1.2-dev.7 → 0.55.2
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/README.md +232 -18
- package/bin/droid +112 -0
- package/install.js +314 -0
- package/package.json +38 -95
- package/platform.js +166 -0
- package/bundle/droid.js +0 -1549
- package/bundle/index.js.map +0 -2584
package/README.md
CHANGED
|
@@ -31,19 +31,57 @@ src/
|
|
|
31
31
|
|
|
32
32
|
All dev tasks are exposed as **npm scripts** – never run compiled `.js` files directly.
|
|
33
33
|
|
|
34
|
-
| Purpose
|
|
35
|
-
|
|
|
36
|
-
| Start CLI (auto mode)
|
|
37
|
-
| Start with Node inspector
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
34
|
+
| Purpose | Command |
|
|
35
|
+
| -------------------------------- | --------------------- |
|
|
36
|
+
| Start CLI (auto mode) | `npm start` |
|
|
37
|
+
| Start with Node inspector | `npm run debug` |
|
|
38
|
+
| Set up shell alias for local dev | `npm run setup:alias` |
|
|
39
|
+
| Build SEA for current platform | `npm run build` |
|
|
40
|
+
| Build and run the binary | `npm run build:run` |
|
|
41
|
+
| Lint source | `npm run lint` |
|
|
42
|
+
| Type-check | `npm run typecheck` |
|
|
43
|
+
| Run tests | `npm test` |
|
|
44
|
+
| Produce executable bundle | `npm run bundle` |
|
|
45
|
+
| Clean build artifacts | `npm run clean` |
|
|
44
46
|
|
|
45
47
|
The `start`/`debug` scripts use **tsx** so you can edit TypeScript and restart instantly.
|
|
46
48
|
|
|
49
|
+
### Shell Alias Setup
|
|
50
|
+
|
|
51
|
+
For easier local development, you can set up a `droid` alias that runs the CLI from your current directory:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Add alias to ~/.zshrc (checks if already exists)
|
|
55
|
+
npm run setup:alias
|
|
56
|
+
|
|
57
|
+
# Start a new terminal or source the file
|
|
58
|
+
source ~/.zshrc
|
|
59
|
+
|
|
60
|
+
# Now you can run 'droid' from anywhere
|
|
61
|
+
droid
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Building Standalone Executables (SEA)
|
|
65
|
+
|
|
66
|
+
The CLI can be compiled into Single Executable Applications (SEA) for different platforms:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Build for your current platform automatically
|
|
70
|
+
npm run build
|
|
71
|
+
|
|
72
|
+
# Build and immediately run the binary
|
|
73
|
+
npm run build:run
|
|
74
|
+
|
|
75
|
+
# Build for specific platforms
|
|
76
|
+
npm run build:sea:mac:arm64 # macOS Apple Silicon
|
|
77
|
+
npm run build:sea:mac:x64 # macOS Intel
|
|
78
|
+
npm run build:sea:linux:arm64 # Linux ARM64
|
|
79
|
+
npm run build:sea:linux:x64 # Linux x64
|
|
80
|
+
npm run build:sea:windows:x64 # Windows x64
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The `build` script automatically detects your OS and architecture, then builds the appropriate binary to `dist/{platform}/{arch}/droid` (or `droid.exe` on Windows).
|
|
84
|
+
|
|
47
85
|
---
|
|
48
86
|
|
|
49
87
|
## 3 · Testing Both Modes Locally
|
|
@@ -90,7 +128,6 @@ The extra `--` after `npm start` passes subsequent flags to the CLI.
|
|
|
90
128
|
| Phase | Command(s) | Result |
|
|
91
129
|
| ----------- | ------------------------------------ | ---------------------------------------------- |
|
|
92
130
|
| **Dev** | `npm start` / `npm run debug` | Runs from TS sources with tsx, fast reload. |
|
|
93
|
-
| **Build** | `npm run build` | Compiles TS → `dist/`. |
|
|
94
131
|
| **Bundle** | `npm run bundle` (calls `build`) | Generates single executable `bundle/droid.js`. |
|
|
95
132
|
| **Publish** | `npm publish` (bundled in `prepare`) | Users install `droid` binary from npm. |
|
|
96
133
|
|
|
@@ -110,9 +147,59 @@ droid headless status
|
|
|
110
147
|
droid headless login
|
|
111
148
|
|
|
112
149
|
# Talk to Droid
|
|
113
|
-
droid headless
|
|
150
|
+
droid headless "Hello" --session-id dOLpXUI8ux6YdZrg3kCs
|
|
151
|
+
|
|
152
|
+
# Streaming input mode
|
|
153
|
+
# Send multiple messages over stdin, maintaining conversation state
|
|
154
|
+
echo '{"role":"user","content":"hello"}' | droid exec --input-format stream-json --output-format stream-json
|
|
114
155
|
```
|
|
115
156
|
|
|
157
|
+
### Streaming Input Mode (Droid Manager Integration)
|
|
158
|
+
|
|
159
|
+
The CLI supports a **streaming input mode** designed for Droid Manager integration, allowing a single process to handle multiple user messages while maintaining conversation state.
|
|
160
|
+
|
|
161
|
+
**Usage Pattern:**
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
// Spawn process once
|
|
165
|
+
const droid = spawn('droid', [
|
|
166
|
+
'exec',
|
|
167
|
+
'--input-format',
|
|
168
|
+
'stream-json',
|
|
169
|
+
'--output-format',
|
|
170
|
+
'stream-json',
|
|
171
|
+
'--auto',
|
|
172
|
+
'low',
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
// Send messages over time (JSONL format via stdin)
|
|
176
|
+
droid.stdin.write('{"role":"user","content":"hello"}\n');
|
|
177
|
+
|
|
178
|
+
// Read events from stdout (JSONL format)
|
|
179
|
+
droid.stdout.on('data', (chunk) => {
|
|
180
|
+
const events = chunk.toString().split('\n').filter(Boolean);
|
|
181
|
+
events.forEach((line) => {
|
|
182
|
+
const event = JSON.parse(line);
|
|
183
|
+
// Handle: user messages, assistant responses, tool calls, etc.
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Send another message later
|
|
188
|
+
droid.stdin.write('{"role":"user","content":"what can you do?"}\n');
|
|
189
|
+
|
|
190
|
+
// Close stdin when done to trigger graceful shutdown
|
|
191
|
+
droid.stdin.end();
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Key Points:**
|
|
195
|
+
|
|
196
|
+
- **Input**: JSONL (newline-delimited JSON) via stdin, each line is `Anthropic.MessageParam` with `role: "user"`
|
|
197
|
+
- **Output**: JSONL events via stdout (same format as `--output-format stream-json`)
|
|
198
|
+
- **stdin stays open** until explicitly closed by the caller
|
|
199
|
+
- **Messages processed sequentially** - one at a time
|
|
200
|
+
- **Session persists** across all messages in the process lifetime
|
|
201
|
+
- **Graceful shutdown** when stdin closes
|
|
202
|
+
|
|
116
203
|
### Interactive example
|
|
117
204
|
|
|
118
205
|
```bash
|
|
@@ -138,7 +225,7 @@ npm link
|
|
|
138
225
|
# 3. Use it anywhere
|
|
139
226
|
droid --help
|
|
140
227
|
droid headless status
|
|
141
|
-
droid headless
|
|
228
|
+
droid headless "Hello" --session-id <sessionId>
|
|
142
229
|
|
|
143
230
|
# 4. (Optional) Un-link when finished
|
|
144
231
|
npm unlink -g factory-cli
|
|
@@ -224,10 +311,10 @@ cat ~/.factory/logs/droid-log-2025-01-15T10-30-45-123Z.log
|
|
|
224
311
|
|
|
225
312
|
```bash
|
|
226
313
|
# Logs appear directly in terminal output
|
|
227
|
-
droid headless
|
|
314
|
+
droid headless "test message" --session-id abc123
|
|
228
315
|
|
|
229
316
|
# Redirect to file if needed
|
|
230
|
-
droid headless
|
|
317
|
+
droid headless "test" --session-id abc123 2>&1 | tee my-session.log
|
|
231
318
|
```
|
|
232
319
|
|
|
233
320
|
### Log Cleanup
|
|
@@ -269,10 +356,137 @@ const toolMapping = buildToolMapping();
|
|
|
269
356
|
|
|
270
357
|
---
|
|
271
358
|
|
|
272
|
-
## 11 ·
|
|
359
|
+
## 11 · E2E Testing of the TUI
|
|
360
|
+
|
|
361
|
+
The Factory CLI includes end-to-end tests for the terminal UI using Microsoft's `@microsoft/tui-test`.
|
|
362
|
+
|
|
363
|
+
### Testing Framework
|
|
364
|
+
|
|
365
|
+
- Framework: `@microsoft/tui-test`
|
|
366
|
+
- Shell: bash
|
|
367
|
+
- Config: `apps/cli/tui-test.config.ts`
|
|
368
|
+
|
|
369
|
+
### Prerequisites
|
|
370
|
+
|
|
371
|
+
The E2E tests require a valid `FACTORY_API_KEY` environment variable to authenticate with Factory services. You can generate an API key from [https://dev.app.factory.ai/settings/api-keys](https://dev.app.factory.ai/settings/api-keys).
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
export FACTORY_API_KEY="your-api-key-here"
|
|
375
|
+
npm run test:e2e
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The tests launch the CLI via a wrapper script that configures the environment:
|
|
379
|
+
|
|
380
|
+
- program: `./e2e-tests/test-workspace/run-droid.sh`
|
|
381
|
+
- The wrapper exports all required env vars (e.g., `FACTORY_HOME_OVERRIDE`) and ensures correct execution context for tests.
|
|
382
|
+
- rows/columns: 30x80
|
|
383
|
+
|
|
384
|
+
### Test Layout
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
apps/cli/
|
|
388
|
+
├── e2e-tests/
|
|
389
|
+
│ ├── chat-input.test.ts # Text entry, deletion, Ctrl+C, file suggestions, cursor nav
|
|
390
|
+
│ ├── settings.template.json # Copied to each test's .factory-dev
|
|
391
|
+
│ ├── templates/ # Blueprint copied to each test workspace
|
|
392
|
+
│ │ ├── package.json
|
|
393
|
+
│ │ ├── tsconfig.json
|
|
394
|
+
│ │ └── src/
|
|
395
|
+
│ └── test-workspace/ # Contains wrapper scripts only
|
|
396
|
+
│ ├── run-droid.sh # Creates unique workspace per invocation
|
|
397
|
+
│ ├── run-droid.ps1 # Windows version
|
|
398
|
+
│ └── run-droid.cmd # Windows CMD wrapper
|
|
399
|
+
└── tui-test.config.ts # TUI test config using the wrapper
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Running Tests
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
# From apps/cli
|
|
406
|
+
npm run test:e2e # bundles CLI then runs all e2e tests
|
|
407
|
+
|
|
408
|
+
# Run a specific test file (or append :line)
|
|
409
|
+
npm run test:e2e e2e-tests/chat-input.test.ts
|
|
410
|
+
|
|
411
|
+
# Tests automatically create isolated workspaces - no setup needed!
|
|
412
|
+
# Each test invocation gets its own /tmp/droid-test-* directory
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Debugging
|
|
416
|
+
|
|
417
|
+
At any point of time, you can access the serialized terminal view using:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
const terminalView = terminal.serialize().view;
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Tracing is enabled in config (`trace: true`). Traces are saved under `tui-traces/`.
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
# Replay a trace
|
|
427
|
+
npm run show-trace tui-traces/<trace-file>
|
|
428
|
+
# Or directly
|
|
429
|
+
npx @microsoft/tui-test show-trace tui-traces/<trace-file>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Test Environment Isolation
|
|
433
|
+
|
|
434
|
+
Each test case runs in **complete isolation** with its own workspace and `.factory-dev` directory:
|
|
435
|
+
|
|
436
|
+
- **Per-test workspaces**: The wrapper scripts (`run-droid.sh`, `run-droid.ps1`) automatically create a unique temporary workspace for each test invocation
|
|
437
|
+
- **Unique identifiers**: Workspaces are named with PID and timestamp: `/tmp/droid-test-{pid}-{timestamp}-XXXXXX`
|
|
438
|
+
- **Template copying**: Files from `e2e-tests/templates/` (package.json, tsconfig.json, src/) are copied to each test workspace
|
|
439
|
+
- **Automatic cleanup**: Temporary workspaces are cleaned up when tests complete (via trap on Unix, try/finally on Windows)
|
|
440
|
+
- **Parallel execution**: Tests can run in parallel without state interference since each has its own `.factory-dev`
|
|
441
|
+
|
|
442
|
+
Settings template (copied to each test's `.factory-dev/settings.json`):
|
|
443
|
+
|
|
444
|
+
```json
|
|
445
|
+
{
|
|
446
|
+
"model": "claude-sonnet-4-20250514",
|
|
447
|
+
"cloudSessionSync": false,
|
|
448
|
+
"diffMode": "github"
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### What’s Covered
|
|
453
|
+
|
|
454
|
+
Current suite validates:
|
|
455
|
+
|
|
456
|
+
- Text entry visibility
|
|
457
|
+
- Backspace/Delete behavior (including forward delete)
|
|
458
|
+
- Ctrl+C warning and double Ctrl+C exit
|
|
459
|
+
- File suggestions via @: show, filter, navigate, select, dismiss
|
|
460
|
+
- Cursor navigation and in-line editing
|
|
461
|
+
|
|
462
|
+
### Writing New Tests
|
|
463
|
+
|
|
464
|
+
Global config already provides the program and env. You can optionally tweak terminal size per test file:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import { test, expect } from '@microsoft/tui-test';
|
|
468
|
+
|
|
469
|
+
test.use({ rows: 24, columns: 100 });
|
|
470
|
+
|
|
471
|
+
test('example', async ({ terminal }) => {
|
|
472
|
+
await expect(
|
|
473
|
+
terminal.getByText('standing in an open terminal', {
|
|
474
|
+
full: false,
|
|
475
|
+
strict: false,
|
|
476
|
+
})
|
|
477
|
+
).toBeVisible({ timeout: 10000 });
|
|
478
|
+
terminal.write('Hello');
|
|
479
|
+
await expect(terminal.getByText('Hello')).toBeVisible();
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## 12 · Contributing
|
|
273
486
|
|
|
274
487
|
1. `pnpm install` (or `npm install`) at repo root
|
|
275
|
-
2. `cd apps/
|
|
488
|
+
2. `cd apps/cli`
|
|
276
489
|
3. Implement feature / fix
|
|
277
490
|
4. Ensure `npm run lint && npm run typecheck && npm test` pass
|
|
278
|
-
5.
|
|
491
|
+
5. Run E2E tests with `npm run test:e2e`
|
|
492
|
+
6. Commit & open PR 🚀
|
package/bin/droid
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JavaScript shim for the droid command.
|
|
5
|
+
*
|
|
6
|
+
* This shim finds and executes the appropriate platform-specific binary.
|
|
7
|
+
* For x64 platforms, it detects AVX2 support and selects the appropriate
|
|
8
|
+
* binary (regular or baseline for older CPUs).
|
|
9
|
+
*
|
|
10
|
+
* In optimized installations, this file is replaced with a hard link to
|
|
11
|
+
* the actual binary, avoiding Node.js startup overhead.
|
|
12
|
+
*
|
|
13
|
+
* Based on esbuild's approach: https://github.com/evanw/esbuild/pull/1621
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const child_process = require('child_process');
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
PLATFORM_PACKAGES,
|
|
22
|
+
getPlatformKey,
|
|
23
|
+
getBinaryName,
|
|
24
|
+
getBinaryPathWithInfo,
|
|
25
|
+
resolveBinaryPath,
|
|
26
|
+
} = require('../platform.js');
|
|
27
|
+
|
|
28
|
+
function runBinary(binaryPath, args) {
|
|
29
|
+
const result = child_process.spawnSync(binaryPath, args, {
|
|
30
|
+
stdio: 'inherit',
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (result.error) {
|
|
35
|
+
throw result.error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getBinaryPath() {
|
|
42
|
+
const platformKey = getPlatformKey();
|
|
43
|
+
const packages = PLATFORM_PACKAGES[platformKey];
|
|
44
|
+
|
|
45
|
+
if (!packages) {
|
|
46
|
+
console.error(`Error: Unsupported platform: ${platformKey}`);
|
|
47
|
+
console.error(
|
|
48
|
+
`Supported platforms: ${Object.keys(PLATFORM_PACKAGES).join(', ')}`
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Try to get binary from optionalDependencies
|
|
54
|
+
const result = getBinaryPathWithInfo();
|
|
55
|
+
if (result) {
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback: check if binary exists alongside this script (for development)
|
|
60
|
+
const localBinary = path.join(
|
|
61
|
+
__dirname,
|
|
62
|
+
'..',
|
|
63
|
+
'dist',
|
|
64
|
+
process.platform,
|
|
65
|
+
process.arch,
|
|
66
|
+
getBinaryName()
|
|
67
|
+
);
|
|
68
|
+
if (fs.existsSync(localBinary)) {
|
|
69
|
+
return {
|
|
70
|
+
path: localBinary,
|
|
71
|
+
pkg: 'local',
|
|
72
|
+
hasBaseline: !!packages.baseline,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.error(
|
|
77
|
+
`Error: Could not find the droid binary for ${platformKey}.\n\n` +
|
|
78
|
+
`This usually means the optional dependency "${packages.regular}" was not installed.\n` +
|
|
79
|
+
`Try running: npm install\n\n` +
|
|
80
|
+
`If you're using npm with --ignore-scripts, the binary may not have been set up correctly.`
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get the binary path and execute
|
|
86
|
+
const { path: binPath, pkg, hasBaseline } = getBinaryPath();
|
|
87
|
+
const args = process.argv.slice(2);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = runBinary(binPath, args);
|
|
91
|
+
|
|
92
|
+
// Check for illegal instruction error (CPU doesn't support AVX2)
|
|
93
|
+
// This can happen on Windows where we couldn't detect AVX2 upfront
|
|
94
|
+
if (result.signal === 'SIGILL' && hasBaseline && !pkg.includes('baseline')) {
|
|
95
|
+
console.error(
|
|
96
|
+
`\nNote: Your CPU doesn't support AVX2 instructions. Trying baseline build...`
|
|
97
|
+
);
|
|
98
|
+
// Try baseline
|
|
99
|
+
const baselinePath = resolveBinaryPath(
|
|
100
|
+
PLATFORM_PACKAGES[getPlatformKey()].baseline
|
|
101
|
+
);
|
|
102
|
+
if (baselinePath) {
|
|
103
|
+
const baselineResult = runBinary(baselinePath, args);
|
|
104
|
+
process.exit(baselineResult.status ?? 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
process.exit(result.status ?? 0);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error(`Error executing droid: ${e.message}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|