repo-starter-kit 0.1.1
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/LICENSE +22 -0
- package/README.md +113 -0
- package/bin/repo-starter-kit +3 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +14 -0
- package/lib/config/load-package-config.d.ts +5 -0
- package/lib/config/load-package-config.js +130 -0
- package/lib/issues/create-issue.d.ts +4 -0
- package/lib/issues/create-issue.js +21 -0
- package/lib/issues/load-issue.d.ts +3 -0
- package/lib/issues/load-issue.js +18 -0
- package/lib/labels/load-labels.d.ts +3 -0
- package/lib/labels/load-labels.js +30 -0
- package/lib/labels/sync-labels.d.ts +4 -0
- package/lib/labels/sync-labels.js +71 -0
- package/lib/repos/check-repo.d.ts +4 -0
- package/lib/repos/check-repo.js +13 -0
- package/lib/repos/exists-repo.d.ts +4 -0
- package/lib/repos/exists-repo.js +14 -0
- package/lib/repos/parse-repo.d.ts +3 -0
- package/lib/repos/parse-repo.js +8 -0
- package/lib/run.d.ts +2 -0
- package/lib/run.js +100 -0
- package/lib/types.d.ts +21 -0
- package/lib/types.js +1 -0
- package/lib/utils/is-node-error.d.ts +1 -0
- package/lib/utils/is-node-error.js +6 -0
- package/lib/utils/is-record.d.ts +1 -0
- package/lib/utils/is-record.js +3 -0
- package/lib/utils/logger.d.ts +6 -0
- package/lib/utils/logger.js +27 -0
- package/lib/utils/parse-repo.d.ts +3 -0
- package/lib/utils/parse-repo.js +8 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2025-present Baptiste Augrain
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
|
4
|
+
obtaining a copy of this software and associated documentation
|
|
5
|
+
files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use,
|
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the
|
|
9
|
+
Software is furnished to do so, subject to the following
|
|
10
|
+
conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be
|
|
13
|
+
included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
[@zokugun/repo-starter-kit](https://github.com/zokugun/repo-starter-kit)
|
|
2
|
+
========================================================================
|
|
3
|
+
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/@zokugun/repo-starter-kit)
|
|
6
|
+
[](https://ko-fi.com/daiyam)
|
|
7
|
+
[](https://liberapay.com/daiyam/donate)
|
|
8
|
+
[](https://paypal.me/daiyam99)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Command-line helper to bootstrap and sync repository settings (labels, issues and basic config) from a reusable package or local files.
|
|
12
|
+
|
|
13
|
+
Why use this tool?
|
|
14
|
+
|
|
15
|
+
- Reuse a central `repo-starter-kit` configuration published as an npm package or provide local files to keep repositories consistent.
|
|
16
|
+
- Manage issue templates and labels programmatically.
|
|
17
|
+
- Lightweight CLI with interactive prompts when needed.
|
|
18
|
+
|
|
19
|
+
Quick start
|
|
20
|
+
-----------
|
|
21
|
+
|
|
22
|
+
Install globally with npm:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g @zokugun/repo-starter-kit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or run directly with npx:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @zokugun/repo-starter-kit --repo owner/name --package @your/package
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Usage
|
|
35
|
+
-----
|
|
36
|
+
|
|
37
|
+
Basic example:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
repo-starter-kit --repo daiyam/temp --package @daiyam/default --keep-labels
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Options
|
|
44
|
+
-------
|
|
45
|
+
|
|
46
|
+
- `-r, --repo <owner/name>`: Target repository (OWNER/NAME). Required.
|
|
47
|
+
- `-l, --labels <path>`: Path to a labels YAML file to apply to the repository.
|
|
48
|
+
- `-i, --issue <path>`: Path to a Markdown file used as an issue template.
|
|
49
|
+
- `-p, --package <name>`: An npm package that includes a `repo-starter-kit` configuration file to apply.
|
|
50
|
+
- `-k, --keep-labels`: Do not delete labels missing from the provided configuration (defaults to false).
|
|
51
|
+
- `-v, --version`: Show version number.
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
|
|
56
|
+
- Apply a published starter package to a repository:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
repo-starter-kit -r myuser/myrepo -p @myorg/myconfig
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- Apply local labels file and an issue template (do not remove existing labels):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
repo-starter-kit -r myuser/myrepo -l labels.yml -i issue.md -k
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Configuration package
|
|
69
|
+
---------------------
|
|
70
|
+
|
|
71
|
+
The configuration package need to be prefixed:
|
|
72
|
+
- `--package @daiyam/default` will load the package `@daiyam/repo-starter-kit-default`
|
|
73
|
+
|
|
74
|
+
At its root, it needs to have one of the following file:
|
|
75
|
+
- `repo-starter-kit.yml`
|
|
76
|
+
- `repo-starter-kit.yaml`
|
|
77
|
+
- `repo-starter-kit.json`
|
|
78
|
+
|
|
79
|
+
With its content as:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
labels: <path to labels file>
|
|
83
|
+
issue: <path to issue file>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For reference, please check https://github.com/daiyam/repo-starter-kit-default.
|
|
87
|
+
|
|
88
|
+
Donations
|
|
89
|
+
---------
|
|
90
|
+
|
|
91
|
+
Support this project by becoming a financial contributor.
|
|
92
|
+
|
|
93
|
+
<table>
|
|
94
|
+
<tr>
|
|
95
|
+
<td><img src="https://raw.githubusercontent.com/daiyam/assets/master/icons/256/funding_kofi.png" alt="Ko-fi" width="80px" height="80px"></td>
|
|
96
|
+
<td><a href="https://ko-fi.com/daiyam" target="_blank">ko-fi.com/daiyam</a></td>
|
|
97
|
+
</tr>
|
|
98
|
+
<tr>
|
|
99
|
+
<td><img src="https://raw.githubusercontent.com/daiyam/assets/master/icons/256/funding_liberapay.png" alt="Liberapay" width="80px" height="80px"></td>
|
|
100
|
+
<td><a href="https://liberapay.com/daiyam/donate" target="_blank">liberapay.com/daiyam/donate</a></td>
|
|
101
|
+
</tr>
|
|
102
|
+
<tr>
|
|
103
|
+
<td><img src="https://raw.githubusercontent.com/daiyam/assets/master/icons/256/funding_paypal.png" alt="PayPal" width="80px" height="80px"></td>
|
|
104
|
+
<td><a href="https://paypal.me/daiyam99" target="_blank">paypal.me/daiyam99</a></td>
|
|
105
|
+
</tr>
|
|
106
|
+
</table>
|
|
107
|
+
|
|
108
|
+
License
|
|
109
|
+
-------
|
|
110
|
+
|
|
111
|
+
Copyright © 2025-present Baptiste Augrain
|
|
112
|
+
|
|
113
|
+
Licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
3
|
+
import { run } from './run.js';
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.version(pkg.version, '-v, --version')
|
|
7
|
+
.description(pkg.description)
|
|
8
|
+
.requiredOption('-r, --repo <owner/name>', 'Target repository (OWNER/NAME)')
|
|
9
|
+
.option('-l, --labels <path>', 'Path to labels YAML')
|
|
10
|
+
.option('-i, --issue <path>', 'Path to issue Markdown template')
|
|
11
|
+
.option('-p, --package <name>', 'NPM package that includes a repo-starter-kit config file')
|
|
12
|
+
.option('-k, --keep-labels', 'Do not delete labels missing from the configuration', false)
|
|
13
|
+
.action(run);
|
|
14
|
+
program.parse();
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { err, ok, stringifyError, xtry } from '@zokugun/xtry';
|
|
5
|
+
import pacote from 'pacote';
|
|
6
|
+
import { temporaryDirectory } from 'tempy';
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
import { isNodeError } from '../utils/is-node-error.js';
|
|
9
|
+
import { isRecord } from '../utils/is-record.js';
|
|
10
|
+
const CONFIG_FILES = [
|
|
11
|
+
{
|
|
12
|
+
name: 'repo-starter-kit.yml',
|
|
13
|
+
type: 'yaml',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'repo-starter-kit.yaml',
|
|
17
|
+
type: 'yaml',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'repo-starter-kit.json',
|
|
21
|
+
type: 'json',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'repo-starter-kit',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
28
|
+
export async function loadPackageConfig(packageName) {
|
|
29
|
+
const normalizedName = normalizePackageName(packageName);
|
|
30
|
+
if (normalizedName.fails) {
|
|
31
|
+
return normalizedName;
|
|
32
|
+
}
|
|
33
|
+
const packageRoot = await downloadPackage(normalizedName.value);
|
|
34
|
+
if (packageRoot.fails) {
|
|
35
|
+
return packageRoot;
|
|
36
|
+
}
|
|
37
|
+
const fileConfig = await readConfigFile(packageRoot.value, packageName);
|
|
38
|
+
if (fileConfig.fails) {
|
|
39
|
+
return fileConfig;
|
|
40
|
+
}
|
|
41
|
+
const labelsPath = typeof fileConfig.value.labels === 'string' ? path.resolve(packageRoot.value, fileConfig.value.labels) : undefined;
|
|
42
|
+
const issuePath = typeof fileConfig.value.issue === 'string' ? path.resolve(packageRoot.value, fileConfig.value.issue) : undefined;
|
|
43
|
+
return ok({ labelsPath, issuePath });
|
|
44
|
+
}
|
|
45
|
+
function normalizePackageName(input) {
|
|
46
|
+
if (!input) {
|
|
47
|
+
return err('Package name cannot be empty.');
|
|
48
|
+
}
|
|
49
|
+
if (input.startsWith('@')) {
|
|
50
|
+
const slashIndex = input.indexOf('/');
|
|
51
|
+
if (slashIndex === -1) {
|
|
52
|
+
return err(`Scoped package '${input}' must include a name after '/'.`);
|
|
53
|
+
}
|
|
54
|
+
const scope = input.slice(0, slashIndex);
|
|
55
|
+
const name = input.slice(slashIndex + 1);
|
|
56
|
+
if (name.length === 0) {
|
|
57
|
+
return err(`Scoped package '${input}' is missing the package name.`);
|
|
58
|
+
}
|
|
59
|
+
if (name.startsWith('repo-starter-kit-')) {
|
|
60
|
+
return ok(`${scope}/${name}`);
|
|
61
|
+
}
|
|
62
|
+
return ok(`${scope}/repo-starter-kit-${name}`);
|
|
63
|
+
}
|
|
64
|
+
if (input.startsWith('repo-starter-kit-')) {
|
|
65
|
+
return ok(input);
|
|
66
|
+
}
|
|
67
|
+
return ok(`repo-starter-kit-${input}`);
|
|
68
|
+
}
|
|
69
|
+
async function downloadPackage(packageName) {
|
|
70
|
+
const registry = resolveRegistry();
|
|
71
|
+
const dir = temporaryDirectory();
|
|
72
|
+
const result = await pacote.extract(packageName, dir, { registry });
|
|
73
|
+
if (!result.resolved) {
|
|
74
|
+
return err(result.from);
|
|
75
|
+
}
|
|
76
|
+
return ok(dir);
|
|
77
|
+
}
|
|
78
|
+
function resolveRegistry() {
|
|
79
|
+
const registry = process.env.npm_config_registry;
|
|
80
|
+
if (typeof registry === 'string' && registry.trim().length > 0) {
|
|
81
|
+
return registry;
|
|
82
|
+
}
|
|
83
|
+
return DEFAULT_REGISTRY;
|
|
84
|
+
}
|
|
85
|
+
async function readConfigFile(packageRoot, packageName) {
|
|
86
|
+
for (const { name, type } of CONFIG_FILES) {
|
|
87
|
+
try {
|
|
88
|
+
const content = await readFile(path.join(packageRoot, name), 'utf8');
|
|
89
|
+
let data;
|
|
90
|
+
if (type === 'json') {
|
|
91
|
+
const result = xtry(() => JSON.parse(content), stringifyError);
|
|
92
|
+
if (result.fails) {
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
data = result.value;
|
|
96
|
+
}
|
|
97
|
+
else if (type === 'yaml') {
|
|
98
|
+
const result = xtry(() => YAML.parse(content), stringifyError);
|
|
99
|
+
if (result.fails) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
data = result.value;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
let result = xtry(() => JSON.parse(content), stringifyError);
|
|
106
|
+
if (result.fails) {
|
|
107
|
+
result = xtry(() => YAML.parse(content), stringifyError);
|
|
108
|
+
if (result.fails) {
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
data = result.value;
|
|
113
|
+
}
|
|
114
|
+
if (!isRecord(data)) {
|
|
115
|
+
return err(`Config file ${name} must export an object.`);
|
|
116
|
+
}
|
|
117
|
+
return ok({
|
|
118
|
+
labels: typeof data.labels === 'string' ? data.labels : undefined,
|
|
119
|
+
issue: typeof data.issue === 'string' ? data.issue : undefined,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
return err(`Failed to read ${name} from package: ${stringifyError(error)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return err(`Package ${packageName} must include one of ${CONFIG_FILES.map(({ name }) => name).join(', ')} at its root.`);
|
|
130
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Octokit } from '@octokit/rest';
|
|
2
|
+
import { type Failure } from '@zokugun/xtry';
|
|
3
|
+
import { type Issue, type RepoReference } from '../types.js';
|
|
4
|
+
export declare function createIssue(octokit: Octokit, repo: RepoReference, { title, body, labels }: Issue): Promise<Failure<string> | undefined>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { err, stringifyError } from '@zokugun/xtry';
|
|
2
|
+
import * as logger from '../utils/logger.js';
|
|
3
|
+
export async function createIssue(octokit, repo, { title, body, labels }) {
|
|
4
|
+
try {
|
|
5
|
+
const response = await octokit.rest.issues.create({
|
|
6
|
+
...repo,
|
|
7
|
+
title,
|
|
8
|
+
body,
|
|
9
|
+
labels,
|
|
10
|
+
});
|
|
11
|
+
const issueNumber = response.data.number;
|
|
12
|
+
await octokit.rest.issues.lock({
|
|
13
|
+
...repo,
|
|
14
|
+
issue_number: issueNumber,
|
|
15
|
+
});
|
|
16
|
+
logger.log(`Created issue '${title}'.`);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return err(stringifyError(error));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { err, ok, stringifyError } from '@zokugun/xtry';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
export async function loadIssue(filename) {
|
|
5
|
+
let content;
|
|
6
|
+
try {
|
|
7
|
+
content = await readFile(filename, 'utf8');
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
return err(`Failed to read ${filename} from package: ${stringifyError(error)}`);
|
|
11
|
+
}
|
|
12
|
+
const parsed = matter(content);
|
|
13
|
+
const title = typeof parsed.data.title === 'string' ? parsed.data.title.trim() : '';
|
|
14
|
+
const rawLabels = Array.isArray(parsed.data.labels) ? parsed.data.labels : [];
|
|
15
|
+
const labels = rawLabels.map((label) => String(label).trim()).filter((label) => label.length > 0);
|
|
16
|
+
const body = parsed.content;
|
|
17
|
+
return ok({ title, body, labels });
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { err, ok, stringifyError } from '@zokugun/xtry';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { isRecord } from '../utils/is-record.js';
|
|
5
|
+
export async function loadLabels(filename) {
|
|
6
|
+
let content;
|
|
7
|
+
try {
|
|
8
|
+
content = await readFile(filename, 'utf8');
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return err(`Failed to read ${filename} from package: ${stringifyError(error)}`);
|
|
12
|
+
}
|
|
13
|
+
const records = YAML.parse(content);
|
|
14
|
+
if (!Array.isArray(records)) {
|
|
15
|
+
return err(`Label file ${filename} must contain an array.`);
|
|
16
|
+
}
|
|
17
|
+
const labels = [];
|
|
18
|
+
for (const record of records) {
|
|
19
|
+
if (!isRecord(record)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const name = String(record.name ?? '').trim();
|
|
23
|
+
const color = String(record.color ?? '').trim();
|
|
24
|
+
const description = String(record.description ?? '').trim();
|
|
25
|
+
if (name.length > 0) {
|
|
26
|
+
labels.push({ name, color, description });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return ok(labels);
|
|
30
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Octokit } from '@octokit/rest';
|
|
2
|
+
import { type Failure } from '@zokugun/xtry';
|
|
3
|
+
import { type Label, type RepoReference } from '../types.js';
|
|
4
|
+
export declare function syncLabels(octokit: Octokit, repo: RepoReference, labels: Label[], keepExisting?: boolean): Promise<Failure<string> | undefined>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { err, stringifyError, xatry } from '@zokugun/xtry';
|
|
2
|
+
import { isRecord } from '../utils/is-record.js';
|
|
3
|
+
import * as logger from '../utils/logger.js';
|
|
4
|
+
export async function syncLabels(octokit, repo, labels, keepExisting = false) {
|
|
5
|
+
if (labels.length === 0) {
|
|
6
|
+
logger.warn('No labels defined; skipping label sync.');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const desiredNames = new Set();
|
|
10
|
+
for (const label of labels) {
|
|
11
|
+
const color = label.color.replace(/^#/, '').toLowerCase();
|
|
12
|
+
if (!color) {
|
|
13
|
+
logger.warn(`Skipping label '${label.name}' because it lacks a color.`);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (label.description && label.description.length > 100) {
|
|
17
|
+
logger.warn(`Skipping label '${label.name}' because its description is too long (100 max).`);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
desiredNames.add(label.name);
|
|
21
|
+
try {
|
|
22
|
+
await octokit.rest.issues.createLabel({
|
|
23
|
+
...repo,
|
|
24
|
+
name: label.name,
|
|
25
|
+
color,
|
|
26
|
+
description: label.description,
|
|
27
|
+
});
|
|
28
|
+
logger.log(`Created label: ${label.name}`);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (isRecord(error) && 'status' in error && error.status === 422) {
|
|
32
|
+
const result = await xatry(octokit.rest.issues.updateLabel({
|
|
33
|
+
...repo,
|
|
34
|
+
name: label.name,
|
|
35
|
+
new_name: label.name,
|
|
36
|
+
color,
|
|
37
|
+
description: label.description,
|
|
38
|
+
}), stringifyError);
|
|
39
|
+
if (result.fails) {
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
logger.log(`Updated label: ${label.name}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
return err(stringifyError(error));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (keepExisting) {
|
|
50
|
+
logger.log('Keeping existing labels that are not in the configuration.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await deleteMissingLabels(octokit, repo, desiredNames);
|
|
54
|
+
}
|
|
55
|
+
async function deleteMissingLabels(octokit, repo, desiredNames) {
|
|
56
|
+
const existingLabels = await octokit.paginate(octokit.rest.issues.listLabelsForRepo, {
|
|
57
|
+
...repo,
|
|
58
|
+
per_page: 100,
|
|
59
|
+
});
|
|
60
|
+
for (const existing of existingLabels) {
|
|
61
|
+
if (!desiredNames.has(existing.name)) {
|
|
62
|
+
try {
|
|
63
|
+
await octokit.rest.issues.deleteLabel({ ...repo, name: existing.name });
|
|
64
|
+
logger.log(`Deleted label: ${existing.name}`);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
logger.warn(`Failed to delete label '${existing.name}': ${stringifyError(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function repoExists(octokit, repo) {
|
|
2
|
+
try {
|
|
3
|
+
await octokit.repos.get({ owner, repo });
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
catch (err) {
|
|
7
|
+
// Octokit surfaces HTTP status on the error object
|
|
8
|
+
if (err.status === 404)
|
|
9
|
+
return false;
|
|
10
|
+
// 403 may indicate rate limit or insufficient permission; rethrow in that case
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { err, ok, stringifyError } from '@zokugun/xtry';
|
|
2
|
+
import { isRecord } from '../utils/is-record.js';
|
|
3
|
+
export async function existsRepo(octokit, { owner, repo }) {
|
|
4
|
+
try {
|
|
5
|
+
await octokit.repos.get({ owner, repo });
|
|
6
|
+
return ok(true);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (isRecord(error) && 'status' in error && error.status === 404) {
|
|
10
|
+
return ok(false);
|
|
11
|
+
}
|
|
12
|
+
return err(stringifyError(error));
|
|
13
|
+
}
|
|
14
|
+
}
|
package/lib/run.d.ts
ADDED
package/lib/run.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device';
|
|
3
|
+
import { Octokit } from '@octokit/rest';
|
|
4
|
+
import clipboardy from 'clipboardy';
|
|
5
|
+
import enquirer from 'enquirer';
|
|
6
|
+
import open from 'open';
|
|
7
|
+
import { loadPackageConfig } from './config/load-package-config.js';
|
|
8
|
+
import { createIssue } from './issues/create-issue.js';
|
|
9
|
+
import { loadIssue } from './issues/load-issue.js';
|
|
10
|
+
import { loadLabels } from './labels/load-labels.js';
|
|
11
|
+
import { syncLabels } from './labels/sync-labels.js';
|
|
12
|
+
import { existsRepo } from './repos/exists-repo.js';
|
|
13
|
+
import { parseRepo } from './repos/parse-repo.js';
|
|
14
|
+
import * as logger from './utils/logger.js';
|
|
15
|
+
export async function run(options) {
|
|
16
|
+
const start = Date.now();
|
|
17
|
+
logger.progress('Configuring');
|
|
18
|
+
const repo = parseRepo(options.repo);
|
|
19
|
+
if (repo.fails) {
|
|
20
|
+
return logger.error(repo.error);
|
|
21
|
+
}
|
|
22
|
+
let labelsPath = options.labels ? path.resolve(options.labels) : undefined;
|
|
23
|
+
let issuePath = options.issue ? path.resolve(options.issue) : undefined;
|
|
24
|
+
logger.progress('Loading');
|
|
25
|
+
if (options.package) {
|
|
26
|
+
const packagePaths = await loadPackageConfig(options.package);
|
|
27
|
+
if (packagePaths.fails) {
|
|
28
|
+
return logger.error(packagePaths.error);
|
|
29
|
+
}
|
|
30
|
+
labelsPath ??= packagePaths.value.labelsPath;
|
|
31
|
+
issuePath ??= packagePaths.value.issuePath;
|
|
32
|
+
}
|
|
33
|
+
let labels;
|
|
34
|
+
let issue;
|
|
35
|
+
if (labelsPath) {
|
|
36
|
+
const result = await loadLabels(labelsPath);
|
|
37
|
+
if (result.fails) {
|
|
38
|
+
return logger.error(result.error);
|
|
39
|
+
}
|
|
40
|
+
labels = result.value;
|
|
41
|
+
}
|
|
42
|
+
if (issuePath) {
|
|
43
|
+
const result = await loadIssue(issuePath);
|
|
44
|
+
if (result.fails) {
|
|
45
|
+
return logger.error(result.error);
|
|
46
|
+
}
|
|
47
|
+
issue = result.value;
|
|
48
|
+
}
|
|
49
|
+
if (labels ?? issue) {
|
|
50
|
+
const octokit = new Octokit({
|
|
51
|
+
authStrategy: createOAuthDeviceAuth,
|
|
52
|
+
auth: {
|
|
53
|
+
clientId: 'Ov23lilx93wDQB9QOLFW',
|
|
54
|
+
clientType: 'oauth-app',
|
|
55
|
+
scopes: ['repo'],
|
|
56
|
+
async onVerification({ verification_uri, user_code }) {
|
|
57
|
+
logger.log('Authenticate your account at:');
|
|
58
|
+
logger.log(verification_uri);
|
|
59
|
+
logger.log('Press ENTER to open in the browser...');
|
|
60
|
+
await enquirer.prompt({
|
|
61
|
+
type: 'invisible',
|
|
62
|
+
name: 'open',
|
|
63
|
+
message: '',
|
|
64
|
+
});
|
|
65
|
+
await open(verification_uri);
|
|
66
|
+
await clipboardy.write(user_code);
|
|
67
|
+
logger.log(`Paste code: ${user_code} (copied to your clipboard)`);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
userAgent: 'repo-starter-kit',
|
|
71
|
+
});
|
|
72
|
+
const exists = await existsRepo(octokit, repo.value);
|
|
73
|
+
if (exists.fails) {
|
|
74
|
+
return logger.error(exists.error);
|
|
75
|
+
}
|
|
76
|
+
else if (!exists.value) {
|
|
77
|
+
return logger.error(`The repository ${repo.value.owner}/${repo.value.repo} can't be found!`);
|
|
78
|
+
}
|
|
79
|
+
if (labels) {
|
|
80
|
+
logger.progress('Syncing labels');
|
|
81
|
+
const result = await syncLabels(octokit, repo.value, labels, options.keepLabels);
|
|
82
|
+
if (result) {
|
|
83
|
+
return logger.error(result.error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (issue) {
|
|
87
|
+
logger.progress('Creating issue');
|
|
88
|
+
const result = await createIssue(octokit, repo.value, issue);
|
|
89
|
+
if (result) {
|
|
90
|
+
return logger.error(result.error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
logger.log(`Repository bootstrap completed for ${repo.value.owner}/${repo.value.repo}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
logger.log('Nothing to do!');
|
|
97
|
+
}
|
|
98
|
+
const duration = Math.ceil((Date.now() - start) / 1000);
|
|
99
|
+
logger.finish(duration);
|
|
100
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type CliOptions = {
|
|
2
|
+
repo: string;
|
|
3
|
+
labels?: string;
|
|
4
|
+
issue?: string;
|
|
5
|
+
package?: string;
|
|
6
|
+
keepLabels: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type RepoReference = {
|
|
9
|
+
owner: string;
|
|
10
|
+
repo: string;
|
|
11
|
+
};
|
|
12
|
+
export type Label = {
|
|
13
|
+
name: string;
|
|
14
|
+
color: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
};
|
|
17
|
+
export type Issue = {
|
|
18
|
+
title: string;
|
|
19
|
+
body: string;
|
|
20
|
+
labels: string[];
|
|
21
|
+
};
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isNodeError(error: unknown): error is NodeJS.ErrnoException;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type IndicatorLoading = ReturnType<typeof setInterval>;
|
|
2
|
+
export declare function progress(label: string): void;
|
|
3
|
+
export declare function finish(duration: number): void;
|
|
4
|
+
export declare function error(message: string): void;
|
|
5
|
+
export declare function log(message: string): void;
|
|
6
|
+
export declare function warn(message: string): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import c from 'ansi-colors';
|
|
2
|
+
import cliSpinners from 'cli-spinners';
|
|
3
|
+
import logUpdate from 'log-update';
|
|
4
|
+
const { dots } = cliSpinners;
|
|
5
|
+
let $loading;
|
|
6
|
+
export function progress(label) {
|
|
7
|
+
clearInterval($loading);
|
|
8
|
+
let index = 0;
|
|
9
|
+
$loading = setInterval(() => {
|
|
10
|
+
logUpdate(`${c.cyan(dots.frames[index = ++index % dots.frames.length])} ${label}`);
|
|
11
|
+
}, cliSpinners.dots.interval);
|
|
12
|
+
}
|
|
13
|
+
export function finish(duration) {
|
|
14
|
+
clearInterval($loading);
|
|
15
|
+
logUpdate(`🏁 ${c.bold('Done')} (in ${duration}s).`);
|
|
16
|
+
}
|
|
17
|
+
export function error(message) {
|
|
18
|
+
clearInterval($loading);
|
|
19
|
+
logUpdate(`${c.red(c.symbols.cross)} ${c.bold('Error!')}`);
|
|
20
|
+
console.log(message);
|
|
21
|
+
}
|
|
22
|
+
export function log(message) {
|
|
23
|
+
logUpdate.persist(`${c.cyan(c.symbols.cross)} ${message}`);
|
|
24
|
+
}
|
|
25
|
+
export function warn(message) {
|
|
26
|
+
logUpdate.persist(`${c.magenta(c.symbols.warning)} ${message}`);
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "repo-starter-kit",
|
|
3
|
+
"description": "CLI to bootstrap and sync repository labels, issues and config.",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Baptiste Augrain",
|
|
7
|
+
"email": "daiyam@zokugun.org"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"homepage": "https://github.com/zokugun/repo-starter-kit",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/zokugun/repo-starter-kit.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/zokugun/repo-starter-kit/issues"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"repo-starter-kit": "bin/repo-starter-kit"
|
|
21
|
+
},
|
|
22
|
+
"main": "lib/cli.js",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"commit": "cz",
|
|
25
|
+
"compile": "tsc -p src",
|
|
26
|
+
"lint": "xo",
|
|
27
|
+
"prepare": "husky; fixpack || true",
|
|
28
|
+
"prepublishOnly": "npm run compile",
|
|
29
|
+
"release": "release-it",
|
|
30
|
+
"watch:source": "tsc-watch -p src"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@octokit/auth-oauth-device": "^8.0.3",
|
|
34
|
+
"@octokit/rest": "^22.0.1",
|
|
35
|
+
"@zokugun/xtry": "^0.3.0",
|
|
36
|
+
"ansi-colors": "^4.1.3",
|
|
37
|
+
"cli-spinners": "^3.3.0",
|
|
38
|
+
"clipboardy": "^5.0.2",
|
|
39
|
+
"commander": "^13.1.0",
|
|
40
|
+
"enquirer": "^2.4.1",
|
|
41
|
+
"gray-matter": "^4.0.3",
|
|
42
|
+
"log-update": "^7.0.2",
|
|
43
|
+
"open": "^11.0.0",
|
|
44
|
+
"pacote": "^21.0.4",
|
|
45
|
+
"tempy": "^3.1.0",
|
|
46
|
+
"yaml": "^2.8.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@commitlint/cli": "^19.7.1",
|
|
50
|
+
"@commitlint/config-conventional": "^19.7.1",
|
|
51
|
+
"@types/node": "^20.14.8",
|
|
52
|
+
"@types/pacote": "^11.1.8",
|
|
53
|
+
"commitizen": "^4.3.1",
|
|
54
|
+
"fixpack": "^4.0.0",
|
|
55
|
+
"husky": "^9.1.7",
|
|
56
|
+
"lint-staged": "^16.1.4",
|
|
57
|
+
"release-it": "^18.1.2",
|
|
58
|
+
"tsc-watch": "^6.3.0",
|
|
59
|
+
"typescript": "^5.7.3",
|
|
60
|
+
"xo": "0.60.0"
|
|
61
|
+
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"automation",
|
|
64
|
+
"cli",
|
|
65
|
+
"github",
|
|
66
|
+
"issues",
|
|
67
|
+
"labels",
|
|
68
|
+
"repo",
|
|
69
|
+
"starter"
|
|
70
|
+
]
|
|
71
|
+
}
|