@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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/package.json +37 -0
  3. 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();