@pylonsync/create-pylon 0.3.51 → 0.3.54

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.
Files changed (180) hide show
  1. package/bin/create-pylon.js +347 -1156
  2. package/package.json +4 -3
  3. package/templates/_root/.env.example +9 -0
  4. package/templates/_root/README.md +43 -0
  5. package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
  6. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  7. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  8. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  9. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  10. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  11. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  12. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  13. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  14. package/templates/backend/b2b/apps/api/package.json +20 -0
  15. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  16. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  17. package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
  18. package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
  19. package/templates/backend/barebones/apps/api/package.json +20 -0
  20. package/templates/backend/barebones/apps/api/schema.ts +61 -0
  21. package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  23. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  24. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  25. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  26. package/templates/backend/chat/apps/api/package.json +20 -0
  27. package/templates/backend/chat/apps/api/schema.ts +93 -0
  28. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  29. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  30. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  31. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  32. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  33. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  34. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  35. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  36. package/templates/backend/consumer/apps/api/package.json +20 -0
  37. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  38. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  39. package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
  40. package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
  41. package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
  42. package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
  43. package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
  44. package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
  45. package/templates/backend/todo/apps/api/package.json +20 -0
  46. package/templates/backend/todo/apps/api/schema.ts +85 -0
  47. package/templates/backend/todo/apps/api/tsconfig.json +13 -0
  48. package/templates/expo/barebones/apps/expo/App.tsx +166 -0
  49. package/templates/expo/barebones/apps/expo/app.json +31 -0
  50. package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
  51. package/templates/expo/barebones/apps/expo/package.json +30 -0
  52. package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
  53. package/templates/expo/chat/apps/expo/App.tsx +414 -0
  54. package/templates/expo/chat/apps/expo/app.json +25 -0
  55. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  56. package/templates/expo/chat/apps/expo/package.json +30 -0
  57. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  58. package/templates/expo/consumer/apps/expo/App.tsx +360 -0
  59. package/templates/expo/consumer/apps/expo/app.json +25 -0
  60. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  61. package/templates/expo/consumer/apps/expo/package.json +30 -0
  62. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  63. package/templates/expo/todo/apps/expo/App.tsx +287 -0
  64. package/templates/expo/todo/apps/expo/app.json +25 -0
  65. package/templates/expo/todo/apps/expo/babel.config.js +6 -0
  66. package/templates/expo/todo/apps/expo/package.json +30 -0
  67. package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
  68. package/templates/ios/barebones/apps/ios/Package.swift +34 -0
  69. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
  70. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  71. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
  72. package/templates/ios/barebones/apps/ios/project.yml +42 -0
  73. package/templates/ios/chat/apps/ios/Package.swift +34 -0
  74. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -0
  75. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  76. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  77. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
  78. package/templates/ios/chat/apps/ios/project.yml +42 -0
  79. package/templates/ios/consumer/apps/ios/Package.swift +23 -0
  80. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  81. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  82. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  83. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  84. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
  85. package/templates/ios/consumer/apps/ios/project.yml +42 -0
  86. package/templates/ios/todo/apps/ios/Package.swift +23 -0
  87. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
  88. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
  89. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
  90. package/templates/ios/todo/apps/ios/project.yml +32 -0
  91. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  92. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  93. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  94. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  95. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  96. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  97. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  98. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  99. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  100. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  101. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  102. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  103. package/templates/mac/chat/apps/mac/Package.swift +33 -0
  104. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -0
  105. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  106. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  107. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
  108. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  109. package/templates/mac/chat/apps/mac/project.yml +34 -0
  110. package/templates/mac/consumer/apps/mac/Package.swift +33 -0
  111. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  112. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  113. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  114. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  115. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
  116. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  117. package/templates/mac/consumer/apps/mac/project.yml +34 -0
  118. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  119. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  120. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  121. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  122. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  123. package/templates/mac/todo/apps/mac/project.yml +34 -0
  124. package/templates/ui/packages/ui/package.json +26 -0
  125. package/templates/ui/packages/ui/src/button.tsx +44 -0
  126. package/templates/ui/packages/ui/src/card.tsx +39 -0
  127. package/templates/ui/packages/ui/src/cn.ts +12 -0
  128. package/templates/ui/packages/ui/src/index.ts +4 -0
  129. package/templates/ui/packages/ui/src/input.tsx +19 -0
  130. package/templates/ui/packages/ui/tsconfig.json +15 -0
  131. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  132. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  133. package/templates/web/b2b/apps/web/package.json +29 -0
  134. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  135. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  136. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  137. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  138. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  139. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  140. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  141. package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
  142. package/templates/web/barebones/apps/web/next.config.ts +40 -0
  143. package/templates/web/barebones/apps/web/package.json +29 -0
  144. package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
  145. package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
  146. package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
  147. package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
  148. package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
  149. package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
  150. package/templates/web/barebones/apps/web/tsconfig.json +26 -0
  151. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  152. package/templates/web/chat/apps/web/next.config.ts +24 -0
  153. package/templates/web/chat/apps/web/package.json +29 -0
  154. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  155. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +250 -0
  156. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  157. package/templates/web/chat/apps/web/src/app/layout.tsx +21 -0
  158. package/templates/web/chat/apps/web/src/app/page.tsx +51 -0
  159. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  160. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  161. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  162. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  163. package/templates/web/consumer/apps/web/package.json +29 -0
  164. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  165. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +295 -0
  166. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  167. package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
  168. package/templates/web/consumer/apps/web/src/app/page.tsx +55 -0
  169. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  170. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  171. package/templates/web/todo/apps/web/next-env.d.ts +2 -0
  172. package/templates/web/todo/apps/web/next.config.ts +24 -0
  173. package/templates/web/todo/apps/web/package.json +32 -0
  174. package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
  175. package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
  176. package/templates/web/todo/apps/web/src/app/globals.css +6 -0
  177. package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
  178. package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
  179. package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
  180. package/templates/web/todo/apps/web/tsconfig.json +26 -0
@@ -0,0 +1,34 @@
1
+ name: __APP_NAME_PASCAL__
2
+ options:
3
+ bundleIdPrefix: com.example
4
+ deploymentTarget:
5
+ macOS: "13.0"
6
+
7
+ packages:
8
+ pylon:
9
+ url: https://github.com/pylonsync/pylon.git
10
+ from: "0.3.0"
11
+
12
+ targets:
13
+ __APP_NAME_PASCAL__:
14
+ type: application
15
+ platform: macOS
16
+ sources:
17
+ - path: Sources/__APP_NAME_PASCAL__
18
+ settings:
19
+ base:
20
+ GENERATE_INFOPLIST_FILE: YES
21
+ INFOPLIST_KEY_CFBundleDisplayName: __APP_NAME__
22
+ INFOPLIST_KEY_LSApplicationCategoryType: "public.app-category.developer-tools"
23
+ INFOPLIST_KEY_NSHumanReadableCopyright: ""
24
+ SWIFT_VERSION: "5.9"
25
+ ENABLE_PREVIEWS: YES
26
+ # The mac app talks to the local Pylon control plane during dev.
27
+ # App Sandbox needs the network client entitlement to reach
28
+ # localhost; we declare it via codesign entitlements at build time.
29
+ CODE_SIGN_ENTITLEMENTS: __APP_NAME_PASCAL__.entitlements
30
+ dependencies:
31
+ - package: pylon
32
+ product: PylonClient
33
+ - package: pylon
34
+ product: PylonSwiftUI
@@ -0,0 +1,33 @@
1
+ // swift-tools-version:5.9
2
+ import PackageDescription
3
+
4
+ // SwiftPM package for the __APP_NAME__ macOS app.
5
+ //
6
+ // The executable target runs locally with `swift run`. For a proper
7
+ // signed `.app` bundle:
8
+ //
9
+ // brew install xcodegen
10
+ // xcodegen generate
11
+ // open __APP_NAME_PASCAL__.xcodeproj
12
+ //
13
+ // The Xcode project pulls the same Sources/__APP_NAME_PASCAL__/ tree
14
+ // as `swift build`, so SwiftPM and Xcode share one source set.
15
+ let package = Package(
16
+ name: "__APP_NAME_PASCAL__",
17
+ platforms: [
18
+ .macOS(.v13),
19
+ ],
20
+ dependencies: [
21
+ .package(url: "https://github.com/pylonsync/pylon.git", from: "0.3.0"),
22
+ ],
23
+ targets: [
24
+ .executableTarget(
25
+ name: "__APP_NAME_PASCAL__",
26
+ dependencies: [
27
+ .product(name: "PylonClient", package: "pylon"),
28
+ .product(name: "PylonSwiftUI", package: "pylon"),
29
+ ],
30
+ path: "Sources/__APP_NAME_PASCAL__"
31
+ ),
32
+ ]
33
+ )
@@ -0,0 +1,104 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ /// macOS list + create form for Widget. Two-pane NavigationSplitView
5
+ /// with sidebar on the left and detail on the right — feels native on
6
+ /// macOS. Same backend as web/iOS/expo.
7
+ struct ContentView: View {
8
+ @EnvironmentObject var session: AppSession
9
+ @State private var widgets: [Widget] = []
10
+ @State private var selected: Widget.ID?
11
+ @State private var newName = ""
12
+ @State private var loading = true
13
+ @State private var creating = false
14
+ @State private var errorMessage: String?
15
+
16
+ var body: some View {
17
+ NavigationSplitView {
18
+ List(selection: $selected) {
19
+ if widgets.isEmpty && !loading {
20
+ Text("No widgets yet.")
21
+ .foregroundStyle(.secondary)
22
+ .font(.callout)
23
+ } else {
24
+ ForEach(widgets) { w in
25
+ HStack {
26
+ Text(w.name)
27
+ Spacer()
28
+ Text("\(w.count)")
29
+ .font(.system(.caption, design: .monospaced))
30
+ .foregroundStyle(.secondary)
31
+ }
32
+ .tag(w.id)
33
+ }
34
+ }
35
+ }
36
+ .navigationTitle("__APP_NAME__")
37
+ .toolbar {
38
+ ToolbarItem(placement: .primaryAction) {
39
+ Button {
40
+ Task { await load() }
41
+ } label: {
42
+ Image(systemName: "arrow.clockwise")
43
+ }
44
+ .disabled(loading)
45
+ }
46
+ }
47
+ } detail: {
48
+ VStack(alignment: .leading, spacing: 18) {
49
+ Text("Create a widget")
50
+ .font(.title3)
51
+ .fontWeight(.semibold)
52
+ HStack {
53
+ TextField("Name…", text: $newName)
54
+ .textFieldStyle(.roundedBorder)
55
+ .onSubmit { Task { await create() } }
56
+ Button("Create") { Task { await create() } }
57
+ .keyboardShortcut(.defaultAction)
58
+ .disabled(newName.trimmingCharacters(in: .whitespaces).isEmpty || creating)
59
+ }
60
+ if let errorMessage {
61
+ Text(errorMessage)
62
+ .foregroundStyle(.red)
63
+ .font(.caption)
64
+ }
65
+ Spacer()
66
+ }
67
+ .padding(24)
68
+ }
69
+ .task { await load() }
70
+ }
71
+
72
+ private func load() async {
73
+ loading = true
74
+ defer { loading = false }
75
+ do {
76
+ let rows: [Widget] = try await session.pylon.callFn(
77
+ "listWidgets",
78
+ args: EmptyArgs(),
79
+ )
80
+ widgets = rows
81
+ errorMessage = nil
82
+ } catch {
83
+ errorMessage = "Load failed: \(error.localizedDescription)"
84
+ }
85
+ }
86
+
87
+ private func create() async {
88
+ let trimmed = newName.trimmingCharacters(in: .whitespaces)
89
+ guard !trimmed.isEmpty else { return }
90
+ creating = true
91
+ defer { creating = false }
92
+ do {
93
+ let widget: Widget = try await session.pylon.callFn(
94
+ "createWidget",
95
+ args: CreateWidgetArgs(name: trimmed),
96
+ )
97
+ widgets.insert(widget, at: 0)
98
+ newName = ""
99
+ errorMessage = nil
100
+ } catch {
101
+ errorMessage = "Create failed: \(error.localizedDescription)"
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,17 @@
1
+ import Foundation
2
+
3
+ /// Mirrors `Widget` from `apps/api/schema.ts`. Regenerate from the
4
+ /// schema with `pylon codegen client schema.ts --target swift` for
5
+ /// production.
6
+ struct Widget: Codable, Identifiable, Hashable {
7
+ let id: String
8
+ let name: String
9
+ let count: Int
10
+ let createdAt: String
11
+ }
12
+
13
+ struct CreateWidgetArgs: Encodable {
14
+ let name: String
15
+ }
16
+
17
+ struct EmptyArgs: Encodable {}
@@ -0,0 +1,30 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ @main
5
+ struct __APP_NAME_PASCAL__App: App {
6
+ @StateObject private var session = AppSession()
7
+
8
+ var body: some Scene {
9
+ Window("__APP_NAME__", id: "main") {
10
+ ContentView()
11
+ .environmentObject(session)
12
+ .frame(minWidth: 480, minHeight: 360)
13
+ }
14
+ .windowResizability(.contentMinSize)
15
+ }
16
+ }
17
+
18
+ @MainActor
19
+ final class AppSession: ObservableObject {
20
+ let pylon: PylonClient
21
+
22
+ init() {
23
+ let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
24
+ ?? "http://localhost:4321"
25
+ guard let url = URL(string: baseURLString) else {
26
+ fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
27
+ }
28
+ self.pylon = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <!-- App Sandbox + outgoing-network client. Without these the
6
+ scaffolded app can't reach localhost:4321 during dev or
7
+ a remote Pylon Cloud URL in production. -->
8
+ <key>com.apple.security.app-sandbox</key>
9
+ <true/>
10
+ <key>com.apple.security.network.client</key>
11
+ <true/>
12
+ </dict>
13
+ </plist>
@@ -0,0 +1,34 @@
1
+ name: __APP_NAME_PASCAL__
2
+ options:
3
+ bundleIdPrefix: com.example
4
+ deploymentTarget:
5
+ macOS: "13.0"
6
+
7
+ packages:
8
+ pylon:
9
+ url: https://github.com/pylonsync/pylon.git
10
+ from: "0.3.0"
11
+
12
+ targets:
13
+ __APP_NAME_PASCAL__:
14
+ type: application
15
+ platform: macOS
16
+ sources:
17
+ - path: Sources/__APP_NAME_PASCAL__
18
+ settings:
19
+ base:
20
+ GENERATE_INFOPLIST_FILE: YES
21
+ INFOPLIST_KEY_CFBundleDisplayName: __APP_NAME__
22
+ INFOPLIST_KEY_LSApplicationCategoryType: "public.app-category.developer-tools"
23
+ INFOPLIST_KEY_NSHumanReadableCopyright: ""
24
+ SWIFT_VERSION: "5.9"
25
+ ENABLE_PREVIEWS: YES
26
+ # The mac app talks to the local Pylon control plane during dev.
27
+ # App Sandbox needs the network client entitlement to reach
28
+ # localhost; we declare it via codesign entitlements at build time.
29
+ CODE_SIGN_ENTITLEMENTS: __APP_NAME_PASCAL__.entitlements
30
+ dependencies:
31
+ - package: pylon
32
+ product: PylonClient
33
+ - package: pylon
34
+ product: PylonSwiftUI
@@ -0,0 +1,33 @@
1
+ // swift-tools-version:5.9
2
+ import PackageDescription
3
+
4
+ // SwiftPM package for the __APP_NAME__ macOS app.
5
+ //
6
+ // The executable target runs locally with `swift run`. For a proper
7
+ // signed `.app` bundle:
8
+ //
9
+ // brew install xcodegen
10
+ // xcodegen generate
11
+ // open __APP_NAME_PASCAL__.xcodeproj
12
+ //
13
+ // The Xcode project pulls the same Sources/__APP_NAME_PASCAL__/ tree
14
+ // as `swift build`, so SwiftPM and Xcode share one source set.
15
+ let package = Package(
16
+ name: "__APP_NAME_PASCAL__",
17
+ platforms: [
18
+ .macOS(.v13),
19
+ ],
20
+ dependencies: [
21
+ .package(url: "https://github.com/pylonsync/pylon.git", from: "0.3.0"),
22
+ ],
23
+ targets: [
24
+ .executableTarget(
25
+ name: "__APP_NAME_PASCAL__",
26
+ dependencies: [
27
+ .product(name: "PylonClient", package: "pylon"),
28
+ .product(name: "PylonSwiftUI", package: "pylon"),
29
+ ],
30
+ path: "Sources/__APP_NAME_PASCAL__"
31
+ ),
32
+ ]
33
+ )
@@ -0,0 +1,140 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ /// macOS chat: rooms in a sidebar, room view in the detail pane.
5
+ /// Polls the active room every 1.5s. Replace with PylonQuery<Message>
6
+ /// from PylonSwiftUI for realtime push.
7
+ struct ChatRootView: View {
8
+ @EnvironmentObject var session: AppSession
9
+ @State private var rooms: [Room] = []
10
+ @State private var selected: Room.ID?
11
+ @State private var showingCreate = false
12
+ @State private var draftSlug = ""
13
+ @State private var draftName = ""
14
+ @State private var loading = true
15
+ @State private var errorMessage: String?
16
+
17
+ var body: some View {
18
+ NavigationSplitView {
19
+ VStack(spacing: 0) {
20
+ List(selection: $selected) {
21
+ Section("Rooms") {
22
+ ForEach(rooms) { r in
23
+ VStack(alignment: .leading, spacing: 2) {
24
+ Text(r.name)
25
+ Text("#\(r.slug)")
26
+ .font(.system(.caption, design: .monospaced))
27
+ .foregroundStyle(.secondary)
28
+ }
29
+ .tag(r.id)
30
+ }
31
+ }
32
+ }
33
+ Divider()
34
+ HStack(spacing: 8) {
35
+ TextField("display name", text: Binding(
36
+ get: { session.authorName },
37
+ set: { session.setAuthorName($0) },
38
+ ))
39
+ .textFieldStyle(.roundedBorder)
40
+ .font(.caption)
41
+ }
42
+ .padding(8)
43
+ }
44
+ .toolbar {
45
+ ToolbarItem(placement: .primaryAction) {
46
+ Button {
47
+ showingCreate.toggle()
48
+ } label: {
49
+ Image(systemName: "plus")
50
+ }
51
+ }
52
+ }
53
+ .sheet(isPresented: $showingCreate) {
54
+ createSheet
55
+ }
56
+ } detail: {
57
+ if let id = selected, let room = rooms.first(where: { $0.id == id }) {
58
+ RoomView(room: room)
59
+ } else {
60
+ placeholder
61
+ }
62
+ }
63
+ .task { await load() }
64
+ }
65
+
66
+ private var createSheet: some View {
67
+ VStack(alignment: .leading, spacing: 12) {
68
+ Text("Create a room")
69
+ .font(.headline)
70
+ TextField("Name", text: $draftName)
71
+ .textFieldStyle(.roundedBorder)
72
+ TextField("Slug (e.g. general)", text: $draftSlug)
73
+ .textFieldStyle(.roundedBorder)
74
+ .autocorrectionDisabled()
75
+ HStack {
76
+ Spacer()
77
+ Button("Cancel") { showingCreate = false }
78
+ .buttonStyle(.bordered)
79
+ Button("Create") { Task { await create() } }
80
+ .keyboardShortcut(.defaultAction)
81
+ .disabled(draftName.trimmingCharacters(in: .whitespaces).isEmpty
82
+ || draftSlug.trimmingCharacters(in: .whitespaces).isEmpty)
83
+ }
84
+ if let errorMessage {
85
+ Text(errorMessage).foregroundStyle(.red).font(.caption)
86
+ }
87
+ }
88
+ .padding(20)
89
+ .frame(width: 360)
90
+ }
91
+
92
+ private var placeholder: some View {
93
+ VStack(spacing: 8) {
94
+ if loading {
95
+ ProgressView()
96
+ } else if rooms.isEmpty {
97
+ Text("No rooms yet.")
98
+ .foregroundStyle(.secondary)
99
+ Button("Create your first room") { showingCreate = true }
100
+ .buttonStyle(.bordered)
101
+ } else {
102
+ Text("Pick a room from the sidebar.")
103
+ .foregroundStyle(.secondary)
104
+ }
105
+ }
106
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
107
+ }
108
+
109
+ private func load() async {
110
+ loading = true
111
+ defer { loading = false }
112
+ do {
113
+ rooms = try await session.pylon.callFn("listRooms", args: EmptyArgs())
114
+ if selected == nil { selected = rooms.first?.id }
115
+ errorMessage = nil
116
+ } catch {
117
+ errorMessage = "Load failed: \(error.localizedDescription)"
118
+ }
119
+ }
120
+
121
+ private func create() async {
122
+ let name = draftName.trimmingCharacters(in: .whitespaces)
123
+ let slug = draftSlug.trimmingCharacters(in: .whitespaces).lowercased()
124
+ guard !name.isEmpty, !slug.isEmpty else { return }
125
+ do {
126
+ let room: Room = try await session.pylon.callFn(
127
+ "createRoom",
128
+ args: CreateRoomArgs(slug: slug, name: name),
129
+ )
130
+ rooms.append(room)
131
+ selected = room.id
132
+ showingCreate = false
133
+ draftName = ""
134
+ draftSlug = ""
135
+ errorMessage = nil
136
+ } catch {
137
+ errorMessage = "Create failed: \(error.localizedDescription)"
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,26 @@
1
+ import Foundation
2
+
3
+ struct Room: Codable, Identifiable, Hashable {
4
+ let id: String
5
+ let slug: String
6
+ let name: String
7
+ let createdAt: String
8
+ }
9
+
10
+ struct Message: Codable, Identifiable, Hashable {
11
+ let id: String
12
+ let roomId: String
13
+ let authorId: String
14
+ let authorName: String
15
+ let body: String
16
+ let createdAt: String
17
+ }
18
+
19
+ struct CreateRoomArgs: Encodable { let slug: String; let name: String }
20
+ struct RoomMessagesArgs: Encodable { let roomId: String }
21
+ struct SendMessageArgs: Encodable {
22
+ let roomId: String
23
+ let body: String
24
+ let authorName: String
25
+ }
26
+ struct EmptyArgs: Encodable {}
@@ -0,0 +1,137 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ struct RoomView: View {
5
+ @EnvironmentObject var session: AppSession
6
+ let room: Room
7
+ @State private var messages: [Message] = []
8
+ @State private var draft = ""
9
+ @State private var sending = false
10
+ @State private var errorMessage: String?
11
+ @State private var pollTimer: Task<Void, Never>?
12
+
13
+ var body: some View {
14
+ VStack(spacing: 0) {
15
+ ScrollViewReader { proxy in
16
+ ScrollView {
17
+ LazyVStack(alignment: .leading, spacing: 12) {
18
+ ForEach(messages) { msg in
19
+ messageRow(msg).id(msg.id)
20
+ }
21
+ if messages.isEmpty {
22
+ Text("No messages yet. Say hi.")
23
+ .foregroundStyle(.secondary)
24
+ .padding(.top, 32)
25
+ .frame(maxWidth: .infinity)
26
+ }
27
+ }
28
+ .padding(16)
29
+ }
30
+ .onChange(of: messages.count) { _, _ in
31
+ if let last = messages.last {
32
+ withAnimation { proxy.scrollTo(last.id, anchor: .bottom) }
33
+ }
34
+ }
35
+ }
36
+
37
+ if let errorMessage {
38
+ Text(errorMessage)
39
+ .foregroundStyle(.red)
40
+ .font(.caption)
41
+ .padding(8)
42
+ }
43
+
44
+ HStack {
45
+ TextField("Message #\(room.slug)…", text: $draft, axis: .vertical)
46
+ .textFieldStyle(.roundedBorder)
47
+ .lineLimit(1...4)
48
+ .onSubmit { Task { await send() } }
49
+ Button("Send") { Task { await send() } }
50
+ .keyboardShortcut(.defaultAction)
51
+ .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || sending)
52
+ }
53
+ .padding(12)
54
+ .background(.thinMaterial)
55
+ }
56
+ .navigationTitle(room.name)
57
+ #if os(iOS)
58
+ .navigationBarTitleDisplayMode(.inline)
59
+ #endif
60
+ .task {
61
+ await loadMessages()
62
+ startPolling()
63
+ }
64
+ .onDisappear { pollTimer?.cancel() }
65
+ }
66
+
67
+ @ViewBuilder
68
+ private func messageRow(_ msg: Message) -> some View {
69
+ VStack(alignment: .leading, spacing: 2) {
70
+ HStack(alignment: .firstTextBaseline) {
71
+ Text(msg.authorName).font(.subheadline.weight(.medium))
72
+ Text(formatTime(msg.createdAt))
73
+ .font(.caption2)
74
+ .foregroundStyle(.tertiary)
75
+ }
76
+ Text(msg.body)
77
+ .font(.body)
78
+ .fixedSize(horizontal: false, vertical: true)
79
+ }
80
+ }
81
+
82
+ private func formatTime(_ iso: String) -> String {
83
+ let formatter = ISO8601DateFormatter()
84
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
85
+ guard let date = formatter.date(from: iso) ?? ISO8601DateFormatter().date(from: iso) else {
86
+ return ""
87
+ }
88
+ let display = DateFormatter()
89
+ display.timeStyle = .short
90
+ return display.string(from: date)
91
+ }
92
+
93
+ private func loadMessages() async {
94
+ do {
95
+ messages = try await session.pylon.callFn(
96
+ "roomMessages",
97
+ args: RoomMessagesArgs(roomId: room.id),
98
+ )
99
+ errorMessage = nil
100
+ } catch {
101
+ errorMessage = "Load failed: \(error.localizedDescription)"
102
+ }
103
+ }
104
+
105
+ private func startPolling() {
106
+ pollTimer?.cancel()
107
+ pollTimer = Task {
108
+ while !Task.isCancelled {
109
+ try? await Task.sleep(nanoseconds: 1_500_000_000)
110
+ if Task.isCancelled { break }
111
+ await loadMessages()
112
+ }
113
+ }
114
+ }
115
+
116
+ private func send() async {
117
+ let body = draft.trimmingCharacters(in: .whitespaces)
118
+ guard !body.isEmpty else { return }
119
+ sending = true
120
+ defer { sending = false }
121
+ do {
122
+ let msg: Message = try await session.pylon.callFn(
123
+ "sendMessage",
124
+ args: SendMessageArgs(
125
+ roomId: room.id,
126
+ body: body,
127
+ authorName: session.authorName,
128
+ ),
129
+ )
130
+ messages.append(msg)
131
+ draft = ""
132
+ errorMessage = nil
133
+ } catch {
134
+ errorMessage = "Send failed: \(error.localizedDescription)"
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,37 @@
1
+ import SwiftUI
2
+ import PylonClient
3
+
4
+ @main
5
+ struct __APP_NAME_PASCAL__App: App {
6
+ @StateObject private var session = AppSession()
7
+
8
+ var body: some Scene {
9
+ Window("__APP_NAME__", id: "main") {
10
+ ChatRootView()
11
+ .environmentObject(session)
12
+ .frame(minWidth: 700, minHeight: 500)
13
+ }
14
+ .windowResizability(.contentMinSize)
15
+ }
16
+ }
17
+
18
+ @MainActor
19
+ final class AppSession: ObservableObject {
20
+ let pylon: PylonClient
21
+ @Published var authorName: String
22
+
23
+ init() {
24
+ let baseURLString = ProcessInfo.processInfo.environment["PYLON_BASE_URL"]
25
+ ?? "http://localhost:4321"
26
+ guard let url = URL(string: baseURLString) else {
27
+ fatalError("Invalid PYLON_BASE_URL: \(baseURLString)")
28
+ }
29
+ self.pylon = PylonClient(baseURL: url, appName: "__APP_NAME_SNAKE__")
30
+ self.authorName = UserDefaults.standard.string(forKey: "authorName") ?? "anonymous"
31
+ }
32
+
33
+ func setAuthorName(_ name: String) {
34
+ authorName = name
35
+ UserDefaults.standard.set(name, forKey: "authorName")
36
+ }
37
+ }
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <!-- App Sandbox + outgoing-network client. Without these the
6
+ scaffolded app can't reach localhost:4321 during dev or
7
+ a remote Pylon Cloud URL in production. -->
8
+ <key>com.apple.security.app-sandbox</key>
9
+ <true/>
10
+ <key>com.apple.security.network.client</key>
11
+ <true/>
12
+ </dict>
13
+ </plist>
@@ -0,0 +1,34 @@
1
+ name: __APP_NAME_PASCAL__
2
+ options:
3
+ bundleIdPrefix: com.example
4
+ deploymentTarget:
5
+ macOS: "13.0"
6
+
7
+ packages:
8
+ pylon:
9
+ url: https://github.com/pylonsync/pylon.git
10
+ from: "0.3.0"
11
+
12
+ targets:
13
+ __APP_NAME_PASCAL__:
14
+ type: application
15
+ platform: macOS
16
+ sources:
17
+ - path: Sources/__APP_NAME_PASCAL__
18
+ settings:
19
+ base:
20
+ GENERATE_INFOPLIST_FILE: YES
21
+ INFOPLIST_KEY_CFBundleDisplayName: __APP_NAME__
22
+ INFOPLIST_KEY_LSApplicationCategoryType: "public.app-category.developer-tools"
23
+ INFOPLIST_KEY_NSHumanReadableCopyright: ""
24
+ SWIFT_VERSION: "5.9"
25
+ ENABLE_PREVIEWS: YES
26
+ # The mac app talks to the local Pylon control plane during dev.
27
+ # App Sandbox needs the network client entitlement to reach
28
+ # localhost; we declare it via codesign entitlements at build time.
29
+ CODE_SIGN_ENTITLEMENTS: __APP_NAME_PASCAL__.entitlements
30
+ dependencies:
31
+ - package: pylon
32
+ product: PylonClient
33
+ - package: pylon
34
+ product: PylonSwiftUI