@sparkletree/cli 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SparkleTree
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @sparkletree/cli
2
+
3
+ `sparkletree add` — copy SparkleTree component source into your repo, in the
4
+ shadcn/ui style. No install needed:
5
+
6
+ ```bash
7
+ npx @sparkletree/cli add hero cta
8
+ ```
9
+
10
+ ## Commands
11
+
12
+ ```
13
+ sparkletree add <component...> [options]
14
+ sparkletree list
15
+ sparkletree help
16
+ ```
17
+
18
+ | Option | |
19
+ |---|---|
20
+ | `--dir <path>` | Where components are written. Default `components`. |
21
+ | `--cwd <path>` | Project root. Default: current directory. |
22
+ | `--force` | Overwrite files that already exist. |
23
+ | `--dry-run` | Show what would happen; write nothing. |
24
+
25
+ Dependencies are resolved for you — asking for `hero` also writes `provider` if
26
+ you do not have it.
27
+
28
+ ## Components
29
+
30
+ | | |
31
+ |---|---|
32
+ | `provider` | The `SparkletreeProvider` wrapper, reading your env vars. |
33
+ | `hero` | Streaming hero: headline, body, art. |
34
+ | `cta` | Streaming call-to-action. |
35
+ | `copy-card` | The copy-card creative. |
36
+ | `product-card` | Product creative with price and CTA. |
37
+
38
+ The copies require Tailwind CSS for their default classes. Swap them freely —
39
+ they are yours.
40
+
41
+ ## What "yours" means, and the one thing it does not include
42
+
43
+ Copied components are your code: we cannot patch them, deprecate them, or know
44
+ what they look like a year from now. So they are **wire-ignorant by
45
+ construction** — no event names, no protocol versions, no transports, no
46
+ endpoints, no timing constants. A CI test enforces this per component.
47
+
48
+ Everything that speaks the wire stays in
49
+ [`@sparkletree/react`](https://www.npmjs.com/package/@sparkletree/react) and
50
+ [`@sparkletree/core`](https://www.npmjs.com/package/@sparkletree/core), which
51
+ the copies import from. That is what buys the property that makes copy-paste
52
+ distribution safe: **a protocol change is a version bump, never a migration
53
+ guide.**
54
+
55
+ It also means the adaptation mark is not in the copied source. It renders
56
+ inside `StreamText`, in npm, and deleting it from your copy is not possible
57
+ because it was never there. That is deliberate — see rule 4 in the
58
+ [SDK README](https://gitlab.com/sparkletree/sparkletree-sdk/-/blob/main/README.md).
59
+
60
+ ## Choose one distribution, not both
61
+
62
+ Either import components from `@sparkletree/react`, or copy them with this CLI
63
+ and import from `./components/sparkletree/…`. Doing both gives you two Heroes
64
+ with the same name and a confusing diff.
65
+
66
+ ## Requirements
67
+
68
+ Node >= 18. ESM only.
package/dist/add.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `sparkletree add <component…>`
3
+ *
4
+ * Copies component source into the project. Everything about how it behaves
5
+ * follows from one principle: THIS WRITES TO SOMEONE ELSE'S REPOSITORY.
6
+ *
7
+ * - never overwrite without `--force`. The file may be three months of the
8
+ * customer's edits, and "the CLI reverted my hero" is unrecoverable if we
9
+ * wrote first and asked later;
10
+ * - report the dependencies to install rather than running a package
11
+ * manager. Which one, which lockfile, which workspace flag — those are
12
+ * theirs to decide, and guessing wrong dirties a lockfile in a PR;
13
+ * - `--dry-run` shows exactly what would happen and touches nothing.
14
+ */
15
+ import { type RegistryComponent } from "./registry.js";
16
+ export interface AddOptions {
17
+ /** Project root. Defaults to the working directory. */
18
+ cwd?: string;
19
+ /** Where components land, relative to cwd. */
20
+ targetDir?: string;
21
+ force?: boolean;
22
+ dryRun?: boolean;
23
+ }
24
+ export interface AddResult {
25
+ written: string[];
26
+ skipped: {
27
+ path: string;
28
+ reason: string;
29
+ }[];
30
+ dependencies: string[];
31
+ components: RegistryComponent[];
32
+ }
33
+ export declare function add(names: string[], options?: AddOptions): Promise<AddResult>;
34
+ /** Human-readable summary. Mirrors what `add` actually did. */
35
+ export declare function formatAddResult(result: AddResult, dryRun?: boolean): string;
36
+ /** Where `add` would write, for `--help` and for tests. */
37
+ export declare function targetPathFor(component: RegistryComponent, targetDir?: string): string;
38
+ //# sourceMappingURL=add.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../src/add.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEhF,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,iBAAiB,EAAE,CAAC;CACjC;AAYD,wBAAsB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAqDvF;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,UAAQ,GAAG,MAAM,CA6BzE;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,SAAS,EAAE,iBAAiB,EAAE,SAAS,SAAe,GAAG,MAAM,CAE5F"}
package/dist/add.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `sparkletree add <component…>`
3
+ *
4
+ * Copies component source into the project. Everything about how it behaves
5
+ * follows from one principle: THIS WRITES TO SOMEONE ELSE'S REPOSITORY.
6
+ *
7
+ * - never overwrite without `--force`. The file may be three months of the
8
+ * customer's edits, and "the CLI reverted my hero" is unrecoverable if we
9
+ * wrote first and asked later;
10
+ * - report the dependencies to install rather than running a package
11
+ * manager. Which one, which lockfile, which workspace flag — those are
12
+ * theirs to decide, and guessing wrong dirties a lockfile in a PR;
13
+ * - `--dry-run` shows exactly what would happen and touches nothing.
14
+ */
15
+ import { mkdir, stat, writeFile } from "node:fs/promises";
16
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
17
+ import { resolveWithDependencies } from "./registry.js";
18
+ /** What already sits at a path, without reading its contents. */
19
+ async function pathKind(path) {
20
+ try {
21
+ const info = await stat(path);
22
+ return info.isDirectory() ? "directory" : "file";
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export async function add(names, options = {}) {
29
+ const cwd = options.cwd ?? process.cwd();
30
+ const targetDir = options.targetDir ?? "components";
31
+ const components = resolveWithDependencies(names);
32
+ const written = [];
33
+ const skipped = [];
34
+ const dependencies = new Set();
35
+ for (const component of components) {
36
+ for (const dependency of component.dependencies)
37
+ dependencies.add(dependency);
38
+ for (const file of component.files) {
39
+ const absolute = resolve(cwd, targetDir, file.path);
40
+ // Refuse to escape the target directory. Registry paths are ours today,
41
+ // but a path-traversal check on a tool that writes files is not
42
+ // something to add after it matters. The check is against ".." as a
43
+ // path segment — a filename that merely STARTS with dots ("..foo.tsx")
44
+ // is still inside the target and must not trip it.
45
+ const relativeToTarget = relative(resolve(cwd, targetDir), absolute);
46
+ if (relativeToTarget.startsWith(".." + sep) || isAbsolute(relativeToTarget)) {
47
+ throw new Error(`Registry path escapes the target directory: ${file.path}`);
48
+ }
49
+ // A directory at the target path is never overwritten, --force or not:
50
+ // writeFile would die with a raw EISDIR, and recursive removal of a
51
+ // customer's directory is not this tool's call to make.
52
+ const existing = await pathKind(absolute);
53
+ if (existing === "directory") {
54
+ skipped.push({
55
+ path: relative(cwd, absolute),
56
+ reason: "a directory exists at this path — remove it or choose another --dir",
57
+ });
58
+ continue;
59
+ }
60
+ if (!options.force && existing === "file") {
61
+ skipped.push({
62
+ path: relative(cwd, absolute),
63
+ reason: "already exists — pass --force to overwrite",
64
+ });
65
+ continue;
66
+ }
67
+ if (!options.dryRun) {
68
+ await mkdir(dirname(absolute), { recursive: true });
69
+ await writeFile(absolute, file.content, "utf8");
70
+ }
71
+ written.push(relative(cwd, absolute));
72
+ }
73
+ }
74
+ return { written, skipped, dependencies: [...dependencies], components };
75
+ }
76
+ /** Human-readable summary. Mirrors what `add` actually did. */
77
+ export function formatAddResult(result, dryRun = false) {
78
+ const lines = [];
79
+ const verb = dryRun ? "Would write" : "Wrote";
80
+ if (result.written.length) {
81
+ lines.push(`${verb}:`);
82
+ for (const path of result.written)
83
+ lines.push(` ${path}`);
84
+ }
85
+ if (result.skipped.length) {
86
+ lines.push("", "Skipped:");
87
+ for (const item of result.skipped)
88
+ lines.push(` ${item.path} — ${item.reason}`);
89
+ }
90
+ if (result.dependencies.length) {
91
+ lines.push("", "Install these if you haven't already:", ` ${result.dependencies.join(" ")}`);
92
+ }
93
+ lines.push("", "These files are yours — edit them freely. Streaming, timing and the", "adaptation mark live in @sparkletree/core, so upgrades reach you without", "re-copying anything.", "", "The default classNames assume Tailwind CSS — swap them freely if you", "style with something else.");
94
+ return lines.join("\n");
95
+ }
96
+ /** Where `add` would write, for `--help` and for tests. */
97
+ export function targetPathFor(component, targetDir = "components") {
98
+ return join(targetDir, component.files[0].path);
99
+ }
100
+ //# sourceMappingURL=add.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add.js","sourceRoot":"","sources":["../src/add.ts"],"names":[],"mappings":"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"}
package/dist/bin.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `sparkletree` binary.
4
+ *
5
+ * Argument parsing by hand rather than a dependency: this CLI has three
6
+ * commands and a handful of flags, and a customer running `npx @sparkletree/cli
7
+ * add hero` should not be installing a parser to do it.
8
+ */
9
+ interface ParsedArgs {
10
+ command: string;
11
+ names: string[];
12
+ dir?: string;
13
+ cwd?: string;
14
+ force: boolean;
15
+ dryRun: boolean;
16
+ help: boolean;
17
+ version: boolean;
18
+ }
19
+ export declare function parseArgs(argv: string[]): ParsedArgs;
20
+ declare function main(argv: string[]): Promise<number>;
21
+ export { main };
22
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AA6BH,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAeD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAuBpD;AAaD,iBAAe,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuEnD;AA6CD,OAAO,EAAE,IAAI,EAAE,CAAC"}
package/dist/bin.js ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `sparkletree` binary.
4
+ *
5
+ * Argument parsing by hand rather than a dependency: this CLI has three
6
+ * commands and a handful of flags, and a customer running `npx @sparkletree/cli
7
+ * add hero` should not be installing a parser to do it.
8
+ */
9
+ import { realpathSync, readFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { add, formatAddResult } from "./add.js";
13
+ import { REGISTRY, findComponent } from "./registry.js";
14
+ const HELP = `sparkletree — add SparkleTree components to your project
15
+
16
+ Usage
17
+ sparkletree add <component...> [options]
18
+ sparkletree list
19
+ sparkletree help
20
+
21
+ Options
22
+ --dir <path> where components are written (default: components)
23
+ --cwd <path> project root (default: current directory)
24
+ --force overwrite files that already exist
25
+ --dry-run show what would happen; write nothing
26
+ -h, --help show this help
27
+ --version show the CLI version
28
+
29
+ Examples
30
+ npx @sparkletree/cli add hero cta
31
+ npx @sparkletree/cli add product-card --dir src/components
32
+ `;
33
+ /**
34
+ * The value after a value-taking flag. `--dir --force` must fail loudly, not
35
+ * write into a directory literally named "--force"; a trailing `--dir` must
36
+ * fail loudly, not become `undefined` and silently mean the default.
37
+ */
38
+ function optionValue(flag, rest, index) {
39
+ const value = rest[index];
40
+ if (value === undefined || value.startsWith("--")) {
41
+ throw new Error(`Option ${flag} needs a value, got ${value ?? "end of arguments"}`);
42
+ }
43
+ return value;
44
+ }
45
+ export function parseArgs(argv) {
46
+ const [command = "help", ...rest] = argv;
47
+ const parsed = {
48
+ command,
49
+ names: [],
50
+ force: false,
51
+ dryRun: false,
52
+ help: false,
53
+ version: false,
54
+ };
55
+ for (let index = 0; index < rest.length; index += 1) {
56
+ const arg = rest[index];
57
+ if (arg === "--force")
58
+ parsed.force = true;
59
+ else if (arg === "--dry-run")
60
+ parsed.dryRun = true;
61
+ else if (arg === "--help" || arg === "-h")
62
+ parsed.help = true;
63
+ else if (arg === "--version")
64
+ parsed.version = true;
65
+ else if (arg === "--dir")
66
+ parsed.dir = optionValue("--dir", rest, ++index);
67
+ else if (arg === "--cwd")
68
+ parsed.cwd = optionValue("--cwd", rest, ++index);
69
+ else if (arg.startsWith("--"))
70
+ throw new Error(`Unknown option ${arg}`);
71
+ else
72
+ parsed.names.push(arg);
73
+ }
74
+ return parsed;
75
+ }
76
+ /** The CLI's own version, read from the package.json the build ships next to. */
77
+ function cliVersion() {
78
+ try {
79
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
80
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
81
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
82
+ }
83
+ catch {
84
+ return "unknown";
85
+ }
86
+ }
87
+ async function main(argv) {
88
+ let args;
89
+ try {
90
+ args = parseArgs(argv);
91
+ }
92
+ catch (error) {
93
+ console.error(error instanceof Error ? error.message : String(error));
94
+ console.error("\n" + HELP);
95
+ return 1;
96
+ }
97
+ // --help and --version work from any position: `sparkletree add --help`
98
+ // shows usage rather than dying on an "unknown option".
99
+ if (args.help) {
100
+ console.log(HELP);
101
+ return 0;
102
+ }
103
+ if (args.version) {
104
+ console.log(cliVersion());
105
+ return 0;
106
+ }
107
+ switch (args.command) {
108
+ case "list": {
109
+ console.log("Available components:\n");
110
+ for (const component of REGISTRY) {
111
+ console.log(` ${component.name.padEnd(14)} ${component.description}`);
112
+ }
113
+ return 0;
114
+ }
115
+ case "add": {
116
+ if (!args.names.length) {
117
+ console.error("Which component? Try `sparkletree list`.\n");
118
+ console.error(HELP);
119
+ return 1;
120
+ }
121
+ const unknown = args.names.filter((name) => !findComponent(name));
122
+ if (unknown.length) {
123
+ console.error(`Unknown component(s): ${unknown.join(", ")}`);
124
+ console.error(`Available: ${REGISTRY.map((c) => c.name).join(", ")}`);
125
+ return 1;
126
+ }
127
+ const result = await add(args.names, {
128
+ cwd: args.cwd,
129
+ targetDir: args.dir,
130
+ force: args.force,
131
+ dryRun: args.dryRun,
132
+ });
133
+ console.log(formatAddResult(result, args.dryRun));
134
+ // Skipped files are not a failure: re-running `add` on a project that
135
+ // already has the component is a normal thing to do, and exiting
136
+ // non-zero would break it in CI for no reason.
137
+ return 0;
138
+ }
139
+ case "help":
140
+ case "--help":
141
+ case "-h":
142
+ console.log(HELP);
143
+ return 0;
144
+ case "--version":
145
+ console.log(cliVersion());
146
+ return 0;
147
+ default:
148
+ console.error(`Unknown command "${args.command}"\n`);
149
+ console.error(HELP);
150
+ return 1;
151
+ }
152
+ }
153
+ /**
154
+ * Are we the entry point, or is something importing us?
155
+ *
156
+ * Importing this module (tests, programmatic use) must not run a command or
157
+ * call process.exit — but the check has to survive how package managers
158
+ * actually install a `bin`.
159
+ *
160
+ * npm installs it as a bare SYMLINK: `node_modules/.bin/sparkletree` →
161
+ * `dist/bin.js`. Node then resolves `import.meta.url` through the symlink to
162
+ * the real file while leaving `process.argv[1]` as the symlink path. Comparing
163
+ * the two textually — which this used to do, against argv[1]'s basename —
164
+ * asks whether "…/dist/bin.js" ends with "sparkletree". It does not, so
165
+ * `npx sparkletree` parsed nothing, printed nothing, and exited 0. A silent
166
+ * no-op on the one command every consumer runs first.
167
+ *
168
+ * (pnpm masked it: pnpm writes a shell shim that execs the real path, so
169
+ * argv[1] and import.meta.url already agree. So do all our own tests, which
170
+ * import this module rather than spawning the installed binary.)
171
+ *
172
+ * realpath both sides and compare. Symlink, shim or direct — they agree.
173
+ */
174
+ function isEntryPoint() {
175
+ const entry = process.argv[1];
176
+ if (!entry)
177
+ return false;
178
+ try {
179
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
180
+ }
181
+ catch {
182
+ // A deleted or unreadable argv[1] is not us.
183
+ return false;
184
+ }
185
+ }
186
+ if (isEntryPoint()) {
187
+ main(process.argv.slice(2))
188
+ .then((code) => {
189
+ process.exitCode = code;
190
+ })
191
+ .catch((error) => {
192
+ console.error(error instanceof Error ? error.message : String(error));
193
+ process.exitCode = 1;
194
+ });
195
+ }
196
+ export { main };
197
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @sparkletree/cli — programmatic surface.
3
+ *
4
+ * The binary is a thin wrapper over these. Exported so the parity harness and
5
+ * anyone scripting a scaffold can use `add` without shelling out.
6
+ */
7
+ export { add, formatAddResult, targetPathFor, type AddOptions, type AddResult } from "./add.js";
8
+ export { REGISTRY, findComponent, resolveWithDependencies, type RegistryComponent, } from "./registry.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAChG,OAAO,EACL,QAAQ,EACR,aAAa,EACb,uBAAuB,EACvB,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @sparkletree/cli — programmatic surface.
3
+ *
4
+ * The binary is a thin wrapper over these. Exported so the parity harness and
5
+ * anyone scripting a scaffold can use `add` without shelling out.
6
+ */
7
+ export { add, formatAddResult, targetPathFor } from "./add.js";
8
+ export { REGISTRY, findComponent, resolveWithDependencies, } from "./registry.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,aAAa,EAAmC,MAAM,UAAU,CAAC;AAChG,OAAO,EACL,QAAQ,EACR,aAAa,EACb,uBAAuB,GAExB,MAAM,eAAe,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The component registry.
3
+ *
4
+ * `sparkletree add hero` copies source into the customer's repo, shadcn-style.
5
+ * Those copies are theirs: we cannot patch them, cannot deprecate them, and
6
+ * cannot assume anything about what they have become by the time we ship the
7
+ * next protocol version.
8
+ *
9
+ * Which is the whole reason for the rule enforced in `registry.test.ts`:
10
+ *
11
+ * COPIES ARE WIRE-IGNORANT. ONLY NPM SPEAKS THE WIRE.
12
+ *
13
+ * A copied component may import from `@sparkletree/react` and
14
+ * `@sparkletree/core`. It may not contain an event name, a protocol version,
15
+ * a timing constant, or a URL. The moment one does, a protocol change stops
16
+ * being a version bump and becomes a migration guide addressed to every
17
+ * customer who ever ran `add` — and they will not all read it.
18
+ *
19
+ * The registry ships as data, not as files fetched at install time: a CLI that
20
+ * pulls source off the network at `add` time is a supply-chain hole in every
21
+ * customer's build.
22
+ */
23
+ export interface RegistryComponent {
24
+ name: string;
25
+ description: string;
26
+ /** Files written, relative to the target directory. */
27
+ files: {
28
+ path: string;
29
+ content: string;
30
+ }[];
31
+ /** npm packages the copy needs. */
32
+ dependencies: string[];
33
+ /** Other registry components this one requires. */
34
+ registryDependencies: string[];
35
+ }
36
+ export declare const REGISTRY: readonly RegistryComponent[];
37
+ export declare function findComponent(name: string): RegistryComponent | undefined;
38
+ /**
39
+ * Resolve a component plus everything it needs, dependencies first.
40
+ *
41
+ * Depth-first with a visited set, so a diamond in the graph copies each file
42
+ * once and a cycle terminates rather than recursing until the stack gives out.
43
+ */
44
+ export declare function resolveWithDependencies(names: string[]): RegistryComponent[];
45
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3C,mCAAmC;IACnC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mDAAmD;IACnD,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC;AAsWD,eAAO,MAAM,QAAQ,EAAE,SAAS,iBAAiB,EAoChD,CAAC;AAEF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,iBAAiB,EAAE,CAe5E"}
@@ -0,0 +1,438 @@
1
+ /**
2
+ * The component registry.
3
+ *
4
+ * `sparkletree add hero` copies source into the customer's repo, shadcn-style.
5
+ * Those copies are theirs: we cannot patch them, cannot deprecate them, and
6
+ * cannot assume anything about what they have become by the time we ship the
7
+ * next protocol version.
8
+ *
9
+ * Which is the whole reason for the rule enforced in `registry.test.ts`:
10
+ *
11
+ * COPIES ARE WIRE-IGNORANT. ONLY NPM SPEAKS THE WIRE.
12
+ *
13
+ * A copied component may import from `@sparkletree/react` and
14
+ * `@sparkletree/core`. It may not contain an event name, a protocol version,
15
+ * a timing constant, or a URL. The moment one does, a protocol change stops
16
+ * being a version bump and becomes a migration guide addressed to every
17
+ * customer who ever ran `add` — and they will not all read it.
18
+ *
19
+ * The registry ships as data, not as files fetched at install time: a CLI that
20
+ * pulls source off the network at `add` time is a supply-chain hole in every
21
+ * customer's build.
22
+ */
23
+ const HERO_TSX = `"use client";
24
+
25
+ import { StreamText, useIsland, type UseIslandOptions } from "@sparkletree/react";
26
+ import { themeStyle } from "@sparkletree/core";
27
+
28
+ /**
29
+ * Your hero. Edit freely — this file is yours.
30
+ *
31
+ * What you should NOT need to touch, and what you will not find here: when the
32
+ * copy paints, how long it holds, whether it types or lands composed, and
33
+ * whether it carries the adaptation mark. All of that lives in
34
+ * @sparkletree/core and @sparkletree/react so it can be fixed for you without
35
+ * you re-copying this file.
36
+ *
37
+ * \`fallback\` is required. There is no loading state and no skeleton: if the
38
+ * network is slow or the campaign is unreachable, this copy is what your
39
+ * visitor reads. Write it as if it were the only version — often it is.
40
+ *
41
+ * Requires Tailwind CSS for the default classes — swap them freely.
42
+ */
43
+ export interface HeroProps {
44
+ campaignId: string;
45
+ /** First paint. Real copy, never a placeholder. */
46
+ fallback: UseIslandOptions["fallback"];
47
+ }
48
+
49
+ /**
50
+ * Server-driven URLs are configuration, not trust. Only http(s), mailto and
51
+ * tel absolutes and relative navigations ("/", "./", "#", "?") pass; anything
52
+ * else — javascript:, data:, an unprefixed string — is dropped.
53
+ */
54
+ function safeActionUrl(url: string | null | undefined): string | undefined {
55
+ if (!url) return undefined;
56
+ if (
57
+ (url.startsWith("/") && !url.startsWith("//")) ||
58
+ url.startsWith("./") ||
59
+ url.startsWith("#") ||
60
+ url.startsWith("?")
61
+ ) {
62
+ return url;
63
+ }
64
+ try {
65
+ const protocol = new URL(url).protocol;
66
+ if (["https:", "http:", "mailto:", "tel:"].includes(protocol)) return url;
67
+ } catch {
68
+ // Not a parseable URL at all.
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ export function Hero({ campaignId, fallback }: HeroProps) {
74
+ const { state, ref } = useIsland({ campaignId, island: "hero", fallback });
75
+ const backgroundImage = safeActionUrl(state.backgroundImage);
76
+
77
+ return (
78
+ <section
79
+ ref={ref}
80
+ className="relative overflow-hidden rounded-2xl p-10"
81
+ style={themeStyle(state.theme)}
82
+ >
83
+ {backgroundImage ? (
84
+ <img
85
+ src={backgroundImage}
86
+ alt=""
87
+ className="absolute inset-0 h-full w-full object-cover"
88
+ />
89
+ ) : null}
90
+
91
+ <div className="relative">
92
+ {state.fields.greeting.text ? (
93
+ <StreamText
94
+ field={state.fields.greeting}
95
+ as="p"
96
+ className="text-sm uppercase tracking-wide opacity-80"
97
+ />
98
+ ) : null}
99
+
100
+ {/* h2 to match the packaged Hero. The heading level is yours: pass
101
+ as="h1" if this section is your page's primary heading. */}
102
+ <StreamText
103
+ field={state.fields.headline}
104
+ as="h2"
105
+ className="mt-2 text-4xl font-semibold"
106
+ />
107
+
108
+ {state.fields.body.text ? (
109
+ <StreamText field={state.fields.body} as="p" className="mt-4 max-w-prose" />
110
+ ) : null}
111
+ </div>
112
+ </section>
113
+ );
114
+ }
115
+ `;
116
+ const CTA_TSX = `"use client";
117
+
118
+ import { StreamText, useIsland, useSparkletree, type UseIslandOptions } from "@sparkletree/react";
119
+ import { readableTextColor, themeStyle, track } from "@sparkletree/core";
120
+
121
+ /**
122
+ * Your call to action. Edit freely.
123
+ *
124
+ * The button keeps its size while the label streams in, so the surrounding
125
+ * layout does not shift under the visitor's cursor mid-click.
126
+ *
127
+ * Requires Tailwind CSS for the default classes — swap them freely.
128
+ */
129
+ export interface CtaProps extends Omit<UseIslandOptions, "island"> {
130
+ /** Where the button goes when the server has not resolved an action. */
131
+ href?: string;
132
+ onClick?: () => void;
133
+ }
134
+
135
+ /**
136
+ * Server-driven URLs are configuration, not trust. Only http(s), mailto and
137
+ * tel absolutes and relative navigations ("/", "./", "#", "?") pass; anything
138
+ * else — javascript:, data:, an unprefixed string — is dropped.
139
+ */
140
+ function safeActionUrl(url: string | null | undefined): string | undefined {
141
+ if (!url) return undefined;
142
+ if (
143
+ (url.startsWith("/") && !url.startsWith("//")) ||
144
+ url.startsWith("./") ||
145
+ url.startsWith("#") ||
146
+ url.startsWith("?")
147
+ ) {
148
+ return url;
149
+ }
150
+ try {
151
+ const protocol = new URL(url).protocol;
152
+ if (["https:", "http:", "mailto:", "tel:"].includes(protocol)) return url;
153
+ } catch {
154
+ // Not a parseable URL at all.
155
+ }
156
+ return undefined;
157
+ }
158
+
159
+ export function Cta(props: CtaProps) {
160
+ const { href, onClick, ...islandOptions } = props;
161
+ const { apiBase, organizationId } = useSparkletree();
162
+ const { state, ref } = useIsland({ ...islandOptions, island: "cta" });
163
+ // A rejected server action falls back to the href prop you control. The
164
+ // prop is vetted too — the same render path, the same rules, one outcome.
165
+ const action = safeActionUrl(state.ctaAction) ?? safeActionUrl(href);
166
+ const { campaignId, surfaceId } = islandOptions;
167
+ const variantId = state.variantId ?? undefined;
168
+ // A surface-only Cta has no campaignId prop, but the click must still be
169
+ // attributed: the server names the campaign it served in the terminal
170
+ // done.meta, mirroring the impression path in useIsland.
171
+ const metaCampaignId =
172
+ typeof state.meta?.campaignId === "string" ? state.meta.campaignId : undefined;
173
+ const resolvedCampaignId = campaignId ?? metaCampaignId;
174
+
175
+ // The click is the other half of CTR. The SDK tells the server to suppress
176
+ // its own counting for this page view — so if nothing here reports the
177
+ // click, CTR for SDK traffic is a division by silence. Fire-and-forget:
178
+ // track never throws and never blocks the navigation.
179
+ const handleClick = () => {
180
+ if (resolvedCampaignId) {
181
+ void track(
182
+ { apiBase, organizationId, campaignId: resolvedCampaignId, variantId, surfaceId },
183
+ "click",
184
+ { island: "cta" },
185
+ );
186
+ }
187
+ onClick?.();
188
+ };
189
+
190
+ // Shared by the link and the no-destination fallback below.
191
+ const shared = {
192
+ onClick: handleClick,
193
+ className:
194
+ "inline-flex min-h-11 min-w-40 items-center justify-center rounded-full px-6 font-medium",
195
+ style: {
196
+ ...themeStyle(state.theme),
197
+ // --st-cta-text is the label ink that reads on the theme's primary, so
198
+ // a light theme can never produce a ghost button.
199
+ ...(state.theme?.colors?.primary
200
+ ? { ["--st-cta-text" as string]: readableTextColor(state.theme.colors.primary) }
201
+ : {}),
202
+ },
203
+ // A CTA whose label is still empty is not yet a button a viewer can
204
+ // meaningfully act on — but it must still occupy its space, or the
205
+ // layout shifts when the copy lands.
206
+ "aria-disabled": state.fields.cta.text ? undefined : true,
207
+ };
208
+
209
+ // Neither the stream nor you resolved a destination: render a
210
+ // non-interactive element rather than an inert <a> with no href, which
211
+ // some assistive tech still announces as a link to nowhere. Clicks are
212
+ // still observed — engagement with the creative counts toward CTR whether
213
+ // or not it navigates.
214
+ if (action === undefined) {
215
+ return (
216
+ <span ref={ref as unknown as (element: HTMLSpanElement | null) => void} {...shared}>
217
+ <StreamText field={state.fields.cta} />
218
+ </span>
219
+ );
220
+ }
221
+
222
+ return (
223
+ <a ref={ref} href={action} {...shared}>
224
+ <StreamText field={state.fields.cta} />
225
+ </a>
226
+ );
227
+ }
228
+ `;
229
+ const COPY_CARD_TSX = `import { StreamText, staticField, type ContentSource } from "@sparkletree/react";
230
+
231
+ /**
232
+ * A copy card fed by /content rather than a stream.
233
+ *
234
+ * \`contentSource\` comes from the /content response's meta. Pass it through —
235
+ * do not default it to "generated" to get the mark, and do not hardcode
236
+ * "static" to lose it. The mark follows the copy's provenance, and both
237
+ * shortcuts make it say something untrue.
238
+ *
239
+ * \`staticField\` builds the field object for you. Keep using it rather than
240
+ * writing the object literal yourself: the field's shape belongs to the npm
241
+ * package, and the helper is what lets it grow without breaking this file.
242
+ *
243
+ * Requires Tailwind CSS for the default classes — swap them freely.
244
+ */
245
+ export interface CopyCardProps {
246
+ headline?: string;
247
+ body?: string;
248
+ /** Provenance of this copy, from the /content response's meta. */
249
+ contentSource?: ContentSource;
250
+ }
251
+
252
+ export function CopyCard({ headline, body, contentSource = "static" }: CopyCardProps) {
253
+ return (
254
+ <article className="rounded-xl border p-6">
255
+ <StreamText
256
+ field={staticField(headline, contentSource)}
257
+ as="h3"
258
+ className="text-lg font-semibold"
259
+ />
260
+ {body ? (
261
+ <StreamText field={staticField(body, contentSource)} as="p" className="mt-2 opacity-80" />
262
+ ) : null}
263
+ </article>
264
+ );
265
+ }
266
+ `;
267
+ const PRODUCT_CARD_TSX = `import { StreamText, staticField, type ContentSource } from "@sparkletree/react";
268
+
269
+ /**
270
+ * A product card.
271
+ *
272
+ * Note that the name and price are always "static": they come from your
273
+ * catalogue, not from a model. Only the description can be adapted, so only
274
+ * the description can carry the mark — marking a price as adapted would be a
275
+ * straightforward falsehood about the price.
276
+ *
277
+ * \`staticField\` builds the field object for you. Keep using it rather than
278
+ * writing the object literal yourself: the field's shape belongs to the npm
279
+ * package, and the helper is what lets it grow without breaking this file.
280
+ *
281
+ * Requires Tailwind CSS for the default classes — swap them freely.
282
+ */
283
+ export interface ProductCardProps {
284
+ product: {
285
+ name: string;
286
+ description?: string;
287
+ price?: string | number;
288
+ imageUrl?: string;
289
+ };
290
+ /** Provenance of the description. The name and price are never generated. */
291
+ contentSource?: ContentSource;
292
+ }
293
+
294
+ export function ProductCard({ product, contentSource = "static" }: ProductCardProps) {
295
+ return (
296
+ <article className="rounded-xl border p-4">
297
+ {product.imageUrl ? (
298
+ <img
299
+ src={product.imageUrl}
300
+ alt={product.name}
301
+ loading="lazy"
302
+ className="aspect-square w-full rounded-lg object-cover"
303
+ />
304
+ ) : null}
305
+ <StreamText field={staticField(product.name, "static")} as="h3" className="mt-3 font-medium" />
306
+ {product.description ? (
307
+ <StreamText
308
+ field={staticField(product.description, contentSource)}
309
+ as="p"
310
+ className="mt-1 text-sm opacity-75"
311
+ />
312
+ ) : null}
313
+ {product.price != null ? <p className="mt-2 font-semibold">{product.price}</p> : null}
314
+ </article>
315
+ );
316
+ }
317
+ `;
318
+ const PROVIDER_TSX = `"use client";
319
+
320
+ import { SparkletreeProvider } from "@sparkletree/react";
321
+ import type { ReactNode } from "react";
322
+
323
+ /**
324
+ * Wrap your app once.
325
+ *
326
+ * One provider per page view. It holds the org, the API base and the context
327
+ * snapshot, so every island on the page is adapted to the same moment rather
328
+ * than to two separate reads of the clock.
329
+ *
330
+ * The env checks below throw at module load, on purpose: a missing variable
331
+ * should fail your build or your first render with its own name in the error,
332
+ * not ship as a provider quietly pointed at \`undefined\`. The environment is
333
+ * read through globalThis because not every bundler (Vite, browser) defines
334
+ * \`process\` — there, the errors below tell you to wire the values yourself.
335
+ */
336
+ const runtimeProcess = (globalThis as { process?: { env?: Record<string, string | undefined> } })
337
+ .process;
338
+ const env: Record<string, string | undefined> = runtimeProcess?.env ?? {};
339
+ const envApiBase = env.NEXT_PUBLIC_SPARKLETREE_API_BASE;
340
+ const envOrganizationId = env.NEXT_PUBLIC_SPARKLETREE_ORG_ID;
341
+
342
+ if (!envApiBase) {
343
+ throw new Error(
344
+ runtimeProcess
345
+ ? "SparkleTree: Set NEXT_PUBLIC_SPARKLETREE_API_BASE (for example in .env.local) " +
346
+ "to your SparkleTree API origin."
347
+ : "SparkleTree: this environment has no process global (Vite, browser, ...). " +
348
+ "Expose your SparkleTree API origin to the bundle (for example via import.meta.env) " +
349
+ "and assign it to apiBase below.",
350
+ );
351
+ }
352
+ if (!envOrganizationId) {
353
+ throw new Error(
354
+ runtimeProcess
355
+ ? "SparkleTree: Set NEXT_PUBLIC_SPARKLETREE_ORG_ID (for example in .env.local) " +
356
+ "to your SparkleTree organization id."
357
+ : "SparkleTree: this environment has no process global (Vite, browser, ...). " +
358
+ "Expose your SparkleTree organization id to the bundle (for example via import.meta.env) " +
359
+ "and assign it to organizationId below.",
360
+ );
361
+ }
362
+
363
+ const apiBase: string = envApiBase;
364
+ const organizationId: string = envOrganizationId;
365
+
366
+ export function SparkleTree({ children }: { children: ReactNode }) {
367
+ return (
368
+ <SparkletreeProvider apiBase={apiBase} organizationId={organizationId}>
369
+ {children}
370
+ </SparkletreeProvider>
371
+ );
372
+ }
373
+ `;
374
+ export const REGISTRY = [
375
+ {
376
+ name: "provider",
377
+ description: "The page-level provider. Every other component needs it.",
378
+ files: [{ path: "sparkletree/provider.tsx", content: PROVIDER_TSX }],
379
+ dependencies: ["@sparkletree/react", "@sparkletree/core"],
380
+ registryDependencies: [],
381
+ },
382
+ {
383
+ name: "hero",
384
+ description: "Streaming hero: greeting, headline, body, adaptive art.",
385
+ files: [{ path: "sparkletree/hero.tsx", content: HERO_TSX }],
386
+ dependencies: ["@sparkletree/react", "@sparkletree/core"],
387
+ registryDependencies: ["provider"],
388
+ },
389
+ {
390
+ name: "cta",
391
+ description: "Streaming call to action with a stable hit area.",
392
+ files: [{ path: "sparkletree/cta.tsx", content: CTA_TSX }],
393
+ dependencies: ["@sparkletree/react", "@sparkletree/core"],
394
+ registryDependencies: ["provider"],
395
+ },
396
+ {
397
+ name: "copy-card",
398
+ description: "Content-fed copy card.",
399
+ files: [{ path: "sparkletree/copy-card.tsx", content: COPY_CARD_TSX }],
400
+ dependencies: ["@sparkletree/react"],
401
+ registryDependencies: ["provider"],
402
+ },
403
+ {
404
+ name: "product-card",
405
+ description: "Content-fed product card.",
406
+ files: [{ path: "sparkletree/product-card.tsx", content: PRODUCT_CARD_TSX }],
407
+ dependencies: ["@sparkletree/react"],
408
+ registryDependencies: ["provider"],
409
+ },
410
+ ];
411
+ export function findComponent(name) {
412
+ return REGISTRY.find((component) => component.name === name);
413
+ }
414
+ /**
415
+ * Resolve a component plus everything it needs, dependencies first.
416
+ *
417
+ * Depth-first with a visited set, so a diamond in the graph copies each file
418
+ * once and a cycle terminates rather than recursing until the stack gives out.
419
+ */
420
+ export function resolveWithDependencies(names) {
421
+ const resolved = [];
422
+ const seen = new Set();
423
+ const visit = (name) => {
424
+ if (seen.has(name))
425
+ return;
426
+ seen.add(name);
427
+ const component = findComponent(name);
428
+ if (!component)
429
+ throw new Error(`Unknown component "${name}"`);
430
+ for (const dependency of component.registryDependencies)
431
+ visit(dependency);
432
+ resolved.push(component);
433
+ };
434
+ for (const name of names)
435
+ visit(name);
436
+ return resolved;
437
+ }
438
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@sparkletree/cli",
3
+ "version": "0.1.0",
4
+ "description": "sparkletree — add SparkleTree components to your project.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "sparkletree": "./dist/bin.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "LICENSE",
21
+ "README.md"
22
+ ],
23
+ "devDependencies": {
24
+ "@types/node": "^20.19.11",
25
+ "typescript": "^5.9.2",
26
+ "vitest": "^3.2.4"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://gitlab.com/sparkletree/sparkletree-sdk.git",
34
+ "directory": "cli"
35
+ },
36
+ "author": "SparkleTree",
37
+ "homepage": "https://gitlab.com/sparkletree/sparkletree-sdk/-/blob/main/README.md",
38
+ "bugs": {
39
+ "url": "https://gitlab.com/sparkletree/sparkletree-sdk/-/issues"
40
+ },
41
+ "keywords": [
42
+ "sparkletree",
43
+ "cli",
44
+ "components",
45
+ "scaffold",
46
+ "react"
47
+ ],
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc --project tsconfig.json",
53
+ "clean": "rm -rf dist",
54
+ "test": "vitest run",
55
+ "typecheck": "tsc --project tsconfig.json --noEmit"
56
+ }
57
+ }