linage-cli 1.0.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/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # linage-cli
2
+
3
+ The Official CLI for Li'nage Cloud - Automate Dependency Discovery & Lineage Mapping.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g linage-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Login
14
+
15
+ First, authenticate with your API Key from the Li'nage Cloud dashboard:
16
+
17
+ ```bash
18
+ linage-cli login
19
+ ```
20
+
21
+ ### Ingest Dependencies
22
+
23
+ Scan your current directory and upload your dependency graph:
24
+
25
+ ```bash
26
+ linage-cli ingest
27
+ ```
28
+
29
+ ### View Projects
30
+
31
+ List all projects in your organization:
32
+
33
+ ```bash
34
+ linage-cli projects
35
+ ```
36
+
37
+ ### Show Stats
38
+
39
+ Show organization health metrics:
40
+
41
+ ```bash
42
+ linage-cli stats
43
+ ```
44
+
45
+ ### Whoami
46
+
47
+ Show current authenticated session info:
48
+
49
+ ```bash
50
+ linage-cli whoami
51
+ ```
52
+
53
+ ### Open Dashboard
54
+
55
+ Open your dashboard in the browser:
56
+
57
+ ```bash
58
+ linage-cli open
59
+ ```
60
+
61
+ ## License
62
+
63
+ ISC
@@ -0,0 +1,60 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const ora = require('ora');
4
+ const api = require('../utils/api');
5
+ const { log } = require('../utils/ui');
6
+ const { detectProject } = require('../utils/scanner');
7
+
8
+ module.exports = async (options) => {
9
+ const projectInfo = detectProject();
10
+
11
+ if (!projectInfo) {
12
+ log.error('No supported dependency file found (package.json, requirements.txt, Cargo.toml).');
13
+ return;
14
+ }
15
+
16
+ const spinner = ora(`Reading ${projectInfo.file}...`).start();
17
+
18
+ try {
19
+ const filePath = path.resolve(process.cwd(), projectInfo.file);
20
+ const content = fs.readFileSync(filePath, 'utf-8');
21
+
22
+ let projectCode = options.project;
23
+ let version = 'unknown';
24
+
25
+ if (projectInfo.type === 'node') {
26
+ const parsed = JSON.parse(content);
27
+ projectCode = projectCode || parsed.name;
28
+ version = parsed.version;
29
+ } else {
30
+ // For python/rust, we'll use the directory name as default project code
31
+ projectCode = projectCode || path.basename(process.cwd());
32
+ }
33
+
34
+ if (!projectCode) {
35
+ spinner.fail('Could not determine project name.');
36
+ log.error('Please specify with --project <name>.');
37
+ return;
38
+ }
39
+
40
+ spinner.text = `Ingesting ${projectCode} (${projectInfo.type})...`;
41
+
42
+ const apiOpts = {};
43
+ if (options.key) apiOpts.apiKey = options.key;
44
+
45
+ const result = await api.post('/api/ingest/cli', {
46
+ projectCode,
47
+ fileName: projectInfo.file,
48
+ ecosystem: projectInfo.type,
49
+ content
50
+ }, apiOpts);
51
+
52
+ spinner.succeed(`Ingested ${projectCode} successfully!`);
53
+ if (result.version) log.info(`Version: ${result.version}`);
54
+ log.dim(`View at: ${require('../utils/config').getApiUrl()}/${result.orgSlug}/projects/${result.projectCode}`);
55
+
56
+ } catch (error) {
57
+ spinner.fail('Ingestion failed.');
58
+ log.error(error.message);
59
+ }
60
+ };
@@ -0,0 +1,37 @@
1
+ const inquirer = require('inquirer');
2
+ const ora = require('ora');
3
+ const config = require('../utils/config');
4
+ const api = require('../utils/api');
5
+ const { log, printBox } = require('../utils/ui');
6
+
7
+ module.exports = async () => {
8
+ log.info("To get started, you need an API Key from your Li'nage Cloud dashboard.");
9
+
10
+ const answers = await inquirer.prompt([
11
+ {
12
+ type: 'password',
13
+ name: 'apiKey',
14
+ message: 'Paste your API Key:',
15
+ validate: (input) => input.length > 5 || 'API Key seems too short.'
16
+ }
17
+ ]);
18
+
19
+ const spinner = ora('Verifying API Key...').start();
20
+
21
+ try {
22
+ const isValid = await api.validateKey(answers.apiKey);
23
+
24
+ if (isValid && isValid.success) {
25
+ config.setApiKey(answers.apiKey);
26
+ spinner.succeed('Authentication successful!');
27
+ printBox(`Logged in as:\nOrganization: ${isValid.organization.name} (${isValid.organization.slug})\nKey Name: ${isValid.keyName}`);
28
+ } else {
29
+ spinner.fail('Invalid API Key.');
30
+ log.error('Please check your key and try again.');
31
+ }
32
+ } catch (error) {
33
+ spinner.fail('Connection failed.');
34
+ log.error(error.message);
35
+ }
36
+ };
37
+
@@ -0,0 +1,7 @@
1
+ const config = require('../utils/config');
2
+ const { log } = require('../utils/ui');
3
+
4
+ module.exports = () => {
5
+ config.clearApiKey();
6
+ log.success('Logged out successfully. API Key removed from config.');
7
+ };
@@ -0,0 +1,32 @@
1
+ const open = require('open');
2
+ const config = require('../utils/config');
3
+ const api = require('../utils/api');
4
+ const { log } = require('../utils/ui');
5
+
6
+ module.exports = async (projectName, options) => {
7
+ const baseUrl = config.getApiUrl();
8
+ const apiKey = options.key || config.getApiKey();
9
+
10
+ if (!apiKey) {
11
+ log.error('Not authenticated. Please run "linage-cli login" first.');
12
+ return;
13
+ }
14
+
15
+ let url = `${baseUrl}`;
16
+
17
+ try {
18
+ const session = await api.get('/api/cli/whoami', { apiKey });
19
+ const orgSlug = session.organization.slug;
20
+
21
+ if (projectName) {
22
+ url = `${baseUrl}/${orgSlug}/projects/${projectName}`;
23
+ } else {
24
+ url = `${baseUrl}/${orgSlug}/dashboard`;
25
+ }
26
+
27
+ log.info(`Opening ${url}...`);
28
+ await open(url);
29
+ } catch (error) {
30
+ log.error(`Failed to open dashboard: ${error.message}`);
31
+ }
32
+ };
@@ -0,0 +1,32 @@
1
+ const { table } = require('table');
2
+ const ora = require('ora');
3
+ const api = require('../utils/api');
4
+ const { log } = require('../utils/ui');
5
+
6
+ module.exports = async (options) => {
7
+ const spinner = ora('Fetching projects...').start();
8
+
9
+ try {
10
+ const apiOpts = {};
11
+ if (options.key) apiOpts.apiKey = options.key;
12
+
13
+ const data = await api.get('/api/cli/projects', apiOpts);
14
+ spinner.stop();
15
+
16
+ if (data.projects.length === 0) {
17
+ log.info('No projects found in this organization.');
18
+ return;
19
+ }
20
+
21
+ const outputData = [
22
+ ['Name', 'Code', 'Status', 'Latest Version'],
23
+ ...data.projects.map(p => [p.name, p.code, p.status, p.latestVersion])
24
+ ];
25
+
26
+ console.log(table(outputData));
27
+
28
+ } catch (error) {
29
+ spinner.fail('Failed to fetch projects.');
30
+ log.error(error.message);
31
+ }
32
+ };
@@ -0,0 +1,33 @@
1
+ const ora = require('ora');
2
+ const api = require('../utils/api');
3
+ const { log, printBox } = require('../utils/ui');
4
+ const { table } = require('table');
5
+
6
+ module.exports = async (options) => {
7
+ const spinner = ora('Calculating metrics...').start();
8
+
9
+ try {
10
+ const apiOpts = {};
11
+ if (options.key) apiOpts.apiKey = options.key;
12
+
13
+ const data = await api.get('/api/cli/stats', apiOpts);
14
+ spinner.stop();
15
+
16
+ const { stats } = data;
17
+
18
+ printBox(`Organization Summary\n\nProjects: ${stats.projects}\nIngestions: ${stats.ingestions}\nTeam Members: ${stats.members}`, { borderColor: 'magenta' });
19
+
20
+ if (stats.ecosystems.length > 0) {
21
+ log.info('Ecosystem Distribution:');
22
+ const ecoTable = [
23
+ ['Ecosystem', 'Count'],
24
+ ...stats.ecosystems.map(e => [e.name || 'Unknown', e.count])
25
+ ];
26
+ console.log(table(ecoTable));
27
+ }
28
+
29
+ } catch (error) {
30
+ spinner.fail('Failed to fetch stats.');
31
+ log.error(error.message);
32
+ }
33
+ };
@@ -0,0 +1,21 @@
1
+ const ora = require('ora');
2
+ const api = require('../utils/api');
3
+ const { log, printBox } = require('../utils/ui');
4
+
5
+ module.exports = async (options) => {
6
+ const spinner = ora('Fetching session info...').start();
7
+
8
+ try {
9
+ const apiOpts = {};
10
+ if (options.key) apiOpts.apiKey = options.key;
11
+
12
+ const data = await api.get('/api/cli/whoami', apiOpts);
13
+
14
+ spinner.stop();
15
+ printBox(`Authenticated Session\n\nOrganization: ${data.organization.name}\nSlug: ${data.organization.slug}\nKey Name: ${data.keyName}`, { borderColor: 'green' });
16
+
17
+ } catch (error) {
18
+ spinner.fail('Failed to fetch info.');
19
+ log.error(error.message);
20
+ }
21
+ };
package/index.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const { printHeader } = require('./utils/ui');
5
+
6
+ // Import Commands
7
+ const loginCmd = require('./commands/login');
8
+ const ingestCmd = require('./commands/ingest');
9
+ const projectsCmd = require('./commands/projects');
10
+ const whoamiCmd = require('./commands/whoami');
11
+ const logoutCmd = require('./commands/logout');
12
+ const openCmd = require('./commands/open');
13
+ const statsCmd = require('./commands/stats');
14
+
15
+ // Initial Setup
16
+ printHeader();
17
+
18
+ program
19
+ .name('linage-cli')
20
+ .description("The Official CLI for Li'nage Cloud - Automate Dependency Discovery")
21
+ .version('1.0.0');
22
+
23
+ program
24
+ .command('login')
25
+ .description('Interactive login to save your API Key')
26
+ .action(loginCmd);
27
+
28
+ program
29
+ .command('ingest')
30
+ .description('Scan current directory and upload dependency graph')
31
+ .option('-k, --key <key>', 'Override API Key')
32
+ .option('-p, --project <name>', 'Override Project Code Name')
33
+ .action(ingestCmd);
34
+
35
+ program
36
+ .command('projects')
37
+ .description('List all projects in your organization')
38
+ .option('-k, --key <key>', 'Override API Key')
39
+ .action(projectsCmd);
40
+
41
+ program
42
+ .command('stats')
43
+ .description('Show organization health metrics')
44
+ .option('-k, --key <key>', 'Override API Key')
45
+ .action(statsCmd);
46
+
47
+ program
48
+ .command('whoami')
49
+ .description('Show current authenticated session info')
50
+ .option('-k, --key <key>', 'Override API Key')
51
+ .action(whoamiCmd);
52
+
53
+ program
54
+ .command('open [project]')
55
+ .description('Open your dashboard in the browser')
56
+ .option('-k, --key <key>', 'Override API Key')
57
+ .action(openCmd);
58
+
59
+ program
60
+ .command('logout')
61
+ .description('Clear saved credentials')
62
+ .action(logoutCmd);
63
+
64
+ // Normalize arguments to handle case-insensitivity
65
+ const knownCommands = ['login', 'ingest', 'projects', 'whoami', 'logout', 'help', 'open', 'stats'];
66
+ const knownFlags = ['--key', '--project', '--version', '--help'];
67
+
68
+ const args = process.argv.map((arg, index) => {
69
+ if (index < 2) return arg; // Skip node and script path
70
+
71
+ const lowerArg = arg.toLowerCase();
72
+
73
+ // 1. Map -v to -V for version
74
+ if (arg === '-v') return '-V';
75
+
76
+ // 2. Lowercase known commands
77
+ if (knownCommands.includes(lowerArg)) return lowerArg;
78
+
79
+ // 3. Handle long flags (e.g., --KEY or --PROJECT)
80
+ if (arg.startsWith('--')) {
81
+ const parts = arg.split('=');
82
+ const flagName = parts[0].toLowerCase();
83
+ if (knownFlags.includes(flagName)) {
84
+ return parts.length > 1 ? `${flagName}=${parts.slice(1).join('=')}` : flagName;
85
+ }
86
+ }
87
+
88
+ return arg;
89
+ });
90
+
91
+ program.parse(args);
92
+
93
+ if (!process.argv.slice(2).length) {
94
+ program.outputHelp();
95
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "linage-cli",
3
+ "version": "1.0.0",
4
+ "description": "The Official CLI for Li'nage Cloud - Automate Dependency Discovery",
5
+ "main": "index.js",
6
+ "bin": "index.js",
7
+ "files": [
8
+ "index.js",
9
+ "commands/",
10
+ "utils/",
11
+ "README.md"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/linage-cloud/linage-cloud.git"
16
+ },
17
+ "scripts": {
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ },
20
+ "keywords": [
21
+ "linage",
22
+ "cli",
23
+ "dependencies",
24
+ "lineage",
25
+ "mapping"
26
+ ],
27
+ "author": "Li'nage",
28
+ "license": "ISC",
29
+ "dependencies": {
30
+ "boxen": "^5.1.2",
31
+ "chalk": "^4.1.2",
32
+ "commander": "^14.0.2",
33
+ "conf": "^10.2.0",
34
+ "figlet": "^1.9.4",
35
+ "gradient-string": "^3.0.0",
36
+ "inquirer": "^8.2.5",
37
+ "node-fetch": "^2.7.0",
38
+ "open": "^8.4.2",
39
+ "ora": "^5.4.1",
40
+ "table": "^6.8.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=14.0.0"
44
+ }
45
+ }
package/utils/api.js ADDED
@@ -0,0 +1,65 @@
1
+ const fetch = global.fetch || require('node-fetch');
2
+ const config = require('./config');
3
+ const { log } = require('./ui');
4
+
5
+ const request = async (endpoint, method = 'GET', opts = {}) => {
6
+ const apiKey = opts.apiKey || config.getApiKey();
7
+ const baseUrl = config.getApiUrl();
8
+
9
+ if (!apiKey) {
10
+ throw new Error('Not authenticated. Please run "linage-cli login" first or provide --key.');
11
+ }
12
+
13
+ const headers = {
14
+ 'Content-Type': 'application/json',
15
+ 'Authorization': `Bearer ${apiKey}`
16
+ };
17
+
18
+ const options = {
19
+ method,
20
+ headers,
21
+ };
22
+
23
+ if (opts.body) {
24
+ options.body = JSON.stringify(opts.body);
25
+ }
26
+
27
+ try {
28
+ const res = await fetch(`${baseUrl}${endpoint}`, options);
29
+
30
+ // Handle 401 specifically
31
+ if (res.status === 401 || res.status === 403) {
32
+ throw new Error('Invalid or expired API Key. Please login again.');
33
+ }
34
+
35
+ const data = await res.json();
36
+
37
+ if (!res.ok) {
38
+ throw new Error(data.error || data.message || `API Error: ${res.statusText}`);
39
+ }
40
+
41
+ return data;
42
+ } catch (error) {
43
+ throw error; // Re-throw to be handled by commands
44
+ }
45
+ };
46
+
47
+ module.exports = {
48
+ get: (endpoint, opts = {}) => request(endpoint, 'GET', opts),
49
+ post: (endpoint, body, opts = {}) => request(endpoint, 'POST', { ...opts, body }),
50
+ delete: (endpoint, opts = {}) => request(endpoint, 'DELETE', opts),
51
+ // Helper to validate key without throwing immediately
52
+ validateKey: async (key) => {
53
+ const baseUrl = config.getApiUrl();
54
+ try {
55
+ // We'll create a simple /api/whoami endpoint to validate
56
+ const res = await fetch(`${baseUrl}/api/cli/whoami`, {
57
+ headers: { 'Authorization': `Bearer ${key}` }
58
+ });
59
+ if (!res.ok) return false;
60
+ return await res.json();
61
+ } catch (e) {
62
+ return false;
63
+ }
64
+ }
65
+ };
@@ -0,0 +1,16 @@
1
+ const Conf = require('conf');
2
+
3
+ const config = new Conf({
4
+ projectName: 'linage-cli',
5
+ projectVersion: '1.0.0'
6
+ });
7
+
8
+ module.exports = {
9
+ getApiKey: () => config.get('apiKey'),
10
+ setApiKey: (key) => config.set('apiKey', key),
11
+ clearApiKey: () => config.delete('apiKey'),
12
+
13
+ // Base URL handling (useful for dev vs prod switching if needed later)
14
+ getApiUrl: () => process.env.LINAGE_API_URL || config.get('apiUrl') || 'http://localhost:3000',
15
+ setApiUrl: (url) => config.set('apiUrl', url)
16
+ };
@@ -0,0 +1,26 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const detectProject = () => {
5
+ const cwd = process.cwd();
6
+
7
+ if (fs.existsSync(path.join(cwd, 'package.json'))) {
8
+ return { type: 'node', file: 'package.json' };
9
+ }
10
+
11
+ if (fs.existsSync(path.join(cwd, 'requirements.txt'))) {
12
+ return { type: 'python', file: 'requirements.txt' };
13
+ }
14
+
15
+ if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) {
16
+ return { type: 'rust', file: 'Cargo.toml' };
17
+ }
18
+
19
+ if (fs.existsSync(path.join(cwd, 'go.mod'))) {
20
+ return { type: 'go', file: 'go.mod' };
21
+ }
22
+
23
+ return null;
24
+ };
25
+
26
+ module.exports = { detectProject };
package/utils/ui.js ADDED
@@ -0,0 +1,35 @@
1
+ const chalk = require('chalk');
2
+ const figlet = require('figlet');
3
+ const gradient = require('gradient-string');
4
+ const boxen = require('boxen');
5
+
6
+ const printHeader = () => {
7
+ console.log('');
8
+ console.log(gradient.pastel.multiline(figlet.textSync("LI'NAGE", { horizontalLayout: 'full' })));
9
+ console.log(chalk.dim(" Automated Dependency Discovery & Lineage Mapping by Li'nage"));
10
+ console.log('');
11
+ };
12
+
13
+ const log = {
14
+ info: (msg) => console.log(chalk.blue('ℹ'), msg),
15
+ success: (msg) => console.log(chalk.green('✔'), msg),
16
+ warning: (msg) => console.log(chalk.yellow('⚠'), msg),
17
+ error: (msg) => console.log(chalk.red('✖'), msg),
18
+ dim: (msg) => console.log(chalk.dim(msg)),
19
+ };
20
+
21
+ const printBox = (text, options = {}) => {
22
+ console.log(boxen(text, {
23
+ padding: 1,
24
+ margin: 1,
25
+ borderStyle: 'round',
26
+ borderColor: 'cyan',
27
+ ...options
28
+ }));
29
+ };
30
+
31
+ module.exports = {
32
+ printHeader,
33
+ log,
34
+ printBox
35
+ };