@klyper/cli 0.3.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,54 @@
1
+ # @klyper/cli
2
+
3
+ Command line tools for managing the Klyper monorepo.
4
+
5
+ ## Commands
6
+
7
+ ```sh
8
+ npm run build --workspace @klyper/cli
9
+ npm run check --workspace @klyper/cli
10
+ npm run lint --workspace @klyper/cli
11
+ npm test --workspace @klyper/cli
12
+ ```
13
+
14
+ `build` uses Rollup for the package output. `check` runs TypeScript without emitting files.
15
+
16
+ ## Create a Package
17
+
18
+ Create a new package template:
19
+
20
+ ```sh
21
+ npm exec klyper create package utils
22
+ ```
23
+
24
+ Alternative syntax:
25
+
26
+ ```sh
27
+ npm exec klyper package create utils
28
+ ```
29
+
30
+ The command creates `packages/utils` with:
31
+
32
+ - `package.json`
33
+ - `tsconfig.json`
34
+ - `README.md`
35
+ - `src/index.ts`
36
+ - `src/index.test.ts`
37
+
38
+ The generated `package.json` follows the public package convention used by the
39
+ monorepo:
40
+
41
+ - `main` points to the CommonJS bundle at `./dist/index.cjs`
42
+ - `module` and `exports.import` point to the ESM bundle at `./dist/index.js`
43
+ - `types` and `exports.types` point to `./dist/index.d.ts`
44
+ - `exports.require` points to the CommonJS bundle
45
+ - `sideEffects` is `false`
46
+ - `prepublishOnly` runs `npm run build && npm test`
47
+
48
+ The root build runs each package through npm workspaces, so new packages do not need root TypeScript project references.
49
+
50
+ Use a custom scope:
51
+
52
+ ```sh
53
+ npm exec klyper create package auth --scope @klyper
54
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ /*!
3
+ * @klyper/cli v0.3.0
4
+ * Command line tools for the Klyper monorepo.
5
+ * (c) 2026 Andrew Caires
6
+ * @license: MIT
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ var node_fs = require('node:fs');
12
+ var node_url = require('node:url');
13
+ var promises = require('node:fs/promises');
14
+ var node_path = require('node:path');
15
+
16
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
17
+ const defaultScope = "@klyper";
18
+ const packageNamePattern = /^[a-z][a-z0-9-]*$/;
19
+ const scopePattern = /^@[a-z][a-z0-9-]*$/;
20
+ const helpText = `Klyper CLI
21
+
22
+ Usage:
23
+ klyper create package <name> [--scope @scope]
24
+ klyper package create <name> [--scope @scope]
25
+
26
+ Examples:
27
+ klyper create package utils
28
+ klyper create package auth --scope @klyper
29
+ `;
30
+ const runCli = async (args, io) => {
31
+ try {
32
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
33
+ io.stdout.write(helpText);
34
+ return 0;
35
+ }
36
+ const command = parseCreatePackageCommand(args);
37
+ const result = await createPackageTemplate({
38
+ cwd: io.cwd,
39
+ name: command.name,
40
+ scope: command.scope
41
+ });
42
+ io.stdout.write(`Created ${result.packageName} in packages/${result.directoryName}\n`);
43
+ return 0;
44
+ }
45
+ catch (error) {
46
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
47
+ return 1;
48
+ }
49
+ };
50
+ const createPackageTemplate = async (options) => {
51
+ const identifier = parsePackageIdentifier(options.name, options.scope ?? defaultScope);
52
+ const packageRoot = node_path.join(options.cwd, "packages", identifier.directoryName);
53
+ if (await pathExists(packageRoot)) {
54
+ throw new Error(`Package directory already exists: packages/${identifier.directoryName}`);
55
+ }
56
+ const files = buildPackageFiles(identifier);
57
+ await promises.mkdir(node_path.join(packageRoot, "src"), { recursive: true });
58
+ await Promise.all(files.map((file) => promises.writeFile(node_path.join(packageRoot, file.path), file.content)));
59
+ return {
60
+ directoryName: identifier.directoryName,
61
+ packageName: identifier.packageName,
62
+ packageRoot,
63
+ files: files.map((file) => node_path.join(packageRoot, file.path))
64
+ };
65
+ };
66
+ const parseCreatePackageCommand = (args) => {
67
+ const [first, second, third, ...rest] = args;
68
+ const isCreatePackage = first === "create" && second === "package";
69
+ const isPackageCreate = first === "package" && second === "create";
70
+ if (!isCreatePackage && !isPackageCreate) {
71
+ throw new Error("Unknown command. Run `klyper --help` for usage.");
72
+ }
73
+ if (!third) {
74
+ throw new Error("Missing package name.");
75
+ }
76
+ const scope = readScopeOption(rest);
77
+ return { name: third, scope };
78
+ };
79
+ const readScopeOption = (args) => {
80
+ if (args.length === 0) {
81
+ return defaultScope;
82
+ }
83
+ if (args.length === 2 && args[0] === "--scope") {
84
+ return args[1] ?? defaultScope;
85
+ }
86
+ throw new Error(`Unknown option: ${args.join(" ")}`);
87
+ };
88
+ const parsePackageIdentifier = (name, scope) => {
89
+ const trimmedName = name.trim();
90
+ const trimmedScope = scope.trim();
91
+ if (trimmedName.startsWith("@")) {
92
+ const [providedScope, packageName, extra] = trimmedName.split("/");
93
+ if (extra || !providedScope || !packageName) {
94
+ throw new Error("Scoped package names must use the @scope/name format.");
95
+ }
96
+ validateScope(providedScope);
97
+ validatePackageName(packageName);
98
+ return {
99
+ directoryName: packageName,
100
+ packageName: `${providedScope}/${packageName}`
101
+ };
102
+ }
103
+ validateScope(trimmedScope);
104
+ validatePackageName(trimmedName);
105
+ return {
106
+ directoryName: trimmedName,
107
+ packageName: `${trimmedScope}/${trimmedName}`
108
+ };
109
+ };
110
+ const validatePackageName = (name) => {
111
+ if (!packageNamePattern.test(name)) {
112
+ throw new Error("Package names must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.");
113
+ }
114
+ };
115
+ const validateScope = (scope) => {
116
+ if (!scopePattern.test(scope)) {
117
+ throw new Error("Package scopes must use @scope and contain only lowercase letters, numbers, and hyphens.");
118
+ }
119
+ };
120
+ const buildPackageFiles = (identifier) => {
121
+ return [
122
+ {
123
+ path: "package.json",
124
+ content: `${JSON.stringify({
125
+ name: identifier.packageName,
126
+ version: "0.0.0",
127
+ description: `Klyper ${identifier.directoryName} package.`,
128
+ author: "Andrew Caires",
129
+ license: "MIT",
130
+ repository: {
131
+ type: "git",
132
+ url: "git+https://github.com/andrewcaires/klyper.git"
133
+ },
134
+ type: "module",
135
+ main: "./dist/index.cjs",
136
+ module: "./dist/index.js",
137
+ types: "./dist/index.d.ts",
138
+ exports: {
139
+ ".": {
140
+ types: "./dist/index.d.ts",
141
+ import: "./dist/index.js",
142
+ require: "./dist/index.cjs",
143
+ default: "./dist/index.js"
144
+ }
145
+ },
146
+ sideEffects: false,
147
+ files: ["dist"],
148
+ scripts: {
149
+ build: "npm run clean && npm run check && npm run bundle",
150
+ bundle: "rollup --bundleConfigAsCjs -c ../../rollup.config.js",
151
+ check: "npm run lint && npm run typecheck",
152
+ clean: "rm -rf dist",
153
+ lint: "eslint .",
154
+ "lint:fix": "eslint . --fix",
155
+ prepublishOnly: "npm run build && npm test",
156
+ test: "node --import tsx --test \"src/**/*.test.ts\"",
157
+ "test:watch": "node --import tsx --test --watch \"src/**/*.test.ts\"",
158
+ typecheck: "tsc -p tsconfig.json --noEmit"
159
+ },
160
+ publishConfig: {
161
+ access: "public"
162
+ }
163
+ }, null, 2)}\n`
164
+ },
165
+ {
166
+ path: "README.md",
167
+ content: `# ${identifier.packageName}
168
+
169
+ Klyper ${identifier.directoryName} package.
170
+
171
+ ## Installation
172
+
173
+ \`\`\`sh
174
+ npm install ${identifier.packageName}
175
+ \`\`\`
176
+
177
+ This package is published as an ES module package and also exposes a CommonJS
178
+ entry through \`main\` and \`exports.require\`.
179
+
180
+ ## Commands
181
+
182
+ \`\`\`sh
183
+ npm run build --workspace ${identifier.packageName}
184
+ npm run check --workspace ${identifier.packageName}
185
+ npm run lint --workspace ${identifier.packageName}
186
+ npm test --workspace ${identifier.packageName}
187
+ \`\`\`
188
+
189
+ \`build\` uses Rollup for the package output. \`check\` runs TypeScript without emitting files.
190
+ \`prepublishOnly\` runs the package build and test suite before npm publish.
191
+
192
+ ## API
193
+
194
+ \`\`\`ts
195
+ import { getPackageName } from "${identifier.packageName}";
196
+
197
+ getPackageName();
198
+ \`\`\`
199
+ `
200
+ },
201
+ {
202
+ path: "tsconfig.json",
203
+ content: `${JSON.stringify({
204
+ extends: "../../tsconfig.base.json",
205
+ compilerOptions: {
206
+ outDir: "dist",
207
+ rootDir: "src"
208
+ },
209
+ include: ["src/**/*.ts"],
210
+ }, null, 2)}\n`
211
+ },
212
+ {
213
+ path: "src/index.ts",
214
+ content: `export const getPackageName = (): string => {
215
+ return "${identifier.packageName}";
216
+ };
217
+ `
218
+ },
219
+ {
220
+ path: "src/index.test.ts",
221
+ content: `import { equal } from "node:assert/strict";
222
+ import test from "node:test";
223
+
224
+ import { getPackageName } from "./index.js";
225
+
226
+ test("getPackageName returns the package name", () => {
227
+ equal(getPackageName(), "${identifier.packageName}");
228
+ });
229
+ `
230
+ }
231
+ ];
232
+ };
233
+ const pathExists = async (path) => {
234
+ try {
235
+ await promises.stat(path);
236
+ return true;
237
+ }
238
+ catch (error) {
239
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
240
+ return false;
241
+ }
242
+ throw error;
243
+ }
244
+ };
245
+
246
+ const isCliEntryPoint = () => {
247
+ return !!process.argv[1] && node_fs.realpathSync(process.argv[1]) === node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
248
+ };
249
+ if (isCliEntryPoint()) {
250
+ void runCli(process.argv.slice(2), {
251
+ cwd: process.cwd(),
252
+ stdout: process.stdout,
253
+ stderr: process.stderr
254
+ }).then((exitCode) => {
255
+ process.exitCode = exitCode;
256
+ });
257
+ }
258
+
259
+ exports.createPackageTemplate = createPackageTemplate;
260
+ exports.runCli = runCli;
@@ -0,0 +1,30 @@
1
+ /*!
2
+ * @klyper/cli v0.3.0
3
+ * Command line tools for the Klyper monorepo.
4
+ * (c) 2026 Andrew Caires
5
+ * @license: MIT
6
+ */
7
+
8
+ type Writable = {
9
+ write: (_chunk: string) => void;
10
+ };
11
+ type CliIo = {
12
+ cwd: string;
13
+ stdout: Writable;
14
+ stderr: Writable;
15
+ };
16
+ type PackageTemplateResult = {
17
+ directoryName: string;
18
+ packageName: string;
19
+ packageRoot: string;
20
+ files: string[];
21
+ };
22
+ declare const runCli: (args: string[], io: CliIo) => Promise<number>;
23
+ declare const createPackageTemplate: (options: {
24
+ cwd: string;
25
+ name: string;
26
+ scope?: string;
27
+ }) => Promise<PackageTemplateResult>;
28
+
29
+ export { createPackageTemplate, runCli };
30
+ export type { CliIo, PackageTemplateResult };
package/dist/index.js ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /*!
3
+ * @klyper/cli v0.3.0
4
+ * Command line tools for the Klyper monorepo.
5
+ * (c) 2026 Andrew Caires
6
+ * @license: MIT
7
+ */
8
+
9
+ import { realpathSync } from 'node:fs';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { mkdir, writeFile, stat } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+
14
+ const defaultScope = "@klyper";
15
+ const packageNamePattern = /^[a-z][a-z0-9-]*$/;
16
+ const scopePattern = /^@[a-z][a-z0-9-]*$/;
17
+ const helpText = `Klyper CLI
18
+
19
+ Usage:
20
+ klyper create package <name> [--scope @scope]
21
+ klyper package create <name> [--scope @scope]
22
+
23
+ Examples:
24
+ klyper create package utils
25
+ klyper create package auth --scope @klyper
26
+ `;
27
+ const runCli = async (args, io) => {
28
+ try {
29
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
30
+ io.stdout.write(helpText);
31
+ return 0;
32
+ }
33
+ const command = parseCreatePackageCommand(args);
34
+ const result = await createPackageTemplate({
35
+ cwd: io.cwd,
36
+ name: command.name,
37
+ scope: command.scope
38
+ });
39
+ io.stdout.write(`Created ${result.packageName} in packages/${result.directoryName}\n`);
40
+ return 0;
41
+ }
42
+ catch (error) {
43
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
44
+ return 1;
45
+ }
46
+ };
47
+ const createPackageTemplate = async (options) => {
48
+ const identifier = parsePackageIdentifier(options.name, options.scope ?? defaultScope);
49
+ const packageRoot = join(options.cwd, "packages", identifier.directoryName);
50
+ if (await pathExists(packageRoot)) {
51
+ throw new Error(`Package directory already exists: packages/${identifier.directoryName}`);
52
+ }
53
+ const files = buildPackageFiles(identifier);
54
+ await mkdir(join(packageRoot, "src"), { recursive: true });
55
+ await Promise.all(files.map((file) => writeFile(join(packageRoot, file.path), file.content)));
56
+ return {
57
+ directoryName: identifier.directoryName,
58
+ packageName: identifier.packageName,
59
+ packageRoot,
60
+ files: files.map((file) => join(packageRoot, file.path))
61
+ };
62
+ };
63
+ const parseCreatePackageCommand = (args) => {
64
+ const [first, second, third, ...rest] = args;
65
+ const isCreatePackage = first === "create" && second === "package";
66
+ const isPackageCreate = first === "package" && second === "create";
67
+ if (!isCreatePackage && !isPackageCreate) {
68
+ throw new Error("Unknown command. Run `klyper --help` for usage.");
69
+ }
70
+ if (!third) {
71
+ throw new Error("Missing package name.");
72
+ }
73
+ const scope = readScopeOption(rest);
74
+ return { name: third, scope };
75
+ };
76
+ const readScopeOption = (args) => {
77
+ if (args.length === 0) {
78
+ return defaultScope;
79
+ }
80
+ if (args.length === 2 && args[0] === "--scope") {
81
+ return args[1] ?? defaultScope;
82
+ }
83
+ throw new Error(`Unknown option: ${args.join(" ")}`);
84
+ };
85
+ const parsePackageIdentifier = (name, scope) => {
86
+ const trimmedName = name.trim();
87
+ const trimmedScope = scope.trim();
88
+ if (trimmedName.startsWith("@")) {
89
+ const [providedScope, packageName, extra] = trimmedName.split("/");
90
+ if (extra || !providedScope || !packageName) {
91
+ throw new Error("Scoped package names must use the @scope/name format.");
92
+ }
93
+ validateScope(providedScope);
94
+ validatePackageName(packageName);
95
+ return {
96
+ directoryName: packageName,
97
+ packageName: `${providedScope}/${packageName}`
98
+ };
99
+ }
100
+ validateScope(trimmedScope);
101
+ validatePackageName(trimmedName);
102
+ return {
103
+ directoryName: trimmedName,
104
+ packageName: `${trimmedScope}/${trimmedName}`
105
+ };
106
+ };
107
+ const validatePackageName = (name) => {
108
+ if (!packageNamePattern.test(name)) {
109
+ throw new Error("Package names must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.");
110
+ }
111
+ };
112
+ const validateScope = (scope) => {
113
+ if (!scopePattern.test(scope)) {
114
+ throw new Error("Package scopes must use @scope and contain only lowercase letters, numbers, and hyphens.");
115
+ }
116
+ };
117
+ const buildPackageFiles = (identifier) => {
118
+ return [
119
+ {
120
+ path: "package.json",
121
+ content: `${JSON.stringify({
122
+ name: identifier.packageName,
123
+ version: "0.0.0",
124
+ description: `Klyper ${identifier.directoryName} package.`,
125
+ author: "Andrew Caires",
126
+ license: "MIT",
127
+ repository: {
128
+ type: "git",
129
+ url: "git+https://github.com/andrewcaires/klyper.git"
130
+ },
131
+ type: "module",
132
+ main: "./dist/index.cjs",
133
+ module: "./dist/index.js",
134
+ types: "./dist/index.d.ts",
135
+ exports: {
136
+ ".": {
137
+ types: "./dist/index.d.ts",
138
+ import: "./dist/index.js",
139
+ require: "./dist/index.cjs",
140
+ default: "./dist/index.js"
141
+ }
142
+ },
143
+ sideEffects: false,
144
+ files: ["dist"],
145
+ scripts: {
146
+ build: "npm run clean && npm run check && npm run bundle",
147
+ bundle: "rollup --bundleConfigAsCjs -c ../../rollup.config.js",
148
+ check: "npm run lint && npm run typecheck",
149
+ clean: "rm -rf dist",
150
+ lint: "eslint .",
151
+ "lint:fix": "eslint . --fix",
152
+ prepublishOnly: "npm run build && npm test",
153
+ test: "node --import tsx --test \"src/**/*.test.ts\"",
154
+ "test:watch": "node --import tsx --test --watch \"src/**/*.test.ts\"",
155
+ typecheck: "tsc -p tsconfig.json --noEmit"
156
+ },
157
+ publishConfig: {
158
+ access: "public"
159
+ }
160
+ }, null, 2)}\n`
161
+ },
162
+ {
163
+ path: "README.md",
164
+ content: `# ${identifier.packageName}
165
+
166
+ Klyper ${identifier.directoryName} package.
167
+
168
+ ## Installation
169
+
170
+ \`\`\`sh
171
+ npm install ${identifier.packageName}
172
+ \`\`\`
173
+
174
+ This package is published as an ES module package and also exposes a CommonJS
175
+ entry through \`main\` and \`exports.require\`.
176
+
177
+ ## Commands
178
+
179
+ \`\`\`sh
180
+ npm run build --workspace ${identifier.packageName}
181
+ npm run check --workspace ${identifier.packageName}
182
+ npm run lint --workspace ${identifier.packageName}
183
+ npm test --workspace ${identifier.packageName}
184
+ \`\`\`
185
+
186
+ \`build\` uses Rollup for the package output. \`check\` runs TypeScript without emitting files.
187
+ \`prepublishOnly\` runs the package build and test suite before npm publish.
188
+
189
+ ## API
190
+
191
+ \`\`\`ts
192
+ import { getPackageName } from "${identifier.packageName}";
193
+
194
+ getPackageName();
195
+ \`\`\`
196
+ `
197
+ },
198
+ {
199
+ path: "tsconfig.json",
200
+ content: `${JSON.stringify({
201
+ extends: "../../tsconfig.base.json",
202
+ compilerOptions: {
203
+ outDir: "dist",
204
+ rootDir: "src"
205
+ },
206
+ include: ["src/**/*.ts"],
207
+ }, null, 2)}\n`
208
+ },
209
+ {
210
+ path: "src/index.ts",
211
+ content: `export const getPackageName = (): string => {
212
+ return "${identifier.packageName}";
213
+ };
214
+ `
215
+ },
216
+ {
217
+ path: "src/index.test.ts",
218
+ content: `import { equal } from "node:assert/strict";
219
+ import test from "node:test";
220
+
221
+ import { getPackageName } from "./index.js";
222
+
223
+ test("getPackageName returns the package name", () => {
224
+ equal(getPackageName(), "${identifier.packageName}");
225
+ });
226
+ `
227
+ }
228
+ ];
229
+ };
230
+ const pathExists = async (path) => {
231
+ try {
232
+ await stat(path);
233
+ return true;
234
+ }
235
+ catch (error) {
236
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
237
+ return false;
238
+ }
239
+ throw error;
240
+ }
241
+ };
242
+
243
+ const isCliEntryPoint = () => {
244
+ return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
245
+ };
246
+ if (isCliEntryPoint()) {
247
+ void runCli(process.argv.slice(2), {
248
+ cwd: process.cwd(),
249
+ stdout: process.stdout,
250
+ stderr: process.stderr
251
+ }).then((exitCode) => {
252
+ process.exitCode = exitCode;
253
+ });
254
+ }
255
+
256
+ export { createPackageTemplate, runCli };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@klyper/cli",
3
+ "version": "0.3.0",
4
+ "description": "Command line tools for the Klyper monorepo.",
5
+ "author": "Andrew Caires",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/andrewcaires/klyper.git"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "klyper": "dist/index.js"
14
+ },
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs",
23
+ "default": "./dist/index.js"
24
+ }
25
+ },
26
+ "sideEffects": false,
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "scripts": {
31
+ "build": "npm run clean && npm run check && npm run bundle",
32
+ "bundle": "rollup --bundleConfigAsCjs -c ../../rollup.config.js && chmod +x dist/index.js",
33
+ "check": "npm run lint && npm run typecheck",
34
+ "clean": "rm -rf dist",
35
+ "lint": "eslint .",
36
+ "lint:fix": "eslint . --fix",
37
+ "prepublishOnly": "npm run build && npm test",
38
+ "test": "node --import tsx --test \"src/**/*.test.ts\"",
39
+ "test:watch": "node --import tsx --test --watch \"src/**/*.test.ts\"",
40
+ "typecheck": "tsc -p tsconfig.json --noEmit"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }