@podosoft/podokit 0.11.1 → 0.12.0
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 +7 -3
- package/dist/add.d.ts +16 -0
- package/dist/add.js +63 -20
- package/dist/assemble.js +9 -9
- package/dist/dev.d.ts +26 -0
- package/dist/dev.js +297 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +82 -2
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +7 -1
- package/dist/locale.d.ts +22 -0
- package/dist/locale.js +184 -0
- package/dist/lockfile.d.ts +5 -3
- package/dist/lockfile.js +21 -5
- package/dist/remove.js +26 -23
- package/dist/templates/fullstack-nest-svelte/README.md +42 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/scripts/.keep +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/README.md +7 -5
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/catalogs/app/en.json +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/catalogs/app/ko.json +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/locales/en.json +6 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/locales/ko.json +6 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/vite.config.ts +3 -9
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +8 -11
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/SKILL.md +63 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/agents/openai.yaml +4 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-add-locale/references/catalog-format.md +49 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.docker +2 -2
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +3 -0
- package/dist/templates/fullstack-nest-svelte/dot-podokit/dev.json +4 -0
- package/dist/templates/fullstack-nest-svelte/tests/README.md +10 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +187 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/auth-errors.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/auth-redirect.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +8 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +564 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +564 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +13 -12
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +90 -722
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +4 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +4 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +8 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +15 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +4 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +32 -35
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +10 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +11 -7
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +78 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +28 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +59 -6
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +5 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/module.manifest.json +14 -3
- package/dist/templates/modules/auth/files/apps/api/scripts/configure-auth.mjs +22 -11
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +6 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +50 -25
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/SKILL.md +15 -2
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/agents/openai.yaml +2 -2
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +51 -0
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/google.md +44 -0
- package/dist/templates/modules/auth/module.manifest.json +3 -0
- package/dist/templates/todo/README.md +42 -0
- package/dist/templates/todo/apps/api/Dockerfile +1 -0
- package/dist/templates/todo/apps/api/scripts/.keep +1 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/README.md +7 -5
- package/dist/templates/todo/apps/web/src/lib/i18n/catalogs/app/en.json +1 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/catalogs/app/ko.json +1 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/locales/en.json +6 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/locales/ko.json +6 -0
- package/dist/templates/todo/apps/web/vite.config.ts +3 -9
- package/dist/templates/todo/compose.dev.yaml +8 -11
- package/dist/templates/todo/dot-claude/skills/podokit-add-locale/SKILL.md +63 -0
- package/dist/templates/todo/dot-claude/skills/podokit-add-locale/agents/openai.yaml +4 -0
- package/dist/templates/todo/dot-claude/skills/podokit-add-locale/references/catalog-format.md +49 -0
- package/dist/templates/todo/dot-env.docker +2 -2
- package/dist/templates/todo/dot-gitignore +3 -0
- package/dist/templates/todo/dot-podokit/dev.json +4 -0
- package/dist/templates/todo/tests/README.md +10 -4
- package/dist/update.js +48 -7
- package/package.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/en.ts +0 -10
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/ko.ts +0 -11
- package/dist/templates/todo/apps/web/src/lib/i18n/en.ts +0 -10
- package/dist/templates/todo/apps/web/src/lib/i18n/ko.ts +0 -11
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ const remove_1 = require("./remove");
|
|
|
12
12
|
const inspect_1 = require("./inspect");
|
|
13
13
|
const update_1 = require("./update");
|
|
14
14
|
const eject_1 = require("./eject");
|
|
15
|
+
const dev_1 = require("./dev");
|
|
16
|
+
const locale_1 = require("./locale");
|
|
15
17
|
const HELP = `podo — PodoKit project generator
|
|
16
18
|
|
|
17
19
|
Usage:
|
|
@@ -21,6 +23,8 @@ Usage:
|
|
|
21
23
|
podo status Show version, modules, file tiers, and local edits
|
|
22
24
|
podo diff List PodoKit-managed files you have edited
|
|
23
25
|
podo doctor Check framework versions against supported ranges
|
|
26
|
+
podo dev <action> [...] Run the shared, portless container development gateway
|
|
27
|
+
podo locale <command> Add, validate, activate, or list JSON locales
|
|
24
28
|
podo update [--apply] Preview (or apply) what a version update would change
|
|
25
29
|
podo eject <path...> Take ownership of managed files (update skips them)
|
|
26
30
|
|
|
@@ -28,6 +32,8 @@ Options:
|
|
|
28
32
|
--template <t> Template to scaffold (see below)
|
|
29
33
|
--dir <path> Target directory (default: ./<name>)
|
|
30
34
|
--pm <name> Package manager: npm | pnpm | yarn (default: npm)
|
|
35
|
+
--name <label> Display name for a locale
|
|
36
|
+
--direction <direction> Text direction: ltr | rtl (default: ltr)
|
|
31
37
|
--adopt Adopt colliding paths explicitly declared managed by a module
|
|
32
38
|
--no-ai Skip AI agent guidance (AGENTS.md, CLAUDE.md, editor rules)
|
|
33
39
|
-y, --yes Skip prompts and accept defaults
|
|
@@ -42,7 +48,14 @@ Example:
|
|
|
42
48
|
cd my-app && npx @podosoft/podokit add auth
|
|
43
49
|
`;
|
|
44
50
|
function parseArgs(argv) {
|
|
45
|
-
const parsed = {
|
|
51
|
+
const parsed = {
|
|
52
|
+
help: false,
|
|
53
|
+
yes: false,
|
|
54
|
+
apply: false,
|
|
55
|
+
adopt: false,
|
|
56
|
+
ai: true,
|
|
57
|
+
positionals: [],
|
|
58
|
+
};
|
|
46
59
|
const positionals = [];
|
|
47
60
|
for (let i = 0; i < argv.length; i += 1) {
|
|
48
61
|
const arg = argv[i];
|
|
@@ -73,12 +86,23 @@ function parseArgs(argv) {
|
|
|
73
86
|
else if (arg === "--pm") {
|
|
74
87
|
parsed.pm = argv[++i];
|
|
75
88
|
}
|
|
89
|
+
else if (arg === "--name") {
|
|
90
|
+
parsed.localeName = argv[++i];
|
|
91
|
+
}
|
|
92
|
+
else if (arg === "--direction") {
|
|
93
|
+
const direction = argv[++i];
|
|
94
|
+
if (direction !== "ltr" && direction !== "rtl") {
|
|
95
|
+
throw new Error('--direction must be either "ltr" or "rtl".');
|
|
96
|
+
}
|
|
97
|
+
parsed.localeDirection = direction;
|
|
98
|
+
}
|
|
76
99
|
else if (arg !== undefined && !arg.startsWith("-")) {
|
|
77
100
|
positionals.push(arg);
|
|
78
101
|
}
|
|
79
102
|
}
|
|
80
103
|
parsed.command = positionals[0];
|
|
81
104
|
parsed.name = positionals[1];
|
|
105
|
+
parsed.positionals = positionals;
|
|
82
106
|
return parsed;
|
|
83
107
|
}
|
|
84
108
|
function fail(message) {
|
|
@@ -203,6 +227,62 @@ async function main(argv) {
|
|
|
203
227
|
}
|
|
204
228
|
return;
|
|
205
229
|
}
|
|
230
|
+
if (args.command === "dev") {
|
|
231
|
+
try {
|
|
232
|
+
(0, dev_1.runDevCommand)(process.cwd(), args.name, argv.slice(2));
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
fail(err.message);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (args.command === "locale") {
|
|
240
|
+
const action = args.positionals[1] ?? "list";
|
|
241
|
+
const code = args.positionals[2];
|
|
242
|
+
try {
|
|
243
|
+
if (action === "add") {
|
|
244
|
+
if (!code)
|
|
245
|
+
fail("Usage: podo locale add <code> [--name <label>] [--direction ltr|rtl]");
|
|
246
|
+
const definition = (0, locale_1.addLocale)(process.cwd(), code, {
|
|
247
|
+
name: args.localeName,
|
|
248
|
+
direction: args.localeDirection,
|
|
249
|
+
});
|
|
250
|
+
process.stdout.write(`Added ${definition.code} (${definition.name}) as inactive. Translate catalogs, run ` +
|
|
251
|
+
`"podo locale validate ${definition.code}", then activate it.\n`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (action === "validate") {
|
|
255
|
+
const locales = code ? [code] : (0, locale_1.listLocales)(process.cwd()).map((locale) => locale.code);
|
|
256
|
+
for (const locale of locales) {
|
|
257
|
+
const coverage = (0, locale_1.validateLocale)(process.cwd(), locale);
|
|
258
|
+
process.stdout.write(`${coverage.definition.code.padEnd(10)} ${String(coverage.percent).padStart(3)}% ` +
|
|
259
|
+
`(${coverage.translated}/${coverage.total})` +
|
|
260
|
+
`${coverage.missing.length ? ` missing ${coverage.missing.length}` : ""}\n`);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (action === "activate" || action === "deactivate") {
|
|
265
|
+
if (!code)
|
|
266
|
+
fail(`Usage: podo locale ${action} <code>`);
|
|
267
|
+
const coverage = (0, locale_1.setLocaleEnabled)(process.cwd(), code, action === "activate");
|
|
268
|
+
process.stdout.write(`${action === "activate" ? "Activated" : "Deactivated"} ${coverage.definition.code} ` +
|
|
269
|
+
`(${coverage.percent}% translated; missing keys use the configured fallback).\n`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (action === "list") {
|
|
273
|
+
for (const locale of (0, locale_1.listLocales)(process.cwd())) {
|
|
274
|
+
const coverage = (0, locale_1.validateLocale)(process.cwd(), locale.code);
|
|
275
|
+
process.stdout.write(`${locale.enabled ? "active " : "inactive"} ${locale.code.padEnd(10)} ` +
|
|
276
|
+
`${locale.name} ${coverage.percent}%\n`);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
fail(`Unknown locale command "${action}". Use add, validate, activate, deactivate, or list.`);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
fail(err.message);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
206
286
|
if (args.command === "update") {
|
|
207
287
|
const templatesDir = (0, node_path_1.join)(__dirname, "templates");
|
|
208
288
|
try {
|
|
@@ -238,7 +318,7 @@ async function main(argv) {
|
|
|
238
318
|
return;
|
|
239
319
|
}
|
|
240
320
|
if (args.command === "eject") {
|
|
241
|
-
const targets =
|
|
321
|
+
const targets = args.positionals.slice(1);
|
|
242
322
|
if (!targets.length) {
|
|
243
323
|
fail("Usage: podo eject <path...>");
|
|
244
324
|
}
|
package/dist/lib.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { status, diff, doctor, SUPPORTED_FRAMEWORKS } from "./inspect";
|
|
|
8
8
|
export type { StatusReport, DoctorFinding } from "./inspect";
|
|
9
9
|
export { planUpdate, summarize } from "./update";
|
|
10
10
|
export type { UpdatePlan, FileChange } from "./update";
|
|
11
|
+
export { readDevConfig, renderRoute, renderRuntimeCompose, resolveDevRuntime, runDevCommand } from "./dev";
|
|
12
|
+
export type { CommandRunner, DevConfig, DevRuntime } from "./dev";
|
|
11
13
|
/** Absolute path to the templates bundled in this installed package. */
|
|
12
14
|
export declare function builtinTemplatesDir(): string;
|
|
13
15
|
/** Absolute path to the modules bundled in this installed package. */
|
package/dist/lib.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.summarize = exports.planUpdate = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.create = void 0;
|
|
3
|
+
exports.runDevCommand = exports.resolveDevRuntime = exports.renderRuntimeCompose = exports.renderRoute = exports.readDevConfig = exports.summarize = exports.planUpdate = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.create = void 0;
|
|
4
4
|
exports.builtinTemplatesDir = builtinTemplatesDir;
|
|
5
5
|
exports.builtinModulesDir = builtinModulesDir;
|
|
6
6
|
// Library entry point: the reusable functions behind the `podo` CLI, for other
|
|
@@ -24,6 +24,12 @@ Object.defineProperty(exports, "SUPPORTED_FRAMEWORKS", { enumerable: true, get:
|
|
|
24
24
|
var update_1 = require("./update");
|
|
25
25
|
Object.defineProperty(exports, "planUpdate", { enumerable: true, get: function () { return update_1.planUpdate; } });
|
|
26
26
|
Object.defineProperty(exports, "summarize", { enumerable: true, get: function () { return update_1.summarize; } });
|
|
27
|
+
var dev_1 = require("./dev");
|
|
28
|
+
Object.defineProperty(exports, "readDevConfig", { enumerable: true, get: function () { return dev_1.readDevConfig; } });
|
|
29
|
+
Object.defineProperty(exports, "renderRoute", { enumerable: true, get: function () { return dev_1.renderRoute; } });
|
|
30
|
+
Object.defineProperty(exports, "renderRuntimeCompose", { enumerable: true, get: function () { return dev_1.renderRuntimeCompose; } });
|
|
31
|
+
Object.defineProperty(exports, "resolveDevRuntime", { enumerable: true, get: function () { return dev_1.resolveDevRuntime; } });
|
|
32
|
+
Object.defineProperty(exports, "runDevCommand", { enumerable: true, get: function () { return dev_1.runDevCommand; } });
|
|
27
33
|
/** Absolute path to the templates bundled in this installed package. */
|
|
28
34
|
function builtinTemplatesDir() {
|
|
29
35
|
return (0, node_path_1.join)(__dirname, "templates");
|
package/dist/locale.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type LocaleDirection = "ltr" | "rtl";
|
|
2
|
+
export interface LocaleDefinition {
|
|
3
|
+
code: string;
|
|
4
|
+
name: string;
|
|
5
|
+
direction: LocaleDirection;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface LocaleCoverage {
|
|
9
|
+
definition: LocaleDefinition;
|
|
10
|
+
translated: number;
|
|
11
|
+
total: number;
|
|
12
|
+
percent: number;
|
|
13
|
+
missing: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function normalizeLocaleCode(value: string): string;
|
|
16
|
+
export declare function listLocales(projectRoot: string): LocaleDefinition[];
|
|
17
|
+
export declare function validateLocale(projectRoot: string, rawCode: string): LocaleCoverage;
|
|
18
|
+
export declare function addLocale(projectRoot: string, rawCode: string, options?: {
|
|
19
|
+
name?: string;
|
|
20
|
+
direction?: LocaleDirection;
|
|
21
|
+
}): LocaleDefinition;
|
|
22
|
+
export declare function setLocaleEnabled(projectRoot: string, rawCode: string, enabled: boolean): LocaleCoverage;
|
package/dist/locale.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeLocaleCode = normalizeLocaleCode;
|
|
4
|
+
exports.listLocales = listLocales;
|
|
5
|
+
exports.validateLocale = validateLocale;
|
|
6
|
+
exports.addLocale = addLocale;
|
|
7
|
+
exports.setLocaleEnabled = setLocaleEnabled;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const WEB_I18N = (0, node_path_1.join)("apps", "web", "src", "lib", "i18n");
|
|
11
|
+
function definitionsDir(projectRoot) {
|
|
12
|
+
return (0, node_path_1.join)(projectRoot, WEB_I18N, "locales");
|
|
13
|
+
}
|
|
14
|
+
function catalogsDir(projectRoot) {
|
|
15
|
+
return (0, node_path_1.join)(projectRoot, WEB_I18N, "catalogs");
|
|
16
|
+
}
|
|
17
|
+
function appCatalogDir(projectRoot) {
|
|
18
|
+
return (0, node_path_1.join)(catalogsDir(projectRoot), "app");
|
|
19
|
+
}
|
|
20
|
+
function normalizeLocaleCode(value) {
|
|
21
|
+
try {
|
|
22
|
+
const normalized = Intl.getCanonicalLocales(value)[0];
|
|
23
|
+
if (!normalized)
|
|
24
|
+
throw new Error("missing locale");
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(`Invalid locale code "${value}". Use a BCP 47 language tag such as en, ko, or pt-BR.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readJson(path) {
|
|
32
|
+
const value = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
34
|
+
throw new Error(`Expected a JSON object: ${path}`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function readDefinition(path) {
|
|
39
|
+
const value = readJson(path);
|
|
40
|
+
const code = typeof value.code === "string" ? normalizeLocaleCode(value.code) : "";
|
|
41
|
+
const name = typeof value.name === "string" ? value.name.trim() : "";
|
|
42
|
+
const direction = value.direction;
|
|
43
|
+
const enabled = value.enabled;
|
|
44
|
+
if (!code || !name || (direction !== "ltr" && direction !== "rtl") || typeof enabled !== "boolean") {
|
|
45
|
+
throw new Error(`Invalid locale definition: ${path}`);
|
|
46
|
+
}
|
|
47
|
+
if (`${code}.json` !== (0, node_path_1.basename)(path)) {
|
|
48
|
+
throw new Error(`Locale definition filename must match its canonical code: ${code}.json`);
|
|
49
|
+
}
|
|
50
|
+
return { code, name, direction, enabled };
|
|
51
|
+
}
|
|
52
|
+
function listLocales(projectRoot) {
|
|
53
|
+
const dir = definitionsDir(projectRoot);
|
|
54
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
55
|
+
throw new Error("This project does not contain PodoKit JSON locale definitions.");
|
|
56
|
+
return (0, node_fs_1.readdirSync)(dir)
|
|
57
|
+
.filter((name) => name.endsWith(".json"))
|
|
58
|
+
.map((name) => readDefinition((0, node_path_1.join)(dir, name)))
|
|
59
|
+
.sort((left, right) => left.code.localeCompare(right.code));
|
|
60
|
+
}
|
|
61
|
+
function merge(target, source) {
|
|
62
|
+
const result = { ...target };
|
|
63
|
+
for (const [key, value] of Object.entries(source)) {
|
|
64
|
+
const previous = result[key];
|
|
65
|
+
if (previous && typeof previous === "object" && !Array.isArray(previous) &&
|
|
66
|
+
value && typeof value === "object" && !Array.isArray(value)) {
|
|
67
|
+
result[key] = merge(previous, value);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
result[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function composedCatalog(projectRoot, code) {
|
|
76
|
+
const root = catalogsDir(projectRoot);
|
|
77
|
+
if (!(0, node_fs_1.existsSync)(root))
|
|
78
|
+
return {};
|
|
79
|
+
const sources = (0, node_fs_1.readdirSync)(root, { withFileTypes: true })
|
|
80
|
+
.filter((entry) => entry.isDirectory())
|
|
81
|
+
.map((entry) => entry.name)
|
|
82
|
+
.sort((left, right) => {
|
|
83
|
+
const leftApp = left === "app" ? 1 : 0;
|
|
84
|
+
const rightApp = right === "app" ? 1 : 0;
|
|
85
|
+
return leftApp - rightApp || left.localeCompare(right);
|
|
86
|
+
});
|
|
87
|
+
let catalog = {};
|
|
88
|
+
for (const source of sources) {
|
|
89
|
+
const path = (0, node_path_1.join)(root, source, `${code}.json`);
|
|
90
|
+
if ((0, node_fs_1.existsSync)(path))
|
|
91
|
+
catalog = merge(catalog, readJson(path));
|
|
92
|
+
}
|
|
93
|
+
return catalog;
|
|
94
|
+
}
|
|
95
|
+
function flatten(value, prefix = "", out = new Map()) {
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
if (value.length === 0)
|
|
98
|
+
out.set(prefix, value);
|
|
99
|
+
for (const [index, child] of value.entries()) {
|
|
100
|
+
flatten(child, `${prefix}[${index}]`, out);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (value && typeof value === "object") {
|
|
104
|
+
for (const [key, child] of Object.entries(value)) {
|
|
105
|
+
flatten(child, prefix ? `${prefix}.${key}` : key, out);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
out.set(prefix, value);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function placeholders(value) {
|
|
114
|
+
if (typeof value !== "string")
|
|
115
|
+
return [];
|
|
116
|
+
return [...value.matchAll(/\{(\w+)\}/g)].map((match) => match[1] ?? "").sort();
|
|
117
|
+
}
|
|
118
|
+
function validateLocale(projectRoot, rawCode) {
|
|
119
|
+
const code = normalizeLocaleCode(rawCode);
|
|
120
|
+
const definitionPath = (0, node_path_1.join)(definitionsDir(projectRoot), `${code}.json`);
|
|
121
|
+
if (!(0, node_fs_1.existsSync)(definitionPath))
|
|
122
|
+
throw new Error(`Locale ${code} has not been added.`);
|
|
123
|
+
const definition = readDefinition(definitionPath);
|
|
124
|
+
const base = new Map([...flatten(composedCatalog(projectRoot, "en"))].filter(([, value]) => value !== ""));
|
|
125
|
+
const selected = flatten(composedCatalog(projectRoot, code));
|
|
126
|
+
const missing = [];
|
|
127
|
+
for (const [key, baseValue] of base) {
|
|
128
|
+
if (!selected.has(key) || selected.get(key) === "") {
|
|
129
|
+
missing.push(key);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const selectedValue = selected.get(key);
|
|
133
|
+
const baseKind = Array.isArray(baseValue) ? "array" : typeof baseValue;
|
|
134
|
+
const selectedKind = Array.isArray(selectedValue) ? "array" : typeof selectedValue;
|
|
135
|
+
if (baseKind !== selectedKind)
|
|
136
|
+
throw new Error(`${code}:${key} must be ${baseKind}, received ${selectedKind}`);
|
|
137
|
+
if (placeholders(baseValue).join(",") !== placeholders(selectedValue).join(",")) {
|
|
138
|
+
throw new Error(`${code}:${key} must preserve placeholders ${placeholders(baseValue).join(", ")}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const total = base.size;
|
|
142
|
+
const translated = total - missing.length;
|
|
143
|
+
return {
|
|
144
|
+
definition,
|
|
145
|
+
translated,
|
|
146
|
+
total,
|
|
147
|
+
percent: total === 0 ? 100 : Math.round((translated / total) * 100),
|
|
148
|
+
missing,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function defaultLocaleName(code) {
|
|
152
|
+
try {
|
|
153
|
+
return new Intl.DisplayNames([code], { type: "language" }).of(code) ?? code;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return code;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function addLocale(projectRoot, rawCode, options = {}) {
|
|
160
|
+
const code = normalizeLocaleCode(rawCode);
|
|
161
|
+
const definitionPath = (0, node_path_1.join)(definitionsDir(projectRoot), `${code}.json`);
|
|
162
|
+
const catalogPath = (0, node_path_1.join)(appCatalogDir(projectRoot), `${code}.json`);
|
|
163
|
+
if ((0, node_fs_1.existsSync)(definitionPath) || (0, node_fs_1.existsSync)(catalogPath))
|
|
164
|
+
throw new Error(`Locale ${code} already exists.`);
|
|
165
|
+
const definition = {
|
|
166
|
+
code,
|
|
167
|
+
name: options.name?.trim() || defaultLocaleName(code),
|
|
168
|
+
direction: options.direction ?? "ltr",
|
|
169
|
+
enabled: false,
|
|
170
|
+
};
|
|
171
|
+
(0, node_fs_1.mkdirSync)(definitionsDir(projectRoot), { recursive: true });
|
|
172
|
+
(0, node_fs_1.mkdirSync)(appCatalogDir(projectRoot), { recursive: true });
|
|
173
|
+
(0, node_fs_1.writeFileSync)(definitionPath, `${JSON.stringify(definition, null, 2)}\n`);
|
|
174
|
+
(0, node_fs_1.writeFileSync)(catalogPath, "{}\n");
|
|
175
|
+
return definition;
|
|
176
|
+
}
|
|
177
|
+
function setLocaleEnabled(projectRoot, rawCode, enabled) {
|
|
178
|
+
const coverage = validateLocale(projectRoot, rawCode);
|
|
179
|
+
if (!enabled && coverage.definition.code === "en")
|
|
180
|
+
throw new Error("The PodoKit English fallback cannot be deactivated.");
|
|
181
|
+
const definition = { ...coverage.definition, enabled };
|
|
182
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(definitionsDir(projectRoot), `${definition.code}.json`), `${JSON.stringify(definition, null, 2)}\n`);
|
|
183
|
+
return { ...coverage, definition };
|
|
184
|
+
}
|
package/dist/lockfile.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface PodokitManifest {
|
|
|
32
32
|
modules: ManifestModule[];
|
|
33
33
|
/** Globs whose files are user-owned and never touched by update. */
|
|
34
34
|
ownedGlobs: string[];
|
|
35
|
+
/** Module-supplied globs that stay managed even inside a broad owned glob. */
|
|
36
|
+
managedOverrides?: string[];
|
|
35
37
|
}
|
|
36
38
|
export interface FileEntry {
|
|
37
39
|
tier: Tier;
|
|
@@ -64,11 +66,11 @@ export declare function podokitVersion(): string;
|
|
|
64
66
|
/** Match a POSIX relative path against a simple glob supporting `*` and `**`. */
|
|
65
67
|
export declare function matchGlob(path: string, glob: string): boolean;
|
|
66
68
|
/** Classify a file's ownership tier. See ADR-0010 priority order. */
|
|
67
|
-
export declare function classifyTier(relPath: string, content: string | Buffer, ownedGlobs: string[]): Tier;
|
|
69
|
+
export declare function classifyTier(relPath: string, content: string | Buffer, ownedGlobs: string[], managedOverrides?: string[]): Tier;
|
|
68
70
|
/** List project files as POSIX-relative paths, skipping generated/vendored dirs. */
|
|
69
71
|
export declare function walkFiles(projectRoot: string, prefix?: string): string[];
|
|
70
72
|
/** Recompute the files.lock by hashing and classifying every project file. */
|
|
71
|
-
export declare function computeFilesLock(projectRoot: string, ownedGlobs: string[]): FilesLock;
|
|
73
|
+
export declare function computeFilesLock(projectRoot: string, ownedGlobs: string[], managedOverrides?: string[]): FilesLock;
|
|
72
74
|
export declare function readFilesLock(projectRoot: string): FilesLock | null;
|
|
73
75
|
export interface Drift {
|
|
74
76
|
/** Managed/assembled files whose content differs from what PodoKit wrote. */
|
|
@@ -101,4 +103,4 @@ export declare function mergeOwnedGlobs(existing: string[], add: string[]): stri
|
|
|
101
103
|
* `ownedGlobs` a module declares are merged into the manifest so its owned paths
|
|
102
104
|
* survive every future recompute. No-op on projects without a manifest.
|
|
103
105
|
*/
|
|
104
|
-
export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot): void;
|
|
106
|
+
export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot, managedOverrides?: string[]): void;
|
package/dist/lockfile.js
CHANGED
|
@@ -45,6 +45,9 @@ const WALK_IGNORE = new Set([
|
|
|
45
45
|
exports.DEFAULT_OWNED_GLOBS = [
|
|
46
46
|
"apps/web/src/routes/**",
|
|
47
47
|
"apps/web/src/lib/components/ui/**",
|
|
48
|
+
// Locale definitions and app messages are application-owned extension points.
|
|
49
|
+
"apps/web/src/lib/i18n/locales/**",
|
|
50
|
+
"apps/web/src/lib/i18n/catalogs/app/**",
|
|
48
51
|
// Owned Nest DI extension slot — override providers here; update never touches it.
|
|
49
52
|
"apps/api/src/app.extensions.ts",
|
|
50
53
|
// AI agent guidance — yours to customize; update never touches it.
|
|
@@ -62,6 +65,7 @@ exports.DEFAULT_OWNED_GLOBS = [
|
|
|
62
65
|
".devcontainer/**",
|
|
63
66
|
".env.docker",
|
|
64
67
|
"infra/traefik/**",
|
|
68
|
+
".podokit/dev.json",
|
|
65
69
|
];
|
|
66
70
|
const TEXT_EXT = /\.(json|md|ts|tsx|js|mjs|cjs|css|html|yml|yaml|txt|env|svelte|gitignore|example)$/i;
|
|
67
71
|
function podokitDir(projectRoot) {
|
|
@@ -114,7 +118,12 @@ function isTextPath(relPath) {
|
|
|
114
118
|
return relPath.startsWith("dot-") || TEXT_EXT.test(relPath);
|
|
115
119
|
}
|
|
116
120
|
/** Classify a file's ownership tier. See ADR-0010 priority order. */
|
|
117
|
-
function classifyTier(relPath, content, ownedGlobs) {
|
|
121
|
+
function classifyTier(relPath, content, ownedGlobs, managedOverrides = []) {
|
|
122
|
+
const explicitlyOwned = ownedGlobs.some((glob) => !glob.includes("*") && matchGlob(relPath, glob));
|
|
123
|
+
if (explicitlyOwned)
|
|
124
|
+
return "owned";
|
|
125
|
+
if (managedOverrides.some((glob) => matchGlob(relPath, glob)))
|
|
126
|
+
return "managed";
|
|
118
127
|
if (ownedGlobs.some((glob) => matchGlob(relPath, glob)))
|
|
119
128
|
return "owned";
|
|
120
129
|
if (isTextPath(relPath) && content.toString("utf8").includes(INJECTION_MARKER))
|
|
@@ -138,11 +147,14 @@ function walkFiles(projectRoot, prefix = "") {
|
|
|
138
147
|
return out;
|
|
139
148
|
}
|
|
140
149
|
/** Recompute the files.lock by hashing and classifying every project file. */
|
|
141
|
-
function computeFilesLock(projectRoot, ownedGlobs) {
|
|
150
|
+
function computeFilesLock(projectRoot, ownedGlobs, managedOverrides = []) {
|
|
142
151
|
const files = {};
|
|
143
152
|
for (const rel of walkFiles(projectRoot).sort()) {
|
|
144
153
|
const content = (0, node_fs_1.readFileSync)((0, node_path_1.join)(projectRoot, rel));
|
|
145
|
-
files[rel] = {
|
|
154
|
+
files[rel] = {
|
|
155
|
+
tier: classifyTier(rel, content, ownedGlobs, managedOverrides),
|
|
156
|
+
outHash: (0, podokit_template_engine_1.hashContent)(content),
|
|
157
|
+
};
|
|
146
158
|
}
|
|
147
159
|
return { schemaVersion: exports.LOCK_SCHEMA_VERSION, files };
|
|
148
160
|
}
|
|
@@ -185,6 +197,7 @@ function initLockfile(projectRoot, options) {
|
|
|
185
197
|
answers: options.answers,
|
|
186
198
|
modules: [],
|
|
187
199
|
ownedGlobs,
|
|
200
|
+
managedOverrides: [],
|
|
188
201
|
};
|
|
189
202
|
writeManifest(projectRoot, manifest);
|
|
190
203
|
writeFilesLock(projectRoot, computeFilesLock(projectRoot, ownedGlobs));
|
|
@@ -209,7 +222,7 @@ function mergeOwnedGlobs(existing, add) {
|
|
|
209
222
|
* `ownedGlobs` a module declares are merged into the manifest so its owned paths
|
|
210
223
|
* survive every future recompute. No-op on projects without a manifest.
|
|
211
224
|
*/
|
|
212
|
-
function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnapshot) {
|
|
225
|
+
function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnapshot, managedOverrides) {
|
|
213
226
|
const manifest = readManifest(projectRoot);
|
|
214
227
|
if (!manifest)
|
|
215
228
|
return;
|
|
@@ -234,8 +247,11 @@ function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnaps
|
|
|
234
247
|
}
|
|
235
248
|
if (ownedGlobs?.length)
|
|
236
249
|
manifest.ownedGlobs = mergeOwnedGlobs(manifest.ownedGlobs, ownedGlobs);
|
|
250
|
+
if (managedOverrides?.length) {
|
|
251
|
+
manifest.managedOverrides = mergeOwnedGlobs(manifest.managedOverrides ?? [], managedOverrides);
|
|
252
|
+
}
|
|
237
253
|
writeManifest(projectRoot, manifest);
|
|
238
|
-
const computed = computeFilesLock(projectRoot, manifest.ownedGlobs);
|
|
254
|
+
const computed = computeFilesLock(projectRoot, manifest.ownedGlobs, manifest.managedOverrides ?? []);
|
|
239
255
|
if (!lockSnapshot) {
|
|
240
256
|
writeFilesLock(projectRoot, computed);
|
|
241
257
|
return;
|
package/dist/remove.js
CHANGED
|
@@ -107,8 +107,11 @@ function removeModule(options) {
|
|
|
107
107
|
manifest.modules = manifest.modules
|
|
108
108
|
.filter((m) => m.name !== module)
|
|
109
109
|
.map((m, i) => ({ ...m, order: i }));
|
|
110
|
+
const sharedManagedOverrides = new Set(others.flatMap((other) => other.manifest.managedOverrides ?? []));
|
|
111
|
+
const targetManagedOverrides = new Set(targetManifest.managedOverrides ?? []);
|
|
112
|
+
manifest.managedOverrides = (manifest.managedOverrides ?? []).filter((glob) => !targetManagedOverrides.has(glob) || sharedManagedOverrides.has(glob));
|
|
110
113
|
(0, lockfile_1.writeManifest)(projectRoot, manifest);
|
|
111
|
-
(0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs));
|
|
114
|
+
(0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs, manifest.managedOverrides));
|
|
112
115
|
return { module, removed, keptEdited, keptShared, unwired };
|
|
113
116
|
}
|
|
114
117
|
/** Remove now-empty directories left behind by a deleted file, up to the root. */
|
|
@@ -126,33 +129,33 @@ function pruneEmptyDirs(projectRoot, relDir) {
|
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
function pruneAppPackage(projectRoot, target, others) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!(0, node_fs_1.existsSync)(appPkgPath))
|
|
133
|
-
return;
|
|
134
|
-
const keep = (section) => new Set(others.flatMap((o) => Object.keys(o.manifest[section] ?? {})));
|
|
135
|
-
const pkg = readJson(appPkgPath);
|
|
136
|
-
let changed = false;
|
|
137
|
-
for (const section of ["dependencies", "devDependencies", "scripts"]) {
|
|
138
|
-
const declared = target[section];
|
|
139
|
-
const current = pkg[section];
|
|
140
|
-
if (!declared || !current)
|
|
132
|
+
for (const [app, declaration] of (0, add_1.modulePackageOverlays)(target)) {
|
|
133
|
+
const appPkgPath = (0, node_path_1.join)(projectRoot, "apps", app, "package.json");
|
|
134
|
+
if (!(0, node_fs_1.existsSync)(appPkgPath))
|
|
141
135
|
continue;
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
136
|
+
const keep = (section) => new Set(others.flatMap((other) => Object.keys((0, add_1.modulePackageOverlays)(other.manifest).get(app)?.[section] ?? {})));
|
|
137
|
+
const pkg = readJson(appPkgPath);
|
|
138
|
+
let changed = false;
|
|
139
|
+
for (const section of ["dependencies", "devDependencies", "scripts"]) {
|
|
140
|
+
const declared = declaration[section];
|
|
141
|
+
const current = pkg[section];
|
|
142
|
+
if (!declared || !current)
|
|
145
143
|
continue;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
const shared = keep(section);
|
|
145
|
+
for (const name of Object.keys(declared)) {
|
|
146
|
+
if (shared.has(name))
|
|
147
|
+
continue;
|
|
148
|
+
if (name in current) {
|
|
149
|
+
delete current[name];
|
|
150
|
+
changed = true;
|
|
151
|
+
}
|
|
149
152
|
}
|
|
153
|
+
if (current && Object.keys(current).length === 0)
|
|
154
|
+
delete pkg[section];
|
|
150
155
|
}
|
|
151
|
-
if (
|
|
152
|
-
|
|
156
|
+
if (changed)
|
|
157
|
+
(0, node_fs_1.writeFileSync)(appPkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
153
158
|
}
|
|
154
|
-
if (changed)
|
|
155
|
-
(0, node_fs_1.writeFileSync)(appPkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
156
159
|
}
|
|
157
160
|
function pruneEnvExample(projectRoot, target, others) {
|
|
158
161
|
if (!target.env?.length)
|
|
@@ -8,10 +8,49 @@ Full-stack TypeScript starter generated with [PodoKit](https://github.com/podoso
|
|
|
8
8
|
|
|
9
9
|
## Getting started
|
|
10
10
|
|
|
11
|
+
### Recommended: containerized development
|
|
12
|
+
|
|
11
13
|
```bash
|
|
12
14
|
{{packageManager}} install
|
|
13
15
|
cp .env.example .env
|
|
16
|
+
npx @podosoft/podokit dev watch
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Open the URL printed by the command; a new project defaults to
|
|
20
|
+
**http://{{projectName}}.localhost**. The committed `.podokit/dev.json` is the
|
|
21
|
+
source of truth if you change the hostname. The first running PodoKit project starts
|
|
22
|
+
one user-level Traefik gateway on `127.0.0.1:80`; additional projects reuse it and
|
|
23
|
+
route by that hostname. Even a single app uses
|
|
24
|
+
the same topology, with one route and no project-specific host port.
|
|
25
|
+
|
|
26
|
+
Run lifecycle and container commands from a second terminal:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @podosoft/podokit dev url
|
|
30
|
+
npx @podosoft/podokit dev ps
|
|
31
|
+
npx @podosoft/podokit dev logs
|
|
32
|
+
npx @podosoft/podokit dev exec api npm run migration:run -w {{projectName}}-api
|
|
33
|
+
npx @podosoft/podokit dev down
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If installed modules require extra services, use their profiles on the initial
|
|
37
|
+
watch command instead:
|
|
14
38
|
|
|
39
|
+
```bash
|
|
40
|
+
npx @podosoft/podokit dev watch \
|
|
41
|
+
--profile cache --profile storage --profile queue
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Pass the same profile flags to other lifecycle commands when applicable. `dev down`
|
|
45
|
+
removes this project's stack and route. If it is the final registered
|
|
46
|
+
project, it also removes the shared gateway and network. Source changes are synced
|
|
47
|
+
through Compose Watch, including Vite HMR on the same portless browser origin.
|
|
48
|
+
|
|
49
|
+
### Alternative: host processes
|
|
50
|
+
|
|
51
|
+
Use this loop when you want only the web and API processes on the host:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
15
54
|
# start local PostgreSQL + Redis
|
|
16
55
|
docker compose -f infra/docker/docker-compose.yml up -d
|
|
17
56
|
|
|
@@ -35,6 +74,9 @@ generate and run a migration:
|
|
|
35
74
|
Want a worked example? Generate the `todo` template instead:
|
|
36
75
|
`npx @podosoft/podokit create my-app --template todo`.
|
|
37
76
|
|
|
77
|
+
For multi-project routing, container profiles, migrations, and OAuth over a
|
|
78
|
+
stable HTTPS tunnel, see the PodoKit [development guide](https://github.com/podosoft-dev/podokit/blob/main/docs/development.md).
|
|
79
|
+
|
|
38
80
|
## Deploy
|
|
39
81
|
|
|
40
82
|
Docker Compose in `infra/docker`; example k3s manifests in `infra/k3s`
|
|
@@ -20,6 +20,7 @@ RUN addgroup -S app && adduser -S app -G app
|
|
|
20
20
|
COPY --from=build /app/node_modules ./node_modules
|
|
21
21
|
COPY --from=deps /app/apps/api ./apps/api
|
|
22
22
|
COPY --from=build /app/apps/api/dist ./apps/api/dist
|
|
23
|
+
COPY --from=build /app/apps/api/scripts ./apps/api/scripts
|
|
23
24
|
WORKDIR /app/apps/api
|
|
24
25
|
USER app
|
|
25
26
|
EXPOSE 3000
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Localization
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Locale definitions live in `locales/<code>.json`. Product copy and intentional
|
|
4
|
+
module overrides live in the app-owned `catalogs/app/<code>.json` files.
|
|
4
5
|
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
The admin-dashboard module installs the composed JSON message runtime and its
|
|
7
|
+
complete English and Korean catalogs. Use `podo locale add`, `validate`, and
|
|
8
|
+
`activate` to extend it. See the generated `$podokit-add-locale` skill for the
|
|
9
|
+
complete workflow.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|