@soleri/cli 0.0.1 → 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.
Files changed (67) hide show
  1. package/README.md +98 -0
  2. package/dist/commands/add-domain.d.ts +2 -0
  3. package/dist/commands/add-domain.js +41 -0
  4. package/dist/commands/add-domain.js.map +1 -0
  5. package/dist/commands/create.d.ts +2 -0
  6. package/dist/commands/create.js +68 -0
  7. package/dist/commands/create.js.map +1 -0
  8. package/dist/commands/dev.d.ts +2 -0
  9. package/dist/commands/dev.js +34 -0
  10. package/dist/commands/dev.js.map +1 -0
  11. package/dist/commands/doctor.d.ts +2 -0
  12. package/dist/commands/doctor.js +31 -0
  13. package/dist/commands/doctor.js.map +1 -0
  14. package/dist/commands/hooks.d.ts +2 -0
  15. package/dist/commands/hooks.js +76 -0
  16. package/dist/commands/hooks.js.map +1 -0
  17. package/dist/commands/install-knowledge.d.ts +2 -0
  18. package/dist/commands/install-knowledge.js +43 -0
  19. package/dist/commands/install-knowledge.js.map +1 -0
  20. package/dist/commands/list.d.ts +2 -0
  21. package/dist/commands/list.js +33 -0
  22. package/dist/commands/list.js.map +1 -0
  23. package/dist/hooks/generator.d.ts +15 -0
  24. package/dist/hooks/generator.js +58 -0
  25. package/dist/hooks/generator.js.map +1 -0
  26. package/dist/hooks/templates.d.ts +3 -0
  27. package/dist/hooks/templates.js +156 -0
  28. package/dist/hooks/templates.js.map +1 -0
  29. package/dist/main.d.ts +2 -0
  30. package/dist/main.js +23 -0
  31. package/dist/main.js.map +1 -0
  32. package/dist/prompts/create-wizard.d.ts +6 -0
  33. package/dist/prompts/create-wizard.js +126 -0
  34. package/dist/prompts/create-wizard.js.map +1 -0
  35. package/dist/utils/agent-context.d.ts +12 -0
  36. package/dist/utils/agent-context.js +31 -0
  37. package/dist/utils/agent-context.js.map +1 -0
  38. package/dist/utils/checks.d.ts +12 -0
  39. package/dist/utils/checks.js +138 -0
  40. package/dist/utils/checks.js.map +1 -0
  41. package/dist/utils/logger.d.ts +10 -0
  42. package/dist/utils/logger.js +33 -0
  43. package/dist/utils/logger.js.map +1 -0
  44. package/package.json +40 -4
  45. package/src/__tests__/add-domain.test.ts +117 -0
  46. package/src/__tests__/create.test.ts +92 -0
  47. package/src/__tests__/dev.test.ts +62 -0
  48. package/src/__tests__/doctor.test.ts +121 -0
  49. package/src/__tests__/hooks.test.ts +133 -0
  50. package/src/__tests__/install-knowledge.test.ts +117 -0
  51. package/src/__tests__/list.test.ts +80 -0
  52. package/src/commands/add-domain.ts +46 -0
  53. package/src/commands/create.ts +73 -0
  54. package/src/commands/dev.ts +39 -0
  55. package/src/commands/doctor.ts +33 -0
  56. package/src/commands/hooks.ts +86 -0
  57. package/src/commands/install-knowledge.ts +49 -0
  58. package/src/commands/list.ts +42 -0
  59. package/src/hooks/generator.ts +65 -0
  60. package/src/hooks/templates.ts +185 -0
  61. package/src/main.ts +27 -0
  62. package/src/prompts/create-wizard.ts +129 -0
  63. package/src/utils/agent-context.ts +38 -0
  64. package/src/utils/checks.ts +148 -0
  65. package/src/utils/logger.ts +39 -0
  66. package/tsconfig.json +21 -0
  67. package/vitest.config.ts +8 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Editor-specific hook/config templates.
3
+ *
4
+ * Each template function reads agent identity and generates
5
+ * editor-appropriate configuration files.
6
+ */
7
+ import { detectAgent } from '../utils/agent-context.js';
8
+
9
+ export type EditorId = 'claude-code' | 'cursor' | 'windsurf' | 'copilot';
10
+
11
+ export const SUPPORTED_EDITORS: EditorId[] = ['claude-code', 'cursor', 'windsurf', 'copilot'];
12
+
13
+ interface AgentMeta {
14
+ agentId: string;
15
+ packageName: string;
16
+ }
17
+
18
+ /** Sanitize agent ID for safe interpolation into shell commands and templates. */
19
+ function sanitizeId(id: string): string {
20
+ return id.replace(/[^a-z0-9-]/g, '');
21
+ }
22
+
23
+ function getAgentMeta(dir?: string): AgentMeta | null {
24
+ const ctx = detectAgent(dir);
25
+ if (!ctx) return null;
26
+ return { agentId: sanitizeId(ctx.agentId), packageName: ctx.packageName };
27
+ }
28
+
29
+ // ── Claude Code ──
30
+
31
+ function generateClaudeCodeSettings(dir?: string): Record<string, string> {
32
+ const meta = getAgentMeta(dir);
33
+ const agentId = meta?.agentId ?? 'my-agent';
34
+
35
+ const settings = JSON.stringify(
36
+ {
37
+ hooks: {
38
+ PreToolUse: [
39
+ {
40
+ matcher: '*',
41
+ hooks: [
42
+ {
43
+ type: 'command',
44
+ command: `echo "[${agentId}] tool: $TOOL_NAME"`,
45
+ },
46
+ ],
47
+ },
48
+ ],
49
+ PostToolUse: [
50
+ {
51
+ matcher: 'Edit|Write',
52
+ hooks: [
53
+ {
54
+ type: 'command',
55
+ command: `echo "[${agentId}] file changed"`,
56
+ },
57
+ ],
58
+ },
59
+ ],
60
+ SessionStart: [
61
+ {
62
+ hooks: [
63
+ {
64
+ type: 'command',
65
+ command: `echo "[${agentId}] session started"`,
66
+ },
67
+ ],
68
+ },
69
+ ],
70
+ },
71
+ },
72
+ null,
73
+ 2,
74
+ );
75
+
76
+ return {
77
+ '.claude/settings.json': settings,
78
+ };
79
+ }
80
+
81
+ // ── Cursor ──
82
+
83
+ function generateCursorRules(dir?: string): Record<string, string> {
84
+ const meta = getAgentMeta(dir);
85
+ const agentId = meta?.agentId ?? 'my-agent';
86
+
87
+ const rules = `# ${agentId} — Cursor Rules
88
+ # Generated by soleri hooks add cursor
89
+
90
+ ## Agent Context
91
+ This project is a Soleri AI agent (${agentId}).
92
+ The agent uses the MCP protocol and is structured with facades, a vault, and a brain.
93
+
94
+ ## Key Directories
95
+ - src/facades/ — MCP tool facades (one per domain)
96
+ - src/intelligence/data/ — Knowledge bundles (JSON)
97
+ - src/identity/ — Persona definition
98
+ - src/activation/ — Activation and CLAUDE.md injection
99
+
100
+ ## Conventions
101
+ - All facades follow the FacadeConfig pattern from @soleri/core
102
+ - Knowledge entries use the pattern/anti-pattern/rule taxonomy
103
+ - Domain names are kebab-case
104
+ - Facade names use the pattern: {agentId}_{domain}
105
+ `;
106
+
107
+ return {
108
+ '.cursorrules': rules,
109
+ };
110
+ }
111
+
112
+ // ── Windsurf ──
113
+
114
+ function generateWindsurfRules(dir?: string): Record<string, string> {
115
+ const meta = getAgentMeta(dir);
116
+ const agentId = meta?.agentId ?? 'my-agent';
117
+
118
+ const rules = `# ${agentId} — Windsurf Rules
119
+ # Generated by soleri hooks add windsurf
120
+
121
+ ## Agent Context
122
+ This project is a Soleri AI agent (${agentId}).
123
+ The agent uses the MCP protocol and is structured with facades, a vault, and a brain.
124
+
125
+ ## Key Directories
126
+ - src/facades/ — MCP tool facades (one per domain)
127
+ - src/intelligence/data/ — Knowledge bundles (JSON)
128
+ - src/identity/ — Persona definition
129
+ - src/activation/ — Activation and CLAUDE.md injection
130
+
131
+ ## Conventions
132
+ - All facades follow the FacadeConfig pattern from @soleri/core
133
+ - Knowledge entries use the pattern/anti-pattern/rule taxonomy
134
+ - Domain names are kebab-case
135
+ - Facade names use the pattern: {agentId}_{domain}
136
+ `;
137
+
138
+ return {
139
+ '.windsurfrules': rules,
140
+ };
141
+ }
142
+
143
+ // ── GitHub Copilot ──
144
+
145
+ function generateCopilotInstructions(dir?: string): Record<string, string> {
146
+ const meta = getAgentMeta(dir);
147
+ const agentId = meta?.agentId ?? 'my-agent';
148
+
149
+ const instructions = `# ${agentId} — Copilot Instructions
150
+ <!-- Generated by soleri hooks add copilot -->
151
+
152
+ ## Agent Context
153
+ This project is a Soleri AI agent (${agentId}).
154
+ The agent uses the MCP protocol and is structured with facades, a vault, and a brain.
155
+
156
+ ## Key Directories
157
+ - \`src/facades/\` — MCP tool facades (one per domain)
158
+ - \`src/intelligence/data/\` — Knowledge bundles (JSON)
159
+ - \`src/identity/\` — Persona definition
160
+ - \`src/activation/\` — Activation and CLAUDE.md injection
161
+
162
+ ## Conventions
163
+ - All facades follow the FacadeConfig pattern from @soleri/core
164
+ - Knowledge entries use the pattern/anti-pattern/rule taxonomy
165
+ - Domain names are kebab-case
166
+ - Facade names use the pattern: \`{agentId}_{domain}\`
167
+ `;
168
+
169
+ return {
170
+ '.github/copilot-instructions.md': instructions,
171
+ };
172
+ }
173
+
174
+ export function getEditorFiles(editor: EditorId, dir?: string): Record<string, string> {
175
+ switch (editor) {
176
+ case 'claude-code':
177
+ return generateClaudeCodeSettings(dir);
178
+ case 'cursor':
179
+ return generateCursorRules(dir);
180
+ case 'windsurf':
181
+ return generateWindsurfRules(dir);
182
+ case 'copilot':
183
+ return generateCopilotInstructions(dir);
184
+ }
185
+ }
package/src/main.ts ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { registerCreate } from './commands/create.js';
5
+ import { registerList } from './commands/list.js';
6
+ import { registerAddDomain } from './commands/add-domain.js';
7
+ import { registerInstallKnowledge } from './commands/install-knowledge.js';
8
+ import { registerDev } from './commands/dev.js';
9
+ import { registerDoctor } from './commands/doctor.js';
10
+ import { registerHooks } from './commands/hooks.js';
11
+
12
+ const program = new Command();
13
+
14
+ program
15
+ .name('soleri')
16
+ .description('Developer CLI for creating and managing Soleri AI agents')
17
+ .version('1.0.0');
18
+
19
+ registerCreate(program);
20
+ registerList(program);
21
+ registerAddDomain(program);
22
+ registerInstallKnowledge(program);
23
+ registerDev(program);
24
+ registerDoctor(program);
25
+ registerHooks(program);
26
+
27
+ program.parse();
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Interactive create wizard using @clack/prompts.
3
+ */
4
+ import * as p from '@clack/prompts';
5
+ import type { AgentConfig } from '@soleri/forge/lib';
6
+
7
+ /**
8
+ * Run the interactive create wizard and return an AgentConfig.
9
+ * Returns null if the user cancels.
10
+ */
11
+ export async function runCreateWizard(initialName?: string): Promise<AgentConfig | null> {
12
+ p.intro('Create a new Soleri agent');
13
+
14
+ const id =
15
+ initialName ??
16
+ ((await p.text({
17
+ message: 'Agent ID (kebab-case)',
18
+ placeholder: 'my-agent',
19
+ validate: (v = '') => {
20
+ if (!/^[a-z][a-z0-9-]*$/.test(v)) return 'Must be kebab-case (e.g., "my-agent")';
21
+ },
22
+ })) as string);
23
+
24
+ if (p.isCancel(id)) return null;
25
+
26
+ const name = (await p.text({
27
+ message: 'Display name',
28
+ placeholder: 'My Agent',
29
+ validate: (v) => {
30
+ if (!v || v.length > 50) return 'Required (max 50 chars)';
31
+ },
32
+ })) as string;
33
+
34
+ if (p.isCancel(name)) return null;
35
+
36
+ const role = (await p.text({
37
+ message: 'Role (one line)',
38
+ placeholder: 'A helpful AI assistant for...',
39
+ validate: (v) => {
40
+ if (!v || v.length > 100) return 'Required (max 100 chars)';
41
+ },
42
+ })) as string;
43
+
44
+ if (p.isCancel(role)) return null;
45
+
46
+ const description = (await p.text({
47
+ message: 'Description',
48
+ placeholder: 'This agent helps developers with...',
49
+ validate: (v) => {
50
+ if (!v || v.length < 10 || v.length > 500) return 'Required (10-500 chars)';
51
+ },
52
+ })) as string;
53
+
54
+ if (p.isCancel(description)) return null;
55
+
56
+ const domainsRaw = (await p.text({
57
+ message: 'Domains (comma-separated, kebab-case)',
58
+ placeholder: 'api-design, security, testing',
59
+ validate: (v = '') => {
60
+ const parts = v
61
+ .split(',')
62
+ .map((s) => s.trim())
63
+ .filter(Boolean);
64
+ if (parts.length === 0) return 'At least one domain required';
65
+ for (const d of parts) {
66
+ if (!/^[a-z][a-z0-9-]*$/.test(d)) return `Invalid domain "${d}" — must be kebab-case`;
67
+ }
68
+ },
69
+ })) as string;
70
+
71
+ if (p.isCancel(domainsRaw)) return null;
72
+
73
+ const domains = domainsRaw
74
+ .split(',')
75
+ .map((s) => s.trim())
76
+ .filter(Boolean);
77
+
78
+ const principlesRaw = (await p.text({
79
+ message: 'Principles (one per line)',
80
+ placeholder: 'Security first\nSimplicity over cleverness\nTest everything',
81
+ validate: (v = '') => {
82
+ const lines = v
83
+ .split('\n')
84
+ .map((s) => s.trim())
85
+ .filter(Boolean);
86
+ if (lines.length === 0) return 'At least one principle required';
87
+ if (lines.length > 10) return 'Max 10 principles';
88
+ },
89
+ })) as string;
90
+
91
+ if (p.isCancel(principlesRaw)) return null;
92
+
93
+ const principles = principlesRaw
94
+ .split('\n')
95
+ .map((s) => s.trim())
96
+ .filter(Boolean);
97
+
98
+ const greeting = (await p.text({
99
+ message: 'Greeting message',
100
+ placeholder: `Hello! I'm ${name}, your AI assistant for...`,
101
+ validate: (v) => {
102
+ if (!v || v.length < 10 || v.length > 300) return 'Required (10-300 chars)';
103
+ },
104
+ })) as string;
105
+
106
+ if (p.isCancel(greeting)) return null;
107
+
108
+ const outputDir = (await p.text({
109
+ message: 'Output directory',
110
+ defaultValue: process.cwd(),
111
+ placeholder: process.cwd(),
112
+ validate: (v) => {
113
+ if (!v) return 'Required';
114
+ },
115
+ })) as string;
116
+
117
+ if (p.isCancel(outputDir)) return null;
118
+
119
+ return {
120
+ id,
121
+ name,
122
+ role,
123
+ description,
124
+ domains,
125
+ principles,
126
+ greeting,
127
+ outputDir,
128
+ };
129
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Detect and validate an agent project in the current working directory.
3
+ */
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { join, resolve } from 'node:path';
6
+
7
+ interface AgentContext {
8
+ agentPath: string;
9
+ agentId: string;
10
+ packageName: string;
11
+ hasBrain: boolean;
12
+ }
13
+
14
+ /**
15
+ * Detect an agent in the given directory.
16
+ * Returns null if the directory is not a valid agent project.
17
+ */
18
+ export function detectAgent(dir?: string): AgentContext | null {
19
+ const agentPath = resolve(dir ?? process.cwd());
20
+ const pkgPath = join(agentPath, 'package.json');
21
+
22
+ if (!existsSync(pkgPath)) return null;
23
+
24
+ try {
25
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
26
+ const name: string = pkg.name ?? '';
27
+ if (!name.endsWith('-mcp')) return null;
28
+
29
+ return {
30
+ agentPath,
31
+ agentId: name.replace(/-mcp$/, ''),
32
+ packageName: name,
33
+ hasBrain: existsSync(join(agentPath, 'src', 'brain')),
34
+ };
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Health check utilities for the doctor command.
3
+ */
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { homedir } from 'node:os';
8
+ import { detectAgent } from './agent-context.js';
9
+
10
+ interface CheckResult {
11
+ status: 'pass' | 'fail' | 'warn';
12
+ label: string;
13
+ detail?: string;
14
+ }
15
+
16
+ export function checkNodeVersion(): CheckResult {
17
+ const [major] = process.versions.node.split('.').map(Number);
18
+ if (major >= 18) {
19
+ return { status: 'pass', label: 'Node.js', detail: `v${process.versions.node}` };
20
+ }
21
+ return { status: 'fail', label: 'Node.js', detail: `v${process.versions.node} (>=18 required)` };
22
+ }
23
+
24
+ export function checkNpm(): CheckResult {
25
+ try {
26
+ const version = execFileSync('npm', ['--version'], { encoding: 'utf-8' }).trim();
27
+ return { status: 'pass', label: 'npm', detail: `v${version}` };
28
+ } catch {
29
+ return { status: 'fail', label: 'npm', detail: 'not found' };
30
+ }
31
+ }
32
+
33
+ function checkTsx(): CheckResult {
34
+ try {
35
+ const version = execFileSync('npx', ['tsx', '--version'], {
36
+ encoding: 'utf-8',
37
+ timeout: 10_000,
38
+ }).trim();
39
+ return { status: 'pass', label: 'tsx', detail: `v${version}` };
40
+ } catch {
41
+ return { status: 'warn', label: 'tsx', detail: 'not found — needed for soleri dev' };
42
+ }
43
+ }
44
+
45
+ export function checkAgentProject(dir?: string): CheckResult {
46
+ const ctx = detectAgent(dir);
47
+ if (!ctx) {
48
+ return { status: 'warn', label: 'Agent project', detail: 'not detected in current directory' };
49
+ }
50
+ return { status: 'pass', label: 'Agent project', detail: `${ctx.agentId} (${ctx.packageName})` };
51
+ }
52
+
53
+ export function checkAgentBuild(dir?: string): CheckResult {
54
+ const ctx = detectAgent(dir);
55
+ if (!ctx) return { status: 'warn', label: 'Agent build', detail: 'no agent detected' };
56
+
57
+ if (!existsSync(join(ctx.agentPath, 'dist'))) {
58
+ return { status: 'fail', label: 'Agent build', detail: 'dist/ not found — run npm run build' };
59
+ }
60
+ if (!existsSync(join(ctx.agentPath, 'dist', 'index.js'))) {
61
+ return {
62
+ status: 'fail',
63
+ label: 'Agent build',
64
+ detail: 'dist/index.js not found — run npm run build',
65
+ };
66
+ }
67
+ return { status: 'pass', label: 'Agent build', detail: 'dist/index.js exists' };
68
+ }
69
+
70
+ export function checkNodeModules(dir?: string): CheckResult {
71
+ const ctx = detectAgent(dir);
72
+ if (!ctx) return { status: 'warn', label: 'Dependencies', detail: 'no agent detected' };
73
+
74
+ if (!existsSync(join(ctx.agentPath, 'node_modules'))) {
75
+ return {
76
+ status: 'fail',
77
+ label: 'Dependencies',
78
+ detail: 'node_modules/ not found — run npm install',
79
+ };
80
+ }
81
+ return { status: 'pass', label: 'Dependencies', detail: 'node_modules/ exists' };
82
+ }
83
+
84
+ function checkMcpRegistration(dir?: string): CheckResult {
85
+ const ctx = detectAgent(dir);
86
+ if (!ctx) return { status: 'warn', label: 'MCP registration', detail: 'no agent detected' };
87
+
88
+ const claudeJsonPath = join(homedir(), '.claude.json');
89
+ if (!existsSync(claudeJsonPath)) {
90
+ return {
91
+ status: 'warn',
92
+ label: 'MCP registration',
93
+ detail: '~/.claude.json not found',
94
+ };
95
+ }
96
+
97
+ try {
98
+ const config = JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));
99
+ const servers = config.mcpServers ?? {};
100
+ if (ctx.agentId in servers) {
101
+ return {
102
+ status: 'pass',
103
+ label: 'MCP registration',
104
+ detail: `registered as "${ctx.agentId}"`,
105
+ };
106
+ }
107
+ return {
108
+ status: 'warn',
109
+ label: 'MCP registration',
110
+ detail: `"${ctx.agentId}" not found in ~/.claude.json`,
111
+ };
112
+ } catch {
113
+ return { status: 'fail', label: 'MCP registration', detail: 'failed to parse ~/.claude.json' };
114
+ }
115
+ }
116
+
117
+ function checkCognee(): CheckResult {
118
+ const url = process.env.COGNEE_URL ?? 'http://localhost:8000/';
119
+ let host: string;
120
+ try {
121
+ host = new URL(url).host;
122
+ } catch {
123
+ return { status: 'warn', label: 'Cognee', detail: `invalid COGNEE_URL: ${url}` };
124
+ }
125
+ try {
126
+ execFileSync('curl', ['-fsS', '--max-time', '5', url], { stdio: 'ignore', timeout: 7_000 });
127
+ return { status: 'pass', label: 'Cognee', detail: `available at ${host}` };
128
+ } catch {
129
+ return {
130
+ status: 'warn',
131
+ label: 'Cognee',
132
+ detail: `not running at ${host} — vector search disabled (FTS5 still works)`,
133
+ };
134
+ }
135
+ }
136
+
137
+ export function runAllChecks(dir?: string): CheckResult[] {
138
+ return [
139
+ checkNodeVersion(),
140
+ checkNpm(),
141
+ checkTsx(),
142
+ checkAgentProject(dir),
143
+ checkNodeModules(dir),
144
+ checkAgentBuild(dir),
145
+ checkMcpRegistration(dir),
146
+ checkCognee(),
147
+ ];
148
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Simple colored output helpers for the CLI.
3
+ * Uses ANSI codes directly — no chalk dependency needed.
4
+ */
5
+
6
+ const RESET = '\x1b[0m';
7
+ const GREEN = '\x1b[32m';
8
+ const RED = '\x1b[31m';
9
+ const YELLOW = '\x1b[33m';
10
+ const CYAN = '\x1b[36m';
11
+ const DIM = '\x1b[2m';
12
+ const BOLD = '\x1b[1m';
13
+
14
+ export function pass(label: string, detail?: string): void {
15
+ const suffix = detail ? ` ${DIM}${detail}${RESET}` : '';
16
+ console.log(` ${GREEN}✓${RESET} ${label}${suffix}`);
17
+ }
18
+
19
+ export function fail(label: string, detail?: string): void {
20
+ const suffix = detail ? ` ${DIM}${detail}${RESET}` : '';
21
+ console.log(` ${RED}✗${RESET} ${label}${suffix}`);
22
+ }
23
+
24
+ export function warn(label: string, detail?: string): void {
25
+ const suffix = detail ? ` ${DIM}${detail}${RESET}` : '';
26
+ console.log(` ${YELLOW}!${RESET} ${label}${suffix}`);
27
+ }
28
+
29
+ export function info(message: string): void {
30
+ console.log(` ${CYAN}ℹ${RESET} ${message}`);
31
+ }
32
+
33
+ export function heading(title: string): void {
34
+ console.log(`\n${BOLD}${title}${RESET}\n`);
35
+ }
36
+
37
+ export function dim(message: string): void {
38
+ console.log(` ${DIM}${message}${RESET}`);
39
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "sourceMap": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "skipLibCheck": true,
16
+ "noEmitOnError": true,
17
+ "composite": true
18
+ },
19
+ "include": ["src/**/*.ts"],
20
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/__tests__/**"]
21
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/__tests__/**/*.test.ts'],
6
+ testTimeout: 15_000,
7
+ },
8
+ });