@fluid-app/fluid-cli-theme-dev 0.1.21 → 0.1.23

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 (48) hide show
  1. package/README.md +14 -0
  2. package/dist/index.mjs +155 -2
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +8 -4
  5. package/.turbo/turbo-build.log +0 -16
  6. package/.turbo/turbo-typecheck.log +0 -4
  7. package/jest.config.cjs +0 -21
  8. package/jest.mocks/fluid-cli.ts +0 -33
  9. package/src/__tests__/plugin-state.test.ts +0 -186
  10. package/src/api.ts +0 -28
  11. package/src/commands/dev.ts +0 -186
  12. package/src/commands/init.ts +0 -51
  13. package/src/commands/lint.ts +0 -186
  14. package/src/commands/navigate.ts +0 -259
  15. package/src/commands/pull.ts +0 -242
  16. package/src/commands/push.ts +0 -220
  17. package/src/commands/theme.ts +0 -23
  18. package/src/index.ts +0 -12
  19. package/src/plugin-state.ts +0 -171
  20. package/src/theme/dev-server/hot-reload.ts +0 -65
  21. package/src/theme/dev-server/index.ts +0 -145
  22. package/src/theme/dev-server/proxy.ts +0 -125
  23. package/src/theme/dev-server/sse.ts +0 -43
  24. package/src/theme/dev-server/watcher.ts +0 -54
  25. package/src/theme/file.ts +0 -104
  26. package/src/theme/fluid-ignore.ts +0 -64
  27. package/src/theme/mime-type.ts +0 -45
  28. package/src/theme/root.ts +0 -54
  29. package/src/theme/syncer.ts +0 -338
  30. package/src/theme-config.ts +0 -34
  31. package/src/theme-picker.ts +0 -164
  32. package/src/workspace.ts +0 -71
  33. package/tsconfig.json +0 -10
  34. package/tsdown.config.ts +0 -19
  35. /package/{skills → dist/skills}/themes-review/SKILL.md +0 -0
  36. /package/{skills → dist/skills}/themes-review/references/blocks-vs-sections.md +0 -0
  37. /package/{skills → dist/skills}/themes-review/references/css-js-hygiene.md +0 -0
  38. /package/{skills → dist/skills}/themes-review/references/dead-code.md +0 -0
  39. /package/{skills → dist/skills}/themes-review/references/dynamism.md +0 -0
  40. /package/{skills → dist/skills}/themes-review/references/editor-attributes.md +0 -0
  41. /package/{skills → dist/skills}/themes-review/references/examples.md +0 -0
  42. /package/{skills → dist/skills}/themes-review/references/fairshare-attributes.md +0 -0
  43. /package/{skills → dist/skills}/themes-review/references/global-settings.md +0 -0
  44. /package/{skills → dist/skills}/themes-review/references/liquid-correctness.md +0 -0
  45. /package/{skills → dist/skills}/themes-review/references/navigation.md +0 -0
  46. /package/{skills → dist/skills}/themes-review/references/performance.md +0 -0
  47. /package/{skills → dist/skills}/themes-review/references/security-accessibility.md +0 -0
  48. /package/{skills → dist/skills}/themes-review/references/setting-types.md +0 -0
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-theme-dev",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Fluid CLI plugin for theme developer workflows — dev server, push, pull, init",
5
+ "files": [
6
+ "dist",
7
+ "README.md"
8
+ ],
5
9
  "type": "module",
6
10
  "main": "./dist/index.mjs",
7
11
  "types": "./dist/index.d.mts",
@@ -34,10 +38,10 @@
34
38
  "jest": "^29.7.0",
35
39
  "tsdown": "^0.21.0",
36
40
  "typescript": "^5",
37
- "@fluid-app/api-client-core": "0.1.0",
38
- "@fluid-app/themes-api-client": "0.1.0",
39
41
  "@fluid-app/theme-schema": "0.1.0",
40
- "@fluid-app/typescript-config": "0.0.0"
42
+ "@fluid-app/themes-api-client": "0.1.0",
43
+ "@fluid-app/typescript-config": "0.0.0",
44
+ "@fluid-app/api-client-core": "0.1.0"
41
45
  },
42
46
  "engines": {
43
47
  "node": ">=18.0.0"
@@ -1,16 +0,0 @@
1
-
2
- > @fluid-app/fluid-cli-theme-dev@0.1.21 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
3
- > tsdown
4
-
5
- ℹ tsdown v0.21.0 powered by rolldown v1.0.0-rc.7
6
- ℹ config file: /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev/tsdown.config.ts
7
- ℹ entry: src/index.ts
8
- ℹ target: node24
9
- ℹ tsconfig: tsconfig.json
10
- ℹ Build start
11
- ℹ dist/index.mjs  76.86 kB │ gzip: 21.05 kB
12
- ℹ dist/index.mjs.map 196.02 kB │ gzip: 46.38 kB
13
- ℹ dist/index.d.mts.map  0.11 kB │ gzip: 0.12 kB
14
- ℹ dist/index.d.mts  0.19 kB │ gzip: 0.16 kB
15
- ℹ 4 files, total: 273.18 kB
16
- ✔ Build complete in 1315ms
@@ -1,4 +0,0 @@
1
-
2
- > @fluid-app/fluid-cli-theme-dev@0.1.21 typecheck /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
3
- > tsc --noEmit
4
-
package/jest.config.cjs DELETED
@@ -1,21 +0,0 @@
1
- module.exports = {
2
- testEnvironment: "node",
3
- testMatch: ["**/src/**/__tests__/**/*.test.ts"],
4
- transform: {
5
- "^.+\\.(t|j)sx?$": [
6
- "@swc/jest",
7
- {
8
- module: {
9
- type: "commonjs",
10
- },
11
- },
12
- ],
13
- },
14
- moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
15
- moduleNameMapper: {
16
- // The real package only ships ESM, which Jest can't resolve in CJS mode;
17
- // map it to a stateful in-memory mock for plugin-state tests.
18
- "^@fluid-app/fluid-cli$": "<rootDir>/jest.mocks/fluid-cli.ts",
19
- "^(\\.{1,2}/.*)\\.js$": "$1",
20
- },
21
- };
@@ -1,33 +0,0 @@
1
- /**
2
- * Jest mock for @fluid-app/fluid-cli.
3
- *
4
- * The real package only exports ESM (.mjs), which Jest can't resolve in CJS
5
- * mode. plugin-state.ts only uses `readConfig`/`updateConfig`, so this mock
6
- * backs them with an in-memory config. Tests seed/reset state through the
7
- * normal `updateConfig` surface, so no test-only exports are needed.
8
- */
9
-
10
- interface FakeConfig {
11
- activeProfile: string | null;
12
- profiles: Record<string, unknown>;
13
- plugins: Record<string, unknown>;
14
- enabledPlugins: string[] | null;
15
- }
16
-
17
- let config: FakeConfig = {
18
- activeProfile: null,
19
- profiles: {},
20
- plugins: {},
21
- enabledPlugins: null,
22
- };
23
-
24
- export function readConfig(): FakeConfig {
25
- return config;
26
- }
27
-
28
- export function updateConfig(
29
- updater: (config: FakeConfig) => FakeConfig,
30
- ): FakeConfig {
31
- config = updater(config);
32
- return config;
33
- }
@@ -1,186 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import {
3
- readConfig,
4
- updateConfig,
5
- type FluidConfig,
6
- } from "@fluid-app/fluid-cli";
7
- import {
8
- devThemeKey,
9
- getDevTheme,
10
- setDevTheme,
11
- clearDevTheme,
12
- setLastDevThemeId,
13
- getLastDevThemeId,
14
- } from "../plugin-state.js";
15
-
16
- // `@fluid-app/fluid-cli` is mapped to an in-memory mock (jest.mocks/fluid-cli.ts)
17
- // so these helpers never touch the real ~/.fluid/config.json. State is seeded
18
- // and reset through the normal `updateConfig` surface.
19
- jest.mock("node:fs", () => ({ existsSync: jest.fn() }));
20
- const existsSyncMock = existsSync as unknown as jest.Mock;
21
-
22
- const PLUGIN_KEY = "theme-dev";
23
-
24
- function freshConfig(): FluidConfig {
25
- return {
26
- activeProfile: null,
27
- profiles: {},
28
- plugins: {},
29
- enabledPlugins: null,
30
- };
31
- }
32
-
33
- function setPluginState(state: Record<string, unknown>): void {
34
- updateConfig((config) => ({
35
- ...config,
36
- plugins: { ...config.plugins, [PLUGIN_KEY]: state },
37
- }));
38
- }
39
-
40
- function pluginState(): Record<string, unknown> {
41
- return readConfig().plugins[PLUGIN_KEY] as Record<string, unknown>;
42
- }
43
-
44
- beforeEach(() => {
45
- updateConfig(() => freshConfig());
46
- // Default: every stored project directory still exists (no pruning).
47
- existsSyncMock.mockReset();
48
- existsSyncMock.mockReturnValue(true);
49
- });
50
-
51
- describe("devThemeKey", () => {
52
- it("combines company and theme root", () => {
53
- expect(devThemeKey("acme", "/themes/store")).toBe("acme:/themes/store");
54
- });
55
-
56
- it("falls back to 'default' when company is undefined", () => {
57
- expect(devThemeKey(undefined, "/themes/store")).toBe(
58
- "default:/themes/store",
59
- );
60
- });
61
- });
62
-
63
- describe("setDevTheme / getDevTheme", () => {
64
- it("stores and retrieves a dev theme per key", () => {
65
- setDevTheme("acme:/a", { id: 1, name: "A" });
66
- expect(getDevTheme("acme:/a")).toEqual({ id: 1, name: "A" });
67
- });
68
-
69
- it("isolates dev themes across keys", () => {
70
- setDevTheme("acme:/a", { id: 1, name: "A" });
71
- setDevTheme("acme:/b", { id: 2, name: "B" });
72
- expect(getDevTheme("acme:/a")).toEqual({ id: 1, name: "A" });
73
- expect(getDevTheme("acme:/b")).toEqual({ id: 2, name: "B" });
74
- });
75
-
76
- it("returns undefined for an unknown key", () => {
77
- expect(getDevTheme("acme:/missing")).toBeUndefined();
78
- });
79
-
80
- it("marks the most recently stored theme as lastDevThemeId", () => {
81
- setDevTheme("acme:/a", { id: 1, name: "A" });
82
- setDevTheme("acme:/b", { id: 2, name: "B" });
83
- expect(getLastDevThemeId()).toBe(2);
84
- });
85
- });
86
-
87
- describe("clearDevTheme", () => {
88
- it("removes only the targeted key", () => {
89
- setDevTheme("acme:/a", { id: 1, name: "A" });
90
- setDevTheme("acme:/b", { id: 2, name: "B" });
91
- clearDevTheme("acme:/a");
92
- expect(getDevTheme("acme:/a")).toBeUndefined();
93
- expect(getDevTheme("acme:/b")).toEqual({ id: 2, name: "B" });
94
- });
95
-
96
- it("is a no-op for an unknown key", () => {
97
- setDevTheme("acme:/a", { id: 1, name: "A" });
98
- clearDevTheme("acme:/missing");
99
- expect(getDevTheme("acme:/a")).toEqual({ id: 1, name: "A" });
100
- });
101
-
102
- it("clears lastDevThemeId when it pointed at the removed theme", () => {
103
- setDevTheme("acme:/a", { id: 1, name: "A" });
104
- expect(getLastDevThemeId()).toBe(1);
105
- clearDevTheme("acme:/a");
106
- expect(getLastDevThemeId()).toBeUndefined();
107
- });
108
-
109
- it("leaves lastDevThemeId when it points at a different theme", () => {
110
- setDevTheme("acme:/a", { id: 1, name: "A" });
111
- setDevTheme("acme:/b", { id: 2, name: "B" });
112
- clearDevTheme("acme:/a"); // most-recent is /b (id 2)
113
- expect(getLastDevThemeId()).toBe(2);
114
- });
115
- });
116
-
117
- describe("devThemes map stays bounded", () => {
118
- it("prunes entries whose theme directory no longer exists on the next write", () => {
119
- setDevTheme("acme:/a", { id: 1, name: "A" });
120
- setDevTheme("acme:/b", { id: 2, name: "B" });
121
-
122
- // /a is deleted; /b and the new /c still exist.
123
- existsSyncMock.mockImplementation((p: string) => p !== "/a");
124
- setDevTheme("acme:/c", { id: 3, name: "C" });
125
-
126
- expect(getDevTheme("acme:/a")).toBeUndefined(); // pruned
127
- expect(getDevTheme("acme:/b")).toEqual({ id: 2, name: "B" });
128
- expect(getDevTheme("acme:/c")).toEqual({ id: 3, name: "C" });
129
- expect(
130
- Object.keys(pluginState()["devThemes"] as Record<string, unknown>),
131
- ).toEqual(["acme:/b", "acme:/c"]);
132
- });
133
-
134
- it("keeps the entry being written even if its own directory check fails", () => {
135
- existsSyncMock.mockReturnValue(false);
136
- setDevTheme("acme:/a", { id: 1, name: "A" });
137
- expect(getDevTheme("acme:/a")).toEqual({ id: 1, name: "A" });
138
- });
139
- });
140
-
141
- describe("setLastDevThemeId", () => {
142
- it("updates the navigate pointer without recording a dev theme", () => {
143
- setLastDevThemeId(123);
144
- expect(getLastDevThemeId()).toBe(123);
145
- expect(getDevTheme("acme:/a")).toBeUndefined();
146
- });
147
- });
148
-
149
- describe("legacy migration", () => {
150
- it("adopts a legacy global devThemeId for the first requesting key, then clears it", () => {
151
- setPluginState({ devThemeId: 99, devThemeName: "Legacy" });
152
-
153
- expect(getDevTheme("acme:/a")).toEqual({ id: 99, name: "Legacy" });
154
-
155
- // Legacy fields are cleared so a second project can't adopt the same theme.
156
- const state = pluginState();
157
- expect(state["devThemeId"]).toBeUndefined();
158
- expect(state["devThemeName"]).toBeUndefined();
159
- expect(getDevTheme("acme:/b")).toBeUndefined();
160
- expect(getLastDevThemeId()).toBe(99);
161
- });
162
-
163
- it("synthesizes a name when the legacy name is absent", () => {
164
- setPluginState({ devThemeId: 42 });
165
- expect(getDevTheme("acme:/a")).toEqual({ id: 42, name: "Development #42" });
166
- });
167
-
168
- it("prefers an explicit per-key entry over the legacy id", () => {
169
- setPluginState({
170
- devThemeId: 99,
171
- devThemes: { "acme:/a": { id: 7, name: "Real" } },
172
- });
173
- expect(getDevTheme("acme:/a")).toEqual({ id: 7, name: "Real" });
174
- });
175
- });
176
-
177
- describe("getLastDevThemeId", () => {
178
- it("falls back to the legacy id before any per-project run", () => {
179
- setPluginState({ devThemeId: 5 });
180
- expect(getLastDevThemeId()).toBe(5);
181
- });
182
-
183
- it("returns undefined when nothing is stored", () => {
184
- expect(getLastDevThemeId()).toBeUndefined();
185
- });
186
- });
package/src/api.ts DELETED
@@ -1,28 +0,0 @@
1
- import {
2
- createFetchClient,
3
- type FetchClient,
4
- } from "@fluid-app/api-client-core";
5
- import { getAuthToken } from "@fluid-app/fluid-cli";
6
-
7
- export type ApiClient = FetchClient;
8
-
9
- /** Base URL for all API calls. Set FLUID_API_BASE to route through a BFF. */
10
- function getApiBase(): string {
11
- return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
12
- }
13
-
14
- export function createApiClient(tokenOverride?: string): ApiClient {
15
- return createFetchClient({
16
- baseUrl: getApiBase(),
17
- getAuthToken: () => tokenOverride ?? getAuthToken() ?? null,
18
- });
19
- }
20
-
21
- export function requireToken(): string {
22
- const token = getAuthToken();
23
- if (!token) {
24
- console.error("Not logged in. Run `fluid login` first.");
25
- process.exit(1);
26
- }
27
- return token;
28
- }
@@ -1,186 +0,0 @@
1
- import { Command } from "commander";
2
- import { requireToken, createApiClient } from "../api.js";
3
- import { readThemeConfig } from "../theme-config.js";
4
- import {
5
- devThemeKey,
6
- getDevTheme,
7
- setDevTheme,
8
- setLastDevThemeId,
9
- clearDevTheme,
10
- } from "../plugin-state.js";
11
- import { ThemeRoot } from "../theme/root.js";
12
- import { startDevServer } from "../theme/dev-server/index.js";
13
- import { themes } from "@fluid-app/themes-api-client";
14
- import { findTheme, type ApplicationTheme } from "../theme-picker.js";
15
- import { findWorkspace, resolveThemeRootFromCwd } from "../workspace.js";
16
-
17
- interface CompanyMe {
18
- data: { company: { subdomain?: string; name?: string } };
19
- }
20
-
21
- async function ensureDevTheme(
22
- api: ReturnType<typeof createApiClient>,
23
- projectKey: string,
24
- identifier?: string,
25
- ): Promise<ApplicationTheme> {
26
- if (identifier) {
27
- const theme = await findTheme(api, identifier);
28
- // Keep `navigate` pointed at whatever the dev server is actually serving.
29
- setLastDevThemeId(theme.id);
30
- return theme;
31
- }
32
-
33
- // Reuse this project's stored dev theme if it still exists and is still a
34
- // development theme (a published/promoted theme must not be edited in place).
35
- const stored = getDevTheme(projectKey);
36
- if (stored) {
37
- try {
38
- const body = await themes.getApplicationTheme(api, stored.id);
39
- const existing = body.application_theme;
40
- if (existing && existing.status === "development") {
41
- console.log(`Using existing dev theme #${existing.id}`);
42
- // Refresh the stored name and mark it most-recent for `navigate`.
43
- setDevTheme(projectKey, { id: existing.id, name: existing.name });
44
- return existing;
45
- }
46
- } catch {
47
- // Theme no longer exists — fall through to create a new one.
48
- }
49
- // Stored theme is gone or no longer a dev theme; forget it.
50
- clearDevTheme(projectKey);
51
- }
52
-
53
- // Create a new development theme
54
- const { hostname } = await import("node:os");
55
- const host = hostname().split(".")[0] ?? "dev";
56
- const name =
57
- `Development (${host}-${Math.random().toString(36).slice(2, 8)})`.slice(
58
- 0,
59
- 50,
60
- );
61
-
62
- const body = await themes.createApplicationTheme(api, {
63
- application_theme: { name, status: "development" },
64
- });
65
- const theme = body.application_theme;
66
- setDevTheme(projectKey, { id: theme.id, name: theme.name });
67
- console.log(`Created dev theme: ${theme.name} (#${theme.id})`);
68
- return theme;
69
- }
70
-
71
- export function createDevCommand(): Command {
72
- return new Command("dev")
73
- .description("Start the theme dev server with hot reload")
74
- .option("--host <host>", "Local server host", "127.0.0.1")
75
- .option("--port <port>", "Local server port", "9292")
76
- .option(
77
- "-t, --theme <name-or-id>",
78
- "Use an existing theme instead of dev theme",
79
- )
80
- .option("-f, --force", "Skip schema validation on upload")
81
- .option("--live-reload <mode>", "Reload mode: full-page | off", "full-page")
82
- .option("--navigate", "Open browser navigator after server starts")
83
- .option("--root <path>", "Theme root directory", ".")
84
- .action(
85
- async (opts: {
86
- host: string;
87
- port: string;
88
- theme?: string;
89
- force?: boolean;
90
- liveReload: string;
91
- navigate?: boolean;
92
- root: string;
93
- }) => {
94
- requireToken();
95
-
96
- // If no explicit --root and we're inside a workspace, resolve to the theme root
97
- let rootPath = opts.root;
98
- if (rootPath === ".") {
99
- const workspace = findWorkspace();
100
- if (workspace) {
101
- rootPath = resolveThemeRootFromCwd(workspace) ?? rootPath;
102
- }
103
- }
104
-
105
- const themeRoot = new ThemeRoot(rootPath);
106
- if (!themeRoot.isValid()) {
107
- console.error(`'${rootPath}' does not look like a theme directory.`);
108
- process.exit(1);
109
- }
110
-
111
- const port = Number(opts.port);
112
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
113
- console.error(
114
- `Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,
115
- );
116
- process.exit(1);
117
- }
118
-
119
- const reloadMode = opts.liveReload === "off" ? "off" : "full-page";
120
- const api = createApiClient();
121
- const config = readThemeConfig(themeRoot.root);
122
-
123
- // Use company from .fluid-theme.json if available, otherwise fetch
124
- let company: string;
125
- if (config?.company) {
126
- company = config.company;
127
- } else {
128
- const companyRes = await api.get<CompanyMe>(
129
- "/api/company/v1/companies/me",
130
- );
131
- company = companyRes.data?.company?.subdomain ?? "";
132
- if (!company) {
133
- console.error(
134
- "Could not determine company subdomain. Make sure your token is valid.",
135
- );
136
- process.exit(1);
137
- }
138
- }
139
-
140
- // Always iterate on an isolated dev theme: reuse the stored one or
141
- // create a fresh `development` theme. `--theme` is the explicit
142
- // escape hatch for targeting an existing theme. We deliberately do NOT
143
- // fall back to `.fluid-theme.json`'s themeId — that points at whatever
144
- // was pulled (often the active/production theme), and the dev server's
145
- // `delete: true` sync would overwrite it in place.
146
- const projectKey = devThemeKey(company, themeRoot.root);
147
- const theme = opts.theme
148
- ? await ensureDevTheme(api, projectKey, opts.theme)
149
- : await ensureDevTheme(api, projectKey);
150
- const editorUrl = `https://admin.fluid.app/themes/${theme.id}/editor`;
151
-
152
- let stop: (() => void) | undefined;
153
-
154
- const cleanup = () => {
155
- stop?.();
156
- process.exit(0);
157
- };
158
- process.on("SIGINT", cleanup);
159
- process.on("SIGTERM", cleanup);
160
-
161
- stop = await startDevServer(
162
- api,
163
- {
164
- id: theme.id,
165
- name: theme.name,
166
- company,
167
- editorUrl,
168
- },
169
- themeRoot,
170
- { host: opts.host, port, reloadMode, validate: !opts.force },
171
- (address) => {
172
- console.log(`\n Dev server: ${address}`);
173
- console.log(` Web editor: ${editorUrl}`);
174
- console.log("\n Watching for file changes…\n");
175
-
176
- if (opts.navigate) {
177
- import("open").then((m) => m.default(`${address}/home`));
178
- }
179
- },
180
- );
181
-
182
- // Keep process alive
183
- await new Promise(() => {});
184
- },
185
- );
186
- }
@@ -1,51 +0,0 @@
1
- import { Command } from "commander";
2
- import { execFileSync } from "node:child_process";
3
- import { rmSync, existsSync } from "node:fs";
4
- import { join } from "node:path";
5
- import prompts from "prompts";
6
-
7
- const DEFAULT_CLONE_URL = "git@github.com:fluid-commerce/base-theme.git";
8
-
9
- const SAFE_NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/;
10
-
11
- export function createInitCommand(): Command {
12
- return new Command("init")
13
- .description("Initialize a new theme by cloning the base theme")
14
- .argument("[name]", "Directory name for the new theme")
15
- .option("-u, --clone-url <url>", "Git URL to clone from", DEFAULT_CLONE_URL)
16
- .action(async (name: string | undefined, opts: { cloneUrl: string }) => {
17
- if (!name) {
18
- const res = await prompts(
19
- {
20
- type: "text",
21
- name: "name",
22
- message: "Theme name",
23
- },
24
- { onCancel: () => process.exit(130) },
25
- );
26
- name = res.name as string;
27
- if (!name) {
28
- console.error("No name provided.");
29
- process.exit(1);
30
- }
31
- }
32
-
33
- if (!SAFE_NAME_RE.test(name)) {
34
- console.error(
35
- `Invalid theme name: '${name}'. Use only letters, numbers, hyphens, underscores, and dots.`,
36
- );
37
- process.exit(1);
38
- }
39
-
40
- console.log(`Cloning theme from ${opts.cloneUrl} into ${name}…`);
41
- execFileSync("git", ["clone", opts.cloneUrl, name], { stdio: "inherit" });
42
-
43
- for (const dir of [".git", ".github"]) {
44
- const path = join(name, dir);
45
- if (existsSync(path)) rmSync(path, { recursive: true, force: true });
46
- }
47
-
48
- console.log(`\nTheme initialized in ./${name}`);
49
- console.log(`Next steps:\n cd ${name}\n fluid theme push`);
50
- });
51
- }