@checkstack/scripts 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +15 -5
  2. package/src/commands/create.ts +16 -23
  3. package/src/commands/plugin-pack.ts +17 -28
  4. package/src/dev-tui/App.render.test.tsx +135 -0
  5. package/src/dev-tui/App.smoke.test.tsx +142 -0
  6. package/src/dev-tui/App.tsx +522 -0
  7. package/src/dev-tui/alert-buffer.test.ts +62 -0
  8. package/src/dev-tui/alert-buffer.ts +51 -0
  9. package/src/dev-tui/alt-screen.test.ts +66 -0
  10. package/src/dev-tui/alt-screen.ts +65 -0
  11. package/src/dev-tui/cli.tsx +89 -0
  12. package/src/dev-tui/fake-supervisor.ts +76 -0
  13. package/src/dev-tui/graceful-shutdown.test.ts +61 -0
  14. package/src/dev-tui/graceful-shutdown.ts +32 -0
  15. package/src/dev-tui/kill-tree.test.ts +47 -0
  16. package/src/dev-tui/kill-tree.ts +64 -0
  17. package/src/dev-tui/layout.test.ts +89 -0
  18. package/src/dev-tui/layout.ts +126 -0
  19. package/src/dev-tui/log-level.test.ts +94 -0
  20. package/src/dev-tui/log-level.ts +104 -0
  21. package/src/dev-tui/plain-runner.ts +60 -0
  22. package/src/dev-tui/process-config.test.ts +42 -0
  23. package/src/dev-tui/process-config.ts +61 -0
  24. package/src/dev-tui/readiness.test.ts +54 -0
  25. package/src/dev-tui/readiness.ts +44 -0
  26. package/src/dev-tui/scrollback.test.ts +83 -0
  27. package/src/dev-tui/scrollback.ts +82 -0
  28. package/src/dev-tui/supervisor.ts +231 -0
  29. package/src/dev-tui/text.test.ts +72 -0
  30. package/src/dev-tui/text.ts +101 -0
  31. package/src/dev-tui/types.ts +29 -0
  32. package/src/scaffold/index.ts +22 -0
  33. package/src/scaffold/resolve-versions.test.ts +49 -0
  34. package/src/scaffold/resolve-versions.ts +55 -0
  35. package/src/scaffold/rewrite-workspace-versions.test.ts +102 -0
  36. package/src/scaffold/rewrite-workspace-versions.ts +111 -0
  37. package/src/scaffold/scaffold-plugin.test.ts +209 -0
  38. package/src/scaffold/scaffold-plugin.ts +309 -0
  39. package/src/templates/backend/.changeset/initial.md.hbs +1 -1
  40. package/src/templates/backend/drizzle/0000_init.sql +7 -0
  41. package/src/templates/backend/drizzle/meta/0000_snapshot.json +65 -0
  42. package/src/templates/backend/drizzle/meta/_journal.json +13 -0
  43. package/src/templates/backend/drizzle.config.ts.hbs +5 -1
  44. package/src/templates/backend/package.json.hbs +7 -3
  45. package/src/templates/backend/src/index.ts.hbs +1 -1
  46. package/src/templates/backend/src/router.ts.hbs +1 -1
  47. package/src/templates/backend/src/service.ts.hbs +1 -1
  48. package/src/templates/common/.changeset/initial.md.hbs +1 -1
  49. package/src/templates/common/README.md.hbs +28 -11
  50. package/src/templates/common/package.json.hbs +1 -1
  51. package/src/templates/common/src/plugin-metadata.ts.hbs +1 -1
  52. package/src/templates/frontend/.changeset/initial.md.hbs +1 -1
  53. package/src/templates/frontend/package.json.hbs +2 -2
  54. package/src/templates/frontend/src/api.ts.hbs +2 -2
  55. package/src/templates/frontend/src/components/{{pluginNamePascal}}ListPage.tsx.hbs +1 -1
  56. package/src/templates/frontend/src/index.tsx.hbs +10 -4
  57. package/src/templates/standalone-root/.changeset/config.json.hbs +11 -0
  58. package/src/templates/standalone-root/.changeset/initial.md.hbs +9 -0
  59. package/src/templates/standalone-root/README.md.hbs +75 -0
  60. package/src/templates/standalone-root/eslint.config.mjs.hbs +37 -0
  61. package/src/templates/standalone-root/package.json.hbs +27 -0
  62. package/src/templates/standalone-root/tsconfig.json.hbs +13 -0
  63. package/src/templates.test.ts +20 -0
  64. package/src/tui/components.test.tsx +28 -0
  65. package/src/tui/components.tsx +159 -0
  66. package/src/tui/index.ts +31 -0
  67. package/src/tui/theme.test.ts +54 -0
  68. package/src/tui/theme.ts +60 -0
  69. package/src/utils/template.ts +42 -0
@@ -0,0 +1,159 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { defaultTheme } from "./theme.ts";
4
+ import type { TuiLevel, TuiStatus, TuiTheme } from "./theme.ts";
5
+
6
+ /**
7
+ * Thin, reusable ink primitives built over the theme tokens. These have no
8
+ * dev-TUI-specific knowledge, so the CLI can adopt them later. Each accepts an
9
+ * optional `theme` so callers can override tokens without forking the kit.
10
+ */
11
+
12
+ export interface StatusDotProps {
13
+ status: TuiStatus;
14
+ theme?: TuiTheme;
15
+ }
16
+
17
+ /** A single colored status dot. */
18
+ export function StatusDot({
19
+ status,
20
+ theme = defaultTheme,
21
+ }: StatusDotProps): React.ReactElement {
22
+ return <Text color={theme.status[status]}>●</Text>;
23
+ }
24
+
25
+ /** Braille spinner frames (smooth, monospace-safe, single column). */
26
+ export const SPINNER_FRAMES = [
27
+ "⠋",
28
+ "⠙",
29
+ "⠹",
30
+ "⠸",
31
+ "⠼",
32
+ "⠴",
33
+ "⠦",
34
+ "⠧",
35
+ "⠇",
36
+ "⠏",
37
+ ] as const;
38
+
39
+ export interface SpinnerProps {
40
+ /** Monotonic tick; the rendered glyph is `frame % SPINNER_FRAMES.length`. */
41
+ frame: number;
42
+ /** Glyph color (defaults to the chrome accent). */
43
+ color?: string;
44
+ theme?: TuiTheme;
45
+ }
46
+
47
+ /**
48
+ * A controlled spinner: it renders the glyph for the given `frame` and holds no
49
+ * timer itself, so the caller owns the animation cadence (and tests stay
50
+ * deterministic). Pair it with a `setInterval` that bumps `frame`.
51
+ */
52
+ export function Spinner({
53
+ frame,
54
+ color,
55
+ theme = defaultTheme,
56
+ }: SpinnerProps): React.ReactElement {
57
+ const glyph = SPINNER_FRAMES[
58
+ ((frame % SPINNER_FRAMES.length) + SPINNER_FRAMES.length) %
59
+ SPINNER_FRAMES.length
60
+ ];
61
+ return <Text color={color ?? theme.chrome.accent}>{glyph}</Text>;
62
+ }
63
+
64
+ /** Line-wrapping behavior, mirroring ink's `Text` `wrap` prop. */
65
+ export type TextWrap = React.ComponentProps<typeof Text>["wrap"];
66
+
67
+ export interface LevelTextProps {
68
+ level: TuiLevel;
69
+ children: React.ReactNode;
70
+ theme?: TuiTheme;
71
+ /**
72
+ * Wrapping behavior. Pass `"truncate"` to keep a line to a single visual row
73
+ * (so a fixed-height pane cannot overflow when lines are long).
74
+ */
75
+ wrap?: TextWrap;
76
+ }
77
+
78
+ /** Text colored by log level (debug dimmed, warn yellow, error red). */
79
+ export function LevelText({
80
+ level,
81
+ children,
82
+ theme = defaultTheme,
83
+ wrap,
84
+ }: LevelTextProps): React.ReactElement {
85
+ return (
86
+ <Text color={theme.level[level]} dimColor={level === "debug"} wrap={wrap}>
87
+ {children}
88
+ </Text>
89
+ );
90
+ }
91
+
92
+ export interface PanelProps {
93
+ title?: string;
94
+ children: React.ReactNode;
95
+ theme?: TuiTheme;
96
+ /** Override the border color (defaults to the chrome border token). */
97
+ borderColor?: string;
98
+ /** Let the panel grow to fill available space in a flex column. */
99
+ flexGrow?: number;
100
+ }
101
+
102
+ /** A bordered box with an optional dim title row. */
103
+ export function Panel({
104
+ title,
105
+ children,
106
+ theme = defaultTheme,
107
+ borderColor,
108
+ flexGrow,
109
+ }: PanelProps): React.ReactElement {
110
+ return (
111
+ <Box
112
+ flexDirection="column"
113
+ borderStyle="round"
114
+ borderColor={borderColor ?? theme.chrome.border}
115
+ paddingX={1}
116
+ flexGrow={flexGrow}
117
+ >
118
+ {title === undefined ? null : (
119
+ <Box marginBottom={0}>
120
+ <Text color={theme.chrome.dim} bold>
121
+ {title}
122
+ </Text>
123
+ </Box>
124
+ )}
125
+ {children}
126
+ </Box>
127
+ );
128
+ }
129
+
130
+ export interface KeyHint {
131
+ /** The key or key combination, e.g. "q", "Tab", "PgUp". */
132
+ keys: string;
133
+ /** What the key does, e.g. "quit". */
134
+ label: string;
135
+ }
136
+
137
+ export interface KeyHintsProps {
138
+ hints: readonly KeyHint[];
139
+ theme?: TuiTheme;
140
+ }
141
+
142
+ /** A footer row rendering keybinding hints with accent-colored keys. */
143
+ export function KeyHints({
144
+ hints,
145
+ theme = defaultTheme,
146
+ }: KeyHintsProps): React.ReactElement {
147
+ return (
148
+ <Box>
149
+ {hints.map((hint, index) => (
150
+ <Box key={hint.keys} marginRight={index === hints.length - 1 ? 0 : 2}>
151
+ <Text color={theme.chrome.accent} bold>
152
+ {hint.keys}
153
+ </Text>
154
+ <Text color={theme.chrome.dim}> {hint.label}</Text>
155
+ </Box>
156
+ ))}
157
+ </Box>
158
+ );
159
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Reusable terminal-UI kit: theme tokens plus thin ink primitives. The kit is
3
+ * self-contained and has a one-way dependency contract (consumers depend on the
4
+ * kit, never the reverse), so the plugin CLI can adopt it without a refactor.
5
+ */
6
+ export { defaultTheme } from "./theme.ts";
7
+ export type {
8
+ TuiTheme,
9
+ TuiLevel,
10
+ TuiStatus,
11
+ LevelColors,
12
+ StatusColors,
13
+ ChromeColors,
14
+ } from "./theme.ts";
15
+
16
+ export {
17
+ StatusDot,
18
+ Spinner,
19
+ SPINNER_FRAMES,
20
+ LevelText,
21
+ Panel,
22
+ KeyHints,
23
+ } from "./components.tsx";
24
+ export type {
25
+ StatusDotProps,
26
+ SpinnerProps,
27
+ LevelTextProps,
28
+ PanelProps,
29
+ KeyHintsProps,
30
+ KeyHint,
31
+ } from "./components.tsx";
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { defaultTheme } from "./theme.ts";
3
+ import type { TuiLevel, TuiStatus } from "./theme.ts";
4
+
5
+ const ALL_LEVELS: TuiLevel[] = ["debug", "info", "warn", "error"];
6
+ const ALL_STATUSES: TuiStatus[] = [
7
+ "starting",
8
+ "ready",
9
+ "errored",
10
+ "stopped",
11
+ ];
12
+
13
+ describe("defaultTheme", () => {
14
+ it("defines a non-empty color for every level", () => {
15
+ for (const level of ALL_LEVELS) {
16
+ expect(defaultTheme.level[level]).toBeString();
17
+ expect(defaultTheme.level[level].length).toBeGreaterThan(0);
18
+ }
19
+ });
20
+
21
+ it("defines a non-empty color for every status", () => {
22
+ for (const status of ALL_STATUSES) {
23
+ expect(defaultTheme.status[status]).toBeString();
24
+ expect(defaultTheme.status[status].length).toBeGreaterThan(0);
25
+ }
26
+ });
27
+
28
+ it("defines all chrome tokens", () => {
29
+ expect(defaultTheme.chrome.border.length).toBeGreaterThan(0);
30
+ expect(defaultTheme.chrome.dim.length).toBeGreaterThan(0);
31
+ expect(defaultTheme.chrome.accent.length).toBeGreaterThan(0);
32
+ expect(defaultTheme.chrome.text.length).toBeGreaterThan(0);
33
+ });
34
+
35
+ it("uses the conventional alert colors (warn yellow, error red)", () => {
36
+ expect(defaultTheme.level.warn).toBe("yellow");
37
+ expect(defaultTheme.level.error).toBe("red");
38
+ });
39
+
40
+ it("maps the ready status to green and errored to red", () => {
41
+ expect(defaultTheme.status.ready).toBe("green");
42
+ expect(defaultTheme.status.errored).toBe("red");
43
+ });
44
+
45
+ it("does not contain duplicate keys between level and status maps", () => {
46
+ // Sanity: the two maps are independent records with their own key spaces.
47
+ expect(Object.keys(defaultTheme.level).sort()).toEqual(
48
+ [...ALL_LEVELS].sort(),
49
+ );
50
+ expect(Object.keys(defaultTheme.status).sort()).toEqual(
51
+ [...ALL_STATUSES].sort(),
52
+ );
53
+ });
54
+ });
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Theme tokens for the reusable terminal-UI kit. These are plain data (no ink
3
+ * imports) so they can be unit-tested and consumed by any ink component. The
4
+ * CLI can adopt the same kit later by importing {@link defaultTheme}.
5
+ *
6
+ * Color values are ink/chalk color names (resolved by ink at render time),
7
+ * keeping this module dependency-free and DOM-free.
8
+ */
9
+
10
+ export type TuiLevel = "debug" | "info" | "warn" | "error";
11
+ export type TuiStatus = "starting" | "ready" | "errored" | "stopped";
12
+
13
+ /** Every level maps to exactly one color. */
14
+ export type LevelColors = Readonly<Record<TuiLevel, string>>;
15
+ /** Every status maps to exactly one color. */
16
+ export type StatusColors = Readonly<Record<TuiStatus, string>>;
17
+
18
+ /** Chrome colors: borders, dimmed labels, and the accent used for focus. */
19
+ export interface ChromeColors {
20
+ /** Border color for panels and bordered boxes. */
21
+ readonly border: string;
22
+ /** Dimmed text for chrome / secondary labels. */
23
+ readonly dim: string;
24
+ /** Accent color for the focused tab / highlights. */
25
+ readonly accent: string;
26
+ /** Foreground for ordinary content text. */
27
+ readonly text: string;
28
+ }
29
+
30
+ export interface TuiTheme {
31
+ readonly level: LevelColors;
32
+ readonly status: StatusColors;
33
+ readonly chrome: ChromeColors;
34
+ }
35
+
36
+ /**
37
+ * The default theme: restrained color, dim chrome, bright content. Debug is
38
+ * dimmed gray, info default-ish, warn yellow, error red. Status dots follow the
39
+ * spec: starting yellow, ready green, errored red, stopped dim gray.
40
+ */
41
+ export const defaultTheme: TuiTheme = {
42
+ level: {
43
+ debug: "gray",
44
+ info: "white",
45
+ warn: "yellow",
46
+ error: "red",
47
+ },
48
+ status: {
49
+ starting: "yellow",
50
+ ready: "green",
51
+ errored: "red",
52
+ stopped: "gray",
53
+ },
54
+ chrome: {
55
+ border: "gray",
56
+ dim: "gray",
57
+ accent: "cyan",
58
+ text: "white",
59
+ },
60
+ };
@@ -17,6 +17,28 @@ export interface TemplateData {
17
17
  pluginId: string;
18
18
  pluginType: string;
19
19
  currentYear: number;
20
+ /**
21
+ * npm scope for the *generated* packages, WITHOUT the leading `@`
22
+ * (e.g. `acme`). Defaults to `checkstack` so the in-monorepo `create`
23
+ * command renders byte-for-byte identical output to before. An empty
24
+ * string means "publish the trio unscoped" (e.g. `widget-backend`),
25
+ * which the {@link scopedPackageName} helper handles.
26
+ */
27
+ packageScope: string;
28
+ }
29
+
30
+ /**
31
+ * Build a published package name from a scope + bare name. With a scope:
32
+ * `@acme/widget-backend`; without (empty scope): `widget-backend`.
33
+ */
34
+ export function scopedPackageName({
35
+ packageScope,
36
+ name,
37
+ }: {
38
+ packageScope: string;
39
+ name: string;
40
+ }): string {
41
+ return packageScope ? `@${packageScope}/${name}` : name;
20
42
  }
21
43
 
22
44
  /**
@@ -45,6 +67,19 @@ export function registerHelpers() {
45
67
  Handlebars.registerHelper("year", () => {
46
68
  return new Date().getFullYear();
47
69
  });
70
+
71
+ // `{{scoped "widget-common"}}` -> `@<packageScope>/widget-common`, or
72
+ // just `widget-common` when packageScope is empty. The scope is read from
73
+ // the template data root so callers don't repeat it at every use site.
74
+ Handlebars.registerHelper(
75
+ "scoped",
76
+ function (this: { packageScope?: string }, name: string) {
77
+ return scopedPackageName({
78
+ packageScope: this.packageScope ?? "checkstack",
79
+ name,
80
+ });
81
+ },
82
+ );
48
83
  }
49
84
 
50
85
  /**
@@ -127,10 +162,16 @@ export function prepareTemplateData({
127
162
  baseName,
128
163
  pluginType,
129
164
  description,
165
+ packageScope = "checkstack",
130
166
  }: {
131
167
  baseName: string;
132
168
  pluginType: string;
133
169
  description: string;
170
+ /**
171
+ * npm scope (without `@`) for the generated packages. Defaults to
172
+ * `checkstack` (the monorepo scope); pass `""` for an unscoped trio.
173
+ */
174
+ packageScope?: string;
134
175
  }): TemplateData {
135
176
  const pluginName = `${baseName}-${pluginType}`;
136
177
  const pluginNamePascal = baseName
@@ -150,5 +191,6 @@ export function prepareTemplateData({
150
191
  pluginId: pluginName,
151
192
  pluginType,
152
193
  currentYear: new Date().getFullYear(),
194
+ packageScope,
153
195
  };
154
196
  }