Skip to content

Commit 8a07c18

Browse files
authored
Make playground always scaffold new files (#40)
1 parent d46d984 commit 8a07c18

4 files changed

Lines changed: 38 additions & 22 deletions

File tree

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,16 @@ previewsmcp run MyView.swift --platform ios-simulator
4242
# Capture a screenshot
4343
previewsmcp snapshot MyView.swift -o preview.png
4444

45-
# Open a playground — creates a temp file with a starter view and live-reloads on edit
45+
# Scaffold a new playground file and start a live preview
4646
previewsmcp playground
47+
previewsmcp playground MyView.swift # creates MyView.swift with starter code
4748
previewsmcp playground --platform ios-simulator
4849

49-
# Or use an existing file
50-
previewsmcp playground MyView.swift
51-
5250
# Pipe to your editor
5351
vim $(previewsmcp playground)
5452
```
5553

56-
The `playground` command opens a live preview with hot-reload — no project setup needed. Pass an existing file or omit to create a temp one.
54+
The `playground` command scaffolds a new SwiftUI file with a starter view and opens a live preview with hot-reload. Pass a path to create the file there, or omit for a temp file. Use `run` to preview existing files.
5755

5856
## MCP Server
5957

Sources/PreviewsCLI/BuildHelpers.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,24 @@ let defaultPlaygroundCode = """
4848
}
4949
"""
5050

51-
/// Create a temporary playground Swift file, returning its URL.
52-
func createPlaygroundFile(code: String? = nil) throws -> URL {
53-
let playgroundDir = FileManager.default.temporaryDirectory
54-
.appendingPathComponent("previewsmcp-playground", isDirectory: true)
55-
try FileManager.default.createDirectory(at: playgroundDir, withIntermediateDirectories: true)
56-
57-
let shortID = UUID().uuidString.prefix(8)
58-
let fileName = "Playground_\(shortID).swift"
59-
let fileURL = playgroundDir.appendingPathComponent(fileName)
60-
try (code ?? defaultPlaygroundCode).write(to: fileURL, atomically: true, encoding: .utf8)
51+
/// Create a playground Swift file, returning its URL.
52+
/// When `at` is provided, writes to that path (creating parent directories).
53+
/// Otherwise creates a temp file with a unique name.
54+
func createPlaygroundFile(code: String? = nil, at outputPath: URL? = nil) throws -> URL {
55+
let fileURL: URL
56+
if let outputPath {
57+
let dir = outputPath.deletingLastPathComponent()
58+
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
59+
fileURL = outputPath
60+
} else {
61+
let playgroundDir = FileManager.default.temporaryDirectory
62+
.appendingPathComponent("previewsmcp-playground", isDirectory: true)
63+
try FileManager.default.createDirectory(at: playgroundDir, withIntermediateDirectories: true)
64+
let shortID = UUID().uuidString.prefix(8)
65+
fileURL = playgroundDir.appendingPathComponent("Playground_\(shortID).swift")
66+
}
6167

68+
try (code ?? defaultPlaygroundCode).write(to: fileURL, atomically: true, encoding: .utf8)
6269
return fileURL
6370
}
6471

Sources/PreviewsCLI/MCPServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ func configureMCPServer() async throws -> (Server, Compiler) {
220220
Tool(
221221
name: "preview_playground",
222222
description:
223-
"Create a temporary SwiftUI file and start a live preview. Returns session ID and file path for editing. No project setup needed.",
223+
"Create a temporary SwiftUI file and start a live preview for quick experimentation. Returns session ID and file path. For previewing existing files, use preview_start instead.",
224224
inputSchema: .object([
225225
"type": .string("object"),
226226
"properties": .object([

Sources/PreviewsCLI/PlaygroundCommand.swift

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import Foundation
55
struct PlaygroundCommand: ParsableCommand {
66
static let configuration = CommandConfiguration(
77
commandName: "playground",
8-
abstract: "Create a temporary SwiftUI file and start a live preview"
8+
abstract: "Scaffold a new SwiftUI file and start a live preview"
99
)
1010

11-
@Argument(help: "Path to an existing Swift file (creates a temp file if omitted)")
11+
@Argument(help: "Output path for the new file (creates a temp file if omitted)")
1212
var file: String?
1313

1414
@Option(name: .long, help: "Window width")
@@ -26,15 +26,26 @@ struct PlaygroundCommand: ParsableCommand {
2626
mutating func run() throws {
2727
let fileURL: URL
2828
if let file {
29-
fileURL = URL(fileURLWithPath: file).standardizedFileURL
30-
guard FileManager.default.fileExists(atPath: fileURL.path) else {
31-
throw ValidationError("File not found: \(file)")
29+
let url = URL(fileURLWithPath: file).standardizedFileURL
30+
var isDir: ObjCBool = false
31+
if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir) {
32+
if isDir.boolValue {
33+
let example = url.appendingPathComponent("MyView.swift").path
34+
throw ValidationError(
35+
"'\(file)' is a directory. Provide a file path, e.g. '\(example)'")
36+
}
37+
throw ValidationError(
38+
"File already exists: \(file). Use 'previewsmcp run \(file)' to preview it.")
3239
}
40+
fileURL = try createPlaygroundFile(at: url)
3341
} else {
3442
fileURL = try createPlaygroundFile()
3543
}
3644

37-
print(fileURL.path)
45+
// Use fputs to stdout — print() is fully buffered when piped, which
46+
// breaks `vim $(previewsmcp playground)` since the path never flushes.
47+
fputs("\(fileURL.path)\n", stdout)
48+
fflush(stdout)
3849
fputs("Edit this file to see live changes.\n", stderr)
3950

4051
let windowWidth = width

0 commit comments

Comments
 (0)