cronus-ui 0.6.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/dist/commands/add-page.d.ts +108 -0
  4. package/dist/commands/add-page.js +642 -0
  5. package/dist/commands/add.d.ts +9 -0
  6. package/dist/commands/add.js +114 -0
  7. package/dist/commands/ai.d.ts +14 -0
  8. package/dist/commands/ai.js +69 -0
  9. package/dist/commands/compose.d.ts +82 -0
  10. package/dist/commands/compose.js +403 -0
  11. package/dist/commands/diff.d.ts +8 -0
  12. package/dist/commands/diff.js +55 -0
  13. package/dist/commands/init.d.ts +9 -0
  14. package/dist/commands/init.js +53 -0
  15. package/dist/commands/list.d.ts +7 -0
  16. package/dist/commands/list.js +28 -0
  17. package/dist/commands/theme.d.ts +23 -0
  18. package/dist/commands/theme.js +735 -0
  19. package/dist/commands/upgrade.d.ts +51 -0
  20. package/dist/commands/upgrade.js +840 -0
  21. package/dist/compose/data-slots.d.ts +71 -0
  22. package/dist/compose/data-slots.js +104 -0
  23. package/dist/compose/manifest.d.ts +90 -0
  24. package/dist/compose/manifest.js +224 -0
  25. package/dist/compose/plan.d.ts +164 -0
  26. package/dist/compose/plan.js +506 -0
  27. package/dist/compose/preview.d.ts +10 -0
  28. package/dist/compose/preview.js +48 -0
  29. package/dist/compose/reload.d.ts +56 -0
  30. package/dist/compose/reload.js +138 -0
  31. package/dist/compose/render.d.ts +123 -0
  32. package/dist/compose/render.js +404 -0
  33. package/dist/compose/templates.d.ts +22 -0
  34. package/dist/compose/templates.js +76 -0
  35. package/dist/compose.d.ts +10 -0
  36. package/dist/compose.js +8 -0
  37. package/dist/config.d.ts +94 -0
  38. package/dist/config.js +38 -0
  39. package/dist/index.d.ts +3 -0
  40. package/dist/index.js +184 -0
  41. package/dist/registry.d.ts +59 -0
  42. package/dist/registry.js +96 -0
  43. package/dist/utils.d.ts +72 -0
  44. package/dist/utils.js +186 -0
  45. package/package.json +68 -0
  46. package/templates/apps/chat.json +44 -0
  47. package/templates/apps/finance.json +44 -0
  48. package/templates/apps/landing-agency.json +31 -0
  49. package/templates/apps/landing-agents.json +32 -0
  50. package/templates/apps/landing-broadcast.json +29 -0
  51. package/templates/apps/landing-care.json +28 -0
  52. package/templates/apps/landing-coverage.json +23 -0
  53. package/templates/apps/landing-docs.json +29 -0
  54. package/templates/apps/landing-glass.json +28 -0
  55. package/templates/apps/landing-ops.json +29 -0
  56. package/templates/apps/landing-premium.json +31 -0
  57. package/templates/apps/landing-secure.json +31 -0
  58. package/templates/apps/landing-shop.json +27 -0
  59. package/templates/apps/landing-studio.json +30 -0
  60. package/templates/apps/landing.json +23 -0
  61. package/templates/apps/mail.json +44 -0
  62. package/templates/apps/saas.json +64 -0
  63. package/templates/apps/store.json +75 -0
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Bundled app-template I/O. Lives here (not in commands/compose.ts) so
3
+ * `reloadManifest` can load a template without a circular import through the
4
+ * compose command module.
5
+ */
6
+ import { type AppManifest } from "./manifest.js";
7
+ /** Canonical `-y` / non-TTY compose target — never lexicographic first. */
8
+ export declare const DEFAULT_COMPOSE_TEMPLATE = "saas";
9
+ /** True when `name` is a safe templates/apps basename (no path traversal). */
10
+ export declare function isTemplateSlug(name: string): boolean;
11
+ /** List the bundled template names (basename without .json), sorted. */
12
+ export declare function listTemplates(): Promise<string[]>;
13
+ /**
14
+ * Template used when the user passes `--yes` or stdin is not a TTY.
15
+ * Prefers SaaS; falls back to the first bundled name if SaaS is missing.
16
+ */
17
+ export declare function defaultComposeTemplate(available: string[]): string;
18
+ /** Load + strictly parse a bundled template manifest by name. Throws on missing/invalid. */
19
+ export declare function loadTemplate(name: string): Promise<AppManifest>;
20
+ /** Load + strictly parse a manifest from an explicit file path. */
21
+ export declare function loadManifestFile(path: string): Promise<AppManifest>;
22
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Bundled app-template I/O. Lives here (not in commands/compose.ts) so
3
+ * `reloadManifest` can load a template without a circular import through the
4
+ * compose command module.
5
+ */
6
+ import { existsSync } from "node:fs";
7
+ import { readdir, readFile } from "node:fs/promises";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { closestName } from "../utils.js";
11
+ import { parseManifest } from "./manifest.js";
12
+ /** Directory holding the bundled app-template manifests (dist/ or src/ layout). */
13
+ function appsTemplatesDir() {
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ // compiled: dist/compose/templates.js → dist/../templates ; source: src/compose → ../templates
16
+ const compiled = join(here, "..", "..", "templates", "apps");
17
+ const fromSource = join(here, "..", "templates", "apps");
18
+ return [compiled, fromSource].find((p) => existsSync(p)) ?? compiled;
19
+ }
20
+ /** Canonical `-y` / non-TTY compose target — never lexicographic first. */
21
+ export const DEFAULT_COMPOSE_TEMPLATE = "saas";
22
+ const TEMPLATE_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
23
+ /** True when `name` is a safe templates/apps basename (no path traversal). */
24
+ export function isTemplateSlug(name) {
25
+ return TEMPLATE_SLUG.test(name);
26
+ }
27
+ /** List the bundled template names (basename without .json), sorted. */
28
+ export async function listTemplates() {
29
+ try {
30
+ return (await readdir(appsTemplatesDir()))
31
+ .filter((f) => f.endsWith(".json"))
32
+ .map((f) => f.slice(0, -".json".length))
33
+ .filter(isTemplateSlug)
34
+ .sort();
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ }
40
+ /**
41
+ * Template used when the user passes `--yes` or stdin is not a TTY.
42
+ * Prefers SaaS; falls back to the first bundled name if SaaS is missing.
43
+ */
44
+ export function defaultComposeTemplate(available) {
45
+ if (available.includes(DEFAULT_COMPOSE_TEMPLATE))
46
+ return DEFAULT_COMPOSE_TEMPLATE;
47
+ const first = available[0];
48
+ if (first === undefined)
49
+ throw new Error("no app templates are bundled with this CLI");
50
+ return first;
51
+ }
52
+ /** Load + strictly parse a bundled template manifest by name. Throws on missing/invalid. */
53
+ export async function loadTemplate(name) {
54
+ if (!isTemplateSlug(name)) {
55
+ throw new Error(`Unknown template "${name}".`);
56
+ }
57
+ const file = join(appsTemplatesDir(), `${name}.json`);
58
+ let raw;
59
+ try {
60
+ raw = await readFile(file, "utf8");
61
+ }
62
+ catch {
63
+ const available = await listTemplates();
64
+ const suggestion = closestName(name, available);
65
+ throw new Error(suggestion
66
+ ? `Unknown template "${name}". Did you mean "${suggestion}"? (available: ${available.join(", ")})`
67
+ : `Unknown template "${name}" (available: ${available.join(", ") || "none"}).`);
68
+ }
69
+ return parseManifest(JSON.parse(raw));
70
+ }
71
+ /** Load + strictly parse a manifest from an explicit file path. */
72
+ export async function loadManifestFile(path) {
73
+ const raw = await readFile(path, "utf8");
74
+ return parseManifest(JSON.parse(raw));
75
+ }
76
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Public library entry for programmatic composition (consumed by
3
+ * `create-cronus-app`'s post-scaffold step). Re-exports the pure apply core so a
4
+ * scaffolder can generate an app template into a freshly-created project without
5
+ * shelling out to the CLI.
6
+ */
7
+ export { type ComposeAppOptions, type ComposeAppResult, composeApp, listTemplates, loadTemplate, } from "./commands/compose.js";
8
+ export type { AppManifest } from "./compose/manifest.js";
9
+ export type { ComposeChoiceInput } from "./compose/plan.js";
10
+ //# sourceMappingURL=compose.d.ts.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Public library entry for programmatic composition (consumed by
3
+ * `create-cronus-app`'s post-scaffold step). Re-exports the pure apply core so a
4
+ * scaffolder can generate an app template into a freshly-created project without
5
+ * shelling out to the CLI.
6
+ */
7
+ export { composeApp, listTemplates, loadTemplate, } from "./commands/compose.js";
8
+ //# sourceMappingURL=compose.js.map
@@ -0,0 +1,94 @@
1
+ export declare const CONFIG_FILE = "cronus-ui.json";
2
+ export declare const CLI_VERSION = "0.6.0";
3
+ export declare const DEFAULT_REGISTRY = "https://raw.githubusercontent.com/pedrogbraz/cronus-ui/v0.6.0/registry";
4
+ /** Manifest entry `add`/`upgrade` record per installed registry item. */
5
+ export interface InstalledRecord {
6
+ /** Registry release the files came from (git tag without the leading "v"). */
7
+ version: string;
8
+ /** Project-relative paths the item wrote (e.g. "components/ui/button.tsx"). */
9
+ files: string[];
10
+ }
11
+ /**
12
+ * The normalized `choices` an app was composed with. Persisted with sorted keys
13
+ * (deterministic) so `compose --plan <dir>` clones reproduce the same plan and
14
+ * the 3-way page upgrade (F4) has a stable comparison base. Phase 1 fills `brand`
15
+ * (and, when passed, `seed`); `variants`/`pages` stay empty until F2.
16
+ */
17
+ export interface ComposedChoices {
18
+ /** Selected block variant per family slug (F2; `{}` in F1). */
19
+ variants: Record<string, string>;
20
+ /** The routes actually generated, in manifest order. */
21
+ pages: string[];
22
+ /** The `--brand` value baked into chrome/hero copy. */
23
+ brand: string;
24
+ /** Aesthetic PRNG seed (only present when the caller passed `--seed`). */
25
+ seed?: number;
26
+ }
27
+ /**
28
+ * Per-composed-app record written by `compose` (mirrors {@link InstalledRecord}
29
+ * for `installed`). `files` are the generated page/layout/chrome paths (NOT the
30
+ * installed blocks — those live in `installed`). The `.cronus-ui/base/<composed-key>/`
31
+ * snapshot (template name; legacy compose used the package.json name and F4
32
+ * falls back to that dir) holds the exact bytes for the F4 page-upgrade merge
33
+ * base.
34
+ */
35
+ export interface ComposedRecord {
36
+ /** Registry release the app was composed from (git tag without the leading "v"). */
37
+ version: string;
38
+ /** Manifest plan schema version the app was generated against. */
39
+ planVersion: number;
40
+ /** The normalized choices (sorted keys) the app was generated with. */
41
+ choices: ComposedChoices;
42
+ /** Project-relative paths the composer generated (pages, layouts, chrome wrappers). */
43
+ files: string[];
44
+ /**
45
+ * Content fingerprint of the manifest this app was composed from (compose
46
+ * provenance). `add-page` reloads a bundled template only when it matches this
47
+ * hash — so a `--manifest`-composed app whose `name` collides with a bundled
48
+ * template (store/landing/saas) is NOT silently reloaded from the wrong bundled
49
+ * manifest. Absent for apps composed before this field existed (legacy): the
50
+ * verification is then skipped (nothing to compare against).
51
+ */
52
+ manifestHash?: string;
53
+ }
54
+ export interface CronusUIConfig {
55
+ /** Import aliases used when rewriting component sources. */
56
+ aliases: {
57
+ ui: string;
58
+ lib: string;
59
+ blocks: string;
60
+ };
61
+ /** Filesystem paths (relative to cwd) where files are written. */
62
+ paths: {
63
+ ui: string;
64
+ lib: string;
65
+ blocks: string;
66
+ };
67
+ /** Registry source (http base URL or local directory). */
68
+ registry: string;
69
+ /** The app's theme preset + color mode (written by the scaffolder / `theme set`). */
70
+ theme?: {
71
+ name: string;
72
+ mode: string;
73
+ };
74
+ /**
75
+ * Install manifest: which items are installed, from which registry release,
76
+ * and which files they own. `upgrade` uses the recorded version as the merge
77
+ * base for its 3-way merge. Absent for installs made before this field
78
+ * existed ("legacy") — `upgrade` then falls back to a 2-way diff.
79
+ */
80
+ installed?: Record<string, InstalledRecord>;
81
+ /**
82
+ * Compose manifest: which app templates were generated into this project, the
83
+ * plan version + normalized choices, and the generated files each owns. Mirrors
84
+ * `installed` (round-tripped verbatim); the F4 page-upgrade reads it to know
85
+ * which pages to re-render against their `.cronus-ui/base/` snapshot.
86
+ */
87
+ composed?: Record<string, ComposedRecord>;
88
+ }
89
+ export declare const DEFAULT_CONFIG: CronusUIConfig;
90
+ export declare function configPath(cwd: string): string;
91
+ export declare function hasConfig(cwd: string): boolean;
92
+ export declare function readConfig(cwd: string): Promise<CronusUIConfig>;
93
+ export declare function writeConfig(cwd: string, config: CronusUIConfig): Promise<void>;
94
+ //# sourceMappingURL=config.d.ts.map
package/dist/config.js ADDED
@@ -0,0 +1,38 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ export const CONFIG_FILE = "cronus-ui.json";
5
+ export const CLI_VERSION = "0.6.0";
6
+ export const DEFAULT_REGISTRY = `https://raw.githubusercontent.com/pedrogbraz/cronus-ui/v${CLI_VERSION}/registry`;
7
+ export const DEFAULT_CONFIG = {
8
+ aliases: { ui: "@/components/ui", lib: "@/lib", blocks: "@/components/blocks" },
9
+ paths: { ui: "components/ui", lib: "lib", blocks: "components/blocks" },
10
+ registry: DEFAULT_REGISTRY,
11
+ };
12
+ export function configPath(cwd) {
13
+ return join(cwd, CONFIG_FILE);
14
+ }
15
+ export function hasConfig(cwd) {
16
+ return existsSync(configPath(cwd));
17
+ }
18
+ export async function readConfig(cwd) {
19
+ const raw = await readFile(configPath(cwd), "utf8");
20
+ const parsed = JSON.parse(raw);
21
+ return {
22
+ aliases: { ...DEFAULT_CONFIG.aliases, ...parsed.aliases },
23
+ paths: { ...DEFAULT_CONFIG.paths, ...parsed.paths },
24
+ registry: parsed.registry ?? DEFAULT_CONFIG.registry,
25
+ // Preserve the theme block when present so `add`/`init` round-trips never drop it.
26
+ ...(parsed.theme ? { theme: parsed.theme } : {}),
27
+ // Same for the install manifest — dropping it would silently downgrade every
28
+ // component to the legacy (2-way) upgrade path.
29
+ ...(parsed.installed ? { installed: parsed.installed } : {}),
30
+ // And for the compose manifest — dropping it would orphan generated pages
31
+ // from their base snapshot, breaking the composed-page 3-way upgrade (F4).
32
+ ...(parsed.composed ? { composed: parsed.composed } : {}),
33
+ };
34
+ }
35
+ export async function writeConfig(cwd, config) {
36
+ await writeFile(configPath(cwd), `${JSON.stringify(config, null, 2)}\n`, "utf8");
37
+ }
38
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { add } from "./commands/add.js";
4
+ import { addPageCommand } from "./commands/add-page.js";
5
+ import { aiAdd } from "./commands/ai.js";
6
+ import { compose } from "./commands/compose.js";
7
+ import { diff } from "./commands/diff.js";
8
+ import { init } from "./commands/init.js";
9
+ import { list } from "./commands/list.js";
10
+ import { themeAdd, themeSet } from "./commands/theme.js";
11
+ import { upgrade } from "./commands/upgrade.js";
12
+ import { CLI_VERSION } from "./config.js";
13
+ const program = new Command();
14
+ program
15
+ .name("cronus-ui")
16
+ .description("Add Cronus UI components to your project, shadcn-style.")
17
+ .version(CLI_VERSION);
18
+ program
19
+ .command("init")
20
+ .description("Set up cronus-ui.json, the cn() helper, and base dependencies.")
21
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
22
+ .option("-r, --registry <source>", "registry URL or local directory")
23
+ .option("-y, --yes", "overwrite an existing cronus-ui.json")
24
+ .option("--skip-install", "do not install base dependencies")
25
+ .action((opts) => init({
26
+ cwd: opts.cwd,
27
+ registry: opts.registry,
28
+ yes: opts.yes,
29
+ skipInstall: opts.skipInstall,
30
+ }));
31
+ program
32
+ .command("add")
33
+ .description("Add one or more components (resolves dependencies).")
34
+ .argument("[components...]", "component names")
35
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
36
+ .option("-r, --registry <source>", "registry URL or local directory")
37
+ .option("-o, --overwrite", "overwrite existing files")
38
+ .option("--skip-install", "do not install npm dependencies")
39
+ .action((components, opts) => add(components, {
40
+ cwd: opts.cwd,
41
+ registry: opts.registry,
42
+ overwrite: opts.overwrite,
43
+ skipInstall: opts.skipInstall,
44
+ }));
45
+ program
46
+ .command("compose")
47
+ .description("Generate a full app from a validated template (pages + chrome from installed blocks).")
48
+ .argument("[template]", "app template name (default with -y: saas)")
49
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
50
+ .option("-r, --registry <source>", "registry URL or local directory")
51
+ .option("-m, --manifest <file>", "compose from an explicit manifest file instead of a bundled template")
52
+ .option("--pages <list>", "comma-separated route subset (e.g. /,products,login)")
53
+ .option("--variant <pair...>", "block variant selection, e.g. --variant login=split (F2)")
54
+ .option("-b, --brand <name>", "brand wordmark baked into chrome/hero")
55
+ .option("-s, --seed <n>", "aesthetic PRNG seed (recorded for reproducibility)")
56
+ .option("-o, --overwrite", "overwrite existing files")
57
+ .option("--skip-install", "do not install npm dependencies")
58
+ .option("--dry-run", "print the validated plan + per-file preview, write nothing")
59
+ .option("-y, --yes", "non-interactive: pick saas if no template is given")
60
+ .action((template, opts) => compose(template, {
61
+ cwd: opts.cwd,
62
+ registry: opts.registry,
63
+ manifest: opts.manifest,
64
+ pages: opts.pages,
65
+ variant: opts.variant,
66
+ brand: opts.brand,
67
+ seed: opts.seed,
68
+ overwrite: opts.overwrite,
69
+ skipInstall: opts.skipInstall,
70
+ dryRun: opts.dryRun,
71
+ yes: opts.yes,
72
+ }));
73
+ program
74
+ .command("add-page")
75
+ .description("Add one page to an already-composed app (installs new blocks, updates the nav + composed record).")
76
+ .requiredOption("--route <route>", "route to add, e.g. /faq")
77
+ .requiredOption("--blocks <list>", "comma-separated blocks, e.g. faq,cta (or login=split)")
78
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
79
+ .option("-r, --registry <source>", "registry URL or local directory")
80
+ .option("--chrome <group>", "chrome group for the page (default: the app's first group)")
81
+ .option("--title <title>", "page <title> (default: Title-Cased route)")
82
+ .option("--nav <label>", "nav label; adds the page to the chrome nav")
83
+ .option("--app <name>", "which composed app to extend (required if the project has >1)")
84
+ .option("-m, --manifest <file>", "reload the manifest from a file (for --manifest-composed apps)")
85
+ .option("-o, --overwrite", "replace the page if the route already exists")
86
+ .option("--skip-install", "do not install npm dependencies")
87
+ .option("--dry-run", "print the plan + files that would be written, write nothing")
88
+ .action((opts) => addPageCommand({
89
+ cwd: opts.cwd,
90
+ route: opts.route,
91
+ blocks: opts.blocks,
92
+ chrome: opts.chrome,
93
+ title: opts.title,
94
+ nav: opts.nav,
95
+ app: opts.app,
96
+ manifest: opts.manifest,
97
+ overwrite: opts.overwrite,
98
+ skipInstall: opts.skipInstall,
99
+ dryRun: opts.dryRun,
100
+ registry: opts.registry,
101
+ }));
102
+ program
103
+ .command("list")
104
+ .alias("ls")
105
+ .description("List all components available in the registry.")
106
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
107
+ .option("-r, --registry <source>", "registry URL or local directory")
108
+ .action((opts) => list({ cwd: opts.cwd, registry: opts.registry }));
109
+ program
110
+ .command("diff")
111
+ .description("Show which installed components have drifted from the registry (run `cronus-ui upgrade` to merge updates without losing local edits).")
112
+ .argument("[components...]", "component names (default: all)")
113
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
114
+ .option("-r, --registry <source>", "registry URL or local directory")
115
+ .action((components, opts) => diff(components, { cwd: opts.cwd, registry: opts.registry }));
116
+ program
117
+ .command("upgrade")
118
+ .description("Upgrade installed components and composed pages/layouts to the current release, keeping your local edits.")
119
+ .argument("[components...]", "component names (or use --all)")
120
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
121
+ .option("-r, --registry <source>", "registry URL or local directory")
122
+ .option("-a, --all", "upgrade every component recorded in cronus-ui.json")
123
+ .option("--dry-run", "print the per-file plan (fast-forward / merge / conflict), write nothing")
124
+ .option("-y, --yes", "assume yes: write conflict markers and confirmed overwrites without asking")
125
+ .option("-o, --overwrite", "components installed before the manifest existed: replace local files with upstream")
126
+ .option("-m, --manifest <file>", "reload composed apps from this manifest file (for --manifest-composed apps)")
127
+ .addHelpText("after", `
128
+ How it works:
129
+ Each component's installed release is recorded in cronus-ui.json ("installed").
130
+ upgrade 3-way merges base (that release), local (your file) and upstream (the
131
+ current release) with \`git merge-file --diff3\`, so upstream fixes land WITHOUT
132
+ losing your edits. Clean merges are written; conflicts are written with markers
133
+ only if you confirm (or --yes). Unresolved files get a ready-to-paste coding
134
+ agent prompt in CRONUS-UPGRADE.md.
135
+
136
+ \`upgrade --all\` also 3-way-merges composed pages and layouts against their
137
+ \`.cronus-ui/base/<template>/\` snapshot (same decision matrix). Named
138
+ \`upgrade button\` does not touch composed pages. Custom --manifest apps must
139
+ re-supply --manifest.
140
+
141
+ Examples:
142
+ cronus-ui upgrade --all --dry-run preview the plan for everything installed
143
+ cronus-ui upgrade button card upgrade two components
144
+ cronus-ui upgrade --all --yes upgrade all, accept conflict markers`)
145
+ .action((components, opts) => upgrade(components, {
146
+ cwd: opts.cwd,
147
+ registry: opts.registry,
148
+ all: opts.all,
149
+ dryRun: opts.dryRun,
150
+ yes: opts.yes,
151
+ overwrite: opts.overwrite,
152
+ manifestPath: opts.manifest,
153
+ }));
154
+ const theme = program.command("theme").description("Manage the app's Cronus UI theme preset.");
155
+ theme
156
+ .command("set")
157
+ .description("Switch the theme preset (and optionally the color mode).")
158
+ .argument("<name>", "theme preset (aurora, neutral, midnight, sunset, emerald)")
159
+ .option("-m, --mode <mode>", "color mode (dark or light)")
160
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
161
+ .action((name, opts) => themeSet({ name, mode: opts.mode, cwd: opts.cwd }));
162
+ theme
163
+ .command("add")
164
+ .description("Apply a theme built in the Create Studio (permalink URL, bare c= payload, or exported theme JSON file).")
165
+ .argument("<source>", "Create Studio permalink, c= payload, or path to a theme JSON export")
166
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
167
+ .option("--css <file>", "globals CSS file for the overrides block (default: auto-detect)")
168
+ .option("--dry-run", "print what would be applied, write nothing")
169
+ .action((source, opts) => themeAdd({ source, cwd: opts.cwd, css: opts.css, dryRun: opts.dryRun }));
170
+ program
171
+ .command("ai")
172
+ .description("Add the AI Kit (AGENTS.md doctrine + Claude Code / Cursor / Copilot / Windsurf / Gemini config).")
173
+ .option("-c, --cwd <dir>", "working directory", process.cwd())
174
+ .option("-a, --assistants <list>", "comma-separated: claude, cursor, copilot, windsurf, gemini (or 'all'/'none')")
175
+ .option("-p, --preset <name>", "doctrine preset: standard, fintech, saas, oss, agency, or none", "standard")
176
+ .option("-s, --skills <list>", "comma-separated Claude Code skills (or 'all'/'none')")
177
+ .action((opts) => aiAdd({
178
+ cwd: opts.cwd,
179
+ assistants: opts.assistants,
180
+ preset: opts.preset,
181
+ skills: opts.skills,
182
+ }));
183
+ program.parseAsync(process.argv);
184
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,59 @@
1
+ /** A single file that ships with a registry item. */
2
+ export interface RegistryFile {
3
+ /** Path relative to the item kind (e.g. "button.tsx" or "cn.ts"). */
4
+ path: string;
5
+ /** Raw source, with canonical `../lib/cn.js` / `./x.js` specifiers. */
6
+ content: string;
7
+ /** Where it is written in the consumer. */
8
+ target: "ui" | "lib" | "block";
9
+ }
10
+ export type RegistryItemType = "registry:ui" | "registry:lib" | "registry:block";
11
+ export interface RegistryItem {
12
+ name: string;
13
+ type: RegistryItemType;
14
+ /** npm packages (with version ranges) this item imports. */
15
+ dependencies: string[];
16
+ /** other registry items this item imports (by name). */
17
+ registryDependencies: string[];
18
+ files: RegistryFile[];
19
+ }
20
+ export interface RegistryIndexEntry {
21
+ name: string;
22
+ type: RegistryItemType;
23
+ dependencies: string[];
24
+ registryDependencies: string[];
25
+ }
26
+ export type RegistryIndex = RegistryIndexEntry[];
27
+ /** Extract the release version a registry source is pinned to, if any. */
28
+ export declare function registrySourceVersion(source: string): string | undefined;
29
+ /**
30
+ * Re-pin a versioned registry source to a different release tag. Because the
31
+ * default registry lives at `raw.githubusercontent.com/.../v<version>/registry`,
32
+ * every published release's registry stays addressable forever — `upgrade`
33
+ * uses this to fetch the exact merge base a component was installed from.
34
+ * Returns undefined when the source carries no version segment (an unversioned
35
+ * URL or plain local directory), in which case past releases are unreachable.
36
+ */
37
+ export declare function registrySourceAtVersion(source: string, version: string): string | undefined;
38
+ /** A registry source: either a local directory or an http(s) base URL. */
39
+ export declare class Registry {
40
+ private readonly base;
41
+ private readonly isUrl;
42
+ private cache;
43
+ constructor(base: string);
44
+ private readJson;
45
+ index(): Promise<RegistryIndex>;
46
+ /**
47
+ * The `registry/meta.json` sidecar, or `null` when the registry does not ship
48
+ * one (older releases / custom registries). Mirrors the MCP client's graceful
49
+ * degradation so `compose` can fail with a clear message rather than a raw 404.
50
+ */
51
+ meta<T = unknown>(): Promise<T | null>;
52
+ item(name: string): Promise<RegistryItem>;
53
+ /**
54
+ * Resolve the full transitive closure of registry items needed for `names`,
55
+ * returned in dependency-first order (so files write cleanly).
56
+ */
57
+ resolve(names: string[]): Promise<RegistryItem[]>;
58
+ }
59
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ /**
4
+ * Matches a release-pinned "/vX.Y.Z/" path segment in a registry source, e.g.
5
+ * ".../cronus-ui/v0.5.0/registry" (the default raw.githubusercontent.com layout)
6
+ * or a local ".../v0.5.0/registry" mirror. The trailing separator is a
7
+ * lookahead so replacements never eat it.
8
+ */
9
+ const VERSION_SEGMENT_RE = /([/\\])v(\d+\.\d+\.\d+(?:-[\w.]+)?)(?=[/\\])/;
10
+ /** Extract the release version a registry source is pinned to, if any. */
11
+ export function registrySourceVersion(source) {
12
+ return VERSION_SEGMENT_RE.exec(source)?.[2];
13
+ }
14
+ /**
15
+ * Re-pin a versioned registry source to a different release tag. Because the
16
+ * default registry lives at `raw.githubusercontent.com/.../v<version>/registry`,
17
+ * every published release's registry stays addressable forever — `upgrade`
18
+ * uses this to fetch the exact merge base a component was installed from.
19
+ * Returns undefined when the source carries no version segment (an unversioned
20
+ * URL or plain local directory), in which case past releases are unreachable.
21
+ */
22
+ export function registrySourceAtVersion(source, version) {
23
+ if (!VERSION_SEGMENT_RE.test(source))
24
+ return undefined;
25
+ return source.replace(VERSION_SEGMENT_RE, `$1v${version}`);
26
+ }
27
+ /** A registry source: either a local directory or an http(s) base URL. */
28
+ export class Registry {
29
+ base;
30
+ isUrl;
31
+ cache = new Map();
32
+ constructor(base) {
33
+ this.isUrl = /^https?:\/\//.test(base);
34
+ this.base = this.isUrl ? base.replace(/\/$/, "") : isAbsolute(base) ? base : resolve(base);
35
+ }
36
+ async readJson(file) {
37
+ if (this.isUrl) {
38
+ const res = await fetch(`${this.base}/${file}`);
39
+ if (!res.ok)
40
+ throw new Error(`registry fetch failed (${res.status}): ${this.base}/${file}`);
41
+ return (await res.json());
42
+ }
43
+ const raw = await readFile(join(this.base, file), "utf8");
44
+ return JSON.parse(raw);
45
+ }
46
+ async index() {
47
+ return this.readJson("index.json");
48
+ }
49
+ /**
50
+ * The `registry/meta.json` sidecar, or `null` when the registry does not ship
51
+ * one (older releases / custom registries). Mirrors the MCP client's graceful
52
+ * degradation so `compose` can fail with a clear message rather than a raw 404.
53
+ */
54
+ async meta() {
55
+ try {
56
+ return await this.readJson("meta.json");
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ async item(name) {
63
+ const cached = this.cache.get(name);
64
+ if (cached)
65
+ return cached;
66
+ const item = await this.readJson(`${name}.json`);
67
+ this.cache.set(name, item);
68
+ return item;
69
+ }
70
+ /**
71
+ * Resolve the full transitive closure of registry items needed for `names`,
72
+ * returned in dependency-first order (so files write cleanly).
73
+ */
74
+ async resolve(names) {
75
+ const ordered = [];
76
+ const seen = new Set();
77
+ const visiting = new Set();
78
+ const visit = async (name) => {
79
+ if (seen.has(name))
80
+ return;
81
+ if (visiting.has(name))
82
+ return; // guard against cycles
83
+ visiting.add(name);
84
+ const item = await this.item(name);
85
+ for (const dep of item.registryDependencies)
86
+ await visit(dep);
87
+ visiting.delete(name);
88
+ seen.add(name);
89
+ ordered.push(item);
90
+ };
91
+ for (const name of names)
92
+ await visit(name);
93
+ return ordered;
94
+ }
95
+ }
96
+ //# sourceMappingURL=registry.js.map