@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 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
+ }
@@ -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
+ }