Files
xtool-mirror/Sources/XToolSupport/NewCommand.swift
Kabir Oberai 205041caa0 Refine NewCommand
Tweak template, respect quiet flag on macOS
2025-05-09 11:10:15 +05:30

184 lines
5.7 KiB
Swift

import ArgumentParser
import Foundation
import PackLib
struct NewCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "new",
abstract: "Create a new xtool SwiftPM project"
)
@Argument var name: String?
@Flag(
help: ArgumentHelp(
"Skip setup steps. (default: false)",
discussion: """
By default, this command first invokes `xtool setup` to complete \
any missing setup steps, like authenticating and installing the SDK. Use \
this flag to always skip setup.
"""
)
) var skipSetup = false
func run() async throws {
if !skipSetup {
// perform any remaining setup steps first
try await SetupOperation(quiet: true).run()
}
let name = try await Console.promptRequired("Package name: ", existing: self.name)
var allowedFirstCharacters: CharacterSet = ["_"]
allowedFirstCharacters.insert(charactersIn: "a"..."z")
allowedFirstCharacters.insert(charactersIn: "A"..."Z")
var allowedOtherCharacters = allowedFirstCharacters
allowedOtherCharacters.insert(charactersIn: "0"..."9")
allowedOtherCharacters.insert("-")
// promptRequired validates !isEmpty
let firstScalar = name.unicodeScalars.first!
guard allowedFirstCharacters.contains(firstScalar) else {
throw Console.Error("""
Package name '\(name)' is invalid. \
The package name must start with one of [a-z, A-Z, _]. Found '\(firstScalar)'.
""")
}
if let firstInvalid = name.rangeOfCharacter(from: allowedOtherCharacters.inverted) {
let invalidValue = name[firstInvalid]
throw Console.Error("""
Package name '\(name)' is invalid. \
The package name may only contain [a-z, A-Z, 0-9, _, -]. Found '\(invalidValue)'.
""")
}
let baseURL = URL(fileURLWithPath: name)
guard !baseURL.exists else {
throw Console.Error("Cannot create \(name): a file already exists at that path.")
}
print("Creating package: \(name)")
let moduleName = name.replacingOccurrences(of: "-", with: "_")
let files: [(String, String)] = [
(
"Package.swift",
"""
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "\(name)",
platforms: [
.iOS(.v17),
.macOS(.v14),
],
products: [
// An xtool project should contain exactly one library product,
// representing the main app.
.library(
name: "\(moduleName)",
targets: ["\(moduleName)"]
),
],
targets: [
.target(
name: "\(moduleName)"
),
]
)
"""
),
(
"xtool.yml",
"""
version: 1
bundleID: com.example.\(name)
"""
),
(
".gitignore",
"""
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
/xtool
"""
),
(
".sourcekit-lsp/config.json",
"""
{
"swiftPM": {
"swiftSDK": "arm64-apple-ios"
}
}
"""
),
(
"Sources/\(moduleName)/\(moduleName)App.swift",
"""
import SwiftUI
@main
struct \(moduleName)App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
"""
),
(
"Sources/\(moduleName)/ContentView.swift",
"""
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
"""
),
]
for (path, contents) in files {
let url = baseURL.appendingPathComponent(path)
try? FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
print("Creating \(path)")
try "\(contents)\n".write(to: url, atomically: true, encoding: .utf8)
}
print("\nFinished generating project \(name). Next steps:")
print("- Enter the directory with `cd \(name)`")
print("- Build and run with `xtool dev`")
}
}