@joint/cli 0.1.1 → 0.2.2
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 +17 -9
- package/dist/__tests__/download.test.js +67 -0
- package/dist/__tests__/github.test.js +92 -0
- package/dist/__tests__/list.test.js +55 -0
- package/dist/cli.js +15 -5
- package/dist/commands/download.js +15 -8
- package/dist/commands/list.js +3 -3
- package/dist/lib/git.js +14 -6
- package/dist/lib/github.js +5 -3
- package/dist/lib/logger.js +5 -1
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Command-line tool for [JointJS](https://jointjs.com).
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx @joint/cli list
|
|
9
|
-
npx @joint/cli download
|
|
9
|
+
npx @joint/cli download kitchen-sink/js
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
@@ -19,7 +19,7 @@ Once installed globally, the `joint` command is available:
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
joint list
|
|
22
|
-
joint download
|
|
22
|
+
joint download kitchen-sink/js
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
## Commands
|
|
@@ -37,14 +37,21 @@ joint list
|
|
|
37
37
|
Download an example into the current working directory.
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
# Downloads into ./
|
|
41
|
-
joint download
|
|
40
|
+
# Downloads into ./kitchen-sink-js/
|
|
41
|
+
joint download kitchen-sink/js
|
|
42
42
|
|
|
43
|
-
# Downloads into ./
|
|
44
|
-
joint download
|
|
43
|
+
# Downloads into ./my-app/
|
|
44
|
+
joint download kitchen-sink/js my-app
|
|
45
45
|
|
|
46
|
-
# Downloads into the current directory
|
|
47
|
-
joint download
|
|
46
|
+
# Downloads into the current directory (must be empty or use --force)
|
|
47
|
+
joint download kitchen-sink/js .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If the destination directory already exists, use `--force` to overwrite:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
joint download kitchen-sink/js --force
|
|
54
|
+
joint download kitchen-sink/js . --force
|
|
48
55
|
```
|
|
49
56
|
|
|
50
57
|
## Options
|
|
@@ -55,6 +62,7 @@ joint download scada/js .
|
|
|
55
62
|
| `--version`, `-v` | Show version number |
|
|
56
63
|
| `--owner <name>` | GitHub repo owner (default: `clientIO`) |
|
|
57
64
|
| `--branch <name>` | GitHub repo branch (default: `main`) |
|
|
65
|
+
| `--force` | Overwrite existing files when downloading |
|
|
58
66
|
|
|
59
67
|
### Working with forks
|
|
60
68
|
|
|
@@ -62,7 +70,7 @@ Use `--owner` and `--branch` to list and download examples from a fork:
|
|
|
62
70
|
|
|
63
71
|
```bash
|
|
64
72
|
joint list --owner myGitHubUser
|
|
65
|
-
joint download
|
|
73
|
+
joint download kitchen-sink/js --owner myGitHubUser --branch dev
|
|
66
74
|
```
|
|
67
75
|
|
|
68
76
|
## Environment Variables
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
const defaultOptions = { owner: 'clientIO', branch: 'main' };
|
|
4
|
+
function mockFetchWithFolders(folders) {
|
|
5
|
+
const tree = folders.flatMap((f) => {
|
|
6
|
+
const top = f.split('/')[0];
|
|
7
|
+
return [
|
|
8
|
+
{ path: top, type: 'tree' },
|
|
9
|
+
{ path: f, type: 'tree' },
|
|
10
|
+
];
|
|
11
|
+
});
|
|
12
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
13
|
+
ok: true,
|
|
14
|
+
json: async () => ({ tree }),
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
describe('download command', () => {
|
|
18
|
+
let originalFetch;
|
|
19
|
+
const originalExit = process.exit;
|
|
20
|
+
let exitCode;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
originalFetch = globalThis.fetch;
|
|
23
|
+
exitCode = undefined;
|
|
24
|
+
process.exit = mock.fn((code) => {
|
|
25
|
+
exitCode = code;
|
|
26
|
+
throw new Error(`process.exit(${code})`);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
globalThis.fetch = originalFetch;
|
|
31
|
+
process.exit = originalExit;
|
|
32
|
+
});
|
|
33
|
+
it('exits with error when example not found', async () => {
|
|
34
|
+
mockFetchWithFolders(['scada/js', 'scada/ts']);
|
|
35
|
+
// Dynamic import to get fresh module
|
|
36
|
+
const { download } = await import('../commands/download.js');
|
|
37
|
+
await assert.rejects(() => download('nonexistent/js', undefined, defaultOptions), { message: 'process.exit(1)' });
|
|
38
|
+
assert.equal(exitCode, 1);
|
|
39
|
+
});
|
|
40
|
+
it('exits with error when folder not found in empty repo', async () => {
|
|
41
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
42
|
+
ok: true,
|
|
43
|
+
json: async () => ({ tree: [] }),
|
|
44
|
+
}));
|
|
45
|
+
const { download } = await import('../commands/download.js');
|
|
46
|
+
await assert.rejects(() => download('scada/js', undefined, defaultOptions), { message: 'process.exit(1)' });
|
|
47
|
+
});
|
|
48
|
+
it('computes default directory name from folder path', async () => {
|
|
49
|
+
// We can test the naming logic by checking what happens when the dir already exists
|
|
50
|
+
// If we provide a folder that exists in the list, it will try to create "scada-js"
|
|
51
|
+
// We test this indirectly — the naming logic is: folder.replace(/\//g, '-')
|
|
52
|
+
const folderName = 'scada/js';
|
|
53
|
+
const expected = 'scada-js';
|
|
54
|
+
assert.equal(folderName.replace(/\//g, '-'), expected);
|
|
55
|
+
});
|
|
56
|
+
it('uses custom target name when provided', () => {
|
|
57
|
+
const folderName = 'scada/js';
|
|
58
|
+
const target = 'my-project';
|
|
59
|
+
const dirName = target ?? folderName.replace(/\//g, '-');
|
|
60
|
+
assert.equal(dirName, 'my-project');
|
|
61
|
+
});
|
|
62
|
+
it('handles dot target for current directory', () => {
|
|
63
|
+
const dirName = '.';
|
|
64
|
+
const displayPath = dirName === '.' ? 'current directory' : `./${dirName}`;
|
|
65
|
+
assert.equal(displayPath, 'current directory');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { listDemoFolders } from '../lib/github.js';
|
|
4
|
+
const defaultOptions = { owner: 'clientIO', branch: 'main' };
|
|
5
|
+
describe('listDemoFolders', () => {
|
|
6
|
+
let originalFetch;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
originalFetch = globalThis.fetch;
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
globalThis.fetch = originalFetch;
|
|
12
|
+
delete process.env.GITHUB_TOKEN;
|
|
13
|
+
});
|
|
14
|
+
it('returns sorted 2-level deep directories', async () => {
|
|
15
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
16
|
+
ok: true,
|
|
17
|
+
json: async () => ({
|
|
18
|
+
tree: [
|
|
19
|
+
{ path: 'scada', type: 'tree' },
|
|
20
|
+
{ path: 'scada/js', type: 'tree' },
|
|
21
|
+
{ path: 'scada/ts', type: 'tree' },
|
|
22
|
+
{ path: 'scada/js/package.json', type: 'blob' },
|
|
23
|
+
{ path: 'kitchen-sink', type: 'tree' },
|
|
24
|
+
{ path: 'kitchen-sink/js', type: 'tree' },
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
const result = await listDemoFolders(defaultOptions);
|
|
29
|
+
assert.deepEqual(result, ['kitchen-sink/js', 'scada/js', 'scada/ts']);
|
|
30
|
+
});
|
|
31
|
+
it('filters out blobs and top-level directories', async () => {
|
|
32
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: async () => ({
|
|
35
|
+
tree: [
|
|
36
|
+
{ path: 'README.md', type: 'blob' },
|
|
37
|
+
{ path: 'scada', type: 'tree' },
|
|
38
|
+
{ path: 'scada/js', type: 'tree' },
|
|
39
|
+
{ path: 'scada/js/src', type: 'tree' },
|
|
40
|
+
],
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
const result = await listDemoFolders(defaultOptions);
|
|
44
|
+
assert.deepEqual(result, ['scada/js']);
|
|
45
|
+
});
|
|
46
|
+
it('returns empty array when tree is empty', async () => {
|
|
47
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
48
|
+
ok: true,
|
|
49
|
+
json: async () => ({ tree: [] }),
|
|
50
|
+
}));
|
|
51
|
+
const result = await listDemoFolders(defaultOptions);
|
|
52
|
+
assert.deepEqual(result, []);
|
|
53
|
+
});
|
|
54
|
+
it('throws on 404', async () => {
|
|
55
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
56
|
+
ok: false,
|
|
57
|
+
status: 404,
|
|
58
|
+
statusText: 'Not Found',
|
|
59
|
+
}));
|
|
60
|
+
await assert.rejects(() => listDemoFolders(defaultOptions), { message: 'Repository or branch not found. Please verify the --owner and --branch options.' });
|
|
61
|
+
});
|
|
62
|
+
it('throws on other HTTP errors', async () => {
|
|
63
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
64
|
+
ok: false,
|
|
65
|
+
status: 403,
|
|
66
|
+
statusText: 'rate limit exceeded',
|
|
67
|
+
}));
|
|
68
|
+
await assert.rejects(() => listDemoFolders(defaultOptions), { message: 'GitHub API request failed: 403 rate limit exceeded' });
|
|
69
|
+
});
|
|
70
|
+
it('uses correct URL with custom owner and branch', async () => {
|
|
71
|
+
const mockFetch = mock.fn(async () => ({
|
|
72
|
+
ok: true,
|
|
73
|
+
json: async () => ({ tree: [] }),
|
|
74
|
+
}));
|
|
75
|
+
globalThis.fetch = mockFetch;
|
|
76
|
+
await listDemoFolders({ owner: 'myFork', branch: 'dev' });
|
|
77
|
+
const calledUrl = mockFetch.mock.calls[0].arguments[0];
|
|
78
|
+
assert.ok(calledUrl.includes('/myFork/'));
|
|
79
|
+
assert.ok(calledUrl.endsWith('/git/trees/dev?recursive=1'));
|
|
80
|
+
});
|
|
81
|
+
it('includes Authorization header when GITHUB_TOKEN is set', async () => {
|
|
82
|
+
process.env.GITHUB_TOKEN = 'test-token';
|
|
83
|
+
const mockFetch = mock.fn(async () => ({
|
|
84
|
+
ok: true,
|
|
85
|
+
json: async () => ({ tree: [] }),
|
|
86
|
+
}));
|
|
87
|
+
globalThis.fetch = mockFetch;
|
|
88
|
+
await listDemoFolders(defaultOptions);
|
|
89
|
+
const calledHeaders = mockFetch.mock.calls[0].arguments[1];
|
|
90
|
+
assert.equal(calledHeaders.headers['Authorization'], 'Bearer test-token');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { list } from '../commands/list.js';
|
|
4
|
+
const defaultOptions = { owner: 'clientIO', branch: 'main' };
|
|
5
|
+
describe('list command', () => {
|
|
6
|
+
let originalFetch;
|
|
7
|
+
const logOutput = [];
|
|
8
|
+
const originalLog = console.log;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
originalFetch = globalThis.fetch;
|
|
11
|
+
logOutput.length = 0;
|
|
12
|
+
console.log = (...args) => {
|
|
13
|
+
logOutput.push(args.join(' '));
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
globalThis.fetch = originalFetch;
|
|
18
|
+
console.log = originalLog;
|
|
19
|
+
});
|
|
20
|
+
it('prints available examples', async () => {
|
|
21
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
22
|
+
ok: true,
|
|
23
|
+
json: async () => ({
|
|
24
|
+
tree: [
|
|
25
|
+
{ path: 'scada', type: 'tree' },
|
|
26
|
+
{ path: 'scada/js', type: 'tree' },
|
|
27
|
+
{ path: 'kitchen-sink', type: 'tree' },
|
|
28
|
+
{ path: 'kitchen-sink/ts', type: 'tree' },
|
|
29
|
+
],
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
await list(defaultOptions);
|
|
33
|
+
assert.ok(logOutput.some((line) => line.includes('scada/js')));
|
|
34
|
+
assert.ok(logOutput.some((line) => line.includes('kitchen-sink/ts')));
|
|
35
|
+
});
|
|
36
|
+
it('handles empty tree', async () => {
|
|
37
|
+
globalThis.fetch = mock.fn(async () => ({
|
|
38
|
+
ok: true,
|
|
39
|
+
json: async () => ({ tree: [] }),
|
|
40
|
+
}));
|
|
41
|
+
await list(defaultOptions);
|
|
42
|
+
assert.ok(!logOutput.some((line) => line.includes(' - ')));
|
|
43
|
+
});
|
|
44
|
+
it('passes options to the API call', async () => {
|
|
45
|
+
const mockFetch = mock.fn(async () => ({
|
|
46
|
+
ok: true,
|
|
47
|
+
json: async () => ({ tree: [] }),
|
|
48
|
+
}));
|
|
49
|
+
globalThis.fetch = mockFetch;
|
|
50
|
+
await list({ owner: 'myFork', branch: 'dev' });
|
|
51
|
+
const calledUrl = mockFetch.mock.calls[0].arguments[0];
|
|
52
|
+
assert.ok(calledUrl.includes('/myFork/'));
|
|
53
|
+
assert.ok(calledUrl.includes('/dev?'));
|
|
54
|
+
});
|
|
55
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -21,20 +21,29 @@ ${logger.bold('Options:')}
|
|
|
21
21
|
--version, -v Show version number
|
|
22
22
|
--owner <name> GitHub repo owner (default: ${DEFAULT_OWNER})
|
|
23
23
|
--branch <name> GitHub repo branch (default: ${DEFAULT_BRANCH})
|
|
24
|
+
--force Overwrite existing files when downloading
|
|
24
25
|
|
|
25
26
|
${logger.bold('Environment:')}
|
|
26
27
|
GITHUB_TOKEN Optional GitHub token to avoid rate limiting
|
|
27
28
|
`;
|
|
28
29
|
function getFlag(args, name) {
|
|
29
30
|
const index = args.indexOf(name);
|
|
30
|
-
if (index === -1
|
|
31
|
+
if (index === -1)
|
|
31
32
|
return undefined;
|
|
32
|
-
|
|
33
|
+
const value = args[index + 1];
|
|
34
|
+
if (index + 1 >= args.length || !value || value.startsWith('--')) {
|
|
35
|
+
logger.error(`Missing value for option "${name}".`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
33
39
|
}
|
|
34
40
|
function stripFlags(args) {
|
|
35
41
|
const result = [];
|
|
36
42
|
for (let i = 0; i < args.length; i++) {
|
|
37
|
-
if (args[i] === '--
|
|
43
|
+
if (args[i] === '--force') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
else if (args[i] === '--owner' || args[i] === '--branch') {
|
|
38
47
|
i++; // skip the value
|
|
39
48
|
}
|
|
40
49
|
else {
|
|
@@ -46,16 +55,17 @@ function stripFlags(args) {
|
|
|
46
55
|
async function main() {
|
|
47
56
|
const rawArgs = process.argv.slice(2);
|
|
48
57
|
if (rawArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
49
|
-
|
|
58
|
+
logger.log(HELP);
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
52
61
|
if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
|
|
53
|
-
|
|
62
|
+
logger.log(VERSION);
|
|
54
63
|
return;
|
|
55
64
|
}
|
|
56
65
|
const options = {
|
|
57
66
|
owner: getFlag(rawArgs, '--owner') ?? DEFAULT_OWNER,
|
|
58
67
|
branch: getFlag(rawArgs, '--branch') ?? DEFAULT_BRANCH,
|
|
68
|
+
force: rawArgs.includes('--force'),
|
|
59
69
|
};
|
|
60
70
|
const args = stripFlags(rawArgs);
|
|
61
71
|
const command = args[0];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { listDemoFolders } from '../lib/github.js';
|
|
4
4
|
import { sparseCheckout } from '../lib/git.js';
|
|
@@ -9,11 +9,11 @@ export async function download(folder, target, options) {
|
|
|
9
9
|
if (!folders.includes(folder)) {
|
|
10
10
|
logger.error(`Example "${folder}" not found.`);
|
|
11
11
|
if (folders.length > 0) {
|
|
12
|
-
|
|
12
|
+
logger.log(`\n${logger.bold('Available examples:')}\n`);
|
|
13
13
|
for (const f of folders) {
|
|
14
|
-
|
|
14
|
+
logger.log(` - ${f}`);
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
logger.log('');
|
|
17
17
|
}
|
|
18
18
|
else {
|
|
19
19
|
logger.warn('No examples are available yet.');
|
|
@@ -22,11 +22,18 @@ export async function download(folder, target, options) {
|
|
|
22
22
|
}
|
|
23
23
|
const dirName = target ?? folder.replace(/\//g, '-');
|
|
24
24
|
const dest = resolve(process.cwd(), dirName);
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
if (existsSync(dest)) {
|
|
26
|
+
if (dirName !== '.' && !options.force) {
|
|
27
|
+
logger.error(`Directory "${dirName}" already exists. Use --force to overwrite.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
if (dirName === '.' && readdirSync(dest).length > 0 && !options.force) {
|
|
31
|
+
logger.error('Current directory is not empty. Use --force to overwrite existing files.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
logger.info(`Downloading "${folder}"...`);
|
|
30
36
|
await sparseCheckout(folder, dest, options);
|
|
31
|
-
|
|
37
|
+
const displayPath = dirName === '.' ? 'current directory' : `./${dirName}`;
|
|
38
|
+
logger.success(`\nDone! Example downloaded to ${displayPath}`);
|
|
32
39
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -7,9 +7,9 @@ export async function list(options) {
|
|
|
7
7
|
logger.warn('No examples available yet.');
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
logger.log(logger.bold('Available examples:\n'));
|
|
11
11
|
for (const folder of folders) {
|
|
12
|
-
|
|
12
|
+
logger.log(` - ${folder}`);
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
logger.log('');
|
|
15
15
|
}
|
package/dist/lib/git.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { mkdtemp, rm, cp } from 'node:fs/promises';
|
|
2
|
+
import { mkdtemp, rm, cp, readdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
5
6
|
import { getRepoUrl } from '../constants.js';
|
|
6
7
|
function run(command, args, cwd) {
|
|
7
8
|
return new Promise((resolve, reject) => {
|
|
@@ -21,12 +22,19 @@ export async function sparseCheckout(folder, dest, options) {
|
|
|
21
22
|
await run('git', ['init', tmp]);
|
|
22
23
|
await run('git', ['remote', 'add', 'origin', repoUrl], tmp);
|
|
23
24
|
await run('git', ['sparse-checkout', 'init', '--cone'], tmp);
|
|
24
|
-
await run('git', ['sparse-checkout', 'set', folder], tmp);
|
|
25
|
-
await run('git', ['pull', 'origin',
|
|
25
|
+
await run('git', ['sparse-checkout', 'set', '--', folder], tmp);
|
|
26
|
+
await run('git', ['pull', 'origin', '--depth=1', '--', options.branch], tmp);
|
|
26
27
|
const src = join(tmp, folder);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
if (existsSync(dest)) {
|
|
29
|
+
// Copy each entry individually into the existing directory
|
|
30
|
+
const entries = await readdir(src);
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
await cp(join(src, entry), join(dest, entry), { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
await cp(src, dest, { recursive: true });
|
|
37
|
+
}
|
|
30
38
|
}
|
|
31
39
|
finally {
|
|
32
40
|
await rm(tmp, { recursive: true, force: true });
|
package/dist/lib/github.js
CHANGED
|
@@ -12,11 +12,11 @@ function buildHeaders() {
|
|
|
12
12
|
}
|
|
13
13
|
export async function listDemoFolders(options) {
|
|
14
14
|
const apiUrl = getGitHubApiUrl(options);
|
|
15
|
-
const url = `${apiUrl}/git/trees/${options.branch}?recursive=1`;
|
|
15
|
+
const url = `${apiUrl}/git/trees/${encodeURIComponent(options.branch)}?recursive=1`;
|
|
16
16
|
const response = await fetch(url, { headers: buildHeaders() });
|
|
17
17
|
if (!response.ok) {
|
|
18
18
|
if (response.status === 404) {
|
|
19
|
-
|
|
19
|
+
throw new Error('Repository or branch not found. Please verify the --owner and --branch options.');
|
|
20
20
|
}
|
|
21
21
|
throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`);
|
|
22
22
|
}
|
|
@@ -26,7 +26,9 @@ export async function listDemoFolders(options) {
|
|
|
26
26
|
}
|
|
27
27
|
// Return only 2-level deep directories (e.g. "scada/js", "kitchen-sink/ts")
|
|
28
28
|
return data.tree
|
|
29
|
-
|
|
29
|
+
// Only include items that are directories (type 'tree'),
|
|
30
|
+
// are exactly 2 levels deep, and don't start with a dot (to exclude hidden folders)
|
|
31
|
+
.filter((item) => item.type === 'tree' && item.path.split('/').length === 2 && !item.path.startsWith('.'))
|
|
30
32
|
.map((item) => item.path)
|
|
31
33
|
.sort();
|
|
32
34
|
}
|
package/dist/lib/logger.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
const RESET = '\x1b[0m';
|
|
2
3
|
const RED = '\x1b[31m';
|
|
3
4
|
const GREEN = '\x1b[32m';
|
|
@@ -11,11 +12,14 @@ export function success(msg) {
|
|
|
11
12
|
console.log(`${GREEN}${msg}${RESET}`);
|
|
12
13
|
}
|
|
13
14
|
export function warn(msg) {
|
|
14
|
-
console.
|
|
15
|
+
console.warn(`${YELLOW}${msg}${RESET}`);
|
|
15
16
|
}
|
|
16
17
|
export function error(msg) {
|
|
17
18
|
console.error(`${RED}${msg}${RESET}`);
|
|
18
19
|
}
|
|
20
|
+
export function log(msg) {
|
|
21
|
+
console.log(msg);
|
|
22
|
+
}
|
|
19
23
|
export function bold(msg) {
|
|
20
24
|
return `${BOLD}${msg}${RESET}`;
|
|
21
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joint/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Command-line tool for JointJS.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,10 +12,18 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc",
|
|
15
|
+
"dist": "yarn build",
|
|
16
|
+
"test": "tsx --test src/__tests__/*.test.ts",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"lint-fix": "eslint . --fix",
|
|
19
|
+
"prepack": "yarn build",
|
|
15
20
|
"prepublishOnly": "echo \"Publishing via NPM is not allowed!\" && exit 1"
|
|
16
21
|
},
|
|
17
22
|
"devDependencies": {
|
|
23
|
+
"@joint/eslint-config": "4.2.3",
|
|
18
24
|
"@types/node": "^22.0.0",
|
|
25
|
+
"eslint": "9.39.2",
|
|
26
|
+
"tsx": "^4.21.0",
|
|
19
27
|
"typescript": "^5.8.0"
|
|
20
28
|
},
|
|
21
29
|
"publishConfig": {
|