@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/install.js ADDED
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * postinstall script for @factory/cli
5
+ *
6
+ * This script optimizes the installed package by replacing the JavaScript shim
7
+ * with a hard link to the actual binary executable. This avoids the overhead
8
+ * of launching another Node.js process when using the "droid" command.
9
+ *
10
+ * Based on esbuild's approach: https://github.com/evanw/esbuild/pull/1621
11
+ * and Sentry's guide: https://sentry.engineering/blog/publishing-binaries-on-npm
12
+ *
13
+ * On Unix: replaces bin/droid with hard link to the platform binary
14
+ * On Windows: keeps JS shim (Windows requires .exe extension)
15
+ * With --ignore-scripts: falls back to JS shim (still works, just slower)
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const https = require('https');
21
+ const zlib = require('zlib');
22
+ const child_process = require('child_process');
23
+
24
+ const {
25
+ PLATFORM_PACKAGES,
26
+ getPlatformKey,
27
+ getBinaryName,
28
+ getBinaryPath,
29
+ detectAVX2Support,
30
+ } = require('./platform.js');
31
+
32
+ // Read version - handle case where package.json doesn't exist yet (template dir)
33
+ let PACKAGE_VERSION = '0.0.0';
34
+ try {
35
+ PACKAGE_VERSION = require('./package.json').version;
36
+ } catch (e) {
37
+ // Running from template directory before build
38
+ }
39
+
40
+ function isYarn2OrAbove() {
41
+ const { npm_config_user_agent } = process.env;
42
+ if (npm_config_user_agent) {
43
+ const match = npm_config_user_agent.match(/yarn\/(\d+)/);
44
+ if (match && match[1]) {
45
+ return parseInt(match[1], 10) >= 2;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
51
+ function validateBinaryVersion(binaryPath) {
52
+ try {
53
+ const result = child_process.execFileSync(binaryPath, ['--version'], {
54
+ encoding: 'utf8',
55
+ timeout: 10000,
56
+ });
57
+ const version = result.trim().split('\n')[0];
58
+ // Version format might be "droid 0.50.0" or just "0.50.0"
59
+ if (PACKAGE_VERSION !== '0.0.0' && !version.includes(PACKAGE_VERSION)) {
60
+ console.warn(
61
+ `Warning: Binary version mismatch. Expected ${PACKAGE_VERSION}, got ${version}`
62
+ );
63
+ }
64
+ } catch (e) {
65
+ // Version check is optional, don't fail install
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Make an HTTPS request with optional npm authentication.
71
+ * Uses NPM_TOKEN env var if available to bypass rate limits.
72
+ */
73
+ function makeRequest(url) {
74
+ return new Promise((resolve, reject) => {
75
+ const parsedUrl = new URL(url);
76
+ const options = {
77
+ hostname: parsedUrl.hostname,
78
+ path: parsedUrl.pathname + parsedUrl.search,
79
+ headers: {},
80
+ };
81
+
82
+ // Add auth header if NPM_TOKEN is available (bypasses rate limits in CI)
83
+ if (process.env.NPM_TOKEN && parsedUrl.hostname.includes('npmjs.org')) {
84
+ options.headers['Authorization'] = `Bearer ${process.env.NPM_TOKEN}`;
85
+ }
86
+
87
+ https
88
+ .get(options, (response) => {
89
+ if (response.statusCode >= 200 && response.statusCode < 300) {
90
+ const chunks = [];
91
+ response.on('data', (chunk) => chunks.push(chunk));
92
+ response.on('end', () => resolve(Buffer.concat(chunks)));
93
+ } else if (
94
+ response.statusCode >= 300 &&
95
+ response.statusCode < 400 &&
96
+ response.headers.location
97
+ ) {
98
+ makeRequest(response.headers.location).then(resolve, reject);
99
+ } else {
100
+ reject(
101
+ new Error(
102
+ `npm responded with status code ${response.statusCode} when downloading the package`
103
+ )
104
+ );
105
+ }
106
+ })
107
+ .on('error', reject);
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Fetch the tarball URL from npm registry packument.
113
+ * This is more reliable than constructing the URL manually.
114
+ */
115
+ function fetchTarballUrl(packageName, version) {
116
+ return new Promise((resolve, reject) => {
117
+ const encodedName = packageName.replace('/', '%2f');
118
+ const url = `https://registry.npmjs.org/${encodedName}`;
119
+
120
+ makeRequest(url)
121
+ .then((buffer) => {
122
+ const packument = JSON.parse(buffer.toString('utf8'));
123
+ const versionData = packument.versions?.[version];
124
+ if (!versionData) {
125
+ throw new Error(
126
+ `Version ${version} not found for package ${packageName}`
127
+ );
128
+ }
129
+ const tarballUrl = versionData.dist?.tarball;
130
+ if (!tarballUrl) {
131
+ throw new Error(`No tarball URL found for ${packageName}@${version}`);
132
+ }
133
+ resolve(tarballUrl);
134
+ })
135
+ .catch(reject);
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Download the platform-specific package from npm registry as a fallback.
141
+ * This handles the case where optionalDependencies were disabled.
142
+ */
143
+ function downloadBinaryFromNpm() {
144
+ return new Promise((resolve, reject) => {
145
+ const platformKey = getPlatformKey();
146
+ const packages = PLATFORM_PACKAGES[platformKey];
147
+
148
+ if (!packages) {
149
+ reject(new Error(`Unsupported platform: ${platformKey}`));
150
+ return;
151
+ }
152
+
153
+ // Choose package based on AVX2 support
154
+ const hasBaseline = !!packages.baseline;
155
+ let useBaseline = false;
156
+ if (hasBaseline) {
157
+ const avx2Supported = detectAVX2Support();
158
+ if (avx2Supported === false) {
159
+ useBaseline = true;
160
+ }
161
+ }
162
+
163
+ const packageName = useBaseline ? packages.baseline : packages.regular;
164
+
165
+ console.log(`Downloading ${packageName}@${PACKAGE_VERSION} from npm...`);
166
+
167
+ // Fetch the tarball URL from packument instead of constructing it manually
168
+ fetchTarballUrl(packageName, PACKAGE_VERSION)
169
+ .then((tarballUrl) => makeRequest(tarballUrl))
170
+ .then((tarballBuffer) => {
171
+ const unzipped = zlib.gunzipSync(tarballBuffer);
172
+ const binaryName = getBinaryName();
173
+ const binaryData = extractFileFromTarball(
174
+ unzipped,
175
+ `package/bin/${binaryName}`
176
+ );
177
+
178
+ if (!binaryData) {
179
+ throw new Error(
180
+ `Binary not found in tarball: package/bin/${binaryName}`
181
+ );
182
+ }
183
+
184
+ // Ensure bin directory exists before writing
185
+ const binDir = path.join(__dirname, 'bin');
186
+ fs.mkdirSync(binDir, { recursive: true });
187
+
188
+ // Write binary to local bin directory
189
+ const localBinPath = path.join(binDir, binaryName);
190
+ fs.writeFileSync(localBinPath, binaryData, { mode: 0o755 });
191
+
192
+ console.log(`Downloaded and extracted binary to ${localBinPath}`);
193
+ resolve(localBinPath);
194
+ })
195
+ .catch(reject);
196
+ });
197
+ }
198
+
199
+ function extractFileFromTarball(tarballBuffer, filepath) {
200
+ // Tar archives are organized in 512 byte blocks
201
+ let offset = 0;
202
+ while (offset < tarballBuffer.length) {
203
+ const header = tarballBuffer.subarray(offset, offset + 512);
204
+ offset += 512;
205
+
206
+ const fileName = header.toString('utf-8', 0, 100).replace(/\0.*/g, '');
207
+ const fileSize = parseInt(
208
+ header.toString('utf-8', 124, 136).replace(/\0.*/g, ''),
209
+ 8
210
+ );
211
+
212
+ if (isNaN(fileSize)) break;
213
+
214
+ if (fileName === filepath) {
215
+ return tarballBuffer.subarray(offset, offset + fileSize);
216
+ }
217
+
218
+ // Clamp offset to the upper multiple of 512
219
+ offset = (offset + fileSize + 511) & ~511;
220
+ }
221
+ return null;
222
+ }
223
+
224
+ async function main() {
225
+ const shimPath = path.join(__dirname, 'bin', 'droid');
226
+ let binaryPath = getBinaryPath();
227
+
228
+ // If optionalDependency not found, try downloading from npm
229
+ if (!binaryPath) {
230
+ console.log('Platform-specific binary not found in optionalDependencies.');
231
+ try {
232
+ binaryPath = await downloadBinaryFromNpm();
233
+ } catch (e) {
234
+ console.log(
235
+ `Failed to download binary: ${e.message}\n` +
236
+ 'The "droid" command will use the JavaScript fallback.\n' +
237
+ 'For better performance, ensure optionalDependencies are installed.'
238
+ );
239
+ return;
240
+ }
241
+ }
242
+
243
+ // On Windows, we can't replace the shim with the binary due to .exe requirement
244
+ // On Yarn 2+, PnP mode has issues with binary modules
245
+ if (process.platform === 'win32' || isYarn2OrAbove()) {
246
+ validateBinaryVersion(binaryPath);
247
+ console.log('Using JavaScript shim for droid command.');
248
+ return;
249
+ }
250
+
251
+ // On Unix, replace the JavaScript shim with a hard link to the binary
252
+ // First, backup the shim in case linking fails
253
+ const shimBackupPath = shimPath + '.backup';
254
+ let shimBackedUp = false;
255
+
256
+ try {
257
+ // Backup the existing shim
258
+ if (fs.existsSync(shimPath)) {
259
+ fs.copyFileSync(shimPath, shimBackupPath);
260
+ shimBackedUp = true;
261
+ }
262
+
263
+ // Remove the existing shim
264
+ fs.unlinkSync(shimPath);
265
+
266
+ // Create a hard link to the binary
267
+ fs.linkSync(binaryPath, shimPath);
268
+
269
+ // Clean up backup
270
+ if (shimBackedUp) {
271
+ fs.unlinkSync(shimBackupPath);
272
+ }
273
+
274
+ validateBinaryVersion(shimPath);
275
+ console.log(
276
+ 'Optimized: droid command now runs the native binary directly.'
277
+ );
278
+ } catch (e) {
279
+ // If hard link fails (e.g., cross-device), try symlink
280
+ try {
281
+ fs.symlinkSync(binaryPath, shimPath);
282
+
283
+ // Clean up backup
284
+ if (shimBackedUp) {
285
+ fs.unlinkSync(shimBackupPath);
286
+ }
287
+
288
+ console.log('Optimized: droid command linked to native binary.');
289
+ } catch (e2) {
290
+ // Restore the shim from backup
291
+ if (shimBackedUp) {
292
+ try {
293
+ fs.renameSync(shimBackupPath, shimPath);
294
+ console.log(
295
+ 'Using JavaScript shim for droid command (optimization failed).'
296
+ );
297
+ } catch (e3) {
298
+ console.error(
299
+ 'Failed to restore shim. Please reinstall the package.'
300
+ );
301
+ }
302
+ } else {
303
+ console.log(
304
+ 'Using JavaScript shim for droid command (optimization failed).'
305
+ );
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ main().catch((e) => {
312
+ console.error('postinstall error:', e.message);
313
+ // Don't fail the install - the JS shim will still work
314
+ });
package/package.json CHANGED
@@ -1,104 +1,47 @@
1
1
  {
2
2
  "name": "@factory/cli",
3
- "version": "0.1.2-dev.7",
4
- "private": false,
5
- "type": "module",
6
- "main": "dist/index.js",
3
+ "version": "0.55.2",
4
+ "description": "Factory Droid CLI - AI-powered software engineering agent",
7
5
  "bin": {
8
- "droid": "bundle/droid.js"
9
- },
10
- "engines": {
11
- "bun": ">=1.0.0"
6
+ "droid": "bin/droid"
12
7
  },
8
+ "main": "platform.js",
13
9
  "files": [
14
- "bundle/",
15
- "README.md",
16
- "LICENSE"
10
+ "bin/",
11
+ "platform.js",
12
+ "install.js",
13
+ "README.md"
17
14
  ],
18
15
  "scripts": {
19
- "start": "bun scripts/start.js",
20
- "debug": "DEBUG=1 bun scripts/start.js",
21
- "build": "bun run clean && bun scripts/build.js",
22
- "build:sea:linux": "bun scripts/build-sea.ts linux x64",
23
- "build:sea:mac:x64": "bun scripts/build-sea.ts darwin x64",
24
- "build:sea:mac:arm64": "bun scripts/build-sea.ts darwin arm64",
25
- "build:sea:windows": "bun scripts/build-sea.ts windows x64",
26
- "bundle": "bun bun.build.ts",
27
- "bundle:dev": "bun bun.build.ts --dev",
28
- "package": "bun run build && bun run bundle",
29
- "clean": "rm -rf dist bundle",
30
- "prepare": "bun run bundle",
31
- "typecheck": "tsc --noEmit",
32
- "test": "NODE_OPTIONS='--experimental-vm-modules' bun run jest --coverage",
33
- "test:watch": "NODE_OPTIONS='--experimental-vm-modules' bun run jest --watch",
34
- "lint": "eslint \"./src/**/*.{ts,tsx}\"",
35
- "fix": "bun run lint -- --fix; bun run format",
36
- "format": "prettier --list-different --write ./src"
16
+ "postinstall": "node install.js"
17
+ },
18
+ "optionalDependencies": {
19
+ "@factory/cli-darwin-arm64": "0.55.2",
20
+ "@factory/cli-darwin-x64": "0.55.2",
21
+ "@factory/cli-darwin-x64-baseline": "0.55.2",
22
+ "@factory/cli-linux-arm64": "0.55.2",
23
+ "@factory/cli-linux-x64": "0.55.2",
24
+ "@factory/cli-linux-x64-baseline": "0.55.2",
25
+ "@factory/cli-win32-x64": "0.55.2",
26
+ "@factory/cli-win32-x64-baseline": "0.55.2"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.0.0"
30
+ },
31
+ "license": "UNLICENSED",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Factory-AI/factory.git",
35
+ "directory": "apps/cli"
37
36
  },
38
- "dependencies": {
39
- "@anthropic-ai/sdk": "^0.57.0",
40
- "@factory/common": "^0.1.0",
41
- "@factory/droid-core": "^0.1.0",
42
- "@factory/errors": "^0.1.0",
43
- "@factory/logging": "^0.1.0",
44
- "@factory/mcp": "^0.1.0",
45
- "@factory/models": "^0.1.0",
46
- "@factory/services": "^0.1.0",
47
- "@factory/utils": "^0.1.0",
48
- "@modelcontextprotocol/sdk": "^1.1.0",
49
- "@statsig/react-bindings": "^3.8.2",
50
- "@statsig/statsig-node-core": "^0.6.1",
51
- "@types/express": "^5.0.3",
52
- "@types/lodash": "^4.17.20",
53
- "@types/marked": "^5.0.2",
54
- "@types/react": "19.0.2",
55
- "@types/uuid": "^10.0.0",
56
- "@vscode/ripgrep": "^1.15.14",
57
- "ansi-escapes": "^7.0.0",
58
- "chalk": "^5.3.0",
59
- "commander": "^11.0.0",
60
- "diff": "^8.0.2",
61
- "express": "^5.1.0",
62
- "glob": "^10.4.5",
63
- "highlight.js": "^11.11.1",
64
- "ink": "^6.1.0",
65
- "inquirer": "^12.8.2",
66
- "jose": "^5.9.4",
67
- "lodash-es": "^4.17.21",
68
- "marked": "^16.1.1",
69
- "mime": "^4.0.7",
70
- "node-fetch": "^3.3.2",
71
- "open": "^10.2.0",
72
- "ora": "^8.1.1",
73
- "prop-types": "^15.8.1",
74
- "shell-quote": "^1.8.3",
75
- "semver": "^7.7.2",
76
- "table": "^6.8.2",
77
- "terminal-link": "^3.0.0",
78
- "uuid": "^9.0.0",
79
- "zod": "^3.23.8",
80
- "zod-to-json-schema": "^3.24.1"
37
+ "publishConfig": {
38
+ "access": "public"
81
39
  },
82
- "devDependencies": {
83
- "@factory/eslint-config": "^0.1.0",
84
- "@jest/globals": "^29.7.0",
85
- "@types/diff": "^7.0.2",
86
- "@types/inquirer": "^9.0.8",
87
- "@types/jest": "^29.5.14",
88
- "@types/jsdom": "^21.1.7",
89
- "@types/lodash-es": "^4.17.12",
90
- "@types/node": "^20",
91
- "@types/node-fetch": "^2.6.11",
92
- "@types/prop-types": "^15.7.15",
93
- "@types/shell-quote": "^1.7.5",
94
- "esbuild": "^0.25.0",
95
- "eslint-plugin-no-barrel-files": "^1.2.2",
96
- "ink-testing-library": "^4.0.0",
97
- "jest": "^29.7.0",
98
- "jsdom": "^26.1.0",
99
- "ts-jest": "^29.2.6",
100
- "ts-node": "^10.9.2",
101
- "tsx": "^4.20.3",
102
- "typescript": "^5.3.3"
103
- }
104
- }
40
+ "keywords": [
41
+ "factory",
42
+ "droid",
43
+ "cli",
44
+ "ai",
45
+ "agent"
46
+ ]
47
+ }
package/platform.js ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Platform detection utilities for @factory/cli
3
+ *
4
+ * Exports the platform package mapping and helper functions for
5
+ * determining the correct binary to use.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const child_process = require('child_process');
10
+
11
+ // Platform-specific package mapping
12
+ // x64 platforms have both regular (AVX2) and baseline (no AVX2) variants
13
+ const PLATFORM_PACKAGES = {
14
+ 'darwin-arm64': { regular: '@factory/cli-darwin-arm64' },
15
+ 'darwin-x64': {
16
+ regular: '@factory/cli-darwin-x64',
17
+ baseline: '@factory/cli-darwin-x64-baseline',
18
+ },
19
+ 'linux-arm64': { regular: '@factory/cli-linux-arm64' },
20
+ 'linux-x64': {
21
+ regular: '@factory/cli-linux-x64',
22
+ baseline: '@factory/cli-linux-x64-baseline',
23
+ },
24
+ 'win32-x64': {
25
+ regular: '@factory/cli-win32-x64',
26
+ baseline: '@factory/cli-win32-x64-baseline',
27
+ },
28
+ };
29
+
30
+ // All packages (used for optionalDependencies)
31
+ const ALL_PACKAGES = Object.values(PLATFORM_PACKAGES).flatMap((p) =>
32
+ [p.regular, p.baseline].filter(Boolean)
33
+ );
34
+
35
+ function getPlatformKey() {
36
+ return `${process.platform}-${process.arch}`;
37
+ }
38
+
39
+ function getBinaryName() {
40
+ return process.platform === 'win32' ? 'droid.exe' : 'droid';
41
+ }
42
+
43
+ function getPlatformPackages() {
44
+ const platformKey = getPlatformKey();
45
+ return PLATFORM_PACKAGES[platformKey] || null;
46
+ }
47
+
48
+ /**
49
+ * Detect if CPU supports AVX2 instructions.
50
+ * Returns true if AVX2 is supported, false if not, null if detection failed.
51
+ */
52
+ function detectAVX2Support() {
53
+ try {
54
+ if (process.platform === 'linux') {
55
+ const cpuinfo = fs.readFileSync('/proc/cpuinfo', 'utf8');
56
+ return cpuinfo.toLowerCase().includes('avx2');
57
+ } else if (process.platform === 'darwin') {
58
+ const result = child_process.execSync(
59
+ 'sysctl -n machdep.cpu.leaf7_features',
60
+ { encoding: 'utf8', timeout: 5000 }
61
+ );
62
+ return result.toLowerCase().includes('avx2');
63
+ } else if (process.platform === 'win32') {
64
+ // Windows: use kernel32.dll IsProcessorFeaturePresent(40) via PowerShell
65
+ // Feature ID 40 = PF_AVX2_INSTRUCTIONS_AVAILABLE
66
+ // This is the same method used by the Factory CLI installer and Bun
67
+ const script = `
68
+ try {
69
+ $hasAvx2 = (Add-Type -MemberDefinition '[DllImport("kernel32.dll")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);' -Name 'Kernel32' -Namespace 'Win32' -PassThru -ErrorAction Stop)::IsProcessorFeaturePresent(40)
70
+ if ($hasAvx2) { Write-Output 'true' } else { Write-Output 'false' }
71
+ } catch {
72
+ try {
73
+ $hasAvx2 = ([Win32.Kernel32]::IsProcessorFeaturePresent(40))
74
+ if ($hasAvx2) { Write-Output 'true' } else { Write-Output 'false' }
75
+ } catch {
76
+ Write-Output 'unknown'
77
+ }
78
+ }
79
+ `;
80
+ const result = child_process.execSync(
81
+ `powershell -NoProfile -Command "${script.replace(/\n/g, ' ')}"`,
82
+ { encoding: 'utf8', timeout: 10000 }
83
+ );
84
+ const output = result.trim().toLowerCase();
85
+ if (output === 'true') return true;
86
+ if (output === 'false') return false;
87
+ // 'unknown' or other - detection failed
88
+ return null;
89
+ }
90
+ } catch (e) {
91
+ // Detection failed
92
+ }
93
+ return null;
94
+ }
95
+
96
+ function resolveBinaryPath(packageName) {
97
+ if (!packageName) return null;
98
+ try {
99
+ return require.resolve(`${packageName}/bin/${getBinaryName()}`);
100
+ } catch (e) {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get the binary path with AVX2-aware package selection.
107
+ * Returns { path, pkg, hasBaseline } or null if not found.
108
+ */
109
+ function getBinaryPathWithInfo() {
110
+ const packages = getPlatformPackages();
111
+ if (!packages) {
112
+ return null;
113
+ }
114
+
115
+ const hasBaseline = !!packages.baseline;
116
+ let useBaseline = false;
117
+
118
+ if (hasBaseline) {
119
+ const avx2Supported = detectAVX2Support();
120
+ if (avx2Supported === false) {
121
+ useBaseline = true;
122
+ }
123
+ }
124
+
125
+ const preferredPkg = useBaseline ? packages.baseline : packages.regular;
126
+ const fallbackPkg = useBaseline ? packages.regular : packages.baseline;
127
+
128
+ let binPath = resolveBinaryPath(preferredPkg);
129
+ let selectedPkg = preferredPkg;
130
+
131
+ if (!binPath && fallbackPkg) {
132
+ binPath = resolveBinaryPath(fallbackPkg);
133
+ selectedPkg = fallbackPkg;
134
+ }
135
+
136
+ if (!binPath) {
137
+ return null;
138
+ }
139
+
140
+ return { path: binPath, pkg: selectedPkg, hasBaseline };
141
+ }
142
+
143
+ /**
144
+ * Get the binary path (simple version for backward compatibility).
145
+ */
146
+ function getBinaryPath() {
147
+ const result = getBinaryPathWithInfo();
148
+ return result ? result.path : null;
149
+ }
150
+
151
+ function isSupportedPlatform() {
152
+ return getPlatformPackages() !== null;
153
+ }
154
+
155
+ module.exports = {
156
+ PLATFORM_PACKAGES,
157
+ ALL_PACKAGES,
158
+ getPlatformKey,
159
+ getBinaryName,
160
+ getPlatformPackages,
161
+ detectAVX2Support,
162
+ resolveBinaryPath,
163
+ getBinaryPathWithInfo,
164
+ getBinaryPath,
165
+ isSupportedPlatform,
166
+ };