design-share 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/bin/design-share.js +8 -0
- package/dist/DesignShare.app/Contents/CodeResources +0 -0
- package/dist/DesignShare.app/Contents/Info.plist +26 -0
- package/dist/DesignShare.app/Contents/MacOS/DesignShare +0 -0
- package/dist/DesignShare.app/Contents/_CodeSignature/CodeResources +115 -0
- package/package.json +18 -0
- package/public/app.js +526 -0
- package/public/index.html +75 -0
- package/public/style.css +435 -0
- package/src/cli.js +246 -0
- package/src/detect.js +76 -0
- package/src/git.js +47 -0
- package/src/menubar.js +48 -0
- package/src/previews.js +240 -0
- package/src/registry.js +42 -0
- package/src/server.js +214 -0
- package/src/state.js +204 -0
package/public/style.css
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #fbfbfb;
|
|
3
|
+
--panel: #ffffff;
|
|
4
|
+
--border: #e6e6e9;
|
|
5
|
+
--text: #17171c;
|
|
6
|
+
--text-2: #6f6f78;
|
|
7
|
+
--text-3: #9a9aa2;
|
|
8
|
+
--accent: #4c6fff;
|
|
9
|
+
--accent-soft: #eef1ff;
|
|
10
|
+
--green: #2da562;
|
|
11
|
+
--amber: #b7791f;
|
|
12
|
+
--red: #d14343;
|
|
13
|
+
--radius: 6px;
|
|
14
|
+
--shadow: 0 1px 2px rgba(20, 20, 25, 0.04), 0 4px 14px rgba(20, 20, 25, 0.05);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
18
|
+
|
|
19
|
+
[hidden] { display: none !important; }
|
|
20
|
+
|
|
21
|
+
html, body, #app { height: 100%; }
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
font: 400 13px/1.45 -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif;
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
color: var(--text);
|
|
27
|
+
-webkit-font-smoothing: antialiased;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#app { display: flex; }
|
|
31
|
+
|
|
32
|
+
button { font: inherit; cursor: pointer; }
|
|
33
|
+
|
|
34
|
+
/* ---------- left rail ---------- */
|
|
35
|
+
|
|
36
|
+
#rail {
|
|
37
|
+
width: 232px;
|
|
38
|
+
flex: none;
|
|
39
|
+
border-right: 1px solid var(--border);
|
|
40
|
+
background: var(--panel);
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.rail-head {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: 8px;
|
|
49
|
+
padding: 14px 16px 10px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.logo-mark {
|
|
53
|
+
width: 14px; height: 14px;
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
background: var(--text);
|
|
56
|
+
flex: none;
|
|
57
|
+
position: relative;
|
|
58
|
+
}
|
|
59
|
+
.logo-mark::after {
|
|
60
|
+
content: "";
|
|
61
|
+
position: absolute;
|
|
62
|
+
left: 4px; top: 4px;
|
|
63
|
+
width: 6px; height: 6px;
|
|
64
|
+
border-radius: 2px;
|
|
65
|
+
background: #fff;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.repo-name { font-size: 13.5px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
69
|
+
|
|
70
|
+
#rail-sections { flex: 1; overflow-y: auto; padding: 4px 8px 12px; }
|
|
71
|
+
|
|
72
|
+
.rail-section-label {
|
|
73
|
+
font-size: 10.5px;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
letter-spacing: 0.06em;
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
color: var(--text-3);
|
|
78
|
+
padding: 12px 8px 4px;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 6px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.branch-row {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 7px;
|
|
88
|
+
width: 100%;
|
|
89
|
+
padding: 5px 8px;
|
|
90
|
+
border: 0;
|
|
91
|
+
background: transparent;
|
|
92
|
+
border-radius: var(--radius);
|
|
93
|
+
color: var(--text);
|
|
94
|
+
text-align: left;
|
|
95
|
+
font-size: 12.5px;
|
|
96
|
+
}
|
|
97
|
+
.branch-row:hover { background: #f2f2f4; }
|
|
98
|
+
.branch-row.selected { background: var(--accent-soft); color: var(--text); }
|
|
99
|
+
.branch-row .branch-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
100
|
+
.branch-row .meta { font-size: 11px; color: var(--text-3); flex: none; }
|
|
101
|
+
.branch-row.dim { color: var(--text-2); }
|
|
102
|
+
|
|
103
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
|
|
104
|
+
.dot.live { background: var(--green); }
|
|
105
|
+
.dot.idle { background: #d4d4d8; }
|
|
106
|
+
.dot.starting { background: var(--amber); }
|
|
107
|
+
|
|
108
|
+
.badge {
|
|
109
|
+
font-size: 10.5px;
|
|
110
|
+
color: var(--accent);
|
|
111
|
+
border: 1px solid var(--border);
|
|
112
|
+
border-radius: 99px;
|
|
113
|
+
padding: 0 6px;
|
|
114
|
+
line-height: 16px;
|
|
115
|
+
flex: none;
|
|
116
|
+
background: #fff;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.share-cta {
|
|
120
|
+
margin: 4px 8px 0;
|
|
121
|
+
width: calc(100% - 16px);
|
|
122
|
+
border: 1px dashed var(--border);
|
|
123
|
+
background: transparent;
|
|
124
|
+
color: var(--text-2);
|
|
125
|
+
border-radius: var(--radius);
|
|
126
|
+
padding: 5px 8px;
|
|
127
|
+
font-size: 12px;
|
|
128
|
+
text-align: left;
|
|
129
|
+
}
|
|
130
|
+
.share-cta:hover { color: var(--text); border-color: #d0d0d5; }
|
|
131
|
+
|
|
132
|
+
.rail-foot { border-top: 1px solid var(--border); padding: 8px 16px; }
|
|
133
|
+
.sync-status { font-size: 11px; color: var(--text-3); display: flex; align-items: center; gap: 6px; }
|
|
134
|
+
.sync-status .dot { width: 6px; height: 6px; }
|
|
135
|
+
|
|
136
|
+
/* ---------- stage ---------- */
|
|
137
|
+
|
|
138
|
+
#stage { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
139
|
+
|
|
140
|
+
#stage-head {
|
|
141
|
+
height: 46px;
|
|
142
|
+
flex: none;
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
justify-content: space-between;
|
|
146
|
+
gap: 12px;
|
|
147
|
+
padding: 0 14px;
|
|
148
|
+
border-bottom: 1px solid var(--border);
|
|
149
|
+
background: var(--panel);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.stage-title-wrap { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
153
|
+
.stage-title { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
154
|
+
.stage-meta { font-size: 11.5px; color: var(--text-3); white-space: nowrap; }
|
|
155
|
+
|
|
156
|
+
.stage-actions { display: flex; align-items: center; gap: 8px; flex: none; }
|
|
157
|
+
|
|
158
|
+
.seg { display: flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
159
|
+
.seg button {
|
|
160
|
+
border: 0;
|
|
161
|
+
background: transparent;
|
|
162
|
+
padding: 4px 8px;
|
|
163
|
+
color: var(--text-3);
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
}
|
|
167
|
+
.seg button + button { border-left: 1px solid var(--border); }
|
|
168
|
+
.seg button.active { color: var(--text); background: #f2f2f4; }
|
|
169
|
+
.seg button svg { display: block; }
|
|
170
|
+
|
|
171
|
+
.btn {
|
|
172
|
+
display: inline-flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 6px;
|
|
175
|
+
height: 27px;
|
|
176
|
+
padding: 0 10px;
|
|
177
|
+
border-radius: var(--radius);
|
|
178
|
+
border: 1px solid var(--text);
|
|
179
|
+
background: var(--text);
|
|
180
|
+
color: #fff;
|
|
181
|
+
font-size: 12px;
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
}
|
|
184
|
+
.btn:hover { background: #2e2e35; border-color: #2e2e35; }
|
|
185
|
+
.btn.ghost {
|
|
186
|
+
background: transparent;
|
|
187
|
+
border-color: var(--border);
|
|
188
|
+
color: var(--text-2);
|
|
189
|
+
}
|
|
190
|
+
.btn.ghost:hover { color: var(--text); border-color: #d0d0d5; background: #fff; }
|
|
191
|
+
.btn.small { height: 24px; padding: 0 8px; font-size: 11.5px; }
|
|
192
|
+
.btn.active-mode { background: var(--accent); border-color: var(--accent); }
|
|
193
|
+
.btn svg { display: block; }
|
|
194
|
+
|
|
195
|
+
/* ---------- frame ---------- */
|
|
196
|
+
|
|
197
|
+
#frame-area {
|
|
198
|
+
flex: 1;
|
|
199
|
+
overflow: auto;
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: stretch;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
background:
|
|
204
|
+
radial-gradient(circle at 1px 1px, #e8e8ec 1px, transparent 0);
|
|
205
|
+
background-size: 22px 22px;
|
|
206
|
+
padding: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#frame-wrap {
|
|
210
|
+
position: relative;
|
|
211
|
+
flex: 1;
|
|
212
|
+
max-width: 100%;
|
|
213
|
+
background: var(--panel);
|
|
214
|
+
transition: max-width 0.18s ease;
|
|
215
|
+
border-left: 1px solid var(--border);
|
|
216
|
+
border-right: 1px solid var(--border);
|
|
217
|
+
min-height: 100%;
|
|
218
|
+
}
|
|
219
|
+
#frame-wrap.vw-tablet { max-width: 834px; margin: 0 auto; box-shadow: var(--shadow); }
|
|
220
|
+
#frame-wrap.vw-mobile { max-width: 390px; margin: 0 auto; box-shadow: var(--shadow); }
|
|
221
|
+
|
|
222
|
+
#frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: #fff; }
|
|
223
|
+
|
|
224
|
+
.frame-state {
|
|
225
|
+
position: absolute;
|
|
226
|
+
inset: 0;
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: column;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: center;
|
|
231
|
+
gap: 8px;
|
|
232
|
+
padding: 32px;
|
|
233
|
+
text-align: center;
|
|
234
|
+
background: var(--panel);
|
|
235
|
+
z-index: 3;
|
|
236
|
+
}
|
|
237
|
+
.frame-state-title { font-size: 13.5px; font-weight: 600; }
|
|
238
|
+
.frame-state-title.err { color: var(--red); }
|
|
239
|
+
.frame-state-sub { font-size: 12px; color: var(--text-2); max-width: 420px; }
|
|
240
|
+
|
|
241
|
+
.log-tail {
|
|
242
|
+
margin-top: 10px;
|
|
243
|
+
max-width: 560px;
|
|
244
|
+
max-height: 180px;
|
|
245
|
+
overflow: auto;
|
|
246
|
+
text-align: left;
|
|
247
|
+
font: 11px/1.5 ui-monospace, "SF Mono", Menlo, monospace;
|
|
248
|
+
color: var(--text-2);
|
|
249
|
+
background: #f6f6f7;
|
|
250
|
+
border: 1px solid var(--border);
|
|
251
|
+
border-radius: var(--radius);
|
|
252
|
+
padding: 10px 12px;
|
|
253
|
+
white-space: pre-wrap;
|
|
254
|
+
word-break: break-all;
|
|
255
|
+
}
|
|
256
|
+
.log-tail:empty { display: none; }
|
|
257
|
+
|
|
258
|
+
.spinner {
|
|
259
|
+
width: 18px; height: 18px;
|
|
260
|
+
border: 2px solid var(--border);
|
|
261
|
+
border-top-color: var(--text-2);
|
|
262
|
+
border-radius: 50%;
|
|
263
|
+
animation: spin 0.9s linear infinite;
|
|
264
|
+
}
|
|
265
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
266
|
+
|
|
267
|
+
/* ---------- pins ---------- */
|
|
268
|
+
|
|
269
|
+
#pin-layer { position: absolute; inset: 0; z-index: 4; pointer-events: none; }
|
|
270
|
+
#pin-layer.capture { pointer-events: auto; cursor: crosshair; }
|
|
271
|
+
|
|
272
|
+
.pin {
|
|
273
|
+
position: absolute;
|
|
274
|
+
width: 22px; height: 22px;
|
|
275
|
+
transform: translate(-4px, -18px);
|
|
276
|
+
border-radius: 50% 50% 50% 0;
|
|
277
|
+
background: var(--accent);
|
|
278
|
+
color: #fff;
|
|
279
|
+
border: 2px solid #fff;
|
|
280
|
+
box-shadow: 0 1px 4px rgba(20,20,25,0.25);
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
justify-content: center;
|
|
284
|
+
font-size: 10.5px;
|
|
285
|
+
font-weight: 600;
|
|
286
|
+
pointer-events: auto;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
rotate: -45deg;
|
|
289
|
+
}
|
|
290
|
+
.pin > span { rotate: 45deg; }
|
|
291
|
+
.pin.resolved { background: var(--green); opacity: 0.55; }
|
|
292
|
+
.pin.pending { background: var(--amber); }
|
|
293
|
+
.pin.focused { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
294
|
+
|
|
295
|
+
#composer {
|
|
296
|
+
position: absolute;
|
|
297
|
+
z-index: 5;
|
|
298
|
+
width: 240px;
|
|
299
|
+
background: var(--panel);
|
|
300
|
+
border: 1px solid var(--border);
|
|
301
|
+
border-radius: 8px;
|
|
302
|
+
box-shadow: var(--shadow);
|
|
303
|
+
padding: 8px;
|
|
304
|
+
}
|
|
305
|
+
#composer textarea {
|
|
306
|
+
width: 100%;
|
|
307
|
+
border: 0;
|
|
308
|
+
resize: none;
|
|
309
|
+
font: inherit;
|
|
310
|
+
outline: none;
|
|
311
|
+
color: var(--text);
|
|
312
|
+
background: transparent;
|
|
313
|
+
}
|
|
314
|
+
.composer-actions { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
|
|
315
|
+
.kbd-hint { font-size: 10.5px; color: var(--text-3); }
|
|
316
|
+
|
|
317
|
+
/* ---------- comments panel ---------- */
|
|
318
|
+
|
|
319
|
+
#comments-panel {
|
|
320
|
+
width: 264px;
|
|
321
|
+
flex: none;
|
|
322
|
+
border-left: 1px solid var(--border);
|
|
323
|
+
background: var(--panel);
|
|
324
|
+
display: flex;
|
|
325
|
+
flex-direction: column;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.panel-head {
|
|
329
|
+
height: 46px;
|
|
330
|
+
flex: none;
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
gap: 8px;
|
|
334
|
+
padding: 0 14px;
|
|
335
|
+
border-bottom: 1px solid var(--border);
|
|
336
|
+
font-size: 13.5px;
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
}
|
|
339
|
+
.panel-head .count { font-size: 11.5px; color: var(--text-3); font-weight: 400; }
|
|
340
|
+
|
|
341
|
+
#comments-list { flex: 1; overflow-y: auto; padding: 10px; }
|
|
342
|
+
|
|
343
|
+
.comment-card {
|
|
344
|
+
border: 1px solid var(--border);
|
|
345
|
+
border-radius: 8px;
|
|
346
|
+
padding: 9px 10px;
|
|
347
|
+
margin-bottom: 8px;
|
|
348
|
+
background: var(--panel);
|
|
349
|
+
}
|
|
350
|
+
.comment-card.focused { border-color: var(--accent); }
|
|
351
|
+
.comment-card.resolved { opacity: 0.6; }
|
|
352
|
+
|
|
353
|
+
.comment-head { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }
|
|
354
|
+
.avatar {
|
|
355
|
+
width: 18px; height: 18px;
|
|
356
|
+
border-radius: 50%;
|
|
357
|
+
background: #ececef;
|
|
358
|
+
color: var(--text-2);
|
|
359
|
+
font-size: 9.5px;
|
|
360
|
+
font-weight: 600;
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
justify-content: center;
|
|
364
|
+
flex: none;
|
|
365
|
+
text-transform: uppercase;
|
|
366
|
+
}
|
|
367
|
+
.comment-author { font-size: 12.5px; font-weight: 600; }
|
|
368
|
+
.comment-num { font-size: 11px; color: var(--text-3); margin-left: auto; }
|
|
369
|
+
|
|
370
|
+
.comment-text { font-size: 12.5px; color: var(--text); }
|
|
371
|
+
.comment-meta { font-size: 11px; color: var(--text-3); margin-top: 5px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
372
|
+
|
|
373
|
+
.chip {
|
|
374
|
+
display: inline-flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
gap: 4px;
|
|
377
|
+
font-size: 10.5px;
|
|
378
|
+
border: 1px solid var(--border);
|
|
379
|
+
border-radius: 99px;
|
|
380
|
+
padding: 0 7px;
|
|
381
|
+
line-height: 16px;
|
|
382
|
+
color: var(--text-2);
|
|
383
|
+
background: #fff;
|
|
384
|
+
}
|
|
385
|
+
.chip.changed { color: var(--amber); }
|
|
386
|
+
.chip .dot { width: 5px; height: 5px; }
|
|
387
|
+
|
|
388
|
+
.comment-replies { margin-top: 6px; border-top: 1px solid var(--border); padding-top: 6px; display: flex; flex-direction: column; gap: 5px; }
|
|
389
|
+
.reply { font-size: 12px; }
|
|
390
|
+
.reply b { font-weight: 600; }
|
|
391
|
+
|
|
392
|
+
.comment-actions { display: flex; gap: 6px; margin-top: 7px; align-items: center; }
|
|
393
|
+
.link-btn {
|
|
394
|
+
border: 0;
|
|
395
|
+
background: transparent;
|
|
396
|
+
color: var(--text-2);
|
|
397
|
+
font-size: 11.5px;
|
|
398
|
+
padding: 0;
|
|
399
|
+
}
|
|
400
|
+
.link-btn:hover { color: var(--text); }
|
|
401
|
+
.link-btn.resolve { color: var(--green); }
|
|
402
|
+
|
|
403
|
+
.reply-input {
|
|
404
|
+
width: 100%;
|
|
405
|
+
margin-top: 6px;
|
|
406
|
+
border: 1px solid var(--border);
|
|
407
|
+
border-radius: var(--radius);
|
|
408
|
+
padding: 4px 8px;
|
|
409
|
+
font: inherit;
|
|
410
|
+
font-size: 12px;
|
|
411
|
+
outline: none;
|
|
412
|
+
color: var(--text);
|
|
413
|
+
background: #fff;
|
|
414
|
+
}
|
|
415
|
+
.reply-input:focus { border-color: #c9c9cf; }
|
|
416
|
+
|
|
417
|
+
.empty-note { color: var(--text-3); font-size: 12px; padding: 14px 6px; text-align: center; }
|
|
418
|
+
|
|
419
|
+
.section-note { font-size: 10.5px; color: var(--text-3); padding: 10px 8px 2px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
|
|
420
|
+
|
|
421
|
+
.toast {
|
|
422
|
+
position: fixed;
|
|
423
|
+
bottom: 16px;
|
|
424
|
+
left: 50%;
|
|
425
|
+
transform: translateX(-50%);
|
|
426
|
+
background: var(--text);
|
|
427
|
+
color: #fff;
|
|
428
|
+
font-size: 12px;
|
|
429
|
+
border-radius: 6px;
|
|
430
|
+
padding: 7px 14px;
|
|
431
|
+
box-shadow: var(--shadow);
|
|
432
|
+
z-index: 50;
|
|
433
|
+
animation: toast-in 0.15s ease;
|
|
434
|
+
}
|
|
435
|
+
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 4px); } }
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { detectRepo, detectIdentity, detectPreview, currentBranch, writeRepoConfig } from './detect.js';
|
|
5
|
+
import { StateStore } from './state.js';
|
|
6
|
+
import { PreviewManager } from './previews.js';
|
|
7
|
+
import { DesignShareServer } from './server.js';
|
|
8
|
+
import { upsertDaemon, removeDaemon, readUserConfig, writeUserConfig } from './registry.js';
|
|
9
|
+
import { appAvailable, appInstalled, installApp, launchApp } from './menubar.js';
|
|
10
|
+
|
|
11
|
+
const CLI_PATH = fileURLToPath(new URL('../bin/design-share.js', import.meta.url));
|
|
12
|
+
|
|
13
|
+
const VERSION = '0.1.0';
|
|
14
|
+
const DIM = '\x1b[2m', GREEN = '\x1b[32m', BLUE = '\x1b[36m', BOLD = '\x1b[1m', RESET = '\x1b[0m';
|
|
15
|
+
const ok = (msg) => console.log(` ${GREEN}✓${RESET} ${msg}`);
|
|
16
|
+
const step = (msg) => console.log(` › ${msg}`);
|
|
17
|
+
const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = { command: 'up', port: 4400, open: true };
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
if (a === '--port') args.port = Number(argv[++i]) || 4400;
|
|
24
|
+
else if (a === '--no-open') args.open = false;
|
|
25
|
+
else if (a === '--yes') args.yes = true;
|
|
26
|
+
else if (a === '--daemon') { args.daemon = true; args.open = false; }
|
|
27
|
+
else if (a === '--version' || a === '-v') args.command = 'version';
|
|
28
|
+
else if (a === '--help' || a === '-h') args.command = 'help';
|
|
29
|
+
else if (!a.startsWith('-')) args.command = a;
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ask(question, fallback) {
|
|
35
|
+
if (!process.stdin.isTTY) return Promise.resolve(fallback);
|
|
36
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
rl.question(question, (answer) => {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(answer.trim() || fallback);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function openBrowser(url) {
|
|
46
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
47
|
+
: process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
48
|
+
try { spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref(); } catch { /* non fatal */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function copyToClipboard(text) {
|
|
52
|
+
if (process.platform !== 'darwin') return false;
|
|
53
|
+
try {
|
|
54
|
+
const p = spawn('pbcopy');
|
|
55
|
+
p.stdin.end(text);
|
|
56
|
+
return true;
|
|
57
|
+
} catch { return false; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function findRunningInstance(repoRoot, basePort) {
|
|
61
|
+
for (let port = basePort; port < basePort + 6; port++) {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`http://localhost:${port}/api/board`, { signal: AbortSignal.timeout(600) });
|
|
64
|
+
if (!res.ok) continue;
|
|
65
|
+
const board = await res.json();
|
|
66
|
+
if (board.repo && board.repo.root === repoRoot) return { port, board };
|
|
67
|
+
} catch { /* not ours */ }
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function resolvePreviewConfig(repo) {
|
|
73
|
+
let preview = detectPreview(repo.root);
|
|
74
|
+
if (preview) return preview;
|
|
75
|
+
|
|
76
|
+
console.log('');
|
|
77
|
+
step('could not auto detect how previews should run for this repo.');
|
|
78
|
+
const answer = await ask(
|
|
79
|
+
` how should previews run?\n 1) a command (like ${DIM}npm run dev${RESET})\n 2) serve a static folder\n pick 1 or 2: `,
|
|
80
|
+
null,
|
|
81
|
+
);
|
|
82
|
+
if (answer === '1') {
|
|
83
|
+
const command = await ask(' command: ', null);
|
|
84
|
+
if (!command) throw new Error('no command given. run npx design-share again when ready.');
|
|
85
|
+
preview = { type: 'command', command, label: command };
|
|
86
|
+
} else if (answer === '2') {
|
|
87
|
+
const dir = await ask(' folder (relative to repo root, default "."): ', '.');
|
|
88
|
+
preview = { type: 'static', dir, label: `static ${dir}` };
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error('preview setup skipped. run npx design-share again when ready.');
|
|
91
|
+
}
|
|
92
|
+
const file = writeRepoConfig(repo.root, { preview: { type: preview.type, command: preview.command, dir: preview.dir, label: preview.label } });
|
|
93
|
+
ok(`saved to ${file.replace(repo.root + '/', '')} — commit it so teammates are never asked`);
|
|
94
|
+
return preview;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function run(argv) {
|
|
98
|
+
const args = parseArgs(argv);
|
|
99
|
+
|
|
100
|
+
if (args.command === 'version') { console.log(VERSION); return; }
|
|
101
|
+
if (args.command === 'help') {
|
|
102
|
+
console.log(`
|
|
103
|
+
${BOLD}design-share${RESET} — live branch previews and comments for design teams
|
|
104
|
+
|
|
105
|
+
usage
|
|
106
|
+
npx design-share start the board for this repo
|
|
107
|
+
npx design-share share share your current branch with the team board
|
|
108
|
+
npx design-share status show what is running
|
|
109
|
+
npx design-share app install and launch the macOS menu bar app
|
|
110
|
+
|
|
111
|
+
flags
|
|
112
|
+
--port <n> dashboard port (default 4400)
|
|
113
|
+
--no-open do not open the browser
|
|
114
|
+
--yes share your branch without asking
|
|
115
|
+
`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (args.command === 'app') {
|
|
120
|
+
const target = await installApp();
|
|
121
|
+
await launchApp();
|
|
122
|
+
ok(`menu bar app installed at ${target} — look up ↗`);
|
|
123
|
+
writeUserConfig({ appOffer: 'done' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const repo = await detectRepo(process.cwd());
|
|
128
|
+
const identity = await detectIdentity(repo.root);
|
|
129
|
+
const branch = await currentBranch(repo.root);
|
|
130
|
+
|
|
131
|
+
if (args.command === 'status') {
|
|
132
|
+
const running = await findRunningInstance(repo.root, args.port);
|
|
133
|
+
if (running) {
|
|
134
|
+
ok(`design-share is running at http://localhost:${running.port} for ${running.board.repo.name}`);
|
|
135
|
+
} else {
|
|
136
|
+
dim('design-share is not running. start it with: npx design-share');
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log('');
|
|
142
|
+
ok(`repo: ${BOLD}${repo.name}${RESET} ${DIM}(cwd)${RESET}`);
|
|
143
|
+
ok(`you: ${BOLD}${identity.name}${RESET} ${DIM}(git config)${RESET}`);
|
|
144
|
+
|
|
145
|
+
const preview = await resolvePreviewConfig(repo);
|
|
146
|
+
const detectedNote = preview.source === 'config' ? 'from .design-share.json' : 'auto detected';
|
|
147
|
+
ok(`previews: ${BOLD}${preview.label || preview.command || 'static'}${RESET} ${DIM}(${detectedNote})${RESET}`);
|
|
148
|
+
|
|
149
|
+
const store = new StateStore(repo.root, identity);
|
|
150
|
+
await store.init();
|
|
151
|
+
|
|
152
|
+
if (args.command === 'share') {
|
|
153
|
+
if (!branch) throw new Error('you are not on a branch (detached HEAD).');
|
|
154
|
+
store.share({ branch });
|
|
155
|
+
await store.sync();
|
|
156
|
+
ok(`shared ${BOLD}${identity.slug}/${branch}${RESET} with the team board`);
|
|
157
|
+
const snippet = `${identity.name} shared ${branch} on the ${repo.name} board — run: npx design-share`;
|
|
158
|
+
if (copyToClipboard(snippet)) dim('slack snippet copied to your clipboard');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// default command: up
|
|
163
|
+
const existing = await findRunningInstance(repo.root, args.port);
|
|
164
|
+
if (existing) {
|
|
165
|
+
ok(`already running at http://localhost:${existing.port}`);
|
|
166
|
+
if (args.open) openBrowser(`http://localhost:${existing.port}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const previews = new PreviewManager(repo.root, preview, identity);
|
|
171
|
+
const server = new DesignShareServer({ repo, identity, store, previews, port: args.port });
|
|
172
|
+
const port = await server.listen();
|
|
173
|
+
const url = `http://localhost:${port}`;
|
|
174
|
+
|
|
175
|
+
upsertDaemon(repo.root, {
|
|
176
|
+
port,
|
|
177
|
+
pid: process.pid,
|
|
178
|
+
name: repo.name,
|
|
179
|
+
user: identity.slug,
|
|
180
|
+
nodePath: process.execPath,
|
|
181
|
+
cliPath: CLI_PATH,
|
|
182
|
+
keepAlive: true,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
console.log('');
|
|
186
|
+
step(`dashboard: ${BLUE}${url}${RESET}`);
|
|
187
|
+
if (branch) {
|
|
188
|
+
previews.ensureOwn(branch).catch(() => { /* surfaced in dashboard */ });
|
|
189
|
+
dim(`your preview of ${branch} is starting behind the scenes`);
|
|
190
|
+
}
|
|
191
|
+
store.sync().catch(() => { /* surfaced in dashboard */ });
|
|
192
|
+
if (args.open) openBrowser(url);
|
|
193
|
+
|
|
194
|
+
const key = branch ? `${identity.slug}/${branch}` : null;
|
|
195
|
+
const alreadyShared = key && store.state.shares[key] && store.state.shares[key].active;
|
|
196
|
+
if (branch && !alreadyShared && !args.daemon) {
|
|
197
|
+
const answer = args.yes ? 'y' : await ask(`\n share ${BOLD}${identity.slug}/${branch}${RESET} with the team board? (Y/n) `, 'y');
|
|
198
|
+
if (answer.toLowerCase() !== 'n') {
|
|
199
|
+
store.share({ branch });
|
|
200
|
+
store.sync().catch(() => {});
|
|
201
|
+
ok('shared — teammates just run: npx design-share');
|
|
202
|
+
const snippet = `${identity.name} shared ${branch} on the ${repo.name} board — run: npx design-share`;
|
|
203
|
+
if (copyToClipboard(snippet)) dim('slack snippet copied to your clipboard');
|
|
204
|
+
} else {
|
|
205
|
+
dim('just browsing. share later from the dashboard or: npx design-share share');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!args.daemon) {
|
|
210
|
+
if (appInstalled()) {
|
|
211
|
+
launchApp().catch(() => {});
|
|
212
|
+
console.log('');
|
|
213
|
+
dim('menu bar app is watching this repo — previews survive closing this terminal.');
|
|
214
|
+
} else if (appAvailable() && process.stdin.isTTY && readUserConfig().appOffer !== 'never') {
|
|
215
|
+
console.log('');
|
|
216
|
+
dim('tip: previews stop when this terminal closes.');
|
|
217
|
+
const answer = await ask(' install the menu bar app to keep them alive? (y/N/never) ', 'n');
|
|
218
|
+
if (answer.toLowerCase() === 'y') {
|
|
219
|
+
await installApp();
|
|
220
|
+
await launchApp();
|
|
221
|
+
writeUserConfig({ appOffer: 'done' });
|
|
222
|
+
ok('installed — look up ↗ the app adopts this repo automatically');
|
|
223
|
+
} else if (answer.toLowerCase() === 'never') {
|
|
224
|
+
writeUserConfig({ appOffer: 'never' });
|
|
225
|
+
dim('ok, never asking again. npx design-share app changes your mind later.');
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
console.log('');
|
|
229
|
+
dim('tip: previews stop when this terminal closes. ctrl+c quits.');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Deliberate quit removes the daemon from the registry. A terminal that
|
|
234
|
+
// simply closes sends SIGHUP; the entry stays so the menu bar app respawns
|
|
235
|
+
// the daemon and previews come back without the terminal.
|
|
236
|
+
const shutdown = (keepEntry) => () => {
|
|
237
|
+
console.log('');
|
|
238
|
+
dim('stopping previews and server…');
|
|
239
|
+
if (!keepEntry) removeDaemon(repo.root);
|
|
240
|
+
server.close();
|
|
241
|
+
process.exit(0);
|
|
242
|
+
};
|
|
243
|
+
process.on('SIGINT', shutdown(false));
|
|
244
|
+
process.on('SIGTERM', shutdown(false));
|
|
245
|
+
process.on('SIGHUP', shutdown(true));
|
|
246
|
+
}
|