@supatype/cli 0.1.2 → 0.1.3
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +108 -105
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/commands/admin.d.ts +12 -0
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +97 -23
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +287 -182
- package/dist/commands/init.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -1
- package/dist/config.js.map +1 -1
- package/dist/dev-compose.js +3 -3
- package/dist/dev-compose.js.map +1 -1
- package/dist/dev-log-bus.d.ts +1 -1
- package/dist/dev-log-bus.d.ts.map +1 -1
- package/dist/dev-log-bus.js +5 -3
- package/dist/dev-log-bus.js.map +1 -1
- package/dist/dev-ready-panel.d.ts +2 -0
- package/dist/dev-ready-panel.d.ts.map +1 -1
- package/dist/dev-ready-panel.js +18 -0
- package/dist/dev-ready-panel.js.map +1 -1
- package/dist/init-project-detect.d.ts +14 -0
- package/dist/init-project-detect.d.ts.map +1 -0
- package/dist/init-project-detect.js +89 -0
- package/dist/init-project-detect.js.map +1 -0
- package/dist/ui/dev/DevDashboard.d.ts.map +1 -1
- package/dist/ui/dev/DevDashboard.js +11 -4
- package/dist/ui/dev/DevDashboard.js.map +1 -1
- package/dist/ui/dev/DevReadyPanelView.d.ts +2 -1
- package/dist/ui/dev/DevReadyPanelView.d.ts.map +1 -1
- package/dist/ui/dev/DevReadyPanelView.js +21 -2
- package/dist/ui/dev/DevReadyPanelView.js.map +1 -1
- package/dist/ui/flows/FlowApp.d.ts.map +1 -1
- package/dist/ui/flows/FlowApp.js +3 -1
- package/dist/ui/flows/FlowApp.js.map +1 -1
- package/dist/ui/flows/prompt-fields.d.ts.map +1 -1
- package/dist/ui/flows/prompt-fields.js +9 -5
- package/dist/ui/flows/prompt-fields.js.map +1 -1
- package/dist/ui/runtime/run-clack-flow.d.ts.map +1 -1
- package/dist/ui/runtime/run-clack-flow.js +15 -3
- package/dist/ui/runtime/run-clack-flow.js.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.d.ts +3 -0
- package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -0
- package/dist/ui/runtime/stdin-after-ink.js +16 -0
- package/dist/ui/runtime/stdin-after-ink.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/admin.ts +135 -26
- package/src/commands/init.ts +360 -213
- package/src/config.ts +13 -1
- package/src/dev-compose.ts +4 -4
- package/src/dev-log-bus.ts +5 -3
- package/src/dev-ready-panel.ts +17 -0
- package/src/init-project-detect.ts +99 -0
- package/src/ui/dev/DevDashboard.tsx +14 -4
- package/src/ui/dev/DevReadyPanelView.tsx +76 -9
- package/src/ui/flows/FlowApp.tsx +3 -1
- package/src/ui/flows/prompt-fields.tsx +13 -7
- package/src/ui/runtime/run-clack-flow.tsx +16 -3
- package/src/ui/runtime/stdin-after-ink.ts +18 -0
- package/tests/admin-ensure.test.ts +80 -0
- package/tests/config.test.ts +19 -0
- package/tests/dev-ready-panel.test.ts +18 -1
- package/tests/init-project-detect.test.ts +53 -0
- package/tests/stdin-after-ink.test.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/config.ts
CHANGED
|
@@ -158,7 +158,7 @@ process.stdout.write(JSON.stringify(config))
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
const failure = result.stderr || result.stdout
|
|
161
|
-
if (!failure
|
|
161
|
+
if (!shouldStripCliImportOnLoadFailure(failure)) {
|
|
162
162
|
throw new Error(`Failed to load ${candidate}:\n${failure}`)
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -169,6 +169,18 @@ process.stdout.write(JSON.stringify(config))
|
|
|
169
169
|
return null
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/** When @supatype/cli is not installed yet (e.g. during `supatype init`), strip its import. */
|
|
173
|
+
function shouldStripCliImportOnLoadFailure(failure: string): boolean {
|
|
174
|
+
if (failure.includes("ERR_PACKAGE_PATH_NOT_EXPORTED")) return true
|
|
175
|
+
if (!failure.includes("@supatype/cli")) return false
|
|
176
|
+
return (
|
|
177
|
+
failure.includes("ERR_MODULE_NOT_FOUND") ||
|
|
178
|
+
failure.includes("MODULE_NOT_FOUND") ||
|
|
179
|
+
failure.includes("Cannot find module '@supatype/cli'") ||
|
|
180
|
+
failure.includes('Cannot find package \'@supatype/cli\'')
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
172
184
|
function loadTsConfigWithoutCliImport(
|
|
173
185
|
configPath: string,
|
|
174
186
|
cwd: string,
|
package/src/dev-compose.ts
CHANGED
|
@@ -768,10 +768,6 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
768
768
|
console.error("[supatype] Initial schema push failed:", (e as Error).message),
|
|
769
769
|
)
|
|
770
770
|
|
|
771
|
-
await ensureFirstAdminUserForProject(cwd, config, {
|
|
772
|
-
compose: { project, composePath: paths.composePath },
|
|
773
|
-
})
|
|
774
|
-
|
|
775
771
|
if (localServerImage !== undefined) {
|
|
776
772
|
console.log("[supatype] Recreating server with local image...")
|
|
777
773
|
const recreateStatus = runDockerCompose(
|
|
@@ -792,6 +788,10 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
792
788
|
console.log("[supatype] Waiting for storage API...")
|
|
793
789
|
await waitStorageApiReady(kongPort, serviceRoleKey, 90)
|
|
794
790
|
|
|
791
|
+
await ensureFirstAdminUserForProject(cwd, config, {
|
|
792
|
+
compose: { project, composePath: paths.composePath },
|
|
793
|
+
})
|
|
794
|
+
|
|
795
795
|
writeLocalEnvironment(cwd, {
|
|
796
796
|
target: "local",
|
|
797
797
|
apiUrl: `http://localhost:${kongPort}`,
|
package/src/dev-log-bus.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { DevReadyPanel } from "./dev-ready-panel.js"
|
|
6
|
-
import { devReadyPanelRowCount } from "./dev-ready-panel.js"
|
|
6
|
+
import { devReadyPanelCompactRowCount, devReadyPanelRowCount } from "./dev-ready-panel.js"
|
|
7
7
|
|
|
8
8
|
export type DevLogLevel = "log" | "warn" | "error"
|
|
9
9
|
|
|
@@ -79,9 +79,11 @@ export class DevLogBus {
|
|
|
79
79
|
return this.readyPanel
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
readyPanelRowCount(): number {
|
|
82
|
+
readyPanelRowCount(compact = false): number {
|
|
83
83
|
if (!this.readyPanel) return 0
|
|
84
|
-
return
|
|
84
|
+
return compact
|
|
85
|
+
? devReadyPanelCompactRowCount(this.readyPanel)
|
|
86
|
+
: devReadyPanelRowCount(this.readyPanel)
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
append(taskId: string, line: string, level: DevLogLevel = "log"): void {
|
package/src/dev-ready-panel.ts
CHANGED
|
@@ -34,6 +34,23 @@ export function devReadyPanelRowCount(panel: DevReadyPanel): number {
|
|
|
34
34
|
return rows
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** Shorter panel when the terminal is too small for every service row. */
|
|
38
|
+
export function devReadyPanelCompactRowCount(panel: DevReadyPanel): number {
|
|
39
|
+
let rows = 2 // border
|
|
40
|
+
rows += 1 // title
|
|
41
|
+
rows += 2 // gateway + studio summary
|
|
42
|
+
for (const hint of panel.hints ?? []) {
|
|
43
|
+
if (hint.trim()) rows += 1
|
|
44
|
+
}
|
|
45
|
+
if (panel.anonKey) {
|
|
46
|
+
rows += 1
|
|
47
|
+
rows += 1
|
|
48
|
+
if (panel.serviceRoleKey) rows += 1
|
|
49
|
+
}
|
|
50
|
+
rows += 1 // marginBottom
|
|
51
|
+
return rows
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
function formatStreamBlock(panel: DevReadyPanel): string {
|
|
38
55
|
const lines = [`[supatype] ${panel.title}`]
|
|
39
56
|
for (const link of panel.links) {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
const VITE_CONFIG_NAMES = [
|
|
5
|
+
"vite.config.ts",
|
|
6
|
+
"vite.config.js",
|
|
7
|
+
"vite.config.mjs",
|
|
8
|
+
"vite.config.cjs",
|
|
9
|
+
] as const
|
|
10
|
+
|
|
11
|
+
const SUPATYPE_CONFIG_NAMES = [
|
|
12
|
+
"supatype.config.ts",
|
|
13
|
+
"supatype.config.js",
|
|
14
|
+
"supatype.config.mjs",
|
|
15
|
+
] as const
|
|
16
|
+
|
|
17
|
+
export interface DetectedProjectSetup {
|
|
18
|
+
/** Directory has files other than `.git`. */
|
|
19
|
+
hasExistingFiles: boolean
|
|
20
|
+
hasSupatypeConfig: boolean
|
|
21
|
+
hasVite: boolean
|
|
22
|
+
hasViteConfig: boolean
|
|
23
|
+
viteDevUrl: string
|
|
24
|
+
staticDir: string
|
|
25
|
+
/** Human-readable bullets for the init wizard. */
|
|
26
|
+
summaryLines: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasPackageVite(cwd: string): boolean {
|
|
30
|
+
const pkgPath = join(cwd, "package.json")
|
|
31
|
+
if (!existsSync(pkgPath)) return false
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as {
|
|
34
|
+
dependencies?: Record<string, string>
|
|
35
|
+
devDependencies?: Record<string, string>
|
|
36
|
+
}
|
|
37
|
+
return Boolean(pkg.devDependencies?.["vite"] ?? pkg.dependencies?.["vite"])
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readVitePort(cwd: string): number {
|
|
44
|
+
for (const name of VITE_CONFIG_NAMES) {
|
|
45
|
+
const path = join(cwd, name)
|
|
46
|
+
if (!existsSync(path)) continue
|
|
47
|
+
const src = readFileSync(path, "utf8")
|
|
48
|
+
const portMatch = src.match(/port:\s*(\d+)/)
|
|
49
|
+
if (portMatch?.[1]) {
|
|
50
|
+
const port = Number.parseInt(portMatch[1], 10)
|
|
51
|
+
if (Number.isInteger(port) && port > 0) return port
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return 5173
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function detectStaticDir(cwd: string): string {
|
|
58
|
+
if (existsSync(join(cwd, "dist", "index.html"))) return "./dist"
|
|
59
|
+
if (existsSync(join(cwd, "public", "index.html"))) return "./public"
|
|
60
|
+
if (existsSync(join(cwd, "dist"))) return "./dist"
|
|
61
|
+
if (existsSync(join(cwd, "public"))) return "./public"
|
|
62
|
+
return "./public"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Inspect `cwd` before init prompts — Vite, static dir, existing Supatype config. */
|
|
66
|
+
export function detectProjectSetup(cwd: string): DetectedProjectSetup {
|
|
67
|
+
let entries: string[] = []
|
|
68
|
+
try {
|
|
69
|
+
entries = readdirSync(cwd).filter((entry) => entry !== ".git")
|
|
70
|
+
} catch {
|
|
71
|
+
entries = []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const hasExistingFiles = entries.length > 0
|
|
75
|
+
const hasViteConfig = VITE_CONFIG_NAMES.some((name) => existsSync(join(cwd, name)))
|
|
76
|
+
const hasVite = hasViteConfig || hasPackageVite(cwd)
|
|
77
|
+
const viteDevUrl = `http://127.0.0.1:${readVitePort(cwd)}`
|
|
78
|
+
const staticDir = detectStaticDir(cwd)
|
|
79
|
+
const hasSupatypeConfig = SUPATYPE_CONFIG_NAMES.some((name) => existsSync(join(cwd, name)))
|
|
80
|
+
|
|
81
|
+
const summaryLines: string[] = []
|
|
82
|
+
if (hasSupatypeConfig) summaryLines.push("supatype.config already present")
|
|
83
|
+
if (hasViteConfig) summaryLines.push(`Vite config found (${viteDevUrl})`)
|
|
84
|
+
else if (hasVite) summaryLines.push(`Vite dependency in package.json (${viteDevUrl})`)
|
|
85
|
+
if (existsSync(join(cwd, staticDir.replace(/^\.\//, "")))) {
|
|
86
|
+
summaryLines.push(`static assets in ${staticDir}`)
|
|
87
|
+
}
|
|
88
|
+
if (existsSync(join(cwd, "package.json"))) summaryLines.push("package.json present")
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
hasExistingFiles,
|
|
92
|
+
hasSupatypeConfig,
|
|
93
|
+
hasVite,
|
|
94
|
+
hasViteConfig,
|
|
95
|
+
viteDevUrl,
|
|
96
|
+
staticDir,
|
|
97
|
+
summaryLines,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -22,6 +22,9 @@ const TASK_COL_WIDTH = 22
|
|
|
22
22
|
const MIN_WIDTH = 60
|
|
23
23
|
const MIN_HEIGHT = 14
|
|
24
24
|
const TAGLINE = "local development"
|
|
25
|
+
const CHROME_ABOVE_LOG = 2 // separator + keybindings
|
|
26
|
+
const CHROME_BELOW_LOG = 2 // footer separator + focused line
|
|
27
|
+
const MIN_LOG_ROWS = 3
|
|
25
28
|
|
|
26
29
|
function truncate(text: string, width: number): string {
|
|
27
30
|
if (width <= 0) return ""
|
|
@@ -97,10 +100,15 @@ export function DevDashboard({ bus }: DevDashboardProps): React.ReactElement {
|
|
|
97
100
|
const focusedId = bus.getFocusedTaskId()
|
|
98
101
|
const taskIds = bus.getTaskOrder()
|
|
99
102
|
const readyPanel = bus.getReadyPanel()
|
|
100
|
-
const readyRows = readyPanel ? bus.readyPanelRowCount() : 0
|
|
101
103
|
const logoRows = logoRowCount() + 1 // figlet + tagline
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
+
const promptRows = promptOpen ? 6 : 0
|
|
105
|
+
const fullReadyRows = readyPanel ? bus.readyPanelRowCount(false) : 0
|
|
106
|
+
const availableForPanel =
|
|
107
|
+
rows - logoRows - CHROME_ABOVE_LOG - CHROME_BELOW_LOG - MIN_LOG_ROWS - promptRows
|
|
108
|
+
const useCompactPanel = readyPanel !== null && fullReadyRows > availableForPanel
|
|
109
|
+
const readyRows = readyPanel ? bus.readyPanelRowCount(useCompactPanel) : 0
|
|
110
|
+
const chromeRows = logoRows + readyRows + CHROME_ABOVE_LOG + CHROME_BELOW_LOG + promptRows
|
|
111
|
+
const logHeight = Math.max(0, rows - chromeRows)
|
|
104
112
|
const logWidth = cols - TASK_COL_WIDTH - 1
|
|
105
113
|
|
|
106
114
|
useInput((input, key) => {
|
|
@@ -148,7 +156,9 @@ export function DevDashboard({ bus }: DevDashboardProps): React.ReactElement {
|
|
|
148
156
|
return (
|
|
149
157
|
<Box flexDirection="column" width={cols} height={rows}>
|
|
150
158
|
<LogoWordmark maxWidth={cols} tagline={TAGLINE} />
|
|
151
|
-
{readyPanel ?
|
|
159
|
+
{readyPanel ? (
|
|
160
|
+
<DevReadyPanelView panel={readyPanel} width={cols} compact={useCompactPanel} />
|
|
161
|
+
) : null}
|
|
152
162
|
<Text dimColor>{"─".repeat(cols)}</Text>
|
|
153
163
|
<Text dimColor>
|
|
154
164
|
{truncate(" ↑/k ↓/j task u/d scroll g/G top/bottom Ctrl+C quit", cols)}
|
|
@@ -21,12 +21,70 @@ function panelInnerWidth(outerWidth: number): number {
|
|
|
21
21
|
return Math.max(20, outerWidth - 4)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function ServiceLinkRow({
|
|
25
|
+
label,
|
|
26
|
+
url,
|
|
27
|
+
innerWidth,
|
|
28
|
+
labelWidth,
|
|
29
|
+
}: {
|
|
30
|
+
label: string
|
|
31
|
+
url: string
|
|
32
|
+
innerWidth: number
|
|
33
|
+
labelWidth: number
|
|
34
|
+
}): React.ReactElement {
|
|
35
|
+
return (
|
|
36
|
+
<Text wrap="truncate">
|
|
37
|
+
<Text dimColor>{truncate(label.padEnd(labelWidth), labelWidth)}</Text>
|
|
38
|
+
<Text color={theme.info}>{truncate(url, Math.max(8, innerWidth - labelWidth))}</Text>
|
|
39
|
+
</Text>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function CompactLinks({ panel, innerWidth }: { panel: DevReadyPanel; innerWidth: number }): React.ReactElement {
|
|
44
|
+
const gateway = panel.links.find((link) => link.label === "API")?.url ?? panel.links[0]?.url ?? ""
|
|
45
|
+
const studio =
|
|
46
|
+
panel.links.find((link) => link.label === "Studio")?.url ??
|
|
47
|
+
panel.links[panel.links.length - 1]?.url ??
|
|
48
|
+
""
|
|
49
|
+
const otherPaths = panel.links
|
|
50
|
+
.filter((link) => link.label !== "API" && link.label !== "Studio")
|
|
51
|
+
.map((link) => {
|
|
52
|
+
const prefix = `${link.url.split("://")[0] ?? "http"}://`
|
|
53
|
+
const path = link.url.slice(link.url.indexOf("://") + 3)
|
|
54
|
+
const slash = path.indexOf("/")
|
|
55
|
+
return slash >= 0 ? path.slice(slash) : link.url
|
|
56
|
+
})
|
|
57
|
+
.join(", ")
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<Text wrap="truncate">
|
|
62
|
+
<Text dimColor>{"Gateway".padEnd(14)}</Text>
|
|
63
|
+
<Text color={theme.info}>{truncate(gateway, innerWidth - 14)}</Text>
|
|
64
|
+
</Text>
|
|
65
|
+
{otherPaths ? (
|
|
66
|
+
<Text dimColor wrap="truncate">
|
|
67
|
+
{truncate(`Also: ${otherPaths}`, innerWidth)}
|
|
68
|
+
</Text>
|
|
69
|
+
) : null}
|
|
70
|
+
{studio ? (
|
|
71
|
+
<Text wrap="truncate">
|
|
72
|
+
<Text dimColor>{"Studio".padEnd(14)}</Text>
|
|
73
|
+
<Text color={theme.info}>{truncate(studio, innerWidth - 14)}</Text>
|
|
74
|
+
</Text>
|
|
75
|
+
) : null}
|
|
76
|
+
</>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
24
80
|
export function DevReadyPanelView({
|
|
25
81
|
panel,
|
|
26
82
|
width,
|
|
83
|
+
compact = false,
|
|
27
84
|
}: {
|
|
28
85
|
panel: DevReadyPanel
|
|
29
86
|
width: number
|
|
87
|
+
compact?: boolean
|
|
30
88
|
}): React.ReactElement {
|
|
31
89
|
const innerWidth = panelInnerWidth(width)
|
|
32
90
|
const labelWidth = 14
|
|
@@ -34,6 +92,8 @@ export function DevReadyPanelView({
|
|
|
34
92
|
return (
|
|
35
93
|
<Box
|
|
36
94
|
flexDirection="column"
|
|
95
|
+
flexShrink={0}
|
|
96
|
+
width={width}
|
|
37
97
|
marginBottom={1}
|
|
38
98
|
borderStyle="round"
|
|
39
99
|
borderColor={theme.brand}
|
|
@@ -42,23 +102,30 @@ export function DevReadyPanelView({
|
|
|
42
102
|
<Text bold color={theme.brand}>
|
|
43
103
|
{truncate(panel.title, innerWidth)}
|
|
44
104
|
</Text>
|
|
45
|
-
{
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
105
|
+
{compact ? (
|
|
106
|
+
<CompactLinks panel={panel} innerWidth={innerWidth} />
|
|
107
|
+
) : (
|
|
108
|
+
panel.links.map((link) => (
|
|
109
|
+
<ServiceLinkRow
|
|
110
|
+
key={`${link.label}-${link.url}`}
|
|
111
|
+
label={link.label}
|
|
112
|
+
url={link.url}
|
|
113
|
+
innerWidth={innerWidth}
|
|
114
|
+
labelWidth={labelWidth}
|
|
115
|
+
/>
|
|
116
|
+
))
|
|
117
|
+
)}
|
|
51
118
|
{panel.hints?.map((hint) => (
|
|
52
|
-
<Text key={hint} dimColor>
|
|
119
|
+
<Text key={hint} dimColor wrap="truncate">
|
|
53
120
|
{truncate(hint, innerWidth)}
|
|
54
121
|
</Text>
|
|
55
122
|
))}
|
|
56
123
|
{panel.anonKey ? (
|
|
57
124
|
<>
|
|
58
125
|
<Text dimColor>API keys (local dev)</Text>
|
|
59
|
-
<Text dimColor>{`anon ${shortenKey(panel.anonKey, innerWidth - 5)}`}</Text>
|
|
126
|
+
<Text dimColor wrap="truncate">{`anon ${shortenKey(panel.anonKey, innerWidth - 5)}`}</Text>
|
|
60
127
|
{panel.serviceRoleKey ? (
|
|
61
|
-
<Text dimColor>{`svc ${shortenKey(panel.serviceRoleKey, innerWidth - 5)}`}</Text>
|
|
128
|
+
<Text dimColor wrap="truncate">{`svc ${shortenKey(panel.serviceRoleKey, innerWidth - 5)}`}</Text>
|
|
62
129
|
) : null}
|
|
63
130
|
</>
|
|
64
131
|
) : null}
|
package/src/ui/flows/FlowApp.tsx
CHANGED
|
@@ -36,6 +36,8 @@ export function FlowApp({ bind }: FlowAppProps): React.ReactElement {
|
|
|
36
36
|
},
|
|
37
37
|
waitForPrompt<T>(spec: ActiveFlowPrompt): Promise<T> {
|
|
38
38
|
return new Promise<T>((resolve) => {
|
|
39
|
+
setLogLines([])
|
|
40
|
+
setSpinner(null)
|
|
39
41
|
setPrompt(spec)
|
|
40
42
|
setPromptResolver(() => (value: unknown) => {
|
|
41
43
|
setPrompt(null)
|
|
@@ -51,7 +53,7 @@ export function FlowApp({ bind }: FlowAppProps): React.ReactElement {
|
|
|
51
53
|
return (
|
|
52
54
|
<Box flexDirection="column" paddingX={1}>
|
|
53
55
|
<FlowLogoHeader />
|
|
54
|
-
<FlowLogPane lines={logLines} />
|
|
56
|
+
{prompt ? null : <FlowLogPane lines={logLines} />}
|
|
55
57
|
{spinner ? <FlowSpinnerLine message={spinner} /> : null}
|
|
56
58
|
{prompt && promptResolver ? (
|
|
57
59
|
<PromptPanel>
|
|
@@ -46,10 +46,16 @@ export function ConfirmPrompt({ spec, onSubmit }: ConfirmPromptProps): React.Rea
|
|
|
46
46
|
const [value, setValue] = useState(spec.initialValue)
|
|
47
47
|
|
|
48
48
|
useInput((input, key) => {
|
|
49
|
+
if (input === "y" || input === "Y") {
|
|
50
|
+
onSubmit(true)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (input === "n" || input === "N") {
|
|
54
|
+
onSubmit(false)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
49
57
|
if (key.leftArrow || input === "h") setValue(false)
|
|
50
58
|
if (key.rightArrow || input === "l") setValue(true)
|
|
51
|
-
if (input === "y" || input === "Y") setValue(true)
|
|
52
|
-
if (input === "n" || input === "N") setValue(false)
|
|
53
59
|
if (key.return) onSubmit(value)
|
|
54
60
|
})
|
|
55
61
|
|
|
@@ -60,19 +66,19 @@ export function ConfirmPrompt({ spec, onSubmit }: ConfirmPromptProps): React.Rea
|
|
|
60
66
|
</Text>
|
|
61
67
|
<Text>
|
|
62
68
|
{value ? (
|
|
63
|
-
<Text color={theme.
|
|
69
|
+
<Text color={theme.brand} bold>
|
|
64
70
|
Yes
|
|
65
71
|
</Text>
|
|
66
72
|
) : (
|
|
67
|
-
<Text>Yes</Text>
|
|
73
|
+
<Text dimColor>Yes</Text>
|
|
68
74
|
)}
|
|
69
|
-
<Text> / </Text>
|
|
75
|
+
<Text dimColor> / </Text>
|
|
70
76
|
{!value ? (
|
|
71
|
-
<Text color={theme.
|
|
77
|
+
<Text color={theme.brand} bold>
|
|
72
78
|
No
|
|
73
79
|
</Text>
|
|
74
80
|
) : (
|
|
75
|
-
<Text>No</Text>
|
|
81
|
+
<Text dimColor>No</Text>
|
|
76
82
|
)}
|
|
77
83
|
<Text dimColor> — y/n, ←/→, enter</Text>
|
|
78
84
|
</Text>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React, { useRef } from "react"
|
|
2
|
-
import { render } from "ink"
|
|
2
|
+
import { render, type Instance } from "ink"
|
|
3
3
|
import { isInteractive } from "../interactive.js"
|
|
4
4
|
import { createClackApi, type ClackApi } from "../clack-api.js"
|
|
5
5
|
import { FlowApp } from "../flows/FlowApp.js"
|
|
6
|
+
import { restoreStdinAfterInk } from "./stdin-after-ink.js"
|
|
6
7
|
|
|
7
8
|
interface RunClackFlowRootProps {
|
|
8
9
|
run: (api: ClackApi) => Promise<unknown>
|
|
@@ -37,16 +38,28 @@ export async function runClackFlow<T>(run: (api: ClackApi) => Promise<T>): Promi
|
|
|
37
38
|
<RunClackFlowRoot
|
|
38
39
|
run={run as (api: ClackApi) => Promise<unknown>}
|
|
39
40
|
onComplete={(value) => {
|
|
40
|
-
instance
|
|
41
|
+
teardownInk(instance)
|
|
41
42
|
resolve(value as T)
|
|
42
43
|
}}
|
|
43
44
|
onError={(err) => {
|
|
44
|
-
instance
|
|
45
|
+
teardownInk(instance)
|
|
45
46
|
reject(err)
|
|
46
47
|
}}
|
|
47
48
|
/>,
|
|
49
|
+
{ patchConsole: false },
|
|
48
50
|
)
|
|
49
51
|
})
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function teardownInk(instance: Instance): void {
|
|
55
|
+
try {
|
|
56
|
+
instance.clear()
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore — terminal may already be reset
|
|
59
|
+
}
|
|
60
|
+
instance.unmount()
|
|
61
|
+
instance.cleanup()
|
|
62
|
+
restoreStdinAfterInk()
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
export { CLACK_CANCEL } from "./cancel.js"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Restore stdin after Ink unmount so one-shot commands (init, link) can exit cleanly. */
|
|
2
|
+
export function restoreStdinAfterInk(): void {
|
|
3
|
+
if (!process.stdin.isTTY) return
|
|
4
|
+
|
|
5
|
+
const stdin = process.stdin as NodeJS.ReadStream & {
|
|
6
|
+
isRaw?: boolean
|
|
7
|
+
setRawMode?(mode: boolean): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
if (stdin.isRaw) stdin.setRawMode?.(false)
|
|
12
|
+
} catch {
|
|
13
|
+
// ignore — stdin may already be restored
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
stdin.resume()
|
|
17
|
+
process.stdout.write("\n")
|
|
18
|
+
}
|
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
ADMIN_EMAIL_ENV,
|
|
4
4
|
ADMIN_PASSWORD_ENV,
|
|
5
5
|
clearAdminSeedPassword,
|
|
6
|
+
composePostgresPassword,
|
|
7
|
+
GOTRUE_NIL_INSTANCE_ID,
|
|
8
|
+
gotrueJwtAud,
|
|
6
9
|
hashPasswordForAuth,
|
|
10
|
+
resolveAuthConfirmedAtColumn,
|
|
7
11
|
} from "../src/commands/admin.js"
|
|
8
12
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
9
13
|
import { join } from "node:path"
|
|
@@ -38,6 +42,82 @@ describe("clearAdminSeedPassword", () => {
|
|
|
38
42
|
})
|
|
39
43
|
})
|
|
40
44
|
|
|
45
|
+
describe("composePostgresPassword", () => {
|
|
46
|
+
it("reads POSTGRES_PASSWORD from .env", () => {
|
|
47
|
+
const dir = join(tmpdir(), `supatype-admin-pgpass-${Date.now()}`)
|
|
48
|
+
mkdirSync(dir, { recursive: true })
|
|
49
|
+
writeFileSync(join(dir, ".env"), "POSTGRES_PASSWORD=secret\n", "utf8")
|
|
50
|
+
try {
|
|
51
|
+
expect(composePostgresPassword(dir)).toBe("secret")
|
|
52
|
+
} finally {
|
|
53
|
+
rmSync(dir, { recursive: true, force: true })
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("defaults to postgres when unset", () => {
|
|
58
|
+
const dir = join(tmpdir(), `supatype-admin-pgpass-default-${Date.now()}`)
|
|
59
|
+
mkdirSync(dir, { recursive: true })
|
|
60
|
+
try {
|
|
61
|
+
expect(composePostgresPassword(dir)).toBe("postgres")
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(dir, { recursive: true, force: true })
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe("gotrueJwtAud", () => {
|
|
69
|
+
it("reads GOTRUE_JWT_AUD from .env", () => {
|
|
70
|
+
const dir = join(tmpdir(), `supatype-admin-aud-${Date.now()}`)
|
|
71
|
+
mkdirSync(dir, { recursive: true })
|
|
72
|
+
writeFileSync(join(dir, ".env"), "GOTRUE_JWT_AUD=custom-aud\n", "utf8")
|
|
73
|
+
try {
|
|
74
|
+
expect(gotrueJwtAud(dir)).toBe("custom-aud")
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(dir, { recursive: true, force: true })
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("defaults to authenticated", () => {
|
|
81
|
+
const dir = join(tmpdir(), `supatype-admin-aud-default-${Date.now()}`)
|
|
82
|
+
mkdirSync(dir, { recursive: true })
|
|
83
|
+
try {
|
|
84
|
+
expect(gotrueJwtAud(dir)).toBe("authenticated")
|
|
85
|
+
} finally {
|
|
86
|
+
rmSync(dir, { recursive: true, force: true })
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe("GOTRUE_NIL_INSTANCE_ID", () => {
|
|
92
|
+
it("is the nil UUID GoTrue uses for lookup", () => {
|
|
93
|
+
expect(GOTRUE_NIL_INSTANCE_ID).toBe("00000000-0000-0000-0000-000000000000")
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe("resolveAuthConfirmedAtColumn", () => {
|
|
98
|
+
it("prefers email_confirmed_at when both columns exist", async () => {
|
|
99
|
+
const column = await resolveAuthConfirmedAtColumn(async () => ({
|
|
100
|
+
rows: [{ column_name: "email_confirmed_at" }],
|
|
101
|
+
rowCount: 1,
|
|
102
|
+
command: "SELECT",
|
|
103
|
+
oid: 0,
|
|
104
|
+
fields: [],
|
|
105
|
+
}))
|
|
106
|
+
expect(column).toBe("email_confirmed_at")
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("falls back to confirmed_at for postgres init schema", async () => {
|
|
110
|
+
const column = await resolveAuthConfirmedAtColumn(async () => ({
|
|
111
|
+
rows: [{ value: "confirmed_at" }],
|
|
112
|
+
rowCount: 1,
|
|
113
|
+
command: "SELECT",
|
|
114
|
+
oid: 0,
|
|
115
|
+
fields: [],
|
|
116
|
+
}))
|
|
117
|
+
expect(column).toBe("confirmed_at")
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
41
121
|
describe("init admin seed in .env", () => {
|
|
42
122
|
it("writes SUPATYPE_ADMIN_* when scaffold options include credentials", () => {
|
|
43
123
|
const dir = join(tmpdir(), `supatype-init-admin-${Date.now()}`)
|
package/tests/config.test.ts
CHANGED
|
@@ -179,6 +179,25 @@ describe("loadConfig()", () => {
|
|
|
179
179
|
expect(cfg.versions!.engine).toBe("0.4.2")
|
|
180
180
|
expect(cfg.schema?.path).toBe("./a.ts")
|
|
181
181
|
})
|
|
182
|
+
|
|
183
|
+
it("loads defineConfig import before @supatype/cli is installed (init scaffold)", () => {
|
|
184
|
+
writeFileSync(
|
|
185
|
+
join(tmpDir, "supatype.config.ts"),
|
|
186
|
+
`import { defineConfig } from "@supatype/cli"
|
|
187
|
+
|
|
188
|
+
export default defineConfig({
|
|
189
|
+
project: { name: "fresh" },
|
|
190
|
+
database: { provider: "docker" },
|
|
191
|
+
server: { mode: "dev" },
|
|
192
|
+
app: { mode: "none" },
|
|
193
|
+
schema: { path: "./schema/index.ts" },
|
|
194
|
+
})
|
|
195
|
+
`,
|
|
196
|
+
)
|
|
197
|
+
const cfg = loadConfig(tmpDir)
|
|
198
|
+
expect(cfg.project?.name).toBe("fresh")
|
|
199
|
+
expect(cfg.schema?.path).toBe("./schema/index.ts")
|
|
200
|
+
})
|
|
182
201
|
})
|
|
183
202
|
|
|
184
203
|
describe("resolveRuntimeProvider()", () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { devReadyPanelRowCount } from "../src/dev-ready-panel.js"
|
|
2
|
+
import { devReadyPanelCompactRowCount, devReadyPanelRowCount } from "../src/dev-ready-panel.js"
|
|
3
3
|
|
|
4
4
|
describe("devReadyPanelRowCount", () => {
|
|
5
5
|
it("accounts for border, keys block, and hints", () => {
|
|
@@ -18,3 +18,20 @@ describe("devReadyPanelRowCount", () => {
|
|
|
18
18
|
expect(rows).toBe(10)
|
|
19
19
|
})
|
|
20
20
|
})
|
|
21
|
+
|
|
22
|
+
describe("devReadyPanelCompactRowCount", () => {
|
|
23
|
+
it("uses a shorter layout than the full panel", () => {
|
|
24
|
+
const panel = {
|
|
25
|
+
title: "Services running",
|
|
26
|
+
links: [
|
|
27
|
+
{ label: "API", url: "http://localhost:1" },
|
|
28
|
+
{ label: "REST", url: "http://localhost:1/rest/v1/" },
|
|
29
|
+
{ label: "Studio", url: "http://localhost:1/studio/" },
|
|
30
|
+
],
|
|
31
|
+
hints: ["Demo data: pnpm seed"],
|
|
32
|
+
anonKey: "anon-jwt",
|
|
33
|
+
serviceRoleKey: "svc-jwt",
|
|
34
|
+
}
|
|
35
|
+
expect(devReadyPanelCompactRowCount(panel)).toBeLessThan(devReadyPanelRowCount(panel))
|
|
36
|
+
})
|
|
37
|
+
})
|