@fermindi/pwn-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/LICENSE +21 -0
- package/README.md +251 -0
- package/cli/batch.js +333 -0
- package/cli/codespaces.js +303 -0
- package/cli/index.js +91 -0
- package/cli/inject.js +53 -0
- package/cli/knowledge.js +531 -0
- package/cli/notify.js +135 -0
- package/cli/patterns.js +665 -0
- package/cli/status.js +91 -0
- package/cli/validate.js +61 -0
- package/package.json +70 -0
- package/src/core/inject.js +128 -0
- package/src/core/state.js +91 -0
- package/src/core/validate.js +202 -0
- package/src/core/workspace.js +176 -0
- package/src/index.js +20 -0
- package/src/knowledge/gc.js +308 -0
- package/src/knowledge/lifecycle.js +401 -0
- package/src/knowledge/promote.js +364 -0
- package/src/knowledge/references.js +342 -0
- package/src/patterns/matcher.js +218 -0
- package/src/patterns/registry.js +375 -0
- package/src/patterns/triggers.js +423 -0
- package/src/services/batch-service.js +849 -0
- package/src/services/notification-service.js +342 -0
- package/templates/codespaces/devcontainer.json +52 -0
- package/templates/codespaces/setup.sh +70 -0
- package/templates/workspace/.ai/README.md +164 -0
- package/templates/workspace/.ai/agents/README.md +204 -0
- package/templates/workspace/.ai/agents/claude.md +625 -0
- package/templates/workspace/.ai/config/.gitkeep +0 -0
- package/templates/workspace/.ai/config/README.md +79 -0
- package/templates/workspace/.ai/config/notifications.template.json +20 -0
- package/templates/workspace/.ai/memory/deadends.md +79 -0
- package/templates/workspace/.ai/memory/decisions.md +58 -0
- package/templates/workspace/.ai/memory/patterns.md +65 -0
- package/templates/workspace/.ai/patterns/backend/README.md +126 -0
- package/templates/workspace/.ai/patterns/frontend/README.md +103 -0
- package/templates/workspace/.ai/patterns/index.md +256 -0
- package/templates/workspace/.ai/patterns/triggers.json +1087 -0
- package/templates/workspace/.ai/patterns/universal/README.md +141 -0
- package/templates/workspace/.ai/state.template.json +8 -0
- package/templates/workspace/.ai/tasks/active.md +77 -0
- package/templates/workspace/.ai/tasks/backlog.md +95 -0
- package/templates/workspace/.ai/workflows/batch-task.md +356 -0
package/cli/status.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { getWorkspaceInfo } from '../src/core/workspace.js';
|
|
3
|
+
import { validate } from '../src/core/validate.js';
|
|
4
|
+
|
|
5
|
+
export default async function statusCommand() {
|
|
6
|
+
const info = getWorkspaceInfo();
|
|
7
|
+
|
|
8
|
+
if (!info.exists) {
|
|
9
|
+
console.log('ā No PWN workspace found\n');
|
|
10
|
+
console.log(' Run: pwn inject');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Validate workspace
|
|
15
|
+
const validation = validate();
|
|
16
|
+
|
|
17
|
+
console.log('š PWN Workspace Status\n');
|
|
18
|
+
|
|
19
|
+
// Developer info
|
|
20
|
+
if (info.state) {
|
|
21
|
+
console.log(`š¤ Developer: ${info.state.developer}`);
|
|
22
|
+
console.log(`š
Session: ${formatDate(info.state.session_started)}`);
|
|
23
|
+
if (info.state.current_task) {
|
|
24
|
+
console.log(`šÆ Task: ${info.state.current_task}`);
|
|
25
|
+
}
|
|
26
|
+
console.log();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Tasks summary
|
|
30
|
+
console.log('š Tasks');
|
|
31
|
+
console.log(` Active: ${info.tasks.active.pending} pending, ${info.tasks.active.completed} completed`);
|
|
32
|
+
console.log(` Backlog: ${info.tasks.backlog.total} items`);
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
// Memory summary
|
|
36
|
+
console.log('š§ Memory');
|
|
37
|
+
console.log(` Decisions: ${info.memory.decisions}`);
|
|
38
|
+
console.log(` Patterns: ${info.memory.patterns}`);
|
|
39
|
+
console.log(` Dead-ends: ${info.memory.deadends}`);
|
|
40
|
+
console.log();
|
|
41
|
+
|
|
42
|
+
// Patterns summary
|
|
43
|
+
console.log('šØ Patterns');
|
|
44
|
+
console.log(` Triggers: ${info.patterns.triggers}`);
|
|
45
|
+
console.log(` Categories: ${info.patterns.categories.join(', ') || 'none'}`);
|
|
46
|
+
console.log();
|
|
47
|
+
|
|
48
|
+
// Validation status
|
|
49
|
+
if (validation.valid) {
|
|
50
|
+
console.log('ā
Workspace structure valid');
|
|
51
|
+
} else {
|
|
52
|
+
console.log('ā ļø Workspace has issues:');
|
|
53
|
+
for (const issue of validation.issues) {
|
|
54
|
+
console.log(` - ${issue}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (validation.warnings.length > 0) {
|
|
59
|
+
console.log('\nā ļø Warnings:');
|
|
60
|
+
for (const warning of validation.warnings) {
|
|
61
|
+
console.log(` - ${warning}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format ISO date to human readable
|
|
68
|
+
* @param {string} isoDate - ISO date string
|
|
69
|
+
* @returns {string} Formatted date
|
|
70
|
+
*/
|
|
71
|
+
function formatDate(isoDate) {
|
|
72
|
+
if (!isoDate) return 'unknown';
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const date = new Date(isoDate);
|
|
76
|
+
const now = new Date();
|
|
77
|
+
const diffMs = now - date;
|
|
78
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
79
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
80
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
81
|
+
|
|
82
|
+
if (diffMins < 1) return 'just now';
|
|
83
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
84
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
85
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
86
|
+
|
|
87
|
+
return date.toLocaleDateString();
|
|
88
|
+
} catch {
|
|
89
|
+
return isoDate;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/cli/validate.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { validate, getStructureReport } from '../src/core/validate.js';
|
|
3
|
+
|
|
4
|
+
export default async function validateCommand(args = []) {
|
|
5
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
6
|
+
|
|
7
|
+
console.log('š Validating PWN workspace...\n');
|
|
8
|
+
|
|
9
|
+
const result = validate();
|
|
10
|
+
|
|
11
|
+
if (verbose) {
|
|
12
|
+
const report = getStructureReport();
|
|
13
|
+
console.log('š Structure Report:\n');
|
|
14
|
+
|
|
15
|
+
console.log(' Directories:');
|
|
16
|
+
for (const [dir, exists] of Object.entries(report.directories)) {
|
|
17
|
+
const icon = exists ? 'ā' : 'ā';
|
|
18
|
+
console.log(` ${icon} .ai/${dir}/`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('\n Files:');
|
|
22
|
+
for (const [file, exists] of Object.entries(report.files)) {
|
|
23
|
+
const icon = exists ? 'ā' : 'ā';
|
|
24
|
+
console.log(` ${icon} .ai/${file}`);
|
|
25
|
+
}
|
|
26
|
+
console.log();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (result.valid) {
|
|
30
|
+
console.log('ā
Workspace is valid\n');
|
|
31
|
+
|
|
32
|
+
if (result.warnings.length > 0) {
|
|
33
|
+
console.log('ā ļø Warnings:');
|
|
34
|
+
for (const warning of result.warnings) {
|
|
35
|
+
console.log(` - ${warning}`);
|
|
36
|
+
}
|
|
37
|
+
console.log();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.exit(0);
|
|
41
|
+
} else {
|
|
42
|
+
console.log('ā Workspace has issues:\n');
|
|
43
|
+
|
|
44
|
+
for (const issue of result.issues) {
|
|
45
|
+
console.log(` ā ${issue}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (result.warnings.length > 0) {
|
|
49
|
+
console.log('\nā ļø Warnings:');
|
|
50
|
+
for (const warning of result.warnings) {
|
|
51
|
+
console.log(` - ${warning}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('\nš” To fix, try:');
|
|
56
|
+
console.log(' pwn inject --force (recreate workspace)');
|
|
57
|
+
console.log();
|
|
58
|
+
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fermindi/pwn-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Professional AI Workspace - Inject structured memory and automation into any project for AI-powered development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pwn": "./cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js",
|
|
12
|
+
"./core/state": "./src/core/state.js",
|
|
13
|
+
"./core/inject": "./src/core/inject.js",
|
|
14
|
+
"./core/validate": "./src/core/validate.js",
|
|
15
|
+
"./core/workspace": "./src/core/workspace.js",
|
|
16
|
+
"./services/batch": "./src/services/batch-service.js",
|
|
17
|
+
"./services/notifications": "./src/services/notification-service.js",
|
|
18
|
+
"./patterns/registry": "./src/patterns/registry.js",
|
|
19
|
+
"./patterns/triggers": "./src/patterns/triggers.js",
|
|
20
|
+
"./knowledge/lifecycle": "./src/knowledge/lifecycle.js",
|
|
21
|
+
"./knowledge/references": "./src/knowledge/references.js",
|
|
22
|
+
"./knowledge/gc": "./src/knowledge/gc.js",
|
|
23
|
+
"./knowledge/promote": "./src/knowledge/promote.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"cli/",
|
|
27
|
+
"src/",
|
|
28
|
+
"templates/",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"start": "node cli/index.js",
|
|
34
|
+
"dev": "node --watch cli/index.js",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:coverage": "vitest run --coverage",
|
|
38
|
+
"prepublishOnly": "npm test"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"ai",
|
|
42
|
+
"cli",
|
|
43
|
+
"workspace",
|
|
44
|
+
"automation",
|
|
45
|
+
"claude",
|
|
46
|
+
"patterns",
|
|
47
|
+
"memory",
|
|
48
|
+
"decisions",
|
|
49
|
+
"batch",
|
|
50
|
+
"codespaces",
|
|
51
|
+
"developer-tools",
|
|
52
|
+
"productivity"
|
|
53
|
+
],
|
|
54
|
+
"author": "Diego Fernandes",
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "git+https://github.com/fermindi/pwn.git"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/fermindi/pwn/issues"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/fermindi/pwn#readme",
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=18.0.0"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"vitest": "^2.0.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { initState } from './state.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the path to the workspace template
|
|
11
|
+
* @returns {string} Path to template directory
|
|
12
|
+
*/
|
|
13
|
+
export function getTemplatePath() {
|
|
14
|
+
return join(__dirname, '../../templates/workspace/.ai');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Inject PWN workspace into a project
|
|
19
|
+
* @param {object} options - Injection options
|
|
20
|
+
* @param {string} options.cwd - Target directory (defaults to process.cwd())
|
|
21
|
+
* @param {boolean} options.force - Force overwrite existing .ai/ directory
|
|
22
|
+
* @param {boolean} options.silent - Suppress console output
|
|
23
|
+
* @returns {object} Result with success status and message
|
|
24
|
+
*/
|
|
25
|
+
export async function inject(options = {}) {
|
|
26
|
+
const {
|
|
27
|
+
cwd = process.cwd(),
|
|
28
|
+
force = false,
|
|
29
|
+
silent = false
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
const templateDir = getTemplatePath();
|
|
33
|
+
const targetDir = join(cwd, '.ai');
|
|
34
|
+
|
|
35
|
+
const log = silent ? () => {} : console.log;
|
|
36
|
+
|
|
37
|
+
// Check if .ai/ already exists
|
|
38
|
+
if (existsSync(targetDir) && !force) {
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
error: 'ALREADY_EXISTS',
|
|
42
|
+
message: '.ai/ directory already exists. Use --force to overwrite.'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Copy workspace template
|
|
48
|
+
log('š¦ Copying workspace template...');
|
|
49
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
// Rename state.template.json ā state.json
|
|
52
|
+
const templateState = join(targetDir, 'state.template.json');
|
|
53
|
+
const stateFile = join(targetDir, 'state.json');
|
|
54
|
+
|
|
55
|
+
if (existsSync(templateState)) {
|
|
56
|
+
renameSync(templateState, stateFile);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Rename notifications.template.json ā notifications.json and generate unique topic
|
|
60
|
+
const templateNotify = join(targetDir, 'config', 'notifications.template.json');
|
|
61
|
+
const notifyFile = join(targetDir, 'config', 'notifications.json');
|
|
62
|
+
|
|
63
|
+
if (existsSync(templateNotify)) {
|
|
64
|
+
renameSync(templateNotify, notifyFile);
|
|
65
|
+
initNotifications(notifyFile);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Initialize state.json with current user
|
|
69
|
+
initState(cwd);
|
|
70
|
+
|
|
71
|
+
// Update .gitignore
|
|
72
|
+
updateGitignore(cwd, silent);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
message: 'PWN workspace injected successfully',
|
|
77
|
+
path: targetDir
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: 'INJECTION_FAILED',
|
|
84
|
+
message: error.message
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Initialize notifications.json with unique topic
|
|
91
|
+
* @param {string} notifyFile - Path to notifications.json
|
|
92
|
+
*/
|
|
93
|
+
function initNotifications(notifyFile) {
|
|
94
|
+
try {
|
|
95
|
+
const content = readFileSync(notifyFile, 'utf8');
|
|
96
|
+
const config = JSON.parse(content);
|
|
97
|
+
|
|
98
|
+
// Generate unique topic ID
|
|
99
|
+
const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
|
|
100
|
+
config.channels.ntfy.topic = `pwn-${uniqueId}`;
|
|
101
|
+
|
|
102
|
+
writeFileSync(notifyFile, JSON.stringify(config, null, 2));
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore errors - notifications will use defaults
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update .gitignore to exclude PWN personal files
|
|
110
|
+
* @param {string} cwd - Working directory
|
|
111
|
+
* @param {boolean} silent - Suppress output
|
|
112
|
+
*/
|
|
113
|
+
function updateGitignore(cwd, silent = false) {
|
|
114
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
115
|
+
let gitignoreContent = '';
|
|
116
|
+
|
|
117
|
+
if (existsSync(gitignorePath)) {
|
|
118
|
+
gitignoreContent = readFileSync(gitignorePath, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!gitignoreContent.includes('.ai/state.json')) {
|
|
122
|
+
const pwnSection = '\n# PWN\n.ai/state.json\n.ai/config/notifications.json\n';
|
|
123
|
+
appendFileSync(gitignorePath, pwnSection);
|
|
124
|
+
if (!silent) {
|
|
125
|
+
console.log('š Updated .gitignore');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the path to state.json in a workspace
|
|
7
|
+
* @param {string} cwd - Working directory (defaults to process.cwd())
|
|
8
|
+
* @returns {string} Path to state.json
|
|
9
|
+
*/
|
|
10
|
+
export function getStatePath(cwd = process.cwd()) {
|
|
11
|
+
return join(cwd, '.ai', 'state.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if a PWN workspace exists in the given directory
|
|
16
|
+
* @param {string} cwd - Working directory
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
export function hasWorkspace(cwd = process.cwd()) {
|
|
20
|
+
return existsSync(join(cwd, '.ai'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the current git username
|
|
25
|
+
* @returns {string} Git username or 'unknown'
|
|
26
|
+
*/
|
|
27
|
+
export function getGitUser() {
|
|
28
|
+
try {
|
|
29
|
+
return execSync('git config user.name', { encoding: 'utf8' }).trim();
|
|
30
|
+
} catch {
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the current state from state.json
|
|
37
|
+
* @param {string} cwd - Working directory
|
|
38
|
+
* @returns {object|null} State object or null if not found
|
|
39
|
+
*/
|
|
40
|
+
export function getState(cwd = process.cwd()) {
|
|
41
|
+
const statePath = getStatePath(cwd);
|
|
42
|
+
|
|
43
|
+
if (!existsSync(statePath)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(statePath, 'utf8');
|
|
49
|
+
return JSON.parse(content);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update the state.json file
|
|
57
|
+
* @param {object} updates - Fields to update
|
|
58
|
+
* @param {string} cwd - Working directory
|
|
59
|
+
* @returns {object} Updated state
|
|
60
|
+
*/
|
|
61
|
+
export function updateState(updates, cwd = process.cwd()) {
|
|
62
|
+
const statePath = getStatePath(cwd);
|
|
63
|
+
const currentState = getState(cwd) || {};
|
|
64
|
+
|
|
65
|
+
const newState = {
|
|
66
|
+
...currentState,
|
|
67
|
+
...updates,
|
|
68
|
+
last_updated: new Date().toISOString()
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
writeFileSync(statePath, JSON.stringify(newState, null, 2));
|
|
72
|
+
return newState;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Initialize a new state.json file
|
|
77
|
+
* @param {string} cwd - Working directory
|
|
78
|
+
* @returns {object} Initial state
|
|
79
|
+
*/
|
|
80
|
+
export function initState(cwd = process.cwd()) {
|
|
81
|
+
const state = {
|
|
82
|
+
developer: getGitUser(),
|
|
83
|
+
session_started: new Date().toISOString(),
|
|
84
|
+
current_task: null,
|
|
85
|
+
context_loaded: []
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const statePath = getStatePath(cwd);
|
|
89
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync, statSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Expected workspace structure
|
|
6
|
+
*/
|
|
7
|
+
const REQUIRED_STRUCTURE = {
|
|
8
|
+
files: [
|
|
9
|
+
'README.md',
|
|
10
|
+
'state.json'
|
|
11
|
+
],
|
|
12
|
+
directories: [
|
|
13
|
+
'memory',
|
|
14
|
+
'tasks',
|
|
15
|
+
'patterns',
|
|
16
|
+
'workflows',
|
|
17
|
+
'agents',
|
|
18
|
+
'config'
|
|
19
|
+
],
|
|
20
|
+
memoryFiles: [
|
|
21
|
+
'memory/decisions.md',
|
|
22
|
+
'memory/patterns.md',
|
|
23
|
+
'memory/deadends.md'
|
|
24
|
+
],
|
|
25
|
+
taskFiles: [
|
|
26
|
+
'tasks/active.md',
|
|
27
|
+
'tasks/backlog.md'
|
|
28
|
+
],
|
|
29
|
+
agentFiles: [
|
|
30
|
+
'agents/README.md',
|
|
31
|
+
'agents/claude.md'
|
|
32
|
+
],
|
|
33
|
+
patternFiles: [
|
|
34
|
+
'patterns/index.md'
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate a PWN workspace structure
|
|
40
|
+
* @param {string} cwd - Working directory
|
|
41
|
+
* @returns {object} Validation result with issues array
|
|
42
|
+
*/
|
|
43
|
+
export function validate(cwd = process.cwd()) {
|
|
44
|
+
const aiDir = join(cwd, '.ai');
|
|
45
|
+
const issues = [];
|
|
46
|
+
const warnings = [];
|
|
47
|
+
|
|
48
|
+
// Check if .ai/ exists
|
|
49
|
+
if (!existsSync(aiDir)) {
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
issues: ['No .ai/ directory found. Run: pwn inject'],
|
|
53
|
+
warnings: []
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check required files
|
|
58
|
+
for (const file of REQUIRED_STRUCTURE.files) {
|
|
59
|
+
const filePath = join(aiDir, file);
|
|
60
|
+
if (!existsSync(filePath)) {
|
|
61
|
+
issues.push(`Missing file: .ai/${file}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check required directories
|
|
66
|
+
for (const dir of REQUIRED_STRUCTURE.directories) {
|
|
67
|
+
const dirPath = join(aiDir, dir);
|
|
68
|
+
if (!existsSync(dirPath)) {
|
|
69
|
+
issues.push(`Missing directory: .ai/${dir}/`);
|
|
70
|
+
} else if (!statSync(dirPath).isDirectory()) {
|
|
71
|
+
issues.push(`.ai/${dir} exists but is not a directory`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check memory files
|
|
76
|
+
for (const file of REQUIRED_STRUCTURE.memoryFiles) {
|
|
77
|
+
const filePath = join(aiDir, file);
|
|
78
|
+
if (!existsSync(filePath)) {
|
|
79
|
+
warnings.push(`Missing memory file: .ai/${file}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check task files
|
|
84
|
+
for (const file of REQUIRED_STRUCTURE.taskFiles) {
|
|
85
|
+
const filePath = join(aiDir, file);
|
|
86
|
+
if (!existsSync(filePath)) {
|
|
87
|
+
warnings.push(`Missing task file: .ai/${file}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check agent files
|
|
92
|
+
for (const file of REQUIRED_STRUCTURE.agentFiles) {
|
|
93
|
+
const filePath = join(aiDir, file);
|
|
94
|
+
if (!existsSync(filePath)) {
|
|
95
|
+
warnings.push(`Missing agent file: .ai/${file}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check pattern files
|
|
100
|
+
for (const file of REQUIRED_STRUCTURE.patternFiles) {
|
|
101
|
+
const filePath = join(aiDir, file);
|
|
102
|
+
if (!existsSync(filePath)) {
|
|
103
|
+
warnings.push(`Missing pattern file: .ai/${file}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate state.json format
|
|
108
|
+
const stateValidation = validateStateJson(aiDir);
|
|
109
|
+
if (!stateValidation.valid) {
|
|
110
|
+
issues.push(...stateValidation.issues);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
valid: issues.length === 0,
|
|
115
|
+
issues,
|
|
116
|
+
warnings
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate state.json structure and content
|
|
122
|
+
* @param {string} aiDir - Path to .ai directory
|
|
123
|
+
* @returns {object} Validation result
|
|
124
|
+
*/
|
|
125
|
+
function validateStateJson(aiDir) {
|
|
126
|
+
const statePath = join(aiDir, 'state.json');
|
|
127
|
+
const issues = [];
|
|
128
|
+
|
|
129
|
+
if (!existsSync(statePath)) {
|
|
130
|
+
return { valid: false, issues: ['state.json not found'] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const content = readFileSync(statePath, 'utf8');
|
|
135
|
+
const state = JSON.parse(content);
|
|
136
|
+
|
|
137
|
+
// Check required fields
|
|
138
|
+
const requiredFields = ['developer', 'session_started'];
|
|
139
|
+
for (const field of requiredFields) {
|
|
140
|
+
if (!(field in state)) {
|
|
141
|
+
issues.push(`state.json missing required field: ${field}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Validate date format
|
|
146
|
+
if (state.session_started && isNaN(Date.parse(state.session_started))) {
|
|
147
|
+
issues.push('state.json: session_started is not a valid date');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof SyntaxError) {
|
|
152
|
+
issues.push('state.json contains invalid JSON');
|
|
153
|
+
} else {
|
|
154
|
+
issues.push(`state.json read error: ${error.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
valid: issues.length === 0,
|
|
160
|
+
issues
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get a detailed report of workspace structure
|
|
166
|
+
* @param {string} cwd - Working directory
|
|
167
|
+
* @returns {object} Structure report
|
|
168
|
+
*/
|
|
169
|
+
export function getStructureReport(cwd = process.cwd()) {
|
|
170
|
+
const aiDir = join(cwd, '.ai');
|
|
171
|
+
const report = {
|
|
172
|
+
exists: existsSync(aiDir),
|
|
173
|
+
directories: {},
|
|
174
|
+
files: {}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (!report.exists) {
|
|
178
|
+
return report;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check directories
|
|
182
|
+
for (const dir of REQUIRED_STRUCTURE.directories) {
|
|
183
|
+
const dirPath = join(aiDir, dir);
|
|
184
|
+
report.directories[dir] = existsSync(dirPath) && statSync(dirPath).isDirectory();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check all expected files
|
|
188
|
+
const allFiles = [
|
|
189
|
+
...REQUIRED_STRUCTURE.files,
|
|
190
|
+
...REQUIRED_STRUCTURE.memoryFiles,
|
|
191
|
+
...REQUIRED_STRUCTURE.taskFiles,
|
|
192
|
+
...REQUIRED_STRUCTURE.agentFiles,
|
|
193
|
+
...REQUIRED_STRUCTURE.patternFiles
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
for (const file of allFiles) {
|
|
197
|
+
const filePath = join(aiDir, file);
|
|
198
|
+
report.files[file] = existsSync(filePath);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return report;
|
|
202
|
+
}
|