@frontera-sdk/cli 1.50.24 → 1.50.25
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 +4 -4
- package/src/commands/app/init.ts +7 -4
- package/src/output.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.25",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/claude-agent-sdk": "^0.3.251",
|
|
42
|
-
"@frontera-sdk/functions": "1.50.
|
|
43
|
-
"@frontera-sdk/core": "1.50.
|
|
42
|
+
"@frontera-sdk/functions": "1.50.25",
|
|
43
|
+
"@frontera-sdk/core": "1.50.25",
|
|
44
44
|
"ai": "^6.0.116",
|
|
45
45
|
"gray-matter": "^4.0.3",
|
|
46
46
|
"yaml": "^2.9.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@frontera-sdk/forge-contracts": "1.50.
|
|
49
|
+
"@frontera-sdk/forge-contracts": "1.50.25",
|
|
50
50
|
"@types/bun": "^1.3.14",
|
|
51
51
|
"typescript": "^5.9.3"
|
|
52
52
|
}
|
package/src/commands/app/init.ts
CHANGED
|
@@ -238,8 +238,9 @@ export const appInit: Command = {
|
|
|
238
238
|
|
|
239
239
|
let installed = false
|
|
240
240
|
if (!flagBool(ctx, 'no-install')) {
|
|
241
|
-
ctx.output.
|
|
242
|
-
|
|
241
|
+
const result = await ctx.output.task('Installing dependencies with bun', () =>
|
|
242
|
+
installDependencies(target),
|
|
243
|
+
)
|
|
243
244
|
installed = result.installed
|
|
244
245
|
ctx.output.note(
|
|
245
246
|
result.installed
|
|
@@ -255,8 +256,10 @@ export const appInit: Command = {
|
|
|
255
256
|
// command that finishes the job instead of being swallowed.
|
|
256
257
|
let components = false
|
|
257
258
|
if (framework === 'next' && !flagBool(ctx, 'no-components')) {
|
|
258
|
-
ctx.output.
|
|
259
|
-
|
|
259
|
+
const result = await ctx.output.task(
|
|
260
|
+
`Adding shadcn components: ${BASELINE_COMPONENTS.join(', ')}`,
|
|
261
|
+
() => addShadcnComponents(target),
|
|
262
|
+
)
|
|
260
263
|
components = result.added
|
|
261
264
|
ctx.output.note(result.added ? ` ${result.summary}` : ` not added: ${result.summary}`)
|
|
262
265
|
ctx.output.note('')
|
package/src/output.ts
CHANGED
|
@@ -12,10 +12,51 @@ export interface Output {
|
|
|
12
12
|
render(data: unknown, toText: (data: unknown) => string): void
|
|
13
13
|
/** Progress and warnings. Always stderr, so piping stays clean. */
|
|
14
14
|
note(message: string): void
|
|
15
|
+
/**
|
|
16
|
+
* Run something slow and say so while it runs.
|
|
17
|
+
*
|
|
18
|
+
* `note` announces and then goes quiet, which is fine for a step that takes
|
|
19
|
+
* a moment and wrong for one that takes half a minute: installing packages
|
|
20
|
+
* and adding components both sit there with a cursor and no evidence the
|
|
21
|
+
* command is alive. This keeps a line moving for exactly as long as the work
|
|
22
|
+
* takes, then replaces it with the outcome and how long it took.
|
|
23
|
+
*
|
|
24
|
+
* The value and the failure both pass straight through — this is a wrapper
|
|
25
|
+
* around work, not a place where errors are handled.
|
|
26
|
+
*/
|
|
27
|
+
task<T>(label: string, run: () => Promise<T>): Promise<T>
|
|
15
28
|
/** Report a failure and return the exit code the shell should use. */
|
|
16
29
|
fail(err: unknown): ExitCode
|
|
17
30
|
}
|
|
18
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Braille frames: one character wide in every terminal font, so the line does
|
|
34
|
+
* not jump as it spins.
|
|
35
|
+
*/
|
|
36
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
37
|
+
const SPINNER_INTERVAL_MS = 80
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Colour for the progress line, decided against STDERR.
|
|
41
|
+
*
|
|
42
|
+
* `help.ts` asks the same question of stdout, which is right for help text and
|
|
43
|
+
* wrong here: progress is written to stderr, and a run whose stdout is piped
|
|
44
|
+
* into a file still has a terminal on the other stream. Honouring NO_COLOR the
|
|
45
|
+
* same way keeps one rule across the CLI.
|
|
46
|
+
*/
|
|
47
|
+
function colorOnErr(): boolean {
|
|
48
|
+
if (process.env.NO_COLOR !== undefined) return false
|
|
49
|
+
if (process.env.FORCE_COLOR !== undefined) return true
|
|
50
|
+
return process.stderr.isTTY === true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const paintErr = (code: string) => (text: string) =>
|
|
54
|
+
colorOnErr() ? `\u001b[${code}m${text}\u001b[39m` : text
|
|
55
|
+
|
|
56
|
+
const green = paintErr('32')
|
|
57
|
+
const red = paintErr('31')
|
|
58
|
+
const grey = paintErr('90')
|
|
59
|
+
|
|
19
60
|
const defaultStreams: Streams = {
|
|
20
61
|
out: (chunk) => process.stdout.write(chunk),
|
|
21
62
|
err: (chunk) => process.stderr.write(chunk),
|
|
@@ -51,6 +92,52 @@ export function createOutput(
|
|
|
51
92
|
streams.err(`${message}\n`)
|
|
52
93
|
},
|
|
53
94
|
|
|
95
|
+
async task(label, run) {
|
|
96
|
+
// Animation only where a human is watching. Piped or redirected, the
|
|
97
|
+
// carriage returns would land in the file as one unreadable line, and in
|
|
98
|
+
// CI logs as thousands of them — so those get the label once and the
|
|
99
|
+
// result once, which is what a log is for.
|
|
100
|
+
const animate = !quiet && streams === defaultStreams && process.stderr.isTTY === true
|
|
101
|
+
if (quiet) return run()
|
|
102
|
+
if (!animate) {
|
|
103
|
+
streams.err(`${label}\n`)
|
|
104
|
+
return run()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const started = Date.now()
|
|
108
|
+
let frame = 0
|
|
109
|
+
const paint = () => {
|
|
110
|
+
// The frame itself stays the terminal's own colour: it is motion, not
|
|
111
|
+
// status. Colour arrives once with the ✓ or ✗, where it means something.
|
|
112
|
+
const spin = SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length]!
|
|
113
|
+
streams.err(`\r\u001b[2K${spin} ${label}`)
|
|
114
|
+
}
|
|
115
|
+
paint()
|
|
116
|
+
const timer = setInterval(paint, SPINNER_INTERVAL_MS)
|
|
117
|
+
|
|
118
|
+
// `unref` so a spinner can never be the reason the process stays alive:
|
|
119
|
+
// a timer is a handle, and one left running outlives the work it was
|
|
120
|
+
// describing.
|
|
121
|
+
timer.unref?.()
|
|
122
|
+
|
|
123
|
+
const stop = (mark: string) => {
|
|
124
|
+
clearInterval(timer)
|
|
125
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1)
|
|
126
|
+
streams.err(`\r\u001b[2K${mark} ${label} ${grey(`[${seconds}s]`)}\n`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const result = await run()
|
|
131
|
+
stop(green('✓'))
|
|
132
|
+
return result
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// The line is closed before the error travels on, or the failure prints
|
|
135
|
+
// over a half-finished spinner.
|
|
136
|
+
stop(red('✗'))
|
|
137
|
+
throw err
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
54
141
|
fail(err) {
|
|
55
142
|
const envelope = toEnvelope(err)
|
|
56
143
|
if (mode === 'json') {
|