@stackable-labs/cli-app-extension 1.4.2 → 1.5.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.
Files changed (2) hide show
  1. package/dist/index.js +986 -237
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -7,27 +7,33 @@ import { render } from "ink";
7
7
 
8
8
  // src/App.tsx
9
9
  import { join as join2 } from "path";
10
- import { Box as Box12, Text as Text12, useApp } from "ink";
10
+ import { Box as Box13, Text as Text13, useApp } from "ink";
11
11
  import { useCallback, useState as useState8 } from "react";
12
12
 
13
13
  // src/components/Confirm.tsx
14
- import { Box as Box2, Text as Text2, useInput } from "ink";
15
- import { useState } from "react";
14
+ import { Box as Box2, Text as Text2, useFocus, useFocusManager, useInput as useInput2 } from "ink";
15
+ import TextInput from "ink-text-input";
16
+ import { useEffect, useState } from "react";
16
17
 
17
18
  // src/components/StepShell.tsx
18
- import { Box, Text } from "ink";
19
+ import { Box, Text, useInput } from "ink";
19
20
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
20
21
  var divider = (width) => "\u2500".repeat(width);
21
22
  var INNER_DIVIDER_WIDTH = 40;
22
- var StepShell = ({ title, hint, children, footer, onBack, backFocused }) => {
23
+ var StepShell = ({ title, hint, children, footer, onBack }) => {
23
24
  const termWidth = process.stdout.columns ?? 80;
25
+ useInput((_, key) => {
26
+ if (onBack && key.escape) {
27
+ onBack();
28
+ }
29
+ });
24
30
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 0, children: [
25
31
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: divider(termWidth) }),
26
32
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, gap: 1, children: [
27
33
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 0, children: [
28
34
  onBack && /* @__PURE__ */ jsxs(Box, { gap: 1, marginBottom: 1, children: [
29
- /* @__PURE__ */ jsx(Text, { color: backFocused ? "cyan" : void 0, children: backFocused ? "\u276F" : " " }),
30
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2190 Back" })
35
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2190 Back" }),
36
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "(Esc)" })
31
37
  ] }),
32
38
  /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: title }),
33
39
  hint && /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint })
@@ -44,23 +50,74 @@ var StepShell = ({ title, hint, children, footer, onBack, backFocused }) => {
44
50
 
45
51
  // src/components/Confirm.tsx
46
52
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
47
- var Confirm = ({ name, extensionVersion, extensionPort, previewPort, targets, outputDir, onConfirm, onCancel, onBack }) => {
48
- const [backFocused, setBackFocused] = useState(false);
49
- const [selected, setSelected] = useState("y");
50
- useInput((input, key) => {
51
- if (key.upArrow && onBack) {
52
- setBackFocused(true);
53
+ var compareSemver = (a, b) => {
54
+ const pa = a.split(".").map(Number);
55
+ const pb = b.split(".").map(Number);
56
+ for (let i = 0; i < 3; i++) {
57
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return 1;
58
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return -1;
59
+ }
60
+ return 0;
61
+ };
62
+ var VersionRow = ({ currentVersion, value, onChange, onFocusChange, onConfirm, onCancel, selected }) => {
63
+ const { isFocused } = useFocus();
64
+ const { focusPrevious, focusNext } = useFocusManager();
65
+ useInput2((_, key) => {
66
+ if (!isFocused) return;
67
+ if (key.upArrow || key.tab && key.shift) {
68
+ focusPrevious();
53
69
  return;
54
70
  }
55
- if (key.downArrow && backFocused) {
56
- setBackFocused(false);
71
+ if (key.tab && !key.shift) {
72
+ focusNext();
57
73
  return;
58
74
  }
59
- if (key.return && backFocused) {
60
- onBack?.();
61
- return;
75
+ if (key.return) {
76
+ const isValid2 = /^\d+\.\d+\.\d+$/.test(value) && compareSemver(value, currentVersion) > 0;
77
+ if (!isValid2) return;
78
+ if (selected === "y") onConfirm();
79
+ else onCancel();
62
80
  }
63
- if (backFocused) return;
81
+ });
82
+ useEffect(() => {
83
+ onFocusChange(isFocused);
84
+ }, [isFocused, onFocusChange]);
85
+ const isValid = /^\d+\.\d+\.\d+$/.test(value) && compareSemver(value, currentVersion) > 0;
86
+ const showError = isFocused && value !== "" && !isValid;
87
+ const hasChange = value !== currentVersion;
88
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
89
+ /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
90
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Version " }),
91
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
92
+ currentVersion,
93
+ hasChange ? " \u2192 " : ""
94
+ ] }),
95
+ isFocused ? /* @__PURE__ */ jsx2(TextInput, { value, onChange, placeholder: value }) : hasChange && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: value }),
96
+ isFocused && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "(edit to override)" })
97
+ ] }),
98
+ showError && /* @__PURE__ */ jsx2(Text2, { color: "red", children: " Version must be greater than " + currentVersion })
99
+ ] });
100
+ };
101
+ var Confirm = ({
102
+ command,
103
+ name,
104
+ extensionVersion,
105
+ extensionPort,
106
+ previewPort,
107
+ targets,
108
+ outputDir,
109
+ bundleUrl,
110
+ enabled,
111
+ newVersion,
112
+ onVersionOverride,
113
+ onConfirm,
114
+ onCancel,
115
+ onBack
116
+ }) => {
117
+ const [selected, setSelected] = useState("y");
118
+ const [overrideFocused, setOverrideFocused] = useState(false);
119
+ useInput2((input, key) => {
120
+ if (overrideFocused) return;
64
121
  if (key.leftArrow || key.rightArrow) {
65
122
  setSelected((s) => s === "y" ? "n" : "y");
66
123
  return;
@@ -74,17 +131,21 @@ var Confirm = ({ name, extensionVersion, extensionPort, previewPort, targets, ou
74
131
  return;
75
132
  }
76
133
  if (key.return) {
77
- if (selected === "y") onConfirm();
78
- else onCancel();
134
+ if (selected === "y") {
135
+ if (newVersion && extensionVersion && !/^\d+\.\d+\.\d+$/.test(newVersion)) return;
136
+ if (newVersion && extensionVersion && compareSemver(newVersion, extensionVersion) <= 0) return;
137
+ onConfirm();
138
+ } else {
139
+ onCancel();
140
+ }
79
141
  }
80
142
  });
81
143
  return /* @__PURE__ */ jsx2(
82
144
  StepShell,
83
145
  {
84
- title: "Ready to scaffold",
146
+ title: command === "update" /* UPDATE */ ? "Ready to update" : "Ready to scaffold",
85
147
  hint: "Review your settings before proceeding",
86
148
  onBack,
87
- backFocused,
88
149
  footer: /* @__PURE__ */ jsxs2(Text2, { children: [
89
150
  "Proceed?",
90
151
  " ",
@@ -97,22 +158,45 @@ var Confirm = ({ name, extensionVersion, extensionPort, previewPort, targets, ou
97
158
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Name " }),
98
159
  /* @__PURE__ */ jsx2(Text2, { dimColor: !!extensionVersion, children: name })
99
160
  ] }),
100
- extensionVersion && /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
161
+ extensionVersion && command === "update" /* UPDATE */ && newVersion && onVersionOverride && /* @__PURE__ */ jsx2(
162
+ VersionRow,
163
+ {
164
+ currentVersion: extensionVersion,
165
+ value: newVersion,
166
+ onChange: onVersionOverride,
167
+ onFocusChange: setOverrideFocused,
168
+ onConfirm,
169
+ onCancel,
170
+ selected
171
+ }
172
+ ),
173
+ extensionVersion && !(command === "update" /* UPDATE */ && newVersion && onVersionOverride) && /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
101
174
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Version " }),
102
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: extensionVersion })
175
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
176
+ extensionVersion,
177
+ newVersion && newVersion !== extensionVersion ? ` \u2192 ${newVersion}` : ""
178
+ ] })
103
179
  ] }),
104
- /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
180
+ command !== "update" /* UPDATE */ && outputDir && /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
105
181
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Directory " }),
106
182
  /* @__PURE__ */ jsx2(Text2, { children: outputDir })
107
183
  ] }),
108
- /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
184
+ command !== "update" /* UPDATE */ && extensionPort !== void 0 && /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
109
185
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Extension port" }),
110
186
  /* @__PURE__ */ jsx2(Text2, { children: extensionPort })
111
187
  ] }),
112
- /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
188
+ command !== "update" /* UPDATE */ && previewPort !== void 0 && /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
113
189
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Preview port " }),
114
190
  /* @__PURE__ */ jsx2(Text2, { children: previewPort })
115
191
  ] }),
192
+ command === "update" /* UPDATE */ && bundleUrl && /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
193
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Bundle URL " }),
194
+ /* @__PURE__ */ jsx2(Text2, { children: bundleUrl })
195
+ ] }),
196
+ command === "update" /* UPDATE */ && enabled !== void 0 && /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
197
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Enabled " }),
198
+ /* @__PURE__ */ jsx2(Text2, { color: enabled ? "green" : "red", children: enabled ? "Yes" : "No" })
199
+ ] }),
116
200
  /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
117
201
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Targets " }),
118
202
  /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: targets.map((t) => /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -167,39 +251,22 @@ var Done = ({ name, outputDir }) => /* @__PURE__ */ jsxs3(Box3, { flexDirection:
167
251
 
168
252
  // src/components/NamePrompt.tsx
169
253
  import { Box as Box5, Text as Text5 } from "ink";
170
- import TextInput from "ink-text-input";
171
- import { useState as useState3 } from "react";
254
+ import TextInput2 from "ink-text-input";
255
+ import { useState as useState2 } from "react";
172
256
 
173
257
  // src/components/BackableInput.tsx
174
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
175
- import { useState as useState2 } from "react";
258
+ import { Box as Box4, Text as Text4 } from "ink";
176
259
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
177
- var BackableInput = ({ label, hint, onBack, children, error }) => {
178
- const [focus, setFocus] = useState2("input");
179
- useInput2((_input, key) => {
180
- if (key.upArrow && focus === "input" && onBack) {
181
- setFocus("back");
182
- return;
183
- }
184
- if (key.downArrow && focus === "back") {
185
- setFocus("input");
186
- return;
187
- }
188
- if (key.return && focus === "back") {
189
- onBack?.();
190
- }
191
- });
192
- return /* @__PURE__ */ jsx4(StepShell, { title: label, hint, onBack, backFocused: focus === "back", children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
193
- children(focus === "input"),
194
- error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error })
195
- ] }) });
196
- };
260
+ var BackableInput = ({ label, hint, onBack, children, error }) => /* @__PURE__ */ jsx4(StepShell, { title: label, hint, onBack, children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
261
+ children(true),
262
+ error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error })
263
+ ] }) });
197
264
 
198
265
  // src/components/NamePrompt.tsx
199
266
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
200
267
  var NamePrompt = ({ initialValue = "", onSubmit, onBack }) => {
201
- const [value, setValue] = useState3(initialValue);
202
- const [error, setError] = useState3();
268
+ const [value, setValue] = useState2(initialValue);
269
+ const [error, setError] = useState2();
203
270
  const handleSubmit = (val) => {
204
271
  const trimmed = val.trim();
205
272
  if (trimmed.length === 0) {
@@ -211,37 +278,31 @@ var NamePrompt = ({ initialValue = "", onSubmit, onBack }) => {
211
278
  };
212
279
  return /* @__PURE__ */ jsx5(BackableInput, { label: "What is your Extension name?", hint: "Press Enter to confirm", onBack, error, children: (isFocused) => /* @__PURE__ */ jsxs5(Box5, { children: [
213
280
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "> " }),
214
- /* @__PURE__ */ jsx5(TextInput, { value, onChange: setValue, onSubmit: handleSubmit, focus: isFocused })
281
+ /* @__PURE__ */ jsx5(TextInput2, { value, onChange: setValue, onSubmit: handleSubmit, focus: isFocused })
215
282
  ] }) });
216
283
  };
217
284
 
218
285
  // src/components/SettingsPrompt.tsx
219
- import { Box as Box6, Text as Text6, useFocus, useFocusManager, useInput as useInput3 } from "ink";
220
- import TextInput2 from "ink-text-input";
221
- import { useState as useState4 } from "react";
286
+ import { Box as Box6, Text as Text6, useFocus as useFocus2, useFocusManager as useFocusManager2, useInput as useInput3 } from "ink";
287
+ import TextInput3 from "ink-text-input";
288
+ import { useState as useState3 } from "react";
222
289
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
223
290
  var DEFAULT_EXTENSION_PORT = 5173;
224
291
  var DEFAULT_PREVIEW_PORT = DEFAULT_EXTENSION_PORT + 1;
225
- var FieldRow = ({ label, value, onChange, onSubmit, onConfirm, placeholder, autoFocus, isFirst, isLast, onFocusBack }) => {
226
- const { isFocused } = useFocus({ autoFocus });
227
- const { focusNext, focusPrevious } = useFocusManager();
292
+ var FieldRow = ({ label, value, onChange, onSubmit, onConfirm, placeholder, autoFocus, isFirst, isLast }) => {
293
+ const { isFocused } = useFocus2({ autoFocus });
294
+ const { focusNext, focusPrevious } = useFocusManager2();
228
295
  useInput3((_input, key) => {
229
296
  if (!isFocused) return;
230
297
  if (key.downArrow && !isLast) focusNext();
231
- if (key.upArrow) {
232
- if (isFirst && onFocusBack) {
233
- onFocusBack();
234
- } else if (!isFirst) {
235
- focusPrevious();
236
- }
237
- }
298
+ if (key.upArrow && !isFirst) focusPrevious();
238
299
  });
239
300
  return /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
240
301
  /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: label }),
241
302
  isFocused ? /* @__PURE__ */ jsxs6(Box6, { children: [
242
303
  /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "\u2192 " }),
243
304
  /* @__PURE__ */ jsx6(
244
- TextInput2,
305
+ TextInput3,
245
306
  {
246
307
  value,
247
308
  onChange,
@@ -257,23 +318,9 @@ var FieldRow = ({ label, value, onChange, onSubmit, onConfirm, placeholder, auto
257
318
  ] });
258
319
  };
259
320
  var SettingsPrompt = ({ defaultDir, onSubmit, onBack }) => {
260
- const [backFocused, setBackFocused] = useState4(false);
261
- const [extensionPort, setExtensionPort] = useState4(String(DEFAULT_EXTENSION_PORT));
262
- const [previewPort, setPreviewPort] = useState4(String(DEFAULT_PREVIEW_PORT));
263
- const [outputDir, setOutputDir] = useState4(defaultDir);
264
- const handleFocusBack = () => {
265
- if (onBack) setBackFocused(true);
266
- };
267
- useInput3((_input, key) => {
268
- if (key.downArrow && backFocused) {
269
- setBackFocused(false);
270
- return;
271
- }
272
- if (key.return && backFocused) {
273
- onBack?.();
274
- return;
275
- }
276
- });
321
+ const [extensionPort, setExtensionPort] = useState3(String(DEFAULT_EXTENSION_PORT));
322
+ const [previewPort, setPreviewPort] = useState3(String(DEFAULT_PREVIEW_PORT));
323
+ const [outputDir, setOutputDir] = useState3(defaultDir);
277
324
  const handleConfirm = (resolvedExtPort, resolvedPrevPort, resolvedDir) => {
278
325
  const dir = resolvedDir.trim();
279
326
  if (!dir) return;
@@ -310,7 +357,6 @@ var SettingsPrompt = ({ defaultDir, onSubmit, onBack }) => {
310
357
  title: "Project settings",
311
358
  hint: "\u2191\u2193 or Tab to move between fields, Enter to confirm",
312
359
  onBack,
313
- backFocused,
314
360
  children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
315
361
  /* @__PURE__ */ jsx6(
316
362
  FieldRow,
@@ -322,8 +368,7 @@ var SettingsPrompt = ({ defaultDir, onSubmit, onBack }) => {
322
368
  onConfirm: handleConfirm,
323
369
  placeholder: String(DEFAULT_EXTENSION_PORT),
324
370
  autoFocus: true,
325
- isFirst: true,
326
- onFocusBack: handleFocusBack
371
+ isFirst: true
327
372
  }
328
373
  ),
329
374
  /* @__PURE__ */ jsx6(
@@ -353,34 +398,284 @@ var SettingsPrompt = ({ defaultDir, onSubmit, onBack }) => {
353
398
  );
354
399
  };
355
400
 
401
+ // src/components/UpdateSettingsPrompt.tsx
402
+ import { Box as Box7, Text as Text7, useFocus as useFocus3, useFocusManager as useFocusManager3, useInput as useInput4 } from "ink";
403
+ import TextInput4 from "ink-text-input";
404
+ import { useState as useState4 } from "react";
405
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
406
+ var FieldRow2 = ({ label, value, onChange, placeholder, autoFocus, isFirst, isLast, onSubmitAll }) => {
407
+ const { isFocused } = useFocus3({ autoFocus });
408
+ const { focusNext, focusPrevious } = useFocusManager3();
409
+ useInput4((_, key) => {
410
+ if (!isFocused) return;
411
+ if (key.tab && !key.shift) {
412
+ if (!isLast) focusNext();
413
+ return;
414
+ }
415
+ if (key.tab && key.shift) {
416
+ if (!isFirst) focusPrevious();
417
+ return;
418
+ }
419
+ if (key.downArrow) {
420
+ if (!isLast) focusNext();
421
+ return;
422
+ }
423
+ if (key.upArrow) {
424
+ if (!isFirst) focusPrevious();
425
+ return;
426
+ }
427
+ if (key.return) {
428
+ onSubmitAll();
429
+ }
430
+ });
431
+ return /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
432
+ /* @__PURE__ */ jsx7(Text7, { dimColor: !isFocused, bold: isFocused, children: label.padEnd(14) }),
433
+ isFocused ? /* @__PURE__ */ jsx7(TextInput4, { value, onChange, placeholder }) : /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: value || placeholder || "" })
434
+ ] });
435
+ };
436
+ var TargetToggleRow = ({ targets, availableTargets, onToggle, autoFocus, isLast, onSubmitAll }) => {
437
+ const { isFocused } = useFocus3({ autoFocus });
438
+ const { focusNext, focusPrevious } = useFocusManager3();
439
+ const [cursor, setCursor] = useState4(0);
440
+ useInput4((input, key) => {
441
+ if (!isFocused) return;
442
+ if (key.tab && !key.shift) {
443
+ focusNext();
444
+ return;
445
+ }
446
+ if (key.tab && key.shift) {
447
+ if (cursor > 0) {
448
+ setCursor((c) => c - 1);
449
+ } else {
450
+ focusPrevious();
451
+ }
452
+ return;
453
+ }
454
+ if (key.downArrow) {
455
+ if (cursor < availableTargets.length - 1) {
456
+ setCursor((c) => c + 1);
457
+ } else if (!isLast) {
458
+ focusNext();
459
+ }
460
+ return;
461
+ }
462
+ if (key.upArrow) {
463
+ if (cursor > 0) {
464
+ setCursor((c) => c - 1);
465
+ } else {
466
+ focusPrevious();
467
+ }
468
+ return;
469
+ }
470
+ if (input === " " && availableTargets[cursor]) {
471
+ onToggle(availableTargets[cursor]);
472
+ return;
473
+ }
474
+ if (key.return) {
475
+ onSubmitAll();
476
+ }
477
+ });
478
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
479
+ /* @__PURE__ */ jsx7(Text7, { dimColor: !isFocused, bold: isFocused, children: "Targets".padEnd(14) }),
480
+ /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingLeft: 2, children: [
481
+ availableTargets.map((t, i) => {
482
+ const selected = targets.includes(t);
483
+ const isCursor = isFocused && i === cursor;
484
+ return /* @__PURE__ */ jsxs7(Box7, { gap: 1, children: [
485
+ /* @__PURE__ */ jsx7(Text7, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
486
+ /* @__PURE__ */ jsx7(Text7, { color: selected ? "green" : "gray", children: selected ? "\u25C9" : "\u25CB" }),
487
+ /* @__PURE__ */ jsx7(Text7, { bold: isCursor, children: t })
488
+ ] }, t);
489
+ }),
490
+ isFocused && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " (space to toggle)" })
491
+ ] })
492
+ ] });
493
+ };
494
+ var EnabledToggleRow = ({ enabled, onToggle, autoFocus, isLast, onSubmitAll }) => {
495
+ const { isFocused } = useFocus3({ autoFocus });
496
+ const { focusNext, focusPrevious } = useFocusManager3();
497
+ useInput4((input, key) => {
498
+ if (!isFocused) return;
499
+ if (key.tab && !key.shift) {
500
+ if (!isLast) focusNext();
501
+ return;
502
+ }
503
+ if (key.tab && key.shift) {
504
+ focusPrevious();
505
+ return;
506
+ }
507
+ if (key.downArrow) {
508
+ if (!isLast) focusNext();
509
+ return;
510
+ }
511
+ if (key.upArrow) {
512
+ focusPrevious();
513
+ return;
514
+ }
515
+ if (input === " " || key.leftArrow || key.rightArrow) {
516
+ onToggle();
517
+ return;
518
+ }
519
+ if (key.return) {
520
+ onSubmitAll();
521
+ }
522
+ });
523
+ return /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
524
+ /* @__PURE__ */ jsx7(Text7, { dimColor: !isFocused, bold: isFocused, children: "Enabled".padEnd(14) }),
525
+ /* @__PURE__ */ jsx7(Text7, { color: enabled ? "green" : "red", bold: isFocused, children: enabled ? "Yes" : "No" }),
526
+ isFocused && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(space/arrows to toggle)" })
527
+ ] });
528
+ };
529
+ var ForceMajorToggleRow = ({ forceMajor, onToggle, autoFocus, isLast, onSubmitAll }) => {
530
+ const { isFocused } = useFocus3({ autoFocus });
531
+ const { focusPrevious, focusNext } = useFocusManager3();
532
+ useInput4((input, key) => {
533
+ if (!isFocused) return;
534
+ if (key.tab && !key.shift) {
535
+ if (!isLast) focusNext();
536
+ return;
537
+ }
538
+ if (key.tab && key.shift) {
539
+ focusPrevious();
540
+ return;
541
+ }
542
+ if (key.downArrow) {
543
+ if (!isLast) focusNext();
544
+ return;
545
+ }
546
+ if (key.upArrow) {
547
+ focusPrevious();
548
+ return;
549
+ }
550
+ if (input === " " || key.leftArrow || key.rightArrow) {
551
+ onToggle();
552
+ return;
553
+ }
554
+ if (key.return) {
555
+ onSubmitAll();
556
+ }
557
+ });
558
+ return /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
559
+ /* @__PURE__ */ jsx7(Text7, { dimColor: !isFocused, bold: isFocused, children: "Force major".padEnd(14) }),
560
+ /* @__PURE__ */ jsx7(Text7, { color: forceMajor ? "yellow" : "gray", bold: isFocused, children: forceMajor ? "Yes" : "No" }),
561
+ isFocused && /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "(space/arrows to toggle)" })
562
+ ] });
563
+ };
564
+ var UpdateSettingsPrompt = ({
565
+ name: initialName,
566
+ targets: initialTargets,
567
+ availableTargets,
568
+ bundleUrl: initialBundleUrl,
569
+ enabled: initialEnabled,
570
+ onSubmit,
571
+ onBack
572
+ }) => {
573
+ const [nameValue, setNameValue] = useState4(initialName);
574
+ const [targetsValue, setTargetsValue] = useState4(initialTargets);
575
+ const [bundleUrlValue, setBundleUrlValue] = useState4(initialBundleUrl);
576
+ const [enabledValue, setEnabledValue] = useState4(initialEnabled);
577
+ const [forceMajorValue, setForceMajorValue] = useState4(false);
578
+ const handleToggleTarget = (target) => {
579
+ setTargetsValue((prev) => prev.includes(target) ? prev.filter((t) => t !== target) : [...prev, target]);
580
+ };
581
+ const handleSubmitAll = () => {
582
+ if (targetsValue.length === 0) return;
583
+ onSubmit({
584
+ name: nameValue,
585
+ targets: targetsValue,
586
+ bundleUrl: bundleUrlValue,
587
+ enabled: enabledValue,
588
+ forceMajor: forceMajorValue
589
+ });
590
+ };
591
+ return /* @__PURE__ */ jsx7(
592
+ StepShell,
593
+ {
594
+ title: "Update Extension settings",
595
+ hint: "Tab/arrows to navigate, Enter to submit",
596
+ onBack,
597
+ children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
598
+ /* @__PURE__ */ jsx7(
599
+ FieldRow2,
600
+ {
601
+ label: "Name",
602
+ value: nameValue,
603
+ onChange: setNameValue,
604
+ placeholder: initialName,
605
+ autoFocus: true,
606
+ isFirst: true,
607
+ onSubmitAll: handleSubmitAll
608
+ }
609
+ ),
610
+ /* @__PURE__ */ jsx7(
611
+ TargetToggleRow,
612
+ {
613
+ targets: targetsValue,
614
+ availableTargets,
615
+ onToggle: handleToggleTarget,
616
+ onSubmitAll: handleSubmitAll
617
+ }
618
+ ),
619
+ /* @__PURE__ */ jsx7(
620
+ FieldRow2,
621
+ {
622
+ label: "Bundle URL",
623
+ value: bundleUrlValue,
624
+ onChange: setBundleUrlValue,
625
+ placeholder: "http://localhost:5173",
626
+ onSubmitAll: handleSubmitAll
627
+ }
628
+ ),
629
+ /* @__PURE__ */ jsx7(
630
+ EnabledToggleRow,
631
+ {
632
+ enabled: enabledValue,
633
+ onToggle: () => setEnabledValue((v) => !v),
634
+ onSubmitAll: handleSubmitAll
635
+ }
636
+ ),
637
+ /* @__PURE__ */ jsx7(
638
+ ForceMajorToggleRow,
639
+ {
640
+ forceMajor: forceMajorValue,
641
+ onToggle: () => setForceMajorValue((v) => !v),
642
+ isLast: true,
643
+ onSubmitAll: handleSubmitAll
644
+ }
645
+ )
646
+ ] })
647
+ }
648
+ );
649
+ };
650
+
356
651
  // src/components/ScaffoldProgress.tsx
357
- import { Box as Box7, Text as Text7 } from "ink";
652
+ import { Box as Box8, Text as Text8 } from "ink";
358
653
  import Spinner from "ink-spinner";
359
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
654
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
360
655
  var stepIcon = (status) => {
361
656
  switch (status) {
362
657
  case "running":
363
- return /* @__PURE__ */ jsx7(Spinner, { type: "dots" });
658
+ return /* @__PURE__ */ jsx8(Spinner, { type: "dots" });
364
659
  case "done":
365
- return /* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2714" });
660
+ return /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714" });
366
661
  case "error":
367
- return /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2716" });
662
+ return /* @__PURE__ */ jsx8(Text8, { color: "red", children: "\u2716" });
368
663
  default:
369
- return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u25CB" });
664
+ return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u25CB" });
370
665
  }
371
666
  };
372
- var ScaffoldProgress = ({ steps }) => /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
373
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Scaffolding your Extension\u2026" }),
374
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: steps.map((step) => /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
667
+ var ScaffoldProgress = ({ steps }) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
668
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Scaffolding your Extension\u2026" }),
669
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: steps.map((step) => /* @__PURE__ */ jsxs8(Box8, { gap: 2, children: [
375
670
  stepIcon(step.status),
376
- /* @__PURE__ */ jsx7(Text7, { dimColor: step.status === "pending", color: step.status === "running" ? "cyan" : void 0, children: step.label })
671
+ /* @__PURE__ */ jsx8(Text8, { dimColor: step.status === "pending", color: step.status === "running" ? "cyan" : void 0, children: step.label })
377
672
  ] }, step.label)) })
378
673
  ] });
379
674
 
380
675
  // src/components/TargetSelect.tsx
381
- import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
676
+ import { Box as Box9, Text as Text9, useInput as useInput5 } from "ink";
382
677
  import { useState as useState5 } from "react";
383
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
678
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
384
679
  var TARGET_DESCRIPTIONS = {
385
680
  "slot.header": "Renders content in the panel header area",
386
681
  "slot.content": "Renders the main panel body (includes store + navigation state)",
@@ -389,30 +684,20 @@ var TARGET_DESCRIPTIONS = {
389
684
  };
390
685
  var TargetSelect = ({ availableTargets, preSelected, onSubmit, onBack }) => {
391
686
  const [cursor, setCursor] = useState5(0);
392
- const [backFocused, setBackFocused] = useState5(false);
393
687
  const [selected, setSelected] = useState5(
394
688
  new Set(preSelected ?? (availableTargets.includes("slot.content") ? ["slot.content"] : []))
395
689
  );
396
690
  const [error, setError] = useState5();
397
- useInput4((input, key) => {
691
+ useInput5((input, key) => {
398
692
  if (key.upArrow) {
399
- if (cursor === 0 && onBack) {
400
- setBackFocused(true);
401
- } else {
402
- setBackFocused(false);
403
- setCursor((c) => Math.max(0, c - 1));
404
- }
693
+ setCursor((c) => Math.max(0, c - 1));
405
694
  return;
406
695
  }
407
696
  if (key.downArrow) {
408
- if (backFocused) {
409
- setBackFocused(false);
410
- } else {
411
- setCursor((c) => Math.min(availableTargets.length - 1, c + 1));
412
- }
697
+ setCursor((c) => Math.min(availableTargets.length - 1, c + 1));
413
698
  return;
414
699
  }
415
- if (input === " " && !backFocused) {
700
+ if (input === " ") {
416
701
  const target = availableTargets[cursor];
417
702
  setSelected((prev) => {
418
703
  const next = new Set(prev);
@@ -427,10 +712,6 @@ var TargetSelect = ({ availableTargets, preSelected, onSubmit, onBack }) => {
427
712
  return;
428
713
  }
429
714
  if (key.return) {
430
- if (backFocused) {
431
- onBack?.();
432
- return;
433
- }
434
715
  if (selected.size === 0) {
435
716
  setError("Select at least one Surface target/slot");
436
717
  return;
@@ -438,36 +719,35 @@ var TargetSelect = ({ availableTargets, preSelected, onSubmit, onBack }) => {
438
719
  onSubmit([...selected]);
439
720
  }
440
721
  });
441
- return /* @__PURE__ */ jsxs8(
722
+ return /* @__PURE__ */ jsxs9(
442
723
  StepShell,
443
724
  {
444
725
  title: "Select Surface targets/slots",
445
726
  hint: "Space to toggle, Enter to confirm",
446
727
  onBack,
447
- backFocused,
448
728
  children: [
449
- /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", gap: 1, children: availableTargets.map((target, i) => {
729
+ /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", gap: 1, children: availableTargets.map((target, i) => {
450
730
  const isSelected = selected.has(target);
451
- const isCursor = i === cursor && !backFocused;
452
- return /* @__PURE__ */ jsxs8(Box8, { gap: 1, children: [
453
- /* @__PURE__ */ jsx8(Text8, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
454
- /* @__PURE__ */ jsx8(Text8, { color: isSelected ? "green" : void 0, children: isSelected ? "\u25C9" : "\u25CB" }),
455
- /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
456
- /* @__PURE__ */ jsx8(Text8, { bold: isSelected, children: target }),
457
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: TARGET_DESCRIPTIONS[target] ?? target })
731
+ const isCursor = i === cursor;
732
+ return /* @__PURE__ */ jsxs9(Box9, { gap: 1, children: [
733
+ /* @__PURE__ */ jsx9(Text9, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
734
+ /* @__PURE__ */ jsx9(Text9, { color: isSelected ? "green" : void 0, children: isSelected ? "\u25C9" : "\u25CB" }),
735
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
736
+ /* @__PURE__ */ jsx9(Text9, { bold: isSelected, children: target }),
737
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: TARGET_DESCRIPTIONS[target] ?? target })
458
738
  ] })
459
739
  ] }, target);
460
740
  }) }),
461
- error && /* @__PURE__ */ jsx8(Text8, { color: "red", children: error })
741
+ error && /* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
462
742
  ]
463
743
  }
464
744
  );
465
745
  };
466
746
 
467
747
  // src/components/AppSelect.tsx
468
- import { Box as Box10, Text as Text10, useInput as useInput5 } from "ink";
748
+ import { Box as Box11, Text as Text11, useInput as useInput6 } from "ink";
469
749
  import Spinner2 from "ink-spinner";
470
- import { useEffect, useState as useState6 } from "react";
750
+ import { useEffect as useEffect2, useState as useState6 } from "react";
471
751
 
472
752
  // src/lib/api.ts
473
753
  var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
@@ -520,10 +800,22 @@ var fetchExtensions = async (appId) => {
520
800
  ])
521
801
  );
522
802
  };
803
+ var updateExtension = async (appId, extensionId, payload) => {
804
+ const baseUrl = getAdminApiBaseUrl();
805
+ const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions/${extensionId}`, {
806
+ method: "PUT",
807
+ headers: { "content-type": "application/json" },
808
+ body: JSON.stringify(payload)
809
+ });
810
+ if (!res.ok) {
811
+ const body = await res.text();
812
+ throw new Error(`Failed to update extension: ${res.status} ${body}`);
813
+ }
814
+ };
523
815
 
524
816
  // src/components/Banner.tsx
525
- import { Box as Box9, Text as Text9 } from "ink";
526
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
817
+ import { Box as Box10, Text as Text10 } from "ink";
818
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
527
819
  var WORDMARK = [
528
820
  " _ _ _ _ ",
529
821
  " ___| |_ __ _ ___| | ____ _| |__ | | ___",
@@ -559,23 +851,23 @@ var gradientColor = (row, col, rows, cols) => {
559
851
  var Banner = () => {
560
852
  const termWidth = process.stdout.columns ?? 80;
561
853
  const maxLen = Math.max(...WORDMARK.map((l) => l.length));
562
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
563
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "\u2500".repeat(termWidth) }),
564
- /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", paddingX: 1, paddingY: 1, children: WORDMARK.map((line, row) => /* @__PURE__ */ jsx9(Box9, { children: line.split("").map((ch, col) => /* @__PURE__ */ jsx9(Text9, { bold: true, color: ch === " " ? void 0 : gradientColor(row, col, WORDMARK.length, maxLen), children: ch }, col)) }, row)) })
854
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
855
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2500".repeat(termWidth) }),
856
+ /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", paddingX: 1, paddingY: 1, children: WORDMARK.map((line, row) => /* @__PURE__ */ jsx10(Box10, { children: line.split("").map((ch, col) => /* @__PURE__ */ jsx10(Text10, { bold: true, color: ch === " " ? void 0 : gradientColor(row, col, WORDMARK.length, maxLen), children: ch }, col)) }, row)) })
565
857
  ] });
566
858
  };
567
859
 
568
860
  // src/components/AppSelect.tsx
569
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
861
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
570
862
  var AppSelect = ({ onSubmit }) => {
571
863
  const [apps, setApps] = useState6([]);
572
864
  const [loading, setLoading] = useState6(true);
573
865
  const [error, setError] = useState6();
574
866
  const [cursor, setCursor] = useState6(0);
575
- useEffect(() => {
867
+ useEffect2(() => {
576
868
  fetchApps().then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
577
869
  }, []);
578
- useInput5((_, key) => {
870
+ useInput6((_, key) => {
579
871
  if (loading || error || apps.length === 0) return;
580
872
  if (key.upArrow) {
581
873
  setCursor((c) => Math.max(0, c - 1));
@@ -587,26 +879,26 @@ var AppSelect = ({ onSubmit }) => {
587
879
  });
588
880
  const renderContent = () => {
589
881
  if (loading) {
590
- return /* @__PURE__ */ jsxs10(Box10, { gap: 2, children: [
591
- /* @__PURE__ */ jsx10(Spinner2, { type: "dots" }),
592
- /* @__PURE__ */ jsx10(Text10, { children: "Loading available Apps\u2026" })
882
+ return /* @__PURE__ */ jsxs11(Box11, { gap: 2, children: [
883
+ /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
884
+ /* @__PURE__ */ jsx11(Text11, { children: "Loading available Apps\u2026" })
593
885
  ] });
594
886
  }
595
887
  if (error) {
596
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", gap: 1, children: [
597
- /* @__PURE__ */ jsx10(Text10, { color: "red", bold: true, children: "Failed to load Apps" }),
598
- /* @__PURE__ */ jsx10(Text10, { color: "red", children: error })
888
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", gap: 1, children: [
889
+ /* @__PURE__ */ jsx11(Text11, { color: "red", bold: true, children: "Failed to load Apps" }),
890
+ /* @__PURE__ */ jsx11(Text11, { color: "red", children: error })
599
891
  ] });
600
892
  }
601
893
  if (apps.length === 0) {
602
- return /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "No Apps available. Contact your administrator." });
894
+ return /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "No Apps available. Contact your administrator." });
603
895
  }
604
- return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: apps.map((app, i) => {
896
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: apps.map((app, i) => {
605
897
  const isCursor = i === cursor;
606
- return /* @__PURE__ */ jsxs10(Box10, { gap: 1, children: [
607
- /* @__PURE__ */ jsx10(Text10, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
608
- /* @__PURE__ */ jsx10(Text10, { bold: isCursor, children: app.name }),
609
- /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
898
+ return /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
899
+ /* @__PURE__ */ jsx11(Text11, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
900
+ /* @__PURE__ */ jsx11(Text11, { bold: isCursor, children: app.name }),
901
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
610
902
  "(",
611
903
  app.id,
612
904
  ")"
@@ -614,49 +906,39 @@ var AppSelect = ({ onSubmit }) => {
614
906
  ] }, app.id);
615
907
  }) });
616
908
  };
617
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
618
- /* @__PURE__ */ jsx10(Banner, {}),
619
- /* @__PURE__ */ jsx10(StepShell, { title: "Select the App you are building an Extension for:", children: renderContent() })
909
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
910
+ /* @__PURE__ */ jsx11(Banner, {}),
911
+ /* @__PURE__ */ jsx11(StepShell, { title: "Select the App you are building an Extension for:", children: renderContent() })
620
912
  ] });
621
913
  };
622
914
 
623
915
  // src/components/ExtensionSelect.tsx
624
- import { Box as Box11, Text as Text11, useInput as useInput6 } from "ink";
916
+ import { Box as Box12, Text as Text12, useInput as useInput7 } from "ink";
625
917
  import Spinner3 from "ink-spinner";
626
- import { useEffect as useEffect2, useState as useState7 } from "react";
627
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
628
- var ExtensionSelect = ({ appId, onSubmit, onBack }) => {
918
+ import { useEffect as useEffect3, useState as useState7 } from "react";
919
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
920
+ var ExtensionSelect = ({ appId, command, onSubmit, onBack }) => {
629
921
  const [extensions, setExtensions] = useState7([]);
630
922
  const [loading, setLoading] = useState7(true);
631
923
  const [error, setError] = useState7();
632
924
  const [cursor, setCursor] = useState7(0);
633
- const [backFocused, setBackFocused] = useState7(false);
634
- useEffect2(() => {
925
+ useEffect3(() => {
635
926
  fetchExtensions(appId).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
636
927
  }, [appId]);
637
- useInput6((_, key) => {
928
+ useInput7((_, key) => {
638
929
  if (key.upArrow) {
639
- if (loading || error || extensions.length === 0 || cursor === 0) {
640
- if (onBack) setBackFocused(true);
641
- } else {
642
- setBackFocused(false);
930
+ if (!loading && !error && extensions.length > 0) {
643
931
  setCursor((c) => Math.max(0, c - 1));
644
932
  }
645
933
  return;
646
934
  }
647
935
  if (key.downArrow) {
648
- if (backFocused) {
649
- setBackFocused(false);
650
- } else if (!loading && !error && extensions.length > 0) {
936
+ if (!loading && !error && extensions.length > 0) {
651
937
  setCursor((c) => Math.min(extensions.length - 1, c + 1));
652
938
  }
653
939
  return;
654
940
  }
655
941
  if (key.return) {
656
- if (backFocused) {
657
- onBack?.();
658
- return;
659
- }
660
942
  if (!loading && !error && extensions.length > 0) {
661
943
  onSubmit(extensions[cursor]);
662
944
  }
@@ -664,30 +946,30 @@ var ExtensionSelect = ({ appId, onSubmit, onBack }) => {
664
946
  });
665
947
  const renderContent = () => {
666
948
  if (loading) {
667
- return /* @__PURE__ */ jsxs11(Box11, { gap: 2, children: [
668
- /* @__PURE__ */ jsx11(Spinner3, { type: "dots" }),
669
- /* @__PURE__ */ jsx11(Text11, { children: "Loading Extensions\u2026" })
949
+ return /* @__PURE__ */ jsxs12(Box12, { gap: 2, children: [
950
+ /* @__PURE__ */ jsx12(Spinner3, { type: "dots" }),
951
+ /* @__PURE__ */ jsx12(Text12, { children: "Loading Extensions\u2026" })
670
952
  ] });
671
953
  }
672
954
  if (error) {
673
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", gap: 1, children: [
674
- /* @__PURE__ */ jsx11(Text11, { color: "red", bold: true, children: "Failed to load Extensions" }),
675
- /* @__PURE__ */ jsx11(Text11, { color: "red", children: error })
955
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", gap: 1, children: [
956
+ /* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "Failed to load Extensions" }),
957
+ /* @__PURE__ */ jsx12(Text12, { color: "red", children: error })
676
958
  ] });
677
959
  }
678
960
  if (extensions.length === 0) {
679
- return /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "No Extensions found for this App." });
961
+ return /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "No Extensions found for this App." });
680
962
  }
681
- return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: extensions.map((ext, i) => {
682
- const isCursor = i === cursor && !backFocused;
683
- return /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
684
- /* @__PURE__ */ jsx11(Text11, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
685
- /* @__PURE__ */ jsx11(Text11, { bold: isCursor, children: ext.manifest.name }),
686
- /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
963
+ return /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", children: extensions.map((ext, i) => {
964
+ const isCursor = i === cursor;
965
+ return /* @__PURE__ */ jsxs12(Box12, { gap: 1, children: [
966
+ /* @__PURE__ */ jsx12(Text12, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
967
+ /* @__PURE__ */ jsx12(Text12, { bold: isCursor, children: ext.manifest.name }),
968
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
687
969
  "v",
688
970
  ext.manifest.version
689
971
  ] }),
690
- /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
972
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
691
973
  "(",
692
974
  ext.id,
693
975
  ")"
@@ -695,7 +977,14 @@ var ExtensionSelect = ({ appId, onSubmit, onBack }) => {
695
977
  ] }, ext.id);
696
978
  }) });
697
979
  };
698
- return /* @__PURE__ */ jsx11(StepShell, { title: "Select an existing Extension to scaffold:", onBack, backFocused, children: renderContent() });
980
+ return /* @__PURE__ */ jsx12(
981
+ StepShell,
982
+ {
983
+ title: command === "update" /* UPDATE */ ? "Select an Extension to update:" : "Select an existing Extension to scaffold:",
984
+ onBack,
985
+ children: renderContent()
986
+ }
987
+ );
699
988
  };
700
989
 
701
990
  // src/constants.ts
@@ -803,6 +1092,29 @@ var generateManifest = async (rootDir, extensionName, targets, permissions) => {
803
1092
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
804
1093
  `);
805
1094
  };
1095
+ var buildFooterSurface = (targets) => {
1096
+ const blocks = [];
1097
+ if (targets.includes("slot.footer")) {
1098
+ blocks.push(
1099
+ ' <Surface id="slot.footer">\n <ui.Text className="text-xs">Powered by My Extension</ui.Text>\n </Surface>'
1100
+ );
1101
+ }
1102
+ if (targets.includes("slot.footer-links")) {
1103
+ blocks.push(
1104
+ ' <Surface id="slot.footer-links">\n <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>\n </Surface>'
1105
+ );
1106
+ }
1107
+ return `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
1108
+
1109
+ export function Footer() {
1110
+ return (
1111
+ <>
1112
+ ${blocks.join("\n")}
1113
+ </>
1114
+ )
1115
+ }
1116
+ `;
1117
+ };
806
1118
  var generateSurfaceFiles = async (rootDir, targets) => {
807
1119
  const surfaceDir = join(rootDir, "packages/extension/src/surfaces");
808
1120
  const wantsHeader = targets.includes("slot.header");
@@ -862,29 +1174,6 @@ export function Content() {
862
1174
  );
863
1175
  await upsertOrRemove(join(surfaceDir, "Footer.tsx"), wantsFooter, buildFooterSurface(targets));
864
1176
  };
865
- var buildFooterSurface = (targets) => {
866
- const blocks = [];
867
- if (targets.includes("slot.footer")) {
868
- blocks.push(
869
- ' <Surface id="slot.footer">\n <ui.Text className="text-xs">Powered by My Extension</ui.Text>\n </Surface>'
870
- );
871
- }
872
- if (targets.includes("slot.footer-links")) {
873
- blocks.push(
874
- ' <Surface id="slot.footer-links">\n <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>\n </Surface>'
875
- );
876
- }
877
- return `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
878
-
879
- export function Footer() {
880
- return (
881
- <>
882
- ${blocks.join("\n")}
883
- </>
884
- )
885
- }
886
- `;
887
- };
888
1177
  var rewriteExtensionIndex = async (rootDir, extensionId, targets) => {
889
1178
  const indexPath = join(rootDir, "packages/extension/src/index.tsx");
890
1179
  const imports = ["import { createExtension } from '@stackable-labs/sdk-extension-react'"];
@@ -1009,6 +1298,20 @@ VITE_PREVIEW_PORT=${previewPort}
1009
1298
  `;
1010
1299
  await writeFile(envPath, content);
1011
1300
  };
1301
+ var updateGitignore = async (dir) => {
1302
+ const gitignorePath = join(dir, ".gitignore");
1303
+ let gitignore = "";
1304
+ try {
1305
+ gitignore = await readFile(gitignorePath, "utf8");
1306
+ } catch {
1307
+ }
1308
+ if (!gitignore.includes(".env.stackable")) {
1309
+ const newGitignore = gitignore ? `${gitignore}
1310
+ .env.stackable
1311
+ ` : ".env.stackable\n";
1312
+ await writeFile(gitignorePath, newGitignore);
1313
+ }
1314
+ };
1012
1315
  var scaffold = async (options) => {
1013
1316
  const { dir } = await downloadTemplate(TEMPLATE_SOURCE, {
1014
1317
  dir: options.outputDir,
@@ -1028,14 +1331,18 @@ var scaffold = async (options) => {
1028
1331
  await rewritePreviewApp(dir, selectedTargets, derivedPermissions);
1029
1332
  await rewriteTurboJson(dir);
1030
1333
  await writeEnvFile(dir, options.extensionPort, options.previewPort);
1334
+ await updateGitignore(dir);
1031
1335
  return options;
1032
1336
  };
1033
1337
 
1034
1338
  // src/App.tsx
1035
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1339
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1036
1340
  var STEPS = {
1037
1341
  ["create" /* CREATE */]: ["app", "name", "targets", "settings", "confirm"],
1038
- ["scaffold" /* SCAFFOLD */]: ["app", "extensionSelect", "confirmTargets", "settings", "confirm"]
1342
+ ["scaffold" /* SCAFFOLD */]: ["app", "extensionSelect", "confirmTargets", "settings", "confirm"],
1343
+ ["update" /* UPDATE */]: ["app", "extensionSelect", "updateSettings", "confirm"],
1344
+ ["dev" /* DEV */]: []
1345
+ // Dev command has no wizard steps
1039
1346
  };
1040
1347
  var PROGRESS_STEPS = {
1041
1348
  ["create" /* CREATE */]: [
@@ -1048,7 +1355,12 @@ var PROGRESS_STEPS = {
1048
1355
  { label: "Fetching template", status: "pending" },
1049
1356
  { label: "Generating files", status: "pending" },
1050
1357
  { label: "Installing dependencies", status: "pending" }
1051
- ]
1358
+ ],
1359
+ ["update" /* UPDATE */]: [
1360
+ { label: "Updating extension", status: "pending" }
1361
+ ],
1362
+ ["dev" /* DEV */]: []
1363
+ // Dev command has no progress steps
1052
1364
  };
1053
1365
  var toKebabCase2 = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1054
1366
  var derivePermissions2 = (targets) => {
@@ -1068,12 +1380,22 @@ var derivePermissions2 = (targets) => {
1068
1380
  }
1069
1381
  return [...permissions];
1070
1382
  };
1071
- var App = ({ command, initialName, options }) => {
1383
+ var App = ({ command, initialName, initialExtensionId, options }) => {
1072
1384
  const { exit } = useApp();
1073
1385
  const [step, setStep] = useState8("app");
1074
- const [name, setName] = useState8(initialName ?? "");
1075
- const [extensionId, setExtensionId] = useState8("");
1386
+ const [name, setName] = useState8(initialName ?? options?.name ?? "");
1387
+ const [extensionId, setExtensionId] = useState8(initialExtensionId ?? "");
1076
1388
  const [extensionVersion, setExtensionVersion] = useState8("");
1389
+ const [bundleUrl, setBundleUrl] = useState8(options?.bundleUrl ?? "");
1390
+ const [enabled, setEnabled] = useState8(
1391
+ options?.enabled !== void 0 ? options.enabled !== "false" : true
1392
+ );
1393
+ const [versionOverride, setVersionOverride] = useState8(void 0);
1394
+ const [forceMajor, setForceMajor] = useState8(false);
1395
+ const [initialExtension, setInitialExtension] = useState8(null);
1396
+ const [targets, setTargets] = useState8(
1397
+ options?.targets ? options.targets.split(",").map((t) => t.trim()) : []
1398
+ );
1077
1399
  const [selectedApp, setSelectedApp] = useState8(null);
1078
1400
  const [extensionPort, setExtensionPort] = useState8(
1079
1401
  options?.extensionPort ? parseInt(options.extensionPort, 10) : 5173
@@ -1081,7 +1403,6 @@ var App = ({ command, initialName, options }) => {
1081
1403
  const [previewPort, setPreviewPort] = useState8(
1082
1404
  options?.previewPort ? parseInt(options.previewPort, 10) : 5174
1083
1405
  );
1084
- const [targets, setTargets] = useState8([]);
1085
1406
  const [outputDir, setOutputDir] = useState8("");
1086
1407
  const [progressSteps, setProgressSteps] = useState8(PROGRESS_STEPS[command]);
1087
1408
  const [errorMessage, setErrorMessage] = useState8();
@@ -1093,8 +1414,19 @@ var App = ({ command, initialName, options }) => {
1093
1414
  const skipped = /* @__PURE__ */ new Set();
1094
1415
  if (command === "create" /* CREATE */ && initialName) skipped.add("name");
1095
1416
  if (options?.extensionPort || options?.previewPort) skipped.add("settings");
1417
+ if (command === "update" /* UPDATE */) {
1418
+ if (options?.appId) skipped.add("app");
1419
+ if (initialExtensionId) skipped.add("extensionSelect");
1420
+ }
1096
1421
  return base.filter((s) => !skipped.has(s));
1097
- }, [command, initialName, options?.extensionPort, options?.previewPort]);
1422
+ }, [
1423
+ command,
1424
+ initialName,
1425
+ initialExtensionId,
1426
+ options?.extensionPort,
1427
+ options?.previewPort,
1428
+ options?.appId
1429
+ ]);
1098
1430
  const goBack = useCallback(() => {
1099
1431
  setStep((prev) => {
1100
1432
  const steps = activeSteps();
@@ -1104,7 +1436,7 @@ var App = ({ command, initialName, options }) => {
1104
1436
  }, [activeSteps]);
1105
1437
  const handleAppSelect = (app) => {
1106
1438
  setSelectedApp(app);
1107
- if (command === "scaffold" /* SCAFFOLD */) {
1439
+ if (command === "scaffold" /* SCAFFOLD */ || command === "update" /* UPDATE */) {
1108
1440
  setStep("extensionSelect");
1109
1441
  return;
1110
1442
  }
@@ -1115,7 +1447,18 @@ var App = ({ command, initialName, options }) => {
1115
1447
  setExtensionId(ext.id);
1116
1448
  setExtensionVersion(ext.manifest.version);
1117
1449
  setTargets(ext.manifest.targets);
1118
- setStep("confirmTargets");
1450
+ setBundleUrl(ext.bundleUrl);
1451
+ setEnabled(ext.enabled ?? true);
1452
+ setInitialExtension({
1453
+ name: ext.manifest.name,
1454
+ targets: ext.manifest.targets,
1455
+ bundleUrl: ext.bundleUrl
1456
+ });
1457
+ if (command === "scaffold" /* SCAFFOLD */) {
1458
+ setStep("confirmTargets");
1459
+ } else if (command === "update" /* UPDATE */) {
1460
+ setStep("updateSettings");
1461
+ }
1119
1462
  };
1120
1463
  const handleConfirmTargets = (value) => {
1121
1464
  setTargets(value);
@@ -1146,7 +1489,45 @@ var App = ({ command, initialName, options }) => {
1146
1489
  setOutputDir(dir);
1147
1490
  setStep("confirm");
1148
1491
  };
1492
+ const computeVersionBump = (currentVersion, newName, newTargets, newBundleUrl, isForceMajor) => {
1493
+ const [major, minor, patch] = currentVersion.split(".").map(Number);
1494
+ if (!initialExtension) return `${major}.${minor}.${patch + 1}`;
1495
+ if (isForceMajor) return `${major + 1}.0.0`;
1496
+ const bundleUrlChanged = newBundleUrl !== initialExtension.bundleUrl;
1497
+ if (bundleUrlChanged) return `${major + 1}.0.0`;
1498
+ const targetsChanged = newTargets.length !== initialExtension.targets.length || newTargets.some((t) => !initialExtension.targets.includes(t));
1499
+ if (targetsChanged) return `${major}.${minor + 1}.0`;
1500
+ const nameChanged = newName !== initialExtension.name;
1501
+ if (nameChanged) return `${major}.${minor}.${patch + 1}`;
1502
+ return currentVersion;
1503
+ };
1149
1504
  const handleConfirm = async () => {
1505
+ if (command === "update" /* UPDATE */) {
1506
+ setStep("updating");
1507
+ setProgressSteps(PROGRESS_STEPS[command]);
1508
+ try {
1509
+ updateStep(0, "running");
1510
+ const resolvedVersion = options?.setVersion ?? versionOverride ?? computeVersionBump(extensionVersion, name, targets, bundleUrl, forceMajor);
1511
+ await updateExtension(selectedApp.id, extensionId, {
1512
+ manifest: {
1513
+ name,
1514
+ version: resolvedVersion,
1515
+ targets,
1516
+ permissions: derivePermissions2(targets)
1517
+ },
1518
+ bundleUrl: bundleUrl || void 0,
1519
+ enabled
1520
+ });
1521
+ updateStep(0, "done");
1522
+ setExtensionVersion(resolvedVersion);
1523
+ setStep("updateDone");
1524
+ } catch (err) {
1525
+ const message = err instanceof Error ? err.message : String(err);
1526
+ setErrorMessage(message);
1527
+ setStep("error");
1528
+ }
1529
+ return;
1530
+ }
1150
1531
  setStep("scaffolding");
1151
1532
  setProgressSteps(PROGRESS_STEPS[command]);
1152
1533
  try {
@@ -1201,20 +1582,21 @@ var App = ({ command, initialName, options }) => {
1201
1582
  };
1202
1583
  switch (step) {
1203
1584
  case "app": {
1204
- return /* @__PURE__ */ jsx12(AppSelect, { onSubmit: handleAppSelect });
1585
+ return /* @__PURE__ */ jsx13(AppSelect, { onSubmit: handleAppSelect });
1205
1586
  }
1206
1587
  case "extensionSelect": {
1207
- return /* @__PURE__ */ jsx12(
1588
+ return /* @__PURE__ */ jsx13(
1208
1589
  ExtensionSelect,
1209
1590
  {
1210
1591
  appId: selectedApp.id,
1592
+ command,
1211
1593
  onSubmit: handleExtensionSelect,
1212
1594
  onBack: goBack
1213
1595
  }
1214
1596
  );
1215
1597
  }
1216
1598
  case "confirmTargets": {
1217
- return /* @__PURE__ */ jsx12(
1599
+ return /* @__PURE__ */ jsx13(
1218
1600
  TargetSelect,
1219
1601
  {
1220
1602
  availableTargets: selectedApp?.targets ?? [],
@@ -1225,7 +1607,7 @@ var App = ({ command, initialName, options }) => {
1225
1607
  );
1226
1608
  }
1227
1609
  case "name": {
1228
- return /* @__PURE__ */ jsx12(
1610
+ return /* @__PURE__ */ jsx13(
1229
1611
  NamePrompt,
1230
1612
  {
1231
1613
  initialValue: name,
@@ -1235,7 +1617,7 @@ var App = ({ command, initialName, options }) => {
1235
1617
  );
1236
1618
  }
1237
1619
  case "targets": {
1238
- return /* @__PURE__ */ jsx12(
1620
+ return /* @__PURE__ */ jsx13(
1239
1621
  TargetSelect,
1240
1622
  {
1241
1623
  availableTargets: selectedApp?.targets ?? [],
@@ -1245,7 +1627,7 @@ var App = ({ command, initialName, options }) => {
1245
1627
  );
1246
1628
  }
1247
1629
  case "settings": {
1248
- return /* @__PURE__ */ jsx12(
1630
+ return /* @__PURE__ */ jsx13(
1249
1631
  SettingsPrompt,
1250
1632
  {
1251
1633
  defaultDir: join2(process.cwd(), toKebabCase2(extensionId || name)),
@@ -1255,15 +1637,20 @@ var App = ({ command, initialName, options }) => {
1255
1637
  );
1256
1638
  }
1257
1639
  case "confirm": {
1258
- return /* @__PURE__ */ jsx12(
1640
+ return /* @__PURE__ */ jsx13(
1259
1641
  Confirm,
1260
1642
  {
1643
+ command,
1261
1644
  name,
1262
1645
  extensionPort,
1263
1646
  previewPort,
1264
1647
  targets,
1265
1648
  outputDir,
1266
- extensionVersion: command === "scaffold" /* SCAFFOLD */ ? extensionVersion : void 0,
1649
+ extensionVersion: command !== "create" /* CREATE */ ? extensionVersion : void 0,
1650
+ bundleUrl: command === "update" /* UPDATE */ ? bundleUrl : void 0,
1651
+ enabled: command === "update" /* UPDATE */ ? enabled : void 0,
1652
+ newVersion: command === "update" /* UPDATE */ ? options?.setVersion ?? versionOverride ?? computeVersionBump(extensionVersion, name, targets, bundleUrl, forceMajor) : void 0,
1653
+ onVersionOverride: command === "update" /* UPDATE */ ? setVersionOverride : void 0,
1267
1654
  onConfirm: handleConfirm,
1268
1655
  onCancel: handleCancel,
1269
1656
  onBack: goBack
@@ -1271,29 +1658,391 @@ var App = ({ command, initialName, options }) => {
1271
1658
  );
1272
1659
  }
1273
1660
  case "scaffolding": {
1274
- return /* @__PURE__ */ jsx12(ScaffoldProgress, { steps: progressSteps });
1661
+ return /* @__PURE__ */ jsx13(ScaffoldProgress, { steps: progressSteps });
1662
+ }
1663
+ case "updateSettings": {
1664
+ return /* @__PURE__ */ jsx13(
1665
+ UpdateSettingsPrompt,
1666
+ {
1667
+ name,
1668
+ targets,
1669
+ availableTargets: selectedApp?.targets ?? [],
1670
+ bundleUrl,
1671
+ enabled,
1672
+ onSubmit: (updated) => {
1673
+ setName(updated.name);
1674
+ setTargets(updated.targets);
1675
+ setBundleUrl(updated.bundleUrl);
1676
+ setEnabled(updated.enabled);
1677
+ setForceMajor(updated.forceMajor);
1678
+ setVersionOverride(void 0);
1679
+ setStep("confirm");
1680
+ },
1681
+ onBack: goBack
1682
+ }
1683
+ );
1684
+ }
1685
+ case "updating": {
1686
+ return /* @__PURE__ */ jsx13(ScaffoldProgress, { steps: progressSteps });
1687
+ }
1688
+ case "updateDone": {
1689
+ return /* @__PURE__ */ jsx13(StepShell, { title: "Extension updated", onBack: goBack, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 1, children: [
1690
+ /* @__PURE__ */ jsx13(Text13, { color: "green", bold: true, children: "Extension updated successfully!" }),
1691
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
1692
+ "Name: ",
1693
+ name
1694
+ ] }),
1695
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
1696
+ "ID: ",
1697
+ extensionId
1698
+ ] }),
1699
+ /* @__PURE__ */ jsxs13(Text13, { dimColor: true, children: [
1700
+ "Version: ",
1701
+ extensionVersion
1702
+ ] })
1703
+ ] }) });
1275
1704
  }
1276
1705
  case "done": {
1277
- return /* @__PURE__ */ jsx12(Done, { name, outputDir });
1706
+ return /* @__PURE__ */ jsx13(Done, { name, outputDir });
1707
+ }
1708
+ case "error": {
1709
+ return /* @__PURE__ */ jsx13(StepShell, { title: "Operation failed", onBack: goBack, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 1, children: [
1710
+ /* @__PURE__ */ jsx13(Text13, { color: "red", bold: true, children: "An error occurred" }),
1711
+ errorMessage && /* @__PURE__ */ jsx13(Text13, { color: "red", children: errorMessage })
1712
+ ] }) });
1278
1713
  }
1279
1714
  default: {
1280
- return /* @__PURE__ */ jsx12(StepShell, { title: "Scaffold failed", onBack: goBack, backFocused: false, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", gap: 1, children: [
1281
- /* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "An error occurred" }),
1282
- errorMessage && /* @__PURE__ */ jsx12(Text12, { color: "red", children: errorMessage })
1715
+ return /* @__PURE__ */ jsx13(StepShell, { title: "Scaffold failed", onBack: goBack, children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 1, children: [
1716
+ /* @__PURE__ */ jsx13(Text13, { color: "red", bold: true, children: "An error occurred" }),
1717
+ errorMessage && /* @__PURE__ */ jsx13(Text13, { color: "red", children: errorMessage })
1283
1718
  ] }) });
1284
1719
  }
1285
1720
  }
1286
1721
  };
1287
1722
 
1723
+ // src/components/DevApp.tsx
1724
+ import { Box as Box16, Text as Text16 } from "ink";
1725
+ import { useState as useState10, useEffect as useEffect6, useCallback as useCallback2 } from "react";
1726
+
1727
+ // src/lib/devContext.ts
1728
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1729
+ import { join as join3 } from "path";
1730
+ var parseEnvFile = (content) => {
1731
+ const lines = content.split("\n");
1732
+ const env = {};
1733
+ for (const line of lines) {
1734
+ const trimmed = line.trim();
1735
+ if (trimmed && !trimmed.startsWith("#")) {
1736
+ const [key, ...rest] = trimmed.split("=");
1737
+ if (key && rest.length > 0) {
1738
+ env[key.trim()] = rest.join("=").trim();
1739
+ }
1740
+ }
1741
+ }
1742
+ return env;
1743
+ };
1744
+ var readEnvFile = async (filePath) => {
1745
+ try {
1746
+ const content = await readFile2(filePath, "utf8");
1747
+ return parseEnvFile(content);
1748
+ } catch {
1749
+ return {};
1750
+ }
1751
+ };
1752
+ var writeEnvFile2 = async (filePath, env) => {
1753
+ const lines = Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n");
1754
+ await writeFile2(filePath, `${lines}
1755
+ `);
1756
+ };
1757
+ var readDevContext = async (projectRoot) => {
1758
+ const stackableEnv = await readEnvFile(join3(projectRoot, ".env.stackable"));
1759
+ const env = await readEnvFile(join3(projectRoot, ".env"));
1760
+ let extensionName = "Unknown Extension";
1761
+ try {
1762
+ const manifestPath = join3(projectRoot, "packages/extension/public/manifest.json");
1763
+ const manifestContent = await readFile2(manifestPath, "utf8");
1764
+ const manifest = JSON.parse(manifestContent);
1765
+ extensionName = manifest.name;
1766
+ } catch {
1767
+ }
1768
+ const extensionPort = parseInt(env.VITE_EXTENSION_PORT || stackableEnv.EXTENSION_PORT || "5173", 10);
1769
+ const previewPort = parseInt(env.VITE_PREVIEW_PORT || stackableEnv.PREVIEW_PORT || "5174", 10);
1770
+ return {
1771
+ projectRoot,
1772
+ extensionName,
1773
+ appId: stackableEnv.APP_ID || null,
1774
+ extensionId: stackableEnv.EXTENSION_ID || null,
1775
+ extensionPort,
1776
+ previewPort
1777
+ };
1778
+ };
1779
+ var writeDevContext = async (projectRoot, ctx) => {
1780
+ const env = {
1781
+ APP_ID: ctx.appId || "",
1782
+ EXTENSION_ID: ctx.extensionId || "",
1783
+ EXTENSION_PORT: ctx.extensionPort.toString(),
1784
+ PREVIEW_PORT: ctx.previewPort.toString()
1785
+ };
1786
+ await writeEnvFile2(join3(projectRoot, ".env.stackable"), env);
1787
+ };
1788
+
1789
+ // src/lib/tunnel.ts
1790
+ import { tunnel } from "cloudflared";
1791
+ var startTunnel = async (port) => {
1792
+ const { url, stop } = tunnel({ "--url": `http://localhost:${port}` });
1793
+ const resolvedUrl = await url;
1794
+ return { url: resolvedUrl, stop };
1795
+ };
1796
+
1797
+ // src/lib/devServer.ts
1798
+ import { spawn } from "child_process";
1799
+ var startDevServer = (projectRoot) => {
1800
+ const child = spawn("pnpm", ["dev"], {
1801
+ cwd: projectRoot,
1802
+ stdio: "pipe"
1803
+ });
1804
+ const stop = () => {
1805
+ child.kill("SIGTERM");
1806
+ };
1807
+ return { process: child, stop };
1808
+ };
1809
+
1810
+ // src/components/DevSetup.tsx
1811
+ import { Box as Box14, Text as Text14 } from "ink";
1812
+ import { useState as useState9, useEffect as useEffect4 } from "react";
1813
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1814
+ var DevSetup = ({ initialContext, onReady }) => {
1815
+ const [step, setStep] = useState9("app");
1816
+ const [selectedApp, setSelectedApp] = useState9(null);
1817
+ useEffect4(() => {
1818
+ if (initialContext.appId) {
1819
+ setStep("extension");
1820
+ }
1821
+ }, [initialContext.appId]);
1822
+ const handleAppSelect = (app) => {
1823
+ setSelectedApp(app);
1824
+ setStep("extension");
1825
+ };
1826
+ const handleExtensionSelect = (extension) => {
1827
+ onReady({
1828
+ appId: selectedApp?.id || initialContext.appId,
1829
+ extensionId: extension.id,
1830
+ appName: selectedApp?.name
1831
+ });
1832
+ };
1833
+ if (step === "app") {
1834
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
1835
+ /* @__PURE__ */ jsx14(Box14, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { children: "Select the App for your extension:" }) }),
1836
+ /* @__PURE__ */ jsx14(AppSelect, { onSubmit: handleAppSelect })
1837
+ ] });
1838
+ }
1839
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
1840
+ /* @__PURE__ */ jsx14(Box14, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { children: "Select the Extension to develop:" }) }),
1841
+ /* @__PURE__ */ jsx14(
1842
+ ExtensionSelect,
1843
+ {
1844
+ appId: selectedApp?.id || initialContext.appId,
1845
+ onSubmit: handleExtensionSelect
1846
+ }
1847
+ )
1848
+ ] });
1849
+ };
1850
+
1851
+ // src/components/DevDashboard.tsx
1852
+ import { Box as Box15, Text as Text15, useInput as useInput8 } from "ink";
1853
+ import { useEffect as useEffect5 } from "react";
1854
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1855
+ var DevDashboard = ({
1856
+ extensionName,
1857
+ extensionId,
1858
+ appId,
1859
+ appName,
1860
+ extensionPort,
1861
+ previewPort,
1862
+ tunnelUrl,
1863
+ bundleUrlUpdated,
1864
+ onQuit
1865
+ }) => {
1866
+ useEffect5(() => {
1867
+ const handler = () => {
1868
+ onQuit();
1869
+ };
1870
+ process.on("SIGINT", handler);
1871
+ return () => {
1872
+ process.off("SIGINT", handler);
1873
+ };
1874
+ }, [onQuit]);
1875
+ useInput8((input) => {
1876
+ if (input === "q") {
1877
+ onQuit();
1878
+ }
1879
+ });
1880
+ return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", padding: 1, children: [
1881
+ /* @__PURE__ */ jsx15(Box15, { marginBottom: 1, children: /* @__PURE__ */ jsx15(Text15, { bold: true, color: "blue", children: "\u2500\u2500 stackable dev \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }),
1882
+ /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", gap: 1, marginBottom: 1, children: [
1883
+ /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1884
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Extension:" }),
1885
+ /* @__PURE__ */ jsxs15(Text15, { children: [
1886
+ extensionName,
1887
+ " (",
1888
+ extensionId,
1889
+ ")"
1890
+ ] })
1891
+ ] }),
1892
+ /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1893
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "App:" }),
1894
+ /* @__PURE__ */ jsx15(Text15, { children: appName || appId })
1895
+ ] })
1896
+ ] }),
1897
+ /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", gap: 1, marginBottom: 1, children: [
1898
+ /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1899
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Local:" }),
1900
+ /* @__PURE__ */ jsxs15(Text15, { children: [
1901
+ "http://localhost:",
1902
+ extensionPort
1903
+ ] })
1904
+ ] }),
1905
+ /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1906
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Tunnel:" }),
1907
+ /* @__PURE__ */ jsx15(Text15, { children: tunnelUrl || "Connecting\u2026" })
1908
+ ] }),
1909
+ /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1910
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Preview:" }),
1911
+ /* @__PURE__ */ jsxs15(Text15, { children: [
1912
+ "http://localhost:",
1913
+ previewPort
1914
+ ] })
1915
+ ] })
1916
+ ] }),
1917
+ bundleUrlUpdated && /* @__PURE__ */ jsx15(Box15, { marginBottom: 1, children: /* @__PURE__ */ jsx15(Text15, { color: "green", children: "bundleUrl updated \u2713" }) }),
1918
+ /* @__PURE__ */ jsx15(Box15, { children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Press Ctrl-C to quit" }) })
1919
+ ] });
1920
+ };
1921
+
1922
+ // src/components/DevApp.tsx
1923
+ import { jsx as jsx16 } from "react/jsx-runtime";
1924
+ var DevApp = ({ options = {} }) => {
1925
+ const [state, setState] = useState10("setup");
1926
+ const [devContext, setDevContext] = useState10(null);
1927
+ const [resolvedContext, setResolvedContext] = useState10(null);
1928
+ const [tunnelUrl, setTunnelUrl] = useState10(null);
1929
+ const [bundleUrlUpdated, setBundleUrlUpdated] = useState10(false);
1930
+ const [tunnelHandle, setTunnelHandle] = useState10(null);
1931
+ const [devServerHandle, setDevServerHandle] = useState10(null);
1932
+ const [originalBundleUrl, setOriginalBundleUrl] = useState10("");
1933
+ const useTunnel = options.tunnel !== false;
1934
+ const useUpdate = options.update !== false;
1935
+ const useRestore = options.restore !== false;
1936
+ useEffect6(() => {
1937
+ const projectRoot = options.dir || process.cwd();
1938
+ readDevContext(projectRoot).then((ctx) => {
1939
+ setDevContext(ctx);
1940
+ });
1941
+ }, [options.dir]);
1942
+ const handleSetupReady = useCallback2(async (resolved) => {
1943
+ if (!devContext) return;
1944
+ setResolvedContext(resolved);
1945
+ await writeDevContext(devContext.projectRoot, {
1946
+ ...devContext,
1947
+ appId: resolved.appId,
1948
+ extensionId: resolved.extensionId
1949
+ });
1950
+ const extensionPort = options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort;
1951
+ const serverHandle = startDevServer(devContext.projectRoot);
1952
+ setDevServerHandle(serverHandle);
1953
+ if (useTunnel) {
1954
+ try {
1955
+ const tunnelResult = await startTunnel(extensionPort);
1956
+ setTunnelHandle(tunnelResult);
1957
+ setTunnelUrl(tunnelResult.url);
1958
+ if (useUpdate) {
1959
+ const originalUrl = `http://localhost:${extensionPort}`;
1960
+ setOriginalBundleUrl(originalUrl);
1961
+ await updateExtension(resolved.appId, resolved.extensionId, {
1962
+ bundleUrl: tunnelResult.url
1963
+ });
1964
+ setBundleUrlUpdated(true);
1965
+ }
1966
+ } catch (err) {
1967
+ console.error("Failed to start tunnel:", err);
1968
+ }
1969
+ }
1970
+ setState("running");
1971
+ }, [devContext, options.extensionPort, useTunnel, useUpdate]);
1972
+ useEffect6(() => {
1973
+ if (state === "setup" && devContext && devContext.appId && devContext.extensionId) {
1974
+ handleSetupReady({
1975
+ appId: devContext.appId,
1976
+ extensionId: devContext.extensionId
1977
+ });
1978
+ }
1979
+ }, [state, devContext, handleSetupReady]);
1980
+ const handleQuit = async () => {
1981
+ if (!devContext || !resolvedContext) return;
1982
+ setState("stopping");
1983
+ try {
1984
+ if (useRestore && originalBundleUrl) {
1985
+ await updateExtension(resolvedContext.appId, resolvedContext.extensionId, {
1986
+ bundleUrl: originalBundleUrl
1987
+ });
1988
+ }
1989
+ if (tunnelHandle) {
1990
+ tunnelHandle.stop();
1991
+ }
1992
+ if (devServerHandle) {
1993
+ devServerHandle.stop();
1994
+ }
1995
+ } catch (err) {
1996
+ console.error("Error during cleanup:", err);
1997
+ }
1998
+ process.exit(0);
1999
+ };
2000
+ if (state === "setup" && devContext) {
2001
+ if (!devContext.appId || !devContext.extensionId) {
2002
+ return /* @__PURE__ */ jsx16(
2003
+ DevSetup,
2004
+ {
2005
+ initialContext: devContext,
2006
+ onReady: handleSetupReady
2007
+ }
2008
+ );
2009
+ }
2010
+ return null;
2011
+ }
2012
+ if (state === "running" && devContext && resolvedContext) {
2013
+ return /* @__PURE__ */ jsx16(
2014
+ DevDashboard,
2015
+ {
2016
+ extensionName: devContext.extensionName,
2017
+ extensionId: resolvedContext.extensionId,
2018
+ appId: resolvedContext.appId,
2019
+ appName: resolvedContext.appName,
2020
+ extensionPort: options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort,
2021
+ previewPort: options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort,
2022
+ tunnelUrl,
2023
+ bundleUrlUpdated,
2024
+ onQuit: handleQuit
2025
+ }
2026
+ );
2027
+ }
2028
+ return /* @__PURE__ */ jsx16(Box16, { children: /* @__PURE__ */ jsx16(Text16, { children: "Loading..." }) });
2029
+ };
2030
+
1288
2031
  // src/index.tsx
1289
- import { jsx as jsx13 } from "react/jsx-runtime";
2032
+ import { jsx as jsx17 } from "react/jsx-runtime";
1290
2033
  var require2 = createRequire(import.meta.url);
1291
2034
  var { version } = require2("../package.json");
1292
2035
  program.name("stackable-app-extension").description("Stackable app extension developer CLI").version(version);
1293
2036
  program.command("create" /* CREATE */).description("Create a new Extension project").argument("[name]", "Extension project name").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview host dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((name, options) => {
1294
- render(/* @__PURE__ */ jsx13(App, { command: "create" /* CREATE */, initialName: name, options }));
2037
+ render(/* @__PURE__ */ jsx17(App, { command: "create" /* CREATE */, initialName: name, options }));
1295
2038
  });
1296
2039
  program.command("scaffold" /* SCAFFOLD */).description("Scaffold a local project from an existing Extension").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview host dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((options) => {
1297
- render(/* @__PURE__ */ jsx13(App, { command: "scaffold" /* SCAFFOLD */, options }));
2040
+ render(/* @__PURE__ */ jsx17(App, { command: "scaffold" /* SCAFFOLD */, options }));
2041
+ });
2042
+ program.command("update" /* UPDATE */).description("Update an existing Extension").argument("[extensionId]", "Extension ID to update").option("--app-id <id>", "Skip app selection").option("--name <name>", "New extension name").option("--targets <targets>", "Comma-separated target slots (validated against app)").option("--bundle-url <url>", "New bundle URL").option("--enabled <bool>", "Enable/disable extension").option("--set-version <version>", "Explicit version (skips auto-compute)").action((extensionId, options) => {
2043
+ render(/* @__PURE__ */ jsx17(App, { command: "update" /* UPDATE */, initialExtensionId: extensionId, options }));
2044
+ });
2045
+ program.command("dev" /* DEV */).description("Start dev servers with a public tunnel").option("--dir <path>", "Project root (default: cwd)").option("--extension-port <port>", "Override extension port").option("--preview-port <port>", "Override preview port").option("--no-tunnel", "Skip tunnel, just run vite dev").option("--no-update", "Skip bundleUrl update on server").option("--no-restore", "On exit, keep tunnel URL as bundleUrl").action((options) => {
2046
+ render(/* @__PURE__ */ jsx17(DevApp, { options }));
1298
2047
  });
1299
2048
  program.parse(process.argv.filter((arg) => arg !== "--"));