@bamptee/aia-code 2.0.0 → 2.0.2

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.
@@ -0,0 +1,153 @@
1
+ import {
2
+ isWtInstalled,
3
+ listWorktrees,
4
+ createWorktree,
5
+ getWorktreePath,
6
+ hasWorktree,
7
+ removeWorktree,
8
+ getFeatureBranch,
9
+ startServices,
10
+ stopServices,
11
+ hasDockerServices,
12
+ getServicesStatus,
13
+ } from '../../services/worktrunk.js';
14
+ import { json, error } from '../router.js';
15
+
16
+ export function registerWorktrunkRoutes(router) {
17
+ // Check if wt CLI is installed
18
+ router.get('/api/wt/status', async (req, res) => {
19
+ const installed = isWtInstalled();
20
+ json(res, { installed });
21
+ });
22
+
23
+ // Get worktree status for a feature
24
+ router.get('/api/features/:name/wt', async (req, res, { params, root }) => {
25
+ const installed = isWtInstalled();
26
+ if (!installed) {
27
+ return json(res, {
28
+ installed: false,
29
+ hasWorktree: false,
30
+ path: null,
31
+ hasServices: false,
32
+ });
33
+ }
34
+
35
+ const branch = getFeatureBranch(params.name);
36
+ const wtPath = getWorktreePath(branch, root);
37
+ const hasWt = wtPath !== null;
38
+ const hasServices = hasWt && hasDockerServices(wtPath);
39
+
40
+ json(res, {
41
+ installed: true,
42
+ hasWorktree: hasWt,
43
+ path: wtPath,
44
+ branch,
45
+ hasServices,
46
+ });
47
+ });
48
+
49
+ // Create a worktree for a feature
50
+ router.post('/api/features/:name/wt/create', async (req, res, { params, root }) => {
51
+ const installed = isWtInstalled();
52
+ if (!installed) {
53
+ return error(res, 'Worktrunk (wt) CLI is not installed', 400);
54
+ }
55
+
56
+ const branch = getFeatureBranch(params.name);
57
+
58
+ // Check if worktree already exists
59
+ if (hasWorktree(branch, root)) {
60
+ return error(res, `Worktree for branch "${branch}" already exists`, 400);
61
+ }
62
+
63
+ try {
64
+ createWorktree(branch, root);
65
+ const wtPath = getWorktreePath(branch, root);
66
+ json(res, {
67
+ ok: true,
68
+ branch,
69
+ path: wtPath,
70
+ }, 201);
71
+ } catch (err) {
72
+ error(res, `Failed to create worktree: ${err.message}`, 500);
73
+ }
74
+ });
75
+
76
+ // Remove a worktree for a feature
77
+ router.delete('/api/features/:name/wt', async (req, res, { params, root }) => {
78
+ const installed = isWtInstalled();
79
+ if (!installed) {
80
+ return error(res, 'Worktrunk (wt) CLI is not installed', 400);
81
+ }
82
+
83
+ const branch = getFeatureBranch(params.name);
84
+
85
+ if (!hasWorktree(branch, root)) {
86
+ return error(res, `No worktree found for branch "${branch}"`, 404);
87
+ }
88
+
89
+ try {
90
+ removeWorktree(branch, root);
91
+ json(res, { ok: true });
92
+ } catch (err) {
93
+ error(res, `Failed to remove worktree: ${err.message}`, 500);
94
+ }
95
+ });
96
+
97
+ // Start docker services in worktree
98
+ router.post('/api/features/:name/wt/services/start', async (req, res, { params, root }) => {
99
+ const branch = getFeatureBranch(params.name);
100
+ const wtPath = getWorktreePath(branch, root);
101
+
102
+ if (!wtPath) {
103
+ return error(res, 'No worktree found for this feature', 404);
104
+ }
105
+
106
+ if (!hasDockerServices(wtPath)) {
107
+ return error(res, 'No docker-compose.wt.yml found in worktree', 404);
108
+ }
109
+
110
+ try {
111
+ startServices(wtPath);
112
+ json(res, { ok: true });
113
+ } catch (err) {
114
+ error(res, `Failed to start services: ${err.message}`, 500);
115
+ }
116
+ });
117
+
118
+ // Stop docker services in worktree
119
+ router.post('/api/features/:name/wt/services/stop', async (req, res, { params, root }) => {
120
+ const branch = getFeatureBranch(params.name);
121
+ const wtPath = getWorktreePath(branch, root);
122
+
123
+ if (!wtPath) {
124
+ return error(res, 'No worktree found for this feature', 404);
125
+ }
126
+
127
+ if (!hasDockerServices(wtPath)) {
128
+ return error(res, 'No docker-compose.wt.yml found in worktree', 404);
129
+ }
130
+
131
+ try {
132
+ stopServices(wtPath);
133
+ json(res, { ok: true });
134
+ } catch (err) {
135
+ error(res, `Failed to stop services: ${err.message}`, 500);
136
+ }
137
+ });
138
+
139
+ // Get services status
140
+ router.get('/api/features/:name/wt/services', async (req, res, { params, root }) => {
141
+ const branch = getFeatureBranch(params.name);
142
+ const wtPath = getWorktreePath(branch, root);
143
+
144
+ if (!wtPath) {
145
+ return json(res, { services: [], hasServices: false });
146
+ }
147
+
148
+ const hasServices = hasDockerServices(wtPath);
149
+ const services = hasServices ? getServicesStatus(wtPath) : [];
150
+
151
+ json(res, { services, hasServices });
152
+ });
153
+ }
@@ -63,6 +63,188 @@ function FileList({ title, files, selectedFile, onSelect }) {
63
63
  );
64
64
  }
65
65
 
66
+ function UserPreferences() {
67
+ const [prefs, setPrefs] = React.useState({
68
+ user_name: '',
69
+ communication_language: 'English',
70
+ });
71
+ const [configPath, setConfigPath] = React.useState('');
72
+ const [loading, setLoading] = React.useState(true);
73
+ const [saving, setSaving] = React.useState(false);
74
+ const [dirty, setDirty] = React.useState(false);
75
+ const [msg, setMsg] = React.useState(null);
76
+
77
+ React.useEffect(() => {
78
+ api.get('/user-config').then(data => {
79
+ const parsed = data.parsed || {};
80
+ setPrefs({
81
+ user_name: parsed.user_name || '',
82
+ communication_language: parsed.communication_language || 'English',
83
+ });
84
+ setConfigPath(data.path || '');
85
+ }).catch(() => {}).finally(() => setLoading(false));
86
+ }, []);
87
+
88
+ const handleChange = (field, value) => {
89
+ setPrefs(p => ({ ...p, [field]: value }));
90
+ setDirty(true);
91
+ };
92
+
93
+ const save = async () => {
94
+ setSaving(true);
95
+ setMsg(null);
96
+ try {
97
+ await api.patch('/user-config', prefs);
98
+ setDirty(false);
99
+ setMsg({ type: 'ok', text: 'Saved.' });
100
+ } catch (e) {
101
+ setMsg({ type: 'err', text: e.message });
102
+ }
103
+ setSaving(false);
104
+ };
105
+
106
+ if (loading) return React.createElement('p', { className: 'text-slate-500 text-sm' }, 'Loading...');
107
+
108
+ const languages = ['English', 'French', 'Spanish', 'German', 'Portuguese', 'Italian', 'Dutch', 'Russian', 'Chinese', 'Japanese', 'Korean'];
109
+
110
+ return React.createElement('div', { className: 'bg-aia-card border border-aia-border rounded p-4 space-y-4' },
111
+ React.createElement('div', { className: 'flex items-center justify-between' },
112
+ React.createElement('h3', { className: 'text-sm font-semibold text-cyan-400' }, 'User Preferences'),
113
+ React.createElement('div', { className: 'flex gap-2 items-center' },
114
+ dirty && React.createElement('span', { className: 'text-xs text-amber-400' }, 'unsaved'),
115
+ msg && React.createElement('span', { className: `text-xs ${msg.type === 'ok' ? 'text-emerald-400' : 'text-red-400'}` }, msg.text),
116
+ React.createElement('button', {
117
+ onClick: save,
118
+ disabled: saving || !dirty,
119
+ className: 'bg-aia-accent/20 text-aia-accent border border-aia-accent/30 rounded px-3 py-1 text-xs hover:bg-aia-accent/30 disabled:opacity-40',
120
+ }, saving ? '...' : 'Save'),
121
+ ),
122
+ ),
123
+
124
+ React.createElement('div', { className: 'grid grid-cols-2 gap-4' },
125
+ // User name
126
+ React.createElement('div', { className: 'space-y-1' },
127
+ React.createElement('label', { className: 'text-xs text-slate-400' }, 'Your Name'),
128
+ React.createElement('input', {
129
+ type: 'text',
130
+ value: prefs.user_name,
131
+ onChange: e => handleChange('user_name', e.target.value),
132
+ placeholder: 'Optional',
133
+ className: 'w-full bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-aia-accent focus:outline-none',
134
+ }),
135
+ ),
136
+
137
+ // Communication language
138
+ React.createElement('div', { className: 'space-y-1' },
139
+ React.createElement('label', { className: 'text-xs text-slate-400' }, 'Communication Language'),
140
+ React.createElement('select', {
141
+ value: prefs.communication_language,
142
+ onChange: e => handleChange('communication_language', e.target.value),
143
+ className: 'w-full bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 focus:border-aia-accent focus:outline-none',
144
+ },
145
+ ...languages.map(lang => React.createElement('option', { key: lang, value: lang }, lang)),
146
+ ),
147
+ ),
148
+ ),
149
+
150
+ React.createElement('p', { className: 'text-xs text-slate-500' },
151
+ 'Your personal preferences. Stored globally in ~/.aia/ and apply to all projects.'
152
+ ),
153
+ configPath && React.createElement('p', { className: 'text-xs text-slate-600' },
154
+ `Location: ${configPath}`
155
+ ),
156
+ );
157
+ }
158
+
159
+ function ProjectSettings({ onSaved }) {
160
+ const [settings, setSettings] = React.useState({
161
+ projectName: '',
162
+ document_output_language: 'English',
163
+ });
164
+ const [loading, setLoading] = React.useState(true);
165
+ const [saving, setSaving] = React.useState(false);
166
+ const [dirty, setDirty] = React.useState(false);
167
+ const [msg, setMsg] = React.useState(null);
168
+
169
+ React.useEffect(() => {
170
+ api.get('/config').then(data => {
171
+ const parsed = data.parsed || {};
172
+ setSettings({
173
+ projectName: parsed.projectName || '',
174
+ document_output_language: parsed.document_output_language || 'English',
175
+ });
176
+ }).catch(() => {}).finally(() => setLoading(false));
177
+ }, []);
178
+
179
+ const handleChange = (field, value) => {
180
+ setSettings(p => ({ ...p, [field]: value }));
181
+ setDirty(true);
182
+ };
183
+
184
+ const save = async () => {
185
+ setSaving(true);
186
+ setMsg(null);
187
+ try {
188
+ await api.patch('/config/project', settings);
189
+ setDirty(false);
190
+ setMsg({ type: 'ok', text: 'Saved.' });
191
+ if (onSaved) onSaved();
192
+ } catch (e) {
193
+ setMsg({ type: 'err', text: e.message });
194
+ }
195
+ setSaving(false);
196
+ };
197
+
198
+ if (loading) return React.createElement('p', { className: 'text-slate-500 text-sm' }, 'Loading...');
199
+
200
+ const languages = ['English', 'French', 'Spanish', 'German', 'Portuguese', 'Italian', 'Dutch', 'Russian', 'Chinese', 'Japanese', 'Korean'];
201
+
202
+ return React.createElement('div', { className: 'bg-aia-card border border-aia-border rounded p-4 space-y-4' },
203
+ React.createElement('div', { className: 'flex items-center justify-between' },
204
+ React.createElement('h3', { className: 'text-sm font-semibold text-violet-400' }, 'Project Settings'),
205
+ React.createElement('div', { className: 'flex gap-2 items-center' },
206
+ dirty && React.createElement('span', { className: 'text-xs text-amber-400' }, 'unsaved'),
207
+ msg && React.createElement('span', { className: `text-xs ${msg.type === 'ok' ? 'text-emerald-400' : 'text-red-400'}` }, msg.text),
208
+ React.createElement('button', {
209
+ onClick: save,
210
+ disabled: saving || !dirty,
211
+ className: 'bg-aia-accent/20 text-aia-accent border border-aia-accent/30 rounded px-3 py-1 text-xs hover:bg-aia-accent/30 disabled:opacity-40',
212
+ }, saving ? '...' : 'Save'),
213
+ ),
214
+ ),
215
+
216
+ React.createElement('div', { className: 'grid grid-cols-2 gap-4' },
217
+ // Project name
218
+ React.createElement('div', { className: 'space-y-1' },
219
+ React.createElement('label', { className: 'text-xs text-slate-400' }, 'Project Name'),
220
+ React.createElement('input', {
221
+ type: 'text',
222
+ value: settings.projectName,
223
+ onChange: e => handleChange('projectName', e.target.value),
224
+ placeholder: 'My Project',
225
+ className: 'w-full bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 placeholder-slate-500 focus:border-aia-accent focus:outline-none',
226
+ }),
227
+ ),
228
+
229
+ // Document output language
230
+ React.createElement('div', { className: 'space-y-1' },
231
+ React.createElement('label', { className: 'text-xs text-slate-400' }, 'Document Output Language'),
232
+ React.createElement('select', {
233
+ value: settings.document_output_language,
234
+ onChange: e => handleChange('document_output_language', e.target.value),
235
+ className: 'w-full bg-slate-900 border border-aia-border rounded px-3 py-1.5 text-sm text-slate-200 focus:border-aia-accent focus:outline-none',
236
+ },
237
+ ...languages.map(lang => React.createElement('option', { key: lang, value: lang }, lang)),
238
+ ),
239
+ ),
240
+ ),
241
+
242
+ React.createElement('p', { className: 'text-xs text-slate-500' },
243
+ 'Project-wide settings. Stored in .aia/config.yaml and shared with your team.'
244
+ ),
245
+ );
246
+ }
247
+
66
248
  export function ConfigView() {
67
249
  const [contextFiles, setContextFiles] = React.useState([]);
68
250
  const [knowledgeCategories, setKnowledgeCategories] = React.useState([]);
@@ -71,12 +253,17 @@ export function ConfigView() {
71
253
  const [selectedCategory, setSelectedCategory] = React.useState(null);
72
254
  const [logs, setLogs] = React.useState('');
73
255
  const [showLogs, setShowLogs] = React.useState(false);
256
+ const [configVersion, setConfigVersion] = React.useState(0);
74
257
 
75
258
  React.useEffect(() => {
76
259
  api.get('/context').then(setContextFiles).catch(() => {});
77
260
  api.get('/knowledge').then(setKnowledgeCategories).catch(() => {});
78
261
  }, []);
79
262
 
263
+ const handlePreferencesSaved = () => {
264
+ setConfigVersion(v => v + 1);
265
+ };
266
+
80
267
  function selectContext(f) {
81
268
  setSelectedFile(f);
82
269
  setSelectedType('context');
@@ -100,9 +287,16 @@ export function ConfigView() {
100
287
  return React.createElement('div', { className: 'space-y-6' },
101
288
  React.createElement('h1', { className: 'text-xl font-bold text-slate-100' }, 'Configuration'),
102
289
 
103
- // config.yaml
290
+ // Two columns: User Preferences + Project Settings
291
+ React.createElement('div', { className: 'grid grid-cols-2 gap-4' },
292
+ React.createElement(UserPreferences),
293
+ React.createElement(ProjectSettings, { onSaved: handlePreferencesSaved }),
294
+ ),
295
+
296
+ // config.yaml (advanced)
104
297
  React.createElement(YamlEditor, {
105
- title: 'config.yaml',
298
+ key: `config-${configVersion}`,
299
+ title: 'config.yaml (advanced)',
106
300
  loadFn: async () => (await api.get('/config')).content,
107
301
  saveFn: async (content) => api.put('/config', { content }),
108
302
  }),
@@ -24,7 +24,13 @@ function FeatureCard({ feature }) {
24
24
  className: 'block bg-aia-card border border-aia-border rounded-lg p-4 hover:border-aia-accent/50 transition-colors',
25
25
  },
26
26
  React.createElement('div', { className: 'flex items-center justify-between mb-3' },
27
- React.createElement('h3', { className: 'text-slate-100 font-semibold' }, feature.name),
27
+ React.createElement('div', { className: 'flex items-center gap-2' },
28
+ React.createElement('h3', { className: 'text-slate-100 font-semibold' }, feature.name),
29
+ feature.hasWorktree && React.createElement('span', {
30
+ className: 'bg-orange-500/20 text-orange-400 text-xs px-1.5 py-0.5 rounded',
31
+ title: 'Git worktree active',
32
+ }, 'wt'),
33
+ ),
28
34
  React.createElement('span', { className: 'text-xs text-slate-500' },
29
35
  `${doneCount}/${totalCount} steps`
30
36
  ),
@@ -167,9 +173,40 @@ function QuickTicketForm({ onDone }) {
167
173
  );
168
174
  }
169
175
 
176
+ function FilterTabs({ filter, onChange, counts }) {
177
+ const tabs = [
178
+ { key: 'all', label: 'All', count: counts.all },
179
+ { key: 'with-wt', label: 'With Worktree', count: counts.withWt },
180
+ { key: 'without-wt', label: 'Without Worktree', count: counts.withoutWt },
181
+ ];
182
+
183
+ return React.createElement('div', { className: 'flex gap-1 border-b border-aia-border' },
184
+ ...tabs.map(tab =>
185
+ React.createElement('button', {
186
+ key: tab.key,
187
+ onClick: () => onChange(tab.key),
188
+ className: `px-3 py-1.5 text-xs border-b-2 transition-colors flex items-center gap-1.5 ${
189
+ filter === tab.key
190
+ ? 'border-aia-accent text-aia-accent'
191
+ : 'border-transparent text-slate-500 hover:text-slate-300'
192
+ }`,
193
+ },
194
+ tab.label,
195
+ tab.key === 'with-wt' && tab.count > 0 && React.createElement('span', {
196
+ className: 'bg-orange-500/20 text-orange-400 text-xs px-1.5 py-0.5 rounded',
197
+ }, tab.count),
198
+ tab.key !== 'with-wt' && React.createElement('span', {
199
+ className: 'text-slate-600',
200
+ }, `(${tab.count})`),
201
+ )
202
+ )
203
+ );
204
+ }
205
+
170
206
  export function Dashboard() {
171
207
  const [features, setFeatures] = React.useState([]);
172
208
  const [loading, setLoading] = React.useState(true);
209
+ const [filter, setFilter] = React.useState('all');
173
210
 
174
211
  async function load() {
175
212
  try {
@@ -181,6 +218,20 @@ export function Dashboard() {
181
218
 
182
219
  React.useEffect(() => { load(); }, []);
183
220
 
221
+ // Filter features
222
+ const filteredFeatures = features.filter(f => {
223
+ if (filter === 'with-wt') return f.hasWorktree;
224
+ if (filter === 'without-wt') return !f.hasWorktree;
225
+ return true;
226
+ });
227
+
228
+ // Counts for tabs
229
+ const counts = {
230
+ all: features.length,
231
+ withWt: features.filter(f => f.hasWorktree).length,
232
+ withoutWt: features.filter(f => !f.hasWorktree).length,
233
+ };
234
+
184
235
  return React.createElement('div', { className: 'space-y-6' },
185
236
  React.createElement('div', { className: 'flex items-center justify-between' },
186
237
  React.createElement('h1', { className: 'text-xl font-bold text-slate-100' }, 'Features'),
@@ -190,12 +241,21 @@ export function Dashboard() {
190
241
  // Quick ticket
191
242
  React.createElement(QuickTicketForm, { onDone: load }),
192
243
 
244
+ // Filter tabs
245
+ !loading && features.length > 0 && React.createElement(FilterTabs, {
246
+ filter,
247
+ onChange: setFilter,
248
+ counts,
249
+ }),
250
+
193
251
  loading
194
252
  ? React.createElement('p', { className: 'text-slate-500' }, 'Loading...')
195
253
  : features.length === 0
196
254
  ? React.createElement('p', { className: 'text-slate-500' }, 'No features yet. Create one to get started.')
197
- : React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4' },
198
- ...features.map(f => React.createElement(FeatureCard, { key: f.name, feature: f }))
199
- )
255
+ : filteredFeatures.length === 0
256
+ ? React.createElement('p', { className: 'text-slate-500' }, 'No features match this filter.')
257
+ : React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4' },
258
+ ...filteredFeatures.map(f => React.createElement(FeatureCard, { key: f.name, feature: f }))
259
+ )
200
260
  );
201
261
  }