@hogsend/cli 0.0.1 → 0.2.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/dist/bin.js +2238 -75
- package/dist/bin.js.map +1 -1
- package/package.json +9 -1
- package/skills/hogsend-authoring-buckets/SKILL.md +69 -0
- package/skills/hogsend-authoring-buckets/references/bucket-id-aliases.md +117 -0
- package/skills/hogsend-authoring-buckets/references/bucket-meta.md +142 -0
- package/skills/hogsend-authoring-buckets/references/buckets-vs-journeys.md +96 -0
- package/skills/hogsend-authoring-buckets/references/register-a-bucket.md +129 -0
- package/skills/hogsend-authoring-emails/SKILL.md +68 -0
- package/skills/hogsend-authoring-emails/references/email-components.md +112 -0
- package/skills/hogsend-authoring-emails/references/preview-and-render.md +116 -0
- package/skills/hogsend-authoring-emails/references/template-four-file-contract.md +134 -0
- package/skills/hogsend-authoring-emails/references/tracking-and-unsubscribe.md +127 -0
- package/skills/hogsend-authoring-journeys/SKILL.md +88 -0
- package/skills/hogsend-authoring-journeys/references/branch-on-engagement.md +93 -0
- package/skills/hogsend-authoring-journeys/references/journey-context.md +110 -0
- package/skills/hogsend-authoring-journeys/references/journey-meta.md +142 -0
- package/skills/hogsend-authoring-journeys/references/register-a-journey.md +99 -0
- package/skills/hogsend-authoring-journeys/references/sending-email-from-a-journey.md +82 -0
- package/skills/hogsend-cli/SKILL.md +81 -0
- package/skills/hogsend-cli/references/debug-a-journey.md +66 -0
- package/skills/hogsend-cli/references/manage-journeys.md +53 -0
- package/skills/hogsend-cli/references/query-stats.md +66 -0
- package/skills/hogsend-cli/references/setup-local.md +52 -0
- package/skills/hogsend-conditions/SKILL.md +70 -0
- package/skills/hogsend-conditions/references/condition-types.md +251 -0
- package/skills/hogsend-conditions/references/durations.md +90 -0
- package/skills/hogsend-conditions/references/examples.md +188 -0
- package/skills/hogsend-database/SKILL.md +70 -0
- package/skills/hogsend-database/references/client-track-schema.md +97 -0
- package/skills/hogsend-database/references/migrations.md +132 -0
- package/skills/hogsend-database/references/schema-drift.md +123 -0
- package/skills/hogsend-deploy/SKILL.md +62 -0
- package/skills/hogsend-deploy/references/env-and-secrets.md +118 -0
- package/skills/hogsend-deploy/references/railway-two-services.md +122 -0
- package/skills/hogsend-deploy/references/upgrade-engine.md +92 -0
- package/skills/hogsend-webhooks-and-workflows/SKILL.md +68 -0
- package/skills/hogsend-webhooks-and-workflows/references/backfill-pattern.md +148 -0
- package/skills/hogsend-webhooks-and-workflows/references/custom-workflow.md +156 -0
- package/skills/hogsend-webhooks-and-workflows/references/webhook-source.md +172 -0
- package/src/bin.ts +73 -111
- package/src/commands/contacts.ts +316 -0
- package/src/commands/doctor.ts +239 -0
- package/src/commands/eject.ts +106 -0
- package/src/commands/events.ts +154 -0
- package/src/commands/index.ts +36 -0
- package/src/commands/journeys.ts +343 -0
- package/src/commands/patch.ts +80 -0
- package/src/commands/setup.ts +322 -0
- package/src/commands/skills.ts +208 -0
- package/src/commands/stats.ts +87 -0
- package/src/commands/studio.ts +261 -0
- package/src/commands/types.ts +41 -0
- package/src/commands/upgrade.ts +245 -0
- package/src/index.ts +2 -0
- package/src/lib/config.ts +147 -0
- package/src/lib/http.ts +145 -0
- package/src/lib/output.ts +185 -0
- package/src/lib/prompt.ts +17 -0
- package/src/lib/skills.ts +186 -0
- package/studio/assets/index-BVA9GZqq.css +1 -0
- package/studio/assets/index-kPwzOOyG.js +230 -0
- package/studio/index.html +13 -0
package/src/lib/http.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "./config.js";
|
|
2
|
+
|
|
3
|
+
/** A non-2xx response (or transport failure) from the admin API. */
|
|
4
|
+
export interface HttpError extends Error {
|
|
5
|
+
/** HTTP status code, or 0 for a transport-level failure (DNS/connect). */
|
|
6
|
+
status: number;
|
|
7
|
+
/** Parsed JSON body when available, else the raw text, else undefined. */
|
|
8
|
+
body: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Query params accepted by `get` — undefined values are dropped. */
|
|
12
|
+
export type Query = Record<string, string | number | undefined>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Thin admin HTTP client over native fetch (Node 22). Hits `<base>/v1/...`,
|
|
16
|
+
* sends `Authorization: Bearer <adminKey>` on admin paths, parses JSON, and
|
|
17
|
+
* throws an {@link HttpError} on any non-2xx response.
|
|
18
|
+
*
|
|
19
|
+
* Path convention: pass the path AFTER the base URL, e.g. `/v1/admin/journeys`
|
|
20
|
+
* or `/v1/health`. The unauthenticated health route is reached via the same
|
|
21
|
+
* `get` — pass `{ auth: false }` so a missing admin key doesn't error.
|
|
22
|
+
*/
|
|
23
|
+
export interface AdminClient {
|
|
24
|
+
get<T = unknown>(
|
|
25
|
+
path: string,
|
|
26
|
+
query?: Query,
|
|
27
|
+
opts?: RequestExtras,
|
|
28
|
+
): Promise<T>;
|
|
29
|
+
patch<T = unknown>(path: string, body: unknown): Promise<T>;
|
|
30
|
+
post<T = unknown>(path: string, body: unknown): Promise<T>;
|
|
31
|
+
/** The resolved config this client is bound to (for messages/JSON output). */
|
|
32
|
+
readonly cfg: ResolvedConfig;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Per-request overrides. */
|
|
36
|
+
export interface RequestExtras {
|
|
37
|
+
/** Set false for unauthenticated routes (e.g. /v1/health). Default true. */
|
|
38
|
+
auth?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isHttpError(value: unknown): value is HttpError {
|
|
42
|
+
return value instanceof Error && "status" in value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeHttpError(
|
|
46
|
+
message: string,
|
|
47
|
+
status: number,
|
|
48
|
+
body: unknown,
|
|
49
|
+
): HttpError {
|
|
50
|
+
const err = new Error(message) as HttpError;
|
|
51
|
+
err.name = "HttpError";
|
|
52
|
+
err.status = status;
|
|
53
|
+
err.body = body;
|
|
54
|
+
return err;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildUrl(baseUrl: string, path: string, query?: Query): string {
|
|
58
|
+
const url = new URL(path.startsWith("/") ? path : `/${path}`, `${baseUrl}/`);
|
|
59
|
+
if (query) {
|
|
60
|
+
for (const [key, value] of Object.entries(query)) {
|
|
61
|
+
if (value === undefined) continue;
|
|
62
|
+
url.searchParams.set(key, String(value));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return url.toString();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function bodyMessage(status: number, body: unknown): string {
|
|
69
|
+
if (
|
|
70
|
+
body &&
|
|
71
|
+
typeof body === "object" &&
|
|
72
|
+
"error" in body &&
|
|
73
|
+
typeof (body as { error: unknown }).error === "string"
|
|
74
|
+
) {
|
|
75
|
+
return `${status}: ${(body as { error: string }).error}`;
|
|
76
|
+
}
|
|
77
|
+
return `request failed with status ${status}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Build an {@link AdminClient} bound to the given resolved config. */
|
|
81
|
+
export function createAdminClient(cfg: ResolvedConfig): AdminClient {
|
|
82
|
+
async function request<T>(
|
|
83
|
+
method: string,
|
|
84
|
+
path: string,
|
|
85
|
+
opts: { query?: Query; body?: unknown; auth: boolean },
|
|
86
|
+
): Promise<T> {
|
|
87
|
+
if (opts.auth && !cfg.adminKey) {
|
|
88
|
+
throw makeHttpError(
|
|
89
|
+
"no admin key configured — pass --admin-key, or set HOGSEND_ADMIN_KEY / ADMIN_API_KEY",
|
|
90
|
+
0,
|
|
91
|
+
undefined,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
96
|
+
if (opts.auth && cfg.adminKey) {
|
|
97
|
+
headers.Authorization = `Bearer ${cfg.adminKey}`;
|
|
98
|
+
}
|
|
99
|
+
if (opts.body !== undefined) {
|
|
100
|
+
headers["Content-Type"] = "application/json";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const url = buildUrl(cfg.baseUrl, path, opts.query);
|
|
104
|
+
|
|
105
|
+
let res: Response;
|
|
106
|
+
try {
|
|
107
|
+
res = await fetch(url, {
|
|
108
|
+
method,
|
|
109
|
+
headers,
|
|
110
|
+
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
111
|
+
});
|
|
112
|
+
} catch (cause) {
|
|
113
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
114
|
+
throw makeHttpError(`cannot reach ${cfg.baseUrl} (${msg})`, 0, undefined);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const text = await res.text();
|
|
118
|
+
let parsed: unknown;
|
|
119
|
+
if (text.length > 0) {
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(text);
|
|
122
|
+
} catch {
|
|
123
|
+
parsed = text;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
throw makeHttpError(bodyMessage(res.status, parsed), res.status, parsed);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return parsed as T;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
cfg,
|
|
136
|
+
get: <T>(path: string, query?: Query, extras?: RequestExtras) =>
|
|
137
|
+
request<T>("GET", path, { query, auth: extras?.auth ?? true }),
|
|
138
|
+
patch: <T>(path: string, body: unknown) =>
|
|
139
|
+
request<T>("PATCH", path, { body, auth: true }),
|
|
140
|
+
post: <T>(path: string, body: unknown) =>
|
|
141
|
+
request<T>("POST", path, { body, auth: true }),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { isHttpError };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancel,
|
|
3
|
+
intro as clackIntro,
|
|
4
|
+
note as clackNote,
|
|
5
|
+
outro as clackOutro,
|
|
6
|
+
spinner,
|
|
7
|
+
} from "@clack/prompts";
|
|
8
|
+
import color from "picocolors";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unified output sink. Two modes:
|
|
12
|
+
*
|
|
13
|
+
* - human: TTY clack chrome (intro badge, spinners, notes, outro) + tables.
|
|
14
|
+
* Falls back to plain console.log lines when stdout is not a TTY.
|
|
15
|
+
* - json (`--json`): ALL chrome is a no-op; the command emits exactly one
|
|
16
|
+
* JSON document via `out.json(payload)`. Nothing else touches stdout, so a
|
|
17
|
+
* --json run is always a single valid JSON document — safe for agents.
|
|
18
|
+
*
|
|
19
|
+
* `interactive` is true only when human mode AND stdout is a TTY; commands key
|
|
20
|
+
* spinner/prompt behaviour off it. `isJson` flips command control flow to the
|
|
21
|
+
* non-interactive branch.
|
|
22
|
+
*/
|
|
23
|
+
export interface Output {
|
|
24
|
+
/** True when human-mode AND stdout is a TTY (clack chrome is live). */
|
|
25
|
+
readonly interactive: boolean;
|
|
26
|
+
/** True when `--json` was passed. */
|
|
27
|
+
readonly isJson: boolean;
|
|
28
|
+
/** Session intro badge. No-op in json / non-TTY. */
|
|
29
|
+
intro(title: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Run an async step with a spinner in interactive mode; a plain awaited call
|
|
32
|
+
* otherwise. The label is logged (not spun) when non-interactive & not json.
|
|
33
|
+
*/
|
|
34
|
+
step<T>(label: string, fn: () => Promise<T>): Promise<T>;
|
|
35
|
+
/** Boxed note. No-op in json / non-TTY (prints plain lines in non-TTY human). */
|
|
36
|
+
note(body: string, title?: string): void;
|
|
37
|
+
/** Render an array of records as a table (human only; no-op in json). */
|
|
38
|
+
table(rows: Record<string, unknown>[], columns?: string[]): void;
|
|
39
|
+
/** Render a key/value object (human only; no-op in json). */
|
|
40
|
+
kv(obj: Record<string, unknown>, title?: string): void;
|
|
41
|
+
/** Plain human/plain-text line. No-op in json. */
|
|
42
|
+
log(msg: string): void;
|
|
43
|
+
/** Emit the single JSON document. Only meaningful in json mode. */
|
|
44
|
+
json(payload: unknown): void;
|
|
45
|
+
/** Session outro. No-op in json / non-TTY. */
|
|
46
|
+
outro(msg: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Fail terminally. json: prints `{ "error": message }` to stdout, exit 1.
|
|
49
|
+
* human (TTY): clack cancel(message), exit 1. human (non-TTY): stderr, exit 1.
|
|
50
|
+
*/
|
|
51
|
+
fail(message: string): never;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderTable(
|
|
55
|
+
rows: Record<string, unknown>[],
|
|
56
|
+
columns?: string[],
|
|
57
|
+
): string {
|
|
58
|
+
if (rows.length === 0) return color.dim("(no rows)");
|
|
59
|
+
const cols =
|
|
60
|
+
columns ??
|
|
61
|
+
Array.from(
|
|
62
|
+
rows.reduce<Set<string>>((set, row) => {
|
|
63
|
+
for (const key of Object.keys(row)) set.add(key);
|
|
64
|
+
return set;
|
|
65
|
+
}, new Set<string>()),
|
|
66
|
+
);
|
|
67
|
+
const cell = (value: unknown): string => {
|
|
68
|
+
if (value === null || value === undefined) return "";
|
|
69
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
70
|
+
return String(value);
|
|
71
|
+
};
|
|
72
|
+
const widths = cols.map((c) =>
|
|
73
|
+
Math.max(c.length, ...rows.map((r) => cell(r[c]).length)),
|
|
74
|
+
);
|
|
75
|
+
const pad = (text: string, width: number) =>
|
|
76
|
+
text + " ".repeat(width - text.length);
|
|
77
|
+
const header = cols
|
|
78
|
+
.map((c, i) => color.bold(pad(c, widths[i] ?? 0)))
|
|
79
|
+
.join(" ");
|
|
80
|
+
const sep = cols.map((_, i) => "-".repeat(widths[i] ?? 0)).join(" ");
|
|
81
|
+
const body = rows
|
|
82
|
+
.map((r) => cols.map((c, i) => pad(cell(r[c]), widths[i] ?? 0)).join(" "))
|
|
83
|
+
.join("\n");
|
|
84
|
+
return `${header}\n${color.dim(sep)}\n${body}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderKv(obj: Record<string, unknown>): string {
|
|
88
|
+
const keys = Object.keys(obj);
|
|
89
|
+
if (keys.length === 0) return color.dim("(empty)");
|
|
90
|
+
const width = Math.max(...keys.map((k) => k.length));
|
|
91
|
+
return keys
|
|
92
|
+
.map((k) => {
|
|
93
|
+
const v = obj[k];
|
|
94
|
+
const str =
|
|
95
|
+
v === null || v === undefined
|
|
96
|
+
? ""
|
|
97
|
+
: typeof v === "object"
|
|
98
|
+
? JSON.stringify(v)
|
|
99
|
+
: String(v);
|
|
100
|
+
return `${color.dim(`${k}:`.padEnd(width + 1))} ${str}`;
|
|
101
|
+
})
|
|
102
|
+
.join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Build an {@link Output} for the given mode. */
|
|
106
|
+
export function createOutput(opts: { json: boolean }): Output {
|
|
107
|
+
const isJson = opts.json;
|
|
108
|
+
const interactive = !isJson && Boolean(process.stdout.isTTY);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
interactive,
|
|
112
|
+
isJson,
|
|
113
|
+
|
|
114
|
+
intro(title) {
|
|
115
|
+
if (!interactive) return;
|
|
116
|
+
clackIntro(title);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async step<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
120
|
+
if (interactive) {
|
|
121
|
+
const s = spinner();
|
|
122
|
+
s.start(label);
|
|
123
|
+
try {
|
|
124
|
+
const result = await fn();
|
|
125
|
+
s.stop(`${color.green("✓")} ${label}`);
|
|
126
|
+
return result;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
s.stop(`${color.red("✗")} ${label}`);
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!isJson) console.log(` ${label} ...`);
|
|
133
|
+
return fn();
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
note(body, title) {
|
|
137
|
+
if (isJson) return;
|
|
138
|
+
if (interactive) {
|
|
139
|
+
clackNote(body, title);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (title) console.log(`\n${title}`);
|
|
143
|
+
console.log(body);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
table(rows, columns) {
|
|
147
|
+
if (isJson) return;
|
|
148
|
+
console.log(renderTable(rows, columns));
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
kv(obj, title) {
|
|
152
|
+
if (isJson) return;
|
|
153
|
+
if (title) console.log(color.bold(title));
|
|
154
|
+
console.log(renderKv(obj));
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
log(msg) {
|
|
158
|
+
if (isJson) return;
|
|
159
|
+
console.log(msg);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
json(payload) {
|
|
163
|
+
// Only stdout write in json mode; pretty-printed, single document.
|
|
164
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
outro(msg) {
|
|
168
|
+
if (!interactive) return;
|
|
169
|
+
clackOutro(msg);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
fail(message): never {
|
|
173
|
+
if (isJson) {
|
|
174
|
+
process.stdout.write(`${JSON.stringify({ error: message })}\n`);
|
|
175
|
+
} else if (interactive) {
|
|
176
|
+
cancel(message);
|
|
177
|
+
} else {
|
|
178
|
+
process.stderr.write(`${color.red("error")} ${message}\n`);
|
|
179
|
+
}
|
|
180
|
+
process.exit(1);
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { color };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cancel, isCancel } from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Guard a clack prompt result. clack returns a cancellation symbol when the
|
|
5
|
+
* user hits Ctrl-C / Esc; this unwraps the value or aborts the whole CLI
|
|
6
|
+
* cleanly (exit 0 — a deliberate cancel, not an error).
|
|
7
|
+
*
|
|
8
|
+
* Re-export clack's `text`/`select`/`confirm`/`multiselect` from
|
|
9
|
+
* `@clack/prompts` directly in command files; wrap each call in `bail()`.
|
|
10
|
+
*/
|
|
11
|
+
export function bail<T>(value: T | symbol): T {
|
|
12
|
+
if (isCancel(value)) {
|
|
13
|
+
cancel("Cancelled.");
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
return value as T;
|
|
17
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shared skill-install machinery, used by `hogsend skills`, `hogsend upgrade`,
|
|
16
|
+
* and the `hogsend doctor` staleness nudge. The CLI ships a `skills/` dir in its
|
|
17
|
+
* published tarball (package.json files[]); these helpers copy it into a
|
|
18
|
+
* consumer project's ./.claude/skills/ and track which CLI version produced it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface BundledSkill {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
installed: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CopyResult {
|
|
28
|
+
name: string;
|
|
29
|
+
installed: boolean;
|
|
30
|
+
skipped: boolean;
|
|
31
|
+
path: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Persisted record of the last skill install — drives the staleness nudge. */
|
|
35
|
+
export interface SkillsStamp {
|
|
36
|
+
/** The @hogsend/cli version that produced the installed skills. */
|
|
37
|
+
cliVersion: string;
|
|
38
|
+
/** Installed skill names. */
|
|
39
|
+
skills: string[];
|
|
40
|
+
/** ISO timestamp of the install/refresh (omitted by build-time stamps). */
|
|
41
|
+
updatedAt?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the directory holding the bundled skills shipped in the tarball.
|
|
46
|
+
* At runtime the CLI is bundled into <pkg>/dist/bin.js, so the skills dir
|
|
47
|
+
* (shipped via package.json files[]) is one level up at <pkg>/skills.
|
|
48
|
+
*/
|
|
49
|
+
export function bundledSkillsDir(): string {
|
|
50
|
+
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Target directory for installed skills in the consumer project. */
|
|
54
|
+
export function installDir(cwd: string): string {
|
|
55
|
+
return join(cwd, ".claude", "skills");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Path to the install stamp (sibling of skills/, NOT inside it). */
|
|
59
|
+
export function stampPath(cwd: string): string {
|
|
60
|
+
return join(cwd, ".claude", ".hogsend-skills.json");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** This CLI's own version (read from its package.json at <pkg>/package.json). */
|
|
64
|
+
export function cliVersion(): string {
|
|
65
|
+
try {
|
|
66
|
+
const require = createRequire(import.meta.url);
|
|
67
|
+
const pkg = require("../package.json") as { version?: string };
|
|
68
|
+
return pkg.version ?? "0.0.0";
|
|
69
|
+
} catch {
|
|
70
|
+
return "0.0.0";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read a file as utf8, returning "" on any error (never throws). */
|
|
75
|
+
function readFileSyncSafe(path: string): string {
|
|
76
|
+
try {
|
|
77
|
+
return readFileSync(path, "utf8");
|
|
78
|
+
} catch {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A single-line `key: value` reader for SKILL.md YAML frontmatter. */
|
|
84
|
+
function readFrontmatterField(skillDir: string, field: string): string {
|
|
85
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
86
|
+
if (!existsSync(skillFile)) return "";
|
|
87
|
+
// Tiny frontmatter scan — avoids a YAML dep. Reads only the top block.
|
|
88
|
+
const raw = readFileSyncSafe(skillFile);
|
|
89
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
90
|
+
if (!fmMatch) return "";
|
|
91
|
+
const block = fmMatch[1] ?? "";
|
|
92
|
+
for (const line of block.split("\n")) {
|
|
93
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
94
|
+
if (m && m[1] === field) {
|
|
95
|
+
return (m[2] ?? "").replace(/^["']|["']$/g, "").trim();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Enumerate bundled skills (each is a subdir with a SKILL.md). */
|
|
102
|
+
export function listBundledSkills(cwd: string): BundledSkill[] {
|
|
103
|
+
const dir = bundledSkillsDir();
|
|
104
|
+
if (!existsSync(dir)) return [];
|
|
105
|
+
const target = installDir(cwd);
|
|
106
|
+
const entries = readdirSync(dir).filter((name) => {
|
|
107
|
+
const full = join(dir, name);
|
|
108
|
+
return statSync(full).isDirectory() && existsSync(join(full, "SKILL.md"));
|
|
109
|
+
});
|
|
110
|
+
return entries.sort().map((name) => ({
|
|
111
|
+
name,
|
|
112
|
+
description: readFrontmatterField(join(dir, name), "description"),
|
|
113
|
+
installed: existsSync(join(target, name)),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Copy one bundled skill into the project, honouring --force. */
|
|
118
|
+
export function copySkill(
|
|
119
|
+
name: string,
|
|
120
|
+
cwd: string,
|
|
121
|
+
force: boolean,
|
|
122
|
+
): CopyResult {
|
|
123
|
+
const src = join(bundledSkillsDir(), name);
|
|
124
|
+
const dest = join(installDir(cwd), name);
|
|
125
|
+
const exists = existsSync(dest);
|
|
126
|
+
if (exists && !force) {
|
|
127
|
+
return { name, installed: false, skipped: true, path: dest };
|
|
128
|
+
}
|
|
129
|
+
mkdirSync(installDir(cwd), { recursive: true });
|
|
130
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
131
|
+
return { name, installed: true, skipped: false, path: dest };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Record which CLI version produced the currently-installed skills. */
|
|
135
|
+
export function writeSkillsStamp(cwd: string, skills: string[]): void {
|
|
136
|
+
const stamp: SkillsStamp = {
|
|
137
|
+
cliVersion: cliVersion(),
|
|
138
|
+
skills: [...skills].sort(),
|
|
139
|
+
updatedAt: new Date().toISOString(),
|
|
140
|
+
};
|
|
141
|
+
mkdirSync(join(cwd, ".claude"), { recursive: true });
|
|
142
|
+
writeFileSync(stampPath(cwd), `${JSON.stringify(stamp, null, 2)}\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Read the install stamp, or null when absent/unreadable. */
|
|
146
|
+
export function readSkillsStamp(cwd: string): SkillsStamp | null {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(readFileSync(stampPath(cwd), "utf8")) as
|
|
149
|
+
| SkillsStamp
|
|
150
|
+
| undefined;
|
|
151
|
+
return parsed && typeof parsed.cliVersion === "string" ? parsed : null;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Numeric semver compare on the release line (prerelease tags ignored). */
|
|
158
|
+
export function compareVersions(a: string, b: string): number {
|
|
159
|
+
const parse = (v: string) =>
|
|
160
|
+
(v.split("-")[0] ?? "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
161
|
+
const pa = parse(a);
|
|
162
|
+
const pb = parse(b);
|
|
163
|
+
for (let i = 0; i < 3; i++) {
|
|
164
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
165
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
166
|
+
}
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Staleness verdict for the skills installed in `cwd`. Returns null when no
|
|
172
|
+
* stamp exists (not a tracked app), else whether the installed skills came from
|
|
173
|
+
* an OLDER CLI than the one running now.
|
|
174
|
+
*/
|
|
175
|
+
export function skillsStaleness(
|
|
176
|
+
cwd: string,
|
|
177
|
+
): { stale: boolean; installed: string; current: string } | null {
|
|
178
|
+
const stamp = readSkillsStamp(cwd);
|
|
179
|
+
if (!stamp) return null;
|
|
180
|
+
const current = cliVersion();
|
|
181
|
+
return {
|
|
182
|
+
stale: compareVersions(stamp.cliVersion, current) < 0,
|
|
183
|
+
installed: stamp.cliVersion,
|
|
184
|
+
current,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 240 10% 3.9%;--card: 0 0% 100%;--card-foreground: 240 10% 3.9%;--primary: 240 5.9% 10%;--primary-foreground: 0 0% 98%;--secondary: 240 4.8% 95.9%;--secondary-foreground: 240 5.9% 10%;--muted: 240 4.8% 95.9%;--muted-foreground: 240 3.8% 46.1%;--accent: 240 4.8% 95.9%;--accent-foreground: 240 5.9% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 240 5.9% 90%;--input: 240 5.9% 90%;--ring: 240 5.9% 10%;--radius: .5rem}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-y-0{top:0;bottom:0}.bottom-4{bottom:1rem}.left-3{left:.75rem}.right-0{right:0}.right-2{right:.5rem}.right-4{right:1rem}.top-1\/2{top:50%}.z-10{z-index:10}.z-50{z-index:50}.z-\[60\]{z-index:60}.-mr-2{margin-right:-.5rem}.-mt-2{margin-top:-.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-20{height:5rem}.h-24{height:6rem}.h-28{height:7rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[600px\]{height:600px}.h-full{height:100%}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-60{width:15rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[2px\]{min-width:2px}.max-w-2xl{max-width:42rem}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-destructive\/40{border-color:hsl(var(--destructive) / .4)}.border-emerald-500\/40{border-color:#10b98166}.border-input{border-color:hsl(var(--input))}.border-sky-500\/40{border-color:#0ea5e966}.border-transparent{border-color:transparent}.border-violet-500\/40{border-color:#8b5cf666}.bg-accent{background-color:hsl(var(--accent))}.bg-background{background-color:hsl(var(--background))}.bg-black\/50{background-color:#00000080}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/5{background-color:hsl(var(--destructive) / .05)}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/20{background-color:hsl(var(--muted) / .2)}.bg-muted\/30{background-color:hsl(var(--muted) / .3)}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/70{background-color:hsl(var(--primary) / .7)}.bg-secondary{background-color:hsl(var(--secondary))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-9{padding-left:2.25rem}.pr-8{padding-right:2rem}.pt-0{padding-top:0}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.text-accent-foreground{color:hsl(var(--accent-foreground))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-500{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sky-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.text-violet-600{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:bg-primary{background-color:hsl(var(--primary))}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:hsl(var(--muted))}.dark\:text-emerald-400:is(.dark *){--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.dark\:text-sky-400:is(.dark *){--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.dark\:text-violet-400:is(.dark *){--tw-text-opacity: 1;color:rgb(167 139 250 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:768px){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[260px_1fr\]{grid-template-columns:260px 1fr}}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:0}.\[\&\>li\:last-child\>div\:first-child\>span\:last-child\]\:hidden>li:last-child>div:first-child>span:last-child{display:none}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}
|