@stackable-labs/cli-app-extension 1.1.0 → 1.3.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 +535 -281
  2. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -43,8 +43,9 @@ var StepShell = ({ title, hint, children, footer, onBack, backFocused }) => {
43
43
 
44
44
  // src/components/Confirm.tsx
45
45
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
46
- var Confirm = ({ name, extensionPort, previewPort, targets, outputDir, onConfirm, onCancel, onBack }) => {
46
+ var Confirm = ({ name, extensionVersion, extensionPort, previewPort, targets, outputDir, onConfirm, onCancel, onBack }) => {
47
47
  const [backFocused, setBackFocused] = useState(false);
48
+ const [selected, setSelected] = useState("y");
48
49
  useInput((input, key) => {
49
50
  if (key.upArrow && onBack) {
50
51
  setBackFocused(true);
@@ -59,12 +60,21 @@ var Confirm = ({ name, extensionPort, previewPort, targets, outputDir, onConfirm
59
60
  return;
60
61
  }
61
62
  if (backFocused) return;
62
- if (input === "y" || key.return) {
63
- onConfirm();
63
+ if (key.leftArrow || key.rightArrow) {
64
+ setSelected((s) => s === "y" ? "n" : "y");
65
+ return;
66
+ }
67
+ if (input === "y") {
68
+ setSelected("y");
64
69
  return;
65
70
  }
66
71
  if (input === "n") {
67
- onCancel();
72
+ setSelected("n");
73
+ return;
74
+ }
75
+ if (key.return) {
76
+ if (selected === "y") onConfirm();
77
+ else onCancel();
68
78
  }
69
79
  });
70
80
  return /* @__PURE__ */ jsx2(
@@ -75,15 +85,20 @@ var Confirm = ({ name, extensionPort, previewPort, targets, outputDir, onConfirm
75
85
  onBack,
76
86
  backFocused,
77
87
  footer: /* @__PURE__ */ jsxs2(Text2, { children: [
78
- "Proceed? ",
79
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "green", children: "Y" }),
80
- "/",
81
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "red", children: "n" })
88
+ "Proceed?",
89
+ " ",
90
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "green", inverse: selected === "y", children: "Y" }),
91
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "/" }),
92
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "red", inverse: selected === "n", children: "n" })
82
93
  ] }),
83
94
  children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
84
95
  /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
85
96
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Name " }),
86
- /* @__PURE__ */ jsx2(Text2, { children: name })
97
+ /* @__PURE__ */ jsx2(Text2, { dimColor: !!extensionVersion, children: name })
98
+ ] }),
99
+ extensionVersion && /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
100
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Version " }),
101
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: extensionVersion })
87
102
  ] }),
88
103
  /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
89
104
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Directory " }),
@@ -109,15 +124,55 @@ var Confirm = ({ name, extensionPort, previewPort, targets, outputDir, onConfirm
109
124
  );
110
125
  };
111
126
 
112
- // src/components/DirPrompt.tsx
113
- import { Box as Box4, Text as Text4 } from "ink";
127
+ // src/components/Done.tsx
128
+ import { Box as Box3, Text as Text3 } from "ink";
129
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
130
+ var Done = ({ name, outputDir }) => /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
131
+ /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
132
+ /* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "\u2714" }),
133
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Extension scaffolded successfully!" })
134
+ ] }),
135
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
136
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
137
+ "Created: ",
138
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: name })
139
+ ] }),
140
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
141
+ "Location: ",
142
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: outputDir })
143
+ ] })
144
+ ] }),
145
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, children: [
146
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Next steps:" }),
147
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, gap: 1, children: [
148
+ /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
149
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "1." }),
150
+ /* @__PURE__ */ jsxs3(Text3, { children: [
151
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "cd " }),
152
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: outputDir })
153
+ ] })
154
+ ] }),
155
+ /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
156
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "2." }),
157
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "pnpm install" })
158
+ ] }),
159
+ /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
160
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "3." }),
161
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "pnpm dev" })
162
+ ] })
163
+ ] })
164
+ ] })
165
+ ] });
166
+
167
+ // src/components/NamePrompt.tsx
168
+ import { Box as Box5, Text as Text5 } from "ink";
114
169
  import TextInput from "ink-text-input";
115
170
  import { useState as useState3 } from "react";
116
171
 
117
172
  // src/components/BackableInput.tsx
118
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
173
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
119
174
  import { useState as useState2 } from "react";
120
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
175
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
121
176
  var BackableInput = ({ label, hint, onBack, children, error }) => {
122
177
  const [focus, setFocus] = useState2("input");
123
178
  useInput2((_input, key) => {
@@ -133,77 +188,17 @@ var BackableInput = ({ label, hint, onBack, children, error }) => {
133
188
  onBack?.();
134
189
  }
135
190
  });
136
- return /* @__PURE__ */ jsx3(StepShell, { title: label, hint, onBack, backFocused: focus === "back", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
191
+ return /* @__PURE__ */ jsx4(StepShell, { title: label, hint, onBack, backFocused: focus === "back", children: /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
137
192
  children(focus === "input"),
138
- error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: error })
139
- ] }) });
140
- };
141
-
142
- // src/components/DirPrompt.tsx
143
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
144
- var DirPrompt = ({ defaultDir, onSubmit, onBack }) => {
145
- const [value, setValue] = useState3(defaultDir);
146
- const handleSubmit = (val) => {
147
- const trimmed = val.trim();
148
- if (trimmed.length === 0) {
149
- return;
150
- }
151
- onSubmit(trimmed);
152
- };
153
- return /* @__PURE__ */ jsx4(BackableInput, { label: "Output directory:", hint: "Press Enter to confirm", onBack, children: (isFocused) => /* @__PURE__ */ jsxs4(Box4, { children: [
154
- /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u2192 " }),
155
- /* @__PURE__ */ jsx4(TextInput, { value, onChange: setValue, onSubmit: handleSubmit, focus: isFocused })
193
+ error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error })
156
194
  ] }) });
157
195
  };
158
196
 
159
- // src/components/Done.tsx
160
- import { Box as Box5, Text as Text5 } from "ink";
161
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
162
- var Done = ({ name, outputDir }) => /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
163
- /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
164
- /* @__PURE__ */ jsx5(Text5, { color: "green", bold: true, children: "\u2714" }),
165
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Extension scaffolded successfully!" })
166
- ] }),
167
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginLeft: 2, children: [
168
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
169
- "Created: ",
170
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: name })
171
- ] }),
172
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
173
- "Location: ",
174
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: outputDir })
175
- ] })
176
- ] }),
177
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, children: [
178
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Next steps:" }),
179
- /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, gap: 1, children: [
180
- /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
181
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "1." }),
182
- /* @__PURE__ */ jsxs5(Text5, { children: [
183
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "cd " }),
184
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: outputDir })
185
- ] })
186
- ] }),
187
- /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
188
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "2." }),
189
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "pnpm install" })
190
- ] }),
191
- /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
192
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "3." }),
193
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "pnpm dev" })
194
- ] })
195
- ] })
196
- ] })
197
- ] });
198
-
199
197
  // src/components/NamePrompt.tsx
200
- import { Box as Box6, Text as Text6 } from "ink";
201
- import TextInput2 from "ink-text-input";
202
- import { useState as useState4 } from "react";
203
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
198
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
204
199
  var NamePrompt = ({ initialValue = "", onSubmit, onBack }) => {
205
- const [value, setValue] = useState4(initialValue);
206
- const [error, setError] = useState4();
200
+ const [value, setValue] = useState3(initialValue);
201
+ const [error, setError] = useState3();
207
202
  const handleSubmit = (val) => {
208
203
  const trimmed = val.trim();
209
204
  if (trimmed.length === 0) {
@@ -213,88 +208,143 @@ var NamePrompt = ({ initialValue = "", onSubmit, onBack }) => {
213
208
  setError(void 0);
214
209
  onSubmit(trimmed);
215
210
  };
216
- return /* @__PURE__ */ jsx6(BackableInput, { label: "What is your Extension name?", hint: "Press Enter to confirm", onBack, error, children: (isFocused) => /* @__PURE__ */ jsxs6(Box6, { children: [
217
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "> " }),
218
- /* @__PURE__ */ jsx6(TextInput2, { value, onChange: setValue, onSubmit: handleSubmit, focus: isFocused })
211
+ return /* @__PURE__ */ jsx5(BackableInput, { label: "What is your Extension name?", hint: "Press Enter to confirm", onBack, error, children: (isFocused) => /* @__PURE__ */ jsxs5(Box5, { children: [
212
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "> " }),
213
+ /* @__PURE__ */ jsx5(TextInput, { value, onChange: setValue, onSubmit: handleSubmit, focus: isFocused })
219
214
  ] }) });
220
215
  };
221
216
 
222
- // src/components/PortsPrompt.tsx
223
- import { Box as Box7, Text as Text7 } from "ink";
224
- import TextInput3 from "ink-text-input";
225
- import { useState as useState5 } from "react";
226
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
217
+ // src/components/SettingsPrompt.tsx
218
+ import { Box as Box6, Text as Text6, useFocus, useFocusManager, useInput as useInput3 } from "ink";
219
+ import TextInput2 from "ink-text-input";
220
+ import { useState as useState4 } from "react";
221
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
227
222
  var DEFAULT_EXTENSION_PORT = 5173;
228
223
  var DEFAULT_PREVIEW_PORT = DEFAULT_EXTENSION_PORT + 1;
229
- var PortsPrompt = ({ onSubmit, onBack }) => {
230
- const [extensionPort, setExtensionPort] = useState5(String(DEFAULT_EXTENSION_PORT));
231
- const [previewPort, setPreviewPort] = useState5(String(DEFAULT_PREVIEW_PORT));
232
- const [step, setStep] = useState5("extension");
233
- const handleExtensionSubmit = (value) => {
234
- const trimmed = value.trim();
235
- const port = trimmed === "" ? DEFAULT_EXTENSION_PORT : parseInt(trimmed, 10);
236
- if (isNaN(port) || port < 1024 || port > 65535) {
237
- return;
224
+ var FieldRow = ({ label, value, onChange, onSubmit, onConfirm, placeholder, autoFocus, isFirst, isLast, onFocusBack }) => {
225
+ const { isFocused } = useFocus({ autoFocus });
226
+ const { focusNext, focusPrevious } = useFocusManager();
227
+ useInput3((_input, key) => {
228
+ if (!isFocused) return;
229
+ if (key.downArrow && !isLast) focusNext();
230
+ if (key.upArrow) {
231
+ if (isFirst && onFocusBack) {
232
+ onFocusBack();
233
+ } else if (!isFirst) {
234
+ focusPrevious();
235
+ }
238
236
  }
239
- setExtensionPort(String(port));
240
- setPreviewPort(String(port + 1));
241
- setStep("preview");
237
+ });
238
+ return /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
239
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: label }),
240
+ isFocused ? /* @__PURE__ */ jsxs6(Box6, { children: [
241
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "\u2192 " }),
242
+ /* @__PURE__ */ jsx6(
243
+ TextInput2,
244
+ {
245
+ value,
246
+ onChange,
247
+ onSubmit: (val) => {
248
+ const result = onSubmit(val);
249
+ if (result) onConfirm(...result);
250
+ },
251
+ placeholder,
252
+ focus: isFocused
253
+ }
254
+ )
255
+ ] }) : /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: value })
256
+ ] });
257
+ };
258
+ var SettingsPrompt = ({ defaultDir, onSubmit, onBack }) => {
259
+ const [backFocused, setBackFocused] = useState4(false);
260
+ const [extensionPort, setExtensionPort] = useState4(String(DEFAULT_EXTENSION_PORT));
261
+ const [previewPort, setPreviewPort] = useState4(String(DEFAULT_PREVIEW_PORT));
262
+ const [outputDir, setOutputDir] = useState4(defaultDir);
263
+ const handleFocusBack = () => {
264
+ if (onBack) setBackFocused(true);
242
265
  };
243
- const handlePreviewSubmit = (value) => {
244
- const extPort = parseInt(extensionPort, 10);
245
- const trimmed = value.trim();
246
- const prevPort = trimmed === "" ? extPort + 1 : parseInt(trimmed, 10);
247
- if (isNaN(prevPort) || prevPort < 1024 || prevPort > 65535) {
266
+ useInput3((_input, key) => {
267
+ if (key.downArrow && backFocused) {
268
+ setBackFocused(false);
248
269
  return;
249
270
  }
250
- onSubmit(extPort, prevPort);
251
- };
252
- const handleBack = () => {
253
- if (step === "preview") {
254
- setStep("extension");
255
- } else {
271
+ if (key.return && backFocused) {
256
272
  onBack?.();
273
+ return;
257
274
  }
275
+ });
276
+ const handleConfirm = (resolvedExtPort, resolvedPrevPort, resolvedDir) => {
277
+ const dir = resolvedDir.trim();
278
+ if (!dir) return;
279
+ onSubmit(parseInt(resolvedExtPort, 10), parseInt(resolvedPrevPort, 10), dir);
258
280
  };
259
- if (step === "extension") {
260
- return /* @__PURE__ */ jsx7(
261
- BackableInput,
262
- {
263
- label: "Extension dev Server port:",
264
- hint: "Press Enter to confirm",
265
- onBack: onBack ? handleBack : void 0,
266
- children: (isFocused) => /* @__PURE__ */ jsxs7(Box7, { children: [
267
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "\u2192 " }),
268
- /* @__PURE__ */ jsx7(
269
- TextInput3,
270
- {
271
- value: extensionPort,
272
- onChange: setExtensionPort,
273
- onSubmit: handleExtensionSubmit,
274
- placeholder: String(DEFAULT_EXTENSION_PORT),
275
- focus: isFocused
276
- }
277
- )
278
- ] })
279
- }
280
- );
281
- }
282
- return /* @__PURE__ */ jsx7(
283
- BackableInput,
281
+ const handleExtensionPortSubmit = (value) => {
282
+ const trimmed = value.trim();
283
+ const port = trimmed === "" ? DEFAULT_EXTENSION_PORT : parseInt(trimmed, 10);
284
+ if (isNaN(port) || port < 1024 || port > 65535) return false;
285
+ const newExtPort = String(port);
286
+ const newPrevPort = String(port + 1);
287
+ setExtensionPort(newExtPort);
288
+ setPreviewPort(newPrevPort);
289
+ return [newExtPort, newPrevPort, outputDir];
290
+ };
291
+ const handlePreviewPortSubmit = (value) => {
292
+ const extPort = parseInt(extensionPort, 10);
293
+ const trimmed = value.trim();
294
+ const port = trimmed === "" ? extPort + 1 : parseInt(trimmed, 10);
295
+ if (isNaN(port) || port < 1024 || port > 65535) return false;
296
+ const newPrevPort = String(port);
297
+ setPreviewPort(newPrevPort);
298
+ return [extensionPort, newPrevPort, outputDir];
299
+ };
300
+ const handleOutputDirSubmit = (value) => {
301
+ const trimmed = value.trim();
302
+ if (trimmed.length === 0) return false;
303
+ setOutputDir(trimmed);
304
+ return [extensionPort, previewPort, trimmed];
305
+ };
306
+ return /* @__PURE__ */ jsx6(
307
+ StepShell,
284
308
  {
285
- label: "Extension dev Preview port:",
286
- hint: "Press Enter to confirm",
287
- onBack: handleBack,
288
- children: (isFocused) => /* @__PURE__ */ jsxs7(Box7, { children: [
289
- /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "\u2192 " }),
290
- /* @__PURE__ */ jsx7(
291
- TextInput3,
309
+ title: "Project settings",
310
+ hint: "\u2191\u2193 or Tab to move between fields, Enter to confirm",
311
+ onBack,
312
+ backFocused,
313
+ children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
314
+ /* @__PURE__ */ jsx6(
315
+ FieldRow,
316
+ {
317
+ label: "Extension port ",
318
+ value: extensionPort,
319
+ onChange: setExtensionPort,
320
+ onSubmit: handleExtensionPortSubmit,
321
+ onConfirm: handleConfirm,
322
+ placeholder: String(DEFAULT_EXTENSION_PORT),
323
+ autoFocus: true,
324
+ isFirst: true,
325
+ onFocusBack: handleFocusBack
326
+ }
327
+ ),
328
+ /* @__PURE__ */ jsx6(
329
+ FieldRow,
292
330
  {
331
+ label: "Preview port ",
293
332
  value: previewPort,
294
333
  onChange: setPreviewPort,
295
- onSubmit: handlePreviewSubmit,
296
- placeholder: String(DEFAULT_EXTENSION_PORT + 1),
297
- focus: isFocused
334
+ onSubmit: handlePreviewPortSubmit,
335
+ onConfirm: handleConfirm,
336
+ placeholder: String(DEFAULT_PREVIEW_PORT)
337
+ }
338
+ ),
339
+ /* @__PURE__ */ jsx6(
340
+ FieldRow,
341
+ {
342
+ label: "Output directory",
343
+ value: outputDir,
344
+ onChange: setOutputDir,
345
+ onSubmit: handleOutputDirSubmit,
346
+ onConfirm: handleConfirm,
347
+ isLast: true
298
348
  }
299
349
  )
300
350
  ] })
@@ -303,47 +353,47 @@ var PortsPrompt = ({ onSubmit, onBack }) => {
303
353
  };
304
354
 
305
355
  // src/components/ScaffoldProgress.tsx
306
- import { Box as Box8, Text as Text8 } from "ink";
356
+ import { Box as Box7, Text as Text7 } from "ink";
307
357
  import Spinner from "ink-spinner";
308
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
358
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
309
359
  var stepIcon = (status) => {
310
360
  switch (status) {
311
361
  case "running":
312
- return /* @__PURE__ */ jsx8(Spinner, { type: "dots" });
362
+ return /* @__PURE__ */ jsx7(Spinner, { type: "dots" });
313
363
  case "done":
314
- return /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714" });
364
+ return /* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2714" });
315
365
  case "error":
316
- return /* @__PURE__ */ jsx8(Text8, { color: "red", children: "\u2716" });
366
+ return /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2716" });
317
367
  default:
318
- return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u25CB" });
368
+ return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u25CB" });
319
369
  }
320
370
  };
321
- var ScaffoldProgress = ({ steps }) => /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
322
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Scaffolding your extension\u2026" }),
323
- /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: steps.map((step) => /* @__PURE__ */ jsxs8(Box8, { gap: 2, children: [
371
+ var ScaffoldProgress = ({ steps }) => /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
372
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Scaffolding your Extension\u2026" }),
373
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: steps.map((step) => /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
324
374
  stepIcon(step.status),
325
- /* @__PURE__ */ jsx8(Text8, { dimColor: step.status === "pending", color: step.status === "running" ? "cyan" : void 0, children: step.label })
375
+ /* @__PURE__ */ jsx7(Text7, { dimColor: step.status === "pending", color: step.status === "running" ? "cyan" : void 0, children: step.label })
326
376
  ] }, step.label)) })
327
377
  ] });
328
378
 
329
379
  // src/components/TargetSelect.tsx
330
- import { Box as Box9, Text as Text9, useInput as useInput3 } from "ink";
331
- import { useState as useState6 } from "react";
332
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
380
+ import { Box as Box8, Text as Text8, useInput as useInput4 } from "ink";
381
+ import { useState as useState5 } from "react";
382
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
333
383
  var TARGET_DESCRIPTIONS = {
334
384
  "slot.header": "Renders content in the panel header area",
335
385
  "slot.content": "Renders the main panel body (includes store + navigation state)",
336
386
  "slot.footer": "Renders a footer bar at the bottom of the panel",
337
387
  "slot.footer-links": "Renders a link row in the global footer"
338
388
  };
339
- var TargetSelect = ({ availableTargets, onSubmit, onBack }) => {
340
- const [cursor, setCursor] = useState6(0);
341
- const [backFocused, setBackFocused] = useState6(false);
342
- const [selected, setSelected] = useState6(
343
- new Set(availableTargets.includes("slot.content") ? ["slot.content"] : [])
389
+ var TargetSelect = ({ availableTargets, preSelected, onSubmit, onBack }) => {
390
+ const [cursor, setCursor] = useState5(0);
391
+ const [backFocused, setBackFocused] = useState5(false);
392
+ const [selected, setSelected] = useState5(
393
+ new Set(preSelected ?? (availableTargets.includes("slot.content") ? ["slot.content"] : []))
344
394
  );
345
- const [error, setError] = useState6();
346
- useInput3((input, key) => {
395
+ const [error, setError] = useState5();
396
+ useInput4((input, key) => {
347
397
  if (key.upArrow) {
348
398
  if (cursor === 0 && onBack) {
349
399
  setBackFocused(true);
@@ -387,7 +437,7 @@ var TargetSelect = ({ availableTargets, onSubmit, onBack }) => {
387
437
  onSubmit([...selected]);
388
438
  }
389
439
  });
390
- return /* @__PURE__ */ jsxs9(
440
+ return /* @__PURE__ */ jsxs8(
391
441
  StepShell,
392
442
  {
393
443
  title: "Select Surface targets/slots",
@@ -395,44 +445,67 @@ var TargetSelect = ({ availableTargets, onSubmit, onBack }) => {
395
445
  onBack,
396
446
  backFocused,
397
447
  children: [
398
- /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", gap: 1, children: availableTargets.map((target, i) => {
448
+ /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", gap: 1, children: availableTargets.map((target, i) => {
399
449
  const isSelected = selected.has(target);
400
450
  const isCursor = i === cursor && !backFocused;
401
- return /* @__PURE__ */ jsxs9(Box9, { gap: 1, children: [
402
- /* @__PURE__ */ jsx9(Text9, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
403
- /* @__PURE__ */ jsx9(Text9, { color: isSelected ? "green" : void 0, children: isSelected ? "\u25C9" : "\u25CB" }),
404
- /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
405
- /* @__PURE__ */ jsx9(Text9, { bold: isSelected, children: target }),
406
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: TARGET_DESCRIPTIONS[target] ?? target })
451
+ return /* @__PURE__ */ jsxs8(Box8, { gap: 1, children: [
452
+ /* @__PURE__ */ jsx8(Text8, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
453
+ /* @__PURE__ */ jsx8(Text8, { color: isSelected ? "green" : void 0, children: isSelected ? "\u25C9" : "\u25CB" }),
454
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
455
+ /* @__PURE__ */ jsx8(Text8, { bold: isSelected, children: target }),
456
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: TARGET_DESCRIPTIONS[target] ?? target })
407
457
  ] })
408
458
  ] }, target);
409
459
  }) }),
410
- error && /* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
460
+ error && /* @__PURE__ */ jsx8(Text8, { color: "red", children: error })
411
461
  ]
412
462
  }
413
463
  );
414
464
  };
415
465
 
416
466
  // src/components/AppSelect.tsx
417
- import { Box as Box11, Text as Text11 } from "ink";
418
- import SelectInput from "ink-select-input";
467
+ import { Box as Box10, Text as Text10, useInput as useInput5 } from "ink";
419
468
  import Spinner2 from "ink-spinner";
420
- import { useEffect, useState as useState7 } from "react";
469
+ import { useEffect, useState as useState6 } from "react";
421
470
 
422
471
  // src/lib/api.ts
423
- var API_BASE_URL = "https://api.stackablelabs.io/app-extension/latest";
472
+ var DEFAULT_API_URL = "https://api.stackablelabs.io/app-extension/latest";
473
+ var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
474
+ var getApiBaseUrl = () => process.env.API_BASE_URL ?? DEFAULT_API_URL;
475
+ var getAdminApiBaseUrl = () => process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
424
476
  var fetchApps = async () => {
425
- const baseURL = `${process.env.API_BASE_URL ?? API_BASE_URL}/apps`;
477
+ const baseURL = `${getApiBaseUrl()}/apps`;
426
478
  const res = await fetch(baseURL);
427
479
  if (!res.ok) {
428
- throw new Error(`Failed to fetch apps${baseURL !== API_BASE_URL ? ` (using ${baseURL})` : ""}: ${res.status} ${res.statusText}`);
480
+ throw new Error(`Failed to fetch apps: ${res.status} ${res.statusText}`);
481
+ }
482
+ return res.json();
483
+ };
484
+ var createExtensionRemote = async (appId, payload) => {
485
+ const baseUrl = getAdminApiBaseUrl();
486
+ const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`, {
487
+ method: "POST",
488
+ headers: { "content-type": "application/json" },
489
+ body: JSON.stringify(payload)
490
+ });
491
+ if (!res.ok) {
492
+ const body = await res.text();
493
+ throw new Error(`Failed to create extension: ${res.status} ${body}`);
494
+ }
495
+ return res.json();
496
+ };
497
+ var fetchExtensions = async (appId) => {
498
+ const baseUrl = getApiBaseUrl();
499
+ const res = await fetch(`${baseUrl}/extensions/${appId}`);
500
+ if (!res.ok) {
501
+ throw new Error(`Failed to fetch extensions: ${res.status} ${res.statusText}`);
429
502
  }
430
503
  return res.json();
431
504
  };
432
505
 
433
506
  // src/components/Banner.tsx
434
- import { Box as Box10, Text as Text10 } from "ink";
435
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
507
+ import { Box as Box9, Text as Text9 } from "ink";
508
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
436
509
  var WORDMARK = [
437
510
  " _ _ _ _ ",
438
511
  " ___| |_ __ _ ___| | ____ _| |__ | | ___",
@@ -468,53 +541,155 @@ var gradientColor = (row, col, rows, cols) => {
468
541
  var Banner = () => {
469
542
  const termWidth = process.stdout.columns ?? 80;
470
543
  const maxLen = Math.max(...WORDMARK.map((l) => l.length));
471
- return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
472
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2500".repeat(termWidth) }),
473
- /* @__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)) })
544
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
545
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "\u2500".repeat(termWidth) }),
546
+ /* @__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)) })
474
547
  ] });
475
548
  };
476
549
 
477
550
  // src/components/AppSelect.tsx
478
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
551
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
479
552
  var AppSelect = ({ onSubmit }) => {
480
- const [apps, setApps] = useState7([]);
481
- const [loading, setLoading] = useState7(true);
482
- const [error, setError] = useState7();
553
+ const [apps, setApps] = useState6([]);
554
+ const [loading, setLoading] = useState6(true);
555
+ const [error, setError] = useState6();
556
+ const [cursor, setCursor] = useState6(0);
483
557
  useEffect(() => {
484
558
  fetchApps().then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
485
559
  }, []);
486
- if (loading) {
487
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
488
- /* @__PURE__ */ jsx11(Banner, {}),
489
- /* @__PURE__ */ jsxs11(Box11, { gap: 2, paddingX: 1, children: [
490
- /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
491
- /* @__PURE__ */ jsx11(Text11, { children: "Loading available apps\u2026" })
492
- ] })
493
- ] });
494
- }
495
- if (error) {
496
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", gap: 1, children: [
497
- /* @__PURE__ */ jsx11(Text11, { color: "red", bold: true, children: "Failed to load apps" }),
498
- /* @__PURE__ */ jsx11(Text11, { color: "red", children: error })
499
- ] });
500
- }
501
- if (apps.length === 0) {
502
- return /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "No apps available. Contact your administrator." });
503
- }
504
- const items = apps.map((app) => ({
505
- label: app.name,
506
- value: app.id
507
- }));
508
- const handleSelect = (item) => {
509
- const app = apps.find((a) => a.id === item.value);
510
- if (app) onSubmit(app);
560
+ useInput5((_, key) => {
561
+ if (loading || error || apps.length === 0) return;
562
+ if (key.upArrow) {
563
+ setCursor((c) => Math.max(0, c - 1));
564
+ } else if (key.downArrow) {
565
+ setCursor((c) => Math.min(apps.length - 1, c + 1));
566
+ } else if (key.return) {
567
+ onSubmit(apps[cursor]);
568
+ }
569
+ });
570
+ const renderContent = () => {
571
+ if (loading) {
572
+ return /* @__PURE__ */ jsxs10(Box10, { gap: 2, children: [
573
+ /* @__PURE__ */ jsx10(Spinner2, { type: "dots" }),
574
+ /* @__PURE__ */ jsx10(Text10, { children: "Loading available Apps\u2026" })
575
+ ] });
576
+ }
577
+ if (error) {
578
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", gap: 1, children: [
579
+ /* @__PURE__ */ jsx10(Text10, { color: "red", bold: true, children: "Failed to load Apps" }),
580
+ /* @__PURE__ */ jsx10(Text10, { color: "red", children: error })
581
+ ] });
582
+ }
583
+ if (apps.length === 0) {
584
+ return /* @__PURE__ */ jsx10(Text10, { color: "yellow", children: "No Apps available. Contact your administrator." });
585
+ }
586
+ return /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", children: apps.map((app, i) => {
587
+ const isCursor = i === cursor;
588
+ return /* @__PURE__ */ jsxs10(Box10, { gap: 1, children: [
589
+ /* @__PURE__ */ jsx10(Text10, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
590
+ /* @__PURE__ */ jsx10(Text10, { bold: isCursor, children: app.name }),
591
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
592
+ "(",
593
+ app.id,
594
+ ")"
595
+ ] })
596
+ ] }, app.id);
597
+ }) });
511
598
  };
512
- return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
513
- /* @__PURE__ */ jsx11(Banner, {}),
514
- /* @__PURE__ */ jsx11(StepShell, { title: "Select the App you are building an Extension for:", children: /* @__PURE__ */ jsx11(SelectInput, { items, onSelect: handleSelect }) })
599
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
600
+ /* @__PURE__ */ jsx10(Banner, {}),
601
+ /* @__PURE__ */ jsx10(StepShell, { title: "Select the App you are building an Extension for:", children: renderContent() })
515
602
  ] });
516
603
  };
517
604
 
605
+ // src/components/ExtensionSelect.tsx
606
+ import { Box as Box11, Text as Text11, useInput as useInput6 } from "ink";
607
+ import Spinner3 from "ink-spinner";
608
+ import { useEffect as useEffect2, useState as useState7 } from "react";
609
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
610
+ var ExtensionSelect = ({ appId, onSubmit, onBack }) => {
611
+ const [extensions, setExtensions] = useState7([]);
612
+ const [loading, setLoading] = useState7(true);
613
+ const [error, setError] = useState7();
614
+ const [cursor, setCursor] = useState7(0);
615
+ const [backFocused, setBackFocused] = useState7(false);
616
+ useEffect2(() => {
617
+ fetchExtensions(appId).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
618
+ }, [appId]);
619
+ useInput6((_, key) => {
620
+ if (key.upArrow) {
621
+ if (loading || error || extensions.length === 0 || cursor === 0) {
622
+ if (onBack) setBackFocused(true);
623
+ } else {
624
+ setBackFocused(false);
625
+ setCursor((c) => Math.max(0, c - 1));
626
+ }
627
+ return;
628
+ }
629
+ if (key.downArrow) {
630
+ if (backFocused) {
631
+ setBackFocused(false);
632
+ } else if (!loading && !error && extensions.length > 0) {
633
+ setCursor((c) => Math.min(extensions.length - 1, c + 1));
634
+ }
635
+ return;
636
+ }
637
+ if (key.return) {
638
+ if (backFocused) {
639
+ onBack?.();
640
+ return;
641
+ }
642
+ if (!loading && !error && extensions.length > 0) {
643
+ onSubmit(extensions[cursor]);
644
+ }
645
+ }
646
+ });
647
+ const renderContent = () => {
648
+ if (loading) {
649
+ return /* @__PURE__ */ jsxs11(Box11, { gap: 2, children: [
650
+ /* @__PURE__ */ jsx11(Spinner3, { type: "dots" }),
651
+ /* @__PURE__ */ jsx11(Text11, { children: "Loading Extensions\u2026" })
652
+ ] });
653
+ }
654
+ if (error) {
655
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", gap: 1, children: [
656
+ /* @__PURE__ */ jsx11(Text11, { color: "red", bold: true, children: "Failed to load Extensions" }),
657
+ /* @__PURE__ */ jsx11(Text11, { color: "red", children: error })
658
+ ] });
659
+ }
660
+ if (extensions.length === 0) {
661
+ return /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "No Extensions found for this App." });
662
+ }
663
+ return /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: extensions.map((ext, i) => {
664
+ const isCursor = i === cursor && !backFocused;
665
+ return /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
666
+ /* @__PURE__ */ jsx11(Text11, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
667
+ /* @__PURE__ */ jsx11(Text11, { bold: isCursor, children: ext.manifest.name }),
668
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
669
+ "v",
670
+ ext.manifest.version
671
+ ] }),
672
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
673
+ "(",
674
+ ext.id,
675
+ ")"
676
+ ] })
677
+ ] }, ext.id);
678
+ }) });
679
+ };
680
+ return /* @__PURE__ */ jsx11(StepShell, { title: "Select an existing Extension to scaffold:", onBack, backFocused, children: renderContent() });
681
+ };
682
+
683
+ // src/constants.ts
684
+ var TEMPLATE_SOURCE = "github:stackable-labs/templates/app-extension";
685
+ var DEFAULT_PERMISSIONS = ["context:read"];
686
+ var TARGET_PERMISSION_MAP = {
687
+ "slot.header": ["context:read"],
688
+ "slot.content": ["context:read", "data:query", "actions:toast", "actions:invoke"],
689
+ "slot.footer": [],
690
+ "slot.footer-links": []
691
+ };
692
+
518
693
  // src/lib/postScaffold.ts
519
694
  import { execFile } from "child_process";
520
695
  import { promisify } from "util";
@@ -541,18 +716,6 @@ var postScaffold = async (options) => {
541
716
  import { readFile, readdir, rm, writeFile } from "fs/promises";
542
717
  import { join } from "path";
543
718
  import { downloadTemplate } from "giget";
544
-
545
- // src/constants.ts
546
- var TEMPLATE_SOURCE = "github:stackable-labs/templates/app-extension";
547
- var DEFAULT_PERMISSIONS = ["context:read"];
548
- var TARGET_PERMISSION_MAP = {
549
- "slot.header": ["context:read"],
550
- "slot.content": ["context:read", "data:query", "actions:toast", "actions:invoke"],
551
- "slot.footer": [],
552
- "slot.footer-links": []
553
- };
554
-
555
- // src/lib/scaffold.ts
556
719
  var normalizeTargets = (targets) => Array.from(new Set(targets));
557
720
  var derivePermissions = (targets) => {
558
721
  const permissions = /* @__PURE__ */ new Set();
@@ -737,7 +900,7 @@ var rewritePreviewApp = async (rootDir, targets, permissions) => {
737
900
  }
738
901
  const appContent = `import { ExtensionProvider, ExtensionSlot } from '@stackable-labs/sdk-extension-host'
739
902
  import type { CapabilityHandlers } from '@stackable-labs/sdk-extension-host'
740
- import { hostComponents } from '@stackable-labs/embeddables/components'
903
+ import hostComponents from '@stackable-labs/embeddables/components'
741
904
  import type {
742
905
  ${typeImports.join(",\n ")}
743
906
  } from '@stackable-labs/sdk-extension-contracts'
@@ -852,18 +1015,47 @@ var scaffold = async (options) => {
852
1015
 
853
1016
  // src/App.tsx
854
1017
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
855
- var STEP_ORDER = ["app", "name", "targets", "ports", "dir", "confirm"];
856
- var INITIAL_STEPS = [
857
- { label: "Fetching template", status: "pending" },
858
- { label: "Generating files", status: "pending" },
859
- { label: "Installing dependencies", status: "pending" }
860
- ];
1018
+ var STEPS = {
1019
+ ["create" /* CREATE */]: ["app", "name", "targets", "settings", "confirm"],
1020
+ ["scaffold" /* SCAFFOLD */]: ["app", "extensionSelect", "confirmTargets", "settings", "confirm"]
1021
+ };
1022
+ var PROGRESS_STEPS = {
1023
+ ["create" /* CREATE */]: [
1024
+ { label: "Registering extension", status: "pending" },
1025
+ { label: "Fetching template", status: "pending" },
1026
+ { label: "Generating files", status: "pending" },
1027
+ { label: "Installing dependencies", status: "pending" }
1028
+ ],
1029
+ ["scaffold" /* SCAFFOLD */]: [
1030
+ { label: "Fetching template", status: "pending" },
1031
+ { label: "Generating files", status: "pending" },
1032
+ { label: "Installing dependencies", status: "pending" }
1033
+ ]
1034
+ };
861
1035
  var toKebabCase2 = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
862
- var App = ({ initialName, options }) => {
1036
+ var derivePermissions2 = (targets) => {
1037
+ const permissions = /* @__PURE__ */ new Set();
1038
+ for (const target of targets) {
1039
+ const mapped = TARGET_PERMISSION_MAP[target];
1040
+ if (mapped && mapped.length > 0) {
1041
+ for (const permission of mapped) {
1042
+ permissions.add(permission);
1043
+ }
1044
+ }
1045
+ }
1046
+ if (permissions.size === 0) {
1047
+ for (const permission of DEFAULT_PERMISSIONS) {
1048
+ permissions.add(permission);
1049
+ }
1050
+ }
1051
+ return [...permissions];
1052
+ };
1053
+ var App = ({ command, initialName, options }) => {
863
1054
  const { exit } = useApp();
864
1055
  const [step, setStep] = useState8("app");
865
1056
  const [name, setName] = useState8(initialName ?? "");
866
1057
  const [extensionId, setExtensionId] = useState8("");
1058
+ const [extensionVersion, setExtensionVersion] = useState8("");
867
1059
  const [selectedApp, setSelectedApp] = useState8(null);
868
1060
  const [extensionPort, setExtensionPort] = useState8(
869
1061
  options?.extensionPort ? parseInt(options.extensionPort, 10) : 5173
@@ -873,25 +1065,49 @@ var App = ({ initialName, options }) => {
873
1065
  );
874
1066
  const [targets, setTargets] = useState8([]);
875
1067
  const [outputDir, setOutputDir] = useState8("");
876
- const [progressSteps, setProgressSteps] = useState8(INITIAL_STEPS);
1068
+ const [progressSteps, setProgressSteps] = useState8(PROGRESS_STEPS[command]);
877
1069
  const [errorMessage, setErrorMessage] = useState8();
878
1070
  const updateStep = useCallback((index, status) => {
879
1071
  setProgressSteps((prev) => prev.map((s, i) => i === index ? { ...s, status } : s));
880
1072
  }, []);
1073
+ const activeSteps = useCallback(() => {
1074
+ const base = STEPS[command];
1075
+ const skipped = /* @__PURE__ */ new Set();
1076
+ if (command === "create" /* CREATE */ && initialName) skipped.add("name");
1077
+ if (options?.extensionPort || options?.previewPort) skipped.add("settings");
1078
+ return base.filter((s) => !skipped.has(s));
1079
+ }, [command, initialName, options?.extensionPort, options?.previewPort]);
881
1080
  const goBack = useCallback(() => {
882
- const skippedSteps = /* @__PURE__ */ new Set();
883
- if (initialName) skippedSteps.add("name");
884
- if (options?.extensionPort || options?.previewPort) skippedSteps.add("ports");
885
- const activeSteps = STEP_ORDER.filter((s) => !skippedSteps.has(s));
886
1081
  setStep((prev) => {
887
- const idx = activeSteps.indexOf(prev);
888
- return idx > 0 ? activeSteps[idx - 1] : prev;
1082
+ const steps = activeSteps();
1083
+ const idx = steps.indexOf(prev);
1084
+ return idx > 0 ? steps[idx - 1] : prev;
889
1085
  });
890
- }, [initialName, options?.extensionPort, options?.previewPort]);
1086
+ }, [activeSteps]);
891
1087
  const handleAppSelect = (app) => {
892
1088
  setSelectedApp(app);
1089
+ if (command === "scaffold" /* SCAFFOLD */) {
1090
+ setStep("extensionSelect");
1091
+ return;
1092
+ }
893
1093
  setStep(initialName ? "targets" : "name");
894
1094
  };
1095
+ const handleExtensionSelect = (ext) => {
1096
+ setName(ext.manifest.name);
1097
+ setExtensionId(ext.id);
1098
+ setExtensionVersion(ext.manifest.version);
1099
+ setTargets(ext.manifest.targets);
1100
+ setStep("confirmTargets");
1101
+ };
1102
+ const handleConfirmTargets = (value) => {
1103
+ setTargets(value);
1104
+ if (options?.extensionPort || options?.previewPort) {
1105
+ setOutputDir(join2(process.cwd(), toKebabCase2(extensionId || name)));
1106
+ setStep("confirm");
1107
+ } else {
1108
+ setStep("settings");
1109
+ }
1110
+ };
895
1111
  const handleName = (value) => {
896
1112
  setName(value);
897
1113
  setExtensionId(toKebabCase2(value));
@@ -899,45 +1115,61 @@ var App = ({ initialName, options }) => {
899
1115
  };
900
1116
  const handleTargets = (value) => {
901
1117
  setTargets(value);
902
- setStep(options?.extensionPort || options?.previewPort ? "dir" : "ports");
1118
+ if (options?.extensionPort || options?.previewPort) {
1119
+ setOutputDir(join2(process.cwd(), toKebabCase2(extensionId || name)));
1120
+ setStep("confirm");
1121
+ } else {
1122
+ setStep("settings");
1123
+ }
903
1124
  };
904
- const handlePorts = (extPort, prevPort) => {
1125
+ const handleSettings = (extPort, prevPort, dir) => {
905
1126
  setExtensionPort(extPort);
906
1127
  setPreviewPort(prevPort);
907
- setStep("dir");
908
- };
909
- const handleDir = (dir) => {
910
1128
  setOutputDir(dir);
911
1129
  setStep("confirm");
912
1130
  };
913
1131
  const handleConfirm = async () => {
914
1132
  setStep("scaffolding");
915
- setProgressSteps([
916
- { label: "Fetching template", status: "running" },
917
- { label: "Generating files", status: "pending" },
918
- { label: "Installing dependencies", status: "pending" }
919
- ]);
1133
+ setProgressSteps(PROGRESS_STEPS[command]);
920
1134
  try {
921
- updateStep(0, "running");
1135
+ let resolvedExtensionId = extensionId || toKebabCase2(name);
1136
+ let scaffoldStepOffset = 0;
1137
+ if (command === "create" /* CREATE */) {
1138
+ scaffoldStepOffset = 1;
1139
+ updateStep(0, "running");
1140
+ const created = await createExtensionRemote(selectedApp.id, {
1141
+ manifest: {
1142
+ name,
1143
+ version: "0.0.0",
1144
+ targets,
1145
+ permissions: derivePermissions2(targets)
1146
+ },
1147
+ bundleUrl: `http://localhost:${extensionPort}`
1148
+ });
1149
+ resolvedExtensionId = created.id;
1150
+ setExtensionId(created.id);
1151
+ updateStep(0, "done");
1152
+ }
1153
+ updateStep(scaffoldStepOffset + 0, "running");
922
1154
  await scaffold({
923
1155
  appId: selectedApp.id,
924
1156
  name,
925
- extensionId,
1157
+ extensionId: resolvedExtensionId,
926
1158
  targets,
927
1159
  outputDir,
928
1160
  extensionPort,
929
1161
  previewPort
930
1162
  });
931
- updateStep(0, "done");
932
- updateStep(1, "running");
1163
+ updateStep(scaffoldStepOffset + 0, "done");
1164
+ updateStep(scaffoldStepOffset + 1, "running");
933
1165
  await new Promise((r) => setTimeout(r, 200));
934
- updateStep(1, "done");
1166
+ updateStep(scaffoldStepOffset + 1, "done");
935
1167
  if (!options?.skipInstall) {
936
- updateStep(2, "running");
1168
+ updateStep(scaffoldStepOffset + 2, "running");
937
1169
  await postScaffold({ outputDir, skipInstall: false, skipGit: options?.skipGit });
938
- updateStep(2, "done");
1170
+ updateStep(scaffoldStepOffset + 2, "done");
939
1171
  } else {
940
- updateStep(2, "done");
1172
+ updateStep(scaffoldStepOffset + 2, "done");
941
1173
  }
942
1174
  setStep("done");
943
1175
  } catch (err) {
@@ -953,41 +1185,53 @@ var App = ({ initialName, options }) => {
953
1185
  case "app": {
954
1186
  return /* @__PURE__ */ jsx12(AppSelect, { onSubmit: handleAppSelect });
955
1187
  }
956
- case "name": {
1188
+ case "extensionSelect": {
957
1189
  return /* @__PURE__ */ jsx12(
958
- NamePrompt,
1190
+ ExtensionSelect,
959
1191
  {
960
- initialValue: name,
961
- onSubmit: handleName,
1192
+ appId: selectedApp.id,
1193
+ onSubmit: handleExtensionSelect,
962
1194
  onBack: goBack
963
1195
  }
964
1196
  );
965
1197
  }
966
- case "targets": {
1198
+ case "confirmTargets": {
967
1199
  return /* @__PURE__ */ jsx12(
968
1200
  TargetSelect,
969
1201
  {
970
1202
  availableTargets: selectedApp?.targets ?? [],
971
- onSubmit: handleTargets,
1203
+ preSelected: targets,
1204
+ onSubmit: handleConfirmTargets,
1205
+ onBack: goBack
1206
+ }
1207
+ );
1208
+ }
1209
+ case "name": {
1210
+ return /* @__PURE__ */ jsx12(
1211
+ NamePrompt,
1212
+ {
1213
+ initialValue: name,
1214
+ onSubmit: handleName,
972
1215
  onBack: goBack
973
1216
  }
974
1217
  );
975
1218
  }
976
- case "ports": {
1219
+ case "targets": {
977
1220
  return /* @__PURE__ */ jsx12(
978
- PortsPrompt,
1221
+ TargetSelect,
979
1222
  {
980
- onSubmit: handlePorts,
1223
+ availableTargets: selectedApp?.targets ?? [],
1224
+ onSubmit: handleTargets,
981
1225
  onBack: goBack
982
1226
  }
983
1227
  );
984
1228
  }
985
- case "dir": {
1229
+ case "settings": {
986
1230
  return /* @__PURE__ */ jsx12(
987
- DirPrompt,
1231
+ SettingsPrompt,
988
1232
  {
989
- defaultDir: join2(process.cwd(), toKebabCase2(name)),
990
- onSubmit: handleDir,
1233
+ defaultDir: join2(process.cwd(), toKebabCase2(extensionId || name)),
1234
+ onSubmit: handleSettings,
991
1235
  onBack: goBack
992
1236
  }
993
1237
  );
@@ -1001,6 +1245,7 @@ var App = ({ initialName, options }) => {
1001
1245
  previewPort,
1002
1246
  targets,
1003
1247
  outputDir,
1248
+ extensionVersion: command === "scaffold" /* SCAFFOLD */ ? extensionVersion : void 0,
1004
1249
  onConfirm: handleConfirm,
1005
1250
  onCancel: handleCancel,
1006
1251
  onBack: goBack
@@ -1014,17 +1259,26 @@ var App = ({ initialName, options }) => {
1014
1259
  return /* @__PURE__ */ jsx12(Done, { name, outputDir });
1015
1260
  }
1016
1261
  default: {
1017
- return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", gap: 1, children: [
1018
- /* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "Scaffold failed" }),
1262
+ return /* @__PURE__ */ jsx12(StepShell, { title: "Scaffold failed", onBack: goBack, backFocused: false, children: /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", gap: 1, children: [
1263
+ /* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "An error occurred" }),
1019
1264
  errorMessage && /* @__PURE__ */ jsx12(Text12, { color: "red", children: errorMessage })
1020
- ] });
1265
+ ] }) });
1021
1266
  }
1022
1267
  }
1023
1268
  };
1024
1269
 
1025
1270
  // src/index.tsx
1026
1271
  import { jsx as jsx13 } from "react/jsx-runtime";
1027
- program.name("create-extension").description("Scaffold a new Stackable 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 (default: extension port + 1)").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((name, options) => {
1028
- render(/* @__PURE__ */ jsx13(App, { initialName: name, options }));
1272
+ program.name("stackable-extension").description("Stackable extension developer CLI");
1273
+ 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) => {
1274
+ render(/* @__PURE__ */ jsx13(App, { command: "create" /* CREATE */, initialName: name, options }));
1275
+ });
1276
+ 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) => {
1277
+ render(/* @__PURE__ */ jsx13(App, { command: "scaffold" /* SCAFFOLD */, options }));
1029
1278
  });
1030
- program.parse(process.argv.filter((arg) => arg !== "--"));
1279
+ if (process.argv[1]?.endsWith("create-extension")) {
1280
+ process.stderr.write("\u26A0\uFE0F DEPRECATED: `create-extension` is deprecated and will be removed in v2.0.0. Use `stackable-extension create` instead.\n");
1281
+ program.parse(["node", "stackable-extension", "create", ...process.argv.slice(2).filter((arg) => arg !== "--")]);
1282
+ } else {
1283
+ program.parse(process.argv.filter((arg) => arg !== "--"));
1284
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@stackable-labs/cli-app-extension",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
7
7
  "create-extension": "./dist/index.js",
8
- "cli-app-extension": "./dist/index.js"
8
+ "stackable-extension": "./dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "dist/",
@@ -16,7 +16,6 @@
16
16
  "commander": "12.x",
17
17
  "giget": "3.x",
18
18
  "ink": "5.x",
19
- "ink-select-input": "6.x",
20
19
  "ink-spinner": "5.x",
21
20
  "ink-text-input": "6.x",
22
21
  "nypm": "0.4.x",