@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/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,167 @@ function createNavigateCommand() {
2369
2370
  });
2370
2371
  }
2371
2372
  //#endregion
2373
+ //#region src/fs/replace-directory.ts
2374
+ /**
2375
+ * Replace `target` with a fresh copy of `source` without ever leaving `target`
2376
+ * missing or partially written.
2377
+ *
2378
+ * Filesystem copies are not atomic, so a naive "delete then copy" loses the
2379
+ * original if the copy fails (permissions, no disk space, an interrupted
2380
+ * process). This stages the copy in a sibling directory and only swaps it into
2381
+ * place once it has fully succeeded; an existing `target` is moved aside to a
2382
+ * sibling backup first and restored if the swap fails. Because the whole
2383
+ * directory is replaced, files removed or renamed in `source` do not linger.
2384
+ *
2385
+ * Staging and backup directories live beside `target`, so its parent must
2386
+ * already exist and be on the same filesystem — that keeps the swap a cheap,
2387
+ * atomic rename rather than a cross-device copy.
2388
+ *
2389
+ * Not safe against a second process racing on the same `target`; intended for
2390
+ * single-process CLI use.
2391
+ */
2392
+ function replaceDirectory(source, target) {
2393
+ const staging = reserveSiblingPath(target, "staging");
2394
+ try {
2395
+ cpSync(source, staging, { recursive: true });
2396
+ } catch (error) {
2397
+ rmSync(staging, {
2398
+ recursive: true,
2399
+ force: true
2400
+ });
2401
+ throw error;
2402
+ }
2403
+ if (!existsSync(target)) {
2404
+ swapIntoPlace(staging, target, null);
2405
+ return;
2406
+ }
2407
+ const backup = reserveSiblingPath(target, "backup");
2408
+ try {
2409
+ renameSync(target, backup);
2410
+ } catch (error) {
2411
+ rmSync(staging, {
2412
+ recursive: true,
2413
+ force: true
2414
+ });
2415
+ throw error;
2416
+ }
2417
+ swapIntoPlace(staging, target, backup);
2418
+ }
2419
+ function swapIntoPlace(staging, target, backup) {
2420
+ try {
2421
+ renameSync(staging, target);
2422
+ } catch (error) {
2423
+ rmSync(staging, {
2424
+ recursive: true,
2425
+ force: true
2426
+ });
2427
+ if (backup !== null) restoreBackup(backup, target, error);
2428
+ throw error;
2429
+ }
2430
+ if (backup !== null) rmSync(backup, {
2431
+ recursive: true,
2432
+ force: true
2433
+ });
2434
+ }
2435
+ function restoreBackup(backup, target, cause) {
2436
+ try {
2437
+ renameSync(backup, target);
2438
+ } catch {
2439
+ throw new Error(`Failed to replace ${target}; its previous contents are preserved at ${backup}.`, { cause });
2440
+ }
2441
+ }
2442
+ function reserveSiblingPath(basePath, label) {
2443
+ let candidate = `${basePath}.${label}`;
2444
+ for (let n = 1; existsSync(candidate); n += 1) candidate = `${basePath}.${label}.${n}`;
2445
+ return candidate;
2446
+ }
2447
+ //#endregion
2448
+ //#region src/skills/install.ts
2449
+ function listSkillNames(skillsDir) {
2450
+ if (!existsSync(skillsDir)) return [];
2451
+ return readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => existsSync(join(skillsDir, name, "SKILL.md"))).sort();
2452
+ }
2453
+ async function installSkills(options) {
2454
+ const { sourceDir, targetRoot, force, confirmOverwrite } = options;
2455
+ const installed = [];
2456
+ const skipped = [];
2457
+ mkdirSync(targetRoot, { recursive: true });
2458
+ for (const name of listSkillNames(sourceDir)) {
2459
+ const from = join(sourceDir, name);
2460
+ const to = join(targetRoot, name);
2461
+ if (existsSync(to) && !force && !await confirmOverwrite(name)) {
2462
+ skipped.push(name);
2463
+ continue;
2464
+ }
2465
+ replaceDirectory(from, to);
2466
+ installed.push(name);
2467
+ }
2468
+ return {
2469
+ installed,
2470
+ skipped
2471
+ };
2472
+ }
2473
+ //#endregion
2474
+ //#region src/commands/skills.ts
2475
+ const DEFAULT_TARGET_DIR = ".agents/skills";
2476
+ function resolveBundledSkillsDir() {
2477
+ const here = dirname(fileURLToPath(import.meta.url));
2478
+ const candidates = [
2479
+ join(here, "skills"),
2480
+ join(here, "..", "skills"),
2481
+ join(here, "..", "..", "skills")
2482
+ ];
2483
+ for (const dir of candidates) if (listSkillNames(dir).length > 0) return dir;
2484
+ let dir = here;
2485
+ for (let depth = 0; depth < 6; depth++) {
2486
+ const candidate = join(dir, "skills");
2487
+ if (listSkillNames(candidate).length > 0) return candidate;
2488
+ dir = dirname(dir);
2489
+ }
2490
+ throw new Error("Could not locate the bundled theme skills — this is a packaging bug.");
2491
+ }
2492
+ function createSkillsCommand() {
2493
+ const skills = new Command("skills").description("Manage the bundled Fluid theme AI skills");
2494
+ 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) => {
2495
+ const sourceDir = resolveBundledSkillsDir();
2496
+ if (listSkillNames(sourceDir).length === 0) {
2497
+ console.error("No bundled skills found to install.");
2498
+ process.exit(1);
2499
+ }
2500
+ const targetRoot = resolve(process.cwd(), opts.dir);
2501
+ const { installed, skipped } = await installSkills({
2502
+ sourceDir,
2503
+ targetRoot,
2504
+ force: Boolean(opts.force),
2505
+ confirmOverwrite: async (name) => {
2506
+ const res = await prompts({
2507
+ type: "confirm",
2508
+ name: "overwrite",
2509
+ message: `${chalk.yellow(name)} already exists in ${opts.dir}. Overwrite?`,
2510
+ initial: false
2511
+ }, { onCancel: () => process.exit(130) });
2512
+ return Boolean(res.overwrite);
2513
+ }
2514
+ });
2515
+ for (const name of installed) console.log(`${chalk.green("✓")} ${name} → ${join(opts.dir, name)}`);
2516
+ for (const name of skipped) console.log(`${chalk.dim(`· skipped ${name} (kept existing)`)}`);
2517
+ const parts = [installed.length > 0 ? `${installed.length} installed` : null, skipped.length > 0 ? `${skipped.length} skipped` : null].filter(Boolean);
2518
+ console.log(`\n${chalk.bold(parts.join(", ") || "Nothing to do")} in ${targetRoot}`);
2519
+ if (installed.length > 0) console.log(chalk.dim("Restart your agent session to pick up the new skills."));
2520
+ });
2521
+ return skills;
2522
+ }
2523
+ //#endregion
2372
2524
  //#region src/commands/theme.ts
2373
2525
  function registerThemeCommand(ctx) {
2374
- const cmd = new Command("theme").description("Theme developer workflow — dev server, push, pull, lint, init");
2526
+ const cmd = new Command("theme").description("Theme developer workflow — dev server, push, pull, lint, init, skills");
2375
2527
  cmd.addCommand(createDevCommand());
2376
2528
  cmd.addCommand(createPushCommand());
2377
2529
  cmd.addCommand(createPullCommand());
2378
2530
  cmd.addCommand(createLintCommand());
2379
2531
  cmd.addCommand(createInitCommand());
2380
2532
  cmd.addCommand(createNavigateCommand());
2533
+ cmd.addCommand(createSkillsCommand());
2381
2534
  ctx.program.addCommand(cmd);
2382
2535
  }
2383
2536
  //#endregion