@deploid/studio 2.0.4 → 2.0.6

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.
@@ -2,31 +2,396 @@ const cwdInput = document.getElementById('cwd');
2
2
  const pickButton = document.getElementById('pick');
3
3
  const runButton = document.getElementById('run');
4
4
  const stopButton = document.getElementById('stop');
5
- const cmdSelect = document.getElementById('cmd');
6
- const logs = document.getElementById('logs');
5
+ const cmdInput = document.getElementById('cmd');
6
+ const recentWrap = document.getElementById('recentWrap');
7
+ const statusPill = document.getElementById('statusPill');
7
8
  const status = document.getElementById('status');
9
+ const runStateTitle = document.getElementById('runStateTitle');
10
+ const projectTitle = document.getElementById('projectTitle');
11
+ const projectSubtitle = document.getElementById('projectSubtitle');
12
+ const metaStatus = document.getElementById('metaStatus');
13
+ const metaArtifacts = document.getElementById('metaArtifacts');
14
+ const metaDevices = document.getElementById('metaDevices');
15
+ const workflowGrid = document.getElementById('workflowGrid');
16
+ const blockersList = document.getElementById('blockersList');
17
+ const quickActionsWrap = document.getElementById('quickActions');
18
+ const artifactsList = document.getElementById('artifactsList');
19
+ const devicesList = document.getElementById('devicesList');
20
+ const logFilter = document.getElementById('logFilter');
21
+ const copyLogsButton = document.getElementById('copyLogs');
22
+ const clearLogsButton = document.getElementById('clearLogs');
23
+ const kpiTask = document.getElementById('kpiTask');
24
+ const kpiRuns = document.getElementById('kpiRuns');
25
+ const kpiResult = document.getElementById('kpiResult');
26
+ const logs = document.getElementById('logs');
27
+
28
+ const RECENT_CWDS_KEY = 'deploidStudio.recentCwds';
29
+ const MAX_RECENT_CWDS = 5;
30
+
31
+ const WORKFLOW_ACTIONS = {
32
+ init: 'init',
33
+ build: 'package',
34
+ release: 'doctor --fix',
35
+ deploy: 'deploy',
36
+ desktop: 'electron'
37
+ };
38
+
39
+ const ACTION_LIBRARY = [
40
+ {
41
+ key: 'doctor --summary',
42
+ title: 'Refresh readiness',
43
+ description: 'Re-run doctor and rebuild the dashboard state.',
44
+ intent: 'primary'
45
+ },
46
+ {
47
+ key: 'doctor --fix',
48
+ title: 'Apply safe fixes',
49
+ description: 'Create missing scaffolding and templates where doctor can do so safely.',
50
+ intent: 'secondary'
51
+ },
52
+ {
53
+ key: 'assets',
54
+ title: 'Generate assets',
55
+ description: 'Create icons and generated image assets from your configured source.',
56
+ intent: 'secondary'
57
+ },
58
+ {
59
+ key: 'package',
60
+ title: 'Package native shell',
61
+ description: 'Sync the web app into Capacitor and generate the Android project.',
62
+ intent: 'secondary'
63
+ },
64
+ {
65
+ key: 'build',
66
+ title: 'Build Android output',
67
+ description: 'Compile the APK/AAB artifacts available for deploy or release.',
68
+ intent: 'secondary'
69
+ },
70
+ {
71
+ key: 'deploy',
72
+ title: 'Deploy to device',
73
+ description: 'Install the latest debug build on connected Android devices.',
74
+ intent: 'secondary'
75
+ },
76
+ {
77
+ key: 'logs',
78
+ title: 'Tail device logs',
79
+ description: 'Stream device logs when you are in a troubleshooting loop.',
80
+ intent: 'secondary'
81
+ }
82
+ ];
83
+
84
+ const logEntries = [];
85
+ let runCount = 0;
86
+ let selectedCommand = 'doctor --summary';
87
+ let currentRunHadError = false;
88
+ let currentOverview = null;
8
89
 
9
- function appendLog(text) {
10
- logs.textContent += text;
90
+ function emptyState(message) {
91
+ const div = document.createElement('div');
92
+ div.className = 'empty';
93
+ div.textContent = message;
94
+ return div;
95
+ }
96
+
97
+ function appendLog(kind, text) {
98
+ logEntries.push({ kind, text });
99
+ renderLogs();
100
+ }
101
+
102
+ function renderLogs() {
103
+ const filter = logFilter.value;
104
+ logs.textContent = logEntries
105
+ .filter((entry) => filter === 'all' || entry.kind === filter)
106
+ .map((entry) => entry.text)
107
+ .join('');
11
108
  logs.scrollTop = logs.scrollHeight;
12
109
  }
13
110
 
14
- pickButton.addEventListener('click', async () => {
15
- const folder = await window.deploidStudio.chooseProject();
16
- if (folder) cwdInput.value = folder;
17
- });
111
+ function getRecentCwds() {
112
+ try {
113
+ const parsed = JSON.parse(localStorage.getItem(RECENT_CWDS_KEY) || '[]');
114
+ return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === 'string' && entry.length > 0) : [];
115
+ } catch {
116
+ return [];
117
+ }
118
+ }
119
+
120
+ function saveRecentCwds(cwds) {
121
+ localStorage.setItem(RECENT_CWDS_KEY, JSON.stringify(cwds.slice(0, MAX_RECENT_CWDS)));
122
+ }
123
+
124
+ function addRecentCwd(cwd) {
125
+ const next = [cwd, ...getRecentCwds().filter((entry) => entry !== cwd)];
126
+ saveRecentCwds(next);
127
+ renderRecentCwds();
128
+ }
129
+
130
+ function renderRecentCwds() {
131
+ const recents = getRecentCwds();
132
+ recentWrap.innerHTML = '';
133
+ for (const cwd of recents) {
134
+ const button = document.createElement('button');
135
+ button.className = 'ghost recent-pill';
136
+ button.type = 'button';
137
+ button.textContent = cwd.length > 36 ? `...${cwd.slice(-36)}` : cwd;
138
+ button.title = cwd;
139
+ button.addEventListener('click', () => {
140
+ cwdInput.value = cwd;
141
+ refreshOverview();
142
+ });
143
+ recentWrap.appendChild(button);
144
+ }
145
+ }
146
+
147
+ function setSelectedCommand(command) {
148
+ selectedCommand = command;
149
+ cmdInput.value = command;
150
+ kpiTask.textContent = command;
151
+ for (const button of quickActionsWrap.querySelectorAll('[data-command]')) {
152
+ button.style.outline = button.dataset.command === command ? '2px solid rgba(255,255,255,0.38)' : 'none';
153
+ }
154
+ }
155
+
156
+ function setRunningState(running) {
157
+ if (running) {
158
+ statusPill.textContent = 'Running';
159
+ statusPill.className = 'status-chip running';
160
+ runStateTitle.textContent = 'Task in progress';
161
+ status.textContent = `Running "${selectedCommand}" in the selected project.`;
162
+ } else {
163
+ statusPill.textContent = 'Ready';
164
+ statusPill.className = 'status-chip';
165
+ runStateTitle.textContent = currentOverview?.doctor?.ok ? 'Project looks healthy' : 'Action still needed';
166
+ status.textContent = currentOverview
167
+ ? 'Dashboard refreshed from project state.'
168
+ : 'Choose a project and Studio will pull readiness, blockers, and quick actions automatically.';
169
+ }
170
+ runButton.disabled = running;
171
+ }
172
+
173
+ function setErrorState(message) {
174
+ statusPill.textContent = 'Needs attention';
175
+ statusPill.className = 'status-chip error';
176
+ runStateTitle.textContent = 'Last command needs attention';
177
+ status.textContent = message;
178
+ }
179
+
180
+ function renderWorkflowGrid(workflows = []) {
181
+ workflowGrid.innerHTML = '';
182
+ if (!workflows.length) {
183
+ workflowGrid.appendChild(emptyState('Select a project to populate the workflow board.'));
184
+ return;
185
+ }
186
+
187
+ for (const workflow of workflows) {
188
+ const card = document.createElement('div');
189
+ card.className = 'workflow-card';
190
+ const command = WORKFLOW_ACTIONS[workflow.id] || 'doctor --summary';
191
+ card.innerHTML = `
192
+ <div class="workflow-top">
193
+ <div class="workflow-name">${workflow.title}</div>
194
+ <div class="workflow-score">${workflow.score}%</div>
195
+ </div>
196
+ <div class="workflow-state">${workflow.status.toUpperCase()}</div>
197
+ <div class="workflow-note">${workflow.nextAction || 'No blockers detected for this workflow.'}</div>
198
+ <button class="primary workflow-action" data-command="${command}">Run ${command}</button>
199
+ `;
200
+ card.querySelector('button').addEventListener('click', () => {
201
+ setSelectedCommand(command);
202
+ });
203
+ workflowGrid.appendChild(card);
204
+ }
205
+ }
206
+
207
+ function renderBlockers(checks = []) {
208
+ blockersList.innerHTML = '';
209
+ const issues = checks.filter((check) => check.status !== 'pass').slice(0, 8);
210
+ if (!issues.length) {
211
+ blockersList.appendChild(emptyState('Doctor is not reporting active blockers or warnings.'));
212
+ return;
213
+ }
214
+
215
+ for (const check of issues) {
216
+ const item = document.createElement('div');
217
+ item.className = 'list-item';
218
+ item.innerHTML = `
219
+ <div class="list-top">
220
+ <div class="list-title">${check.title}</div>
221
+ <div class="badge ${check.status}">${check.status}</div>
222
+ </div>
223
+ <div>${check.message}</div>
224
+ ${check.details ? `<div class="artifact-path">${check.details}</div>` : ''}
225
+ `;
226
+ blockersList.appendChild(item);
227
+ }
228
+ }
229
+
230
+ function computeRecommendedActions(overview) {
231
+ const checks = overview?.doctor?.checks || [];
232
+ const actions = [];
233
+
234
+ if (checks.some((check) => check.id === 'deploid-config' && check.status !== 'pass')) actions.push('init');
235
+ if (checks.some((check) => check.id === 'assets-source' && check.status !== 'pass')) actions.push('doctor --fix');
236
+ if (checks.some((check) => check.id === 'capacitor-config' && check.status !== 'pass')) actions.push('package');
237
+ if (checks.some((check) => check.id === 'android-project' && check.status !== 'pass')) actions.push('package');
238
+ if (checks.some((check) => check.id === 'android-signing' && check.status !== 'pass')) actions.push('doctor --fix');
239
+ if (overview?.devices?.count > 0) actions.push('deploy');
240
+
241
+ actions.push('doctor --summary', 'assets', 'build', 'logs');
242
+ return [...new Set(actions)].slice(0, 6);
243
+ }
244
+
245
+ function renderQuickActions(overview) {
246
+ quickActionsWrap.innerHTML = '';
247
+ const recommended = computeRecommendedActions(overview);
248
+ const items = ACTION_LIBRARY.filter((action) => recommended.includes(action.key));
249
+
250
+ for (const action of items) {
251
+ const button = document.createElement('button');
252
+ button.type = 'button';
253
+ button.className = 'quick-action';
254
+ button.dataset.command = action.key;
255
+ button.innerHTML = `<div>${action.title}</div><small>${action.description}</small>`;
256
+ button.addEventListener('click', () => setSelectedCommand(action.key));
257
+ quickActionsWrap.appendChild(button);
258
+ }
259
+ if (!items.length) {
260
+ quickActionsWrap.appendChild(emptyState('No quick actions available until a project overview is loaded.'));
261
+ } else if (!selectedCommand || !recommended.includes(selectedCommand)) {
262
+ setSelectedCommand(items[0].key);
263
+ }
264
+ }
265
+
266
+ function renderArtifacts(artifacts = []) {
267
+ artifactsList.innerHTML = '';
268
+ if (!artifacts.length) {
269
+ artifactsList.appendChild(emptyState('No APK, AAB, or desktop output is available yet.'));
270
+ return;
271
+ }
272
+
273
+ for (const artifact of artifacts) {
274
+ const item = document.createElement('div');
275
+ item.className = 'list-item';
276
+ item.innerHTML = `
277
+ <div class="list-top">
278
+ <div class="list-title">${artifact.label}</div>
279
+ <div class="badge pass">${artifact.size}</div>
280
+ </div>
281
+ <div class="artifact-path">${artifact.path}</div>
282
+ `;
283
+ artifactsList.appendChild(item);
284
+ }
285
+ }
286
+
287
+ function renderDevices(overview) {
288
+ devicesList.innerHTML = '';
289
+ const entries = overview?.devices?.entries || [];
290
+ const presence = overview?.presence || {};
291
+
292
+ const presenceItem = document.createElement('div');
293
+ presenceItem.className = 'list-item';
294
+ presenceItem.innerHTML = `
295
+ <div class="list-top">
296
+ <div class="list-title">Project surface</div>
297
+ <div class="badge ${presence.config ? 'pass' : 'warn'}">${presence.config ? 'ready' : 'missing config'}</div>
298
+ </div>
299
+ <div class="device-line">Config: ${presence.config ? 'yes' : 'no'} · Capacitor: ${presence.capacitor ? 'yes' : 'no'} · Android: ${presence.android ? 'yes' : 'no'} · Electron: ${presence.electron ? 'yes' : 'no'}</div>
300
+ `;
301
+ devicesList.appendChild(presenceItem);
302
+
303
+ if (!overview?.devices?.available) {
304
+ devicesList.appendChild(emptyState('ADB is not available in this environment.'));
305
+ return;
306
+ }
307
+
308
+ if (!entries.length) {
309
+ devicesList.appendChild(emptyState('ADB is available, but no Android devices are connected.'));
310
+ return;
311
+ }
312
+
313
+ for (const entry of entries) {
314
+ const item = document.createElement('div');
315
+ item.className = 'list-item';
316
+ item.innerHTML = `
317
+ <div class="list-top">
318
+ <div class="list-title">${entry.id}</div>
319
+ <div class="badge ${entry.status === 'device' ? 'pass' : 'warn'}">${entry.status}</div>
320
+ </div>
321
+ <div class="device-line">ADB target state for deploy/log workflows.</div>
322
+ `;
323
+ devicesList.appendChild(item);
324
+ }
325
+ }
326
+
327
+ function renderOverview(overview) {
328
+ currentOverview = overview;
329
+
330
+ if (!overview) {
331
+ projectTitle.textContent = 'Deploid Studio';
332
+ projectSubtitle.textContent = 'Pick a project folder to turn this into a workflow dashboard.';
333
+ metaStatus.textContent = 'No project';
334
+ metaArtifacts.textContent = '0';
335
+ metaDevices.textContent = '0';
336
+ renderWorkflowGrid();
337
+ renderBlockers();
338
+ renderQuickActions(null);
339
+ renderArtifacts();
340
+ renderDevices(null);
341
+ return;
342
+ }
343
+
344
+ projectTitle.textContent = overview.projectName || 'Deploid project';
345
+ projectSubtitle.textContent = overview.doctor?.ok
346
+ ? 'This project is in a healthy state. Use the workflow board to keep moving without dropping into the terminal.'
347
+ : `Studio found ${overview.doctor?.totals?.fail || 0} blockers and ${overview.doctor?.totals?.warn || 0} warnings that should shape your next move.`;
348
+ metaStatus.textContent = overview.doctor?.ok ? 'Healthy' : 'Action needed';
349
+ metaArtifacts.textContent = String((overview.artifacts || []).length);
350
+ metaDevices.textContent = String(overview.devices?.count || 0);
351
+
352
+ renderWorkflowGrid(overview.doctor?.workflows || []);
353
+ renderBlockers(overview.doctor?.checks || []);
354
+ renderQuickActions(overview);
355
+ renderArtifacts(overview.artifacts || []);
356
+ renderDevices(overview);
357
+ }
358
+
359
+ async function refreshOverview() {
360
+ const cwd = cwdInput.value.trim();
361
+ if (!cwd) {
362
+ renderOverview(null);
363
+ return;
364
+ }
365
+
366
+ try {
367
+ const overview = await window.deploidStudio.getProjectOverview(cwd);
368
+ renderOverview(overview);
369
+ } catch {
370
+ renderOverview(null);
371
+ }
372
+ }
18
373
 
19
374
  runButton.addEventListener('click', async () => {
20
375
  const cwd = cwdInput.value.trim();
21
376
  if (!cwd) {
22
- appendLog('Select a project folder first.\n');
377
+ setErrorState('Choose a project folder before running an action.');
378
+ appendLog('system', 'Choose a project folder before running an action.\n');
23
379
  return;
24
380
  }
25
- const command = cmdSelect.value;
381
+
382
+ const command = cmdInput.value || selectedCommand;
26
383
  try {
384
+ addRecentCwd(cwd);
385
+ runCount += 1;
386
+ currentRunHadError = false;
387
+ kpiRuns.textContent = String(runCount);
388
+ kpiResult.textContent = 'Running';
389
+ setRunningState(true);
27
390
  await window.deploidStudio.runCommand(cwd, command);
28
391
  } catch (error) {
29
- appendLog(`Error: ${error.message}\n`);
392
+ kpiResult.textContent = 'Failed';
393
+ setErrorState(error.message);
394
+ appendLog('stderr', `Error: ${error.message}\n`);
30
395
  }
31
396
  });
32
397
 
@@ -35,16 +400,50 @@ stopButton.addEventListener('click', async () => {
35
400
  });
36
401
 
37
402
  window.deploidStudio.onLog((entry) => {
38
- appendLog(entry.message);
403
+ if (entry.kind === 'stderr') currentRunHadError = true;
404
+ appendLog(entry.kind, entry.message);
39
405
  });
40
406
 
41
407
  window.deploidStudio.onState((state) => {
42
- status.textContent = state.running ? 'Running...' : 'Idle';
43
- runButton.disabled = state.running;
408
+ setRunningState(state.running);
409
+ if (!state.running) {
410
+ kpiResult.textContent = currentRunHadError ? 'Warning' : 'Success';
411
+ refreshOverview();
412
+ }
44
413
  });
45
414
 
46
- window.deploidStudio.getDefaultCwd().then((cwd) => {
47
- if (!cwdInput.value) {
48
- cwdInput.value = cwd;
415
+ logFilter.addEventListener('change', renderLogs);
416
+
417
+ clearLogsButton.addEventListener('click', () => {
418
+ logEntries.length = 0;
419
+ renderLogs();
420
+ });
421
+
422
+ copyLogsButton.addEventListener('click', async () => {
423
+ const text = logs.textContent || '';
424
+ if (!text) return;
425
+ try {
426
+ await navigator.clipboard.writeText(text);
427
+ status.textContent = 'Activity copied to clipboard.';
428
+ } catch {
429
+ status.textContent = 'Unable to copy logs from this environment.';
49
430
  }
50
431
  });
432
+
433
+ window.deploidStudio.getDefaultCwd().then((cwd) => {
434
+ if (!cwdInput.value) cwdInput.value = cwd;
435
+ addRecentCwd(cwd);
436
+ renderRecentCwds();
437
+ refreshOverview();
438
+ });
439
+
440
+ pickButton.addEventListener('click', async () => {
441
+ const folder = await window.deploidStudio.chooseProject();
442
+ if (!folder) return;
443
+ cwdInput.value = folder;
444
+ addRecentCwd(folder);
445
+ refreshOverview();
446
+ });
447
+
448
+ renderRecentCwds();
449
+ renderOverview(null);