@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.
- package/bin/create-pylon.js +98 -42
- package/package.json +1 -1
- package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
- package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
- package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
- package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
- package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
- package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
- package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
- package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
- package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
- package/templates/backend/b2b/apps/api/package.json +20 -0
- package/templates/backend/b2b/apps/api/schema.ts +171 -0
- package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
- package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
- package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
- package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
- package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
- package/templates/backend/chat/apps/api/package.json +20 -0
- package/templates/backend/chat/apps/api/schema.ts +93 -0
- package/templates/backend/chat/apps/api/tsconfig.json +13 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
- package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
- package/templates/backend/consumer/apps/api/package.json +20 -0
- package/templates/backend/consumer/apps/api/schema.ts +130 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
- package/templates/expo/chat/apps/expo/App.tsx +384 -0
- package/templates/expo/chat/apps/expo/app.json +25 -0
- package/templates/expo/chat/apps/expo/babel.config.js +6 -0
- package/templates/expo/chat/apps/expo/package.json +30 -0
- package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
- package/templates/expo/consumer/apps/expo/App.tsx +392 -0
- package/templates/expo/consumer/apps/expo/app.json +25 -0
- package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
- package/templates/expo/consumer/apps/expo/package.json +30 -0
- package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
- package/templates/ios/chat/apps/ios/Package.swift +24 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +116 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
- package/templates/ios/chat/apps/ios/project.yml +44 -0
- package/templates/ios/consumer/apps/ios/Package.swift +24 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +57 -0
- package/templates/ios/consumer/apps/ios/project.yml +44 -0
- package/templates/mac/b2b/apps/mac/Package.swift +22 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/b2b/apps/mac/project.yml +34 -0
- package/templates/mac/barebones/apps/mac/Package.swift +33 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/barebones/apps/mac/project.yml +34 -0
- package/templates/mac/chat/apps/mac/Package.swift +34 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +143 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +136 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +58 -0
- package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/chat/apps/mac/project.yml +36 -0
- package/templates/mac/consumer/apps/mac/Package.swift +34 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +199 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +34 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +68 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +40 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +59 -0
- package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/consumer/apps/mac/project.yml +36 -0
- package/templates/mac/todo/apps/mac/Package.swift +33 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/todo/apps/mac/project.yml +34 -0
- package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
- package/templates/web/b2b/apps/web/next.config.ts +24 -0
- package/templates/web/b2b/apps/web/package.json +29 -0
- package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
- package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
- package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
- package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
- package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/b2b/apps/web/tsconfig.json +26 -0
- package/templates/web/chat/apps/web/next-env.d.ts +2 -0
- package/templates/web/chat/apps/web/next.config.ts +24 -0
- package/templates/web/chat/apps/web/package.json +29 -0
- package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
- package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +231 -0
- package/templates/web/chat/apps/web/src/app/globals.css +6 -0
- package/templates/web/chat/apps/web/src/app/layout.tsx +22 -0
- package/templates/web/chat/apps/web/src/app/page.tsx +12 -0
- package/templates/web/chat/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/chat/apps/web/tsconfig.json +26 -0
- package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
- package/templates/web/consumer/apps/web/next.config.ts +24 -0
- package/templates/web/consumer/apps/web/package.json +29 -0
- package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
- package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +315 -0
- package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
- package/templates/web/consumer/apps/web/src/app/layout.tsx +22 -0
- package/templates/web/consumer/apps/web/src/app/page.tsx +21 -0
- package/templates/web/consumer/apps/web/src/app/providers.tsx +25 -0
- package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/consumer/apps/web/tsconfig.json +26 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Package.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/ContentView.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
- /package/templates/{mobile/barebones/apps/mobile → ios/barebones/apps/ios}/project.yml +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Package.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/Models.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/TodoListView.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +0 -0
- /package/templates/{mobile/todo/apps/mobile → ios/todo/apps/ios}/project.yml +0 -0
package/bin/create-pylon.js
CHANGED
|
@@ -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
|
|
63
|
-
|
|
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>
|
|
102
|
-
|
|
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,
|
|
109
|
-
npm create @pylonsync/pylon my-app --template
|
|
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
|
|
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
|
|
264
|
-
// 2. backend/<
|
|
265
|
-
// 3. ui
|
|
266
|
-
// 4.
|
|
267
|
-
// 5.
|
|
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
|
-
|
|
280
|
-
copyTemplate(
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
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("
|
|
310
|
-
|
|
311
|
-
|
|
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("
|
|
372
|
-
platformLines.push(` →
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|