@deploid/studio 2.0.4 → 2.0.5
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/README.md +8 -2
- package/dist/renderer/index.html +281 -52
- package/dist/renderer/renderer.js +182 -13
- package/package.json +1 -1
- package/renderer/index.html +281 -52
- package/renderer/renderer.js +182 -13
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Deploid Studio
|
|
2
2
|
|
|
3
|
-
Desktop
|
|
3
|
+
Desktop app for running Deploid workflows in a project folder with a guided UI.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -14,7 +14,13 @@ npm install -g @deploid/studio
|
|
|
14
14
|
deploid-studio
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## What You Get
|
|
18
|
+
|
|
19
|
+
- Project folder picker with recent-folder shortcuts.
|
|
20
|
+
- Guided task selection cards (setup, build, deploy, debug, platform, integrations).
|
|
21
|
+
- Live activity panel with output filters and copy/clear controls.
|
|
22
|
+
- Run state indicators and quick session metrics.
|
|
18
23
|
|
|
24
|
+
## Notes
|
|
19
25
|
- Deploid Studio executes `@deploid/cli` commands from the selected working directory.
|
|
20
26
|
- Packaging in Deploid 2.0 is `capacitor` only.
|
package/dist/renderer/index.html
CHANGED
|
@@ -6,62 +6,214 @@
|
|
|
6
6
|
<title>Deploid Studio</title>
|
|
7
7
|
<style>
|
|
8
8
|
:root {
|
|
9
|
-
--bg: #
|
|
10
|
-
--
|
|
11
|
-
--line: #
|
|
12
|
-
--text: #
|
|
13
|
-
--muted: #
|
|
14
|
-
--accent: #
|
|
9
|
+
--bg: #f2f6f3;
|
|
10
|
+
--panel: #ffffff;
|
|
11
|
+
--line: #d4dfd8;
|
|
12
|
+
--text: #112019;
|
|
13
|
+
--muted: #52685d;
|
|
14
|
+
--accent: #1ea96a;
|
|
15
|
+
--accent-strong: #0f7f4c;
|
|
16
|
+
--warning: #f3a62a;
|
|
17
|
+
--error: #cf3d2e;
|
|
18
|
+
--shadow: 0 14px 38px rgba(31, 61, 45, 0.1);
|
|
19
|
+
}
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
15
22
|
}
|
|
16
23
|
body {
|
|
17
24
|
margin: 0;
|
|
18
|
-
font-family: "Segoe UI", Arial, sans-serif;
|
|
19
|
-
background:
|
|
25
|
+
font-family: "Avenir Next", "Segoe UI", Arial, sans-serif;
|
|
26
|
+
background:
|
|
27
|
+
radial-gradient(circle at top right, rgba(31, 169, 106, 0.18), transparent 34%),
|
|
28
|
+
radial-gradient(circle at top left, rgba(45, 123, 180, 0.12), transparent 24%),
|
|
29
|
+
var(--bg);
|
|
20
30
|
color: var(--text);
|
|
31
|
+
min-height: 100vh;
|
|
21
32
|
}
|
|
22
33
|
.wrap {
|
|
23
|
-
max-width:
|
|
24
|
-
margin:
|
|
25
|
-
padding: 0
|
|
34
|
+
max-width: 1140px;
|
|
35
|
+
margin: 32px auto 48px;
|
|
36
|
+
padding: 0 20px;
|
|
37
|
+
}
|
|
38
|
+
.shell {
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: 360px 1fr;
|
|
41
|
+
gap: 18px;
|
|
26
42
|
}
|
|
27
43
|
.card {
|
|
28
|
-
background:
|
|
44
|
+
background: var(--panel);
|
|
29
45
|
border: 1px solid var(--line);
|
|
30
|
-
border-radius:
|
|
31
|
-
padding:
|
|
46
|
+
border-radius: 16px;
|
|
47
|
+
padding: 18px;
|
|
48
|
+
box-shadow: var(--shadow);
|
|
49
|
+
}
|
|
50
|
+
h1 {
|
|
51
|
+
margin: 0;
|
|
52
|
+
font-size: 26px;
|
|
53
|
+
letter-spacing: -0.02em;
|
|
54
|
+
}
|
|
55
|
+
h2 {
|
|
56
|
+
margin: 0 0 8px;
|
|
57
|
+
font-size: 18px;
|
|
58
|
+
}
|
|
59
|
+
p {
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
.hero {
|
|
63
|
+
margin-bottom: 14px;
|
|
64
|
+
}
|
|
65
|
+
.hero p {
|
|
66
|
+
color: var(--muted);
|
|
67
|
+
margin-top: 6px;
|
|
68
|
+
}
|
|
69
|
+
.status-pill {
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 6px;
|
|
73
|
+
padding: 7px 11px;
|
|
74
|
+
border-radius: 999px;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
border: 1px solid #b7dac9;
|
|
77
|
+
color: #165f3d;
|
|
78
|
+
background: #ecf9f3;
|
|
79
|
+
margin-top: 12px;
|
|
80
|
+
}
|
|
81
|
+
.status-pill.running {
|
|
82
|
+
color: #754b00;
|
|
83
|
+
border-color: #efddba;
|
|
84
|
+
background: #fff7e7;
|
|
85
|
+
}
|
|
86
|
+
.status-pill.error {
|
|
87
|
+
color: #7a1f14;
|
|
88
|
+
border-color: #efcabf;
|
|
89
|
+
background: #ffefeb;
|
|
90
|
+
}
|
|
91
|
+
.left-col,
|
|
92
|
+
.right-col {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 14px;
|
|
32
96
|
}
|
|
33
97
|
.row {
|
|
34
98
|
display: flex;
|
|
35
99
|
gap: 12px;
|
|
36
|
-
align-items:
|
|
100
|
+
align-items: stretch;
|
|
37
101
|
flex-wrap: wrap;
|
|
38
102
|
}
|
|
39
|
-
|
|
40
|
-
|
|
103
|
+
.row.stack {
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
}
|
|
106
|
+
label {
|
|
107
|
+
font-size: 13px;
|
|
108
|
+
color: var(--muted);
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
margin-bottom: 8px;
|
|
111
|
+
display: block;
|
|
112
|
+
}
|
|
113
|
+
input,
|
|
114
|
+
select,
|
|
115
|
+
button {
|
|
116
|
+
border-radius: 10px;
|
|
41
117
|
border: 1px solid var(--line);
|
|
42
|
-
background: #
|
|
118
|
+
background: #ffffff;
|
|
43
119
|
color: var(--text);
|
|
44
120
|
padding: 10px 12px;
|
|
121
|
+
font-size: 14px;
|
|
45
122
|
}
|
|
46
123
|
input {
|
|
47
124
|
flex: 1;
|
|
48
|
-
min-width:
|
|
125
|
+
min-width: 180px;
|
|
49
126
|
}
|
|
50
127
|
button {
|
|
51
128
|
cursor: pointer;
|
|
129
|
+
transition: transform 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
|
|
130
|
+
}
|
|
131
|
+
button:hover {
|
|
132
|
+
transform: translateY(-1px);
|
|
52
133
|
}
|
|
53
134
|
button.primary {
|
|
54
135
|
background: var(--accent);
|
|
55
|
-
color: #
|
|
56
|
-
border:
|
|
136
|
+
color: #ffffff;
|
|
137
|
+
border-color: var(--accent-strong);
|
|
138
|
+
font-weight: 700;
|
|
139
|
+
}
|
|
140
|
+
button.primary:hover {
|
|
141
|
+
background: var(--accent-strong);
|
|
142
|
+
}
|
|
143
|
+
button.ghost {
|
|
144
|
+
background: #f7faf8;
|
|
145
|
+
}
|
|
146
|
+
button:disabled {
|
|
147
|
+
opacity: 0.6;
|
|
148
|
+
cursor: not-allowed;
|
|
149
|
+
transform: none;
|
|
150
|
+
}
|
|
151
|
+
.commands {
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-template-columns: 1fr;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
}
|
|
156
|
+
.command {
|
|
157
|
+
text-align: left;
|
|
158
|
+
border-radius: 12px;
|
|
159
|
+
padding: 12px;
|
|
160
|
+
border: 1px solid var(--line);
|
|
161
|
+
background: #ffffff;
|
|
162
|
+
}
|
|
163
|
+
.command:hover {
|
|
164
|
+
border-color: #9ed0b8;
|
|
165
|
+
background: #f6fcf9;
|
|
166
|
+
}
|
|
167
|
+
.command.active {
|
|
168
|
+
border-color: var(--accent);
|
|
169
|
+
background: #eefaf4;
|
|
170
|
+
}
|
|
171
|
+
.command-title {
|
|
172
|
+
display: flex;
|
|
173
|
+
justify-content: space-between;
|
|
174
|
+
align-items: center;
|
|
57
175
|
font-weight: 700;
|
|
176
|
+
margin-bottom: 4px;
|
|
177
|
+
}
|
|
178
|
+
.command-tag {
|
|
179
|
+
font-size: 11px;
|
|
180
|
+
padding: 4px 8px;
|
|
181
|
+
border-radius: 999px;
|
|
182
|
+
background: #edf4f0;
|
|
183
|
+
color: #395245;
|
|
184
|
+
}
|
|
185
|
+
.command-desc {
|
|
186
|
+
color: var(--muted);
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
}
|
|
189
|
+
.recent {
|
|
190
|
+
margin-top: 10px;
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-wrap: wrap;
|
|
193
|
+
gap: 8px;
|
|
194
|
+
}
|
|
195
|
+
.recent button {
|
|
196
|
+
padding: 7px 10px;
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
background: #f3f8f5;
|
|
199
|
+
}
|
|
200
|
+
.log-toolbar {
|
|
201
|
+
display: flex;
|
|
202
|
+
justify-content: space-between;
|
|
203
|
+
gap: 8px;
|
|
204
|
+
align-items: center;
|
|
205
|
+
margin-bottom: 8px;
|
|
206
|
+
}
|
|
207
|
+
.log-toolbar .row {
|
|
208
|
+
gap: 8px;
|
|
58
209
|
}
|
|
59
210
|
#logs {
|
|
60
|
-
margin-top:
|
|
61
|
-
background: #
|
|
211
|
+
margin-top: 10px;
|
|
212
|
+
background: #0f1a15;
|
|
62
213
|
border: 1px solid var(--line);
|
|
63
|
-
|
|
64
|
-
|
|
214
|
+
color: #d9fce9;
|
|
215
|
+
border-radius: 10px;
|
|
216
|
+
height: 470px;
|
|
65
217
|
overflow: auto;
|
|
66
218
|
padding: 12px;
|
|
67
219
|
white-space: pre-wrap;
|
|
@@ -70,45 +222,122 @@
|
|
|
70
222
|
}
|
|
71
223
|
.muted {
|
|
72
224
|
color: var(--muted);
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
}
|
|
227
|
+
.hint {
|
|
228
|
+
margin-top: 8px;
|
|
73
229
|
font-size: 12px;
|
|
74
230
|
}
|
|
231
|
+
.kpi {
|
|
232
|
+
display: grid;
|
|
233
|
+
grid-template-columns: repeat(3, 1fr);
|
|
234
|
+
gap: 8px;
|
|
235
|
+
margin-top: 12px;
|
|
236
|
+
}
|
|
237
|
+
.kpi-item {
|
|
238
|
+
border: 1px solid var(--line);
|
|
239
|
+
border-radius: 12px;
|
|
240
|
+
padding: 10px;
|
|
241
|
+
background: #fbfdfc;
|
|
242
|
+
}
|
|
243
|
+
.kpi-label {
|
|
244
|
+
font-size: 12px;
|
|
245
|
+
color: var(--muted);
|
|
246
|
+
}
|
|
247
|
+
.kpi-value {
|
|
248
|
+
margin-top: 4px;
|
|
249
|
+
font-size: 16px;
|
|
250
|
+
font-weight: 700;
|
|
251
|
+
}
|
|
75
252
|
@media (max-width: 640px) {
|
|
76
|
-
|
|
77
|
-
|
|
253
|
+
.wrap {
|
|
254
|
+
margin: 16px auto 24px;
|
|
255
|
+
padding: 0 12px;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
@media (max-width: 980px) {
|
|
259
|
+
.shell {
|
|
260
|
+
grid-template-columns: 1fr;
|
|
78
261
|
}
|
|
79
262
|
}
|
|
80
263
|
</style>
|
|
81
264
|
</head>
|
|
82
265
|
<body>
|
|
83
266
|
<div class="wrap">
|
|
84
|
-
<div class="
|
|
85
|
-
<
|
|
86
|
-
<p
|
|
87
|
-
<div class="
|
|
88
|
-
|
|
89
|
-
|
|
267
|
+
<div class="hero">
|
|
268
|
+
<h1>Deploid Studio</h1>
|
|
269
|
+
<p>Build and ship your mobile app through a guided workflow, not a terminal window.</p>
|
|
270
|
+
<div id="statusPill" class="status-pill">Ready</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div class="shell">
|
|
274
|
+
<div class="left-col">
|
|
275
|
+
<div class="card">
|
|
276
|
+
<h2>Project</h2>
|
|
277
|
+
<p class="muted">Pick the app folder you want to work on.</p>
|
|
278
|
+
<label for="cwd" style="margin-top: 14px;">Project folder</label>
|
|
279
|
+
<div class="row">
|
|
280
|
+
<input id="cwd" type="text" placeholder="Select your project folder..." />
|
|
281
|
+
<button id="pick" class="ghost">Browse</button>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="hint muted">Default is the folder Studio was launched from.</div>
|
|
284
|
+
<div id="recentWrap" class="recent"></div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div class="card">
|
|
288
|
+
<h2>What do you want to do?</h2>
|
|
289
|
+
<p class="muted">Choose a task. Studio will run the matching Deploid command.</p>
|
|
290
|
+
<div id="commands" class="commands" style="margin-top: 12px;"></div>
|
|
291
|
+
<input id="cmd" type="hidden" value="build" />
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div class="card">
|
|
295
|
+
<h2>Run</h2>
|
|
296
|
+
<div class="row">
|
|
297
|
+
<button id="run" class="primary">Run Task</button>
|
|
298
|
+
<button id="stop" class="ghost">Stop</button>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="kpi">
|
|
301
|
+
<div class="kpi-item">
|
|
302
|
+
<div class="kpi-label">Current task</div>
|
|
303
|
+
<div id="kpiTask" class="kpi-value">build</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="kpi-item">
|
|
306
|
+
<div class="kpi-label">Runs this session</div>
|
|
307
|
+
<div id="kpiRuns" class="kpi-value">0</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="kpi-item">
|
|
310
|
+
<div class="kpi-label">Last result</div>
|
|
311
|
+
<div id="kpiResult" class="kpi-value">Idle</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
90
315
|
</div>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
316
|
+
|
|
317
|
+
<div class="right-col">
|
|
318
|
+
<div class="card">
|
|
319
|
+
<div class="log-toolbar">
|
|
320
|
+
<div>
|
|
321
|
+
<h2 style="margin-bottom: 4px;">Activity</h2>
|
|
322
|
+
<p class="muted">Live output from your selected task.</p>
|
|
323
|
+
</div>
|
|
324
|
+
<div class="row">
|
|
325
|
+
<select id="logFilter">
|
|
326
|
+
<option value="all">All output</option>
|
|
327
|
+
<option value="stdout">Standard output</option>
|
|
328
|
+
<option value="stderr">Errors only</option>
|
|
329
|
+
<option value="system">System messages</option>
|
|
330
|
+
</select>
|
|
331
|
+
<button id="copyLogs" class="ghost">Copy</button>
|
|
332
|
+
<button id="clearLogs" class="ghost">Clear</button>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
<div id="logs"></div>
|
|
336
|
+
<div id="status" class="muted" style="margin-top: 10px;">Ready to run</div>
|
|
337
|
+
</div>
|
|
107
338
|
</div>
|
|
108
|
-
<div id="status" class="muted" style="margin-top: 10px;">Idle</div>
|
|
109
|
-
<div id="logs"></div>
|
|
110
339
|
</div>
|
|
111
340
|
</div>
|
|
112
341
|
<script src="./renderer.js"></script>
|
|
113
342
|
</body>
|
|
114
|
-
|
|
343
|
+
</html>
|
|
@@ -2,31 +2,164 @@ 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
|
|
5
|
+
const cmdInput = document.getElementById('cmd');
|
|
6
|
+
const commandsWrap = document.getElementById('commands');
|
|
7
|
+
const recentWrap = document.getElementById('recentWrap');
|
|
8
|
+
const statusPill = document.getElementById('statusPill');
|
|
9
|
+
const logFilter = document.getElementById('logFilter');
|
|
10
|
+
const copyLogsButton = document.getElementById('copyLogs');
|
|
11
|
+
const clearLogsButton = document.getElementById('clearLogs');
|
|
12
|
+
const kpiTask = document.getElementById('kpiTask');
|
|
13
|
+
const kpiRuns = document.getElementById('kpiRuns');
|
|
14
|
+
const kpiResult = document.getElementById('kpiResult');
|
|
6
15
|
const logs = document.getElementById('logs');
|
|
7
16
|
const status = document.getElementById('status');
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
18
|
+
const RECENT_CWDS_KEY = 'deploidStudio.recentCwds';
|
|
19
|
+
const MAX_RECENT_CWDS = 5;
|
|
20
|
+
|
|
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.' }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const logEntries = [];
|
|
38
|
+
let runCount = 0;
|
|
39
|
+
let selectedCommand = 'build';
|
|
40
|
+
let currentRunHadError = false;
|
|
41
|
+
|
|
42
|
+
function appendLog(kind, text) {
|
|
43
|
+
logEntries.push({ kind, text });
|
|
44
|
+
renderLogs();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderLogs() {
|
|
48
|
+
const filter = logFilter.value;
|
|
49
|
+
const text = logEntries
|
|
50
|
+
.filter((entry) => filter === 'all' || entry.kind === filter)
|
|
51
|
+
.map((entry) => entry.text)
|
|
52
|
+
.join('');
|
|
53
|
+
logs.textContent = text;
|
|
11
54
|
logs.scrollTop = logs.scrollHeight;
|
|
12
55
|
}
|
|
13
56
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
function getRecentCwds() {
|
|
88
|
+
try {
|
|
89
|
+
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);
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function saveRecentCwds(cwds) {
|
|
98
|
+
localStorage.setItem(RECENT_CWDS_KEY, JSON.stringify(cwds.slice(0, MAX_RECENT_CWDS)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addRecentCwd(cwd) {
|
|
102
|
+
const next = [cwd, ...getRecentCwds().filter((entry) => entry !== cwd)];
|
|
103
|
+
saveRecentCwds(next);
|
|
104
|
+
renderRecentCwds();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderRecentCwds() {
|
|
108
|
+
const recents = getRecentCwds();
|
|
109
|
+
recentWrap.innerHTML = '';
|
|
110
|
+
for (const cwd of recents) {
|
|
111
|
+
const button = document.createElement('button');
|
|
112
|
+
button.className = 'ghost';
|
|
113
|
+
button.type = 'button';
|
|
114
|
+
button.title = cwd;
|
|
115
|
+
button.textContent = cwd.length > 34 ? `...${cwd.slice(-34)}` : cwd;
|
|
116
|
+
button.addEventListener('click', () => {
|
|
117
|
+
cwdInput.value = cwd;
|
|
118
|
+
});
|
|
119
|
+
recentWrap.appendChild(button);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderCommands() {
|
|
124
|
+
commandsWrap.innerHTML = '';
|
|
125
|
+
for (const command of COMMANDS) {
|
|
126
|
+
const button = document.createElement('button');
|
|
127
|
+
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>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="command-desc">${command.description}</div>
|
|
136
|
+
`;
|
|
137
|
+
button.addEventListener('click', () => updateSelectedCommand(command.key));
|
|
138
|
+
commandsWrap.appendChild(button);
|
|
139
|
+
}
|
|
140
|
+
updateSelectedCommand(selectedCommand);
|
|
141
|
+
}
|
|
18
142
|
|
|
19
143
|
runButton.addEventListener('click', async () => {
|
|
20
144
|
const cwd = cwdInput.value.trim();
|
|
21
145
|
if (!cwd) {
|
|
22
|
-
|
|
146
|
+
setErrorState('Choose a project folder before running a task.');
|
|
147
|
+
appendLog('system', 'Choose a project folder before running a task.\n');
|
|
23
148
|
return;
|
|
24
149
|
}
|
|
25
|
-
const command =
|
|
150
|
+
const command = cmdInput.value || selectedCommand;
|
|
26
151
|
try {
|
|
152
|
+
addRecentCwd(cwd);
|
|
153
|
+
runCount += 1;
|
|
154
|
+
currentRunHadError = false;
|
|
155
|
+
kpiRuns.textContent = String(runCount);
|
|
156
|
+
kpiResult.textContent = 'Running';
|
|
157
|
+
setRunningState(true);
|
|
27
158
|
await window.deploidStudio.runCommand(cwd, command);
|
|
28
159
|
} catch (error) {
|
|
29
|
-
|
|
160
|
+
kpiResult.textContent = 'Failed';
|
|
161
|
+
setErrorState(error.message);
|
|
162
|
+
appendLog('stderr', `Error: ${error.message}\n`);
|
|
30
163
|
}
|
|
31
164
|
});
|
|
32
165
|
|
|
@@ -35,16 +168,52 @@ stopButton.addEventListener('click', async () => {
|
|
|
35
168
|
});
|
|
36
169
|
|
|
37
170
|
window.deploidStudio.onLog((entry) => {
|
|
38
|
-
|
|
171
|
+
if (entry.kind === 'stderr') {
|
|
172
|
+
currentRunHadError = true;
|
|
173
|
+
}
|
|
174
|
+
appendLog(entry.kind, entry.message);
|
|
39
175
|
});
|
|
40
176
|
|
|
41
177
|
window.deploidStudio.onState((state) => {
|
|
42
|
-
|
|
43
|
-
|
|
178
|
+
setRunningState(state.running);
|
|
179
|
+
if (!state.running) {
|
|
180
|
+
kpiResult.textContent = currentRunHadError ? 'Warning' : 'Success';
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
logFilter.addEventListener('change', () => {
|
|
185
|
+
renderLogs();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
clearLogsButton.addEventListener('click', () => {
|
|
189
|
+
logEntries.length = 0;
|
|
190
|
+
renderLogs();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
copyLogsButton.addEventListener('click', async () => {
|
|
194
|
+
const text = logs.textContent || '';
|
|
195
|
+
if (!text) return;
|
|
196
|
+
try {
|
|
197
|
+
await navigator.clipboard.writeText(text);
|
|
198
|
+
status.textContent = 'Activity copied to clipboard.';
|
|
199
|
+
} catch {
|
|
200
|
+
status.textContent = 'Unable to copy logs from this environment.';
|
|
201
|
+
}
|
|
44
202
|
});
|
|
45
203
|
|
|
46
204
|
window.deploidStudio.getDefaultCwd().then((cwd) => {
|
|
47
205
|
if (!cwdInput.value) {
|
|
48
206
|
cwdInput.value = cwd;
|
|
49
207
|
}
|
|
208
|
+
addRecentCwd(cwd);
|
|
50
209
|
});
|
|
210
|
+
|
|
211
|
+
pickButton.addEventListener('click', async () => {
|
|
212
|
+
const folder = await window.deploidStudio.chooseProject();
|
|
213
|
+
if (!folder) return;
|
|
214
|
+
cwdInput.value = folder;
|
|
215
|
+
addRecentCwd(folder);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
renderCommands();
|
|
219
|
+
renderRecentCwds();
|
package/package.json
CHANGED
package/renderer/index.html
CHANGED
|
@@ -6,62 +6,214 @@
|
|
|
6
6
|
<title>Deploid Studio</title>
|
|
7
7
|
<style>
|
|
8
8
|
:root {
|
|
9
|
-
--bg: #
|
|
10
|
-
--
|
|
11
|
-
--line: #
|
|
12
|
-
--text: #
|
|
13
|
-
--muted: #
|
|
14
|
-
--accent: #
|
|
9
|
+
--bg: #f2f6f3;
|
|
10
|
+
--panel: #ffffff;
|
|
11
|
+
--line: #d4dfd8;
|
|
12
|
+
--text: #112019;
|
|
13
|
+
--muted: #52685d;
|
|
14
|
+
--accent: #1ea96a;
|
|
15
|
+
--accent-strong: #0f7f4c;
|
|
16
|
+
--warning: #f3a62a;
|
|
17
|
+
--error: #cf3d2e;
|
|
18
|
+
--shadow: 0 14px 38px rgba(31, 61, 45, 0.1);
|
|
19
|
+
}
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
15
22
|
}
|
|
16
23
|
body {
|
|
17
24
|
margin: 0;
|
|
18
|
-
font-family: "Segoe UI", Arial, sans-serif;
|
|
19
|
-
background:
|
|
25
|
+
font-family: "Avenir Next", "Segoe UI", Arial, sans-serif;
|
|
26
|
+
background:
|
|
27
|
+
radial-gradient(circle at top right, rgba(31, 169, 106, 0.18), transparent 34%),
|
|
28
|
+
radial-gradient(circle at top left, rgba(45, 123, 180, 0.12), transparent 24%),
|
|
29
|
+
var(--bg);
|
|
20
30
|
color: var(--text);
|
|
31
|
+
min-height: 100vh;
|
|
21
32
|
}
|
|
22
33
|
.wrap {
|
|
23
|
-
max-width:
|
|
24
|
-
margin:
|
|
25
|
-
padding: 0
|
|
34
|
+
max-width: 1140px;
|
|
35
|
+
margin: 32px auto 48px;
|
|
36
|
+
padding: 0 20px;
|
|
37
|
+
}
|
|
38
|
+
.shell {
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: 360px 1fr;
|
|
41
|
+
gap: 18px;
|
|
26
42
|
}
|
|
27
43
|
.card {
|
|
28
|
-
background:
|
|
44
|
+
background: var(--panel);
|
|
29
45
|
border: 1px solid var(--line);
|
|
30
|
-
border-radius:
|
|
31
|
-
padding:
|
|
46
|
+
border-radius: 16px;
|
|
47
|
+
padding: 18px;
|
|
48
|
+
box-shadow: var(--shadow);
|
|
49
|
+
}
|
|
50
|
+
h1 {
|
|
51
|
+
margin: 0;
|
|
52
|
+
font-size: 26px;
|
|
53
|
+
letter-spacing: -0.02em;
|
|
54
|
+
}
|
|
55
|
+
h2 {
|
|
56
|
+
margin: 0 0 8px;
|
|
57
|
+
font-size: 18px;
|
|
58
|
+
}
|
|
59
|
+
p {
|
|
60
|
+
margin: 0;
|
|
61
|
+
}
|
|
62
|
+
.hero {
|
|
63
|
+
margin-bottom: 14px;
|
|
64
|
+
}
|
|
65
|
+
.hero p {
|
|
66
|
+
color: var(--muted);
|
|
67
|
+
margin-top: 6px;
|
|
68
|
+
}
|
|
69
|
+
.status-pill {
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 6px;
|
|
73
|
+
padding: 7px 11px;
|
|
74
|
+
border-radius: 999px;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
border: 1px solid #b7dac9;
|
|
77
|
+
color: #165f3d;
|
|
78
|
+
background: #ecf9f3;
|
|
79
|
+
margin-top: 12px;
|
|
80
|
+
}
|
|
81
|
+
.status-pill.running {
|
|
82
|
+
color: #754b00;
|
|
83
|
+
border-color: #efddba;
|
|
84
|
+
background: #fff7e7;
|
|
85
|
+
}
|
|
86
|
+
.status-pill.error {
|
|
87
|
+
color: #7a1f14;
|
|
88
|
+
border-color: #efcabf;
|
|
89
|
+
background: #ffefeb;
|
|
90
|
+
}
|
|
91
|
+
.left-col,
|
|
92
|
+
.right-col {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 14px;
|
|
32
96
|
}
|
|
33
97
|
.row {
|
|
34
98
|
display: flex;
|
|
35
99
|
gap: 12px;
|
|
36
|
-
align-items:
|
|
100
|
+
align-items: stretch;
|
|
37
101
|
flex-wrap: wrap;
|
|
38
102
|
}
|
|
39
|
-
|
|
40
|
-
|
|
103
|
+
.row.stack {
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
}
|
|
106
|
+
label {
|
|
107
|
+
font-size: 13px;
|
|
108
|
+
color: var(--muted);
|
|
109
|
+
font-weight: 600;
|
|
110
|
+
margin-bottom: 8px;
|
|
111
|
+
display: block;
|
|
112
|
+
}
|
|
113
|
+
input,
|
|
114
|
+
select,
|
|
115
|
+
button {
|
|
116
|
+
border-radius: 10px;
|
|
41
117
|
border: 1px solid var(--line);
|
|
42
|
-
background: #
|
|
118
|
+
background: #ffffff;
|
|
43
119
|
color: var(--text);
|
|
44
120
|
padding: 10px 12px;
|
|
121
|
+
font-size: 14px;
|
|
45
122
|
}
|
|
46
123
|
input {
|
|
47
124
|
flex: 1;
|
|
48
|
-
min-width:
|
|
125
|
+
min-width: 180px;
|
|
49
126
|
}
|
|
50
127
|
button {
|
|
51
128
|
cursor: pointer;
|
|
129
|
+
transition: transform 120ms ease, background-color 120ms ease, box-shadow 120ms ease;
|
|
130
|
+
}
|
|
131
|
+
button:hover {
|
|
132
|
+
transform: translateY(-1px);
|
|
52
133
|
}
|
|
53
134
|
button.primary {
|
|
54
135
|
background: var(--accent);
|
|
55
|
-
color: #
|
|
56
|
-
border:
|
|
136
|
+
color: #ffffff;
|
|
137
|
+
border-color: var(--accent-strong);
|
|
138
|
+
font-weight: 700;
|
|
139
|
+
}
|
|
140
|
+
button.primary:hover {
|
|
141
|
+
background: var(--accent-strong);
|
|
142
|
+
}
|
|
143
|
+
button.ghost {
|
|
144
|
+
background: #f7faf8;
|
|
145
|
+
}
|
|
146
|
+
button:disabled {
|
|
147
|
+
opacity: 0.6;
|
|
148
|
+
cursor: not-allowed;
|
|
149
|
+
transform: none;
|
|
150
|
+
}
|
|
151
|
+
.commands {
|
|
152
|
+
display: grid;
|
|
153
|
+
grid-template-columns: 1fr;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
}
|
|
156
|
+
.command {
|
|
157
|
+
text-align: left;
|
|
158
|
+
border-radius: 12px;
|
|
159
|
+
padding: 12px;
|
|
160
|
+
border: 1px solid var(--line);
|
|
161
|
+
background: #ffffff;
|
|
162
|
+
}
|
|
163
|
+
.command:hover {
|
|
164
|
+
border-color: #9ed0b8;
|
|
165
|
+
background: #f6fcf9;
|
|
166
|
+
}
|
|
167
|
+
.command.active {
|
|
168
|
+
border-color: var(--accent);
|
|
169
|
+
background: #eefaf4;
|
|
170
|
+
}
|
|
171
|
+
.command-title {
|
|
172
|
+
display: flex;
|
|
173
|
+
justify-content: space-between;
|
|
174
|
+
align-items: center;
|
|
57
175
|
font-weight: 700;
|
|
176
|
+
margin-bottom: 4px;
|
|
177
|
+
}
|
|
178
|
+
.command-tag {
|
|
179
|
+
font-size: 11px;
|
|
180
|
+
padding: 4px 8px;
|
|
181
|
+
border-radius: 999px;
|
|
182
|
+
background: #edf4f0;
|
|
183
|
+
color: #395245;
|
|
184
|
+
}
|
|
185
|
+
.command-desc {
|
|
186
|
+
color: var(--muted);
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
}
|
|
189
|
+
.recent {
|
|
190
|
+
margin-top: 10px;
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-wrap: wrap;
|
|
193
|
+
gap: 8px;
|
|
194
|
+
}
|
|
195
|
+
.recent button {
|
|
196
|
+
padding: 7px 10px;
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
background: #f3f8f5;
|
|
199
|
+
}
|
|
200
|
+
.log-toolbar {
|
|
201
|
+
display: flex;
|
|
202
|
+
justify-content: space-between;
|
|
203
|
+
gap: 8px;
|
|
204
|
+
align-items: center;
|
|
205
|
+
margin-bottom: 8px;
|
|
206
|
+
}
|
|
207
|
+
.log-toolbar .row {
|
|
208
|
+
gap: 8px;
|
|
58
209
|
}
|
|
59
210
|
#logs {
|
|
60
|
-
margin-top:
|
|
61
|
-
background: #
|
|
211
|
+
margin-top: 10px;
|
|
212
|
+
background: #0f1a15;
|
|
62
213
|
border: 1px solid var(--line);
|
|
63
|
-
|
|
64
|
-
|
|
214
|
+
color: #d9fce9;
|
|
215
|
+
border-radius: 10px;
|
|
216
|
+
height: 470px;
|
|
65
217
|
overflow: auto;
|
|
66
218
|
padding: 12px;
|
|
67
219
|
white-space: pre-wrap;
|
|
@@ -70,45 +222,122 @@
|
|
|
70
222
|
}
|
|
71
223
|
.muted {
|
|
72
224
|
color: var(--muted);
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
}
|
|
227
|
+
.hint {
|
|
228
|
+
margin-top: 8px;
|
|
73
229
|
font-size: 12px;
|
|
74
230
|
}
|
|
231
|
+
.kpi {
|
|
232
|
+
display: grid;
|
|
233
|
+
grid-template-columns: repeat(3, 1fr);
|
|
234
|
+
gap: 8px;
|
|
235
|
+
margin-top: 12px;
|
|
236
|
+
}
|
|
237
|
+
.kpi-item {
|
|
238
|
+
border: 1px solid var(--line);
|
|
239
|
+
border-radius: 12px;
|
|
240
|
+
padding: 10px;
|
|
241
|
+
background: #fbfdfc;
|
|
242
|
+
}
|
|
243
|
+
.kpi-label {
|
|
244
|
+
font-size: 12px;
|
|
245
|
+
color: var(--muted);
|
|
246
|
+
}
|
|
247
|
+
.kpi-value {
|
|
248
|
+
margin-top: 4px;
|
|
249
|
+
font-size: 16px;
|
|
250
|
+
font-weight: 700;
|
|
251
|
+
}
|
|
75
252
|
@media (max-width: 640px) {
|
|
76
|
-
|
|
77
|
-
|
|
253
|
+
.wrap {
|
|
254
|
+
margin: 16px auto 24px;
|
|
255
|
+
padding: 0 12px;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
@media (max-width: 980px) {
|
|
259
|
+
.shell {
|
|
260
|
+
grid-template-columns: 1fr;
|
|
78
261
|
}
|
|
79
262
|
}
|
|
80
263
|
</style>
|
|
81
264
|
</head>
|
|
82
265
|
<body>
|
|
83
266
|
<div class="wrap">
|
|
84
|
-
<div class="
|
|
85
|
-
<
|
|
86
|
-
<p
|
|
87
|
-
<div class="
|
|
88
|
-
|
|
89
|
-
|
|
267
|
+
<div class="hero">
|
|
268
|
+
<h1>Deploid Studio</h1>
|
|
269
|
+
<p>Build and ship your mobile app through a guided workflow, not a terminal window.</p>
|
|
270
|
+
<div id="statusPill" class="status-pill">Ready</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div class="shell">
|
|
274
|
+
<div class="left-col">
|
|
275
|
+
<div class="card">
|
|
276
|
+
<h2>Project</h2>
|
|
277
|
+
<p class="muted">Pick the app folder you want to work on.</p>
|
|
278
|
+
<label for="cwd" style="margin-top: 14px;">Project folder</label>
|
|
279
|
+
<div class="row">
|
|
280
|
+
<input id="cwd" type="text" placeholder="Select your project folder..." />
|
|
281
|
+
<button id="pick" class="ghost">Browse</button>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="hint muted">Default is the folder Studio was launched from.</div>
|
|
284
|
+
<div id="recentWrap" class="recent"></div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div class="card">
|
|
288
|
+
<h2>What do you want to do?</h2>
|
|
289
|
+
<p class="muted">Choose a task. Studio will run the matching Deploid command.</p>
|
|
290
|
+
<div id="commands" class="commands" style="margin-top: 12px;"></div>
|
|
291
|
+
<input id="cmd" type="hidden" value="build" />
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div class="card">
|
|
295
|
+
<h2>Run</h2>
|
|
296
|
+
<div class="row">
|
|
297
|
+
<button id="run" class="primary">Run Task</button>
|
|
298
|
+
<button id="stop" class="ghost">Stop</button>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="kpi">
|
|
301
|
+
<div class="kpi-item">
|
|
302
|
+
<div class="kpi-label">Current task</div>
|
|
303
|
+
<div id="kpiTask" class="kpi-value">build</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="kpi-item">
|
|
306
|
+
<div class="kpi-label">Runs this session</div>
|
|
307
|
+
<div id="kpiRuns" class="kpi-value">0</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="kpi-item">
|
|
310
|
+
<div class="kpi-label">Last result</div>
|
|
311
|
+
<div id="kpiResult" class="kpi-value">Idle</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
90
315
|
</div>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
316
|
+
|
|
317
|
+
<div class="right-col">
|
|
318
|
+
<div class="card">
|
|
319
|
+
<div class="log-toolbar">
|
|
320
|
+
<div>
|
|
321
|
+
<h2 style="margin-bottom: 4px;">Activity</h2>
|
|
322
|
+
<p class="muted">Live output from your selected task.</p>
|
|
323
|
+
</div>
|
|
324
|
+
<div class="row">
|
|
325
|
+
<select id="logFilter">
|
|
326
|
+
<option value="all">All output</option>
|
|
327
|
+
<option value="stdout">Standard output</option>
|
|
328
|
+
<option value="stderr">Errors only</option>
|
|
329
|
+
<option value="system">System messages</option>
|
|
330
|
+
</select>
|
|
331
|
+
<button id="copyLogs" class="ghost">Copy</button>
|
|
332
|
+
<button id="clearLogs" class="ghost">Clear</button>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
<div id="logs"></div>
|
|
336
|
+
<div id="status" class="muted" style="margin-top: 10px;">Ready to run</div>
|
|
337
|
+
</div>
|
|
107
338
|
</div>
|
|
108
|
-
<div id="status" class="muted" style="margin-top: 10px;">Idle</div>
|
|
109
|
-
<div id="logs"></div>
|
|
110
339
|
</div>
|
|
111
340
|
</div>
|
|
112
341
|
<script src="./renderer.js"></script>
|
|
113
342
|
</body>
|
|
114
|
-
|
|
343
|
+
</html>
|
package/renderer/renderer.js
CHANGED
|
@@ -2,31 +2,164 @@ 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
|
|
5
|
+
const cmdInput = document.getElementById('cmd');
|
|
6
|
+
const commandsWrap = document.getElementById('commands');
|
|
7
|
+
const recentWrap = document.getElementById('recentWrap');
|
|
8
|
+
const statusPill = document.getElementById('statusPill');
|
|
9
|
+
const logFilter = document.getElementById('logFilter');
|
|
10
|
+
const copyLogsButton = document.getElementById('copyLogs');
|
|
11
|
+
const clearLogsButton = document.getElementById('clearLogs');
|
|
12
|
+
const kpiTask = document.getElementById('kpiTask');
|
|
13
|
+
const kpiRuns = document.getElementById('kpiRuns');
|
|
14
|
+
const kpiResult = document.getElementById('kpiResult');
|
|
6
15
|
const logs = document.getElementById('logs');
|
|
7
16
|
const status = document.getElementById('status');
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
18
|
+
const RECENT_CWDS_KEY = 'deploidStudio.recentCwds';
|
|
19
|
+
const MAX_RECENT_CWDS = 5;
|
|
20
|
+
|
|
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.' }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const logEntries = [];
|
|
38
|
+
let runCount = 0;
|
|
39
|
+
let selectedCommand = 'build';
|
|
40
|
+
let currentRunHadError = false;
|
|
41
|
+
|
|
42
|
+
function appendLog(kind, text) {
|
|
43
|
+
logEntries.push({ kind, text });
|
|
44
|
+
renderLogs();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderLogs() {
|
|
48
|
+
const filter = logFilter.value;
|
|
49
|
+
const text = logEntries
|
|
50
|
+
.filter((entry) => filter === 'all' || entry.kind === filter)
|
|
51
|
+
.map((entry) => entry.text)
|
|
52
|
+
.join('');
|
|
53
|
+
logs.textContent = text;
|
|
11
54
|
logs.scrollTop = logs.scrollHeight;
|
|
12
55
|
}
|
|
13
56
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
function getRecentCwds() {
|
|
88
|
+
try {
|
|
89
|
+
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);
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function saveRecentCwds(cwds) {
|
|
98
|
+
localStorage.setItem(RECENT_CWDS_KEY, JSON.stringify(cwds.slice(0, MAX_RECENT_CWDS)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addRecentCwd(cwd) {
|
|
102
|
+
const next = [cwd, ...getRecentCwds().filter((entry) => entry !== cwd)];
|
|
103
|
+
saveRecentCwds(next);
|
|
104
|
+
renderRecentCwds();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderRecentCwds() {
|
|
108
|
+
const recents = getRecentCwds();
|
|
109
|
+
recentWrap.innerHTML = '';
|
|
110
|
+
for (const cwd of recents) {
|
|
111
|
+
const button = document.createElement('button');
|
|
112
|
+
button.className = 'ghost';
|
|
113
|
+
button.type = 'button';
|
|
114
|
+
button.title = cwd;
|
|
115
|
+
button.textContent = cwd.length > 34 ? `...${cwd.slice(-34)}` : cwd;
|
|
116
|
+
button.addEventListener('click', () => {
|
|
117
|
+
cwdInput.value = cwd;
|
|
118
|
+
});
|
|
119
|
+
recentWrap.appendChild(button);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderCommands() {
|
|
124
|
+
commandsWrap.innerHTML = '';
|
|
125
|
+
for (const command of COMMANDS) {
|
|
126
|
+
const button = document.createElement('button');
|
|
127
|
+
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>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="command-desc">${command.description}</div>
|
|
136
|
+
`;
|
|
137
|
+
button.addEventListener('click', () => updateSelectedCommand(command.key));
|
|
138
|
+
commandsWrap.appendChild(button);
|
|
139
|
+
}
|
|
140
|
+
updateSelectedCommand(selectedCommand);
|
|
141
|
+
}
|
|
18
142
|
|
|
19
143
|
runButton.addEventListener('click', async () => {
|
|
20
144
|
const cwd = cwdInput.value.trim();
|
|
21
145
|
if (!cwd) {
|
|
22
|
-
|
|
146
|
+
setErrorState('Choose a project folder before running a task.');
|
|
147
|
+
appendLog('system', 'Choose a project folder before running a task.\n');
|
|
23
148
|
return;
|
|
24
149
|
}
|
|
25
|
-
const command =
|
|
150
|
+
const command = cmdInput.value || selectedCommand;
|
|
26
151
|
try {
|
|
152
|
+
addRecentCwd(cwd);
|
|
153
|
+
runCount += 1;
|
|
154
|
+
currentRunHadError = false;
|
|
155
|
+
kpiRuns.textContent = String(runCount);
|
|
156
|
+
kpiResult.textContent = 'Running';
|
|
157
|
+
setRunningState(true);
|
|
27
158
|
await window.deploidStudio.runCommand(cwd, command);
|
|
28
159
|
} catch (error) {
|
|
29
|
-
|
|
160
|
+
kpiResult.textContent = 'Failed';
|
|
161
|
+
setErrorState(error.message);
|
|
162
|
+
appendLog('stderr', `Error: ${error.message}\n`);
|
|
30
163
|
}
|
|
31
164
|
});
|
|
32
165
|
|
|
@@ -35,16 +168,52 @@ stopButton.addEventListener('click', async () => {
|
|
|
35
168
|
});
|
|
36
169
|
|
|
37
170
|
window.deploidStudio.onLog((entry) => {
|
|
38
|
-
|
|
171
|
+
if (entry.kind === 'stderr') {
|
|
172
|
+
currentRunHadError = true;
|
|
173
|
+
}
|
|
174
|
+
appendLog(entry.kind, entry.message);
|
|
39
175
|
});
|
|
40
176
|
|
|
41
177
|
window.deploidStudio.onState((state) => {
|
|
42
|
-
|
|
43
|
-
|
|
178
|
+
setRunningState(state.running);
|
|
179
|
+
if (!state.running) {
|
|
180
|
+
kpiResult.textContent = currentRunHadError ? 'Warning' : 'Success';
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
logFilter.addEventListener('change', () => {
|
|
185
|
+
renderLogs();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
clearLogsButton.addEventListener('click', () => {
|
|
189
|
+
logEntries.length = 0;
|
|
190
|
+
renderLogs();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
copyLogsButton.addEventListener('click', async () => {
|
|
194
|
+
const text = logs.textContent || '';
|
|
195
|
+
if (!text) return;
|
|
196
|
+
try {
|
|
197
|
+
await navigator.clipboard.writeText(text);
|
|
198
|
+
status.textContent = 'Activity copied to clipboard.';
|
|
199
|
+
} catch {
|
|
200
|
+
status.textContent = 'Unable to copy logs from this environment.';
|
|
201
|
+
}
|
|
44
202
|
});
|
|
45
203
|
|
|
46
204
|
window.deploidStudio.getDefaultCwd().then((cwd) => {
|
|
47
205
|
if (!cwdInput.value) {
|
|
48
206
|
cwdInput.value = cwd;
|
|
49
207
|
}
|
|
208
|
+
addRecentCwd(cwd);
|
|
50
209
|
});
|
|
210
|
+
|
|
211
|
+
pickButton.addEventListener('click', async () => {
|
|
212
|
+
const folder = await window.deploidStudio.chooseProject();
|
|
213
|
+
if (!folder) return;
|
|
214
|
+
cwdInput.value = folder;
|
|
215
|
+
addRecentCwd(folder);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
renderCommands();
|
|
219
|
+
renderRecentCwds();
|