@fermindi/pwn-cli 0.1.1 → 0.2.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 -21
- package/README.md +265 -251
- package/cli/batch.js +333 -333
- package/cli/codespaces.js +303 -303
- package/cli/index.js +98 -91
- package/cli/inject.js +78 -67
- package/cli/knowledge.js +531 -531
- package/cli/migrate.js +466 -0
- package/cli/notify.js +135 -135
- package/cli/patterns.js +665 -665
- package/cli/status.js +91 -91
- package/cli/validate.js +61 -61
- package/package.json +70 -70
- package/src/core/inject.js +208 -204
- package/src/core/state.js +91 -91
- package/src/core/validate.js +202 -202
- package/src/core/workspace.js +176 -176
- package/src/index.js +20 -20
- package/src/knowledge/gc.js +308 -308
- package/src/knowledge/lifecycle.js +401 -401
- package/src/knowledge/promote.js +364 -364
- package/src/knowledge/references.js +342 -342
- package/src/patterns/matcher.js +218 -218
- package/src/patterns/registry.js +375 -375
- package/src/patterns/triggers.js +423 -423
- package/src/services/batch-service.js +849 -849
- package/src/services/notification-service.js +342 -342
- package/templates/codespaces/devcontainer.json +52 -52
- package/templates/codespaces/setup.sh +70 -70
- package/templates/workspace/.ai/README.md +164 -164
- package/templates/workspace/.ai/agents/README.md +204 -204
- package/templates/workspace/.ai/agents/claude.md +625 -625
- package/templates/workspace/.ai/config/README.md +79 -79
- package/templates/workspace/.ai/config/notifications.template.json +20 -20
- package/templates/workspace/.ai/memory/deadends.md +79 -79
- package/templates/workspace/.ai/memory/decisions.md +58 -58
- package/templates/workspace/.ai/memory/patterns.md +65 -65
- package/templates/workspace/.ai/patterns/backend/README.md +126 -126
- package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
- package/templates/workspace/.ai/patterns/index.md +256 -256
- package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
- package/templates/workspace/.ai/patterns/universal/README.md +141 -141
- package/templates/workspace/.ai/state.template.json +8 -8
- package/templates/workspace/.ai/tasks/active.md +77 -77
- package/templates/workspace/.ai/tasks/backlog.md +95 -95
- package/templates/workspace/.ai/workflows/batch-task.md +356 -356
package/src/core/inject.js
CHANGED
|
@@ -1,204 +1,208 @@
|
|
|
1
|
-
import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync } 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
|
-
* Known AI instruction files in the industry
|
|
11
|
-
* PWN should detect these and warn/offer migration
|
|
12
|
-
*/
|
|
13
|
-
export const KNOWN_AI_FILES = [
|
|
14
|
-
// Claude Code
|
|
15
|
-
{ pattern: 'CLAUDE.md', type: 'claude', description: 'Claude Code instructions' },
|
|
16
|
-
{ pattern: 'claude.md', type: 'claude', description: 'Claude Code instructions' },
|
|
17
|
-
{ pattern: '.claude', type: 'claude', description: 'Claude config directory' },
|
|
18
|
-
|
|
19
|
-
// Cursor
|
|
20
|
-
{ pattern: '.cursorrules', type: 'cursor', description: 'Cursor AI rules' },
|
|
21
|
-
{ pattern: '.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{ pattern: '
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
*
|
|
91
|
-
* @param {
|
|
92
|
-
* @
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
config
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
1
|
+
import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync } 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
|
+
* Known AI instruction files in the industry
|
|
11
|
+
* PWN should detect these and warn/offer migration
|
|
12
|
+
*/
|
|
13
|
+
export const KNOWN_AI_FILES = [
|
|
14
|
+
// Claude Code
|
|
15
|
+
{ pattern: 'CLAUDE.md', type: 'claude', description: 'Claude Code instructions', migratable: true },
|
|
16
|
+
{ pattern: 'claude.md', type: 'claude', description: 'Claude Code instructions', migratable: true },
|
|
17
|
+
{ pattern: '.claude', type: 'claude', description: 'Claude config directory', migratable: false },
|
|
18
|
+
|
|
19
|
+
// Cursor
|
|
20
|
+
{ pattern: '.cursorrules', type: 'cursor', description: 'Cursor AI rules', migratable: true },
|
|
21
|
+
{ pattern: '.cursor/rules', type: 'cursor', description: 'Cursor AI rules', migratable: true },
|
|
22
|
+
{ pattern: '.cursorignore', type: 'cursor', description: 'Cursor ignore file', migratable: false },
|
|
23
|
+
|
|
24
|
+
// Cline memory-bank
|
|
25
|
+
{ pattern: 'memory-bank', type: 'memory-bank', description: 'Cline memory bank', migratable: true },
|
|
26
|
+
|
|
27
|
+
// GitHub Copilot
|
|
28
|
+
{ pattern: '.github/copilot-instructions.md', type: 'copilot', description: 'GitHub Copilot instructions', migratable: true },
|
|
29
|
+
|
|
30
|
+
// Session files (common pattern)
|
|
31
|
+
{ pattern: 'session.*.md', type: 'session', description: 'Session file (legacy pattern)', migratable: false },
|
|
32
|
+
|
|
33
|
+
// Other AI tools
|
|
34
|
+
{ pattern: '.aider', type: 'aider', description: 'Aider AI config', migratable: false },
|
|
35
|
+
{ pattern: '.continue', type: 'continue', description: 'Continue AI config', migratable: false },
|
|
36
|
+
{ pattern: '.codeium', type: 'codeium', description: 'Codeium config', migratable: false },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect known AI instruction files in a directory
|
|
41
|
+
* @param {string} cwd - Directory to scan
|
|
42
|
+
* @returns {Object[]} Array of detected files with metadata
|
|
43
|
+
*/
|
|
44
|
+
export function detectKnownAIFiles(cwd = process.cwd()) {
|
|
45
|
+
const detected = [];
|
|
46
|
+
|
|
47
|
+
for (const known of KNOWN_AI_FILES) {
|
|
48
|
+
// Handle glob patterns like session.*.md
|
|
49
|
+
if (known.pattern.includes('*')) {
|
|
50
|
+
const regex = new RegExp('^' + known.pattern.replace('.', '\\.').replace('*', '.*') + '$');
|
|
51
|
+
try {
|
|
52
|
+
const files = readdirSync(cwd);
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (regex.test(file)) {
|
|
55
|
+
detected.push({
|
|
56
|
+
...known,
|
|
57
|
+
file,
|
|
58
|
+
path: join(cwd, file)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore read errors
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Direct file check
|
|
67
|
+
const filePath = join(cwd, known.pattern);
|
|
68
|
+
if (existsSync(filePath)) {
|
|
69
|
+
detected.push({
|
|
70
|
+
...known,
|
|
71
|
+
file: known.pattern,
|
|
72
|
+
path: filePath
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return detected;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the path to the workspace template
|
|
83
|
+
* @returns {string} Path to template directory
|
|
84
|
+
*/
|
|
85
|
+
export function getTemplatePath() {
|
|
86
|
+
return join(__dirname, '../../templates/workspace/.ai');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Inject PWN workspace into a project
|
|
91
|
+
* @param {object} options - Injection options
|
|
92
|
+
* @param {string} options.cwd - Target directory (defaults to process.cwd())
|
|
93
|
+
* @param {boolean} options.force - Force overwrite existing .ai/ directory
|
|
94
|
+
* @param {boolean} options.silent - Suppress console output
|
|
95
|
+
* @param {boolean} options.skipDetection - Skip detection of known AI files
|
|
96
|
+
* @returns {object} Result with success status and message
|
|
97
|
+
*/
|
|
98
|
+
export async function inject(options = {}) {
|
|
99
|
+
const {
|
|
100
|
+
cwd = process.cwd(),
|
|
101
|
+
force = false,
|
|
102
|
+
silent = false,
|
|
103
|
+
skipDetection = false
|
|
104
|
+
} = options;
|
|
105
|
+
|
|
106
|
+
const templateDir = getTemplatePath();
|
|
107
|
+
const targetDir = join(cwd, '.ai');
|
|
108
|
+
|
|
109
|
+
const log = silent ? () => {} : console.log;
|
|
110
|
+
|
|
111
|
+
// Detect known AI instruction files
|
|
112
|
+
const detectedFiles = skipDetection ? [] : detectKnownAIFiles(cwd);
|
|
113
|
+
|
|
114
|
+
// Check if .ai/ already exists
|
|
115
|
+
if (existsSync(targetDir) && !force) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
error: 'ALREADY_EXISTS',
|
|
119
|
+
message: '.ai/ directory already exists. Use --force to overwrite.',
|
|
120
|
+
detected: detectedFiles
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Copy workspace template
|
|
126
|
+
log('📦 Copying workspace template...');
|
|
127
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
128
|
+
|
|
129
|
+
// Rename state.template.json → state.json
|
|
130
|
+
const templateState = join(targetDir, 'state.template.json');
|
|
131
|
+
const stateFile = join(targetDir, 'state.json');
|
|
132
|
+
|
|
133
|
+
if (existsSync(templateState)) {
|
|
134
|
+
renameSync(templateState, stateFile);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Rename notifications.template.json → notifications.json and generate unique topic
|
|
138
|
+
const templateNotify = join(targetDir, 'config', 'notifications.template.json');
|
|
139
|
+
const notifyFile = join(targetDir, 'config', 'notifications.json');
|
|
140
|
+
|
|
141
|
+
if (existsSync(templateNotify)) {
|
|
142
|
+
renameSync(templateNotify, notifyFile);
|
|
143
|
+
initNotifications(notifyFile);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Initialize state.json with current user
|
|
147
|
+
initState(cwd);
|
|
148
|
+
|
|
149
|
+
// Update .gitignore
|
|
150
|
+
updateGitignore(cwd, silent);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
message: 'PWN workspace injected successfully',
|
|
155
|
+
path: targetDir,
|
|
156
|
+
detected: detectedFiles
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
error: 'INJECTION_FAILED',
|
|
163
|
+
message: error.message,
|
|
164
|
+
detected: detectedFiles
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Initialize notifications.json with unique topic
|
|
171
|
+
* @param {string} notifyFile - Path to notifications.json
|
|
172
|
+
*/
|
|
173
|
+
function initNotifications(notifyFile) {
|
|
174
|
+
try {
|
|
175
|
+
const content = readFileSync(notifyFile, 'utf8');
|
|
176
|
+
const config = JSON.parse(content);
|
|
177
|
+
|
|
178
|
+
// Generate unique topic ID
|
|
179
|
+
const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
|
|
180
|
+
config.channels.ntfy.topic = `pwn-${uniqueId}`;
|
|
181
|
+
|
|
182
|
+
writeFileSync(notifyFile, JSON.stringify(config, null, 2));
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore errors - notifications will use defaults
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Update .gitignore to exclude PWN personal files
|
|
190
|
+
* @param {string} cwd - Working directory
|
|
191
|
+
* @param {boolean} silent - Suppress output
|
|
192
|
+
*/
|
|
193
|
+
function updateGitignore(cwd, silent = false) {
|
|
194
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
195
|
+
let gitignoreContent = '';
|
|
196
|
+
|
|
197
|
+
if (existsSync(gitignorePath)) {
|
|
198
|
+
gitignoreContent = readFileSync(gitignorePath, 'utf8');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!gitignoreContent.includes('.ai/state.json')) {
|
|
202
|
+
const pwnSection = '\n# PWN\n.ai/state.json\n.ai/config/notifications.json\n';
|
|
203
|
+
appendFileSync(gitignorePath, pwnSection);
|
|
204
|
+
if (!silent) {
|
|
205
|
+
console.log('📝 Updated .gitignore');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/core/state.js
CHANGED
|
@@ -1,91 +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
|
-
}
|
|
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
|
+
}
|