@fluid-app/fluid-cli-theme-dev 0.1.22 → 0.1.24

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 +159 -2
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +6 -2
  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/README.md CHANGED
@@ -84,6 +84,20 @@ Interactively select a route and open it in the browser (requires a running dev
84
84
  fluid theme navigate
85
85
  ```
86
86
 
87
+ ### `fluid theme skills install`
88
+
89
+ Copy the bundled theme AI skills (e.g. `themes-review`) into the current directory so an
90
+ agent can load them. Defaults to `.agents/skills/`, the tool-neutral convention for agent
91
+ skills:
92
+
93
+ ```bash
94
+ fluid theme skills install # → .agents/skills/
95
+ fluid theme skills install --dir .claude/skills # custom location
96
+ fluid theme skills install --force # overwrite existing skills without prompting
97
+ ```
98
+
99
+ Existing skills are left untouched unless you confirm the overwrite (or pass `--force`).
100
+
87
101
  ## Theme Directory Structure
88
102
 
89
103
  A valid theme directory must contain at least one of: `templates/`, `assets/`, or `config/`.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { getAuthToken, readConfig, updateConfig } from "@fluid-app/fluid-cli";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { createHash } from "node:crypto";
6
6
  import http from "node:http";
@@ -10,6 +10,7 @@ import chalk from "chalk";
10
10
  import prompts from "prompts";
11
11
  import ora from "ora";
12
12
  import { execFileSync } from "node:child_process";
13
+ import { fileURLToPath } from "node:url";
13
14
  //#region ../../platform/api-client-core/src/fetch-client.ts
14
15
  /**
15
16
  * API Error class compatible with fluid-admin's ApiError
@@ -2369,15 +2370,171 @@ function createNavigateCommand() {
2369
2370
  });
2370
2371
  }
2371
2372
  //#endregion
2373
+ //#region src/skills/install.ts
2374
+ function listSkillNames(skillsDir) {
2375
+ if (!existsSync(skillsDir)) return [];
2376
+ return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => existsSync(join(skillsDir, name, "SKILL.md"))).sort();
2377
+ }
2378
+ async function installSkills(options) {
2379
+ const { sourceDir, targetRoot, force, confirmOverwrite, onLeftover } = options;
2380
+ const installed = [];
2381
+ const skipped = [];
2382
+ mkdirSync(targetRoot, { recursive: true });
2383
+ for (const name of listSkillNames(sourceDir)) {
2384
+ const from = join(sourceDir, name);
2385
+ const to = join(targetRoot, name);
2386
+ if (existsSync(to) && !force && !await confirmOverwrite(name)) {
2387
+ skipped.push(name);
2388
+ continue;
2389
+ }
2390
+ const leftover = replaceDirectory(from, to);
2391
+ if (leftover !== null) onLeftover(leftover);
2392
+ installed.push(name);
2393
+ }
2394
+ return {
2395
+ installed,
2396
+ skipped
2397
+ };
2398
+ }
2399
+ /**
2400
+ * Replace `target` with a fresh copy of `source` without ever leaving `target`
2401
+ * missing or partially written.
2402
+ *
2403
+ * Filesystem copies are not atomic, so a naive "delete then copy" loses the
2404
+ * original if the copy fails (permissions, no disk space, an interrupted
2405
+ * process). This stages the copy in a sibling directory and only swaps it into
2406
+ * place once it has fully succeeded; an existing `target` is moved aside to a
2407
+ * sibling backup first and restored if the swap fails. Because the whole
2408
+ * directory is replaced, files removed or renamed in `source` do not linger.
2409
+ *
2410
+ * Staging and backup directories live beside `target`, so its parent must
2411
+ * already exist and be on the same filesystem — that keeps the swap a cheap,
2412
+ * atomic rename rather than a cross-device copy.
2413
+ *
2414
+ * Not safe against a second process racing on the same `target`; intended for
2415
+ * single-process CLI use.
2416
+ *
2417
+ * @returns the path of a leftover backup directory that could not be removed
2418
+ * after an otherwise-successful replace (the previous contents are retained
2419
+ * there for manual cleanup), or `null` when nothing was left behind. The caller
2420
+ * should surface a non-null result so the leftover isn't silently hidden.
2421
+ */
2422
+ function replaceDirectory(source, target) {
2423
+ const staging = reserveSiblingPath(target, "staging");
2424
+ try {
2425
+ cpSync(source, staging, { recursive: true });
2426
+ } catch (error) {
2427
+ removeQuietly(staging);
2428
+ throw error;
2429
+ }
2430
+ if (!existsSync(target)) return swapIntoPlace(staging, target, null);
2431
+ const backup = reserveSiblingPath(target, "backup");
2432
+ try {
2433
+ renameSync(target, backup);
2434
+ } catch (error) {
2435
+ removeQuietly(staging);
2436
+ throw error;
2437
+ }
2438
+ return swapIntoPlace(staging, target, backup);
2439
+ }
2440
+ function swapIntoPlace(staging, target, backup) {
2441
+ try {
2442
+ renameSync(staging, target);
2443
+ } catch (error) {
2444
+ if (backup !== null) restoreBackup(backup, target, error);
2445
+ removeQuietly(staging);
2446
+ throw error;
2447
+ }
2448
+ return removeQuietly(backup);
2449
+ }
2450
+ function restoreBackup(backup, target, cause) {
2451
+ try {
2452
+ renameSync(backup, target);
2453
+ } catch {
2454
+ throw new Error(`Failed to replace ${target}; its previous contents are preserved at ${backup}.`, { cause });
2455
+ }
2456
+ }
2457
+ function removeQuietly(path) {
2458
+ if (path === null) return null;
2459
+ try {
2460
+ rmSync(path, {
2461
+ recursive: true,
2462
+ force: true
2463
+ });
2464
+ return null;
2465
+ } catch {
2466
+ return path;
2467
+ }
2468
+ }
2469
+ function reserveSiblingPath(basePath, label) {
2470
+ let candidate = `${basePath}.${label}`;
2471
+ for (let n = 1; existsSync(candidate); n += 1) candidate = `${basePath}.${label}.${n}`;
2472
+ return candidate;
2473
+ }
2474
+ //#endregion
2475
+ //#region src/commands/skills.ts
2476
+ const DEFAULT_TARGET_DIR = ".agents/skills";
2477
+ function resolveBundledSkillsDir() {
2478
+ const here = dirname(fileURLToPath(import.meta.url));
2479
+ const candidates = [
2480
+ join(here, "skills"),
2481
+ join(here, "..", "skills"),
2482
+ join(here, "..", "..", "skills")
2483
+ ];
2484
+ for (const dir of candidates) if (listSkillNames(dir).length > 0) return dir;
2485
+ let dir = here;
2486
+ for (let depth = 0; depth < 6; depth++) {
2487
+ const candidate = join(dir, "skills");
2488
+ if (listSkillNames(candidate).length > 0) return candidate;
2489
+ dir = dirname(dir);
2490
+ }
2491
+ throw new Error("Could not locate the bundled theme skills — this is a packaging bug.");
2492
+ }
2493
+ function createSkillsCommand() {
2494
+ const skills = new Command("skills").description("Manage the bundled Fluid theme AI skills");
2495
+ skills.command("install").description("Copy the bundled theme skills into the current directory (default: .agents/skills/)").option("-d, --dir <path>", "Directory to install into", DEFAULT_TARGET_DIR).option("-f, --force", "Overwrite existing skills without prompting").action(async (opts) => {
2496
+ const sourceDir = resolveBundledSkillsDir();
2497
+ if (listSkillNames(sourceDir).length === 0) {
2498
+ console.error("No bundled skills found to install.");
2499
+ process.exit(1);
2500
+ }
2501
+ const targetRoot = resolve(process.cwd(), opts.dir);
2502
+ const { installed, skipped } = await installSkills({
2503
+ sourceDir,
2504
+ targetRoot,
2505
+ force: Boolean(opts.force),
2506
+ confirmOverwrite: async (name) => {
2507
+ const res = await prompts({
2508
+ type: "confirm",
2509
+ name: "overwrite",
2510
+ message: `${chalk.yellow(name)} already exists in ${opts.dir}. Overwrite?`,
2511
+ initial: false
2512
+ }, { onCancel: () => process.exit(130) });
2513
+ return Boolean(res.overwrite);
2514
+ },
2515
+ onLeftover: (path) => {
2516
+ console.log(`${chalk.yellow("⚠")} kept the previous copy at ${path} (couldn't remove it — delete it manually)`);
2517
+ }
2518
+ });
2519
+ for (const name of installed) console.log(`${chalk.green("✓")} ${name} → ${join(opts.dir, name)}`);
2520
+ for (const name of skipped) console.log(`${chalk.dim(`· skipped ${name} (kept existing)`)}`);
2521
+ const parts = [installed.length > 0 ? `${installed.length} installed` : null, skipped.length > 0 ? `${skipped.length} skipped` : null].filter(Boolean);
2522
+ console.log(`\n${chalk.bold(parts.join(", ") || "Nothing to do")} in ${targetRoot}`);
2523
+ if (installed.length > 0) console.log(chalk.dim("Restart your agent session to pick up the new skills."));
2524
+ });
2525
+ return skills;
2526
+ }
2527
+ //#endregion
2372
2528
  //#region src/commands/theme.ts
2373
2529
  function registerThemeCommand(ctx) {
2374
- const cmd = new Command("theme").description("Theme developer workflow — dev server, push, pull, lint, init");
2530
+ const cmd = new Command("theme").description("Theme developer workflow — dev server, push, pull, lint, init, skills");
2375
2531
  cmd.addCommand(createDevCommand());
2376
2532
  cmd.addCommand(createPushCommand());
2377
2533
  cmd.addCommand(createPullCommand());
2378
2534
  cmd.addCommand(createLintCommand());
2379
2535
  cmd.addCommand(createInitCommand());
2380
2536
  cmd.addCommand(createNavigateCommand());
2537
+ cmd.addCommand(createSkillsCommand());
2381
2538
  ctx.program.addCommand(cmd);
2382
2539
  }
2383
2540
  //#endregion