@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.
- package/package.json +15 -5
- package/src/commands/create.ts +16 -23
- package/src/commands/plugin-pack.ts +17 -28
- package/src/dev-tui/App.render.test.tsx +135 -0
- package/src/dev-tui/App.smoke.test.tsx +142 -0
- package/src/dev-tui/App.tsx +522 -0
- package/src/dev-tui/alert-buffer.test.ts +62 -0
- package/src/dev-tui/alert-buffer.ts +51 -0
- package/src/dev-tui/alt-screen.test.ts +66 -0
- package/src/dev-tui/alt-screen.ts +65 -0
- package/src/dev-tui/cli.tsx +89 -0
- package/src/dev-tui/fake-supervisor.ts +76 -0
- package/src/dev-tui/graceful-shutdown.test.ts +61 -0
- package/src/dev-tui/graceful-shutdown.ts +32 -0
- package/src/dev-tui/kill-tree.test.ts +47 -0
- package/src/dev-tui/kill-tree.ts +64 -0
- package/src/dev-tui/layout.test.ts +89 -0
- package/src/dev-tui/layout.ts +126 -0
- package/src/dev-tui/log-level.test.ts +94 -0
- package/src/dev-tui/log-level.ts +104 -0
- package/src/dev-tui/plain-runner.ts +60 -0
- package/src/dev-tui/process-config.test.ts +42 -0
- package/src/dev-tui/process-config.ts +61 -0
- package/src/dev-tui/readiness.test.ts +54 -0
- package/src/dev-tui/readiness.ts +44 -0
- package/src/dev-tui/scrollback.test.ts +83 -0
- package/src/dev-tui/scrollback.ts +82 -0
- package/src/dev-tui/supervisor.ts +231 -0
- package/src/dev-tui/text.test.ts +72 -0
- package/src/dev-tui/text.ts +101 -0
- package/src/dev-tui/types.ts +29 -0
- package/src/scaffold/index.ts +22 -0
- package/src/scaffold/resolve-versions.test.ts +49 -0
- package/src/scaffold/resolve-versions.ts +55 -0
- package/src/scaffold/rewrite-workspace-versions.test.ts +102 -0
- package/src/scaffold/rewrite-workspace-versions.ts +111 -0
- package/src/scaffold/scaffold-plugin.test.ts +209 -0
- package/src/scaffold/scaffold-plugin.ts +309 -0
- package/src/templates/backend/.changeset/initial.md.hbs +1 -1
- package/src/templates/backend/drizzle/0000_init.sql +7 -0
- package/src/templates/backend/drizzle/meta/0000_snapshot.json +65 -0
- package/src/templates/backend/drizzle/meta/_journal.json +13 -0
- package/src/templates/backend/drizzle.config.ts.hbs +5 -1
- package/src/templates/backend/package.json.hbs +7 -3
- package/src/templates/backend/src/index.ts.hbs +1 -1
- package/src/templates/backend/src/router.ts.hbs +1 -1
- package/src/templates/backend/src/service.ts.hbs +1 -1
- package/src/templates/common/.changeset/initial.md.hbs +1 -1
- package/src/templates/common/README.md.hbs +28 -11
- package/src/templates/common/package.json.hbs +1 -1
- package/src/templates/common/src/plugin-metadata.ts.hbs +1 -1
- package/src/templates/frontend/.changeset/initial.md.hbs +1 -1
- package/src/templates/frontend/package.json.hbs +2 -2
- package/src/templates/frontend/src/api.ts.hbs +2 -2
- package/src/templates/frontend/src/components/{{pluginNamePascal}}ListPage.tsx.hbs +1 -1
- package/src/templates/frontend/src/index.tsx.hbs +10 -4
- package/src/templates/standalone-root/.changeset/config.json.hbs +11 -0
- package/src/templates/standalone-root/.changeset/initial.md.hbs +9 -0
- package/src/templates/standalone-root/README.md.hbs +75 -0
- package/src/templates/standalone-root/eslint.config.mjs.hbs +37 -0
- package/src/templates/standalone-root/package.json.hbs +27 -0
- package/src/templates/standalone-root/tsconfig.json.hbs +13 -0
- package/src/templates.test.ts +20 -0
- package/src/tui/components.test.tsx +28 -0
- package/src/tui/components.tsx +159 -0
- package/src/tui/index.ts +31 -0
- package/src/tui/theme.test.ts +54 -0
- package/src/tui/theme.ts +60 -0
- 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
|
+
}
|
package/src/tui/index.ts
ADDED
|
@@ -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
|
+
});
|
package/src/tui/theme.ts
ADDED
|
@@ -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
|
+
};
|
package/src/utils/template.ts
CHANGED
|
@@ -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
|
}
|