@stackable-labs/cli-app-extension 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -7
  2. package/dist/index.js +586 -418
  3. package/package.json +9 -9
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 Box12, Text as Text12, useApp } from "ink";
10
+ import { useCallback, useState as useState8 } 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 })
30
- ] }),
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 })
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" })
38
82
  ] }),
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, 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"] : [])
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,200 @@ 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);
354
- 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] })
361
- ] })
362
- ] }, target);
363
- }) }),
364
- error && /* @__PURE__ */ jsx8(Text8, { color: "red", children: error })
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 } from "ink";
418
+ import SelectInput from "ink-select-input";
419
+ import Spinner2 from "ink-spinner";
420
+ import { useEffect, useState as useState7 } from "react";
421
+
422
+ // src/lib/api.ts
423
+ var API_BASE_URL = "https://api.stackablelabs.io/app-extension/latest";
424
+ var fetchApps = async () => {
425
+ const baseURL = `${process.env.API_BASE_URL ?? API_BASE_URL}/apps`;
426
+ const res = await fetch(baseURL);
427
+ if (!res.ok) {
428
+ throw new Error(`Failed to fetch apps${baseURL !== API_BASE_URL ? ` (using ${baseURL})` : ""}: ${res.status} ${res.statusText}`);
429
+ }
430
+ return res.json();
431
+ };
432
+
433
+ // 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";
436
+ var WORDMARK = [
437
+ " _ _ _ _ ",
438
+ " ___| |_ __ _ ___| | ____ _| |__ | | ___",
439
+ "/ __| __/ _` |/ __| |/ / _` | '_ \\| |/ _ \\",
440
+ "\\__ \\ || (_| | (__| < (_| | |_) | | __/",
441
+ "|___/\\__\\__,_|\\___|_|\\_\\__,_|_.__/|_|\\___|"
442
+ ];
443
+ var COLORS = [
444
+ [232, 218, 234],
445
+ // Lit Lilac #E8DAEA
446
+ [197, 96, 255],
447
+ // Poppy Purple #C560FF
448
+ [0, 174, 247],
449
+ // Bluetooth Blue #00AEF7
450
+ [70, 224, 177],
451
+ // Tropical Teal #46E0B1
452
+ [252, 248, 161]
453
+ // Not Mellow Yellow #FCF8A1
454
+ ];
455
+ var lerp = (a, b, t) => {
456
+ const r = Math.round(a[0] + (b[0] - a[0]) * t);
457
+ const g = Math.round(a[1] + (b[1] - a[1]) * t);
458
+ const bl = Math.round(a[2] + (b[2] - a[2]) * t);
459
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${bl.toString(16).padStart(2, "0")}`;
460
+ };
461
+ var gradientColor = (row, col, rows, cols) => {
462
+ const t = (row / (rows - 1) + col / (cols - 1)) / 2;
463
+ const idx = t * (COLORS.length - 1);
464
+ const lo = Math.floor(idx);
465
+ const hi = Math.min(lo + 1, COLORS.length - 1);
466
+ return lerp(COLORS[lo], COLORS[hi], idx - lo);
467
+ };
468
+ var Banner = () => {
469
+ const termWidth = process.stdout.columns ?? 80;
470
+ 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)) })
365
474
  ] });
366
- }
475
+ };
476
+
477
+ // src/components/AppSelect.tsx
478
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
479
+ var AppSelect = ({ onSubmit }) => {
480
+ const [apps, setApps] = useState7([]);
481
+ const [loading, setLoading] = useState7(true);
482
+ const [error, setError] = useState7();
483
+ useEffect(() => {
484
+ fetchApps().then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
485
+ }, []);
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);
511
+ };
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 }) })
515
+ ] });
516
+ };
367
517
 
368
518
  // src/lib/postScaffold.ts
369
519
  import { execFile } from "child_process";
370
520
  import { promisify } from "util";
371
521
  import { installDependencies } from "nypm";
372
522
  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) {
523
+ var gitInit = async (dir) => {
382
524
  try {
383
525
  await execFileAsync("git", ["init"], { cwd: dir });
384
526
  await execFileAsync("git", ["add", "-A"], { cwd: dir });
385
527
  await execFileAsync("git", ["commit", "-m", "chore: initial scaffold"], { cwd: dir });
386
528
  } catch {
387
529
  }
388
- }
530
+ };
531
+ var postScaffold = async (options) => {
532
+ if (!options.skipGit) {
533
+ await gitInit(options.outputDir);
534
+ }
535
+ if (!options.skipInstall) {
536
+ await installDependencies({ cwd: options.outputDir, silent: true });
537
+ }
538
+ };
389
539
 
390
540
  // src/lib/scaffold.ts
391
- import { downloadTemplate } from "giget";
392
541
  import { readFile, readdir, rm, writeFile } from "fs/promises";
393
542
  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) {
543
+ 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
+ var normalizeTargets = (targets) => Array.from(new Set(targets));
557
+ var derivePermissions = (targets) => {
424
558
  const permissions = /* @__PURE__ */ new Set();
425
559
  for (const target of targets) {
426
- for (const permission of CAPABILITY_PERMISSION_MAP[target]) {
560
+ for (const permission of TARGET_PERMISSION_MAP[target] ?? DEFAULT_PERMISSIONS) {
427
561
  permissions.add(permission);
428
562
  }
429
563
  }
430
564
  if (permissions.size === 0) {
431
- for (const fallback of DEFAULT_PERMISSION_FALLBACK) {
565
+ for (const fallback of DEFAULT_PERMISSIONS) {
432
566
  permissions.add(fallback);
433
567
  }
434
568
  }
435
569
  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) {
570
+ };
571
+ var toKebabCase = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
572
+ var replacePlaceholders = async (rootDir, replacements) => {
441
573
  const files = await walkFiles(rootDir);
442
574
  for (const filePath of files) {
443
575
  if (!isTextFile(filePath)) {
@@ -455,8 +587,8 @@ async function replacePlaceholders(rootDir, replacements) {
455
587
  await writeFile(filePath, content);
456
588
  }
457
589
  }
458
- }
459
- async function generateManifest(rootDir, extensionName, targets, permissions) {
590
+ };
591
+ var generateManifest = async (rootDir, extensionName, targets, permissions) => {
460
592
  const manifestPath = join(rootDir, "packages/extension/public/manifest.json");
461
593
  const raw = await readFile(manifestPath, "utf8");
462
594
  const manifest = JSON.parse(raw);
@@ -465,8 +597,8 @@ async function generateManifest(rootDir, extensionName, targets, permissions) {
465
597
  manifest.permissions = permissions;
466
598
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
467
599
  `);
468
- }
469
- async function generateSurfaceFiles(rootDir, targets) {
600
+ };
601
+ var generateSurfaceFiles = async (rootDir, targets) => {
470
602
  const surfaceDir = join(rootDir, "packages/extension/src/surfaces");
471
603
  const wantsHeader = targets.includes("slot.header");
472
604
  const wantsContent = targets.includes("slot.content");
@@ -521,35 +653,20 @@ export function Content() {
521
653
  await upsertOrRemove(
522
654
  join(rootDir, "packages/extension/src/store.ts"),
523
655
  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
- `
656
+ "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
657
  );
537
658
  await upsertOrRemove(join(surfaceDir, "Footer.tsx"), wantsFooter, buildFooterSurface(targets));
538
- }
539
- function buildFooterSurface(targets) {
659
+ };
660
+ var buildFooterSurface = (targets) => {
540
661
  const blocks = [];
541
662
  if (targets.includes("slot.footer")) {
542
663
  blocks.push(
543
- ` <Surface id="slot.footer">
544
- <ui.Text className="text-xs">Powered by My Extension</ui.Text>
545
- </Surface>`
664
+ ' <Surface id="slot.footer">\n <ui.Text className="text-xs">Powered by My Extension</ui.Text>\n </Surface>'
546
665
  );
547
666
  }
548
667
  if (targets.includes("slot.footer-links")) {
549
668
  blocks.push(
550
- ` <Surface id="slot.footer-links">
551
- <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>
552
- </Surface>`
669
+ ' <Surface id="slot.footer-links">\n <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>\n </Surface>'
553
670
  );
554
671
  }
555
672
  return `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
@@ -562,8 +679,8 @@ ${blocks.join("\n")}
562
679
  )
563
680
  }
564
681
  `;
565
- }
566
- async function rewriteExtensionIndex(rootDir, extensionId, targets) {
682
+ };
683
+ var rewriteExtensionIndex = async (rootDir, extensionId, targets) => {
567
684
  const indexPath = join(rootDir, "packages/extension/src/index.tsx");
568
685
  const imports = ["import { createExtension } from '@stackable-labs/sdk-extension-react'"];
569
686
  const components = [];
@@ -587,12 +704,12 @@ createExtension(
587
704
  ${components.join("\n")}
588
705
  </>
589
706
  ),
590
- { extensionId: '${toKebabCase2(extensionId)}' },
707
+ { extensionId: '${toKebabCase(extensionId)}' },
591
708
  )
592
709
  `;
593
710
  await writeFile(indexPath, content);
594
- }
595
- async function rewritePreviewApp(rootDir, targets, permissions) {
711
+ };
712
+ var rewritePreviewApp = async (rootDir, targets, permissions) => {
596
713
  const appPath = join(rootDir, "packages/preview/src/App.tsx");
597
714
  const includeDataQuery = permissions.includes("data:query");
598
715
  const includeToast = permissions.includes("actions:toast");
@@ -607,20 +724,13 @@ async function rewritePreviewApp(rootDir, targets, permissions) {
607
724
  ].filter(Boolean);
608
725
  const handlers = [];
609
726
  if (includeDataQuery) {
610
- handlers.push(` 'data.query': async (_payload: ApiRequest) => {
611
- return mockData
612
- },`);
727
+ handlers.push(" 'data.query': async (_payload: ApiRequest) => {\n return mockData\n },");
613
728
  }
614
729
  if (includeToast) {
615
- handlers.push(` 'actions.toast': async (payload: ToastPayload) => {
616
- console.log('[Preview] toast:', payload)
617
- },`);
730
+ handlers.push(" 'actions.toast': async (payload: ToastPayload) => {\n console.log('[Preview] toast:', payload)\n },");
618
731
  }
619
732
  if (includeInvoke) {
620
- handlers.push(` 'actions.invoke': async (payload: ActionInvokePayload) => {
621
- console.log('[Preview] action invoke:', payload)
622
- return {}
623
- },`);
733
+ handlers.push(" 'actions.invoke': async (payload: ActionInvokePayload) => {\n console.log('[Preview] action invoke:', payload)\n return {}\n },");
624
734
  }
625
735
  if (includeContextRead) {
626
736
  handlers.push(" 'context.read': async () => mockContext,");
@@ -677,8 +787,8 @@ export default function App() {
677
787
  }
678
788
  `;
679
789
  await writeFile(appPath, appContent);
680
- }
681
- async function rewriteTurboJson(rootDir) {
790
+ };
791
+ var rewriteTurboJson = async (rootDir) => {
682
792
  const turboPath = join(rootDir, "turbo.json");
683
793
  const raw = await readFile(turboPath, "utf8");
684
794
  const turbo = JSON.parse(raw);
@@ -686,8 +796,8 @@ async function rewriteTurboJson(rootDir) {
686
796
  turbo["globalEnv"] = ["VITE_EXTENSION_PORT", "VITE_PREVIEW_PORT"];
687
797
  await writeFile(turboPath, `${JSON.stringify(turbo, null, 2)}
688
798
  `);
689
- }
690
- async function walkFiles(rootDir) {
799
+ };
800
+ var walkFiles = async (rootDir) => {
691
801
  const entries = await readdir(rootDir, { withFileTypes: true });
692
802
  const files = [];
693
803
  for (const entry of entries) {
@@ -702,63 +812,89 @@ async function walkFiles(rootDir) {
702
812
  files.push(fullPath);
703
813
  }
704
814
  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) {
815
+ };
816
+ var isTextFile = (filePath) => /\.(ts|tsx|js|jsx|json|md|html|yml|yaml|env|gitignore|nvmrc)$/i.test(filePath);
817
+ var writeEnvFile = async (dir, extensionPort, previewPort) => {
710
818
  const envPath = join(dir, ".env");
711
819
  const content = `VITE_EXTENSION_PORT=${extensionPort}
712
820
  VITE_PREVIEW_PORT=${previewPort}
713
821
  `;
714
822
  await writeFile(envPath, content);
715
- }
716
- async function upsertOrRemove(filePath, shouldExist, content) {
823
+ };
824
+ var upsertOrRemove = async (filePath, shouldExist, content) => {
717
825
  if (shouldExist) {
718
826
  await writeFile(filePath, content);
719
827
  return;
720
828
  }
721
829
  await rm(filePath, { force: true });
722
- }
830
+ };
831
+ var scaffold = async (options) => {
832
+ const { dir } = await downloadTemplate(TEMPLATE_SOURCE, {
833
+ dir: options.outputDir,
834
+ force: true
835
+ });
836
+ const selectedTargets = normalizeTargets(options.targets);
837
+ const derivedPermissions = derivePermissions(selectedTargets);
838
+ await replacePlaceholders(dir, {
839
+ "__EXTENSION_ID__": toKebabCase(options.extensionId || options.name),
840
+ "__EXTENSION_DISPLAY_NAME__": options.name,
841
+ "replace-with-extension-name": toKebabCase(options.name),
842
+ "replace-with-extension-package-name": `@agnostack/extensions-${toKebabCase(options.extensionId || options.name)}`
843
+ });
844
+ await generateManifest(dir, options.name, selectedTargets, derivedPermissions);
845
+ await generateSurfaceFiles(dir, selectedTargets);
846
+ await rewriteExtensionIndex(dir, options.extensionId || options.name, selectedTargets);
847
+ await rewritePreviewApp(dir, selectedTargets, derivedPermissions);
848
+ await rewriteTurboJson(dir);
849
+ await writeEnvFile(dir, options.extensionPort, options.previewPort);
850
+ return options;
851
+ };
723
852
 
724
853
  // src/App.tsx
725
- import { join as join2 } from "path";
726
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
854
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
855
+ var STEP_ORDER = ["app", "name", "targets", "ports", "dir", "confirm"];
727
856
  var INITIAL_STEPS = [
728
857
  { label: "Fetching template", status: "pending" },
729
858
  { label: "Generating files", status: "pending" },
730
859
  { label: "Installing dependencies", status: "pending" }
731
860
  ];
732
- function toKebabCase3(value) {
733
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
734
- }
735
- function App({ initialName, options }) {
861
+ var toKebabCase2 = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
862
+ var App = ({ initialName, options }) => {
736
863
  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(
864
+ const [step, setStep] = useState8("app");
865
+ const [name, setName] = useState8(initialName ?? "");
866
+ const [extensionId, setExtensionId] = useState8("");
867
+ const [selectedApp, setSelectedApp] = useState8(null);
868
+ const [extensionPort, setExtensionPort] = useState8(
741
869
  options?.extensionPort ? parseInt(options.extensionPort, 10) : 5173
742
870
  );
743
- const [previewPort, setPreviewPort] = useState6(
871
+ const [previewPort, setPreviewPort] = useState8(
744
872
  options?.previewPort ? parseInt(options.previewPort, 10) : 5174
745
873
  );
746
- const [targets, setTargets] = useState6([]);
747
- const [outputDir, setOutputDir] = useState6("");
748
- const [progressSteps, setProgressSteps] = useState6(INITIAL_STEPS);
749
- const [errorMessage, setErrorMessage] = useState6();
874
+ const [targets, setTargets] = useState8([]);
875
+ const [outputDir, setOutputDir] = useState8("");
876
+ const [progressSteps, setProgressSteps] = useState8(INITIAL_STEPS);
877
+ const [errorMessage, setErrorMessage] = useState8();
750
878
  const updateStep = useCallback((index, status) => {
751
- setProgressSteps(
752
- (prev) => prev.map((s, i) => i === index ? { ...s, status } : s)
753
- );
879
+ setProgressSteps((prev) => prev.map((s, i) => i === index ? { ...s, status } : s));
754
880
  }, []);
881
+ 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
+ setStep((prev) => {
887
+ const idx = activeSteps.indexOf(prev);
888
+ return idx > 0 ? activeSteps[idx - 1] : prev;
889
+ });
890
+ }, [initialName, options?.extensionPort, options?.previewPort]);
891
+ const handleAppSelect = (app) => {
892
+ setSelectedApp(app);
893
+ setStep(initialName ? "targets" : "name");
894
+ };
755
895
  const handleName = (value) => {
756
896
  setName(value);
757
- setExtensionId(toKebabCase3(value));
758
- setStep("id");
759
- };
760
- const handleId = (value) => {
761
- setExtensionId(value);
897
+ setExtensionId(toKebabCase2(value));
762
898
  setStep("targets");
763
899
  };
764
900
  const handleTargets = (value) => {
@@ -784,6 +920,7 @@ function App({ initialName, options }) {
784
920
  try {
785
921
  updateStep(0, "running");
786
922
  await scaffold({
923
+ appId: selectedApp.id,
787
924
  name,
788
925
  extensionId,
789
926
  targets,
@@ -812,51 +949,82 @@ function App({ initialName, options }) {
812
949
  const handleCancel = () => {
813
950
  exit();
814
951
  };
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 });
952
+ switch (step) {
953
+ case "app": {
954
+ return /* @__PURE__ */ jsx12(AppSelect, { onSubmit: handleAppSelect });
955
+ }
956
+ case "name": {
957
+ return /* @__PURE__ */ jsx12(
958
+ NamePrompt,
959
+ {
960
+ initialValue: name,
961
+ onSubmit: handleName,
962
+ onBack: goBack
963
+ }
964
+ );
965
+ }
966
+ case "targets": {
967
+ return /* @__PURE__ */ jsx12(
968
+ TargetSelect,
969
+ {
970
+ availableTargets: selectedApp?.targets ?? [],
971
+ onSubmit: handleTargets,
972
+ onBack: goBack
973
+ }
974
+ );
975
+ }
976
+ case "ports": {
977
+ return /* @__PURE__ */ jsx12(
978
+ PortsPrompt,
979
+ {
980
+ onSubmit: handlePorts,
981
+ onBack: goBack
982
+ }
983
+ );
984
+ }
985
+ case "dir": {
986
+ return /* @__PURE__ */ jsx12(
987
+ DirPrompt,
988
+ {
989
+ defaultDir: join2(process.cwd(), toKebabCase2(name)),
990
+ onSubmit: handleDir,
991
+ onBack: goBack
992
+ }
993
+ );
994
+ }
995
+ case "confirm": {
996
+ return /* @__PURE__ */ jsx12(
997
+ Confirm,
998
+ {
999
+ name,
1000
+ extensionPort,
1001
+ previewPort,
1002
+ targets,
1003
+ outputDir,
1004
+ onConfirm: handleConfirm,
1005
+ onCancel: handleCancel,
1006
+ onBack: goBack
1007
+ }
1008
+ );
1009
+ }
1010
+ case "scaffolding": {
1011
+ return /* @__PURE__ */ jsx12(ScaffoldProgress, { steps: progressSteps });
1012
+ }
1013
+ case "done": {
1014
+ return /* @__PURE__ */ jsx12(Done, { name, outputDir });
1015
+ }
1016
+ default: {
1017
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", gap: 1, children: [
1018
+ /* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "Scaffold failed" }),
1019
+ errorMessage && /* @__PURE__ */ jsx12(Text12, { color: "red", children: errorMessage })
1020
+ ] });
1021
+ }
850
1022
  }
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
- }
1023
+ };
856
1024
 
857
1025
  // 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 }));
1026
+ 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 }));
861
1029
  });
862
- program.parse();
1030
+ program.parse(process.argv.filter((arg) => arg !== "--"));