@memnexus-ai/cli 0.1.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/.env.example +13 -0
- package/.eslintrc.js +24 -0
- package/.github/ISSUE_TEMPLATE/phase-1-foundation.md +1078 -0
- package/.github/workflows/publish.yml +277 -0
- package/.github/workflows/test-app-token.yml +54 -0
- package/.npmrc.backup +3 -0
- package/.npmrc.example +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +138 -0
- package/PLATFORM_TESTING.md +243 -0
- package/README.md +986 -0
- package/RELEASE.md +428 -0
- package/RELEASE_READINESS.md +253 -0
- package/USAGE.md +1373 -0
- package/bin/mx.js +2 -0
- package/dist/commands/apikeys.d.ts +7 -0
- package/dist/commands/apikeys.d.ts.map +1 -0
- package/dist/commands/apikeys.js +133 -0
- package/dist/commands/apikeys.js.map +1 -0
- package/dist/commands/artifacts.d.ts +7 -0
- package/dist/commands/artifacts.d.ts.map +1 -0
- package/dist/commands/artifacts.js +277 -0
- package/dist/commands/artifacts.js.map +1 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +119 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/communities.d.ts +7 -0
- package/dist/commands/communities.d.ts.map +1 -0
- package/dist/commands/communities.js +137 -0
- package/dist/commands/communities.js.map +1 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +138 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/conversations.d.ts +7 -0
- package/dist/commands/conversations.d.ts.map +1 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/conversations.js.map +1 -0
- package/dist/commands/facts.d.ts +7 -0
- package/dist/commands/facts.d.ts.map +1 -0
- package/dist/commands/facts.js +298 -0
- package/dist/commands/facts.js.map +1 -0
- package/dist/commands/graphrag.d.ts +7 -0
- package/dist/commands/graphrag.d.ts.map +1 -0
- package/dist/commands/graphrag.js +139 -0
- package/dist/commands/graphrag.js.map +1 -0
- package/dist/commands/memories.d.ts +7 -0
- package/dist/commands/memories.d.ts.map +1 -0
- package/dist/commands/memories.js +304 -0
- package/dist/commands/memories.js.map +1 -0
- package/dist/commands/patterns.d.ts +7 -0
- package/dist/commands/patterns.d.ts.map +1 -0
- package/dist/commands/patterns.js +227 -0
- package/dist/commands/patterns.js.map +1 -0
- package/dist/commands/system.d.ts +7 -0
- package/dist/commands/system.d.ts.map +1 -0
- package/dist/commands/system.js +97 -0
- package/dist/commands/system.js.map +1 -0
- package/dist/commands/topics.d.ts +7 -0
- package/dist/commands/topics.d.ts.map +1 -0
- package/dist/commands/topics.js +314 -0
- package/dist/commands/topics.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +29 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +64 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/auth.d.ts +10 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +47 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/config.d.ts +19 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +59 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/errors.d.ts +7 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +133 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/formatters.d.ts +12 -0
- package/dist/lib/formatters.d.ts.map +1 -0
- package/dist/lib/formatters.js +103 -0
- package/dist/lib/formatters.js.map +1 -0
- package/dist/lib/spinner.d.ts +54 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +108 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/lib/validators.d.ts +92 -0
- package/dist/lib/validators.d.ts.map +1 -0
- package/dist/lib/validators.js +257 -0
- package/dist/lib/validators.js.map +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/docs/README.md +219 -0
- package/docs/code-generation-strategy.md +560 -0
- package/docs/prd.md +748 -0
- package/docs/sync-strategy.md +533 -0
- package/jest.config.js +30 -0
- package/package.json +67 -0
- package/scripts/install-deps.sh +38 -0
- package/src/commands/apikeys.ts +144 -0
- package/src/commands/artifacts.ts +296 -0
- package/src/commands/auth.ts +122 -0
- package/src/commands/communities.ts +153 -0
- package/src/commands/config.ts +144 -0
- package/src/commands/conversations.ts +176 -0
- package/src/commands/facts.ts +320 -0
- package/src/commands/graphrag.ts +149 -0
- package/src/commands/memories.ts +332 -0
- package/src/commands/patterns.ts +251 -0
- package/src/commands/system.ts +102 -0
- package/src/commands/topics.ts +354 -0
- package/src/index.ts +43 -0
- package/src/lib/api-client.ts +68 -0
- package/src/lib/auth.ts +42 -0
- package/src/lib/config.ts +68 -0
- package/src/lib/errors.ts +143 -0
- package/src/lib/formatters.ts +123 -0
- package/src/lib/spinner.ts +113 -0
- package/src/lib/validators.ts +302 -0
- package/src/types/index.ts +17 -0
- package/tests/__mocks__/chalk.ts +16 -0
- package/tests/__mocks__/cli-table3.ts +37 -0
- package/tests/__mocks__/configstore.ts +38 -0
- package/tests/commands/apikeys.test.ts +179 -0
- package/tests/commands/artifacts.test.ts +194 -0
- package/tests/commands/auth.test.ts +120 -0
- package/tests/commands/communities.test.ts +154 -0
- package/tests/commands/config.test.ts +154 -0
- package/tests/commands/conversations.test.ts +136 -0
- package/tests/commands/facts.test.ts +210 -0
- package/tests/commands/graphrag.test.ts +194 -0
- package/tests/commands/memories.test.ts +215 -0
- package/tests/commands/patterns.test.ts +201 -0
- package/tests/commands/system.test.ts +172 -0
- package/tests/commands/topics.test.ts +274 -0
- package/tests/lib/auth.test.ts +77 -0
- package/tests/lib/config.test.ts +50 -0
- package/tests/lib/errors.test.ts +126 -0
- package/tests/lib/formatters.test.ts +87 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Phase 1 - Foundation (Week 1)
|
|
3
|
+
about: Set up project infrastructure and core utilities for mx-cli v1.0
|
|
4
|
+
title: 'Phase 1: Foundation - Project Infrastructure & Core Utilities'
|
|
5
|
+
labels: 'enhancement, infrastructure, phase-1'
|
|
6
|
+
assignees: ''
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Phase 1: Foundation (Week 1)
|
|
10
|
+
|
|
11
|
+
## Goal
|
|
12
|
+
Set up project infrastructure and core utilities to enable rapid CLI command development in subsequent phases.
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
This phase establishes the foundational architecture for the MemNexus CLI. By the end of this phase, we'll have a working TypeScript project structure with authentication, configuration management, and output formatting ready for command implementations.
|
|
16
|
+
|
|
17
|
+
## Success Criteria
|
|
18
|
+
- ✅ TypeScript project compiles without errors
|
|
19
|
+
- ✅ Can install `@memnexus-ai/contracts` and use types
|
|
20
|
+
- ✅ Configuration can be saved/loaded from disk
|
|
21
|
+
- ✅ API client connects to mx-core-api successfully
|
|
22
|
+
- ✅ Output formatters work for JSON, table, and YAML
|
|
23
|
+
- ✅ All utility tests pass with >80% coverage
|
|
24
|
+
- ✅ ESLint and Prettier configured and passing
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Tasks
|
|
29
|
+
|
|
30
|
+
### 1. Initialize TypeScript Project
|
|
31
|
+
**Estimated Time**: 1-2 hours
|
|
32
|
+
|
|
33
|
+
**Description**: Set up the basic Node.js + TypeScript project structure
|
|
34
|
+
|
|
35
|
+
**Steps**:
|
|
36
|
+
```bash
|
|
37
|
+
cd /workspaces/memnexus/mx-cli
|
|
38
|
+
|
|
39
|
+
# Initialize package.json
|
|
40
|
+
npm init -y
|
|
41
|
+
|
|
42
|
+
# Install TypeScript and core dependencies
|
|
43
|
+
npm install --save-dev typescript @types/node ts-node
|
|
44
|
+
|
|
45
|
+
# Install production dependencies
|
|
46
|
+
npm install commander chalk ora cli-table3 inquirer configstore dotenv
|
|
47
|
+
|
|
48
|
+
# Install type definitions
|
|
49
|
+
npm install --save-dev @types/inquirer
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Create `package.json`**:
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "@memnexus-ai/cli",
|
|
56
|
+
"version": "0.1.0",
|
|
57
|
+
"description": "Command-line interface for MemNexus Core API",
|
|
58
|
+
"main": "dist/index.js",
|
|
59
|
+
"bin": {
|
|
60
|
+
"mx": "./bin/mx.js"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsc",
|
|
64
|
+
"dev": "ts-node src/index.ts",
|
|
65
|
+
"test": "jest",
|
|
66
|
+
"test:watch": "jest --watch",
|
|
67
|
+
"test:coverage": "jest --coverage",
|
|
68
|
+
"lint": "eslint 'src/**/*.ts'",
|
|
69
|
+
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
|
70
|
+
"format": "prettier --write 'src/**/*.ts'",
|
|
71
|
+
"prepublishOnly": "npm run build"
|
|
72
|
+
},
|
|
73
|
+
"keywords": ["memnexus", "cli", "api", "memory", "ai"],
|
|
74
|
+
"author": "MemNexus Team",
|
|
75
|
+
"license": "MIT",
|
|
76
|
+
"repository": {
|
|
77
|
+
"type": "git",
|
|
78
|
+
"url": "https://github.com/memnexus-ai/memnexus.git",
|
|
79
|
+
"directory": "mx-cli"
|
|
80
|
+
},
|
|
81
|
+
"publishConfig": {
|
|
82
|
+
"registry": "https://npm.pkg.github.com",
|
|
83
|
+
"@memnexus-ai:registry": "https://npm.pkg.github.com"
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=18.0.0"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Create `tsconfig.json`**:
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"compilerOptions": {
|
|
95
|
+
"target": "ES2022",
|
|
96
|
+
"module": "commonjs",
|
|
97
|
+
"lib": ["ES2022"],
|
|
98
|
+
"outDir": "./dist",
|
|
99
|
+
"rootDir": "./src",
|
|
100
|
+
"strict": true,
|
|
101
|
+
"esModuleInterop": true,
|
|
102
|
+
"skipLibCheck": true,
|
|
103
|
+
"forceConsistentCasingInFileNames": true,
|
|
104
|
+
"resolveJsonModule": true,
|
|
105
|
+
"declaration": true,
|
|
106
|
+
"declarationMap": true,
|
|
107
|
+
"sourceMap": true,
|
|
108
|
+
"moduleResolution": "node"
|
|
109
|
+
},
|
|
110
|
+
"include": ["src/**/*"],
|
|
111
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Acceptance Criteria**:
|
|
116
|
+
- [x] `npm run build` compiles successfully
|
|
117
|
+
- [x] TypeScript strict mode enabled
|
|
118
|
+
- [x] Source maps generated for debugging
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### 2. Add @memnexus-ai/contracts Dependency
|
|
123
|
+
**Estimated Time**: 30 minutes
|
|
124
|
+
|
|
125
|
+
**Description**: Install and configure the contracts package for API client access
|
|
126
|
+
|
|
127
|
+
**Steps**:
|
|
128
|
+
```bash
|
|
129
|
+
# Configure npm to use GitHub Packages
|
|
130
|
+
echo "@memnexus-ai:registry=https://npm.pkg.github.com" >> .npmrc
|
|
131
|
+
|
|
132
|
+
# Install contracts package (requires GitHub authentication)
|
|
133
|
+
npm install @memnexus-ai/contracts@^1.0.4
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Create `.npmrc`**:
|
|
137
|
+
```
|
|
138
|
+
@memnexus-ai:registry=https://npm.pkg.github.com
|
|
139
|
+
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Test import**:
|
|
143
|
+
```typescript
|
|
144
|
+
// src/lib/api-client.ts
|
|
145
|
+
import { CoreApiClient, createClient } from '@memnexus-ai/contracts';
|
|
146
|
+
|
|
147
|
+
// Verify types are available
|
|
148
|
+
const client: CoreApiClient = createClient({
|
|
149
|
+
baseURL: 'http://localhost:3000',
|
|
150
|
+
apiKey: 'test'
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Acceptance Criteria**:
|
|
155
|
+
- [x] Package installs without errors
|
|
156
|
+
- [x] TypeScript recognizes all imports
|
|
157
|
+
- [x] Types from contracts package are available
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### 3. Configure TypeScript, ESLint, Prettier
|
|
162
|
+
**Estimated Time**: 1-2 hours
|
|
163
|
+
|
|
164
|
+
**Description**: Set up code quality tools for consistent style and error prevention
|
|
165
|
+
|
|
166
|
+
**Install Dev Dependencies**:
|
|
167
|
+
```bash
|
|
168
|
+
npm install --save-dev \
|
|
169
|
+
eslint \
|
|
170
|
+
@typescript-eslint/parser \
|
|
171
|
+
@typescript-eslint/eslint-plugin \
|
|
172
|
+
prettier \
|
|
173
|
+
eslint-config-prettier \
|
|
174
|
+
eslint-plugin-prettier
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Create `.eslintrc.js`**:
|
|
178
|
+
```javascript
|
|
179
|
+
module.exports = {
|
|
180
|
+
parser: '@typescript-eslint/parser',
|
|
181
|
+
extends: [
|
|
182
|
+
'eslint:recommended',
|
|
183
|
+
'plugin:@typescript-eslint/recommended',
|
|
184
|
+
'prettier'
|
|
185
|
+
],
|
|
186
|
+
plugins: ['@typescript-eslint', 'prettier'],
|
|
187
|
+
parserOptions: {
|
|
188
|
+
ecmaVersion: 2022,
|
|
189
|
+
sourceType: 'module',
|
|
190
|
+
project: './tsconfig.json'
|
|
191
|
+
},
|
|
192
|
+
rules: {
|
|
193
|
+
'prettier/prettier': 'error',
|
|
194
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
195
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
196
|
+
'@typescript-eslint/no-explicit-any': 'warn'
|
|
197
|
+
},
|
|
198
|
+
env: {
|
|
199
|
+
node: true,
|
|
200
|
+
es2022: true
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Create `.prettierrc`**:
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"semi": true,
|
|
209
|
+
"trailingComma": "es5",
|
|
210
|
+
"singleQuote": true,
|
|
211
|
+
"printWidth": 100,
|
|
212
|
+
"tabWidth": 2,
|
|
213
|
+
"arrowParens": "always"
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Create `.prettierignore`**:
|
|
218
|
+
```
|
|
219
|
+
node_modules
|
|
220
|
+
dist
|
|
221
|
+
coverage
|
|
222
|
+
*.md
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Acceptance Criteria**:
|
|
226
|
+
- [x] `npm run lint` passes
|
|
227
|
+
- [x] `npm run format` formats all files
|
|
228
|
+
- [x] ESLint catches type errors and style issues
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### 4. Set Up Project Structure
|
|
233
|
+
**Estimated Time**: 30 minutes
|
|
234
|
+
|
|
235
|
+
**Description**: Create the directory structure for organized code
|
|
236
|
+
|
|
237
|
+
**Create directories**:
|
|
238
|
+
```bash
|
|
239
|
+
mkdir -p src/{commands,lib,types}
|
|
240
|
+
mkdir -p tests/{commands,lib}
|
|
241
|
+
mkdir -p bin
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Create initial files**:
|
|
245
|
+
|
|
246
|
+
`src/index.ts`:
|
|
247
|
+
```typescript
|
|
248
|
+
#!/usr/bin/env node
|
|
249
|
+
import { Command } from 'commander';
|
|
250
|
+
import packageJson from '../package.json';
|
|
251
|
+
|
|
252
|
+
const program = new Command();
|
|
253
|
+
|
|
254
|
+
program
|
|
255
|
+
.name('mx')
|
|
256
|
+
.description('MemNexus CLI - Command-line interface for MemNexus Core API')
|
|
257
|
+
.version(packageJson.version);
|
|
258
|
+
|
|
259
|
+
// Commands will be registered here
|
|
260
|
+
|
|
261
|
+
program.parse(process.argv);
|
|
262
|
+
|
|
263
|
+
// Show help if no command provided
|
|
264
|
+
if (!process.argv.slice(2).length) {
|
|
265
|
+
program.outputHelp();
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`bin/mx.js`:
|
|
270
|
+
```javascript
|
|
271
|
+
#!/usr/bin/env node
|
|
272
|
+
require('../dist/index.js');
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`src/types/index.ts`:
|
|
276
|
+
```typescript
|
|
277
|
+
// Re-export types from contracts
|
|
278
|
+
export type * from '@memnexus-ai/contracts';
|
|
279
|
+
|
|
280
|
+
// CLI-specific types
|
|
281
|
+
export interface CLIConfig {
|
|
282
|
+
apiUrl: string;
|
|
283
|
+
apiKey?: string;
|
|
284
|
+
defaultFormat: 'json' | 'table' | 'yaml';
|
|
285
|
+
defaultPageSize: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface CommandOptions {
|
|
289
|
+
format?: 'json' | 'table' | 'yaml';
|
|
290
|
+
verbose?: boolean;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Acceptance Criteria**:
|
|
295
|
+
- [x] All directories created
|
|
296
|
+
- [x] CLI entry point exists
|
|
297
|
+
- [x] Can run `npm run dev` without errors
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### 5. Implement Configuration Management
|
|
302
|
+
**Estimated Time**: 2-3 hours
|
|
303
|
+
|
|
304
|
+
**Description**: Create config storage using configstore for persistent settings
|
|
305
|
+
|
|
306
|
+
**Create `src/lib/config.ts`**:
|
|
307
|
+
```typescript
|
|
308
|
+
import Configstore from 'configstore';
|
|
309
|
+
import { CLIConfig } from '../types';
|
|
310
|
+
import packageJson from '../../package.json';
|
|
311
|
+
|
|
312
|
+
const DEFAULT_CONFIG: CLIConfig = {
|
|
313
|
+
apiUrl: 'https://api.memnexus.io',
|
|
314
|
+
defaultFormat: 'table',
|
|
315
|
+
defaultPageSize: 20,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
class ConfigManager {
|
|
319
|
+
private store: Configstore;
|
|
320
|
+
|
|
321
|
+
constructor() {
|
|
322
|
+
this.store = new Configstore(packageJson.name, DEFAULT_CONFIG);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
get(key?: keyof CLIConfig): CLIConfig | string | number | undefined {
|
|
326
|
+
if (key) {
|
|
327
|
+
return this.store.get(key);
|
|
328
|
+
}
|
|
329
|
+
return this.store.all as CLIConfig;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
set(key: keyof CLIConfig, value: string | number): void {
|
|
333
|
+
this.store.set(key, value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
delete(key: keyof CLIConfig): void {
|
|
337
|
+
this.store.delete(key);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
clear(): void {
|
|
341
|
+
this.store.clear();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
reset(): void {
|
|
345
|
+
this.store.all = DEFAULT_CONFIG;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
getApiUrl(): string {
|
|
349
|
+
return process.env.MX_API_URL || this.store.get('apiUrl') || DEFAULT_CONFIG.apiUrl;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
getApiKey(): string | undefined {
|
|
353
|
+
return process.env.MX_API_KEY || this.store.get('apiKey');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
getFormat(): 'json' | 'table' | 'yaml' {
|
|
357
|
+
return (
|
|
358
|
+
(process.env.MX_OUTPUT_FORMAT as 'json' | 'table' | 'yaml') ||
|
|
359
|
+
this.store.get('defaultFormat') ||
|
|
360
|
+
DEFAULT_CONFIG.defaultFormat
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
setApiKey(apiKey: string): void {
|
|
365
|
+
// TODO: Encrypt API key before storing
|
|
366
|
+
this.store.set('apiKey', apiKey);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
hasApiKey(): boolean {
|
|
370
|
+
return !!(process.env.MX_API_KEY || this.store.get('apiKey'));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export const config = new ConfigManager();
|
|
375
|
+
export const getConfig = (): CLIConfig => config.get() as CLIConfig;
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Create tests `tests/lib/config.test.ts`**:
|
|
379
|
+
```typescript
|
|
380
|
+
import { config } from '../../src/lib/config';
|
|
381
|
+
|
|
382
|
+
describe('ConfigManager', () => {
|
|
383
|
+
beforeEach(() => {
|
|
384
|
+
config.reset();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should return default config', () => {
|
|
388
|
+
const cfg = config.get();
|
|
389
|
+
expect(cfg).toHaveProperty('apiUrl');
|
|
390
|
+
expect(cfg).toHaveProperty('defaultFormat');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should set and get values', () => {
|
|
394
|
+
config.set('apiUrl', 'http://localhost:3000');
|
|
395
|
+
expect(config.get('apiUrl')).toBe('http://localhost:3000');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should respect environment variables', () => {
|
|
399
|
+
process.env.MX_API_URL = 'http://env-url:3000';
|
|
400
|
+
expect(config.getApiUrl()).toBe('http://env-url:3000');
|
|
401
|
+
delete process.env.MX_API_URL;
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should reset to defaults', () => {
|
|
405
|
+
config.set('apiUrl', 'http://custom:3000');
|
|
406
|
+
config.reset();
|
|
407
|
+
expect(config.get('apiUrl')).toBe('https://api.memnexus.io');
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Acceptance Criteria**:
|
|
413
|
+
- [x] Config persists between CLI runs
|
|
414
|
+
- [x] Environment variables override config
|
|
415
|
+
- [x] Can reset to defaults
|
|
416
|
+
- [x] Tests pass
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
### 6. Create API Client Wrapper
|
|
421
|
+
**Estimated Time**: 1-2 hours
|
|
422
|
+
|
|
423
|
+
**Description**: Wrapper around @memnexus-ai/contracts with config integration
|
|
424
|
+
|
|
425
|
+
**Create `src/lib/api-client.ts`**:
|
|
426
|
+
```typescript
|
|
427
|
+
import { CoreApiClient, createClient } from '@memnexus-ai/contracts';
|
|
428
|
+
import { config } from './config';
|
|
429
|
+
import chalk from 'chalk';
|
|
430
|
+
|
|
431
|
+
let cachedClient: CoreApiClient | null = null;
|
|
432
|
+
|
|
433
|
+
export function getApiClient(): CoreApiClient {
|
|
434
|
+
if (cachedClient) {
|
|
435
|
+
return cachedClient;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const apiKey = config.getApiKey();
|
|
439
|
+
if (!apiKey) {
|
|
440
|
+
console.error(chalk.red('Error: No API key configured.'));
|
|
441
|
+
console.log(chalk.yellow('Run: mx auth login --api-key <your-key>'));
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const apiUrl = config.getApiUrl();
|
|
446
|
+
|
|
447
|
+
cachedClient = createClient({
|
|
448
|
+
baseURL: apiUrl,
|
|
449
|
+
apiKey: apiKey,
|
|
450
|
+
timeout: 30000,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return cachedClient;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function resetApiClient(): void {
|
|
457
|
+
cachedClient = null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export { CoreApiClient };
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Create tests `tests/lib/api-client.test.ts`**:
|
|
464
|
+
```typescript
|
|
465
|
+
import { getApiClient, resetApiClient } from '../../src/lib/api-client';
|
|
466
|
+
import { config } from '../../src/lib/config';
|
|
467
|
+
|
|
468
|
+
describe('API Client', () => {
|
|
469
|
+
beforeEach(() => {
|
|
470
|
+
resetApiClient();
|
|
471
|
+
config.reset();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should create client with config', () => {
|
|
475
|
+
config.setApiKey('test-key');
|
|
476
|
+
const client = getApiClient();
|
|
477
|
+
expect(client).toBeDefined();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should exit if no API key', () => {
|
|
481
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation();
|
|
482
|
+
getApiClient();
|
|
483
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
484
|
+
exitSpy.mockRestore();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should cache client instance', () => {
|
|
488
|
+
config.setApiKey('test-key');
|
|
489
|
+
const client1 = getApiClient();
|
|
490
|
+
const client2 = getApiClient();
|
|
491
|
+
expect(client1).toBe(client2);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**Acceptance Criteria**:
|
|
497
|
+
- [x] Client initializes with config values
|
|
498
|
+
- [x] Error shown if API key missing
|
|
499
|
+
- [x] Client is cached (singleton pattern)
|
|
500
|
+
- [x] Tests pass
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
### 7. Create Authentication Helpers
|
|
505
|
+
**Estimated Time**: 1-2 hours
|
|
506
|
+
|
|
507
|
+
**Description**: Helper functions for API key storage and validation
|
|
508
|
+
|
|
509
|
+
**Create `src/lib/auth.ts`**:
|
|
510
|
+
```typescript
|
|
511
|
+
import { config } from './config';
|
|
512
|
+
import chalk from 'chalk';
|
|
513
|
+
|
|
514
|
+
export function storeApiKey(apiKey: string): void {
|
|
515
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
516
|
+
throw new Error('API key cannot be empty');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// TODO: Add encryption before storing
|
|
520
|
+
config.setApiKey(apiKey);
|
|
521
|
+
console.log(chalk.green('✓ API key saved successfully'));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function removeApiKey(): void {
|
|
525
|
+
config.delete('apiKey');
|
|
526
|
+
console.log(chalk.green('✓ API key removed'));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function hasApiKey(): boolean {
|
|
530
|
+
return config.hasApiKey();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function getApiKeyStatus(): { configured: boolean; source: 'env' | 'config' | 'none' } {
|
|
534
|
+
if (process.env.MX_API_KEY) {
|
|
535
|
+
return { configured: true, source: 'env' };
|
|
536
|
+
}
|
|
537
|
+
if (config.get('apiKey')) {
|
|
538
|
+
return { configured: true, source: 'config' };
|
|
539
|
+
}
|
|
540
|
+
return { configured: false, source: 'none' };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// TODO: Implement encryption/decryption
|
|
544
|
+
export function encryptApiKey(apiKey: string): string {
|
|
545
|
+
// Placeholder - implement proper encryption
|
|
546
|
+
return apiKey;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function decryptApiKey(encryptedKey: string): string {
|
|
550
|
+
// Placeholder - implement proper decryption
|
|
551
|
+
return encryptedKey;
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Create tests `tests/lib/auth.test.ts`**:
|
|
556
|
+
```typescript
|
|
557
|
+
import { storeApiKey, removeApiKey, hasApiKey, getApiKeyStatus } from '../../src/lib/auth';
|
|
558
|
+
import { config } from '../../src/lib/config';
|
|
559
|
+
|
|
560
|
+
describe('Authentication Helpers', () => {
|
|
561
|
+
beforeEach(() => {
|
|
562
|
+
config.reset();
|
|
563
|
+
delete process.env.MX_API_KEY;
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should store API key', () => {
|
|
567
|
+
storeApiKey('test-key-123');
|
|
568
|
+
expect(hasApiKey()).toBe(true);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should reject empty API key', () => {
|
|
572
|
+
expect(() => storeApiKey('')).toThrow();
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('should remove API key', () => {
|
|
576
|
+
storeApiKey('test-key');
|
|
577
|
+
removeApiKey();
|
|
578
|
+
expect(hasApiKey()).toBe(false);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should detect API key source', () => {
|
|
582
|
+
expect(getApiKeyStatus().source).toBe('none');
|
|
583
|
+
|
|
584
|
+
storeApiKey('config-key');
|
|
585
|
+
expect(getApiKeyStatus().source).toBe('config');
|
|
586
|
+
|
|
587
|
+
process.env.MX_API_KEY = 'env-key';
|
|
588
|
+
expect(getApiKeyStatus().source).toBe('env');
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
**Acceptance Criteria**:
|
|
594
|
+
- [x] Can store/remove API keys
|
|
595
|
+
- [x] Status detection works correctly
|
|
596
|
+
- [x] Tests pass
|
|
597
|
+
- [x] TODO comments for encryption
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
### 8. Set Up Error Handling Framework
|
|
602
|
+
**Estimated Time**: 2-3 hours
|
|
603
|
+
|
|
604
|
+
**Description**: Centralized error handling with user-friendly messages
|
|
605
|
+
|
|
606
|
+
**Create `src/lib/errors.ts`**:
|
|
607
|
+
```typescript
|
|
608
|
+
import chalk from 'chalk';
|
|
609
|
+
import { AxiosError } from 'axios';
|
|
610
|
+
|
|
611
|
+
export class CLIError extends Error {
|
|
612
|
+
constructor(
|
|
613
|
+
message: string,
|
|
614
|
+
public exitCode: number = 1,
|
|
615
|
+
public details?: unknown
|
|
616
|
+
) {
|
|
617
|
+
super(message);
|
|
618
|
+
this.name = 'CLIError';
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function handleError(error: unknown): never {
|
|
623
|
+
if (error instanceof CLIError) {
|
|
624
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
625
|
+
if (error.details && process.env.DEBUG) {
|
|
626
|
+
console.error(chalk.gray(JSON.stringify(error.details, null, 2)));
|
|
627
|
+
}
|
|
628
|
+
process.exit(error.exitCode);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (isAxiosError(error)) {
|
|
632
|
+
handleApiError(error);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Unknown error
|
|
636
|
+
console.error(chalk.red('An unexpected error occurred'));
|
|
637
|
+
if (error instanceof Error) {
|
|
638
|
+
console.error(chalk.gray(error.message));
|
|
639
|
+
if (process.env.DEBUG && error.stack) {
|
|
640
|
+
console.error(chalk.gray(error.stack));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function isAxiosError(error: unknown): error is AxiosError {
|
|
647
|
+
return (error as AxiosError).isAxiosError === true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function handleApiError(error: AxiosError): never {
|
|
651
|
+
if (!error.response) {
|
|
652
|
+
console.error(chalk.red('Network Error: Unable to connect to API'));
|
|
653
|
+
console.log(chalk.yellow('Check your internet connection and API URL'));
|
|
654
|
+
process.exit(4);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const status = error.response.status;
|
|
658
|
+
const data = error.response.data as { error?: string; message?: string };
|
|
659
|
+
|
|
660
|
+
switch (status) {
|
|
661
|
+
case 401:
|
|
662
|
+
console.error(chalk.red('Authentication Error: Invalid or missing API key'));
|
|
663
|
+
console.log(chalk.yellow('Run: mx auth login --api-key <your-key>'));
|
|
664
|
+
process.exit(3);
|
|
665
|
+
|
|
666
|
+
case 403:
|
|
667
|
+
console.error(chalk.red('Authorization Error: Insufficient permissions'));
|
|
668
|
+
process.exit(3);
|
|
669
|
+
|
|
670
|
+
case 404:
|
|
671
|
+
console.error(chalk.red('Not Found: Resource does not exist'));
|
|
672
|
+
process.exit(1);
|
|
673
|
+
|
|
674
|
+
case 422:
|
|
675
|
+
console.error(chalk.red('Validation Error: Invalid input'));
|
|
676
|
+
if (data.message) {
|
|
677
|
+
console.error(chalk.yellow(data.message));
|
|
678
|
+
}
|
|
679
|
+
process.exit(2);
|
|
680
|
+
|
|
681
|
+
case 429:
|
|
682
|
+
console.error(chalk.red('Rate Limit Exceeded: Too many requests'));
|
|
683
|
+
console.log(chalk.yellow('Please wait a moment and try again'));
|
|
684
|
+
process.exit(5);
|
|
685
|
+
|
|
686
|
+
default:
|
|
687
|
+
console.error(chalk.red(`API Error: ${status} ${error.response.statusText}`));
|
|
688
|
+
if (data.error || data.message) {
|
|
689
|
+
console.error(chalk.yellow(data.error || data.message));
|
|
690
|
+
}
|
|
691
|
+
process.exit(5);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**Create tests `tests/lib/errors.test.ts`**:
|
|
697
|
+
```typescript
|
|
698
|
+
import { CLIError, handleError } from '../../src/lib/errors';
|
|
699
|
+
import { AxiosError } from 'axios';
|
|
700
|
+
|
|
701
|
+
describe('Error Handling', () => {
|
|
702
|
+
let exitSpy: jest.SpyInstance;
|
|
703
|
+
let consoleErrorSpy: jest.SpyInstance;
|
|
704
|
+
|
|
705
|
+
beforeEach(() => {
|
|
706
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation();
|
|
707
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
afterEach(() => {
|
|
711
|
+
exitSpy.mockRestore();
|
|
712
|
+
consoleErrorSpy.mockRestore();
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('should handle CLIError', () => {
|
|
716
|
+
const error = new CLIError('Test error', 2);
|
|
717
|
+
handleError(error);
|
|
718
|
+
expect(exitSpy).toHaveBeenCalledWith(2);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('should handle 401 API errors', () => {
|
|
722
|
+
const error: AxiosError = {
|
|
723
|
+
isAxiosError: true,
|
|
724
|
+
response: { status: 401, statusText: 'Unauthorized', data: {} },
|
|
725
|
+
} as AxiosError;
|
|
726
|
+
|
|
727
|
+
handleError(error);
|
|
728
|
+
expect(exitSpy).toHaveBeenCalledWith(3);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('should handle network errors', () => {
|
|
732
|
+
const error: AxiosError = {
|
|
733
|
+
isAxiosError: true,
|
|
734
|
+
response: undefined,
|
|
735
|
+
} as AxiosError;
|
|
736
|
+
|
|
737
|
+
handleError(error);
|
|
738
|
+
expect(exitSpy).toHaveBeenCalledWith(4);
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
**Acceptance Criteria**:
|
|
744
|
+
- [x] All error types handled appropriately
|
|
745
|
+
- [x] User-friendly error messages
|
|
746
|
+
- [x] Correct exit codes used
|
|
747
|
+
- [x] Tests pass
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
### 9. Create Output Formatters
|
|
752
|
+
**Estimated Time**: 3-4 hours
|
|
753
|
+
|
|
754
|
+
**Description**: Format command output as JSON, table, or YAML
|
|
755
|
+
|
|
756
|
+
**Install dependencies**:
|
|
757
|
+
```bash
|
|
758
|
+
npm install js-yaml cli-table3
|
|
759
|
+
npm install --save-dev @types/js-yaml
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**Create `src/lib/formatters.ts`**:
|
|
763
|
+
```typescript
|
|
764
|
+
import chalk from 'chalk';
|
|
765
|
+
import Table from 'cli-table3';
|
|
766
|
+
import yaml from 'js-yaml';
|
|
767
|
+
import { config } from './config';
|
|
768
|
+
|
|
769
|
+
export type OutputFormat = 'json' | 'table' | 'yaml';
|
|
770
|
+
|
|
771
|
+
export function formatOutput(
|
|
772
|
+
data: unknown,
|
|
773
|
+
format?: OutputFormat,
|
|
774
|
+
tableOptions?: TableOptions
|
|
775
|
+
): string {
|
|
776
|
+
const outputFormat = format || config.getFormat();
|
|
777
|
+
|
|
778
|
+
switch (outputFormat) {
|
|
779
|
+
case 'json':
|
|
780
|
+
return formatJson(data);
|
|
781
|
+
case 'yaml':
|
|
782
|
+
return formatYaml(data);
|
|
783
|
+
case 'table':
|
|
784
|
+
return formatTable(data, tableOptions);
|
|
785
|
+
default:
|
|
786
|
+
return formatJson(data);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function formatJson(data: unknown): string {
|
|
791
|
+
return JSON.stringify(data, null, 2);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export function formatYaml(data: unknown): string {
|
|
795
|
+
return yaml.dump(data, { indent: 2, lineWidth: 120 });
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
interface TableOptions {
|
|
799
|
+
columns?: string[];
|
|
800
|
+
maxColumnWidth?: number;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function formatTable(data: unknown, options?: TableOptions): string {
|
|
804
|
+
if (!data) {
|
|
805
|
+
return 'No data';
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Single object
|
|
809
|
+
if (typeof data === 'object' && !Array.isArray(data)) {
|
|
810
|
+
return formatObjectAsTable(data as Record<string, unknown>);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Array of objects
|
|
814
|
+
if (Array.isArray(data)) {
|
|
815
|
+
if (data.length === 0) {
|
|
816
|
+
return 'No results';
|
|
817
|
+
}
|
|
818
|
+
return formatArrayAsTable(data, options);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Primitive value
|
|
822
|
+
return String(data);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function formatObjectAsTable(obj: Record<string, unknown>): string {
|
|
826
|
+
const table = new Table({
|
|
827
|
+
head: [chalk.cyan('Property'), chalk.cyan('Value')],
|
|
828
|
+
colWidths: [30, 70],
|
|
829
|
+
wordWrap: true,
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
Object.entries(obj).forEach(([key, value]) => {
|
|
833
|
+
table.push([key, formatValue(value)]);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
return table.toString();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function formatArrayAsTable(data: unknown[], options?: TableOptions): string {
|
|
840
|
+
const firstItem = data[0];
|
|
841
|
+
if (typeof firstItem !== 'object' || firstItem === null) {
|
|
842
|
+
// Array of primitives
|
|
843
|
+
const table = new Table({ head: [chalk.cyan('Value')] });
|
|
844
|
+
data.forEach((item) => table.push([formatValue(item)]));
|
|
845
|
+
return table.toString();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Array of objects - use keys from first object
|
|
849
|
+
const keys = options?.columns || Object.keys(firstItem as Record<string, unknown>);
|
|
850
|
+
const table = new Table({
|
|
851
|
+
head: keys.map((k) => chalk.cyan(k)),
|
|
852
|
+
wordWrap: true,
|
|
853
|
+
colWidths: keys.map(() => options?.maxColumnWidth || 30),
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
data.forEach((item) => {
|
|
857
|
+
const row = keys.map((key) => {
|
|
858
|
+
const value = (item as Record<string, unknown>)[key];
|
|
859
|
+
return formatValue(value);
|
|
860
|
+
});
|
|
861
|
+
table.push(row);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
return table.toString();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function formatValue(value: unknown): string {
|
|
868
|
+
if (value === null || value === undefined) {
|
|
869
|
+
return chalk.gray('null');
|
|
870
|
+
}
|
|
871
|
+
if (typeof value === 'boolean') {
|
|
872
|
+
return value ? chalk.green('true') : chalk.red('false');
|
|
873
|
+
}
|
|
874
|
+
if (typeof value === 'object') {
|
|
875
|
+
return JSON.stringify(value);
|
|
876
|
+
}
|
|
877
|
+
return String(value);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export function printOutput(data: unknown, format?: OutputFormat, tableOptions?: TableOptions): void {
|
|
881
|
+
const output = formatOutput(data, format, tableOptions);
|
|
882
|
+
console.log(output);
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
**Create tests `tests/lib/formatters.test.ts`**:
|
|
887
|
+
```typescript
|
|
888
|
+
import { formatJson, formatYaml, formatTable, formatOutput } from '../../src/lib/formatters';
|
|
889
|
+
|
|
890
|
+
describe('Output Formatters', () => {
|
|
891
|
+
const testData = { id: '123', name: 'Test', active: true };
|
|
892
|
+
|
|
893
|
+
it('should format as JSON', () => {
|
|
894
|
+
const output = formatJson(testData);
|
|
895
|
+
expect(output).toContain('"id"');
|
|
896
|
+
expect(output).toContain('"123"');
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it('should format as YAML', () => {
|
|
900
|
+
const output = formatYaml(testData);
|
|
901
|
+
expect(output).toContain('id: ');
|
|
902
|
+
expect(output).toContain('name: Test');
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('should format object as table', () => {
|
|
906
|
+
const output = formatTable(testData);
|
|
907
|
+
expect(output).toContain('Property');
|
|
908
|
+
expect(output).toContain('Value');
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should format array as table', () => {
|
|
912
|
+
const output = formatTable([testData, testData]);
|
|
913
|
+
expect(output).toContain('id');
|
|
914
|
+
expect(output).toContain('name');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('should use default format from config', () => {
|
|
918
|
+
const output = formatOutput(testData);
|
|
919
|
+
expect(output).toBeDefined();
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
**Acceptance Criteria**:
|
|
925
|
+
- [x] JSON formatting works
|
|
926
|
+
- [x] YAML formatting works
|
|
927
|
+
- [x] Table formatting works for objects and arrays
|
|
928
|
+
- [x] Format selection respects config/flags
|
|
929
|
+
- [x] Tests pass
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
### 10. Write Basic Tests for Utilities
|
|
934
|
+
**Estimated Time**: 2-3 hours
|
|
935
|
+
|
|
936
|
+
**Description**: Set up Jest and write comprehensive tests
|
|
937
|
+
|
|
938
|
+
**Install Jest**:
|
|
939
|
+
```bash
|
|
940
|
+
npm install --save-dev jest ts-jest @types/jest
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
**Create `jest.config.js`**:
|
|
944
|
+
```javascript
|
|
945
|
+
module.exports = {
|
|
946
|
+
preset: 'ts-jest',
|
|
947
|
+
testEnvironment: 'node',
|
|
948
|
+
roots: ['<rootDir>/tests'],
|
|
949
|
+
testMatch: ['**/*.test.ts'],
|
|
950
|
+
collectCoverageFrom: [
|
|
951
|
+
'src/**/*.ts',
|
|
952
|
+
'!src/index.ts',
|
|
953
|
+
'!src/**/*.d.ts'
|
|
954
|
+
],
|
|
955
|
+
coverageThreshold: {
|
|
956
|
+
global: {
|
|
957
|
+
branches: 80,
|
|
958
|
+
functions: 80,
|
|
959
|
+
lines: 80,
|
|
960
|
+
statements: 80
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
moduleNameMapper: {
|
|
964
|
+
'^@/(.*)$': '<rootDir>/src/$1'
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
**Run tests**:
|
|
970
|
+
```bash
|
|
971
|
+
npm test
|
|
972
|
+
npm run test:coverage
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
**Acceptance Criteria**:
|
|
976
|
+
- [x] All tests pass
|
|
977
|
+
- [x] Coverage >80% for utility modules
|
|
978
|
+
- [x] `npm test` works in CI/CD
|
|
979
|
+
|
|
980
|
+
---
|
|
981
|
+
|
|
982
|
+
## Validation Checklist
|
|
983
|
+
|
|
984
|
+
Before marking this phase complete, verify:
|
|
985
|
+
|
|
986
|
+
- [ ] `npm install` succeeds without errors
|
|
987
|
+
- [ ] `npm run build` compiles TypeScript successfully
|
|
988
|
+
- [ ] `npm run lint` passes without errors
|
|
989
|
+
- [ ] `npm run format` formats code correctly
|
|
990
|
+
- [ ] `npm test` passes all tests with >80% coverage
|
|
991
|
+
- [ ] `npm run dev` runs CLI without crashing
|
|
992
|
+
- [ ] Can import from `@memnexus-ai/contracts`
|
|
993
|
+
- [ ] Config saves and loads correctly
|
|
994
|
+
- [ ] API client initializes (with dummy key)
|
|
995
|
+
- [ ] All three formatters work (JSON, table, YAML)
|
|
996
|
+
- [ ] Error handling shows user-friendly messages
|
|
997
|
+
|
|
998
|
+
---
|
|
999
|
+
|
|
1000
|
+
## Documentation
|
|
1001
|
+
|
|
1002
|
+
**Create/Update**:
|
|
1003
|
+
- [ ] Add basic usage to `README.md`
|
|
1004
|
+
- [ ] Document config file location and format
|
|
1005
|
+
- [ ] Document environment variables
|
|
1006
|
+
- [ ] Add development setup instructions
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
|
|
1010
|
+
## Dependencies
|
|
1011
|
+
|
|
1012
|
+
**Production**:
|
|
1013
|
+
- `@memnexus-ai/contracts@^1.0.4`
|
|
1014
|
+
- `commander@^11.1.0`
|
|
1015
|
+
- `chalk@^5.3.0`
|
|
1016
|
+
- `ora@^7.0.1`
|
|
1017
|
+
- `cli-table3@^0.6.3`
|
|
1018
|
+
- `inquirer@^9.2.12`
|
|
1019
|
+
- `configstore@^7.0.0`
|
|
1020
|
+
- `dotenv@^16.3.1`
|
|
1021
|
+
- `js-yaml@^4.1.0`
|
|
1022
|
+
|
|
1023
|
+
**Development**:
|
|
1024
|
+
- `typescript@^5.3.3`
|
|
1025
|
+
- `@types/node@^20.10.0`
|
|
1026
|
+
- `ts-node@^10.9.2`
|
|
1027
|
+
- `jest@^29.7.0`
|
|
1028
|
+
- `ts-jest@^29.1.1`
|
|
1029
|
+
- `@types/jest@^29.5.8`
|
|
1030
|
+
- `eslint@^8.55.0`
|
|
1031
|
+
- `@typescript-eslint/parser@^6.13.2`
|
|
1032
|
+
- `@typescript-eslint/eslint-plugin@^6.13.2`
|
|
1033
|
+
- `prettier@^3.1.0`
|
|
1034
|
+
- `eslint-config-prettier@^9.1.0`
|
|
1035
|
+
- `@types/js-yaml@^4.0.9`
|
|
1036
|
+
|
|
1037
|
+
---
|
|
1038
|
+
|
|
1039
|
+
## Timeline
|
|
1040
|
+
|
|
1041
|
+
**Day 1-2**: Tasks 1-4 (Setup, dependencies, project structure)
|
|
1042
|
+
**Day 3-4**: Tasks 5-7 (Config, API client, auth)
|
|
1043
|
+
**Day 4-5**: Tasks 8-10 (Errors, formatters, tests)
|
|
1044
|
+
|
|
1045
|
+
**Total**: 5 working days (1 week)
|
|
1046
|
+
|
|
1047
|
+
---
|
|
1048
|
+
|
|
1049
|
+
## Next Steps
|
|
1050
|
+
|
|
1051
|
+
After Phase 1 completion:
|
|
1052
|
+
- **Phase 2**: Implement core commands (memories, conversations, facts)
|
|
1053
|
+
- Set up CI/CD workflows
|
|
1054
|
+
- Begin integration testing with real API
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## Related Documentation
|
|
1059
|
+
|
|
1060
|
+
- [PRD - Phase 1 Section](../docs/prd.md#phase-1-foundation-week-1)
|
|
1061
|
+
- [CLI-API Sync Strategy](../docs/sync-strategy.md)
|
|
1062
|
+
- [Code Generation Strategy](../docs/code-generation-strategy.md)
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## Questions or Issues?
|
|
1067
|
+
|
|
1068
|
+
If you encounter any blockers or need clarification:
|
|
1069
|
+
1. Check the [PRD documentation](../docs/prd.md)
|
|
1070
|
+
2. Review existing code in `@memnexus-ai/contracts`
|
|
1071
|
+
3. Comment on this issue or create a new one
|
|
1072
|
+
4. Tag relevant team members
|
|
1073
|
+
|
|
1074
|
+
---
|
|
1075
|
+
|
|
1076
|
+
**Estimated Total Time**: 15-20 hours (1 week)
|
|
1077
|
+
**Priority**: High (Blocks Phase 2)
|
|
1078
|
+
**Complexity**: Medium
|