@nboard-dev/octus 0.3.0
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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/bin/octus.js +5 -0
- package/package.json +68 -0
- package/src/commands/config.js +29 -0
- package/src/commands/doctor.js +254 -0
- package/src/commands/generate-profile.js +46 -0
- package/src/commands/init.js +311 -0
- package/src/commands/login.js +60 -0
- package/src/commands/logout.js +21 -0
- package/src/commands/ping.js +72 -0
- package/src/commands/setup.js +439 -0
- package/src/index.js +62 -0
- package/src/lib/api.js +210 -0
- package/src/lib/config.js +254 -0
- package/src/lib/octus-contract.js +231 -0
- package/src/lib/profile-generator.js +443 -0
- package/src/lib/ui.js +186 -0
- package/src/lib/version.js +1 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join, resolve, basename } from 'path';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
import { config } from '../lib/config.js';
|
|
9
|
+
import { api } from '../lib/api.js';
|
|
10
|
+
import { approvedProfiles } from '../lib/config.js';
|
|
11
|
+
import ui from '../lib/ui.js';
|
|
12
|
+
import { createHash } from 'crypto';
|
|
13
|
+
import { scanRepository } from '../lib/octus-contract.js';
|
|
14
|
+
import { generateProfileWithAi } from '../lib/profile-generator.js';
|
|
15
|
+
|
|
16
|
+
export const initCommand = new Command('init')
|
|
17
|
+
.description('Initialize and register a repository with Octus')
|
|
18
|
+
.argument('[path]', 'Local repo path or Git URL to clone', '.')
|
|
19
|
+
.option('-d, --clone-dir <dir>', 'Target directory for cloning Git URLs')
|
|
20
|
+
.option('--run-setup', 'Immediately run setup after init')
|
|
21
|
+
.option('--setup-profile <profile>', 'Profile to use for setup', 'auto')
|
|
22
|
+
.option('--setup-ignore-repo-profile', 'Ignore repo-local profile during setup')
|
|
23
|
+
.option('--setup-timeout <seconds>', 'Per-step timeout for setup', '300')
|
|
24
|
+
.action(async (pathArg, options) => {
|
|
25
|
+
// Check login
|
|
26
|
+
if (!config.isLoggedIn()) {
|
|
27
|
+
ui.error('Not logged in. Run `octus login` first.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let repoPath;
|
|
32
|
+
const isGitUrl = pathArg.startsWith('http') || pathArg.startsWith('git@');
|
|
33
|
+
|
|
34
|
+
ui.header('Octus Init');
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
// Handle Git URL cloning
|
|
38
|
+
// ─────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
if (isGitUrl) {
|
|
41
|
+
const cloneDir = options.cloneDir || basename(pathArg.replace(/\.git$/, ''));
|
|
42
|
+
repoPath = resolve(cloneDir);
|
|
43
|
+
|
|
44
|
+
if (existsSync(repoPath)) {
|
|
45
|
+
ui.error(`Directory ${cloneDir} already exists`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const spin = ui.spinner(`Cloning ${pathArg}...`).start();
|
|
50
|
+
try {
|
|
51
|
+
await execa('git', ['clone', pathArg, cloneDir]);
|
|
52
|
+
spin.succeed(`Cloned to ${cloneDir}`);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
spin.fail(`Clone failed: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
repoPath = resolve(pathArg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Verify it's a git repo
|
|
62
|
+
if (!existsSync(join(repoPath, '.git'))) {
|
|
63
|
+
ui.error(`${repoPath} is not a git repository`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ui.info(`Repository: ${repoPath}`);
|
|
68
|
+
|
|
69
|
+
// ─────────────────────────────────────────────────────────────
|
|
70
|
+
// Analyze repository
|
|
71
|
+
// ─────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const spin = ui.spinner('Analyzing repository...').start();
|
|
74
|
+
|
|
75
|
+
// Get git remote
|
|
76
|
+
let gitRemoteUrl = null;
|
|
77
|
+
try {
|
|
78
|
+
const { stdout } = await execa('git', ['remote', 'get-url', 'origin'], { cwd: repoPath });
|
|
79
|
+
gitRemoteUrl = stdout.trim();
|
|
80
|
+
} catch {
|
|
81
|
+
// No remote, that's ok
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Detect project type
|
|
85
|
+
const projectType = detectProjectType(repoPath);
|
|
86
|
+
spin.text = `Detected: ${projectType}`;
|
|
87
|
+
|
|
88
|
+
// Scan repo structure
|
|
89
|
+
const scanResult = scanRepository(repoPath);
|
|
90
|
+
const repoData = {
|
|
91
|
+
name: basename(repoPath),
|
|
92
|
+
local_path: repoPath,
|
|
93
|
+
git_url: gitRemoteUrl,
|
|
94
|
+
default_branch: 'main',
|
|
95
|
+
project_type: projectType,
|
|
96
|
+
metadata: scanResult.metadata,
|
|
97
|
+
has_readme: scanResult.has_readme,
|
|
98
|
+
has_dockerfile: scanResult.has_dockerfile,
|
|
99
|
+
has_docker_compose: scanResult.has_docker_compose
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
spin.succeed('Repository analyzed');
|
|
103
|
+
|
|
104
|
+
// ─────────────────────────────────────────────────────────────
|
|
105
|
+
// Ensure .octusignore
|
|
106
|
+
// ─────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
const octusignorePath = join(repoPath, '.octusignore');
|
|
109
|
+
if (!existsSync(octusignorePath)) {
|
|
110
|
+
writeFileSync(octusignorePath, getDefaultOctusignore());
|
|
111
|
+
ui.info('Created .octusignore');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─────────────────────────────────────────────────────────────
|
|
115
|
+
// Generate onboarding profile
|
|
116
|
+
// ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const profileDir = join(repoPath, '.octus');
|
|
119
|
+
const profilePath = join(profileDir, 'profile.json');
|
|
120
|
+
|
|
121
|
+
if (!existsSync(profileDir)) {
|
|
122
|
+
mkdirSync(profileDir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let profile;
|
|
126
|
+
if (existsSync(profilePath)) {
|
|
127
|
+
ui.info('Using existing profile');
|
|
128
|
+
profile = JSON.parse(readFileSync(profilePath, 'utf-8'));
|
|
129
|
+
} else {
|
|
130
|
+
const genSpin = ui.spinner('Generating onboarding profile...').start();
|
|
131
|
+
const generation = await generateProfileWithAi(repoPath);
|
|
132
|
+
profile = generation.profile;
|
|
133
|
+
if (generation.usedAi) {
|
|
134
|
+
genSpin.succeed('AI onboarding profile generated');
|
|
135
|
+
} else {
|
|
136
|
+
genSpin.succeed('Rule-based onboarding profile generated');
|
|
137
|
+
if (generation.fallbackReason) {
|
|
138
|
+
ui.warning(generation.fallbackReason);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Show profile for review
|
|
143
|
+
ui.divider();
|
|
144
|
+
ui.header('Generated Onboarding Steps');
|
|
145
|
+
profile.steps.forEach((step, i) => {
|
|
146
|
+
console.log(` ${chalk.cyan(i + 1)}. ${step.name || step.title}`);
|
|
147
|
+
console.log(` ${chalk.dim(step.command || step.description || '')}`);
|
|
148
|
+
});
|
|
149
|
+
ui.divider();
|
|
150
|
+
|
|
151
|
+
// Ask for approval
|
|
152
|
+
const { approved } = await inquirer.prompt([
|
|
153
|
+
{
|
|
154
|
+
type: 'confirm',
|
|
155
|
+
name: 'approved',
|
|
156
|
+
message: 'Approve this onboarding profile?',
|
|
157
|
+
default: true
|
|
158
|
+
}
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
if (!approved) {
|
|
162
|
+
ui.warning('Profile not approved. Edit .octus/profile.json and run init again.');
|
|
163
|
+
writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Save profile
|
|
168
|
+
writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
|
169
|
+
|
|
170
|
+
// Record approval
|
|
171
|
+
const profileHash = createHash('sha256')
|
|
172
|
+
.update(JSON.stringify(profile))
|
|
173
|
+
.digest('hex');
|
|
174
|
+
approvedProfiles.add(profilePath, profileHash, profile.profile_name || profile.name || 'generated');
|
|
175
|
+
|
|
176
|
+
ui.success('Profile approved and saved');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─────────────────────────────────────────────────────────────
|
|
180
|
+
// Register with backend
|
|
181
|
+
// ─────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
const regSpin = ui.spinner('Registering repository...').start();
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const repo = await api.registerRepository(repoData);
|
|
187
|
+
regSpin.text = 'Resolving profile...';
|
|
188
|
+
|
|
189
|
+
const resolved = await api.resolveProfile(repo.id, gitRemoteUrl);
|
|
190
|
+
regSpin.text = 'Getting signed init file...';
|
|
191
|
+
|
|
192
|
+
const initFile = await api.createInitFile(repo.id, resolved.profile_key || 'auto');
|
|
193
|
+
|
|
194
|
+
// Write .octus.yaml
|
|
195
|
+
const octusYamlPath = join(repoPath, '.octus.yaml');
|
|
196
|
+
writeFileSync(
|
|
197
|
+
octusYamlPath,
|
|
198
|
+
typeof initFile.content === 'string' ? initFile.content : YAML.stringify(initFile)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
regSpin.succeed('Repository registered');
|
|
202
|
+
|
|
203
|
+
// Update .gitignore
|
|
204
|
+
updateGitignore(repoPath);
|
|
205
|
+
|
|
206
|
+
} catch (err) {
|
|
207
|
+
regSpin.fail(`Registration failed: ${err.message}`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─────────────────────────────────────────────────────────────
|
|
212
|
+
// Success
|
|
213
|
+
// ─────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
ui.divider();
|
|
216
|
+
ui.successPanel('Repository Initialized', `
|
|
217
|
+
${chalk.bold('Created files:')}
|
|
218
|
+
• .octus/profile.json
|
|
219
|
+
• .octus.yaml
|
|
220
|
+
• .octusignore
|
|
221
|
+
|
|
222
|
+
${chalk.bold('Next steps:')}
|
|
223
|
+
${chalk.cyan('octus setup')} Run onboarding
|
|
224
|
+
${chalk.cyan('octus doctor')} Check status
|
|
225
|
+
`);
|
|
226
|
+
|
|
227
|
+
// Run setup if requested
|
|
228
|
+
if (options.runSetup) {
|
|
229
|
+
ui.divider();
|
|
230
|
+
ui.info('Running setup...');
|
|
231
|
+
// Import and run setup command
|
|
232
|
+
const { runSetup } = await import('./setup.js');
|
|
233
|
+
await runSetup(repoPath, {
|
|
234
|
+
profile: options.setupProfile,
|
|
235
|
+
ignoreRepoProfile: options.setupIgnoreRepoProfile,
|
|
236
|
+
timeout: parseInt(options.setupTimeout, 10)
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Detect project type based on files
|
|
243
|
+
*/
|
|
244
|
+
function detectProjectType(repoPath) {
|
|
245
|
+
if (existsSync(join(repoPath, 'package.json'))) return 'node';
|
|
246
|
+
if (existsSync(join(repoPath, 'pyproject.toml'))) return 'python';
|
|
247
|
+
if (existsSync(join(repoPath, 'requirements.txt'))) return 'python';
|
|
248
|
+
if (existsSync(join(repoPath, 'Cargo.toml'))) return 'rust';
|
|
249
|
+
if (existsSync(join(repoPath, 'go.mod'))) return 'go';
|
|
250
|
+
if (existsSync(join(repoPath, 'pom.xml'))) return 'java';
|
|
251
|
+
if (existsSync(join(repoPath, 'Gemfile'))) return 'ruby';
|
|
252
|
+
return 'generic';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Default .octusignore content
|
|
257
|
+
*/
|
|
258
|
+
function getDefaultOctusignore() {
|
|
259
|
+
return `# Octus ignore file - files to exclude from AI context
|
|
260
|
+
# Syntax is similar to .gitignore
|
|
261
|
+
|
|
262
|
+
# Secrets and credentials
|
|
263
|
+
.env
|
|
264
|
+
.env.*
|
|
265
|
+
*.pem
|
|
266
|
+
*.key
|
|
267
|
+
secrets/
|
|
268
|
+
credentials/
|
|
269
|
+
|
|
270
|
+
# Dependencies
|
|
271
|
+
node_modules/
|
|
272
|
+
.venv/
|
|
273
|
+
venv/
|
|
274
|
+
__pycache__/
|
|
275
|
+
|
|
276
|
+
# Build outputs
|
|
277
|
+
dist/
|
|
278
|
+
build/
|
|
279
|
+
*.pyc
|
|
280
|
+
*.o
|
|
281
|
+
*.so
|
|
282
|
+
|
|
283
|
+
# IDE
|
|
284
|
+
.idea/
|
|
285
|
+
.vscode/
|
|
286
|
+
*.swp
|
|
287
|
+
|
|
288
|
+
# OS
|
|
289
|
+
.DS_Store
|
|
290
|
+
Thumbs.db
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Update .gitignore to include Octus files
|
|
296
|
+
*/
|
|
297
|
+
function updateGitignore(repoPath) {
|
|
298
|
+
const gitignorePath = join(repoPath, '.gitignore');
|
|
299
|
+
const octusEntries = '\n# Octus\n.octus/profile.json\n.octus.yaml\n';
|
|
300
|
+
|
|
301
|
+
if (existsSync(gitignorePath)) {
|
|
302
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
303
|
+
if (!content.includes('.octus/profile.json') || !content.includes('.octus.yaml')) {
|
|
304
|
+
writeFileSync(gitignorePath, content + octusEntries);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
writeFileSync(gitignorePath, octusEntries);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export default initCommand;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { config } from '../lib/config.js';
|
|
4
|
+
import { api } from '../lib/api.js';
|
|
5
|
+
import ui from '../lib/ui.js';
|
|
6
|
+
|
|
7
|
+
export const loginCommand = new Command('login')
|
|
8
|
+
.description('Authenticate with Octus Backend and save API key')
|
|
9
|
+
.option('-u, --backend-url <url>', 'Backend API URL', 'http://localhost:8000')
|
|
10
|
+
.option('-k, --api-key <key>', 'API key (will prompt if not provided)')
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
let { backendUrl, apiKey } = options;
|
|
13
|
+
const previousConfig = config.get();
|
|
14
|
+
|
|
15
|
+
// Prompt for API key if not provided
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
const answers = await inquirer.prompt([
|
|
18
|
+
{
|
|
19
|
+
type: 'password',
|
|
20
|
+
name: 'apiKey',
|
|
21
|
+
message: 'Enter your Octus API key:',
|
|
22
|
+
mask: '•',
|
|
23
|
+
validate: (input) => input.length > 0 || 'API key is required'
|
|
24
|
+
}
|
|
25
|
+
]);
|
|
26
|
+
apiKey = answers.apiKey;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const spin = ui.spinner('Authenticating...').start();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Temporarily set config to test the credentials
|
|
33
|
+
config.set({ apiKey, backendUrl });
|
|
34
|
+
|
|
35
|
+
// Validate credentials
|
|
36
|
+
const user = await api.getMe();
|
|
37
|
+
spin.text = 'Fetching organization...';
|
|
38
|
+
|
|
39
|
+
const org = await api.getMyOrganization();
|
|
40
|
+
|
|
41
|
+
spin.succeed('Authenticated successfully!');
|
|
42
|
+
|
|
43
|
+
ui.divider();
|
|
44
|
+
ui.success(`Logged in as ${user.email || user.name || 'user'}`);
|
|
45
|
+
ui.keyValue([
|
|
46
|
+
['Organization', org.name || 'N/A'],
|
|
47
|
+
['Backend', backendUrl],
|
|
48
|
+
['Config saved', config.getConfigPath()]
|
|
49
|
+
]);
|
|
50
|
+
ui.divider();
|
|
51
|
+
|
|
52
|
+
} catch (err) {
|
|
53
|
+
spin.fail('Authentication failed');
|
|
54
|
+
config.set({ apiKey: previousConfig.apiKey, backendUrl: previousConfig.backendUrl });
|
|
55
|
+
ui.error(err.message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export default loginCommand;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { config } from '../lib/config.js';
|
|
3
|
+
import ui from '../lib/ui.js';
|
|
4
|
+
|
|
5
|
+
export const logoutCommand = new Command('logout')
|
|
6
|
+
.description('Clear stored credentials')
|
|
7
|
+
.action(async () => {
|
|
8
|
+
const spin = ui.spinner('Clearing credentials...').start();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
config.clear();
|
|
12
|
+
spin.succeed('Logged out successfully');
|
|
13
|
+
ui.info('All local Octus data has been cleared');
|
|
14
|
+
} catch (err) {
|
|
15
|
+
spin.fail('Failed to clear credentials');
|
|
16
|
+
ui.error(err.message);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export default logoutCommand;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { config } from '../lib/config.js';
|
|
4
|
+
import { api } from '../lib/api.js';
|
|
5
|
+
import ui from '../lib/ui.js';
|
|
6
|
+
|
|
7
|
+
export const pingCommand = new Command('ping')
|
|
8
|
+
.description('Check connectivity and authentication with Octus Backend')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
ui.header('Octus Connection Check');
|
|
11
|
+
|
|
12
|
+
const { backendUrl, apiKey } = config.get();
|
|
13
|
+
const results = [];
|
|
14
|
+
|
|
15
|
+
// Check 1: Backend URL configured
|
|
16
|
+
results.push(['Backend URL', backendUrl || 'Not configured', !!backendUrl]);
|
|
17
|
+
|
|
18
|
+
// Check 2: API key present
|
|
19
|
+
results.push(['API Key', apiKey ? ui.maskSecret(apiKey) : 'Not set', !!apiKey]);
|
|
20
|
+
|
|
21
|
+
// Check 3: Health endpoint
|
|
22
|
+
let healthOk = false;
|
|
23
|
+
try {
|
|
24
|
+
const health = await api.healthCheck();
|
|
25
|
+
healthOk = health.ok;
|
|
26
|
+
results.push(['Backend Health', healthOk ? 'Healthy' : 'Unhealthy', healthOk]);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
results.push(['Backend Health', `Error: ${err.message}`, false]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check 4: Authentication (only if health passed)
|
|
32
|
+
if (healthOk && apiKey) {
|
|
33
|
+
try {
|
|
34
|
+
const user = await api.getMe();
|
|
35
|
+
results.push(['Authentication', `✓ ${user.email || user.name || 'Authenticated'}`, true]);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const org = await api.getMyOrganization();
|
|
39
|
+
results.push(['Organization', org.name || 'N/A', true]);
|
|
40
|
+
} catch {
|
|
41
|
+
results.push(['Organization', 'Could not fetch', false]);
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
results.push(['Authentication', `Failed: ${err.message}`, false]);
|
|
45
|
+
}
|
|
46
|
+
} else if (!apiKey) {
|
|
47
|
+
results.push(['Authentication', 'Skipped (no API key)', false]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Display results
|
|
51
|
+
ui.divider();
|
|
52
|
+
ui.table(
|
|
53
|
+
['Check', 'Status', 'Result'],
|
|
54
|
+
results.map(([check, status, ok]) => [
|
|
55
|
+
check,
|
|
56
|
+
status,
|
|
57
|
+
ok ? chalk.green('✓ Pass') : chalk.red('✗ Fail')
|
|
58
|
+
])
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Summary
|
|
62
|
+
ui.divider();
|
|
63
|
+
const allPassed = results.every(([, , ok]) => ok);
|
|
64
|
+
if (allPassed) {
|
|
65
|
+
ui.success('All checks passed! Octus is ready to use.');
|
|
66
|
+
} else {
|
|
67
|
+
ui.warning('Some checks failed. Run `octus login` to configure.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export default pingCommand;
|