create-tilcayo-app 0.0.1 → 0.0.3
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/README.md +294 -49
- package/dist/bin.js +13 -4
- package/dist/fullstack.d.ts +2 -0
- package/dist/fullstack.js +107 -0
- package/dist/index.js +38 -10
- package/dist/installTenancy.d.ts +2 -0
- package/dist/installTenancy.js +86 -0
- package/dist/options.d.ts +4 -2
- package/dist/options.js +38 -14
- package/dist/react-auth.json +83 -0
- package/dist/react-starter.json +75 -0
- package/dist/templates.js +113 -74
- package/package.json +27 -26
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Applied only after generating a full-stack auth/admin app. Also used for explicit upgrades. */
|
|
4
|
+
export async function installTenancy(root) {
|
|
5
|
+
const edit = async (relative, update) => {
|
|
6
|
+
const file = path.join(root, relative);
|
|
7
|
+
await writeFile(file, update(await readFile(file, 'utf8')));
|
|
8
|
+
};
|
|
9
|
+
for (const part of ['', 'api', 'client'])
|
|
10
|
+
await edit(path.join(part, 'package.json'), source => {
|
|
11
|
+
const pkg = JSON.parse(source);
|
|
12
|
+
pkg.tilcayo = { ...pkg.tilcayo, tenancy: true };
|
|
13
|
+
if (part === 'api')
|
|
14
|
+
pkg.scripts['tenancy:setup'] = 'node --env-file-if-exists=.env dist/scripts/setup-tenancy.js';
|
|
15
|
+
if (!part)
|
|
16
|
+
pkg.scripts['tenancy:setup'] = 'npm run build --workspace api && npm run tenancy:setup --workspace api';
|
|
17
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
18
|
+
});
|
|
19
|
+
await edit('api/src/auth.ts', source => source.includes('tenancy:') ? source : source.replace('createAuth({', 'createAuth({\n tenancy: { baseDomain: process.env.TENANCY_BASE_DOMAIN || "localhost" },'));
|
|
20
|
+
await edit('api/src/app.ts', source => {
|
|
21
|
+
if (source.includes('auth.tenantMiddleware'))
|
|
22
|
+
return source;
|
|
23
|
+
return source.replace('middleware: [', 'middleware: [auth.tenantMiddleware, auth.tenantAccessMiddleware, ').replace('app.routes(auth.adminRoutes);', 'app.routes(auth.adminRoutes);\napp.routes(auth.tenancyRoutes);');
|
|
24
|
+
});
|
|
25
|
+
const models = [];
|
|
26
|
+
for (const filename of await readdir(path.join(root, 'api/src/models'))) {
|
|
27
|
+
if (!filename.endsWith('.ts'))
|
|
28
|
+
continue;
|
|
29
|
+
await edit('api/src/models/' + filename, source => {
|
|
30
|
+
if (!source.includes('mongoModel('))
|
|
31
|
+
return source;
|
|
32
|
+
models.push(filename.slice(0, -3));
|
|
33
|
+
return source.includes('tenantScoped: true') ? source : source.replace(/mongoModel\((['"][^'"]+['"]),\s*(\w+)\)/g, (match, name, schema) => schema === 'platformSchema' ? match : 'mongoModel(' + name + ', ' + schema + ', { tenantScoped: true })');
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
await edit('client/vite.config.ts', source => source.includes('allowedHosts:') ? source : source.replace('server: {', "server: { host: '127.0.0.1', allowedHosts: ['.localhost'],"));
|
|
37
|
+
await edit('client/src/components/navigation.tsx', source => {
|
|
38
|
+
let text = source.replaceAll('WorkspaceSwitcher', 'WorkspaceIdentity');
|
|
39
|
+
if (!text.includes('<WorkspaceIdentity'))
|
|
40
|
+
text = "import { WorkspaceIdentity } from '../features/tenancy/workspace-switcher';\n" + text.replace('<NavLink to="/dashboard"', '<WorkspaceIdentity />\n <NavLink to="/dashboard"');
|
|
41
|
+
return text;
|
|
42
|
+
});
|
|
43
|
+
await edit('client/src/components/session-skeleton.tsx', source => source.includes('useGetWorkspaceQuery') ? source : "import { useGetWorkspaceQuery } from '../features/tenancy/tenancy.api';\n" + source.replace(' const { pathname }', ' useGetWorkspaceQuery();\n const { pathname }'));
|
|
44
|
+
await edit('client/src/layouts/auth-layout.tsx', source => source.includes('<WorkspaceIdentity') ? source : "import { WorkspaceIdentity } from '../features/tenancy/workspace-switcher';\n" + source.replace('<BrandMark />Tilcayo</Link></div>', '<BrandMark />Tilcayo</Link><WorkspaceIdentity /></div>'));
|
|
45
|
+
await edit('client/src/layouts/app-layout.tsx', source => source.includes('<WorkspaceGate>') ? source : "import { WorkspaceGate } from '../features/tenancy/workspace-switcher';\n" + source.replace('<Outlet />', '<WorkspaceGate><Outlet /></WorkspaceGate>'));
|
|
46
|
+
await edit('client/src/features/admin/admin-layout.tsx', source => {
|
|
47
|
+
let text = source.includes('WorkspaceGate') ? source.replaceAll('WorkspaceSwitcher', 'WorkspaceIdentity').replace('{ WorkspaceGate }', '{ WorkspaceGate, WorkspaceIdentity }') : "import { WorkspaceGate, WorkspaceIdentity } from '../tenancy/workspace-switcher';\n" + source;
|
|
48
|
+
text = text.replace("import { useLocation } from 'react-router';", "import { Outlet, useLocation } from 'react-router';");
|
|
49
|
+
if (!text.includes('<WorkspaceGate>'))
|
|
50
|
+
text = text.replace('sections={sections.filter(section => can(section.tab))} />;', 'sections={sections.filter(section => can(section.tab))}><WorkspaceGate><Outlet /></WorkspaceGate></AdminLayout>;');
|
|
51
|
+
return text.includes('accountContent=') ? text : text.replace('<AdminLayout user={user}', '<AdminLayout accountContent={<WorkspaceIdentity />} user={user}');
|
|
52
|
+
});
|
|
53
|
+
// Demo writes must explicitly enter the selected workspace, never the platform.
|
|
54
|
+
const demoFile = 'api/src/scripts/seed-admin-demo.ts';
|
|
55
|
+
try {
|
|
56
|
+
await edit(demoFile, source => source.includes('runInTenantScope') ? source : "import { Tenant } from '@tilcayo/auth';\nimport { runInTenantScope } from '@tilcayo/core';\n" + source.replace(' try {', " try {\n const slug = process.env.TENANCY_SEED_SLUG;\n if (!slug) throw new Error('Set TENANCY_SEED_SLUG to a tenant created in Tenant administration.');\n const tenant = await Tenant.firstOrFail({ slug });\n await runInTenantScope({ enabled: true, tenantId: String(tenant._id) }, async () => {").replace(' } finally {', ' }); } finally {'));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code !== 'ENOENT')
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
await mkdir(path.join(root, 'api/src/scripts'), { recursive: true });
|
|
63
|
+
await writeFile(path.join(root, 'api/src/scripts/setup-tenancy.ts'), `import { connectMongo, disconnectMongo } from '@tilcayo/core';
|
|
64
|
+
import { bootstrapTenancy } from '@tilcayo/auth';
|
|
65
|
+
${models.map(name => `import { ${name} } from '../models/${name}.js';`).join('\n')}
|
|
66
|
+
|
|
67
|
+
async function main() {
|
|
68
|
+
const email = process.env.TENANCY_SUPERADMIN_EMAIL;
|
|
69
|
+
const password = process.env.TENANCY_SUPERADMIN_PASSWORD;
|
|
70
|
+
if (!email || !password) throw new Error('Set TENANCY_SUPERADMIN_EMAIL and TENANCY_SUPERADMIN_PASSWORD for this setup command.');
|
|
71
|
+
await connectMongo(process.env.MONGODB_URI || '', { serverSelectionTimeoutMS: 10000, autoIndex: false });
|
|
72
|
+
try {
|
|
73
|
+
await bootstrapTenancy({ email, password });
|
|
74
|
+
// Setup does not create a tenant or change ownership of existing records.
|
|
75
|
+
// Driver access is limited to this offline setup command, not request handlers.
|
|
76
|
+
for (const model of [${models.join(', ')}]) {
|
|
77
|
+
const indexes = await model.raw.collection.indexes().catch(error => { if (error.code === 26) return []; throw error; });
|
|
78
|
+
for (const index of indexes) if (index.unique && index.name === 'sku_1' && Object.keys(index.key).length === 1) await model.raw.collection.dropIndex(index.name!);
|
|
79
|
+
await model.raw.createIndexes();
|
|
80
|
+
}
|
|
81
|
+
console.log('Super Admin ready. Create tenants and assign administrators in Tenant administration on the main domain.');
|
|
82
|
+
} finally { await disconnectMongo(); }
|
|
83
|
+
}
|
|
84
|
+
main().catch(() => { console.error('Tenancy setup failed. Check bootstrap inputs, MongoDB connectivity and workspace conflicts.'); process.exitCode = 1; });
|
|
85
|
+
`);
|
|
86
|
+
}
|
package/dist/options.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export interface StarterOptions {
|
|
2
2
|
name: string;
|
|
3
3
|
packageManager: "npm" | "pnpm" | "yarn";
|
|
4
|
-
type: "api" | "minimal";
|
|
4
|
+
type: "api" | "minimal" | "react" | "fullstack";
|
|
5
5
|
auth: boolean;
|
|
6
|
+
admin?: boolean;
|
|
7
|
+
tenancy?: boolean;
|
|
6
8
|
install: boolean;
|
|
7
9
|
}
|
|
8
|
-
export declare const help = "Usage: create-tilcayo-app [project-name] [options]\n\n --package-manager npm|pnpm|yarn\n --type api|minimal
|
|
10
|
+
export declare const help = "Usage: create-tilcayo-app [project-name] [options]\n\n --package-manager npm|pnpm|yarn\n --type api|minimal|react|fullstack\n --api / --fullstack Shortcuts for application type\n --auth / --no-auth Include backend auth or React auth pages and route guards\n --admin Add an admin shell (requires --auth and React/fullstack)\n --tenancy Subdomain workspaces (requires --fullstack --auth --admin)\n --no-install Generate files without installing dependencies\n --yes Use defaults for unanswered choices\n --help Show this help\n\nDefaults: my-api, npm, api, no authentication, install dependencies.\nInteractive terminals prompt for missing choices. Automation uses defaults.\nExisting directories are never overwritten.";
|
|
9
11
|
export declare function validateOptions(options: StarterOptions): void;
|
|
10
12
|
export declare function parseOptions(args: string[]): {
|
|
11
13
|
options: StarterOptions;
|
package/dist/options.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
export const help = `Usage: create-tilcayo-app [project-name] [options]
|
|
2
|
-
|
|
3
|
-
--package-manager npm|pnpm|yarn
|
|
4
|
-
--type api|minimal
|
|
5
|
-
--
|
|
6
|
-
--no-
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
export const help = `Usage: create-tilcayo-app [project-name] [options]
|
|
2
|
+
|
|
3
|
+
--package-manager npm|pnpm|yarn
|
|
4
|
+
--type api|minimal|react|fullstack
|
|
5
|
+
--api / --fullstack Shortcuts for application type
|
|
6
|
+
--auth / --no-auth Include backend auth or React auth pages and route guards
|
|
7
|
+
--admin Add an admin shell (requires --auth and React/fullstack)
|
|
8
|
+
--tenancy Subdomain workspaces (requires --fullstack --auth --admin)
|
|
9
|
+
--no-install Generate files without installing dependencies
|
|
10
|
+
--yes Use defaults for unanswered choices
|
|
11
|
+
--help Show this help
|
|
12
|
+
|
|
13
|
+
Defaults: my-api, npm, api, no authentication, install dependencies.
|
|
14
|
+
Interactive terminals prompt for missing choices. Automation uses defaults.
|
|
12
15
|
Existing directories are never overwritten.`;
|
|
13
16
|
export function validateOptions(options) {
|
|
14
17
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.name) || options.name.length > 214 || ["node_modules", "con", "prn", "aux", "nul"].includes(options.name) || /^(com|lpt)[0-9]$/.test(options.name)) {
|
|
@@ -16,8 +19,16 @@ export function validateOptions(options) {
|
|
|
16
19
|
}
|
|
17
20
|
if (!["npm", "pnpm", "yarn"].includes(options.packageManager))
|
|
18
21
|
throw new Error("Package manager must be npm, pnpm, or yarn.");
|
|
19
|
-
if (!["api", "minimal"].includes(options.type))
|
|
20
|
-
throw new Error("Application type must be api or
|
|
22
|
+
if (!["api", "minimal", "react", "fullstack"].includes(options.type))
|
|
23
|
+
throw new Error("Application type must be api, minimal, react, or fullstack.");
|
|
24
|
+
if (options.admin !== undefined && typeof options.admin !== "boolean")
|
|
25
|
+
throw new Error("Admin choice must be a boolean.");
|
|
26
|
+
if (options.admin && (!options.auth || !["react", "fullstack"].includes(options.type)))
|
|
27
|
+
throw new Error("Admin requires --auth and a React or fullstack application.");
|
|
28
|
+
if (options.tenancy !== undefined && typeof options.tenancy !== "boolean")
|
|
29
|
+
throw new Error("Tenancy choice must be a boolean.");
|
|
30
|
+
if (options.tenancy && (options.type !== "fullstack" || !options.auth || !options.admin))
|
|
31
|
+
throw new Error("Tenancy requires --fullstack --auth --admin.");
|
|
21
32
|
if (typeof options.auth !== "boolean" || typeof options.install !== "boolean")
|
|
22
33
|
throw new Error("Authentication and install choices must be booleans.");
|
|
23
34
|
}
|
|
@@ -35,7 +46,7 @@ export function parseOptions(args) {
|
|
|
35
46
|
missing.delete("name");
|
|
36
47
|
continue;
|
|
37
48
|
}
|
|
38
|
-
const key = arg === "--no-auth" ? "--auth" : arg;
|
|
49
|
+
const key = arg === "--no-auth" ? "--auth" : ["--api", "--fullstack"].includes(arg) ? "--type" : arg;
|
|
39
50
|
if (seen.has(key))
|
|
40
51
|
throw new Error(`Duplicate or conflicting option: ${arg}`);
|
|
41
52
|
seen.add(key);
|
|
@@ -47,6 +58,19 @@ export function parseOptions(args) {
|
|
|
47
58
|
options.install = false;
|
|
48
59
|
continue;
|
|
49
60
|
}
|
|
61
|
+
if (arg === "--tenancy") {
|
|
62
|
+
options.tenancy = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--admin") {
|
|
66
|
+
options.admin = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === "--api" || arg === "--fullstack") {
|
|
70
|
+
options.type = arg === "--api" ? "api" : "fullstack";
|
|
71
|
+
missing.delete("type");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
50
74
|
if (arg === "--auth" || arg === "--no-auth") {
|
|
51
75
|
options.auth = arg === "--auth";
|
|
52
76
|
missing.delete("auth");
|