clispark 1.0.0 → 1.0.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/dist/cli.js +0 -0
- package/dist/cli.js.map +1 -1
- package/package.json +60 -60
- package/templates/base/ARCHITECTURE.md +36 -36
- package/templates/base/README.md +3 -3
- package/templates/base/bin/run.js +5 -5
- package/templates/base/gitignore +3 -3
- package/templates/base/package.json +46 -46
- package/templates/base/src/base-command.ts +28 -28
- package/templates/base/src/commands/hello.test.ts +10 -10
- package/templates/base/src/commands/hello.ts +13 -13
- package/templates/base/src/index.ts +1 -1
- package/templates/base/src/logger.ts +28 -28
- package/templates/base/tsconfig.json +16 -16
- package/templates/base/tsup.config.ts +10 -10
- package/templates/base/vitest.config.ts +8 -8
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/wizard.ts","../src/registry.ts","../src/scaffold.ts","../src/logger.ts"],"sourcesContent":["// src/cli.ts\r\nimport { createRequire } from 'node:module';\r\nimport path from 'node:path';\r\nimport { Command } from 'commander';\r\nimport { runWizard } from './wizard.js';\r\nimport { scaffoldProject } from './scaffold.js';\r\nimport { withLogging } from './logger.js';\r\n\r\nconst require = createRequire(import.meta.url);\r\nconst pkg = require('../package.json') as { version: string };\r\n\r\nconst program = new Command();\r\n\r\nprogram\r\n .name('clispark')\r\n .description('Interactive scaffolding tool for new CLI projects')\r\n .version(pkg.version);\r\n\r\nprogram.action(\r\n withLogging('scaffold', async (logger) => {\r\n const answers = await runWizard();\r\n const targetDir = path.join(process.cwd(), answers.projectName);\r\n\r\n logger.info({ projectName: answers.projectName, targetDir }, 'scaffold started');\r\n await scaffoldProject({ projectName: answers.projectName, targetDir, registryUrl: answers.registryUrl });\r\n logger.info({ projectName: answers.projectName }, 'scaffold completed');\r\n\r\n console.log(`\\nDone! Your new CLI project is ready at ${targetDir}`);\r\n }),\r\n);\r\n\r\nprogram.parseAsync(process.argv).catch((error: unknown) => {\r\n console.error(error);\r\n process.exit(1);\r\n});\r\n","// src/wizard.ts\r\nimport { intro, outro, text, select, log, isCancel, cancel } from '@clack/prompts';\r\nimport { checkPackageNameAvailability, DEFAULT_REGISTRY_URL } from './registry.js';\r\nimport type { Profile, WizardAnswers } from './types.js';\r\n\r\nexport interface WizardDeps {\r\n checkAvailability: typeof checkPackageNameAvailability;\r\n}\r\n\r\nconst defaultDeps: WizardDeps = {\r\n checkAvailability: checkPackageNameAvailability,\r\n};\r\n\r\nfunction exitIfCancelled(value: unknown): void {\r\n if (isCancel(value)) {\r\n cancel('Operation cancelled.');\r\n process.exit(1);\r\n }\r\n}\r\n\r\nfunction validateProjectName(value: string): string | undefined {\r\n if (!value || value.trim().length === 0) return 'Project name is required.';\r\n if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {\r\n return 'Use lowercase letters and numbers, with single hyphens between words (no leading, trailing, or repeated hyphens).';\r\n }\r\n return undefined;\r\n}\r\n\r\nexport async function runWizard(deps: WizardDeps = defaultDeps): Promise<WizardAnswers> {\r\n intro('clispark — scaffold a new CLI project');\r\n\r\n const nameValue = await text({\r\n message: 'Project name',\r\n validate: validateProjectName,\r\n });\r\n exitIfCancelled(nameValue);\r\n let projectName = nameValue as string;\r\n\r\n const profileValue = await select({\r\n message: 'Is this a work or private project?',\r\n options: [\r\n { value: 'private', label: 'Private' },\r\n { value: 'work', label: 'Work' },\r\n ],\r\n });\r\n exitIfCancelled(profileValue);\r\n const profile = profileValue as Profile;\r\n\r\n let registryUrl = DEFAULT_REGISTRY_URL;\r\n if (profile === 'work') {\r\n const registryValue = await text({\r\n message: 'Custom npm registry URL (leave empty for npmjs.org)',\r\n placeholder: DEFAULT_REGISTRY_URL,\r\n defaultValue: DEFAULT_REGISTRY_URL,\r\n });\r\n exitIfCancelled(registryValue);\r\n registryUrl = (registryValue as string) || DEFAULT_REGISTRY_URL;\r\n }\r\n\r\n let nameAvailability = await deps.checkAvailability(projectName, registryUrl);\r\n\r\n while (nameAvailability === 'taken') {\r\n log.warn(`\"${projectName}\" is already taken on ${registryUrl}. Please choose a different name.`);\r\n\r\n const retryValue = await text({\r\n message: 'Project name',\r\n validate: validateProjectName,\r\n });\r\n exitIfCancelled(retryValue);\r\n projectName = retryValue as string;\r\n\r\n nameAvailability = await deps.checkAvailability(projectName, registryUrl);\r\n }\r\n\r\n if (nameAvailability === 'unverified') {\r\n log.warn(`Could not verify availability of \"${projectName}\" on ${registryUrl}. Continuing anyway.`);\r\n }\r\n\r\n outro(`Ready to scaffold \"${projectName}\".`);\r\n\r\n return { projectName, profile, registryUrl, nameAvailability };\r\n}\r\n","export const DEFAULT_REGISTRY_URL = 'https://registry.npmjs.org';\r\n\r\nexport type NameCheckResult = 'available' | 'taken' | 'unverified';\r\n\r\nexport async function checkPackageNameAvailability(\r\n name: string,\r\n registryUrl: string = DEFAULT_REGISTRY_URL,\r\n): Promise<NameCheckResult> {\r\n const url = `${registryUrl.replace(/\\/$/, '')}/${encodeURIComponent(name)}`;\r\n\r\n try {\r\n const response = await fetch(url);\r\n if (response.status === 404) return 'available';\r\n if (response.status === 200) return 'taken';\r\n return 'unverified';\r\n } catch {\r\n return 'unverified';\r\n }\r\n}\r\n","import spawn from 'cross-spawn';\r\nimport { fileURLToPath } from 'node:url';\r\nimport path from 'node:path';\r\nimport { cp, readdir, readFile, rename, writeFile } from 'node:fs/promises';\r\nimport { DEFAULT_REGISTRY_URL } from './registry.js';\r\n\r\nexport interface ScaffoldOptions {\r\n projectName: string;\r\n targetDir: string;\r\n registryUrl?: string;\r\n}\r\n\r\nconst TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates', 'base');\r\n\r\nasync function assertTargetDirIsUsable(targetDir: string): Promise<void> {\r\n let entries: string[];\r\n try {\r\n entries = await readdir(targetDir);\r\n } catch {\r\n return;\r\n }\r\n if (entries.length > 0) {\r\n throw new Error(`Directory \"${targetDir}\" already exists and is not empty.`);\r\n }\r\n}\r\n\r\nasync function replacePlaceholder(filePath: string, projectName: string): Promise<void> {\r\n const content = (await readFile(filePath, 'utf8')).replaceAll('{{projectName}}', projectName);\r\n await writeFile(filePath, content);\r\n}\r\n\r\nexport async function copyTemplate(options: ScaffoldOptions): Promise<void> {\r\n const { projectName, targetDir, registryUrl } = options;\r\n\r\n await assertTargetDirIsUsable(targetDir);\r\n await cp(TEMPLATE_DIR, targetDir, { recursive: true });\r\n\r\n await rename(path.join(targetDir, 'gitignore'), path.join(targetDir, '.gitignore'));\r\n\r\n await replacePlaceholder(path.join(targetDir, 'package.json'), projectName);\r\n await replacePlaceholder(path.join(targetDir, 'README.md'), projectName);\r\n await replacePlaceholder(path.join(targetDir, 'src', 'logger.ts'), projectName);\r\n await replacePlaceholder(path.join(targetDir, 'ARCHITECTURE.md'), projectName);\r\n\r\n if (registryUrl && registryUrl !== DEFAULT_REGISTRY_URL) {\r\n await writeFile(path.join(targetDir, '.npmrc'), `registry=${registryUrl}\\n`);\r\n }\r\n}\r\n\r\nexport interface ScaffoldDeps {\r\n runCommand: (command: string, args: string[], cwd: string) => Promise<void>;\r\n}\r\n\r\nasync function defaultRunCommand(command: string, args: string[], cwd: string): Promise<void> {\r\n await new Promise<void>((resolve, reject) => {\r\n const child = spawn(command, args, { cwd, stdio: 'inherit' });\r\n child.on('error', reject);\r\n child.on('close', (code) => {\r\n if (code === 0) {\r\n resolve();\r\n } else {\r\n reject(new Error(`Command \"${command} ${args.join(' ')}\" exited with code ${code}`));\r\n }\r\n });\r\n });\r\n}\r\n\r\nconst defaultScaffoldDeps: ScaffoldDeps = { runCommand: defaultRunCommand };\r\n\r\nexport async function scaffoldProject(\r\n options: ScaffoldOptions,\r\n deps: ScaffoldDeps = defaultScaffoldDeps,\r\n): Promise<void> {\r\n await copyTemplate(options);\r\n\r\n const { targetDir } = options;\r\n await deps.runCommand('git', ['init'], targetDir);\r\n await deps.runCommand('git', ['add', '-A'], targetDir);\r\n await deps.runCommand('git', ['commit', '-m', 'chore: initial scaffold from clispark'], targetDir);\r\n await deps.runCommand('npm', ['install'], targetDir);\r\n await deps.runCommand('npm', ['run', 'build'], targetDir);\r\n}\r\n","import { randomBytes } from 'node:crypto';\r\nimport { mkdirSync } from 'node:fs';\r\nimport path from 'node:path';\r\nimport envPaths from 'env-paths';\r\nimport pino, { type Logger } from 'pino';\r\n\r\nconst paths = envPaths('clispark', { suffix: '' });\r\n\r\nexport interface LoggerHandle {\r\n logger: Logger;\r\n logFilePath: string;\r\n}\r\n\r\nfunction buildLogFileName(commandName: string): string {\r\n const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');\r\n const suffix = randomBytes(3).toString('hex');\r\n return `${commandName}-${timestamp}-${suffix}.log`;\r\n}\r\n\r\nexport function createLogger(commandName: string, logDir: string = paths.log): LoggerHandle {\r\n mkdirSync(logDir, { recursive: true });\r\n\r\n const logFilePath = path.join(logDir, buildLogFileName(commandName));\r\n const logger = pino(pino.destination({ dest: logFilePath, sync: true }));\r\n\r\n return { logger, logFilePath };\r\n}\r\n\r\nexport function withLogging(\r\n commandName: string,\r\n action: (logger: Logger) => Promise<void>,\r\n logDir: string = paths.log,\r\n): () => Promise<void> {\r\n return async () => {\r\n let handle: LoggerHandle;\r\n try {\r\n handle = createLogger(commandName, logDir);\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : String(error);\r\n console.error(`\\n✖ ${message}`);\r\n process.exit(1);\r\n return;\r\n }\r\n\r\n const { logger, logFilePath } = handle;\r\n logger.info({ command: commandName }, 'started');\r\n\r\n try {\r\n await action(logger);\r\n logger.info({ command: commandName }, 'completed');\r\n } catch (error) {\r\n logger.error({ command: commandName, err: error }, 'failed');\r\n const message = error instanceof Error ? error.message : String(error);\r\n console.error(`\\n✖ ${message}`);\r\n console.error(`Details: ${logFilePath}`);\r\n process.exit(1);\r\n }\r\n };\r\n}\r\n"],"mappings":"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","names":["path","path","require","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/wizard.ts","../src/registry.ts","../src/scaffold.ts","../src/logger.ts"],"sourcesContent":["// src/cli.ts\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { Command } from 'commander';\nimport { runWizard } from './wizard.js';\nimport { scaffoldProject } from './scaffold.js';\nimport { withLogging } from './logger.js';\n\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\nconst program = new Command();\n\nprogram\n .name('clispark')\n .description('Interactive scaffolding tool for new CLI projects')\n .version(pkg.version);\n\nprogram.action(\n withLogging('scaffold', async (logger) => {\n const answers = await runWizard();\n const targetDir = path.join(process.cwd(), answers.projectName);\n\n logger.info({ projectName: answers.projectName, targetDir }, 'scaffold started');\n await scaffoldProject({ projectName: answers.projectName, targetDir, registryUrl: answers.registryUrl });\n logger.info({ projectName: answers.projectName }, 'scaffold completed');\n\n console.log(`\\nDone! Your new CLI project is ready at ${targetDir}`);\n }),\n);\n\nprogram.parseAsync(process.argv).catch((error: unknown) => {\n console.error(error);\n process.exit(1);\n});\n","// src/wizard.ts\nimport { intro, outro, text, select, log, isCancel, cancel } from '@clack/prompts';\nimport { checkPackageNameAvailability, DEFAULT_REGISTRY_URL } from './registry.js';\nimport type { Profile, WizardAnswers } from './types.js';\n\nexport interface WizardDeps {\n checkAvailability: typeof checkPackageNameAvailability;\n}\n\nconst defaultDeps: WizardDeps = {\n checkAvailability: checkPackageNameAvailability,\n};\n\nfunction exitIfCancelled(value: unknown): void {\n if (isCancel(value)) {\n cancel('Operation cancelled.');\n process.exit(1);\n }\n}\n\nfunction validateProjectName(value: string): string | undefined {\n if (!value || value.trim().length === 0) return 'Project name is required.';\n if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) {\n return 'Use lowercase letters and numbers, with single hyphens between words (no leading, trailing, or repeated hyphens).';\n }\n return undefined;\n}\n\nexport async function runWizard(deps: WizardDeps = defaultDeps): Promise<WizardAnswers> {\n intro('clispark — scaffold a new CLI project');\n\n const nameValue = await text({\n message: 'Project name',\n validate: validateProjectName,\n });\n exitIfCancelled(nameValue);\n let projectName = nameValue as string;\n\n const profileValue = await select({\n message: 'Is this a work or private project?',\n options: [\n { value: 'private', label: 'Private' },\n { value: 'work', label: 'Work' },\n ],\n });\n exitIfCancelled(profileValue);\n const profile = profileValue as Profile;\n\n let registryUrl = DEFAULT_REGISTRY_URL;\n if (profile === 'work') {\n const registryValue = await text({\n message: 'Custom npm registry URL (leave empty for npmjs.org)',\n placeholder: DEFAULT_REGISTRY_URL,\n defaultValue: DEFAULT_REGISTRY_URL,\n });\n exitIfCancelled(registryValue);\n registryUrl = (registryValue as string) || DEFAULT_REGISTRY_URL;\n }\n\n let nameAvailability = await deps.checkAvailability(projectName, registryUrl);\n\n while (nameAvailability === 'taken') {\n log.warn(`\"${projectName}\" is already taken on ${registryUrl}. Please choose a different name.`);\n\n const retryValue = await text({\n message: 'Project name',\n validate: validateProjectName,\n });\n exitIfCancelled(retryValue);\n projectName = retryValue as string;\n\n nameAvailability = await deps.checkAvailability(projectName, registryUrl);\n }\n\n if (nameAvailability === 'unverified') {\n log.warn(`Could not verify availability of \"${projectName}\" on ${registryUrl}. Continuing anyway.`);\n }\n\n outro(`Ready to scaffold \"${projectName}\".`);\n\n return { projectName, profile, registryUrl, nameAvailability };\n}\n","export const DEFAULT_REGISTRY_URL = 'https://registry.npmjs.org';\n\nexport type NameCheckResult = 'available' | 'taken' | 'unverified';\n\nexport async function checkPackageNameAvailability(\n name: string,\n registryUrl: string = DEFAULT_REGISTRY_URL,\n): Promise<NameCheckResult> {\n const url = `${registryUrl.replace(/\\/$/, '')}/${encodeURIComponent(name)}`;\n\n try {\n const response = await fetch(url);\n if (response.status === 404) return 'available';\n if (response.status === 200) return 'taken';\n return 'unverified';\n } catch {\n return 'unverified';\n }\n}\n","import spawn from 'cross-spawn';\nimport { fileURLToPath } from 'node:url';\nimport path from 'node:path';\nimport { cp, readdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { DEFAULT_REGISTRY_URL } from './registry.js';\n\nexport interface ScaffoldOptions {\n projectName: string;\n targetDir: string;\n registryUrl?: string;\n}\n\nconst TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'templates', 'base');\n\nasync function assertTargetDirIsUsable(targetDir: string): Promise<void> {\n let entries: string[];\n try {\n entries = await readdir(targetDir);\n } catch {\n return;\n }\n if (entries.length > 0) {\n throw new Error(`Directory \"${targetDir}\" already exists and is not empty.`);\n }\n}\n\nasync function replacePlaceholder(filePath: string, projectName: string): Promise<void> {\n const content = (await readFile(filePath, 'utf8')).replaceAll('{{projectName}}', projectName);\n await writeFile(filePath, content);\n}\n\nexport async function copyTemplate(options: ScaffoldOptions): Promise<void> {\n const { projectName, targetDir, registryUrl } = options;\n\n await assertTargetDirIsUsable(targetDir);\n await cp(TEMPLATE_DIR, targetDir, { recursive: true });\n\n await rename(path.join(targetDir, 'gitignore'), path.join(targetDir, '.gitignore'));\n\n await replacePlaceholder(path.join(targetDir, 'package.json'), projectName);\n await replacePlaceholder(path.join(targetDir, 'README.md'), projectName);\n await replacePlaceholder(path.join(targetDir, 'src', 'logger.ts'), projectName);\n await replacePlaceholder(path.join(targetDir, 'ARCHITECTURE.md'), projectName);\n\n if (registryUrl && registryUrl !== DEFAULT_REGISTRY_URL) {\n await writeFile(path.join(targetDir, '.npmrc'), `registry=${registryUrl}\\n`);\n }\n}\n\nexport interface ScaffoldDeps {\n runCommand: (command: string, args: string[], cwd: string) => Promise<void>;\n}\n\nasync function defaultRunCommand(command: string, args: string[], cwd: string): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const child = spawn(command, args, { cwd, stdio: 'inherit' });\n child.on('error', reject);\n child.on('close', (code) => {\n if (code === 0) {\n resolve();\n } else {\n reject(new Error(`Command \"${command} ${args.join(' ')}\" exited with code ${code}`));\n }\n });\n });\n}\n\nconst defaultScaffoldDeps: ScaffoldDeps = { runCommand: defaultRunCommand };\n\nexport async function scaffoldProject(\n options: ScaffoldOptions,\n deps: ScaffoldDeps = defaultScaffoldDeps,\n): Promise<void> {\n await copyTemplate(options);\n\n const { targetDir } = options;\n await deps.runCommand('git', ['init'], targetDir);\n await deps.runCommand('git', ['add', '-A'], targetDir);\n await deps.runCommand('git', ['commit', '-m', 'chore: initial scaffold from clispark'], targetDir);\n await deps.runCommand('npm', ['install'], targetDir);\n await deps.runCommand('npm', ['run', 'build'], targetDir);\n}\n","import { randomBytes } from 'node:crypto';\nimport { mkdirSync } from 'node:fs';\nimport path from 'node:path';\nimport envPaths from 'env-paths';\nimport pino, { type Logger } from 'pino';\n\nconst paths = envPaths('clispark', { suffix: '' });\n\nexport interface LoggerHandle {\n logger: Logger;\n logFilePath: string;\n}\n\nfunction buildLogFileName(commandName: string): string {\n const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');\n const suffix = randomBytes(3).toString('hex');\n return `${commandName}-${timestamp}-${suffix}.log`;\n}\n\nexport function createLogger(commandName: string, logDir: string = paths.log): LoggerHandle {\n mkdirSync(logDir, { recursive: true });\n\n const logFilePath = path.join(logDir, buildLogFileName(commandName));\n const logger = pino(pino.destination({ dest: logFilePath, sync: true }));\n\n return { logger, logFilePath };\n}\n\nexport function withLogging(\n commandName: string,\n action: (logger: Logger) => Promise<void>,\n logDir: string = paths.log,\n): () => Promise<void> {\n return async () => {\n let handle: LoggerHandle;\n try {\n handle = createLogger(commandName, logDir);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`\\n✖ ${message}`);\n process.exit(1);\n return;\n }\n\n const { logger, logFilePath } = handle;\n logger.info({ command: commandName }, 'started');\n\n try {\n await action(logger);\n logger.info({ command: commandName }, 'completed');\n } catch (error) {\n logger.error({ command: commandName, err: error }, 'failed');\n const message = error instanceof Error ? error.message : String(error);\n console.error(`\\n✖ ${message}`);\n console.error(`Details: ${logFilePath}`);\n process.exit(1);\n }\n };\n}\n"],"mappings":"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","names":["path","path","require","path"]}
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "clispark",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Interactive scaffolding tool for new CLI projects",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"cli",
|
|
7
|
-
"scaffolding",
|
|
8
|
-
"generator",
|
|
9
|
-
"boilerplate",
|
|
10
|
-
"starter",
|
|
11
|
-
"oclif",
|
|
12
|
-
"typescript"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://github.com/martinwichner/clispark#readme",
|
|
15
|
-
"bugs": {
|
|
16
|
-
"url": "https://github.com/martinwichner/clispark/issues"
|
|
17
|
-
},
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/martinwichner/clispark.git"
|
|
21
|
-
},
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"author": "Martin Mohn <martin.wichner@googlemail.com>",
|
|
24
|
-
"type": "module",
|
|
25
|
-
"bin": {
|
|
26
|
-
"clispark": "./dist/cli.js"
|
|
27
|
-
},
|
|
28
|
-
"files": [
|
|
29
|
-
"dist",
|
|
30
|
-
"templates",
|
|
31
|
-
"LICENSE",
|
|
32
|
-
"README.md"
|
|
33
|
-
],
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=18"
|
|
36
|
-
},
|
|
37
|
-
"scripts": {
|
|
38
|
-
"build": "tsup",
|
|
39
|
-
"postbuild": "shx chmod +x dist/cli.js",
|
|
40
|
-
"dev": "tsx src/cli.ts",
|
|
41
|
-
"test": "vitest run",
|
|
42
|
-
"typecheck": "tsc --noEmit"
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"@clack/prompts": "^0.9.1",
|
|
46
|
-
"commander": "^13.1.0",
|
|
47
|
-
"cross-spawn": "^7.0.3",
|
|
48
|
-
"env-paths": "^3.0.0",
|
|
49
|
-
"pino": "^9.6.0"
|
|
50
|
-
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@types/cross-spawn": "^6.0.6",
|
|
53
|
-
"@types/node": "^22.10.5",
|
|
54
|
-
"shx": "^0.3.4",
|
|
55
|
-
"tsup": "^8.3.5",
|
|
56
|
-
"tsx": "^4.19.2",
|
|
57
|
-
"typescript": "^5.7.2",
|
|
58
|
-
"vitest": "^4.1.10"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "clispark",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Interactive scaffolding tool for new CLI projects",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"scaffolding",
|
|
8
|
+
"generator",
|
|
9
|
+
"boilerplate",
|
|
10
|
+
"starter",
|
|
11
|
+
"oclif",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/martinwichner/clispark#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/martinwichner/clispark/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/martinwichner/clispark.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Martin Mohn <martin.wichner@googlemail.com>",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"clispark": "./dist/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"templates",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"postbuild": "shx chmod +x dist/cli.js",
|
|
40
|
+
"dev": "tsx src/cli.ts",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@clack/prompts": "^0.9.1",
|
|
46
|
+
"commander": "^13.1.0",
|
|
47
|
+
"cross-spawn": "^7.0.3",
|
|
48
|
+
"env-paths": "^3.0.0",
|
|
49
|
+
"pino": "^9.6.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/cross-spawn": "^6.0.6",
|
|
53
|
+
"@types/node": "^22.10.5",
|
|
54
|
+
"shx": "^0.3.4",
|
|
55
|
+
"tsup": "^8.3.5",
|
|
56
|
+
"tsx": "^4.19.2",
|
|
57
|
+
"typescript": "^5.7.2",
|
|
58
|
+
"vitest": "^4.1.10"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
# {{projectName}} Architecture
|
|
2
|
-
|
|
3
|
-
This document explains the conventions this project was generated with, so the automatic behavior (command discovery, logging, error handling) doesn't feel like unexplained magic.
|
|
4
|
-
|
|
5
|
-
## Commands
|
|
6
|
-
|
|
7
|
-
Every command lives in `src/commands/` and extends `BaseCommand` (`src/base-command.ts`) instead of oclif's own `Command` class directly:
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
import { BaseCommand } from '../base-command.js';
|
|
11
|
-
|
|
12
|
-
export default class MyCommand extends BaseCommand {
|
|
13
|
-
static description = 'What this command does';
|
|
14
|
-
static args = {};
|
|
15
|
-
static flags = {};
|
|
16
|
-
|
|
17
|
-
async run(): Promise<void> {
|
|
18
|
-
await this.parse(MyCommand);
|
|
19
|
-
// ...
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
`BaseCommand` overrides oclif's `init()`/`catch()`/`finally()` lifecycle methods to log every command's start, failure, and completion automatically — no manual logging calls needed inside `run()`.
|
|
25
|
-
|
|
26
|
-
## Command Discovery
|
|
27
|
-
|
|
28
|
-
oclif discovers commands at runtime from the `oclif.commands` path in `package.json` (`./dist/commands`) — there is no custom filesystem-scanning code. Dropping a new file in `src/commands/` and building the project is enough for oclif to pick it up; nothing needs to be manually registered.
|
|
29
|
-
|
|
30
|
-
## Logging
|
|
31
|
-
|
|
32
|
-
`src/logger.ts` writes structured JSON logs via `pino`, one file per command invocation, to an OS-appropriate log directory (via `env-paths`) — never to the project's own working directory. Every log line includes the command name. On failure, the full error (including stack trace) is logged, while the terminal only ever shows a clean `Error: <message>` — never a raw stack trace.
|
|
33
|
-
|
|
34
|
-
## Testing
|
|
35
|
-
|
|
36
|
-
Tests use `@oclif/test`'s `runCommand()` helper and live next to the command they test (e.g. `src/commands/hello.test.ts`). Running `npm test` first runs a build (via the `pretest` script) — `runCommand()` reads compiled output from `dist/commands`, so a stale or missing build produces empty, unhelpful output instead of a clear error. Every command's `run()` must call `await this.parse(<CommandClass>)`, even with no flags/args, to avoid an oclif `UnparsedCommand` warning.
|
|
1
|
+
# {{projectName}} Architecture
|
|
2
|
+
|
|
3
|
+
This document explains the conventions this project was generated with, so the automatic behavior (command discovery, logging, error handling) doesn't feel like unexplained magic.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
Every command lives in `src/commands/` and extends `BaseCommand` (`src/base-command.ts`) instead of oclif's own `Command` class directly:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { BaseCommand } from '../base-command.js';
|
|
11
|
+
|
|
12
|
+
export default class MyCommand extends BaseCommand {
|
|
13
|
+
static description = 'What this command does';
|
|
14
|
+
static args = {};
|
|
15
|
+
static flags = {};
|
|
16
|
+
|
|
17
|
+
async run(): Promise<void> {
|
|
18
|
+
await this.parse(MyCommand);
|
|
19
|
+
// ...
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`BaseCommand` overrides oclif's `init()`/`catch()`/`finally()` lifecycle methods to log every command's start, failure, and completion automatically — no manual logging calls needed inside `run()`.
|
|
25
|
+
|
|
26
|
+
## Command Discovery
|
|
27
|
+
|
|
28
|
+
oclif discovers commands at runtime from the `oclif.commands` path in `package.json` (`./dist/commands`) — there is no custom filesystem-scanning code. Dropping a new file in `src/commands/` and building the project is enough for oclif to pick it up; nothing needs to be manually registered.
|
|
29
|
+
|
|
30
|
+
## Logging
|
|
31
|
+
|
|
32
|
+
`src/logger.ts` writes structured JSON logs via `pino`, one file per command invocation, to an OS-appropriate log directory (via `env-paths`) — never to the project's own working directory. Every log line includes the command name. On failure, the full error (including stack trace) is logged, while the terminal only ever shows a clean `Error: <message>` — never a raw stack trace.
|
|
33
|
+
|
|
34
|
+
## Testing
|
|
35
|
+
|
|
36
|
+
Tests use `@oclif/test`'s `runCommand()` helper and live next to the command they test (e.g. `src/commands/hello.test.ts`). Running `npm test` first runs a build (via the `pretest` script) — `runCommand()` reads compiled output from `dist/commands`, so a stale or missing build produces empty, unhelpful output instead of a clear error. Every command's `run()` must call `await this.parse(<CommandClass>)`, even with no flags/args, to avoid an oclif `UnparsedCommand` warning.
|
package/templates/base/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# {{projectName}}
|
|
2
|
-
|
|
3
|
-
Generated with [clispark](https://github.com/martinwichner/clispark).
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
Generated with [clispark](https://github.com/martinwichner/clispark).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execute } from '@oclif/core';
|
|
4
|
-
|
|
5
|
-
await execute({ dir: import.meta.url });
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execute } from '@oclif/core';
|
|
4
|
+
|
|
5
|
+
await execute({ dir: import.meta.url });
|
package/templates/base/gitignore
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
node_modules
|
|
2
|
-
dist
|
|
3
|
-
*.log
|
|
1
|
+
node_modules
|
|
2
|
+
dist
|
|
3
|
+
*.log
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "{{projectName}}",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"description": "",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"{{projectName}}": "./bin/run.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"bin",
|
|
11
|
-
"dist"
|
|
12
|
-
],
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=18"
|
|
15
|
-
},
|
|
16
|
-
"oclif": {
|
|
17
|
-
"bin": "{{projectName}}",
|
|
18
|
-
"dirname": "{{projectName}}",
|
|
19
|
-
"commands": "./dist/commands",
|
|
20
|
-
"topicSeparator": " ",
|
|
21
|
-
"plugins": [
|
|
22
|
-
"@oclif/plugin-help"
|
|
23
|
-
]
|
|
24
|
-
},
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsup",
|
|
27
|
-
"postbuild": "shx chmod +x bin/run.js",
|
|
28
|
-
"pretest": "npm run build",
|
|
29
|
-
"test": "vitest run",
|
|
30
|
-
"typecheck": "tsc --noEmit"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"@oclif/core": "^4.0.0",
|
|
34
|
-
"@oclif/plugin-help": "^6.0.0",
|
|
35
|
-
"env-paths": "^3.0.0",
|
|
36
|
-
"pino": "^9.6.0"
|
|
37
|
-
},
|
|
38
|
-
"devDependencies": {
|
|
39
|
-
"@oclif/test": "^4.0.0",
|
|
40
|
-
"@types/node": "^22.10.5",
|
|
41
|
-
"shx": "^0.3.4",
|
|
42
|
-
"tsup": "^8.3.5",
|
|
43
|
-
"typescript": "^5.7.2",
|
|
44
|
-
"vitest": "^2.1.8"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"{{projectName}}": "./bin/run.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"oclif": {
|
|
17
|
+
"bin": "{{projectName}}",
|
|
18
|
+
"dirname": "{{projectName}}",
|
|
19
|
+
"commands": "./dist/commands",
|
|
20
|
+
"topicSeparator": " ",
|
|
21
|
+
"plugins": [
|
|
22
|
+
"@oclif/plugin-help"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"postbuild": "shx chmod +x bin/run.js",
|
|
28
|
+
"pretest": "npm run build",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@oclif/core": "^4.0.0",
|
|
34
|
+
"@oclif/plugin-help": "^6.0.0",
|
|
35
|
+
"env-paths": "^3.0.0",
|
|
36
|
+
"pino": "^9.6.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@oclif/test": "^4.0.0",
|
|
40
|
+
"@types/node": "^22.10.5",
|
|
41
|
+
"shx": "^0.3.4",
|
|
42
|
+
"tsup": "^8.3.5",
|
|
43
|
+
"typescript": "^5.7.2",
|
|
44
|
+
"vitest": "^2.1.8"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
// templates/base/src/base-command.ts
|
|
2
|
-
import { Command, type Interfaces } from '@oclif/core';
|
|
3
|
-
import type { Logger } from 'pino';
|
|
4
|
-
import { createLogger } from './logger.js';
|
|
5
|
-
|
|
6
|
-
export abstract class BaseCommand extends Command {
|
|
7
|
-
protected logger?: Logger;
|
|
8
|
-
|
|
9
|
-
async init(): Promise<void> {
|
|
10
|
-
await super.init();
|
|
11
|
-
|
|
12
|
-
const { logger } = createLogger(this.id ?? 'unknown');
|
|
13
|
-
this.logger = logger;
|
|
14
|
-
this.logger.info({ command: this.id }, 'started');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
protected async catch(err: Interfaces.CommandError): Promise<unknown> {
|
|
18
|
-
this.logger?.error({ command: this.id, err }, 'failed');
|
|
19
|
-
return super.catch(err);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
protected async finally(err: Error | undefined): Promise<unknown> {
|
|
23
|
-
if (!err) {
|
|
24
|
-
this.logger?.info({ command: this.id }, 'completed');
|
|
25
|
-
}
|
|
26
|
-
return super.finally(err);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
// templates/base/src/base-command.ts
|
|
2
|
+
import { Command, type Interfaces } from '@oclif/core';
|
|
3
|
+
import type { Logger } from 'pino';
|
|
4
|
+
import { createLogger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
export abstract class BaseCommand extends Command {
|
|
7
|
+
protected logger?: Logger;
|
|
8
|
+
|
|
9
|
+
async init(): Promise<void> {
|
|
10
|
+
await super.init();
|
|
11
|
+
|
|
12
|
+
const { logger } = createLogger(this.id ?? 'unknown');
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
this.logger.info({ command: this.id }, 'started');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected async catch(err: Interfaces.CommandError): Promise<unknown> {
|
|
18
|
+
this.logger?.error({ command: this.id, err }, 'failed');
|
|
19
|
+
return super.catch(err);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected async finally(err: Error | undefined): Promise<unknown> {
|
|
23
|
+
if (!err) {
|
|
24
|
+
this.logger?.info({ command: this.id }, 'completed');
|
|
25
|
+
}
|
|
26
|
+
return super.finally(err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// templates/base/src/commands/hello.test.ts
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { runCommand } from '@oclif/test';
|
|
4
|
-
|
|
5
|
-
describe('hello', () => {
|
|
6
|
-
it('prints a greeting', async () => {
|
|
7
|
-
const { stdout } = await runCommand('hello');
|
|
8
|
-
expect(stdout).toContain('Hello from your new CLI!');
|
|
9
|
-
});
|
|
10
|
-
});
|
|
1
|
+
// templates/base/src/commands/hello.test.ts
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { runCommand } from '@oclif/test';
|
|
4
|
+
|
|
5
|
+
describe('hello', () => {
|
|
6
|
+
it('prints a greeting', async () => {
|
|
7
|
+
const { stdout } = await runCommand('hello');
|
|
8
|
+
expect(stdout).toContain('Hello from your new CLI!');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// templates/base/src/commands/hello.ts
|
|
2
|
-
import { BaseCommand } from '../base-command.js';
|
|
3
|
-
|
|
4
|
-
export default class Hello extends BaseCommand {
|
|
5
|
-
static description = 'Say hello';
|
|
6
|
-
static args = {};
|
|
7
|
-
static flags = {};
|
|
8
|
-
|
|
9
|
-
async run(): Promise<void> {
|
|
10
|
-
await this.parse(Hello);
|
|
11
|
-
this.log('Hello from your new CLI!');
|
|
12
|
-
}
|
|
13
|
-
}
|
|
1
|
+
// templates/base/src/commands/hello.ts
|
|
2
|
+
import { BaseCommand } from '../base-command.js';
|
|
3
|
+
|
|
4
|
+
export default class Hello extends BaseCommand {
|
|
5
|
+
static description = 'Say hello';
|
|
6
|
+
static args = {};
|
|
7
|
+
static flags = {};
|
|
8
|
+
|
|
9
|
+
async run(): Promise<void> {
|
|
10
|
+
await this.parse(Hello);
|
|
11
|
+
this.log('Hello from your new CLI!');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { run } from '@oclif/core';
|
|
1
|
+
export { run } from '@oclif/core';
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
// templates/base/src/logger.ts
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import { mkdirSync } from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import envPaths from 'env-paths';
|
|
6
|
-
import pino, { type Logger } from 'pino';
|
|
7
|
-
|
|
8
|
-
const paths = envPaths('{{projectName}}', { suffix: '' });
|
|
9
|
-
|
|
10
|
-
export interface LoggerHandle {
|
|
11
|
-
logger: Logger;
|
|
12
|
-
logFilePath: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function buildLogFileName(commandName: string): string {
|
|
16
|
-
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
|
|
17
|
-
const suffix = randomBytes(3).toString('hex');
|
|
18
|
-
return `${commandName}-${timestamp}-${suffix}.log`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function createLogger(commandName: string, logDir: string = paths.log): LoggerHandle {
|
|
22
|
-
mkdirSync(logDir, { recursive: true });
|
|
23
|
-
|
|
24
|
-
const logFilePath = path.join(logDir, buildLogFileName(commandName));
|
|
25
|
-
const logger = pino(pino.destination({ dest: logFilePath, sync: true }));
|
|
26
|
-
|
|
27
|
-
return { logger, logFilePath };
|
|
28
|
-
}
|
|
1
|
+
// templates/base/src/logger.ts
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import envPaths from 'env-paths';
|
|
6
|
+
import pino, { type Logger } from 'pino';
|
|
7
|
+
|
|
8
|
+
const paths = envPaths('{{projectName}}', { suffix: '' });
|
|
9
|
+
|
|
10
|
+
export interface LoggerHandle {
|
|
11
|
+
logger: Logger;
|
|
12
|
+
logFilePath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildLogFileName(commandName: string): string {
|
|
16
|
+
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
|
|
17
|
+
const suffix = randomBytes(3).toString('hex');
|
|
18
|
+
return `${commandName}-${timestamp}-${suffix}.log`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createLogger(commandName: string, logDir: string = paths.log): LoggerHandle {
|
|
22
|
+
mkdirSync(logDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
const logFilePath = path.join(logDir, buildLogFileName(commandName));
|
|
25
|
+
const logger = pino(pino.destination({ dest: logFilePath, sync: true }));
|
|
26
|
+
|
|
27
|
+
return { logger, logFilePath };
|
|
28
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Bundler",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"esModuleInterop": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"resolveJsonModule": true,
|
|
11
|
-
"rootDir": "src",
|
|
12
|
-
"outDir": "dist",
|
|
13
|
-
"types": ["node"]
|
|
14
|
-
},
|
|
15
|
-
"include": ["src"]
|
|
16
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"rootDir": "src",
|
|
12
|
+
"outDir": "dist",
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { defineConfig } from 'tsup';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ['src/index.ts', 'src/commands/**/*.ts', '!src/commands/**/*.test.ts'],
|
|
5
|
-
format: ['esm'],
|
|
6
|
-
target: 'node18',
|
|
7
|
-
outDir: 'dist',
|
|
8
|
-
clean: true,
|
|
9
|
-
sourcemap: true,
|
|
10
|
-
});
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts', 'src/commands/**/*.ts', '!src/commands/**/*.test.ts'],
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
target: 'node18',
|
|
7
|
+
outDir: 'dist',
|
|
8
|
+
clean: true,
|
|
9
|
+
sourcemap: true,
|
|
10
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
environment: 'node',
|
|
6
|
-
disableConsoleIntercept: true,
|
|
7
|
-
},
|
|
8
|
-
});
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: 'node',
|
|
6
|
+
disableConsoleIntercept: true,
|
|
7
|
+
},
|
|
8
|
+
});
|