dithercharts 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/README.md +37 -0
- package/dist/api.js +53 -0
- package/dist/auth-store.js +29 -0
- package/dist/bin.js +8 -0
- package/dist/cli.js +115 -0
- package/dist/commands/account.js +25 -0
- package/dist/commands/add.js +59 -0
- package/dist/commands/list.js +12 -0
- package/dist/commands/login.js +47 -0
- package/dist/config.js +3 -0
- package/dist/errors.js +30 -0
- package/dist/install.js +55 -0
- package/dist/project.js +55 -0
- package/dist/prompt.js +19 -0
- package/dist/types.js +1 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# dithercharts
|
|
2
|
+
|
|
3
|
+
The CLI for [DitherCharts](https://dithercharts.vercel.app), dithered and ASCII chart components for shadcn/ui. It copies chart source into your React app, so you own and can edit every line.
|
|
4
|
+
|
|
5
|
+
You need a DitherCharts Pro subscription, Node 20 or later, and a React 19 app with TypeScript. Tailwind is optional.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Log in once per machine. Your browser opens to confirm a code.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx dithercharts login
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then add charts from your app's folder:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npx dithercharts add area-chart
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Files land in `src/components/dithercharts/` (or `components/dithercharts/` without a `src` folder), and the CLI installs the chart's npm dependencies with your package manager.
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
| Command | What it does |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `login` | Sign in through the browser and save a key |
|
|
28
|
+
| `logout` | Revoke this machine's key and forget it |
|
|
29
|
+
| `whoami` | Show the signed-in account and plan |
|
|
30
|
+
| `list` | List the charts and what's installed here |
|
|
31
|
+
| `add <names...>` | Install charts into this project |
|
|
32
|
+
|
|
33
|
+
`add` options: `--all`, `--dir <path>`, `--yes`, `--overwrite`, `--no-install`.
|
|
34
|
+
|
|
35
|
+
In CI, set `DITHERCHARTS_API_KEY` instead of running `login`.
|
|
36
|
+
|
|
37
|
+
Full docs: https://dithercharts.vercel.app/docs/installation
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ApiError, messageFor } from "./errors.js";
|
|
2
|
+
/** The DitherCharts server. 5xx answers and network errors are retried once. */
|
|
3
|
+
export function createApi(base, key, fetchImpl = fetch) {
|
|
4
|
+
async function request(path, init = {}, name, retry = true) {
|
|
5
|
+
const headers = new Headers();
|
|
6
|
+
if (key)
|
|
7
|
+
headers.set("authorization", `Bearer ${key}`);
|
|
8
|
+
if (init.body)
|
|
9
|
+
headers.set("content-type", "application/json");
|
|
10
|
+
let res;
|
|
11
|
+
try {
|
|
12
|
+
res = await fetchImpl(base + path, { ...init, headers });
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (retry)
|
|
16
|
+
return request(path, init, name, false);
|
|
17
|
+
throw new ApiError(0, "network", `Couldn't reach ${base}: ${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (res.status >= 500 && retry)
|
|
20
|
+
return request(path, init, name, false);
|
|
21
|
+
const body = await res.json().catch(() => ({}));
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
throw new ApiError(res.status, body.error, messageFor(res.status, body, base, name));
|
|
24
|
+
return body;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
index: () => request("/r/index.json"),
|
|
28
|
+
item: (name) => request(`/r/${encodeURIComponent(name)}.json`, {}, name),
|
|
29
|
+
startDevice: (host, os) => request("/api/cli/device", { method: "POST", body: JSON.stringify({ host, os }) }),
|
|
30
|
+
/** A 400 here is the protocol talking (pending, slow down, denied, expired), not a failure. */
|
|
31
|
+
async pollToken(deviceCode) {
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetchImpl(`${base}/api/cli/token`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ok: false, error: "authorization_pending" }; // a network blip: keep polling until the code expires
|
|
42
|
+
}
|
|
43
|
+
const body = await res.json().catch(() => ({}));
|
|
44
|
+
if (res.ok)
|
|
45
|
+
return { ok: true, ...body };
|
|
46
|
+
if (res.status === 400 && typeof body.error === "string")
|
|
47
|
+
return { ok: false, error: body.error };
|
|
48
|
+
throw new ApiError(res.status, body.error, messageFor(res.status, body, base));
|
|
49
|
+
},
|
|
50
|
+
whoami: () => request("/api/cli/whoami"),
|
|
51
|
+
logout: () => request("/api/cli/logout", { method: "POST" }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, posix, win32 } from "node:path";
|
|
4
|
+
/** ~/.config/dithercharts/auth.json (XDG_CONFIG_HOME honored), or %APPDATA%\dithercharts\auth.json on Windows. */
|
|
5
|
+
export function authPath(env = process.env, platform = process.platform, home = homedir()) {
|
|
6
|
+
if (platform === "win32")
|
|
7
|
+
return win32.join(env.APPDATA || win32.join(home, "AppData", "Roaming"), "dithercharts", "auth.json");
|
|
8
|
+
return posix.join(env.XDG_CONFIG_HOME || posix.join(home, ".config"), "dithercharts", "auth.json");
|
|
9
|
+
}
|
|
10
|
+
export function readAuth(path = authPath()) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
return typeof parsed.apiKey === "string" ? { apiKey: parsed.apiKey, email: parsed.email ?? null } : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Owner-only: the key installs paid code. */
|
|
20
|
+
export function writeAuth(auth, path = authPath()) {
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
22
|
+
writeFileSync(path, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
23
|
+
chmodSync(path, 0o600);
|
|
24
|
+
}
|
|
25
|
+
export const deleteAuth = (path = authPath()) => rmSync(path, { force: true });
|
|
26
|
+
/** DITHERCHARTS_API_KEY wins, so CI never needs a login. */
|
|
27
|
+
export function resolveKey(env = process.env, path = authPath()) {
|
|
28
|
+
return env.DITHERCHARTS_API_KEY?.trim() || readAuth(path)?.apiKey || null;
|
|
29
|
+
}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from "./cli.js";
|
|
3
|
+
main(process.argv.slice(2), { log: console.log, error: console.error, cwd: process.cwd(), env: process.env }).then((code) => {
|
|
4
|
+
process.exitCode = code;
|
|
5
|
+
}, (error) => {
|
|
6
|
+
console.error(error);
|
|
7
|
+
process.exitCode = 1;
|
|
8
|
+
});
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { hostname, platform } from "node:os";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { createApi } from "./api.js";
|
|
6
|
+
import { authPath, deleteAuth, readAuth, resolveKey, writeAuth } from "./auth-store.js";
|
|
7
|
+
import { logout, whoami } from "./commands/account.js";
|
|
8
|
+
import { login, openBrowser } from "./commands/login.js";
|
|
9
|
+
import { add } from "./commands/add.js";
|
|
10
|
+
import { list } from "./commands/list.js";
|
|
11
|
+
import { baseUrl, DEFAULT_BASE_URL } from "./config.js";
|
|
12
|
+
import { CliError } from "./errors.js";
|
|
13
|
+
import { askOverwrite, confirmDir, interactive } from "./prompt.js";
|
|
14
|
+
const HELP = `Usage: dithercharts <command>
|
|
15
|
+
|
|
16
|
+
Commands:
|
|
17
|
+
login Sign in through the browser and save a key
|
|
18
|
+
logout Revoke this machine's key and forget it
|
|
19
|
+
whoami Show the signed-in account and plan
|
|
20
|
+
list List the charts and what's installed here
|
|
21
|
+
add <names...> Install charts into this project
|
|
22
|
+
--all Install every chart
|
|
23
|
+
--dir <path> Folder to install into
|
|
24
|
+
--yes, -y Accept the default folder
|
|
25
|
+
--overwrite Replace files you changed locally
|
|
26
|
+
--no-install Print the dependency install command instead of running it
|
|
27
|
+
|
|
28
|
+
Environment:
|
|
29
|
+
DITHERCHARTS_API_KEY Use this key instead of the saved login (CI)
|
|
30
|
+
DITHERCHARTS_URL Server to use (default ${DEFAULT_BASE_URL})`;
|
|
31
|
+
const version = () => JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
32
|
+
function runCommand(cmd, cwd) {
|
|
33
|
+
const result = spawnSync(cmd[0], cmd.slice(1), { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
34
|
+
if (result.status !== 0)
|
|
35
|
+
throw new CliError(`\`${cmd.join(" ")}\` failed. Run it yourself to finish.`);
|
|
36
|
+
}
|
|
37
|
+
export async function main(argv, io) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = parseArgs({
|
|
41
|
+
args: argv,
|
|
42
|
+
allowPositionals: true,
|
|
43
|
+
options: {
|
|
44
|
+
all: { type: "boolean", default: false },
|
|
45
|
+
dir: { type: "string" },
|
|
46
|
+
yes: { type: "boolean", short: "y", default: false },
|
|
47
|
+
overwrite: { type: "boolean", default: false },
|
|
48
|
+
"no-install": { type: "boolean", default: false },
|
|
49
|
+
help: { type: "boolean", short: "h", default: false },
|
|
50
|
+
version: { type: "boolean", short: "v", default: false },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
io.error(`${error.message}\n\n${HELP}`);
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
const { values, positionals } = parsed;
|
|
59
|
+
const [command, ...names] = positionals;
|
|
60
|
+
if (values.version) {
|
|
61
|
+
io.log(version());
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (values.help || !command) {
|
|
65
|
+
io.log(HELP);
|
|
66
|
+
return values.help ? 0 : 2;
|
|
67
|
+
}
|
|
68
|
+
const base = baseUrl(io.env);
|
|
69
|
+
const path = authPath(io.env);
|
|
70
|
+
const key = resolveKey(io.env, path);
|
|
71
|
+
const needKey = () => {
|
|
72
|
+
if (!key)
|
|
73
|
+
throw new CliError("Not logged in. Run `npx dithercharts login`, or set DITHERCHARTS_API_KEY.");
|
|
74
|
+
return createApi(base, key);
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
switch (command) {
|
|
78
|
+
case "login":
|
|
79
|
+
return await login({
|
|
80
|
+
api: createApi(base, null),
|
|
81
|
+
open: (url) => void openBrowser(url),
|
|
82
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
83
|
+
now: Date.now,
|
|
84
|
+
log: io.log,
|
|
85
|
+
host: hostname(),
|
|
86
|
+
os: platform(),
|
|
87
|
+
save: (auth) => writeAuth(auth, path),
|
|
88
|
+
});
|
|
89
|
+
case "logout":
|
|
90
|
+
return await logout({
|
|
91
|
+
envKey: Boolean(io.env.DITHERCHARTS_API_KEY),
|
|
92
|
+
saved: readAuth(path),
|
|
93
|
+
api: (savedKey) => createApi(base, savedKey),
|
|
94
|
+
remove: () => deleteAuth(path),
|
|
95
|
+
log: io.log,
|
|
96
|
+
});
|
|
97
|
+
case "whoami":
|
|
98
|
+
return await whoami({ api: needKey(), log: io.log });
|
|
99
|
+
case "list":
|
|
100
|
+
return await list({ cwd: io.cwd, api: needKey(), log: io.log });
|
|
101
|
+
case "add":
|
|
102
|
+
return await add({ names, all: values.all, yes: values.yes, overwrite: values.overwrite, install: !values["no-install"], dir: values.dir }, { cwd: io.cwd, api: needKey(), interactive: interactive(), askOverwrite, confirmDir, run: runCommand, log: io.log });
|
|
103
|
+
default:
|
|
104
|
+
io.error(`Unknown command "${command}".\n\n${HELP}`);
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof CliError) {
|
|
110
|
+
io.error(error.message);
|
|
111
|
+
return error.exitCode;
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ApiError, CliError } from "../errors.js";
|
|
2
|
+
export async function logout(deps) {
|
|
3
|
+
if (deps.envKey)
|
|
4
|
+
throw new CliError("DITHERCHARTS_API_KEY is set; unset it to stop using that key. Logout only revokes a saved login.");
|
|
5
|
+
if (!deps.saved) {
|
|
6
|
+
deps.log("Not logged in.");
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
await deps.api(deps.saved.apiKey).logout();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (!(error instanceof ApiError && error.status === 401))
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
deps.remove();
|
|
17
|
+
deps.log("✓ Logged out. This machine's key is revoked.");
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
export async function whoami(deps) {
|
|
21
|
+
const me = await deps.api.whoami();
|
|
22
|
+
const renews = me.renewsAt ? ` · renews ${new Date(me.renewsAt).toISOString().slice(0, 10)}` : "";
|
|
23
|
+
deps.log(`${me.email ?? "(no email)"} · ${me.plan}${renews}`);
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { CliError } from "../errors.js";
|
|
4
|
+
import { applyFiles, planFiles, resolveOrder } from "../install.js";
|
|
5
|
+
import { componentName, defaultDir, detectPackageManager, findProjectRoot, importBase, installCommand, missing, reactMajor, readConfig, readPackageJson, writeConfig, } from "../project.js";
|
|
6
|
+
const unique = (list) => [...new Set(list)];
|
|
7
|
+
const MARK = { written: "✓", unchanged: "=", skipped: "!" };
|
|
8
|
+
export async function add(opts, deps) {
|
|
9
|
+
const root = findProjectRoot(deps.cwd);
|
|
10
|
+
if (!root)
|
|
11
|
+
throw new CliError("No package.json here or above. Run this inside your app.");
|
|
12
|
+
const pkg = readPackageJson(root);
|
|
13
|
+
const major = reactMajor(pkg);
|
|
14
|
+
if (major === null || major < 19)
|
|
15
|
+
deps.log(`! DitherCharts needs React 19; this app has ${major === null ? "no react" : `react ${major}`}.`);
|
|
16
|
+
const saved = readConfig(root);
|
|
17
|
+
const dir = opts.dir ?? saved?.dir ?? defaultDir(root);
|
|
18
|
+
if (!saved && !opts.dir && !opts.yes && deps.interactive && !(await deps.confirmDir(dir))) {
|
|
19
|
+
throw new CliError("Rerun with --dir <path> to choose the folder.");
|
|
20
|
+
}
|
|
21
|
+
const index = await deps.api.index();
|
|
22
|
+
const names = opts.all ? index.items.map((item) => item.name) : opts.names;
|
|
23
|
+
if (names.length === 0)
|
|
24
|
+
throw new CliError("Name a chart, e.g. `npx dithercharts add area-chart`, or pass --all.", 2);
|
|
25
|
+
// Every item is fetched before any file is written, so a failure leaves the app untouched.
|
|
26
|
+
const items = await Promise.all(resolveOrder(index, names).map((name) => deps.api.item(name)));
|
|
27
|
+
const results = await applyFiles(planFiles(join(root, dir), items), {
|
|
28
|
+
overwrite: opts.overwrite,
|
|
29
|
+
ask: deps.interactive ? deps.askOverwrite : null,
|
|
30
|
+
});
|
|
31
|
+
for (const r of results) {
|
|
32
|
+
deps.log(`${MARK[r.result]} ${relative(root, r.path)}${r.result === "skipped" ? " (local changes kept; --overwrite replaces)" : ""}`);
|
|
33
|
+
}
|
|
34
|
+
writeConfig(root, { dir, items: { ...saved?.items, ...Object.fromEntries(items.map((i) => [i.name, i.version])) } });
|
|
35
|
+
const typescript = existsSync(join(root, "tsconfig.json"));
|
|
36
|
+
const pm = detectPackageManager(root);
|
|
37
|
+
const groups = [
|
|
38
|
+
{ specs: missing(pkg, unique(items.flatMap((i) => i.dependencies))), dev: false },
|
|
39
|
+
{ specs: typescript ? missing(pkg, unique(items.flatMap((i) => i.devDependencies))) : [], dev: true },
|
|
40
|
+
];
|
|
41
|
+
for (const { specs, dev } of groups) {
|
|
42
|
+
if (specs.length === 0)
|
|
43
|
+
continue;
|
|
44
|
+
const cmd = installCommand(pm, specs, dev);
|
|
45
|
+
if (!opts.install) {
|
|
46
|
+
deps.log(`Install the dependencies: ${cmd.join(" ")}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
deps.log(`$ ${cmd.join(" ")}`);
|
|
50
|
+
deps.run(cmd, root);
|
|
51
|
+
}
|
|
52
|
+
const base = importBase(dir);
|
|
53
|
+
deps.log("");
|
|
54
|
+
deps.log(`import "${base}/dithercharts.css"`);
|
|
55
|
+
for (const name of names.filter((n) => n.endsWith("-chart"))) {
|
|
56
|
+
deps.log(`import { ${componentName(name)} } from "${base}/${name}"`);
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { findProjectRoot, readConfig } from "../project.js";
|
|
2
|
+
export async function list(deps) {
|
|
3
|
+
const index = await deps.api.index();
|
|
4
|
+
const root = findProjectRoot(deps.cwd);
|
|
5
|
+
const installed = (root && readConfig(root)?.items) || {};
|
|
6
|
+
for (const item of index.items) {
|
|
7
|
+
const have = installed[item.name];
|
|
8
|
+
const note = !have ? "" : have === index.version ? ` [installed ${have}]` : ` [installed ${have}, update: ${index.version}]`;
|
|
9
|
+
deps.log(`${item.name.padEnd(14)} ${item.description}${note}`);
|
|
10
|
+
}
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CliError } from "../errors.js";
|
|
2
|
+
const EXPIRED = "The code expired. Run `npx dithercharts login` again.";
|
|
3
|
+
export async function login(deps) {
|
|
4
|
+
const start = await deps.api.startDevice(deps.host, deps.os);
|
|
5
|
+
deps.log(`Code: ${start.user_code}`);
|
|
6
|
+
deps.log(`Opening ${start.verification_url} …`);
|
|
7
|
+
deps.log("If the browser doesn't open, visit that URL and confirm the code.");
|
|
8
|
+
deps.open(start.verification_url);
|
|
9
|
+
let interval = start.interval * 1000;
|
|
10
|
+
const deadline = deps.now() + start.expires_in * 1000;
|
|
11
|
+
while (deps.now() < deadline) {
|
|
12
|
+
await deps.sleep(interval);
|
|
13
|
+
const poll = await deps.api.pollToken(start.device_code);
|
|
14
|
+
if (poll.ok) {
|
|
15
|
+
deps.save({ apiKey: poll.api_key, email: poll.email });
|
|
16
|
+
deps.log(`✓ Logged in as ${poll.email ?? "your account"} (${poll.plan})`);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
if (poll.error === "authorization_pending") {
|
|
20
|
+
// keep polling
|
|
21
|
+
}
|
|
22
|
+
else if (poll.error === "slow_down") {
|
|
23
|
+
interval += 5000;
|
|
24
|
+
}
|
|
25
|
+
else if (poll.error === "access_denied") {
|
|
26
|
+
throw new CliError("Login was denied in the browser.");
|
|
27
|
+
}
|
|
28
|
+
else if (poll.error === "expired_token") {
|
|
29
|
+
throw new CliError(EXPIRED);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
throw new CliError(`Login failed: ${poll.error}. Run \`npx dithercharts login\` again.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new CliError(EXPIRED);
|
|
36
|
+
}
|
|
37
|
+
/** Best effort: the URL is printed anyway. */
|
|
38
|
+
export async function openBrowser(url) {
|
|
39
|
+
const { spawn } = await import("node:child_process");
|
|
40
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
41
|
+
try {
|
|
42
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => { }).unref();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// no browser here (SSH, container): the printed URL is the way in
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/config.js
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** A failure meant for the user: print the message, exit with the code. */
|
|
2
|
+
export class CliError extends Error {
|
|
3
|
+
exitCode;
|
|
4
|
+
constructor(message, exitCode = 1) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.exitCode = exitCode;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** A failure answered by the DitherCharts server. */
|
|
10
|
+
export class ApiError extends CliError {
|
|
11
|
+
status;
|
|
12
|
+
code;
|
|
13
|
+
constructor(status, code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** What to tell the user for a failed request. */
|
|
20
|
+
export function messageFor(status, body, base, name) {
|
|
21
|
+
if (status === 401)
|
|
22
|
+
return "Your key is invalid or revoked. Run `npx dithercharts login`.";
|
|
23
|
+
if (status === 403 && body.error === "pro_required")
|
|
24
|
+
return `No active Pro subscription: ${body.pricing ?? `${base}/pricing`}. Charts you already installed keep working.`;
|
|
25
|
+
if (status === 403)
|
|
26
|
+
return "This key can't install charts. Log in with `npx dithercharts login` to get a personal key.";
|
|
27
|
+
if (status === 404)
|
|
28
|
+
return name ? `No chart named "${name}". Run \`npx dithercharts list\`.` : "Not found on the server.";
|
|
29
|
+
return `The server answered ${status}. Nothing was written; try again.`;
|
|
30
|
+
}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { CliError } from "./errors.js";
|
|
4
|
+
/** Each requested item after its registry dependencies, each once. */
|
|
5
|
+
export function resolveOrder(index, names) {
|
|
6
|
+
const byName = new Map(index.items.map((item) => [item.name, item]));
|
|
7
|
+
const order = [];
|
|
8
|
+
const visit = (name) => {
|
|
9
|
+
const item = byName.get(name);
|
|
10
|
+
if (!item)
|
|
11
|
+
throw new CliError(`No chart named "${name}". Run \`npx dithercharts list\`.`);
|
|
12
|
+
if (order.includes(name))
|
|
13
|
+
return;
|
|
14
|
+
item.registryDependencies.forEach(visit);
|
|
15
|
+
order.push(name);
|
|
16
|
+
};
|
|
17
|
+
names.forEach(visit);
|
|
18
|
+
return order;
|
|
19
|
+
}
|
|
20
|
+
/** Every file of `items` under `dir`, compared with what's on disk. */
|
|
21
|
+
export function planFiles(dir, items) {
|
|
22
|
+
return items.flatMap((item) => item.files.map((file) => {
|
|
23
|
+
if (isAbsolute(file.path) || file.path.split(/[\\/]/).includes("..")) {
|
|
24
|
+
throw new CliError(`Refusing to write ${file.path}: it's outside the install folder.`);
|
|
25
|
+
}
|
|
26
|
+
const path = join(dir, file.path);
|
|
27
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
28
|
+
const state = current === null ? "new" : current === file.content ? "same" : "changed";
|
|
29
|
+
return { path, content: file.content, state };
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
/** Writes new files; a locally changed file is replaced only with --overwrite or a yes. */
|
|
33
|
+
export async function applyFiles(plan, opts) {
|
|
34
|
+
let all = opts.overwrite;
|
|
35
|
+
const results = [];
|
|
36
|
+
for (const file of plan) {
|
|
37
|
+
let write = file.state === "new";
|
|
38
|
+
if (file.state === "changed") {
|
|
39
|
+
if (!all && opts.ask) {
|
|
40
|
+
const answer = await opts.ask(file.path);
|
|
41
|
+
all = answer === "all";
|
|
42
|
+
write = answer !== "no";
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
write = all;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (write) {
|
|
49
|
+
mkdirSync(dirname(file.path), { recursive: true });
|
|
50
|
+
writeFileSync(file.path, file.content);
|
|
51
|
+
}
|
|
52
|
+
results.push({ path: file.path, result: write ? "written" : file.state === "same" ? "unchanged" : "skipped" });
|
|
53
|
+
}
|
|
54
|
+
return results;
|
|
55
|
+
}
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
export const CONFIG_FILE = "dithercharts.json";
|
|
4
|
+
/** The nearest folder at or above `cwd` with a package.json. */
|
|
5
|
+
export function findProjectRoot(cwd) {
|
|
6
|
+
for (let dir = cwd;; dir = dirname(dir)) {
|
|
7
|
+
if (existsSync(join(dir, "package.json")))
|
|
8
|
+
return dir;
|
|
9
|
+
if (dirname(dir) === dir)
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const readPackageJson = (root) => JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
14
|
+
export function readConfig(root) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(join(root, CONFIG_FILE), "utf8"));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const writeConfig = (root, config) => writeFileSync(join(root, CONFIG_FILE), `${JSON.stringify(config, null, 2)}\n`);
|
|
23
|
+
export const defaultDir = (root) => existsSync(join(root, "src")) ? "src/components/dithercharts" : "components/dithercharts";
|
|
24
|
+
export function detectPackageManager(root) {
|
|
25
|
+
if (existsSync(join(root, "pnpm-lock.yaml")))
|
|
26
|
+
return "pnpm";
|
|
27
|
+
if (existsSync(join(root, "yarn.lock")))
|
|
28
|
+
return "yarn";
|
|
29
|
+
if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb")))
|
|
30
|
+
return "bun";
|
|
31
|
+
return "npm";
|
|
32
|
+
}
|
|
33
|
+
/** "@types/d3-scale@^4.0.9" → "@types/d3-scale". */
|
|
34
|
+
export const packageName = (spec) => spec.startsWith("@") ? spec.split("@").slice(0, 2).join("@") : spec.split("@")[0];
|
|
35
|
+
/** The specs whose package the app doesn't list yet. */
|
|
36
|
+
export function missing(pkg, specs) {
|
|
37
|
+
const have = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
38
|
+
return specs.filter((spec) => !(packageName(spec) in have));
|
|
39
|
+
}
|
|
40
|
+
const DEV_FLAG = { npm: "--save-dev", pnpm: "-D", yarn: "-D", bun: "--dev" };
|
|
41
|
+
export function installCommand(pm, specs, dev) {
|
|
42
|
+
return [pm, pm === "npm" ? "install" : "add", ...(dev ? [DEV_FLAG[pm]] : []), ...specs];
|
|
43
|
+
}
|
|
44
|
+
export function reactMajor(pkg) {
|
|
45
|
+
const range = pkg.dependencies?.react ?? pkg.devDependencies?.react;
|
|
46
|
+
const major = range?.match(/\d+/)?.[0];
|
|
47
|
+
return major ? Number(major) : null;
|
|
48
|
+
}
|
|
49
|
+
/** How app code imports the folder, assuming the common `@/` alias. */
|
|
50
|
+
export const importBase = (dir) => `@/${dir.replace(/^src\//, "")}`;
|
|
51
|
+
/** "area-chart" → "AreaChart". */
|
|
52
|
+
export const componentName = (item) => item
|
|
53
|
+
.split("-")
|
|
54
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
55
|
+
.join("");
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
export const interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3
|
+
async function question(text) {
|
|
4
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5
|
+
try {
|
|
6
|
+
return (await rl.question(text)).trim().toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
rl.close();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function askOverwrite(path) {
|
|
13
|
+
const answer = await question(`${path} has local changes. Overwrite? [y/N/all] `);
|
|
14
|
+
return answer === "all" || answer === "a" ? "all" : answer === "y" || answer === "yes" ? "yes" : "no";
|
|
15
|
+
}
|
|
16
|
+
export async function confirmDir(dir) {
|
|
17
|
+
const answer = await question(`Install into ${dir}? [Y/n] `);
|
|
18
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
19
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dithercharts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install DitherCharts Pro charts into your React app.",
|
|
5
|
+
"keywords": ["dithercharts", "charts", "react", "dither", "ascii", "shadcn-ui", "cli"],
|
|
6
|
+
"homepage": "https://dithercharts.vercel.app/docs/installation",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": { "dithercharts": "dist/bin.js" },
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"engines": { "node": ">=20" },
|
|
11
|
+
"scripts": { "build": "tsc -p . && chmod +x dist/bin.js", "prepublishOnly": "npm run build" },
|
|
12
|
+
"license": "UNLICENSED"
|
|
13
|
+
}
|