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 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
+ [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
+ [![NPM Version](https://img.shields.io/npm/v/@zokugun/repo-starter-kit.svg?colorB=green)](https://www.npmjs.com/package/@zokugun/repo-starter-kit)
6
+ [![Donation](https://img.shields.io/badge/donate-ko--fi-green)](https://ko-fi.com/daiyam)
7
+ [![Donation](https://img.shields.io/badge/donate-liberapay-green)](https://liberapay.com/daiyam/donate)
8
+ [![Donation](https://img.shields.io/badge/donate-paypal-green)](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 &copy; 2025-present Baptiste Augrain
112
+
113
+ Licensed under the [MIT license](https://opensource.org/licenses/MIT).
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import '../lib/cli.js';
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,5 @@
1
+ import { type Result } from '@zokugun/xtry';
2
+ export declare function loadPackageConfig(packageName: string): Promise<Result<{
3
+ labelsPath?: string;
4
+ issuePath?: string;
5
+ }, string>>;
@@ -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,3 @@
1
+ import { type Result } from '@zokugun/xtry';
2
+ import { type Issue } from '../types.js';
3
+ export declare function loadIssue(filename: string): Promise<Result<Issue, string>>;
@@ -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,3 @@
1
+ import { type Result } from '@zokugun/xtry';
2
+ import { type Label } from '../types.js';
3
+ export declare function loadLabels(filename: string): Promise<Result<Label[], string>>;
@@ -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,4 @@
1
+ import { Failure } from '@zokugun/xtry';
2
+ import { type RepoReference } from '../types.js';
3
+ import { Octokit } from '@octokit/rest';
4
+ export declare function repoExists(octokit: Octokit, repo: RepoReference): Promise<Failure<string> | undefined>;
@@ -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,4 @@
1
+ import { type Octokit } from '@octokit/rest';
2
+ import { type Result } from '@zokugun/xtry';
3
+ import { type RepoReference } from '../types.js';
4
+ export declare function existsRepo(octokit: Octokit, { owner, repo }: RepoReference): Promise<Result<boolean, string>>;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { type Result } from '@zokugun/xtry';
2
+ import { type RepoReference } from '../types.js';
3
+ export declare function parseRepo(input: string): Result<RepoReference, string>;
@@ -0,0 +1,8 @@
1
+ import { err, ok } from '@zokugun/xtry';
2
+ export function parseRepo(input) {
3
+ const [owner, repo] = input.split('/');
4
+ if (!owner || !repo) {
5
+ return err('Repository must use OWNER/NAME format.');
6
+ }
7
+ return ok({ owner, repo });
8
+ }
package/lib/run.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { type CliOptions } from './types.js';
2
+ export declare function run(options: CliOptions): Promise<void>;
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,6 @@
1
+ export function isNodeError(error) {
2
+ if (!error || typeof error !== 'object') {
3
+ return false;
4
+ }
5
+ return 'code' in error;
6
+ }
@@ -0,0 +1 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,3 @@
1
+ export function isRecord(value) {
2
+ return Boolean(value) && !Array.isArray(value) && typeof value === 'object';
3
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { type Result } from '@zokugun/xtry';
2
+ import { type RepoReference } from '../types.js';
3
+ export declare function parseRepo(input: string): Result<RepoReference, string>;
@@ -0,0 +1,8 @@
1
+ import { err, ok } from '@zokugun/xtry';
2
+ export function parseRepo(input) {
3
+ const [owner, repo] = input.split('/');
4
+ if (!owner || !repo) {
5
+ return err('Repository must use OWNER/NAME format.');
6
+ }
7
+ return ok({ owner, repo });
8
+ }
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
+ }