@stackable-labs/cli-app-extension 1.0.1 → 1.2.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 (3) hide show
  1. package/README.md +20 -7
  2. package/dist/index.js +803 -431
  3. package/package.json +10 -10
package/dist/index.js CHANGED
@@ -5,69 +5,144 @@ import { program } from "commander";
5
5
  import { render } from "ink";
6
6
 
7
7
  // src/App.tsx
8
- import { Box as Box9, Text as Text9, useApp } from "ink";
9
- import { useCallback, useState as useState6 } from "react";
8
+ import { join as join2 } from "path";
9
+ import { Box as Box13, Text as Text13, useApp } from "ink";
10
+ import { useCallback, useState as useState9 } from "react";
11
+
12
+ // src/components/Confirm.tsx
13
+ import { Box as Box2, Text as Text2, useInput } from "ink";
14
+ import { useState } from "react";
15
+
16
+ // src/components/StepShell.tsx
17
+ import { Box, Text } from "ink";
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+ var divider = (width) => "\u2500".repeat(width);
20
+ var INNER_DIVIDER_WIDTH = 40;
21
+ var StepShell = ({ title, hint, children, footer, onBack, backFocused }) => {
22
+ const termWidth = process.stdout.columns ?? 80;
23
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 0, children: [
24
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: divider(termWidth) }),
25
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, gap: 1, children: [
26
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 0, children: [
27
+ onBack && /* @__PURE__ */ jsxs(Box, { gap: 1, marginBottom: 1, children: [
28
+ /* @__PURE__ */ jsx(Text, { color: backFocused ? "cyan" : void 0, children: backFocused ? "\u276F" : " " }),
29
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2190 Back" })
30
+ ] }),
31
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: title }),
32
+ hint && /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint })
33
+ ] }),
34
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: divider(INNER_DIVIDER_WIDTH) }),
35
+ children
36
+ ] }),
37
+ footer && /* @__PURE__ */ jsxs(Fragment, { children: [
38
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: divider(termWidth) }),
39
+ /* @__PURE__ */ jsx(Box, { paddingX: 1, paddingTop: 1, children: footer })
40
+ ] })
41
+ ] });
42
+ };
10
43
 
11
44
  // src/components/Confirm.tsx
12
- import { Box, Text, useInput } from "ink";
13
- import { jsx, jsxs } from "react/jsx-runtime";
14
- function Confirm({ name, extensionId, extensionPort, previewPort, targets, outputDir, onConfirm, onCancel }) {
45
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
46
+ var Confirm = ({ name, extensionPort, previewPort, targets, outputDir, onConfirm, onCancel, onBack }) => {
47
+ const [backFocused, setBackFocused] = useState(false);
15
48
  useInput((input, key) => {
49
+ if (key.upArrow && onBack) {
50
+ setBackFocused(true);
51
+ return;
52
+ }
53
+ if (key.downArrow && backFocused) {
54
+ setBackFocused(false);
55
+ return;
56
+ }
57
+ if (key.return && backFocused) {
58
+ onBack?.();
59
+ return;
60
+ }
61
+ if (backFocused) return;
16
62
  if (input === "y" || key.return) {
17
63
  onConfirm();
18
64
  return;
19
65
  }
20
- if (input === "n" || key.escape) {
66
+ if (input === "n") {
21
67
  onCancel();
22
68
  }
23
69
  });
24
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
25
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Ready to scaffold" }),
26
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
27
- /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
28
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Name" }),
29
- /* @__PURE__ */ jsx(Text, { children: name })
70
+ return /* @__PURE__ */ jsx2(
71
+ StepShell,
72
+ {
73
+ title: "Ready to scaffold",
74
+ hint: "Review your settings before proceeding",
75
+ onBack,
76
+ backFocused,
77
+ 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" })
30
82
  ] }),
31
- /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
32
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "ID " }),
33
- /* @__PURE__ */ jsx(Text, { children: extensionId })
34
- ] }),
35
- /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
36
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Dir " }),
37
- /* @__PURE__ */ jsx(Text, { children: outputDir })
38
- ] }),
39
- /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
40
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Extension port" }),
41
- /* @__PURE__ */ jsx(Text, { children: extensionPort })
42
- ] }),
43
- /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
44
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Preview port " }),
45
- /* @__PURE__ */ jsx(Text, { children: previewPort })
46
- ] }),
47
- /* @__PURE__ */ jsxs(Box, { gap: 2, flexDirection: "column", children: [
48
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Targets" }),
49
- targets.map((t) => /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
50
- /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2022 " }),
51
- /* @__PURE__ */ jsx(Text, { children: t })
52
- ] }, t))
83
+ children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
84
+ /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
85
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Name " }),
86
+ /* @__PURE__ */ jsx2(Text2, { children: name })
87
+ ] }),
88
+ /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
89
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Directory " }),
90
+ /* @__PURE__ */ jsx2(Text2, { children: outputDir })
91
+ ] }),
92
+ /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
93
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Extension port" }),
94
+ /* @__PURE__ */ jsx2(Text2, { children: extensionPort })
95
+ ] }),
96
+ /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
97
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Preview port " }),
98
+ /* @__PURE__ */ jsx2(Text2, { children: previewPort })
99
+ ] }),
100
+ /* @__PURE__ */ jsxs2(Box2, { gap: 2, children: [
101
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Targets " }),
102
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: targets.map((t) => /* @__PURE__ */ jsxs2(Box2, { children: [
103
+ /* @__PURE__ */ jsx2(Text2, { color: "green", children: "\u2022 " }),
104
+ /* @__PURE__ */ jsx2(Text2, { children: t })
105
+ ] }, t)) })
106
+ ] })
53
107
  ] })
54
- ] }),
55
- /* @__PURE__ */ jsxs(Text, { children: [
56
- "Proceed? ",
57
- /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "Y" }),
58
- "/",
59
- /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "n" })
60
- ] })
61
- ] });
62
- }
108
+ }
109
+ );
110
+ };
63
111
 
64
112
  // src/components/DirPrompt.tsx
65
- import { Box as Box2, Text as Text2 } from "ink";
113
+ import { Box as Box4, Text as Text4 } from "ink";
66
114
  import TextInput from "ink-text-input";
67
- import { useState } from "react";
68
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
69
- function DirPrompt({ defaultDir, onSubmit }) {
70
- const [value, setValue] = useState(defaultDir);
115
+ import { useState as useState3 } from "react";
116
+
117
+ // src/components/BackableInput.tsx
118
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
119
+ import { useState as useState2 } from "react";
120
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
121
+ var BackableInput = ({ label, hint, onBack, children, error }) => {
122
+ const [focus, setFocus] = useState2("input");
123
+ useInput2((_input, key) => {
124
+ if (key.upArrow && focus === "input" && onBack) {
125
+ setFocus("back");
126
+ return;
127
+ }
128
+ if (key.downArrow && focus === "back") {
129
+ setFocus("input");
130
+ return;
131
+ }
132
+ if (key.return && focus === "back") {
133
+ onBack?.();
134
+ }
135
+ });
136
+ return /* @__PURE__ */ jsx3(StepShell, { title: label, hint, onBack, backFocused: focus === "back", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
137
+ 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);
71
146
  const handleSubmit = (val) => {
72
147
  const trimmed = val.trim();
73
148
  if (trimmed.length === 0) {
@@ -75,101 +150,60 @@ function DirPrompt({ defaultDir, onSubmit }) {
75
150
  }
76
151
  onSubmit(trimmed);
77
152
  };
78
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
79
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: "Output directory:" }),
80
- /* @__PURE__ */ jsxs2(Box2, { children: [
81
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2192 " }),
82
- /* @__PURE__ */ jsx2(TextInput, { value, onChange: setValue, onSubmit: handleSubmit })
83
- ] }),
84
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "(Press Enter to confirm)" })
85
- ] });
86
- }
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 })
156
+ ] }) });
157
+ };
87
158
 
88
159
  // src/components/Done.tsx
89
- import { Box as Box3, Text as Text3 } from "ink";
90
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
91
- function Done({ name, outputDir }) {
92
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
93
- /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
94
- /* @__PURE__ */ jsx3(Text3, { color: "green", bold: true, children: "\u2714" }),
95
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Extension scaffolded successfully!" })
96
- ] }),
97
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, children: [
98
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
99
- "Created: ",
100
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: name })
101
- ] }),
102
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
103
- "Location: ",
104
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: outputDir })
105
- ] })
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 })
106
171
  ] }),
107
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, children: [
108
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Next steps:" }),
109
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, gap: 1, children: [
110
- /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
111
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "1." }),
112
- /* @__PURE__ */ jsxs3(Text3, { children: [
113
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "cd " }),
114
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: outputDir })
115
- ] })
116
- ] }),
117
- /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
118
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "2." }),
119
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "pnpm install" })
120
- ] }),
121
- /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
122
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "3." }),
123
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "pnpm dev" })
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 })
124
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" })
125
194
  ] })
126
195
  ] })
127
- ] });
128
- }
129
-
130
- // src/components/IdPrompt.tsx
131
- import { Box as Box4, Text as Text4 } from "ink";
132
- import TextInput2 from "ink-text-input";
133
- import { useEffect, useState as useState2 } from "react";
134
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
135
- function toKebabCase(value) {
136
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
137
- }
138
- function IdPrompt({ extensionName, onSubmit }) {
139
- const derived = toKebabCase(extensionName);
140
- const [value, setValue] = useState2(derived);
141
- const [error, setError] = useState2();
142
- useEffect(() => {
143
- setValue(toKebabCase(extensionName));
144
- }, [extensionName]);
145
- const handleSubmit = (val) => {
146
- const trimmed = val.trim() || derived;
147
- if (!/^[a-z0-9][a-z0-9-]*$/.test(trimmed)) {
148
- setError("Extension ID must be lowercase kebab-case (letters, numbers, hyphens only)");
149
- return;
150
- }
151
- setError(void 0);
152
- onSubmit(trimmed);
153
- };
154
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
155
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Extension ID" }),
156
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Used as the unique identifier in the manifest. Press Enter to accept the default." }),
157
- /* @__PURE__ */ jsxs4(Box4, { children: [
158
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "> " }),
159
- /* @__PURE__ */ jsx4(TextInput2, { value, onChange: setValue, onSubmit: handleSubmit })
160
- ] }),
161
- error && /* @__PURE__ */ jsx4(Text4, { color: "red", children: error })
162
- ] });
163
- }
196
+ ] })
197
+ ] });
164
198
 
165
199
  // src/components/NamePrompt.tsx
166
- import { Box as Box5, Text as Text5 } from "ink";
167
- import TextInput3 from "ink-text-input";
168
- import { useState as useState3 } from "react";
169
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
170
- function NamePrompt({ initialValue = "", onSubmit }) {
171
- const [value, setValue] = useState3(initialValue);
172
- const [error, setError] = useState3();
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";
204
+ var NamePrompt = ({ initialValue = "", onSubmit, onBack }) => {
205
+ const [value, setValue] = useState4(initialValue);
206
+ const [error, setError] = useState4();
173
207
  const handleSubmit = (val) => {
174
208
  const trimmed = val.trim();
175
209
  if (trimmed.length === 0) {
@@ -179,28 +213,26 @@ function NamePrompt({ initialValue = "", onSubmit }) {
179
213
  setError(void 0);
180
214
  onSubmit(trimmed);
181
215
  };
182
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
183
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "What is your extension name?" }),
184
- /* @__PURE__ */ jsxs5(Box5, { children: [
185
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "> " }),
186
- /* @__PURE__ */ jsx5(TextInput3, { value, onChange: setValue, onSubmit: handleSubmit })
187
- ] }),
188
- error && /* @__PURE__ */ jsx5(Text5, { color: "red", children: error })
189
- ] });
190
- }
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 })
219
+ ] }) });
220
+ };
191
221
 
192
222
  // src/components/PortsPrompt.tsx
193
- import { Box as Box6, Text as Text6 } from "ink";
194
- import TextInput4 from "ink-text-input";
195
- import { useState as useState4 } from "react";
196
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
197
- function PortsPrompt({ onSubmit }) {
198
- const [extensionPort, setExtensionPort] = useState4("5173");
199
- const [previewPort, setPreviewPort] = useState4("");
200
- const [step, setStep] = useState4("extension");
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";
227
+ var DEFAULT_EXTENSION_PORT = 5173;
228
+ 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");
201
233
  const handleExtensionSubmit = (value) => {
202
234
  const trimmed = value.trim();
203
- const port = trimmed === "" ? 5173 : parseInt(trimmed, 10);
235
+ const port = trimmed === "" ? DEFAULT_EXTENSION_PORT : parseInt(trimmed, 10);
204
236
  if (isNaN(port) || port < 1024 || port > 65535) {
205
237
  return;
206
238
  }
@@ -217,115 +249,120 @@ function PortsPrompt({ onSubmit }) {
217
249
  }
218
250
  onSubmit(extPort, prevPort);
219
251
  };
252
+ const handleBack = () => {
253
+ if (step === "preview") {
254
+ setStep("extension");
255
+ } else {
256
+ onBack?.();
257
+ }
258
+ };
220
259
  if (step === "extension") {
221
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
222
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Extension dev server port:" }),
223
- /* @__PURE__ */ jsxs6(Box6, { children: [
224
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "\u2192 " }),
225
- /* @__PURE__ */ jsx6(
226
- TextInput4,
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,
284
+ {
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,
227
292
  {
228
- value: extensionPort,
229
- onChange: setExtensionPort,
230
- onSubmit: handleExtensionSubmit,
231
- placeholder: "5173"
293
+ value: previewPort,
294
+ onChange: setPreviewPort,
295
+ onSubmit: handlePreviewSubmit,
296
+ placeholder: String(DEFAULT_EXTENSION_PORT + 1),
297
+ focus: isFocused
232
298
  }
233
299
  )
234
- ] }),
235
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "(Press Enter to use default: 5173)" })
236
- ] });
237
- }
238
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
239
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Preview host dev server port:" }),
240
- /* @__PURE__ */ jsxs6(Box6, { children: [
241
- /* @__PURE__ */ jsx6(Text6, { color: "cyan", children: "\u2192 " }),
242
- /* @__PURE__ */ jsx6(
243
- TextInput4,
244
- {
245
- value: previewPort,
246
- onChange: setPreviewPort,
247
- onSubmit: handlePreviewSubmit,
248
- placeholder: String(parseInt(extensionPort, 10) + 1)
249
- }
250
- )
251
- ] }),
252
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
253
- "(Press Enter to use: ",
254
- parseInt(extensionPort, 10) + 1,
255
- ")"
256
- ] })
257
- ] });
258
- }
300
+ ] })
301
+ }
302
+ );
303
+ };
259
304
 
260
305
  // src/components/ScaffoldProgress.tsx
261
- import { Box as Box7, Text as Text7 } from "ink";
306
+ import { Box as Box8, Text as Text8 } from "ink";
262
307
  import Spinner from "ink-spinner";
263
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
264
- function stepIcon(status) {
308
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
309
+ var stepIcon = (status) => {
265
310
  switch (status) {
266
311
  case "running":
267
- return /* @__PURE__ */ jsx7(Spinner, { type: "dots" });
312
+ return /* @__PURE__ */ jsx8(Spinner, { type: "dots" });
268
313
  case "done":
269
- return /* @__PURE__ */ jsx7(Text7, { color: "green", children: "\u2714" });
314
+ return /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714" });
270
315
  case "error":
271
- return /* @__PURE__ */ jsx7(Text7, { color: "red", children: "\u2716" });
316
+ return /* @__PURE__ */ jsx8(Text8, { color: "red", children: "\u2716" });
272
317
  default:
273
- return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u25CB" });
318
+ return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "\u25CB" });
274
319
  }
275
- }
276
- function ScaffoldProgress({ steps }) {
277
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
278
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Scaffolding your extension\u2026" }),
279
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: steps.map((step) => /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
280
- stepIcon(step.status),
281
- /* @__PURE__ */ jsx7(Text7, { dimColor: step.status === "pending", color: step.status === "running" ? "cyan" : void 0, children: step.label })
282
- ] }, step.label)) })
283
- ] });
284
- }
285
-
286
- // src/components/TargetSelect.tsx
287
- import { Box as Box8, Text as Text8, useInput as useInput2 } from "ink";
288
- import { useState as useState5 } from "react";
289
-
290
- // src/constants.ts
291
- var TARGET_OPTIONS = [
292
- "slot.header",
293
- "slot.content",
294
- "slot.footer",
295
- "slot.footer-links"
296
- ];
297
- var CAPABILITY_PERMISSION_MAP = {
298
- "slot.header": ["context:read"],
299
- "slot.content": ["context:read", "data:query", "actions:toast", "actions:invoke"],
300
- "slot.footer": [],
301
- "slot.footer-links": []
302
320
  };
303
- var DEFAULT_PERMISSION_FALLBACK = ["context:read"];
304
- var TEMPLATE_SOURCE = "github:stackable-labs/templates/app-extension";
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: [
324
+ stepIcon(step.status),
325
+ /* @__PURE__ */ jsx8(Text8, { dimColor: step.status === "pending", color: step.status === "running" ? "cyan" : void 0, children: step.label })
326
+ ] }, step.label)) })
327
+ ] });
305
328
 
306
329
  // src/components/TargetSelect.tsx
307
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
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";
308
333
  var TARGET_DESCRIPTIONS = {
309
334
  "slot.header": "Renders content in the panel header area",
310
335
  "slot.content": "Renders the main panel body (includes store + navigation state)",
311
336
  "slot.footer": "Renders a footer bar at the bottom of the panel",
312
337
  "slot.footer-links": "Renders a link row in the global footer"
313
338
  };
314
- function TargetSelect({ onSubmit }) {
315
- const [cursor, setCursor] = useState5(0);
316
- const [selected, setSelected] = useState5(/* @__PURE__ */ new Set(["slot.content"]));
317
- const [error, setError] = useState5();
318
- useInput2((input, key) => {
339
+ var TargetSelect = ({ availableTargets, preSelected, onSubmit, onBack }) => {
340
+ const [cursor, setCursor] = useState6(0);
341
+ const [backFocused, setBackFocused] = useState6(false);
342
+ const [selected, setSelected] = useState6(
343
+ new Set(preSelected ?? (availableTargets.includes("slot.content") ? ["slot.content"] : []))
344
+ );
345
+ const [error, setError] = useState6();
346
+ useInput3((input, key) => {
319
347
  if (key.upArrow) {
320
- setCursor((c) => (c - 1 + TARGET_OPTIONS.length) % TARGET_OPTIONS.length);
348
+ if (cursor === 0 && onBack) {
349
+ setBackFocused(true);
350
+ } else {
351
+ setBackFocused(false);
352
+ setCursor((c) => Math.max(0, c - 1));
353
+ }
321
354
  return;
322
355
  }
323
356
  if (key.downArrow) {
324
- setCursor((c) => (c + 1) % TARGET_OPTIONS.length);
357
+ if (backFocused) {
358
+ setBackFocused(false);
359
+ } else {
360
+ setCursor((c) => Math.min(availableTargets.length - 1, c + 1));
361
+ }
325
362
  return;
326
363
  }
327
- if (input === " ") {
328
- const target = TARGET_OPTIONS[cursor];
364
+ if (input === " " && !backFocused) {
365
+ const target = availableTargets[cursor];
329
366
  setSelected((prev) => {
330
367
  const next = new Set(prev);
331
368
  if (next.has(target)) {
@@ -339,105 +376,305 @@ function TargetSelect({ onSubmit }) {
339
376
  return;
340
377
  }
341
378
  if (key.return) {
379
+ if (backFocused) {
380
+ onBack?.();
381
+ return;
382
+ }
342
383
  if (selected.size === 0) {
343
- setError("Select at least one target slot");
384
+ setError("Select at least one Surface target/slot");
344
385
  return;
345
386
  }
346
387
  onSubmit([...selected]);
347
388
  }
348
389
  });
349
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", gap: 1, children: [
350
- /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Select target slots" }),
351
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Space to toggle, Enter to confirm" }),
352
- /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", children: TARGET_OPTIONS.map((target, i) => {
353
- const isSelected = selected.has(target);
390
+ return /* @__PURE__ */ jsxs9(
391
+ StepShell,
392
+ {
393
+ title: "Select Surface targets/slots",
394
+ hint: "Space to toggle, Enter to confirm",
395
+ onBack,
396
+ backFocused,
397
+ children: [
398
+ /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", gap: 1, children: availableTargets.map((target, i) => {
399
+ const isSelected = selected.has(target);
400
+ 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 })
407
+ ] })
408
+ ] }, target);
409
+ }) }),
410
+ error && /* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
411
+ ]
412
+ }
413
+ );
414
+ };
415
+
416
+ // src/components/AppSelect.tsx
417
+ import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
418
+ import Spinner2 from "ink-spinner";
419
+ import { useEffect, useState as useState7 } from "react";
420
+
421
+ // src/lib/api.ts
422
+ var DEFAULT_API_URL = "https://api.stackablelabs.io/app-extension/latest";
423
+ var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
424
+ var getApiBaseUrl = () => process.env.API_BASE_URL ?? DEFAULT_API_URL;
425
+ var getAdminApiBaseUrl = () => process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
426
+ var fetchApps = async () => {
427
+ const baseURL = `${getApiBaseUrl()}/apps`;
428
+ const res = await fetch(baseURL);
429
+ if (!res.ok) {
430
+ throw new Error(`Failed to fetch apps: ${res.status} ${res.statusText}`);
431
+ }
432
+ return res.json();
433
+ };
434
+ var createExtensionRemote = async (appId, payload) => {
435
+ const baseUrl = getAdminApiBaseUrl();
436
+ const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`, {
437
+ method: "POST",
438
+ headers: { "content-type": "application/json" },
439
+ body: JSON.stringify(payload)
440
+ });
441
+ if (!res.ok) {
442
+ const body = await res.text();
443
+ throw new Error(`Failed to create extension: ${res.status} ${body}`);
444
+ }
445
+ return res.json();
446
+ };
447
+ var fetchExtensions = async (appId) => {
448
+ const baseUrl = getApiBaseUrl();
449
+ const res = await fetch(`${baseUrl}/extensions/${appId}`);
450
+ if (!res.ok) {
451
+ throw new Error(`Failed to fetch extensions: ${res.status} ${res.statusText}`);
452
+ }
453
+ return res.json();
454
+ };
455
+
456
+ // src/components/Banner.tsx
457
+ import { Box as Box10, Text as Text10 } from "ink";
458
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
459
+ var WORDMARK = [
460
+ " _ _ _ _ ",
461
+ " ___| |_ __ _ ___| | ____ _| |__ | | ___",
462
+ "/ __| __/ _` |/ __| |/ / _` | '_ \\| |/ _ \\",
463
+ "\\__ \\ || (_| | (__| < (_| | |_) | | __/",
464
+ "|___/\\__\\__,_|\\___|_|\\_\\__,_|_.__/|_|\\___|"
465
+ ];
466
+ var COLORS = [
467
+ [232, 218, 234],
468
+ // Lit Lilac #E8DAEA
469
+ [197, 96, 255],
470
+ // Poppy Purple #C560FF
471
+ [0, 174, 247],
472
+ // Bluetooth Blue #00AEF7
473
+ [70, 224, 177],
474
+ // Tropical Teal #46E0B1
475
+ [252, 248, 161]
476
+ // Not Mellow Yellow #FCF8A1
477
+ ];
478
+ var lerp = (a, b, t) => {
479
+ const r = Math.round(a[0] + (b[0] - a[0]) * t);
480
+ const g = Math.round(a[1] + (b[1] - a[1]) * t);
481
+ const bl = Math.round(a[2] + (b[2] - a[2]) * t);
482
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${bl.toString(16).padStart(2, "0")}`;
483
+ };
484
+ var gradientColor = (row, col, rows, cols) => {
485
+ const t = (row / (rows - 1) + col / (cols - 1)) / 2;
486
+ const idx = t * (COLORS.length - 1);
487
+ const lo = Math.floor(idx);
488
+ const hi = Math.min(lo + 1, COLORS.length - 1);
489
+ return lerp(COLORS[lo], COLORS[hi], idx - lo);
490
+ };
491
+ var Banner = () => {
492
+ const termWidth = process.stdout.columns ?? 80;
493
+ const maxLen = Math.max(...WORDMARK.map((l) => l.length));
494
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
495
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2500".repeat(termWidth) }),
496
+ /* @__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)) })
497
+ ] });
498
+ };
499
+
500
+ // src/components/AppSelect.tsx
501
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
502
+ var AppSelect = ({ onSubmit }) => {
503
+ const [apps, setApps] = useState7([]);
504
+ const [loading, setLoading] = useState7(true);
505
+ const [error, setError] = useState7();
506
+ const [cursor, setCursor] = useState7(0);
507
+ useEffect(() => {
508
+ fetchApps().then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
509
+ }, []);
510
+ useInput4((_, key) => {
511
+ if (loading || error || apps.length === 0) return;
512
+ if (key.upArrow) {
513
+ setCursor((c) => Math.max(0, c - 1));
514
+ } else if (key.downArrow) {
515
+ setCursor((c) => Math.min(apps.length - 1, c + 1));
516
+ } else if (key.return) {
517
+ onSubmit(apps[cursor]);
518
+ }
519
+ });
520
+ if (loading) {
521
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
522
+ /* @__PURE__ */ jsx11(Banner, {}),
523
+ /* @__PURE__ */ jsxs11(Box11, { gap: 2, paddingX: 1, children: [
524
+ /* @__PURE__ */ jsx11(Spinner2, { type: "dots" }),
525
+ /* @__PURE__ */ jsx11(Text11, { children: "Loading available Apps\u2026" })
526
+ ] })
527
+ ] });
528
+ }
529
+ if (error) {
530
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", gap: 1, children: [
531
+ /* @__PURE__ */ jsx11(Text11, { color: "red", bold: true, children: "Failed to load apps" }),
532
+ /* @__PURE__ */ jsx11(Text11, { color: "red", children: error })
533
+ ] });
534
+ }
535
+ if (apps.length === 0) {
536
+ return /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: "No apps available. Contact your administrator." });
537
+ }
538
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
539
+ /* @__PURE__ */ jsx11(Banner, {}),
540
+ /* @__PURE__ */ jsx11(StepShell, { title: "Select the App you are building an Extension for:", children: /* @__PURE__ */ jsx11(Box11, { flexDirection: "column", children: apps.map((app, i) => {
354
541
  const isCursor = i === cursor;
355
- return /* @__PURE__ */ jsxs8(Box8, { gap: 1, children: [
356
- /* @__PURE__ */ jsx8(Text8, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
357
- /* @__PURE__ */ jsx8(Text8, { color: isSelected ? "green" : void 0, children: isSelected ? "\u25C9" : "\u25CB" }),
358
- /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
359
- /* @__PURE__ */ jsx8(Text8, { bold: isSelected, children: target }),
360
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: TARGET_DESCRIPTIONS[target] })
542
+ return /* @__PURE__ */ jsxs11(Box11, { gap: 1, children: [
543
+ /* @__PURE__ */ jsx11(Text11, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
544
+ /* @__PURE__ */ jsx11(Text11, { bold: isCursor, children: app.name }),
545
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
546
+ "(",
547
+ app.id,
548
+ ")"
361
549
  ] })
362
- ] }, target);
363
- }) }),
364
- error && /* @__PURE__ */ jsx8(Text8, { color: "red", children: error })
550
+ ] }, app.id);
551
+ }) }) })
365
552
  ] });
366
- }
553
+ };
554
+
555
+ // src/components/ExtensionSelect.tsx
556
+ import { Box as Box12, Text as Text12, useInput as useInput5 } from "ink";
557
+ import Spinner3 from "ink-spinner";
558
+ import { useEffect as useEffect2, useState as useState8 } from "react";
559
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
560
+ var ExtensionSelect = ({ appId, onSubmit, onBack }) => {
561
+ const [extensions, setExtensions] = useState8([]);
562
+ const [loading, setLoading] = useState8(true);
563
+ const [error, setError] = useState8();
564
+ const [cursor, setCursor] = useState8(0);
565
+ const [backFocused, setBackFocused] = useState8(false);
566
+ useEffect2(() => {
567
+ fetchExtensions(appId).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
568
+ }, [appId]);
569
+ useInput5((_, key) => {
570
+ if (loading || error || extensions.length === 0) return;
571
+ if (key.upArrow) {
572
+ if (cursor === 0 && onBack) {
573
+ setBackFocused(true);
574
+ } else {
575
+ setBackFocused(false);
576
+ setCursor((c) => Math.max(0, c - 1));
577
+ }
578
+ return;
579
+ }
580
+ if (key.downArrow) {
581
+ if (backFocused) {
582
+ setBackFocused(false);
583
+ } else {
584
+ setCursor((c) => Math.min(extensions.length - 1, c + 1));
585
+ }
586
+ return;
587
+ }
588
+ if (key.return) {
589
+ if (backFocused) {
590
+ onBack?.();
591
+ return;
592
+ }
593
+ onSubmit(extensions[cursor]);
594
+ }
595
+ });
596
+ if (loading) {
597
+ return /* @__PURE__ */ jsxs12(Box12, { gap: 2, paddingX: 1, children: [
598
+ /* @__PURE__ */ jsx12(Spinner3, { type: "dots" }),
599
+ /* @__PURE__ */ jsx12(Text12, { children: "Loading Extensions\u2026" })
600
+ ] });
601
+ }
602
+ if (error) {
603
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", gap: 1, children: [
604
+ /* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "Failed to load Extensions" }),
605
+ /* @__PURE__ */ jsx12(Text12, { color: "red", children: error })
606
+ ] });
607
+ }
608
+ if (extensions.length === 0) {
609
+ return /* @__PURE__ */ jsx12(Text12, { color: "yellow", children: "No Extensions found for this App." });
610
+ }
611
+ return /* @__PURE__ */ jsx12(StepShell, { title: "Select an existing Extension to scaffold:", onBack, backFocused, children: /* @__PURE__ */ jsx12(Box12, { flexDirection: "column", children: extensions.map((ext, i) => {
612
+ const isCursor = i === cursor && !backFocused;
613
+ return /* @__PURE__ */ jsxs12(Box12, { gap: 1, children: [
614
+ /* @__PURE__ */ jsx12(Text12, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u276F" : " " }),
615
+ /* @__PURE__ */ jsx12(Text12, { bold: isCursor, children: ext.manifest.name }),
616
+ /* @__PURE__ */ jsxs12(Text12, { dimColor: true, children: [
617
+ "(",
618
+ ext.id,
619
+ ")"
620
+ ] })
621
+ ] }, ext.id);
622
+ }) }) });
623
+ };
624
+
625
+ // src/constants.ts
626
+ var TEMPLATE_SOURCE = "github:stackable-labs/templates/app-extension";
627
+ var DEFAULT_PERMISSIONS = ["context:read"];
628
+ var TARGET_PERMISSION_MAP = {
629
+ "slot.header": ["context:read"],
630
+ "slot.content": ["context:read", "data:query", "actions:toast", "actions:invoke"],
631
+ "slot.footer": [],
632
+ "slot.footer-links": []
633
+ };
367
634
 
368
635
  // src/lib/postScaffold.ts
369
636
  import { execFile } from "child_process";
370
637
  import { promisify } from "util";
371
638
  import { installDependencies } from "nypm";
372
639
  var execFileAsync = promisify(execFile);
373
- async function postScaffold(options) {
374
- if (!options.skipGit) {
375
- await gitInit(options.outputDir);
376
- }
377
- if (!options.skipInstall) {
378
- await installDependencies({ cwd: options.outputDir, silent: true });
379
- }
380
- }
381
- async function gitInit(dir) {
640
+ var gitInit = async (dir) => {
382
641
  try {
383
642
  await execFileAsync("git", ["init"], { cwd: dir });
384
643
  await execFileAsync("git", ["add", "-A"], { cwd: dir });
385
644
  await execFileAsync("git", ["commit", "-m", "chore: initial scaffold"], { cwd: dir });
386
645
  } catch {
387
646
  }
388
- }
647
+ };
648
+ var postScaffold = async (options) => {
649
+ if (!options.skipGit) {
650
+ await gitInit(options.outputDir);
651
+ }
652
+ if (!options.skipInstall) {
653
+ await installDependencies({ cwd: options.outputDir, silent: true });
654
+ }
655
+ };
389
656
 
390
657
  // src/lib/scaffold.ts
391
- import { downloadTemplate } from "giget";
392
658
  import { readFile, readdir, rm, writeFile } from "fs/promises";
393
659
  import { join } from "path";
394
- async function scaffold(options) {
395
- const { dir } = await downloadTemplate(TEMPLATE_SOURCE, {
396
- dir: options.outputDir,
397
- force: true
398
- });
399
- const selectedTargets = normalizeTargets(options.targets);
400
- const derivedPermissions = derivePermissions(selectedTargets);
401
- await replacePlaceholders(dir, {
402
- "__EXTENSION_ID__": toKebabCase2(options.extensionId || options.name),
403
- "__EXTENSION_DISPLAY_NAME__": options.name,
404
- "replace-with-extension-name": toKebabCase2(options.name),
405
- "replace-with-extension-package-name": `@agnostack/extensions-${toKebabCase2(options.extensionId || options.name)}`
406
- });
407
- await generateManifest(dir, options.name, selectedTargets, derivedPermissions);
408
- await generateSurfaceFiles(dir, selectedTargets);
409
- await rewriteExtensionIndex(dir, options.extensionId || options.name, selectedTargets);
410
- await rewritePreviewApp(dir, selectedTargets, derivedPermissions);
411
- await rewriteTurboJson(dir);
412
- await writeEnvFile(dir, options.extensionPort, options.previewPort);
413
- return options;
414
- }
415
- function normalizeTargets(targets) {
416
- const valid = new Set(TARGET_OPTIONS);
417
- const selected = targets.filter((target) => valid.has(target));
418
- if (selected.length > 0) {
419
- return Array.from(new Set(selected));
420
- }
421
- return ["slot.content"];
422
- }
423
- function derivePermissions(targets) {
660
+ import { downloadTemplate } from "giget";
661
+ var normalizeTargets = (targets) => Array.from(new Set(targets));
662
+ var derivePermissions = (targets) => {
424
663
  const permissions = /* @__PURE__ */ new Set();
425
664
  for (const target of targets) {
426
- for (const permission of CAPABILITY_PERMISSION_MAP[target]) {
665
+ for (const permission of TARGET_PERMISSION_MAP[target] ?? DEFAULT_PERMISSIONS) {
427
666
  permissions.add(permission);
428
667
  }
429
668
  }
430
669
  if (permissions.size === 0) {
431
- for (const fallback of DEFAULT_PERMISSION_FALLBACK) {
670
+ for (const fallback of DEFAULT_PERMISSIONS) {
432
671
  permissions.add(fallback);
433
672
  }
434
673
  }
435
674
  return [...permissions];
436
- }
437
- function toKebabCase2(value) {
438
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
439
- }
440
- async function replacePlaceholders(rootDir, replacements) {
675
+ };
676
+ var toKebabCase = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
677
+ var replacePlaceholders = async (rootDir, replacements) => {
441
678
  const files = await walkFiles(rootDir);
442
679
  for (const filePath of files) {
443
680
  if (!isTextFile(filePath)) {
@@ -455,8 +692,8 @@ async function replacePlaceholders(rootDir, replacements) {
455
692
  await writeFile(filePath, content);
456
693
  }
457
694
  }
458
- }
459
- async function generateManifest(rootDir, extensionName, targets, permissions) {
695
+ };
696
+ var generateManifest = async (rootDir, extensionName, targets, permissions) => {
460
697
  const manifestPath = join(rootDir, "packages/extension/public/manifest.json");
461
698
  const raw = await readFile(manifestPath, "utf8");
462
699
  const manifest = JSON.parse(raw);
@@ -465,8 +702,8 @@ async function generateManifest(rootDir, extensionName, targets, permissions) {
465
702
  manifest.permissions = permissions;
466
703
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
467
704
  `);
468
- }
469
- async function generateSurfaceFiles(rootDir, targets) {
705
+ };
706
+ var generateSurfaceFiles = async (rootDir, targets) => {
470
707
  const surfaceDir = join(rootDir, "packages/extension/src/surfaces");
471
708
  const wantsHeader = targets.includes("slot.header");
472
709
  const wantsContent = targets.includes("slot.content");
@@ -521,35 +758,20 @@ export function Content() {
521
758
  await upsertOrRemove(
522
759
  join(rootDir, "packages/extension/src/store.ts"),
523
760
  wantsContent,
524
- `import { createStore } from '@stackable-labs/sdk-extension-react'
525
-
526
- export type ViewState = { type: 'menu' }
527
-
528
- export interface AppState {
529
- viewState: ViewState
530
- }
531
-
532
- export const appStore = createStore<AppState>({
533
- viewState: { type: 'menu' },
534
- })
535
- `
761
+ "import { createStore } from '@stackable-labs/sdk-extension-react'\n\nexport type ViewState = { type: 'menu' }\n\nexport interface AppState {\n viewState: ViewState\n}\n\nexport const appStore = createStore<AppState>({\n viewState: { type: 'menu' },\n})\n"
536
762
  );
537
763
  await upsertOrRemove(join(surfaceDir, "Footer.tsx"), wantsFooter, buildFooterSurface(targets));
538
- }
539
- function buildFooterSurface(targets) {
764
+ };
765
+ var buildFooterSurface = (targets) => {
540
766
  const blocks = [];
541
767
  if (targets.includes("slot.footer")) {
542
768
  blocks.push(
543
- ` <Surface id="slot.footer">
544
- <ui.Text className="text-xs">Powered by My Extension</ui.Text>
545
- </Surface>`
769
+ ' <Surface id="slot.footer">\n <ui.Text className="text-xs">Powered by My Extension</ui.Text>\n </Surface>'
546
770
  );
547
771
  }
548
772
  if (targets.includes("slot.footer-links")) {
549
773
  blocks.push(
550
- ` <Surface id="slot.footer-links">
551
- <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>
552
- </Surface>`
774
+ ' <Surface id="slot.footer-links">\n <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>\n </Surface>'
553
775
  );
554
776
  }
555
777
  return `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
@@ -562,8 +784,8 @@ ${blocks.join("\n")}
562
784
  )
563
785
  }
564
786
  `;
565
- }
566
- async function rewriteExtensionIndex(rootDir, extensionId, targets) {
787
+ };
788
+ var rewriteExtensionIndex = async (rootDir, extensionId, targets) => {
567
789
  const indexPath = join(rootDir, "packages/extension/src/index.tsx");
568
790
  const imports = ["import { createExtension } from '@stackable-labs/sdk-extension-react'"];
569
791
  const components = [];
@@ -587,12 +809,12 @@ createExtension(
587
809
  ${components.join("\n")}
588
810
  </>
589
811
  ),
590
- { extensionId: '${toKebabCase2(extensionId)}' },
812
+ { extensionId: '${toKebabCase(extensionId)}' },
591
813
  )
592
814
  `;
593
815
  await writeFile(indexPath, content);
594
- }
595
- async function rewritePreviewApp(rootDir, targets, permissions) {
816
+ };
817
+ var rewritePreviewApp = async (rootDir, targets, permissions) => {
596
818
  const appPath = join(rootDir, "packages/preview/src/App.tsx");
597
819
  const includeDataQuery = permissions.includes("data:query");
598
820
  const includeToast = permissions.includes("actions:toast");
@@ -607,27 +829,20 @@ async function rewritePreviewApp(rootDir, targets, permissions) {
607
829
  ].filter(Boolean);
608
830
  const handlers = [];
609
831
  if (includeDataQuery) {
610
- handlers.push(` 'data.query': async (_payload: ApiRequest) => {
611
- return mockData
612
- },`);
832
+ handlers.push(" 'data.query': async (_payload: ApiRequest) => {\n return mockData\n },");
613
833
  }
614
834
  if (includeToast) {
615
- handlers.push(` 'actions.toast': async (payload: ToastPayload) => {
616
- console.log('[Preview] toast:', payload)
617
- },`);
835
+ handlers.push(" 'actions.toast': async (payload: ToastPayload) => {\n console.log('[Preview] toast:', payload)\n },");
618
836
  }
619
837
  if (includeInvoke) {
620
- handlers.push(` 'actions.invoke': async (payload: ActionInvokePayload) => {
621
- console.log('[Preview] action invoke:', payload)
622
- return {}
623
- },`);
838
+ handlers.push(" 'actions.invoke': async (payload: ActionInvokePayload) => {\n console.log('[Preview] action invoke:', payload)\n return {}\n },");
624
839
  }
625
840
  if (includeContextRead) {
626
841
  handlers.push(" 'context.read': async () => mockContext,");
627
842
  }
628
843
  const appContent = `import { ExtensionProvider, ExtensionSlot } from '@stackable-labs/sdk-extension-host'
629
844
  import type { CapabilityHandlers } from '@stackable-labs/sdk-extension-host'
630
- import { hostComponents } from '@stackable-labs/embeddables/components'
845
+ import hostComponents from '@stackable-labs/embeddables/components'
631
846
  import type {
632
847
  ${typeImports.join(",\n ")}
633
848
  } from '@stackable-labs/sdk-extension-contracts'
@@ -677,8 +892,8 @@ export default function App() {
677
892
  }
678
893
  `;
679
894
  await writeFile(appPath, appContent);
680
- }
681
- async function rewriteTurboJson(rootDir) {
895
+ };
896
+ var rewriteTurboJson = async (rootDir) => {
682
897
  const turboPath = join(rootDir, "turbo.json");
683
898
  const raw = await readFile(turboPath, "utf8");
684
899
  const turbo = JSON.parse(raw);
@@ -686,8 +901,8 @@ async function rewriteTurboJson(rootDir) {
686
901
  turbo["globalEnv"] = ["VITE_EXTENSION_PORT", "VITE_PREVIEW_PORT"];
687
902
  await writeFile(turboPath, `${JSON.stringify(turbo, null, 2)}
688
903
  `);
689
- }
690
- async function walkFiles(rootDir) {
904
+ };
905
+ var walkFiles = async (rootDir) => {
691
906
  const entries = await readdir(rootDir, { withFileTypes: true });
692
907
  const files = [];
693
908
  for (const entry of entries) {
@@ -702,63 +917,135 @@ async function walkFiles(rootDir) {
702
917
  files.push(fullPath);
703
918
  }
704
919
  return files;
705
- }
706
- function isTextFile(filePath) {
707
- return /\.(ts|tsx|js|jsx|json|md|html|yml|yaml|env|gitignore|nvmrc)$/i.test(filePath);
708
- }
709
- async function writeEnvFile(dir, extensionPort, previewPort) {
920
+ };
921
+ var isTextFile = (filePath) => /\.(ts|tsx|js|jsx|json|md|html|yml|yaml|env|gitignore|nvmrc)$/i.test(filePath);
922
+ var writeEnvFile = async (dir, extensionPort, previewPort) => {
710
923
  const envPath = join(dir, ".env");
711
924
  const content = `VITE_EXTENSION_PORT=${extensionPort}
712
925
  VITE_PREVIEW_PORT=${previewPort}
713
926
  `;
714
927
  await writeFile(envPath, content);
715
- }
716
- async function upsertOrRemove(filePath, shouldExist, content) {
928
+ };
929
+ var upsertOrRemove = async (filePath, shouldExist, content) => {
717
930
  if (shouldExist) {
718
931
  await writeFile(filePath, content);
719
932
  return;
720
933
  }
721
934
  await rm(filePath, { force: true });
722
- }
935
+ };
936
+ var scaffold = async (options) => {
937
+ const { dir } = await downloadTemplate(TEMPLATE_SOURCE, {
938
+ dir: options.outputDir,
939
+ force: true
940
+ });
941
+ const selectedTargets = normalizeTargets(options.targets);
942
+ const derivedPermissions = derivePermissions(selectedTargets);
943
+ await replacePlaceholders(dir, {
944
+ "__EXTENSION_ID__": toKebabCase(options.extensionId || options.name),
945
+ "__EXTENSION_DISPLAY_NAME__": options.name,
946
+ "replace-with-extension-name": toKebabCase(options.name),
947
+ "replace-with-extension-package-name": `@agnostack/extensions-${toKebabCase(options.extensionId || options.name)}`
948
+ });
949
+ await generateManifest(dir, options.name, selectedTargets, derivedPermissions);
950
+ await generateSurfaceFiles(dir, selectedTargets);
951
+ await rewriteExtensionIndex(dir, options.extensionId || options.name, selectedTargets);
952
+ await rewritePreviewApp(dir, selectedTargets, derivedPermissions);
953
+ await rewriteTurboJson(dir);
954
+ await writeEnvFile(dir, options.extensionPort, options.previewPort);
955
+ return options;
956
+ };
723
957
 
724
958
  // src/App.tsx
725
- import { join as join2 } from "path";
726
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
727
- var INITIAL_STEPS = [
959
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
960
+ var STEP_ORDER_CREATE = ["app", "name", "targets", "ports", "dir", "confirm"];
961
+ var STEP_ORDER_SCAFFOLD = ["app", "extensionSelect", "confirmName", "confirmTargets", "ports", "dir", "confirm"];
962
+ var CREATE_STEPS = [
963
+ { label: "Registering extension", status: "pending" },
728
964
  { label: "Fetching template", status: "pending" },
729
965
  { label: "Generating files", status: "pending" },
730
966
  { label: "Installing dependencies", status: "pending" }
731
967
  ];
732
- function toKebabCase3(value) {
733
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
734
- }
735
- function App({ initialName, options }) {
968
+ var SCAFFOLD_STEPS = [
969
+ { label: "Fetching template", status: "pending" },
970
+ { label: "Generating files", status: "pending" },
971
+ { label: "Installing dependencies", status: "pending" }
972
+ ];
973
+ var toKebabCase2 = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
974
+ var derivePermissions2 = (targets) => {
975
+ const permissions = /* @__PURE__ */ new Set();
976
+ for (const target of targets) {
977
+ const mapped = TARGET_PERMISSION_MAP[target];
978
+ if (mapped && mapped.length > 0) {
979
+ for (const permission of mapped) {
980
+ permissions.add(permission);
981
+ }
982
+ }
983
+ }
984
+ if (permissions.size === 0) {
985
+ for (const permission of DEFAULT_PERMISSIONS) {
986
+ permissions.add(permission);
987
+ }
988
+ }
989
+ return [...permissions];
990
+ };
991
+ var App = ({ mode, initialName, options }) => {
736
992
  const { exit } = useApp();
737
- const [step, setStep] = useState6(initialName ? "id" : "name");
738
- const [name, setName] = useState6(initialName ?? "");
739
- const [extensionId, setExtensionId] = useState6(options?.id ?? "");
740
- const [extensionPort, setExtensionPort] = useState6(
993
+ const [step, setStep] = useState9("app");
994
+ const [name, setName] = useState9(initialName ?? "");
995
+ const [extensionId, setExtensionId] = useState9("");
996
+ const [selectedApp, setSelectedApp] = useState9(null);
997
+ const [extensionPort, setExtensionPort] = useState9(
741
998
  options?.extensionPort ? parseInt(options.extensionPort, 10) : 5173
742
999
  );
743
- const [previewPort, setPreviewPort] = useState6(
1000
+ const [previewPort, setPreviewPort] = useState9(
744
1001
  options?.previewPort ? parseInt(options.previewPort, 10) : 5174
745
1002
  );
746
- const [targets, setTargets] = useState6([]);
747
- const [outputDir, setOutputDir] = useState6("");
748
- const [progressSteps, setProgressSteps] = useState6(INITIAL_STEPS);
749
- const [errorMessage, setErrorMessage] = useState6();
1003
+ const [targets, setTargets] = useState9([]);
1004
+ const [outputDir, setOutputDir] = useState9("");
1005
+ const [progressSteps, setProgressSteps] = useState9(mode === "create" ? CREATE_STEPS : SCAFFOLD_STEPS);
1006
+ const [errorMessage, setErrorMessage] = useState9();
750
1007
  const updateStep = useCallback((index, status) => {
751
- setProgressSteps(
752
- (prev) => prev.map((s, i) => i === index ? { ...s, status } : s)
753
- );
1008
+ setProgressSteps((prev) => prev.map((s, i) => i === index ? { ...s, status } : s));
754
1009
  }, []);
755
- const handleName = (value) => {
1010
+ const activeSteps = useCallback(() => {
1011
+ const base = mode === "create" ? STEP_ORDER_CREATE : STEP_ORDER_SCAFFOLD;
1012
+ const skipped = /* @__PURE__ */ new Set();
1013
+ if (mode === "create" && initialName) skipped.add("name");
1014
+ if (options?.extensionPort || options?.previewPort) skipped.add("ports");
1015
+ return base.filter((s) => !skipped.has(s));
1016
+ }, [mode, initialName, options?.extensionPort, options?.previewPort]);
1017
+ const goBack = useCallback(() => {
1018
+ setStep((prev) => {
1019
+ const steps = activeSteps();
1020
+ const idx = steps.indexOf(prev);
1021
+ return idx > 0 ? steps[idx - 1] : prev;
1022
+ });
1023
+ }, [activeSteps]);
1024
+ const handleAppSelect = (app) => {
1025
+ setSelectedApp(app);
1026
+ if (mode === "scaffold") {
1027
+ setStep("extensionSelect");
1028
+ return;
1029
+ }
1030
+ setStep(initialName ? "targets" : "name");
1031
+ };
1032
+ const handleExtensionSelect = (ext) => {
1033
+ setName(ext.manifest.name);
1034
+ setExtensionId(ext.id);
1035
+ setTargets(ext.manifest.targets);
1036
+ setStep("confirmName");
1037
+ };
1038
+ const handleConfirmName = (value) => {
756
1039
  setName(value);
757
- setExtensionId(toKebabCase3(value));
758
- setStep("id");
1040
+ setStep("confirmTargets");
759
1041
  };
760
- const handleId = (value) => {
761
- setExtensionId(value);
1042
+ const handleConfirmTargets = (value) => {
1043
+ setTargets(value);
1044
+ setStep(options?.extensionPort || options?.previewPort ? "dir" : "ports");
1045
+ };
1046
+ const handleName = (value) => {
1047
+ setName(value);
1048
+ setExtensionId(toKebabCase2(value));
762
1049
  setStep("targets");
763
1050
  };
764
1051
  const handleTargets = (value) => {
@@ -776,31 +1063,46 @@ function App({ initialName, options }) {
776
1063
  };
777
1064
  const handleConfirm = async () => {
778
1065
  setStep("scaffolding");
779
- setProgressSteps([
780
- { label: "Fetching template", status: "running" },
781
- { label: "Generating files", status: "pending" },
782
- { label: "Installing dependencies", status: "pending" }
783
- ]);
1066
+ setProgressSteps(mode === "create" ? CREATE_STEPS : SCAFFOLD_STEPS);
784
1067
  try {
785
- updateStep(0, "running");
1068
+ let resolvedExtensionId = extensionId || toKebabCase2(name);
1069
+ let scaffoldStepOffset = 0;
1070
+ if (mode === "create") {
1071
+ scaffoldStepOffset = 1;
1072
+ updateStep(0, "running");
1073
+ const created = await createExtensionRemote(selectedApp.id, {
1074
+ manifest: {
1075
+ name,
1076
+ version: "0.0.0",
1077
+ targets,
1078
+ permissions: derivePermissions2(targets)
1079
+ },
1080
+ bundleUrl: `http://localhost:${extensionPort}`
1081
+ });
1082
+ resolvedExtensionId = created.id;
1083
+ setExtensionId(created.id);
1084
+ updateStep(0, "done");
1085
+ }
1086
+ updateStep(scaffoldStepOffset + 0, "running");
786
1087
  await scaffold({
1088
+ appId: selectedApp.id,
787
1089
  name,
788
- extensionId,
1090
+ extensionId: resolvedExtensionId,
789
1091
  targets,
790
1092
  outputDir,
791
1093
  extensionPort,
792
1094
  previewPort
793
1095
  });
794
- updateStep(0, "done");
795
- updateStep(1, "running");
1096
+ updateStep(scaffoldStepOffset + 0, "done");
1097
+ updateStep(scaffoldStepOffset + 1, "running");
796
1098
  await new Promise((r) => setTimeout(r, 200));
797
- updateStep(1, "done");
1099
+ updateStep(scaffoldStepOffset + 1, "done");
798
1100
  if (!options?.skipInstall) {
799
- updateStep(2, "running");
1101
+ updateStep(scaffoldStepOffset + 2, "running");
800
1102
  await postScaffold({ outputDir, skipInstall: false, skipGit: options?.skipGit });
801
- updateStep(2, "done");
1103
+ updateStep(scaffoldStepOffset + 2, "done");
802
1104
  } else {
803
- updateStep(2, "done");
1105
+ updateStep(scaffoldStepOffset + 2, "done");
804
1106
  }
805
1107
  setStep("done");
806
1108
  } catch (err) {
@@ -812,51 +1114,121 @@ function App({ initialName, options }) {
812
1114
  const handleCancel = () => {
813
1115
  exit();
814
1116
  };
815
- if (step === "name") {
816
- return /* @__PURE__ */ jsx9(NamePrompt, { initialValue: name, onSubmit: handleName });
817
- }
818
- if (step === "id") {
819
- return /* @__PURE__ */ jsx9(IdPrompt, { extensionName: name, onSubmit: handleId });
820
- }
821
- if (step === "targets") {
822
- return /* @__PURE__ */ jsx9(TargetSelect, { onSubmit: handleTargets });
823
- }
824
- if (step === "ports") {
825
- return /* @__PURE__ */ jsx9(PortsPrompt, { onSubmit: handlePorts });
826
- }
827
- if (step === "dir") {
828
- return /* @__PURE__ */ jsx9(DirPrompt, { defaultDir: join2(process.cwd(), toKebabCase3(name)), onSubmit: handleDir });
829
- }
830
- if (step === "confirm") {
831
- return /* @__PURE__ */ jsx9(
832
- Confirm,
833
- {
834
- name,
835
- extensionId,
836
- extensionPort,
837
- previewPort,
838
- targets,
839
- outputDir,
840
- onConfirm: handleConfirm,
841
- onCancel: handleCancel
842
- }
843
- );
844
- }
845
- if (step === "scaffolding") {
846
- return /* @__PURE__ */ jsx9(ScaffoldProgress, { steps: progressSteps });
847
- }
848
- if (step === "done") {
849
- return /* @__PURE__ */ jsx9(Done, { name, outputDir });
1117
+ switch (step) {
1118
+ case "app": {
1119
+ return /* @__PURE__ */ jsx13(AppSelect, { onSubmit: handleAppSelect });
1120
+ }
1121
+ case "extensionSelect": {
1122
+ return /* @__PURE__ */ jsx13(
1123
+ ExtensionSelect,
1124
+ {
1125
+ appId: selectedApp.id,
1126
+ onSubmit: handleExtensionSelect,
1127
+ onBack: goBack
1128
+ }
1129
+ );
1130
+ }
1131
+ case "confirmName": {
1132
+ return /* @__PURE__ */ jsx13(
1133
+ NamePrompt,
1134
+ {
1135
+ initialValue: name,
1136
+ onSubmit: handleConfirmName,
1137
+ onBack: goBack
1138
+ }
1139
+ );
1140
+ }
1141
+ case "confirmTargets": {
1142
+ return /* @__PURE__ */ jsx13(
1143
+ TargetSelect,
1144
+ {
1145
+ availableTargets: selectedApp?.targets ?? [],
1146
+ preSelected: targets,
1147
+ onSubmit: handleConfirmTargets,
1148
+ onBack: goBack
1149
+ }
1150
+ );
1151
+ }
1152
+ case "name": {
1153
+ return /* @__PURE__ */ jsx13(
1154
+ NamePrompt,
1155
+ {
1156
+ initialValue: name,
1157
+ onSubmit: handleName,
1158
+ onBack: goBack
1159
+ }
1160
+ );
1161
+ }
1162
+ case "targets": {
1163
+ return /* @__PURE__ */ jsx13(
1164
+ TargetSelect,
1165
+ {
1166
+ availableTargets: selectedApp?.targets ?? [],
1167
+ onSubmit: handleTargets,
1168
+ onBack: goBack
1169
+ }
1170
+ );
1171
+ }
1172
+ case "ports": {
1173
+ return /* @__PURE__ */ jsx13(
1174
+ PortsPrompt,
1175
+ {
1176
+ onSubmit: handlePorts,
1177
+ onBack: goBack
1178
+ }
1179
+ );
1180
+ }
1181
+ case "dir": {
1182
+ return /* @__PURE__ */ jsx13(
1183
+ DirPrompt,
1184
+ {
1185
+ defaultDir: join2(process.cwd(), toKebabCase2(extensionId || name)),
1186
+ onSubmit: handleDir,
1187
+ onBack: goBack
1188
+ }
1189
+ );
1190
+ }
1191
+ case "confirm": {
1192
+ return /* @__PURE__ */ jsx13(
1193
+ Confirm,
1194
+ {
1195
+ name,
1196
+ extensionPort,
1197
+ previewPort,
1198
+ targets,
1199
+ outputDir,
1200
+ onConfirm: handleConfirm,
1201
+ onCancel: handleCancel,
1202
+ onBack: goBack
1203
+ }
1204
+ );
1205
+ }
1206
+ case "scaffolding": {
1207
+ return /* @__PURE__ */ jsx13(ScaffoldProgress, { steps: progressSteps });
1208
+ }
1209
+ case "done": {
1210
+ return /* @__PURE__ */ jsx13(Done, { name, outputDir });
1211
+ }
1212
+ default: {
1213
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", gap: 1, children: [
1214
+ /* @__PURE__ */ jsx13(Text13, { color: "red", bold: true, children: "Scaffold failed" }),
1215
+ errorMessage && /* @__PURE__ */ jsx13(Text13, { color: "red", children: errorMessage })
1216
+ ] });
1217
+ }
850
1218
  }
851
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", gap: 1, children: [
852
- /* @__PURE__ */ jsx9(Text9, { color: "red", bold: true, children: "Scaffold failed" }),
853
- errorMessage && /* @__PURE__ */ jsx9(Text9, { color: "red", children: errorMessage })
854
- ] });
855
- }
1219
+ };
856
1220
 
857
1221
  // src/index.tsx
858
- import { jsx as jsx10 } from "react/jsx-runtime";
859
- program.name("create-extension").description("Scaffold a new Stackable extension project").argument("[name]", "Extension project name").option("--id <id>", "Extension ID").option("--targets <targets>", "Comma-separated target slots").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) => {
860
- render(/* @__PURE__ */ jsx10(App, { initialName: name, options }));
1222
+ import { jsx as jsx14 } from "react/jsx-runtime";
1223
+ program.name("stackable-extension").description("Stackable extension developer CLI");
1224
+ program.command("create").description("Create a new extension project").argument("[name]", "Extension project name").option("--targets <targets>", "Comma-separated target slots").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) => {
1225
+ render(/* @__PURE__ */ jsx14(App, { mode: "create", initialName: name, options }));
861
1226
  });
862
- program.parse();
1227
+ program.command("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) => {
1228
+ render(/* @__PURE__ */ jsx14(App, { mode: "scaffold", options }));
1229
+ });
1230
+ if (process.argv[1]?.endsWith("create-extension")) {
1231
+ program.parse(["node", "stackable-extension", "create", ...process.argv.slice(2).filter((arg) => arg !== "--")]);
1232
+ } else {
1233
+ program.parse(process.argv.filter((arg) => arg !== "--"));
1234
+ }