@webflow/webflow-cli 1.21.0 → 1.22.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cloud-scaffolds/__tests__/fetcher.test.ts +249 -0
  2. package/dist/cloud-scaffolds/__tests__/registry.test.ts +134 -0
  3. package/dist/cloud-scaffolds/fetcher.ts +205 -0
  4. package/dist/cloud-scaffolds/index.ts +19 -0
  5. package/dist/cloud-scaffolds/registry.ts +132 -0
  6. package/dist/cloud-scaffolds/types.ts +68 -0
  7. package/dist/index.js +98 -82
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/dist/cloud-scaffolds/astro/README.md +0 -76
  11. package/dist/cloud-scaffolds/astro/astro.config.mjs +0 -27
  12. package/dist/cloud-scaffolds/astro/gitignore +0 -31
  13. package/dist/cloud-scaffolds/astro/package.json +0 -26
  14. package/dist/cloud-scaffolds/astro/public/favicon.svg +0 -9
  15. package/dist/cloud-scaffolds/astro/src/assets/astro.svg +0 -1
  16. package/dist/cloud-scaffolds/astro/src/assets/background.svg +0 -1
  17. package/dist/cloud-scaffolds/astro/src/components/Welcome.astro +0 -210
  18. package/dist/cloud-scaffolds/astro/src/env.d.ts +0 -12
  19. package/dist/cloud-scaffolds/astro/src/layouts/Layout.astro +0 -32
  20. package/dist/cloud-scaffolds/astro/src/pages/index.astro +0 -73
  21. package/dist/cloud-scaffolds/astro/tsconfig.json +0 -17
  22. package/dist/cloud-scaffolds/astro/webflow.json +0 -13
  23. package/dist/cloud-scaffolds/astro/worker-configuration.d.ts +0 -4
  24. package/dist/cloud-scaffolds/astro/wrangler.json +0 -14
  25. package/dist/cloud-scaffolds/nextjs/README.md +0 -47
  26. package/dist/cloud-scaffolds/nextjs/cloudflare-env.d.ts +0 -5
  27. package/dist/cloud-scaffolds/nextjs/eslint.config.mjs +0 -22
  28. package/dist/cloud-scaffolds/nextjs/gitignore +0 -47
  29. package/dist/cloud-scaffolds/nextjs/next.config.ts +0 -10
  30. package/dist/cloud-scaffolds/nextjs/open-next.config.ts +0 -9
  31. package/dist/cloud-scaffolds/nextjs/package.json +0 -34
  32. package/dist/cloud-scaffolds/nextjs/postcss.config.mjs +0 -5
  33. package/dist/cloud-scaffolds/nextjs/public/file.svg +0 -1
  34. package/dist/cloud-scaffolds/nextjs/public/globe.svg +0 -1
  35. package/dist/cloud-scaffolds/nextjs/public/next.svg +0 -1
  36. package/dist/cloud-scaffolds/nextjs/public/window.svg +0 -1
  37. package/dist/cloud-scaffolds/nextjs/src/app/favicon.ico +0 -0
  38. package/dist/cloud-scaffolds/nextjs/src/app/globals.css +0 -31
  39. package/dist/cloud-scaffolds/nextjs/src/app/layout.tsx +0 -44
  40. package/dist/cloud-scaffolds/nextjs/src/app/page.css +0 -45
  41. package/dist/cloud-scaffolds/nextjs/src/app/page.tsx +0 -30
  42. package/dist/cloud-scaffolds/nextjs/src/webflow.d.ts +0 -14
  43. package/dist/cloud-scaffolds/nextjs/tsconfig.json +0 -30
  44. package/dist/cloud-scaffolds/nextjs/webflow.json +0 -13
  45. package/dist/cloud-scaffolds/nextjs/wrangler.json +0 -14
@@ -0,0 +1,249 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import os from "os";
4
+ import * as tar from "tar";
5
+ import {
6
+ fetchScaffoldContent,
7
+ getScaffoldContent,
8
+ getScaffoldFromLocal,
9
+ } from "../fetcher";
10
+ import * as registry from "../registry";
11
+ import { ScaffoldError } from "../types";
12
+
13
+ jest.mock("../registry");
14
+ jest.mock("../../../utils/logger", () => ({
15
+ __esModule: true,
16
+ default: {
17
+ spinner: jest.fn(() => ({
18
+ succeed: jest.fn(),
19
+ fail: jest.fn(),
20
+ warn: jest.fn(),
21
+ info: jest.fn(),
22
+ })),
23
+ },
24
+ }));
25
+
26
+ const mockGetFrameworkDefinition =
27
+ registry.getFrameworkDefinition as jest.MockedFunction<
28
+ typeof registry.getFrameworkDefinition
29
+ >;
30
+ const mockToMinimalScaffoldId =
31
+ registry.toMinimalScaffoldId as jest.MockedFunction<
32
+ typeof registry.toMinimalScaffoldId
33
+ >;
34
+
35
+ describe("scaffold fetcher", () => {
36
+ let tempDir: string;
37
+
38
+ beforeEach(() => {
39
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "fetcher-test-"));
40
+ jest.clearAllMocks();
41
+ });
42
+
43
+ afterEach(() => {
44
+ fs.rmSync(tempDir, { recursive: true, force: true });
45
+ });
46
+
47
+ describe("getScaffoldFromLocal", () => {
48
+ it("returns ScaffoldContent from local directory", async () => {
49
+ const scaffoldDir = path.join(tempDir, "scaffold");
50
+ fs.mkdirSync(scaffoldDir, { recursive: true });
51
+ fs.writeFileSync(path.join(scaffoldDir, "package.json"), "{}");
52
+ fs.mkdirSync(path.join(scaffoldDir, "src"), { recursive: true });
53
+ fs.writeFileSync(path.join(scaffoldDir, "src", "index.ts"), "// test");
54
+
55
+ mockGetFrameworkDefinition.mockReturnValue({
56
+ id: "test-minimal",
57
+ name: "Test",
58
+ source: {
59
+ type: "local",
60
+ path: path.resolve(scaffoldDir),
61
+ },
62
+ });
63
+
64
+ const result = await getScaffoldFromLocal("test-minimal");
65
+
66
+ expect(result.scaffoldId).toBe("test-minimal");
67
+ expect(result.extractionRoot).toBe(scaffoldDir);
68
+ expect(result.files).toHaveLength(2);
69
+ expect(result.files).toContain(path.join(scaffoldDir, "package.json"));
70
+ expect(result.files).toContain(path.join(scaffoldDir, "src", "index.ts"));
71
+ });
72
+
73
+ it("throws ScaffoldError when source is not local", async () => {
74
+ mockGetFrameworkDefinition.mockReturnValue({
75
+ id: "astro-minimal",
76
+ name: "Astro",
77
+ source: {
78
+ type: "github",
79
+ repo: "Webflow-Examples/hello-world-astro",
80
+ ref: "main",
81
+ },
82
+ });
83
+
84
+ await expect(getScaffoldFromLocal("astro-minimal")).rejects.toThrow(
85
+ ScaffoldError
86
+ );
87
+ await expect(getScaffoldFromLocal("astro-minimal")).rejects.toThrow(
88
+ "does not have a local source"
89
+ );
90
+ });
91
+
92
+ it("throws ScaffoldError when directory does not exist", async () => {
93
+ const missingPath = path.resolve(path.join(tempDir, "nonexistent"));
94
+
95
+ mockGetFrameworkDefinition.mockReturnValue({
96
+ id: "test-minimal",
97
+ name: "Test",
98
+ source: { type: "local", path: missingPath },
99
+ });
100
+
101
+ await expect(getScaffoldFromLocal("test-minimal")).rejects.toThrow(
102
+ ScaffoldError
103
+ );
104
+ await expect(getScaffoldFromLocal("test-minimal")).rejects.toThrow(
105
+ "Scaffold directory not found"
106
+ );
107
+ });
108
+ });
109
+
110
+ describe("fetchScaffoldContent", () => {
111
+ it("dispatches to getScaffoldFromLocal for local source", async () => {
112
+ const scaffoldDir = path.join(tempDir, "scaffold");
113
+ fs.mkdirSync(scaffoldDir, { recursive: true });
114
+ fs.writeFileSync(path.join(scaffoldDir, "package.json"), "{}");
115
+
116
+ mockGetFrameworkDefinition.mockReturnValue({
117
+ id: "test-minimal",
118
+ name: "Test",
119
+ source: { type: "local", path: path.resolve(scaffoldDir) },
120
+ });
121
+
122
+ const result = await fetchScaffoldContent("test-minimal");
123
+
124
+ expect(result.scaffoldId).toBe("test-minimal");
125
+ expect(result.files).toHaveLength(1);
126
+ });
127
+
128
+ it("fetches from GitHub and returns ScaffoldContent", async () => {
129
+ // Create a minimal tarball matching GitHub structure: owner-repo-sha/
130
+ const topLevelDir = "Webflow-Examples-hello-world-astro-abc123";
131
+ const tarballDir = path.join(tempDir, "tarball-src");
132
+ fs.mkdirSync(path.join(tarballDir, topLevelDir), { recursive: true });
133
+ fs.writeFileSync(
134
+ path.join(tarballDir, topLevelDir, "package.json"),
135
+ '{"name":"test"}'
136
+ );
137
+
138
+ const tarballPath = path.join(tempDir, "archive.tar.gz");
139
+ await tar.create(
140
+ {
141
+ gzip: true,
142
+ file: tarballPath,
143
+ cwd: tarballDir,
144
+ },
145
+ [topLevelDir]
146
+ );
147
+ const tarballBuffer = fs.readFileSync(tarballPath);
148
+
149
+ const originalFetch = globalThis.fetch;
150
+ const arrayBuffer = tarballBuffer.buffer.slice(
151
+ tarballBuffer.byteOffset,
152
+ tarballBuffer.byteOffset + tarballBuffer.byteLength
153
+ );
154
+ globalThis.fetch = jest.fn().mockResolvedValue({
155
+ ok: true,
156
+ arrayBuffer: () => Promise.resolve(arrayBuffer),
157
+ });
158
+
159
+ mockGetFrameworkDefinition.mockReturnValue({
160
+ id: "astro-minimal",
161
+ name: "Astro",
162
+ source: {
163
+ type: "github",
164
+ repo: "Webflow-Examples/hello-world-astro",
165
+ ref: "main",
166
+ },
167
+ });
168
+
169
+ try {
170
+ const result = await fetchScaffoldContent("astro-minimal");
171
+
172
+ expect(result.scaffoldId).toBe("astro-minimal");
173
+ expect(result.files).toHaveLength(1);
174
+ expect(result.files[0]).toContain("package.json");
175
+ expect(result.extractionRoot).toContain(topLevelDir);
176
+ } finally {
177
+ globalThis.fetch = originalFetch;
178
+ }
179
+ });
180
+
181
+ it("throws ScaffoldError when GitHub fetch fails", async () => {
182
+ const originalFetch = globalThis.fetch;
183
+ globalThis.fetch = jest.fn().mockResolvedValue({
184
+ ok: false,
185
+ status: 404,
186
+ statusText: "Not Found",
187
+ });
188
+
189
+ mockGetFrameworkDefinition.mockReturnValue({
190
+ id: "astro-minimal",
191
+ name: "Astro",
192
+ source: {
193
+ type: "github",
194
+ repo: "Webflow-Examples/hello-world-astro",
195
+ ref: "main",
196
+ },
197
+ });
198
+
199
+ try {
200
+ await expect(fetchScaffoldContent("astro-minimal")).rejects.toThrow(
201
+ ScaffoldError
202
+ );
203
+ await expect(fetchScaffoldContent("astro-minimal")).rejects.toThrow(
204
+ "Failed to fetch scaffold from GitHub"
205
+ );
206
+ } finally {
207
+ globalThis.fetch = originalFetch;
208
+ }
209
+ });
210
+ });
211
+
212
+ describe("getScaffoldContent", () => {
213
+ it("resolves framework name to minimal ID and fetches", async () => {
214
+ const scaffoldDir = path.join(tempDir, "scaffold");
215
+ fs.mkdirSync(scaffoldDir, { recursive: true });
216
+ fs.writeFileSync(path.join(scaffoldDir, "package.json"), "{}");
217
+
218
+ mockToMinimalScaffoldId.mockReturnValue("astro-minimal");
219
+ mockGetFrameworkDefinition.mockReturnValue({
220
+ id: "astro-minimal",
221
+ name: "Astro",
222
+ source: { type: "local", path: path.resolve(scaffoldDir) },
223
+ });
224
+
225
+ const result = await getScaffoldContent("astro");
226
+
227
+ expect(mockToMinimalScaffoldId).toHaveBeenCalledWith("astro");
228
+ expect(result.scaffoldId).toBe("astro-minimal");
229
+ expect(result.files).toHaveLength(1);
230
+ });
231
+
232
+ it("passes through full scaffold ID unchanged", async () => {
233
+ const scaffoldDir = path.join(tempDir, "scaffold");
234
+ fs.mkdirSync(scaffoldDir, { recursive: true });
235
+ fs.writeFileSync(path.join(scaffoldDir, "package.json"), "{}");
236
+
237
+ mockGetFrameworkDefinition.mockReturnValue({
238
+ id: "astro-minimal",
239
+ name: "Astro",
240
+ source: { type: "local", path: path.resolve(scaffoldDir) },
241
+ });
242
+
243
+ const result = await getScaffoldContent("astro-minimal");
244
+
245
+ expect(mockToMinimalScaffoldId).not.toHaveBeenCalled();
246
+ expect(result.scaffoldId).toBe("astro-minimal");
247
+ });
248
+ });
249
+ });
@@ -0,0 +1,134 @@
1
+ import {
2
+ FRAMEWORK_REGISTRY,
3
+ getAvailableFrameworkNames,
4
+ getFrameworkDefinition,
5
+ listSupportedScaffoldIds,
6
+ toMinimalScaffoldId,
7
+ } from "../registry";
8
+ import { ScaffoldError } from "../types";
9
+
10
+ describe("scaffold registry", () => {
11
+ describe("getFrameworkDefinition", () => {
12
+ it("returns definition for astro-minimal", () => {
13
+ const def = getFrameworkDefinition("astro-minimal");
14
+ expect(def.id).toBe("astro-minimal");
15
+ expect(def.name).toBe("Astro");
16
+ expect(def.source.type).toBe("github");
17
+ if (def.source.type === "github") {
18
+ expect(def.source.repo).toBe("Webflow-Examples/hello-world-astro");
19
+ expect(def.source.ref).toBe("v1");
20
+ }
21
+ });
22
+
23
+ it("returns definition for nextjs-minimal", () => {
24
+ const def = getFrameworkDefinition("nextjs-minimal");
25
+ expect(def.id).toBe("nextjs-minimal");
26
+ expect(def.name).toBe("Next.js");
27
+ expect(def.source.type).toBe("github");
28
+ if (def.source.type === "github") {
29
+ expect(def.source.repo).toBe("Webflow-Examples/hello-world-nextjs");
30
+ }
31
+ });
32
+
33
+ it("returns definition for astro (site-attached)", () => {
34
+ const def = getFrameworkDefinition("astro");
35
+ expect(def.id).toBe("astro");
36
+ expect(def.name).toBe("Astro (site-attached)");
37
+ expect(def.source.type).toBe("github");
38
+ if (def.source.type === "github") {
39
+ expect(def.source.repo).toBe(
40
+ "Webflow-Examples/hello-world-astro-devlink"
41
+ );
42
+ }
43
+ });
44
+
45
+ it("returns definition for nextjs (site-attached)", () => {
46
+ const def = getFrameworkDefinition("nextjs");
47
+ expect(def.id).toBe("nextjs");
48
+ expect(def.name).toBe("Next.js (site-attached)");
49
+ expect(def.source.type).toBe("github");
50
+ if (def.source.type === "github") {
51
+ expect(def.source.repo).toBe(
52
+ "Webflow-Examples/hello-world-nextjs-devlink"
53
+ );
54
+ }
55
+ });
56
+
57
+ it("throws ScaffoldError for unsupported scaffold ID", () => {
58
+ expect(() => getFrameworkDefinition("remix-minimal")).toThrow(
59
+ ScaffoldError
60
+ );
61
+ expect(() => getFrameworkDefinition("remix-minimal")).toThrow(
62
+ 'Unsupported framework: "remix-minimal"'
63
+ );
64
+ });
65
+
66
+ it("includes supported IDs in ScaffoldError", () => {
67
+ try {
68
+ getFrameworkDefinition("unknown");
69
+ fail("Expected ScaffoldError");
70
+ } catch (err) {
71
+ expect(err).toBeInstanceOf(ScaffoldError);
72
+ expect((err as ScaffoldError).scaffoldId).toBe("unknown");
73
+ expect((err as ScaffoldError).supportedIds).toContain("astro-minimal");
74
+ expect((err as ScaffoldError).supportedIds).toContain("nextjs-minimal");
75
+ expect((err as ScaffoldError).supportedIds).toContain("astro");
76
+ expect((err as ScaffoldError).supportedIds).toContain("nextjs");
77
+ }
78
+ });
79
+ });
80
+
81
+ describe("listSupportedScaffoldIds", () => {
82
+ it("returns all registry keys", () => {
83
+ const ids = listSupportedScaffoldIds();
84
+ expect(ids).toEqual(
85
+ expect.arrayContaining([
86
+ "astro-minimal",
87
+ "nextjs-minimal",
88
+ "astro",
89
+ "nextjs",
90
+ ])
91
+ );
92
+ expect(ids).toHaveLength(Object.keys(FRAMEWORK_REGISTRY).length);
93
+ });
94
+ });
95
+
96
+ describe("getAvailableFrameworkNames", () => {
97
+ it("returns framework names without -minimal suffix", () => {
98
+ const names = getAvailableFrameworkNames();
99
+ expect(names).toEqual(expect.arrayContaining(["astro", "nextjs"]));
100
+ expect(names).toHaveLength(2);
101
+ });
102
+ });
103
+
104
+ describe("toMinimalScaffoldId", () => {
105
+ it("maps astro to astro-minimal", () => {
106
+ expect(toMinimalScaffoldId("astro")).toBe("astro-minimal");
107
+ });
108
+
109
+ it("maps nextjs to nextjs-minimal", () => {
110
+ expect(toMinimalScaffoldId("nextjs")).toBe("nextjs-minimal");
111
+ });
112
+
113
+ it("passes through astro-minimal unchanged", () => {
114
+ expect(toMinimalScaffoldId("astro-minimal")).toBe("astro-minimal");
115
+ });
116
+
117
+ it("throws ScaffoldError for unsupported framework", () => {
118
+ expect(() => toMinimalScaffoldId("remix")).toThrow(ScaffoldError);
119
+ expect(() => toMinimalScaffoldId("remix")).toThrow(
120
+ 'No minimal scaffold for "remix"'
121
+ );
122
+ });
123
+
124
+ it("includes supported IDs in ScaffoldError", () => {
125
+ try {
126
+ toMinimalScaffoldId("vue");
127
+ fail("Expected ScaffoldError");
128
+ } catch (err) {
129
+ expect(err).toBeInstanceOf(ScaffoldError);
130
+ expect((err as ScaffoldError).supportedIds).toBeDefined();
131
+ }
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,205 @@
1
+ import * as fs from "fs";
2
+ import fsExtra from "fs-extra";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import * as tar from "tar";
6
+ import { getFrameworkDefinition, toMinimalScaffoldId } from "./registry";
7
+ import type { ScaffoldContent } from "./types";
8
+ import { ScaffoldError } from "./types";
9
+ import logger from "../../utils/logger";
10
+
11
+ const GITHUB_TARBALL_BASE = "https://github.com";
12
+
13
+ /**
14
+ * Get scaffold content for a framework.
15
+ * Resolves the scaffold ID (e.g. "astro" -> "astro-minimal") and fetches.
16
+ *
17
+ * @param frameworkOrScaffoldId - "astro", "nextjs", or full "astro-minimal", "nextjs-minimal"
18
+ * @returns ScaffoldContent ready for applyScaffold
19
+ */
20
+ export async function getScaffoldContent(
21
+ frameworkOrScaffoldId: string
22
+ ): Promise<ScaffoldContent> {
23
+ const scaffoldId = frameworkOrScaffoldId.endsWith("-minimal")
24
+ ? frameworkOrScaffoldId
25
+ : toMinimalScaffoldId(frameworkOrScaffoldId);
26
+ return fetchScaffoldContent(scaffoldId);
27
+ }
28
+
29
+ /**
30
+ * Get scaffold content from the configured source (github or local).
31
+ * Dispatches to the appropriate fetcher based on source.type.
32
+ */
33
+ export async function fetchScaffoldContent(
34
+ scaffoldId: string
35
+ ): Promise<ScaffoldContent> {
36
+ const def = getFrameworkDefinition(scaffoldId);
37
+
38
+ if (def.source.type === "github") {
39
+ return fetchScaffoldFromGitHub(scaffoldId);
40
+ }
41
+ return getScaffoldFromLocal(scaffoldId);
42
+ }
43
+
44
+ /**
45
+ * Get scaffold content from local filesystem.
46
+ */
47
+ export async function getScaffoldFromLocal(
48
+ scaffoldId: string
49
+ ): Promise<ScaffoldContent> {
50
+ const def = getFrameworkDefinition(scaffoldId);
51
+
52
+ if (def.source.type !== "local") {
53
+ throw new ScaffoldError(
54
+ `Scaffold "${scaffoldId}" does not have a local source`,
55
+ scaffoldId
56
+ );
57
+ }
58
+
59
+ // path is relative to dist/ (e.g. "cloud-scaffolds/astro-minimal") or absolute
60
+ const scaffoldPath = path.isAbsolute(def.source.path)
61
+ ? def.source.path
62
+ : path.join(__dirname, def.source.path);
63
+ if (!fs.existsSync(scaffoldPath)) {
64
+ throw new ScaffoldError(
65
+ `Scaffold directory not found: ${scaffoldPath}`,
66
+ scaffoldId
67
+ );
68
+ }
69
+
70
+ const files = await listFilesRecursive(scaffoldPath);
71
+ return { scaffoldId, files, extractionRoot: scaffoldPath };
72
+ }
73
+
74
+ /**
75
+ * Download and extract a scaffold from GitHub.
76
+ * Uses the tarball API; no git dependency.
77
+ */
78
+ async function fetchScaffoldFromGitHub(
79
+ scaffoldId: string
80
+ ): Promise<ScaffoldContent> {
81
+ const def = getFrameworkDefinition(scaffoldId);
82
+
83
+ if (def.source.type !== "github") {
84
+ throw new ScaffoldError(
85
+ `Scaffold "${scaffoldId}" does not have a GitHub source`,
86
+ scaffoldId
87
+ );
88
+ }
89
+
90
+ const ref = def.source.ref ?? "main";
91
+ const tarballUrl = `${GITHUB_TARBALL_BASE}/${def.source.repo}/archive/${ref}.tar.gz`;
92
+
93
+ const extractDir = await fs.promises.mkdtemp(
94
+ path.join(os.tmpdir(), `webflow-scaffold-${scaffoldId}-`)
95
+ );
96
+
97
+ const downloadSpinner = logger.spinner(
98
+ `Downloading scaffold (${def.source.repo}@${ref})`
99
+ );
100
+
101
+ try {
102
+ let response: Response;
103
+ try {
104
+ response = await fetch(tarballUrl, {
105
+ redirect: "follow",
106
+ headers: { "User-Agent": "webflow-cli" },
107
+ });
108
+ } catch (fetchErr) {
109
+ downloadSpinner.fail("Download failed");
110
+ throw new ScaffoldError(
111
+ `Failed to fetch scaffold from GitHub: ${
112
+ fetchErr instanceof Error ? fetchErr.message : String(fetchErr)
113
+ }. Repo: ${def.source.repo}, ref: ${ref}`,
114
+ scaffoldId
115
+ );
116
+ }
117
+
118
+ if (!response.ok) {
119
+ downloadSpinner.fail(
120
+ `GitHub returned ${response.status} ${response.statusText}`
121
+ );
122
+ throw new ScaffoldError(
123
+ `Failed to fetch scaffold from GitHub: ${response.status} ${response.statusText}. ` +
124
+ `Repo: ${def.source.repo}, ref: ${ref}`,
125
+ scaffoldId
126
+ );
127
+ }
128
+
129
+ const buffer = Buffer.from(await response.arrayBuffer());
130
+ downloadSpinner.succeed("Downloaded scaffold");
131
+
132
+ const unpackSpinner = logger.spinner("Unpacking scaffold");
133
+ try {
134
+ await extractTarball(buffer, extractDir);
135
+ unpackSpinner.succeed("Scaffold ready");
136
+ } catch (unpackErr) {
137
+ unpackSpinner.fail("Unpack failed");
138
+ throw unpackErr;
139
+ }
140
+
141
+ const entries = await fs.promises.readdir(extractDir);
142
+ const topLevelDir = entries[0];
143
+ if (!topLevelDir || entries.length !== 1) {
144
+ throw new ScaffoldError(
145
+ `Unexpected tarball structure for ${def.source.repo}`,
146
+ scaffoldId
147
+ );
148
+ }
149
+
150
+ const extractionRoot = path.join(extractDir, topLevelDir);
151
+ const files = await listFilesRecursive(extractionRoot);
152
+
153
+ return {
154
+ scaffoldId,
155
+ files,
156
+ extractionRoot,
157
+ };
158
+ } catch (err) {
159
+ try {
160
+ fsExtra.removeSync(extractDir);
161
+ } catch {
162
+ // Ignore cleanup errors
163
+ }
164
+ if (err instanceof ScaffoldError) throw err;
165
+ throw new ScaffoldError(
166
+ `Failed to fetch scaffold "${scaffoldId}": ${
167
+ err instanceof Error ? err.message : String(err)
168
+ }`,
169
+ scaffoldId
170
+ );
171
+ }
172
+ }
173
+
174
+ async function extractTarball(buffer: Buffer, destDir: string): Promise<void> {
175
+ const tmpFile = path.join(
176
+ os.tmpdir(),
177
+ `webflow-scaffold-${Date.now()}.tar.gz`
178
+ );
179
+ await fs.promises.writeFile(
180
+ tmpFile,
181
+ new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
182
+ );
183
+ try {
184
+ await tar.extract({ file: tmpFile, cwd: destDir });
185
+ } finally {
186
+ await fs.promises.unlink(tmpFile).catch(() => {
187
+ // Ignore cleanup errors
188
+ });
189
+ }
190
+ }
191
+
192
+ async function listFilesRecursive(dirPath: string): Promise<string[]> {
193
+ const result: string[] = [];
194
+ const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
195
+
196
+ for (const entry of entries) {
197
+ const fullPath = path.join(dirPath, entry.name);
198
+ if (entry.isDirectory()) {
199
+ result.push(...(await listFilesRecursive(fullPath)));
200
+ } else {
201
+ result.push(fullPath);
202
+ }
203
+ }
204
+ return result;
205
+ }
@@ -0,0 +1,19 @@
1
+ export {
2
+ getScaffoldContent,
3
+ fetchScaffoldContent,
4
+ getScaffoldFromLocal,
5
+ } from "./fetcher";
6
+ export {
7
+ FRAMEWORK_REGISTRY,
8
+ getFrameworkDefinition,
9
+ listSupportedScaffoldIds,
10
+ listExistingScaffoldIds,
11
+ getAvailableFrameworkNames,
12
+ toMinimalScaffoldId,
13
+ } from "./registry";
14
+ export type {
15
+ FrameworkDefinition,
16
+ ScaffoldContent,
17
+ ScaffoldSource,
18
+ } from "./types";
19
+ export { ScaffoldError } from "./types";