@pylonsync/create-pylon 0.3.53 → 0.3.55

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 (129) hide show
  1. package/bin/create-pylon.js +98 -42
  2. package/package.json +1 -1
  3. package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
  4. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  5. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  6. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  7. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  8. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  9. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  10. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  11. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  12. package/templates/backend/b2b/apps/api/package.json +20 -0
  13. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  14. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  15. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  16. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  17. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  18. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  19. package/templates/backend/chat/apps/api/package.json +20 -0
  20. package/templates/backend/chat/apps/api/schema.ts +93 -0
  21. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  23. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  24. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  25. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  26. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  27. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  28. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  29. package/templates/backend/consumer/apps/api/package.json +20 -0
  30. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  31. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  32. package/templates/expo/chat/apps/expo/App.tsx +384 -0
  33. package/templates/expo/chat/apps/expo/app.json +25 -0
  34. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  35. package/templates/expo/chat/apps/expo/package.json +30 -0
  36. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  37. package/templates/expo/consumer/apps/expo/App.tsx +392 -0
  38. package/templates/expo/consumer/apps/expo/app.json +25 -0
  39. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  40. package/templates/expo/consumer/apps/expo/package.json +30 -0
  41. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  42. package/templates/ios/chat/apps/ios/Package.swift +24 -0
  43. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +116 -0
  44. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  45. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
  46. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
  47. package/templates/ios/chat/apps/ios/project.yml +44 -0
  48. package/templates/ios/consumer/apps/ios/Package.swift +24 -0
  49. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
  50. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
  51. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
  52. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
  53. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +57 -0
  54. package/templates/ios/consumer/apps/ios/project.yml +44 -0
  55. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  56. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  57. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  58. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  59. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  60. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  61. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  62. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  63. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  64. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  65. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  66. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  67. package/templates/mac/chat/apps/mac/Package.swift +34 -0
  68. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +143 -0
  69. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  70. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
  71. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
  72. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  73. package/templates/mac/chat/apps/mac/project.yml +36 -0
  74. package/templates/mac/consumer/apps/mac/Package.swift +34 -0
  75. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
  76. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
  77. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
  78. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
  79. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +59 -0
  80. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  81. package/templates/mac/consumer/apps/mac/project.yml +36 -0
  82. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  83. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  84. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  85. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  86. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  87. package/templates/mac/todo/apps/mac/project.yml +34 -0
  88. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  89. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  90. package/templates/web/b2b/apps/web/package.json +29 -0
  91. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  92. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  93. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  94. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  95. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  96. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  97. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  98. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  99. package/templates/web/chat/apps/web/next.config.ts +24 -0
  100. package/templates/web/chat/apps/web/package.json +29 -0
  101. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  102. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +231 -0
  103. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  104. package/templates/web/chat/apps/web/src/app/layout.tsx +22 -0
  105. package/templates/web/chat/apps/web/src/app/page.tsx +12 -0
  106. package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
  107. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  108. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  109. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  110. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  111. package/templates/web/consumer/apps/web/package.json +29 -0
  112. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  113. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +315 -0
  114. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  115. package/templates/web/consumer/apps/web/src/app/layout.tsx +22 -0
  116. package/templates/web/consumer/apps/web/src/app/page.tsx +21 -0
  117. package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
  118. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  119. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  120. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
  121. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
  122. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  123. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  124. /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
  125. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Package.swift +0 -0
  126. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
  127. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
  128. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
  129. /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/project.yml +0 -0
@@ -57,10 +57,38 @@ const PYLON_VERSION = JSON.parse(
57
57
 
58
58
  // ---------------------------------------------------------------------------
59
59
  // Templates + platforms registry
60
+ //
61
+ // Each template declares which platforms it supports — `b2b` is web/mac
62
+ // only because the demo flow (org switcher, member invite, RBAC admin
63
+ // panel) is desktop-shaped and porting it to mobile would be busy work
64
+ // without value. Pick a different template if you want mobile.
60
65
  // ---------------------------------------------------------------------------
61
66
 
62
- const TEMPLATES_AVAILABLE = ["barebones", "todo"];
63
- const PLATFORMS_AVAILABLE = ["web", "mobile", "expo"];
67
+ const PLATFORMS_AVAILABLE = ["web", "ios", "mac", "expo"];
68
+
69
+ const TEMPLATE_REGISTRY = {
70
+ barebones: {
71
+ blurb: "Single entity, list + create. The smallest working app.",
72
+ platforms: ["web", "ios", "mac", "expo"],
73
+ },
74
+ todo: {
75
+ blurb: "CRUD + drag-reorder + optimistic mutations.",
76
+ platforms: ["web", "ios", "mac", "expo"],
77
+ },
78
+ b2b: {
79
+ blurb: "Multi-tenant SaaS: orgs, members, roles, RBAC policies.",
80
+ platforms: ["web", "mac"],
81
+ },
82
+ consumer: {
83
+ blurb: "Social feed: profiles, posts, likes, follows.",
84
+ platforms: ["web", "ios", "mac", "expo"],
85
+ },
86
+ chat: {
87
+ blurb: "Realtime messaging: rooms, presence, live message feed.",
88
+ platforms: ["web", "ios", "mac", "expo"],
89
+ },
90
+ };
91
+ const TEMPLATES_AVAILABLE = Object.keys(TEMPLATE_REGISTRY);
64
92
 
65
93
  // ---------------------------------------------------------------------------
66
94
  // CLI args + interactive prompts
@@ -95,18 +123,24 @@ function pickValue(arr, ...candidates) {
95
123
  }
96
124
 
97
125
  if (flags.help) {
126
+ const tmplLines = Object.entries(TEMPLATE_REGISTRY).map(
127
+ ([k, v]) => ` ${k.padEnd(10)} ${v.blurb} (${v.platforms.join(", ")})`,
128
+ );
98
129
  process.stdout.write(`
99
130
  Usage: npm create @pylonsync/pylon [name] [options]
100
131
 
101
- --template <t> barebones | todo
102
- --platforms <list> comma list: web,mobile,expo (default: web)
132
+ --template <t> ${TEMPLATES_AVAILABLE.join(" | ")}
133
+ ${tmplLines.join("\n")}
134
+
135
+ --platforms <list> comma list: ${PLATFORMS_AVAILABLE.join(",")} (default: web)
103
136
  --bun|--pnpm|--yarn|--npm
104
137
  --skip-install scaffold only, don't run install
105
138
 
106
139
  Examples:
107
140
  npm create @pylonsync/pylon my-app
108
- npm create @pylonsync/pylon my-app --template todo --platforms web,mobile
109
- npm create @pylonsync/pylon my-app --template barebones --platforms expo --bun
141
+ npm create @pylonsync/pylon my-app --template todo --platforms web,ios
142
+ npm create @pylonsync/pylon my-app --template b2b --platforms web,mac
143
+ npm create @pylonsync/pylon my-app --template chat --platforms ios,mac,expo
110
144
  `);
111
145
  exit(0);
112
146
  }
@@ -116,6 +150,10 @@ if (!projectName) {
116
150
  projectName = (await rl.question("Project name: ")).trim() || "my-pylon-app";
117
151
  }
118
152
  if (!flags.template) {
153
+ const lines = Object.entries(TEMPLATE_REGISTRY)
154
+ .map(([k, v]) => ` ${k.padEnd(10)} ${v.blurb}`)
155
+ .join("\n");
156
+ process.stdout.write(`\n${lines}\n`);
119
157
  const ans = (
120
158
  await rl.question(
121
159
  `Template (${TEMPLATES_AVAILABLE.join(", ")}) [todo]: `,
@@ -126,9 +164,10 @@ if (!flags.template) {
126
164
  flags.template = TEMPLATES_AVAILABLE.includes(ans) ? ans : "todo";
127
165
  }
128
166
  if (!flags.platforms) {
167
+ const supported = TEMPLATE_REGISTRY[flags.template].platforms.join(", ");
129
168
  const ans = (
130
169
  await rl.question(
131
- `Platforms (${PLATFORMS_AVAILABLE.join(", ")}, comma-separated) [web]: `,
170
+ `Platforms for ${flags.template} (${supported}, comma-separated) [web]: `,
132
171
  )
133
172
  ).trim();
134
173
  flags.platforms = ans || "web";
@@ -169,6 +208,21 @@ if (!TEMPLATES_AVAILABLE.includes(flags.template)) {
169
208
  exit(1);
170
209
  }
171
210
 
211
+ // Reject combos a template doesn't yet support — better to fail loud
212
+ // than to scaffold an incomplete tree (e.g. b2b + expo would skip
213
+ // frontend entirely and leave the user with a half-empty workspace).
214
+ const supportedPlatforms = TEMPLATE_REGISTRY[flags.template].platforms;
215
+ const invalidForTemplate = platforms.filter(
216
+ (p) => !supportedPlatforms.includes(p),
217
+ );
218
+ if (invalidForTemplate.length > 0) {
219
+ console.error(
220
+ `\nError: template "${flags.template}" doesn't support platform(s): ${invalidForTemplate.join(", ")}.\n` +
221
+ ` supported: ${supportedPlatforms.join(", ")}\n`,
222
+ );
223
+ exit(1);
224
+ }
225
+
172
226
  // Some PMs reject the `workspace:` protocol. Bun/pnpm/yarn berry
173
227
  // understand it and rewrite to the local sibling version at install
174
228
  // time. npm errors EUNSUPPORTEDPROTOCOL ("Unsupported URL Type").
@@ -260,13 +314,11 @@ function copyTemplate(srcSubpath, destSubpath = "") {
260
314
 
261
315
  // ---------------------------------------------------------------------------
262
316
  // Apply templates in order:
263
- // 1. _root/_shared — gitignore, env.example, basic README
264
- // 2. backend/<template> — apps/api/* always present
265
- // 3. ui — packages/ui (only if web is in platforms)
266
- // 4. web/<template> apps/web/* (only if web in platforms)
267
- // 5. mobile/<template> — apps/mobile/* (only if mobile in platforms)
268
- // 6. expo/<template> — apps/expo/* (only if expo in platforms)
269
- // 7. Root package.json — generated, not templated; depends on platforms
317
+ // 1. _root — gitignore, env.example, README
318
+ // 2. backend/<t> — apps/api/* always present (one per template)
319
+ // 3. ui — packages/ui (only if web is in platforms)
320
+ // 4. <platform>/<t> one per requested platform under apps/<platform>/
321
+ // 5. Root package.json generated, not templated
270
322
  // ---------------------------------------------------------------------------
271
323
 
272
324
  copyTemplate("_root");
@@ -276,11 +328,8 @@ if (platforms.includes("web")) {
276
328
  copyTemplate("ui");
277
329
  copyTemplate(`web/${flags.template}`);
278
330
  }
279
- if (platforms.includes("mobile")) {
280
- copyTemplate(`mobile/${flags.template}`);
281
- }
282
- if (platforms.includes("expo")) {
283
- copyTemplate(`expo/${flags.template}`);
331
+ for (const p of ["ios", "mac", "expo"]) {
332
+ if (platforms.includes(p)) copyTemplate(`${p}/${flags.template}`);
284
333
  }
285
334
 
286
335
  walkAndSubstitute(root);
@@ -293,23 +342,19 @@ walkAndSubstitute(root);
293
342
 
294
343
  const wsScripts = pmScripts(flags.pm);
295
344
  const devScripts = {};
296
- const buildScripts = {};
297
- if (platforms.includes("web")) {
298
- devScripts["dev:api"] = wsScripts.devApi;
299
- devScripts["dev:web"] = wsScripts.devWeb;
300
- }
301
- if (!platforms.includes("web")) {
302
- // API still runs even without a web platform mobile / expo connect
303
- // to it directly.
304
- devScripts["dev:api"] = wsScripts.devApi;
305
- }
306
- if (platforms.includes("expo")) {
307
- devScripts["dev:expo"] = wsScripts.devExpo;
345
+ // API runs always — every frontend connects to it.
346
+ devScripts["dev:api"] = wsScripts.devApi;
347
+ if (platforms.includes("web")) devScripts["dev:web"] = wsScripts.devWeb;
348
+ if (platforms.includes("expo")) devScripts["dev:expo"] = wsScripts.devExpo;
349
+ if (platforms.includes("ios")) {
350
+ // `xcodegen generate` materializes the .xcodeproj from project.yml,
351
+ // then it's an Xcode-driven flow no `bun run dev` semantics.
352
+ devScripts["dev:ios"] =
353
+ "echo 'cd apps/ios && xcodegen generate && open *.xcodeproj (or: swift run for a quick macOS preview)'";
308
354
  }
309
- if (platforms.includes("mobile")) {
310
- // Swift/iOS isn't a `bun run dev` thing — surfaced as a separate
311
- // script invocation since `swift run` blocks and Xcode is out-of-band.
312
- devScripts["dev:mobile"] = "echo 'Open apps/mobile in Xcode (or run: cd apps/mobile && swift run)'";
355
+ if (platforms.includes("mac")) {
356
+ devScripts["dev:mac"] =
357
+ "echo 'cd apps/mac && swift run (or: xcodegen generate && open *.xcodeproj)'";
313
358
  }
314
359
 
315
360
  const parallelDevs = Object.keys(devScripts);
@@ -363,13 +408,27 @@ if (!flags.skipInstall) {
363
408
  const runDev = flags.pm === "npm" ? "npm run dev" : `${flags.pm} run dev`;
364
409
 
365
410
  const platformLines = [];
411
+ platformLines.push(" → api http://localhost:4321 (Pylon control plane)");
366
412
  if (platforms.includes("web"))
367
413
  platformLines.push(" → web http://localhost:3000 (Next.js)");
368
- platformLines.push(" → api http://localhost:4321 (Pylon control plane)");
369
414
  if (platforms.includes("expo"))
370
415
  platformLines.push(` → expo ${flags.pm} run dev:expo (Metro + simulator)`);
371
- if (platforms.includes("mobile"))
372
- platformLines.push(` → mobile open apps/mobile in Xcode (or: swift run)`);
416
+ if (platforms.includes("ios"))
417
+ platformLines.push(` → ios cd apps/ios && xcodegen generate && open *.xcodeproj`);
418
+ if (platforms.includes("mac"))
419
+ platformLines.push(` → mac cd apps/mac && swift run (or xcodegen for .app)`);
420
+
421
+ const layoutLines = [" apps/api schema + functions/ handlers"];
422
+ if (platforms.includes("web")) {
423
+ layoutLines.push(" apps/web Next.js 16 + React 19 + Tailwind v4");
424
+ layoutLines.push(" packages/ui shared UI primitives");
425
+ }
426
+ if (platforms.includes("ios"))
427
+ layoutLines.push(" apps/ios Swift / SwiftUI (iOS)");
428
+ if (platforms.includes("mac"))
429
+ layoutLines.push(" apps/mac Swift / SwiftUI (macOS)");
430
+ if (platforms.includes("expo"))
431
+ layoutLines.push(" apps/expo Expo + React Native");
373
432
 
374
433
  console.log(`
375
434
  ✓ Created ${projectName}
@@ -380,10 +439,7 @@ console.log(`
380
439
  ${platformLines.join("\n")}
381
440
 
382
441
  Layout:
383
- apps/api schema + functions/ handlers
384
- ${platforms.includes("web") ? " apps/web Next.js 16 + React 19 + Tailwind v4\n packages/ui shared UI primitives" : ""}
385
- ${platforms.includes("mobile") ? " apps/mobile Swift / SwiftUI" : ""}
386
- ${platforms.includes("expo") ? " apps/expo Expo + React Native" : ""}
442
+ ${layoutLines.join("\n")}
387
443
 
388
444
  Docs: https://pylonsync.com/docs
389
445
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.53",
3
+ "version": "0.3.55",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,15 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Soft-delete: set archived = true. Project policy gates updates to
5
+ * org owner/admin only, so a regular member can't archive a project
6
+ * even if they created it. Restore by writing archived = false (this
7
+ * scaffold doesn't ship a restore action — left as an exercise).
8
+ */
9
+ export default mutation({
10
+ args: { id: v.id("Project") },
11
+ async handler(ctx, args: { id: string }) {
12
+ await ctx.db.update("Project", args.id, { archived: true });
13
+ return await ctx.db.get("Project", args.id);
14
+ },
15
+ });
@@ -0,0 +1,43 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Create a new Org with the caller as owner. Atomically writes the
5
+ * Org row + the founding Membership; both must succeed or neither
6
+ * does (Pylon mutations run in a single transaction).
7
+ */
8
+ export default mutation({
9
+ args: { slug: v.string(), name: v.string() },
10
+ async handler(ctx, args: { slug: string; name: string }) {
11
+ if (!ctx.auth.userId) {
12
+ throw ctx.error("UNAUTHENTICATED", "log in first");
13
+ }
14
+ const slug = args.slug.trim().toLowerCase();
15
+ if (!/^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/.test(slug)) {
16
+ throw ctx.error(
17
+ "INVALID_SLUG",
18
+ "slug must be 3–32 chars, alphanumeric + dashes, no leading/trailing dash",
19
+ );
20
+ }
21
+ const name = args.name.trim();
22
+ if (!name) {
23
+ throw ctx.error("EMPTY_NAME", "org name cannot be empty");
24
+ }
25
+ const dup = (await ctx.db.query("Org", { slug })) as any[];
26
+ if (dup.length > 0) {
27
+ throw ctx.error("SLUG_TAKEN", `org slug "${slug}" is taken`);
28
+ }
29
+ const orgId = await ctx.db.insert("Org", {
30
+ slug,
31
+ name,
32
+ ownerId: ctx.auth.userId,
33
+ createdAt: new Date().toISOString(),
34
+ });
35
+ await ctx.db.insert("Membership", {
36
+ orgId,
37
+ userId: ctx.auth.userId,
38
+ role: "owner",
39
+ createdAt: new Date().toISOString(),
40
+ });
41
+ return await ctx.db.get("Org", orgId);
42
+ },
43
+ });
@@ -0,0 +1,25 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Create a Project inside an Org. Project policy already gates this
5
+ * to org members; we additionally trim the name and stamp the caller
6
+ * as creator.
7
+ */
8
+ export default mutation({
9
+ args: { orgId: v.id("Org"), name: v.string() },
10
+ async handler(ctx, args: { orgId: string; name: string }) {
11
+ if (!ctx.auth.userId) {
12
+ throw ctx.error("UNAUTHENTICATED", "log in first");
13
+ }
14
+ const name = args.name.trim();
15
+ if (!name) throw ctx.error("EMPTY_NAME", "project name cannot be empty");
16
+ const id = await ctx.db.insert("Project", {
17
+ orgId: args.orgId,
18
+ name,
19
+ createdBy: ctx.auth.userId,
20
+ archived: false,
21
+ createdAt: new Date().toISOString(),
22
+ });
23
+ return await ctx.db.get("Project", id);
24
+ },
25
+ });
@@ -0,0 +1,49 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Add a User to an Org with a role. Membership policy already gates
5
+ * this to owners / admins; we additionally validate the role string
6
+ * here (Pylon enforces enum at the schema level too, but a clearer
7
+ * error message is nicer than a generic constraint failure).
8
+ *
9
+ * For a real app, replace this with an email-invite flow that emits
10
+ * an Invite row + emails the user — but the wiring is the same: the
11
+ * Membership row is what grants access.
12
+ */
13
+ const ROLES = new Set(["owner", "admin", "member"]);
14
+
15
+ export default mutation({
16
+ args: {
17
+ orgId: v.id("Org"),
18
+ userId: v.id("User"),
19
+ role: v.string(),
20
+ },
21
+ async handler(
22
+ ctx,
23
+ args: { orgId: string; userId: string; role: string },
24
+ ) {
25
+ if (!ROLES.has(args.role)) {
26
+ throw ctx.error(
27
+ "INVALID_ROLE",
28
+ `role must be one of ${[...ROLES].join(", ")}`,
29
+ );
30
+ }
31
+ const dup = (await ctx.db.query("Membership", {
32
+ orgId: args.orgId,
33
+ userId: args.userId,
34
+ })) as any[];
35
+ if (dup.length > 0) {
36
+ throw ctx.error(
37
+ "ALREADY_MEMBER",
38
+ "user is already a member of this org",
39
+ );
40
+ }
41
+ const id = await ctx.db.insert("Membership", {
42
+ orgId: args.orgId,
43
+ userId: args.userId,
44
+ role: args.role,
45
+ createdAt: new Date().toISOString(),
46
+ });
47
+ return await ctx.db.get("Membership", id);
48
+ },
49
+ });
@@ -0,0 +1,37 @@
1
+ import { query } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * List every Org the caller belongs to. Joins Membership against Org
5
+ * so the response carries everything the org switcher needs to render
6
+ * a row (name, slug, the caller's role).
7
+ */
8
+ export default query({
9
+ args: {},
10
+ async handler(ctx) {
11
+ if (!ctx.auth.userId) return [];
12
+ const memberships = await ctx.db.query("Membership", {
13
+ userId: ctx.auth.userId,
14
+ });
15
+ const out: Array<{
16
+ id: string;
17
+ slug: string;
18
+ name: string;
19
+ role: string;
20
+ createdAt: string;
21
+ }> = [];
22
+ for (const m of memberships as any[]) {
23
+ const org = (await ctx.db.get("Org", m.orgId)) as any;
24
+ if (!org) continue;
25
+ out.push({
26
+ id: org.id,
27
+ slug: org.slug,
28
+ name: org.name,
29
+ role: m.role,
30
+ createdAt: org.createdAt,
31
+ });
32
+ }
33
+ return out.sort((a, b) =>
34
+ Date.parse(b.createdAt) - Date.parse(a.createdAt),
35
+ );
36
+ },
37
+ });
@@ -0,0 +1,13 @@
1
+ import { query, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * List every member of an Org. Membership policy gates this to
5
+ * org-admins + owners (members see only their own row), so non-admin
6
+ * callers get a single-element array; admins get everyone.
7
+ */
8
+ export default query({
9
+ args: { orgId: v.id("Org") },
10
+ async handler(ctx, args: { orgId: string }) {
11
+ return await ctx.db.query("Membership", { orgId: args.orgId });
12
+ },
13
+ });
@@ -0,0 +1,18 @@
1
+ import { query, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Every Project in an Org. Project policy enforces tenant scope, so
5
+ * even if the caller passes a foreign `orgId` they're not a member of,
6
+ * the query returns nothing.
7
+ */
8
+ export default query({
9
+ args: { orgId: v.id("Org") },
10
+ async handler(ctx, args: { orgId: string }) {
11
+ const rows = (await ctx.db.query("Project", {
12
+ orgId: args.orgId,
13
+ })) as any[];
14
+ return rows
15
+ .filter((r) => !r.archived)
16
+ .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
17
+ },
18
+ });
@@ -0,0 +1,29 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Remove a User from an Org. Refuses to remove the org's owner
5
+ * (transfer ownership first via setMemberRole + a manual Org.ownerId
6
+ * update — left as an exercise; this scaffold stops at the safety
7
+ * check).
8
+ */
9
+ export default mutation({
10
+ args: { orgId: v.id("Org"), userId: v.id("User") },
11
+ async handler(ctx, args: { orgId: string; userId: string }) {
12
+ const org = (await ctx.db.get("Org", args.orgId)) as any;
13
+ if (!org) throw ctx.error("NOT_FOUND", "org not found");
14
+ if (org.ownerId === args.userId) {
15
+ throw ctx.error(
16
+ "OWNER_CANT_LEAVE",
17
+ "the org owner can't be removed; transfer ownership first",
18
+ );
19
+ }
20
+ const memberships = (await ctx.db.query("Membership", {
21
+ orgId: args.orgId,
22
+ userId: args.userId,
23
+ })) as any[];
24
+ for (const m of memberships) {
25
+ await ctx.db.delete("Membership", m.id);
26
+ }
27
+ return memberships[0] ?? null;
28
+ },
29
+ });
@@ -0,0 +1,38 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ const ROLES = new Set(["owner", "admin", "member"]);
4
+
5
+ /**
6
+ * Promote/demote a member. Only one row per (orgId, userId) so we
7
+ * find by composite then update. Setting role=owner does NOT change
8
+ * Org.ownerId — those are separate concepts (the founder vs current
9
+ * org-leadership members); this scaffold leaves transfer-of-ownership
10
+ * as a separate flow.
11
+ */
12
+ export default mutation({
13
+ args: {
14
+ orgId: v.id("Org"),
15
+ userId: v.id("User"),
16
+ role: v.string(),
17
+ },
18
+ async handler(
19
+ ctx,
20
+ args: { orgId: string; userId: string; role: string },
21
+ ) {
22
+ if (!ROLES.has(args.role)) {
23
+ throw ctx.error(
24
+ "INVALID_ROLE",
25
+ `role must be one of ${[...ROLES].join(", ")}`,
26
+ );
27
+ }
28
+ const memberships = (await ctx.db.query("Membership", {
29
+ orgId: args.orgId,
30
+ userId: args.userId,
31
+ })) as any[];
32
+ if (memberships.length === 0) {
33
+ throw ctx.error("NOT_A_MEMBER", "user is not a member of this org");
34
+ }
35
+ await ctx.db.update("Membership", memberships[0].id, { role: args.role });
36
+ return await ctx.db.get("Membership", memberships[0].id);
37
+ },
38
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/api",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev schema.ts --port 4321",
8
+ "build": "pylon codegen schema.ts --out pylon.manifest.json && pylon codegen client pylon.manifest.json --out pylon.client.ts",
9
+ "schema:push": "pylon schema push pylon.manifest.json --sqlite dev.db",
10
+ "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
+ },
12
+ "dependencies": {
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/functions": "^__PYLON_VERSION__"
15
+ },
16
+ "devDependencies": {
17
+ "@pylonsync/cli": "^__PYLON_VERSION__",
18
+ "typescript": "^5.5.0"
19
+ }
20
+ }