@ottocode/install 0.1.173
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 +33 -0
- package/package.json +37 -0
- package/start.js +339 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @ottocode/install
|
|
2
|
+
|
|
3
|
+
This is a thin installer package for the otto CLI tool.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @ottocode/install
|
|
9
|
+
# or
|
|
10
|
+
bun install -g @ottocode/install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This will automatically download and install the otto CLI binary via the official install script.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
After installation, you can use the `otto` command:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
otto --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Manual Installation
|
|
24
|
+
|
|
25
|
+
If you prefer to install manually:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
curl -fsSL https://install.ottocode.io | sh
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## More Information
|
|
32
|
+
|
|
33
|
+
For more information, visit: https://github.com/nitishxyz/otto
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ottocode/install",
|
|
3
|
+
"version": "0.1.173",
|
|
4
|
+
"description": "AI-powered development assistant CLI - npm installer",
|
|
5
|
+
"author": "nitishxyz",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/nitishxyz/otto#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/nitishxyz/otto.git",
|
|
11
|
+
"directory": "packages/install"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nitishxyz/otto/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"cli",
|
|
19
|
+
"development",
|
|
20
|
+
"assistant",
|
|
21
|
+
"code-review",
|
|
22
|
+
"anthropic",
|
|
23
|
+
"openai",
|
|
24
|
+
"gemini"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"otto": "./start.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"start.js",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"postinstall": "node start.js"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/start.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
createWriteStream,
|
|
6
|
+
chmodSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
statSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
appendFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { resolve, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { get } from 'node:https';
|
|
15
|
+
import { homedir, platform, arch } from 'node:os';
|
|
16
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
const REPO = 'nitishxyz/otto';
|
|
19
|
+
const BIN_NAME = 'otto';
|
|
20
|
+
|
|
21
|
+
function isInWorkspace() {
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const workspaceRoot = resolve(__dirname, '../../');
|
|
24
|
+
return (
|
|
25
|
+
existsSync(resolve(workspaceRoot, 'apps')) &&
|
|
26
|
+
existsSync(resolve(workspaceRoot, 'packages'))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findBinaryInPath() {
|
|
31
|
+
const pathDirs = (process.env.PATH || '').split(':');
|
|
32
|
+
const ext = platform() === 'win32' ? '.exe' : '';
|
|
33
|
+
const currentScript = fileURLToPath(import.meta.url);
|
|
34
|
+
|
|
35
|
+
for (const dir of pathDirs) {
|
|
36
|
+
const binPath = resolve(dir, `${BIN_NAME}${ext}`);
|
|
37
|
+
if (existsSync(binPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const stat = statSync(binPath);
|
|
40
|
+
if (stat.isFile() && binPath !== currentScript) {
|
|
41
|
+
const result = spawnSync('file', [binPath], { encoding: 'utf8' });
|
|
42
|
+
if (!result.stdout.includes('script text')) {
|
|
43
|
+
return binPath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch (_err) {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getVersion(binaryPath) {
|
|
53
|
+
try {
|
|
54
|
+
const result = spawnSync(binaryPath, ['--version'], { encoding: 'utf8' });
|
|
55
|
+
if (result.status === 0 && result.stdout) {
|
|
56
|
+
// Extract version number from output (e.g., "otto 1.2.3" -> "1.2.3")
|
|
57
|
+
const match = result.stdout.trim().match(/[\d.]+/);
|
|
58
|
+
return match ? match[0] : null;
|
|
59
|
+
}
|
|
60
|
+
} catch (_err) {
|
|
61
|
+
// If we can't get version, return null
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getLatestVersion() {
|
|
67
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
const packageJsonPath = resolve(__dirname, 'package.json');
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
72
|
+
return Promise.resolve(packageJson.version);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
return Promise.reject(
|
|
75
|
+
new Error(`Could not read package.json version: ${err.message}`),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function compareVersions(v1, v2) {
|
|
81
|
+
if (!v1 || !v2) return null;
|
|
82
|
+
|
|
83
|
+
const parts1 = v1.split('.').map(Number);
|
|
84
|
+
const parts2 = v2.split('.').map(Number);
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
87
|
+
const part1 = parts1[i] || 0;
|
|
88
|
+
const part2 = parts2[i] || 0;
|
|
89
|
+
|
|
90
|
+
if (part1 > part2) return 1;
|
|
91
|
+
if (part1 < part2) return -1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return 0; // Equal
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getPlatformInfo() {
|
|
98
|
+
const platformMap = {
|
|
99
|
+
darwin: 'darwin',
|
|
100
|
+
linux: 'linux',
|
|
101
|
+
win32: 'windows',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const archMap = {
|
|
105
|
+
x64: 'x64',
|
|
106
|
+
arm64: 'arm64',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const os = platformMap[platform()];
|
|
110
|
+
const architecture = archMap[arch()];
|
|
111
|
+
const ext = platform() === 'win32' ? '.exe' : '';
|
|
112
|
+
|
|
113
|
+
if (!os || !architecture) {
|
|
114
|
+
throw new Error(`Unsupported platform: ${platform()}-${arch()}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { os, arch: architecture, ext };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function downloadWithProgress(url, dest) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const file = createWriteStream(dest);
|
|
123
|
+
let totalBytes = 0;
|
|
124
|
+
let downloadedBytes = 0;
|
|
125
|
+
|
|
126
|
+
function handleRedirect(response) {
|
|
127
|
+
if (
|
|
128
|
+
response.statusCode >= 300 &&
|
|
129
|
+
response.statusCode < 400 &&
|
|
130
|
+
response.headers.location
|
|
131
|
+
) {
|
|
132
|
+
get(response.headers.location, handleRedirect);
|
|
133
|
+
} else if (response.statusCode === 200) {
|
|
134
|
+
totalBytes = Number.parseInt(
|
|
135
|
+
response.headers['content-length'] || '0',
|
|
136
|
+
10,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
response.on('data', (chunk) => {
|
|
140
|
+
downloadedBytes += chunk.length;
|
|
141
|
+
if (totalBytes > 0) {
|
|
142
|
+
const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
143
|
+
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(1);
|
|
144
|
+
const totalMB = (totalBytes / 1024 / 1024).toFixed(1);
|
|
145
|
+
process.stdout.write(
|
|
146
|
+
`\rDownloading: ${percent}% (${downloadedMB}MB / ${totalMB}MB)`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
response.pipe(file);
|
|
152
|
+
file.on('finish', () => {
|
|
153
|
+
file.close();
|
|
154
|
+
process.stdout.write('\n');
|
|
155
|
+
resolve();
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
reject(new Error(`Download failed: ${response.statusCode}`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get(url, handleRedirect).on('error', (err) => {
|
|
163
|
+
file.close();
|
|
164
|
+
reject(err);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function updateShellProfile(userBin) {
|
|
170
|
+
// Skip on Windows
|
|
171
|
+
if (platform() === 'win32') return;
|
|
172
|
+
|
|
173
|
+
const shell = process.env.SHELL || '';
|
|
174
|
+
let configFile;
|
|
175
|
+
let shellType;
|
|
176
|
+
|
|
177
|
+
if (shell.includes('zsh')) {
|
|
178
|
+
configFile = resolve(homedir(), '.zshrc');
|
|
179
|
+
shellType = 'zsh';
|
|
180
|
+
} else if (shell.includes('bash')) {
|
|
181
|
+
configFile = resolve(homedir(), '.bashrc');
|
|
182
|
+
shellType = 'bash';
|
|
183
|
+
} else {
|
|
184
|
+
configFile = resolve(homedir(), '.profile');
|
|
185
|
+
shellType = 'shell';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const pathExport = 'export PATH="$HOME/.local/bin:$PATH"';
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
let fileContent = '';
|
|
192
|
+
if (existsSync(configFile)) {
|
|
193
|
+
fileContent = readFileSync(configFile, 'utf8');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if .local/bin is already in the config file
|
|
197
|
+
if (fileContent.includes('.local/bin')) {
|
|
198
|
+
console.log(`✓ PATH already configured in ${configFile}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Add the PATH export
|
|
203
|
+
appendFileSync(configFile, `\n${pathExport}\n`);
|
|
204
|
+
console.log(`✓ Added ${userBin} to PATH in ${configFile}`);
|
|
205
|
+
console.log(`✓ Restart your ${shellType} or run: source ${configFile}`);
|
|
206
|
+
} catch (_error) {
|
|
207
|
+
// Silently fail if we can't update the profile
|
|
208
|
+
console.log(`⚠️ Could not automatically update ${configFile}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function install() {
|
|
213
|
+
try {
|
|
214
|
+
const { os, arch: architecture, ext } = getPlatformInfo();
|
|
215
|
+
const asset = `${BIN_NAME}-${os}-${architecture}${ext}`;
|
|
216
|
+
const url = `https://github.com/${REPO}/releases/latest/download/${asset}`;
|
|
217
|
+
|
|
218
|
+
console.log(`Installing ${BIN_NAME} (${os}/${architecture})...`);
|
|
219
|
+
|
|
220
|
+
const userBin = resolve(homedir(), '.local', 'bin');
|
|
221
|
+
mkdirSync(userBin, { recursive: true });
|
|
222
|
+
const binPath = resolve(userBin, `${BIN_NAME}${ext}`);
|
|
223
|
+
|
|
224
|
+
await downloadWithProgress(url, binPath);
|
|
225
|
+
|
|
226
|
+
chmodSync(binPath, 0o755);
|
|
227
|
+
|
|
228
|
+
const result = spawnSync(binPath, ['--version'], { encoding: 'utf8' });
|
|
229
|
+
if (result.status === 0) {
|
|
230
|
+
console.log(`\n✓ ${BIN_NAME} installed successfully!`);
|
|
231
|
+
console.log(`Version: ${result.stdout.trim()}`);
|
|
232
|
+
console.log(`Location: ${binPath}`);
|
|
233
|
+
|
|
234
|
+
const pathDirs = (process.env.PATH || '').split(':');
|
|
235
|
+
if (!pathDirs.includes(userBin)) {
|
|
236
|
+
updateShellProfile(userBin);
|
|
237
|
+
console.log(`\n⚠️ Add ${userBin} to your PATH:`);
|
|
238
|
+
console.log(
|
|
239
|
+
` echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc`,
|
|
240
|
+
);
|
|
241
|
+
console.log(
|
|
242
|
+
` Or for zsh: echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc`,
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
console.log(`✓ ${userBin} already in PATH`);
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
console.log(`\n✓ Installed to ${binPath}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log(`\nRun: ${BIN_NAME} --help`);
|
|
252
|
+
return binPath;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error('Failed to install otto CLI:', error.message);
|
|
255
|
+
console.error('\nPlease try installing manually:');
|
|
256
|
+
console.error(' curl -fsSL https://install.ottocode.io | sh');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function checkAndUpdateVersion(binaryPath) {
|
|
262
|
+
try {
|
|
263
|
+
const currentVersion = getVersion(binaryPath);
|
|
264
|
+
|
|
265
|
+
if (!currentVersion) {
|
|
266
|
+
console.log('⚠️ Could not determine current version');
|
|
267
|
+
return { needsUpdate: false, binaryPath };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(`Current version: ${currentVersion}`);
|
|
271
|
+
console.log('Checking for updates...');
|
|
272
|
+
|
|
273
|
+
const latestVersion = await getLatestVersion();
|
|
274
|
+
console.log(`Latest version: ${latestVersion}`);
|
|
275
|
+
|
|
276
|
+
const comparison = compareVersions(currentVersion, latestVersion);
|
|
277
|
+
|
|
278
|
+
if (comparison < 0) {
|
|
279
|
+
// Current version is older
|
|
280
|
+
console.log(
|
|
281
|
+
`\n🔄 New version available: ${currentVersion} → ${latestVersion}`,
|
|
282
|
+
);
|
|
283
|
+
console.log('Updating...\n');
|
|
284
|
+
const newBinaryPath = await install();
|
|
285
|
+
return { needsUpdate: true, binaryPath: newBinaryPath };
|
|
286
|
+
} else if (comparison > 0) {
|
|
287
|
+
// Current version is newer (dev version?)
|
|
288
|
+
console.log(
|
|
289
|
+
`✓ You have a newer version (${currentVersion}) than the latest release`,
|
|
290
|
+
);
|
|
291
|
+
return { needsUpdate: false, binaryPath };
|
|
292
|
+
} else {
|
|
293
|
+
// Versions match
|
|
294
|
+
console.log('✓ You have the latest version');
|
|
295
|
+
return { needsUpdate: false, binaryPath };
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.log(`⚠️ Could not check for updates: ${error.message}`);
|
|
299
|
+
return { needsUpdate: false, binaryPath };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function main() {
|
|
304
|
+
if (isInWorkspace()) {
|
|
305
|
+
console.log('Detected workspace environment, skipping install script.');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let binaryPath = findBinaryInPath();
|
|
310
|
+
|
|
311
|
+
if (binaryPath) {
|
|
312
|
+
// Binary exists, check version
|
|
313
|
+
const { binaryPath: updatedPath } = await checkAndUpdateVersion(binaryPath);
|
|
314
|
+
binaryPath = updatedPath;
|
|
315
|
+
|
|
316
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
317
|
+
stdio: 'inherit',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
child.on('exit', (code) => {
|
|
321
|
+
process.exit(code || 0);
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
// No binary found, install fresh
|
|
325
|
+
const installedPath = await install();
|
|
326
|
+
|
|
327
|
+
if (process.argv.length > 2) {
|
|
328
|
+
const child = spawn(installedPath, process.argv.slice(2), {
|
|
329
|
+
stdio: 'inherit',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
child.on('exit', (code) => {
|
|
333
|
+
process.exit(code || 0);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
main();
|