@joint/cli 0.1.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 +77 -0
- package/dist/cli.js +86 -0
- package/dist/commands/download.js +32 -0
- package/dist/commands/list.js +15 -0
- package/dist/constants.js +9 -0
- package/dist/lib/git.js +34 -0
- package/dist/lib/github.js +32 -0
- package/dist/lib/logger.js +21 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @joint/cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for [JointJS](https://jointjs.com).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @joint/cli list
|
|
9
|
+
npx @joint/cli download scada/js
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @joint/cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Once installed globally, the `joint` command is available:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
joint list
|
|
22
|
+
joint download scada/js
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
### `joint list`
|
|
28
|
+
|
|
29
|
+
List available examples from the [joint-demos](https://github.com/clientIO/joint-demos) repository.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
joint list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### `joint download <name> [dest]`
|
|
36
|
+
|
|
37
|
+
Download an example into the current working directory.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Downloads into ./scada-js/
|
|
41
|
+
joint download scada/js
|
|
42
|
+
|
|
43
|
+
# Downloads into ./scada/
|
|
44
|
+
joint download scada/js scada
|
|
45
|
+
|
|
46
|
+
# Downloads into the current directory
|
|
47
|
+
joint download scada/js .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Options
|
|
51
|
+
|
|
52
|
+
| Option | Description |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `--help`, `-h` | Show help message |
|
|
55
|
+
| `--version`, `-v` | Show version number |
|
|
56
|
+
| `--owner <name>` | GitHub repo owner (default: `clientIO`) |
|
|
57
|
+
| `--branch <name>` | GitHub repo branch (default: `main`) |
|
|
58
|
+
|
|
59
|
+
### Working with forks
|
|
60
|
+
|
|
61
|
+
Use `--owner` and `--branch` to list and download examples from a fork:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
joint list --owner myGitHubUser
|
|
65
|
+
joint download scada/js --owner myGitHubUser --branch dev
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Environment Variables
|
|
69
|
+
|
|
70
|
+
| Variable | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `GITHUB_TOKEN` | Optional GitHub token to avoid API rate limiting |
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
- Node.js >= 22
|
|
77
|
+
- Git
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { list } from './commands/list.js';
|
|
4
|
+
import { download } from './commands/download.js';
|
|
5
|
+
import { DEFAULT_OWNER, DEFAULT_BRANCH } from './constants.js';
|
|
6
|
+
import * as logger from './lib/logger.js';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version: VERSION } = require('../package.json');
|
|
9
|
+
const HELP = `
|
|
10
|
+
${logger.bold('joint')} — Command-line tool for JointJS
|
|
11
|
+
|
|
12
|
+
${logger.bold('Usage:')}
|
|
13
|
+
joint <command> [options]
|
|
14
|
+
|
|
15
|
+
${logger.bold('Commands:')}
|
|
16
|
+
list List available examples
|
|
17
|
+
download <name> [dest] Download an example
|
|
18
|
+
|
|
19
|
+
${logger.bold('Options:')}
|
|
20
|
+
--help, -h Show this help message
|
|
21
|
+
--version, -v Show version number
|
|
22
|
+
--owner <name> GitHub repo owner (default: ${DEFAULT_OWNER})
|
|
23
|
+
--branch <name> GitHub repo branch (default: ${DEFAULT_BRANCH})
|
|
24
|
+
|
|
25
|
+
${logger.bold('Environment:')}
|
|
26
|
+
GITHUB_TOKEN Optional GitHub token to avoid rate limiting
|
|
27
|
+
`;
|
|
28
|
+
function getFlag(args, name) {
|
|
29
|
+
const index = args.indexOf(name);
|
|
30
|
+
if (index === -1 || index + 1 >= args.length)
|
|
31
|
+
return undefined;
|
|
32
|
+
return args[index + 1];
|
|
33
|
+
}
|
|
34
|
+
function stripFlags(args) {
|
|
35
|
+
const result = [];
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
if (args[i] === '--owner' || args[i] === '--branch') {
|
|
38
|
+
i++; // skip the value
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
result.push(args[i]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
async function main() {
|
|
47
|
+
const rawArgs = process.argv.slice(2);
|
|
48
|
+
if (rawArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
49
|
+
console.log(HELP);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
|
|
53
|
+
console.log(VERSION);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const options = {
|
|
57
|
+
owner: getFlag(rawArgs, '--owner') ?? DEFAULT_OWNER,
|
|
58
|
+
branch: getFlag(rawArgs, '--branch') ?? DEFAULT_BRANCH,
|
|
59
|
+
};
|
|
60
|
+
const args = stripFlags(rawArgs);
|
|
61
|
+
const command = args[0];
|
|
62
|
+
switch (command) {
|
|
63
|
+
case 'list':
|
|
64
|
+
await list(options);
|
|
65
|
+
break;
|
|
66
|
+
case 'download': {
|
|
67
|
+
const folder = args[1];
|
|
68
|
+
if (!folder) {
|
|
69
|
+
logger.error('Missing required argument: <name>\n');
|
|
70
|
+
logger.info('Usage: joint download <name> [dest]');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const target = args[2];
|
|
74
|
+
await download(folder, target, options);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
logger.error(`Unknown command: "${command}"\n`);
|
|
79
|
+
logger.info('Run "joint --help" to see available commands.');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
main().catch((err) => {
|
|
84
|
+
logger.error(err.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { listDemoFolders } from '../lib/github.js';
|
|
4
|
+
import { sparseCheckout } from '../lib/git.js';
|
|
5
|
+
import * as logger from '../lib/logger.js';
|
|
6
|
+
export async function download(folder, target, options) {
|
|
7
|
+
logger.info(`Validating example "${folder}"...\n`);
|
|
8
|
+
const folders = await listDemoFolders(options);
|
|
9
|
+
if (!folders.includes(folder)) {
|
|
10
|
+
logger.error(`Example "${folder}" not found.`);
|
|
11
|
+
if (folders.length > 0) {
|
|
12
|
+
console.log(`\n${logger.bold('Available examples:')}\n`);
|
|
13
|
+
for (const f of folders) {
|
|
14
|
+
console.log(` - ${f}`);
|
|
15
|
+
}
|
|
16
|
+
console.log();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
logger.warn('No examples are available yet.');
|
|
20
|
+
}
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const dirName = target ?? folder.replace(/\//g, '-');
|
|
24
|
+
const dest = resolve(process.cwd(), dirName);
|
|
25
|
+
if (dirName !== '.' && existsSync(dest)) {
|
|
26
|
+
logger.error(`Directory "${dirName}" already exists in the current directory.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
logger.info(`Downloading "${folder}"...`);
|
|
30
|
+
await sparseCheckout(folder, dest, options);
|
|
31
|
+
logger.success(`\nDone! Example downloaded to ./${dirName}`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { listDemoFolders } from '../lib/github.js';
|
|
2
|
+
import * as logger from '../lib/logger.js';
|
|
3
|
+
export async function list(options) {
|
|
4
|
+
logger.info('Fetching available examples...\n');
|
|
5
|
+
const folders = await listDemoFolders(options);
|
|
6
|
+
if (folders.length === 0) {
|
|
7
|
+
logger.warn('No examples available yet.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
console.log(logger.bold('Available examples:\n'));
|
|
11
|
+
for (const folder of folders) {
|
|
12
|
+
console.log(` - ${folder}`);
|
|
13
|
+
}
|
|
14
|
+
console.log();
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const DEFAULT_OWNER = 'clientIO';
|
|
2
|
+
export const DEFAULT_REPO = 'joint-demos';
|
|
3
|
+
export const DEFAULT_BRANCH = 'main';
|
|
4
|
+
export function getRepoUrl({ owner }) {
|
|
5
|
+
return `https://github.com/${owner}/${DEFAULT_REPO}.git`;
|
|
6
|
+
}
|
|
7
|
+
export function getGitHubApiUrl({ owner }) {
|
|
8
|
+
return `https://api.github.com/repos/${owner}/${DEFAULT_REPO}`;
|
|
9
|
+
}
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm, cp } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { getRepoUrl } from '../constants.js';
|
|
6
|
+
function run(command, args, cwd) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
execFile(command, args, { cwd }, (err, stdout, stderr) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
reject(new Error(stderr || err.message));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
resolve(stdout);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function sparseCheckout(folder, dest, options) {
|
|
18
|
+
const repoUrl = getRepoUrl(options);
|
|
19
|
+
const tmp = await mkdtemp(join(tmpdir(), 'joint-cli-'));
|
|
20
|
+
try {
|
|
21
|
+
await run('git', ['init', tmp]);
|
|
22
|
+
await run('git', ['remote', 'add', 'origin', repoUrl], tmp);
|
|
23
|
+
await run('git', ['sparse-checkout', 'init', '--cone'], tmp);
|
|
24
|
+
await run('git', ['sparse-checkout', 'set', folder], tmp);
|
|
25
|
+
await run('git', ['pull', 'origin', options.branch, '--depth=1'], tmp);
|
|
26
|
+
const src = join(tmp, folder);
|
|
27
|
+
// If dest already exists (e.g. "."), copy contents into it.
|
|
28
|
+
// Otherwise, move the folder directly.
|
|
29
|
+
await cp(src, dest, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await rm(tmp, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getGitHubApiUrl } from '../constants.js';
|
|
2
|
+
function buildHeaders() {
|
|
3
|
+
const headers = {
|
|
4
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
5
|
+
'User-Agent': 'joint-cli',
|
|
6
|
+
};
|
|
7
|
+
const token = process.env.GITHUB_TOKEN;
|
|
8
|
+
if (token) {
|
|
9
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
10
|
+
}
|
|
11
|
+
return headers;
|
|
12
|
+
}
|
|
13
|
+
export async function listDemoFolders(options) {
|
|
14
|
+
const apiUrl = getGitHubApiUrl(options);
|
|
15
|
+
const url = `${apiUrl}/git/trees/${options.branch}?recursive=1`;
|
|
16
|
+
const response = await fetch(url, { headers: buildHeaders() });
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
if (response.status === 404) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`);
|
|
22
|
+
}
|
|
23
|
+
const data = await response.json();
|
|
24
|
+
if (!Array.isArray(data.tree)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
// Return only 2-level deep directories (e.g. "scada/js", "kitchen-sink/ts")
|
|
28
|
+
return data.tree
|
|
29
|
+
.filter((item) => item.type === 'tree' && item.path.split('/').length === 2)
|
|
30
|
+
.map((item) => item.path)
|
|
31
|
+
.sort();
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const RESET = '\x1b[0m';
|
|
2
|
+
const RED = '\x1b[31m';
|
|
3
|
+
const GREEN = '\x1b[32m';
|
|
4
|
+
const YELLOW = '\x1b[33m';
|
|
5
|
+
const CYAN = '\x1b[36m';
|
|
6
|
+
const BOLD = '\x1b[1m';
|
|
7
|
+
export function info(msg) {
|
|
8
|
+
console.log(`${CYAN}${msg}${RESET}`);
|
|
9
|
+
}
|
|
10
|
+
export function success(msg) {
|
|
11
|
+
console.log(`${GREEN}${msg}${RESET}`);
|
|
12
|
+
}
|
|
13
|
+
export function warn(msg) {
|
|
14
|
+
console.log(`${YELLOW}${msg}${RESET}`);
|
|
15
|
+
}
|
|
16
|
+
export function error(msg) {
|
|
17
|
+
console.error(`${RED}${msg}${RESET}`);
|
|
18
|
+
}
|
|
19
|
+
export function bold(msg) {
|
|
20
|
+
return `${BOLD}${msg}${RESET}`;
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joint/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Command-line tool for JointJS.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"joint": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "echo \"Publishing via NPM is not allowed!\" && exit 1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"typescript": "^5.8.0"
|
|
20
|
+
},
|
|
21
|
+
"volta": {
|
|
22
|
+
"node": "22.14.0",
|
|
23
|
+
"npm": "11.2.0",
|
|
24
|
+
"yarn": "4.7.0"
|
|
25
|
+
}
|
|
26
|
+
}
|