@deploid/studio 2.0.5 → 2.0.7

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.
@@ -3,9 +3,20 @@ const pickButton = document.getElementById('pick');
3
3
  const runButton = document.getElementById('run');
4
4
  const stopButton = document.getElementById('stop');
5
5
  const cmdInput = document.getElementById('cmd');
6
- const commandsWrap = document.getElementById('commands');
7
6
  const recentWrap = document.getElementById('recentWrap');
8
7
  const statusPill = document.getElementById('statusPill');
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');
9
20
  const logFilter = document.getElementById('logFilter');
10
21
  const copyLogsButton = document.getElementById('copyLogs');
11
22
  const clearLogsButton = document.getElementById('clearLogs');
@@ -13,31 +24,75 @@ const kpiTask = document.getElementById('kpiTask');
13
24
  const kpiRuns = document.getElementById('kpiRuns');
14
25
  const kpiResult = document.getElementById('kpiResult');
15
26
  const logs = document.getElementById('logs');
16
- const status = document.getElementById('status');
17
27
 
18
28
  const RECENT_CWDS_KEY = 'deploidStudio.recentCwds';
19
29
  const MAX_RECENT_CWDS = 5;
20
30
 
21
- const COMMANDS = [
22
- { key: 'init', title: 'Set up project', category: 'Setup', description: 'Create or reconfigure Deploid project files.' },
23
- { key: 'assets', title: 'Generate assets', category: 'Setup', description: 'Generate app icons and splash assets.' },
24
- { key: 'build', title: 'Build app', category: 'Build', description: 'Compile the app and prepare platform artifacts.' },
25
- { key: 'package', title: 'Package release', category: 'Build', description: 'Create a distributable package for testing/release.' },
26
- { key: 'deploy', title: 'Deploy', category: 'Release', description: 'Deploy app artifacts to connected delivery targets.' },
27
- { key: 'devices', title: 'List devices', category: 'Debug', description: 'Inspect connected iOS/Android devices.' },
28
- { key: 'logs', title: 'View logs', category: 'Debug', description: 'Stream runtime logs from the current app.' },
29
- { key: 'debug', title: 'Debug mode', category: 'Debug', description: 'Run developer debug workflow for troubleshooting.' },
30
- { key: 'ios', title: 'Open iOS workflow', category: 'Platform', description: 'Run iOS-specific build and sync workflow.' },
31
- { key: 'ios:handbook', title: 'iOS handbook', category: 'Platform', description: 'Show iOS workflow help and guidance.' },
32
- { key: 'firebase', title: 'Firebase helper', category: 'Integrations', description: 'Run Firebase-related setup and utility tasks.' },
33
- { key: 'plugin', title: 'Plugin helper', category: 'Integrations', description: 'Manage plugin tasks and project integration.' },
34
- { key: 'uninstall', title: 'Uninstall setup', category: 'Maintenance', description: 'Remove Deploid-specific project setup.' }
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
+ }
35
82
  ];
36
83
 
37
84
  const logEntries = [];
38
85
  let runCount = 0;
39
- let selectedCommand = 'build';
86
+ let selectedCommand = 'doctor --summary';
40
87
  let currentRunHadError = false;
88
+ let currentOverview = null;
89
+
90
+ function emptyState(message) {
91
+ const div = document.createElement('div');
92
+ div.className = 'empty';
93
+ div.textContent = message;
94
+ return div;
95
+ }
41
96
 
42
97
  function appendLog(kind, text) {
43
98
  logEntries.push({ kind, text });
@@ -46,49 +101,17 @@ function appendLog(kind, text) {
46
101
 
47
102
  function renderLogs() {
48
103
  const filter = logFilter.value;
49
- const text = logEntries
104
+ logs.textContent = logEntries
50
105
  .filter((entry) => filter === 'all' || entry.kind === filter)
51
106
  .map((entry) => entry.text)
52
107
  .join('');
53
- logs.textContent = text;
54
108
  logs.scrollTop = logs.scrollHeight;
55
109
  }
56
110
 
57
- function updateSelectedCommand(command) {
58
- selectedCommand = command;
59
- cmdInput.value = command;
60
- kpiTask.textContent = command;
61
- for (const el of commandsWrap.querySelectorAll('.command')) {
62
- el.classList.toggle('active', el.dataset.command === command);
63
- }
64
- }
65
-
66
- function setRunningState(running) {
67
- if (running) {
68
- statusPill.textContent = 'Running';
69
- statusPill.classList.add('running');
70
- statusPill.classList.remove('error');
71
- status.textContent = `Running "${selectedCommand}"...`;
72
- } else {
73
- statusPill.textContent = 'Ready';
74
- statusPill.classList.remove('running');
75
- status.textContent = 'Ready to run';
76
- }
77
- runButton.disabled = running;
78
- }
79
-
80
- function setErrorState(message) {
81
- statusPill.textContent = 'Needs attention';
82
- statusPill.classList.remove('running');
83
- statusPill.classList.add('error');
84
- status.textContent = message;
85
- }
86
-
87
111
  function getRecentCwds() {
88
112
  try {
89
113
  const parsed = JSON.parse(localStorage.getItem(RECENT_CWDS_KEY) || '[]');
90
- if (!Array.isArray(parsed)) return [];
91
- return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
114
+ return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === 'string' && entry.length > 0) : [];
92
115
  } catch {
93
116
  return [];
94
117
  }
@@ -109,44 +132,253 @@ function renderRecentCwds() {
109
132
  recentWrap.innerHTML = '';
110
133
  for (const cwd of recents) {
111
134
  const button = document.createElement('button');
112
- button.className = 'ghost';
135
+ button.className = 'ghost recent-pill';
113
136
  button.type = 'button';
137
+ button.textContent = cwd.length > 36 ? `...${cwd.slice(-36)}` : cwd;
114
138
  button.title = cwd;
115
- button.textContent = cwd.length > 34 ? `...${cwd.slice(-34)}` : cwd;
116
139
  button.addEventListener('click', () => {
117
140
  cwdInput.value = cwd;
141
+ refreshOverview();
118
142
  });
119
143
  recentWrap.appendChild(button);
120
144
  }
121
145
  }
122
146
 
123
- function renderCommands() {
124
- commandsWrap.innerHTML = '';
125
- for (const command of COMMANDS) {
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) {
126
251
  const button = document.createElement('button');
127
252
  button.type = 'button';
128
- button.className = 'command';
129
- button.dataset.command = command.key;
130
- button.innerHTML = `
131
- <div class="command-title">
132
- <span>${command.title}</span>
133
- <span class="command-tag">${command.category}</span>
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>
134
280
  </div>
135
- <div class="command-desc">${command.description}</div>
281
+ <div class="artifact-path">${artifact.path}</div>
136
282
  `;
137
- button.addEventListener('click', () => updateSelectedCommand(command.key));
138
- commandsWrap.appendChild(button);
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);
139
371
  }
140
- updateSelectedCommand(selectedCommand);
141
372
  }
142
373
 
143
374
  runButton.addEventListener('click', async () => {
144
375
  const cwd = cwdInput.value.trim();
145
376
  if (!cwd) {
146
- setErrorState('Choose a project folder before running a task.');
147
- appendLog('system', 'Choose a project folder before running a task.\n');
377
+ setErrorState('Choose a project folder before running an action.');
378
+ appendLog('system', 'Choose a project folder before running an action.\n');
148
379
  return;
149
380
  }
381
+
150
382
  const command = cmdInput.value || selectedCommand;
151
383
  try {
152
384
  addRecentCwd(cwd);
@@ -168,9 +400,7 @@ stopButton.addEventListener('click', async () => {
168
400
  });
169
401
 
170
402
  window.deploidStudio.onLog((entry) => {
171
- if (entry.kind === 'stderr') {
172
- currentRunHadError = true;
173
- }
403
+ if (entry.kind === 'stderr') currentRunHadError = true;
174
404
  appendLog(entry.kind, entry.message);
175
405
  });
176
406
 
@@ -178,12 +408,11 @@ window.deploidStudio.onState((state) => {
178
408
  setRunningState(state.running);
179
409
  if (!state.running) {
180
410
  kpiResult.textContent = currentRunHadError ? 'Warning' : 'Success';
411
+ refreshOverview();
181
412
  }
182
413
  });
183
414
 
184
- logFilter.addEventListener('change', () => {
185
- renderLogs();
186
- });
415
+ logFilter.addEventListener('change', renderLogs);
187
416
 
188
417
  clearLogsButton.addEventListener('click', () => {
189
418
  logEntries.length = 0;
@@ -202,18 +431,19 @@ copyLogsButton.addEventListener('click', async () => {
202
431
  });
203
432
 
204
433
  window.deploidStudio.getDefaultCwd().then((cwd) => {
205
- if (!cwdInput.value) {
206
- cwdInput.value = cwd;
207
- }
434
+ if (!cwdInput.value) cwdInput.value = cwd;
208
435
  addRecentCwd(cwd);
436
+ renderRecentCwds();
437
+ refreshOverview();
209
438
  });
210
439
 
211
440
  pickButton.addEventListener('click', async () => {
212
- const folder = await window.deploidStudio.chooseProject();
441
+ const folder = await window.deploidStudio.chooseProject(cwdInput.value.trim());
213
442
  if (!folder) return;
214
443
  cwdInput.value = folder;
215
444
  addRecentCwd(folder);
445
+ refreshOverview();
216
446
  });
217
447
 
218
- renderCommands();
219
448
  renderRecentCwds();
449
+ renderOverview(null);