@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.
- package/README.md +20 -7
- package/dist/index.js +803 -431
- 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 {
|
|
9
|
-
import {
|
|
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 {
|
|
13
|
-
|
|
14
|
-
|
|
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"
|
|
66
|
+
if (input === "n") {
|
|
21
67
|
onCancel();
|
|
22
68
|
}
|
|
23
69
|
});
|
|
24
|
-
return /* @__PURE__ */
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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__ */
|
|
32
|
-
/* @__PURE__ */
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/* @__PURE__ */
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
/* @__PURE__ */
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/* @__PURE__ */
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
/* @__PURE__ */
|
|
49
|
-
|
|
50
|
-
/* @__PURE__ */
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
113
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
66
114
|
import TextInput from "ink-text-input";
|
|
67
|
-
import { useState } from "react";
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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__ */
|
|
79
|
-
/* @__PURE__ */
|
|
80
|
-
/* @__PURE__ */
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
90
|
-
import { jsx as
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
/* @__PURE__ */
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/* @__PURE__ */
|
|
98
|
-
|
|
99
|
-
|
|
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__ */
|
|
108
|
-
|
|
109
|
-
/* @__PURE__ */
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
/* @__PURE__ */
|
|
118
|
-
/* @__PURE__ */
|
|
119
|
-
/* @__PURE__ */
|
|
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
|
|
167
|
-
import
|
|
168
|
-
import { useState as
|
|
169
|
-
import { jsx as
|
|
170
|
-
|
|
171
|
-
const [value, setValue] =
|
|
172
|
-
const [error, setError] =
|
|
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__ */
|
|
183
|
-
/* @__PURE__ */
|
|
184
|
-
/* @__PURE__ */
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
194
|
-
import
|
|
195
|
-
import { useState as
|
|
196
|
-
import { jsx as
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const [
|
|
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 === "" ?
|
|
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__ */
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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:
|
|
229
|
-
onChange:
|
|
230
|
-
onSubmit:
|
|
231
|
-
placeholder:
|
|
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
|
-
|
|
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
|
|
306
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
262
307
|
import Spinner from "ink-spinner";
|
|
263
|
-
import { jsx as
|
|
264
|
-
|
|
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__ */
|
|
312
|
+
return /* @__PURE__ */ jsx8(Spinner, { type: "dots" });
|
|
268
313
|
case "done":
|
|
269
|
-
return /* @__PURE__ */
|
|
314
|
+
return /* @__PURE__ */ jsx8(Text8, { color: "green", children: "\u2714" });
|
|
270
315
|
case "error":
|
|
271
|
-
return /* @__PURE__ */
|
|
316
|
+
return /* @__PURE__ */ jsx8(Text8, { color: "red", children: "\u2716" });
|
|
272
317
|
default:
|
|
273
|
-
return /* @__PURE__ */
|
|
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
|
|
304
|
-
|
|
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 {
|
|
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
|
-
|
|
315
|
-
const [cursor, setCursor] =
|
|
316
|
-
const [
|
|
317
|
-
const [
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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__ */
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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__ */
|
|
356
|
-
/* @__PURE__ */
|
|
357
|
-
/* @__PURE__ */
|
|
358
|
-
/* @__PURE__ */
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
] },
|
|
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
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
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
|
|
670
|
+
for (const fallback of DEFAULT_PERMISSIONS) {
|
|
432
671
|
permissions.add(fallback);
|
|
433
672
|
}
|
|
434
673
|
}
|
|
435
674
|
return [...permissions];
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
764
|
+
};
|
|
765
|
+
var buildFooterSurface = (targets) => {
|
|
540
766
|
const blocks = [];
|
|
541
767
|
if (targets.includes("slot.footer")) {
|
|
542
768
|
blocks.push(
|
|
543
|
-
|
|
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
|
-
|
|
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
|
|
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: '${
|
|
812
|
+
{ extensionId: '${toKebabCase(extensionId)}' },
|
|
591
813
|
)
|
|
592
814
|
`;
|
|
593
815
|
await writeFile(indexPath, content);
|
|
594
|
-
}
|
|
595
|
-
async
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
707
|
-
|
|
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
|
|
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 {
|
|
726
|
-
|
|
727
|
-
var
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
|
|
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] =
|
|
738
|
-
const [name, setName] =
|
|
739
|
-
const [extensionId, setExtensionId] =
|
|
740
|
-
const [
|
|
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] =
|
|
1000
|
+
const [previewPort, setPreviewPort] = useState9(
|
|
744
1001
|
options?.previewPort ? parseInt(options.previewPort, 10) : 5174
|
|
745
1002
|
);
|
|
746
|
-
const [targets, setTargets] =
|
|
747
|
-
const [outputDir, setOutputDir] =
|
|
748
|
-
const [progressSteps, setProgressSteps] =
|
|
749
|
-
const [errorMessage, setErrorMessage] =
|
|
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
|
|
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
|
-
|
|
758
|
-
setStep("id");
|
|
1040
|
+
setStep("confirmTargets");
|
|
759
1041
|
};
|
|
760
|
-
const
|
|
761
|
-
|
|
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
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
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
|
|
859
|
-
program.name("
|
|
860
|
-
|
|
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.
|
|
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
|
+
}
|