@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.
- package/README.md +20 -7
- package/dist/index.js +586 -418
- 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 {
|
|
9
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
/* @__PURE__ */
|
|
32
|
-
|
|
33
|
-
/* @__PURE__ */
|
|
34
|
-
|
|
35
|
-
|
|
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__ */
|
|
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, 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
|
-
|
|
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,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
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
/* @__PURE__ */
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
/* @__PURE__ */
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
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
|
|
565
|
+
for (const fallback of DEFAULT_PERMISSIONS) {
|
|
432
566
|
permissions.add(fallback);
|
|
433
567
|
}
|
|
434
568
|
}
|
|
435
569
|
return [...permissions];
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
659
|
+
};
|
|
660
|
+
var buildFooterSurface = (targets) => {
|
|
540
661
|
const blocks = [];
|
|
541
662
|
if (targets.includes("slot.footer")) {
|
|
542
663
|
blocks.push(
|
|
543
|
-
|
|
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
|
-
|
|
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
|
|
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: '${
|
|
707
|
+
{ extensionId: '${toKebabCase(extensionId)}' },
|
|
591
708
|
)
|
|
592
709
|
`;
|
|
593
710
|
await writeFile(indexPath, content);
|
|
594
|
-
}
|
|
595
|
-
async
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
707
|
-
|
|
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
|
|
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 {
|
|
726
|
-
|
|
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
|
-
|
|
733
|
-
|
|
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] =
|
|
738
|
-
const [name, setName] =
|
|
739
|
-
const [extensionId, setExtensionId] =
|
|
740
|
-
const [
|
|
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] =
|
|
871
|
+
const [previewPort, setPreviewPort] = useState8(
|
|
744
872
|
options?.previewPort ? parseInt(options.previewPort, 10) : 5174
|
|
745
873
|
);
|
|
746
|
-
const [targets, setTargets] =
|
|
747
|
-
const [outputDir, setOutputDir] =
|
|
748
|
-
const [progressSteps, setProgressSteps] =
|
|
749
|
-
const [errorMessage, setErrorMessage] =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
859
|
-
program.name("create-extension").description("Scaffold a new Stackable extension project").argument("[name]", "Extension project name").option("--
|
|
860
|
-
render(/* @__PURE__ */
|
|
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 !== "--"));
|