create-oke 0.1.4

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 (74) hide show
  1. package/package.json +36 -0
  2. package/src/cli.test.ts +110 -0
  3. package/src/cli.ts +156 -0
  4. package/src/index.ts +8 -0
  5. package/src/scaffold.ts +160 -0
  6. package/src/sync-templates.ts +65 -0
  7. package/src/templates.ts +85 -0
  8. package/src/transform.ts +124 -0
  9. package/templates/linkly/oke.config.ts +10 -0
  10. package/templates/linkly/package.json +15 -0
  11. package/templates/linkly/src/app.ts +21 -0
  12. package/templates/linkly/src/core.ts +4 -0
  13. package/templates/linkly/src/flows/analytics/index.ts +24 -0
  14. package/templates/linkly/src/flows/links/index.ts +59 -0
  15. package/templates/linkly/src/flows/links/shapes.ts +22 -0
  16. package/templates/linkly/src/flows/links/signals.ts +13 -0
  17. package/templates/linkly/src/gates.ts +8 -0
  18. package/templates/linkly/src/schema.ts +17 -0
  19. package/templates/linkly/tests/linkly.test.ts +19 -0
  20. package/templates/notes/oke.config.ts +7 -0
  21. package/templates/notes/package.json +16 -0
  22. package/templates/notes/src/app.ts +13 -0
  23. package/templates/notes/src/core.ts +4 -0
  24. package/templates/notes/src/flows/notes/index.ts +44 -0
  25. package/templates/notes/src/schema.ts +9 -0
  26. package/templates/notes/tests/notes.test.ts +10 -0
  27. package/templates/provisions/oke.config.ts +17 -0
  28. package/templates/provisions/package.json +15 -0
  29. package/templates/provisions/src/app.ts +60 -0
  30. package/templates/provisions/src/channels.ts +14 -0
  31. package/templates/provisions/src/core.ts +4 -0
  32. package/templates/provisions/src/flows/notifications/index.ts +19 -0
  33. package/templates/provisions/src/flows/orders/index.ts +70 -0
  34. package/templates/provisions/src/flows/orders/shapes.ts +21 -0
  35. package/templates/provisions/src/flows/orders/signals.ts +19 -0
  36. package/templates/provisions/src/flows/payments/index.ts +37 -0
  37. package/templates/provisions/src/flows/payments/shapes.ts +3 -0
  38. package/templates/provisions/src/flows/payments/stripe.ts +18 -0
  39. package/templates/provisions/src/gates.ts +4 -0
  40. package/templates/provisions/src/locales/ar.ts +1 -0
  41. package/templates/provisions/src/locales/en.ts +1 -0
  42. package/templates/provisions/src/plugins/audit-schema.ts +2 -0
  43. package/templates/provisions/src/plugins/audit.ts +14 -0
  44. package/templates/provisions/src/schema.ts +16 -0
  45. package/templates/provisions/src/vault.ts +16 -0
  46. package/templates/provisions/tests/orders.test.ts +18 -0
  47. package/templates/skyport/evals/triage.jsonl +1 -0
  48. package/templates/skyport/oke.config.ts +44 -0
  49. package/templates/skyport/oke.images.lock +4 -0
  50. package/templates/skyport/package.json +16 -0
  51. package/templates/skyport/src/ai.ts +25 -0
  52. package/templates/skyport/src/app.ts +57 -0
  53. package/templates/skyport/src/channels.ts +12 -0
  54. package/templates/skyport/src/core.ts +4 -0
  55. package/templates/skyport/src/flows/bookings/index.ts +50 -0
  56. package/templates/skyport/src/flows/bookings/shapes.ts +6 -0
  57. package/templates/skyport/src/flows/bookings/signals.ts +9 -0
  58. package/templates/skyport/src/flows/notifications/index.ts +16 -0
  59. package/templates/skyport/src/flows/payments/index.ts +14 -0
  60. package/templates/skyport/src/flows/payments/shapes.ts +5 -0
  61. package/templates/skyport/src/flows/support/index.ts +38 -0
  62. package/templates/skyport/src/flows/users/elements.ts +2 -0
  63. package/templates/skyport/src/flows/users/index.ts +12 -0
  64. package/templates/skyport/src/flows/users/shapes.ts +6 -0
  65. package/templates/skyport/src/gates.ts +10 -0
  66. package/templates/skyport/src/journeys.ts +9 -0
  67. package/templates/skyport/src/locales/ar.ts +4 -0
  68. package/templates/skyport/src/locales/en.ts +4 -0
  69. package/templates/skyport/src/plugins/audit-schema.ts +7 -0
  70. package/templates/skyport/src/plugins/audit.ts +14 -0
  71. package/templates/skyport/src/plugins/panel.tsx +4 -0
  72. package/templates/skyport/src/schema.ts +22 -0
  73. package/templates/skyport/src/vault.ts +24 -0
  74. package/templates/skyport/tests/support.test.ts +16 -0
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "create-oke",
3
+ "version": "0.1.4",
4
+ "description": "Scaffold an okengine app — bunx create-oke@latest <name>",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/omqkhafi/okengine.git",
11
+ "directory": "packages/create-oke"
12
+ },
13
+ "bin": {
14
+ "create-oke": "./src/index.ts"
15
+ },
16
+ "exports": {
17
+ ".": "./src/index.ts"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "templates"
22
+ ],
23
+ "scripts": {
24
+ "prepack": "bun ./src/sync-templates.ts",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "bun test",
27
+ "test:integration": "CREATE_OKE_INTEGRATION=1 bun test tests/scaffold.integration.test.ts tests/dev.integration.test.ts"
28
+ },
29
+ "engines": {
30
+ "bun": ">=1.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/bun": "latest",
34
+ "typescript": "^7.0.2"
35
+ }
36
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Unit tests for create-oke argument parsing and transforms.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { formatCdPath, parseArgs } from "./cli.ts";
10
+ import { listTemplateFiles, scaffold } from "./scaffold.ts";
11
+ import { resolveTemplateDir, TEMPLATES } from "./templates.ts";
12
+ import {
13
+ sanitizeProjectName,
14
+ shouldSkipTemplatePath,
15
+ transformPackageJson,
16
+ } from "./transform.ts";
17
+
18
+ describe("parseArgs", () => {
19
+ test("defaults template to notes", () => {
20
+ const a = parseArgs(["my-app"]);
21
+ expect(a.name).toBe("my-app");
22
+ expect(a.template).toBe("notes");
23
+ });
24
+
25
+ test("accepts --template and -t", () => {
26
+ expect(parseArgs(["x", "--template", "linkly"]).template).toBe("linkly");
27
+ expect(parseArgs(["x", "-t", "skyport"]).template).toBe("skyport");
28
+ expect(parseArgs(["x", "--template=provisions"]).template).toBe(
29
+ "provisions",
30
+ );
31
+ });
32
+
33
+ test("rejects unknown template", () => {
34
+ expect(() => parseArgs(["x", "--template", "nope"])).toThrow(/template/);
35
+ });
36
+ });
37
+
38
+ describe("transformPackageJson", () => {
39
+ test("rewrites name and okengine file:../.. to installable ref", () => {
40
+ const next = transformPackageJson(
41
+ {
42
+ name: "@oke/example-notes",
43
+ private: true,
44
+ dependencies: {
45
+ okengine: "file:../..",
46
+ zod: "^4.4.3",
47
+ },
48
+ },
49
+ "my-app",
50
+ "0.0.26",
51
+ );
52
+ expect(next.name).toBe("my-app");
53
+ expect(next.dependencies?.["okengine"]).toBe("0.0.26");
54
+ expect(next.dependencies?.["okengine"]).not.toBe("file:../..");
55
+ expect(next.dependencies?.["zod"]).toBe("^4.4.3");
56
+ });
57
+ });
58
+
59
+ describe("shouldSkipTemplatePath", () => {
60
+ test("skips node_modules, locks, and monorepo docker test", () => {
61
+ expect(shouldSkipTemplatePath("node_modules/okengine/package.json")).toBe(
62
+ true,
63
+ );
64
+ expect(shouldSkipTemplatePath("bun.lock")).toBe(true);
65
+ expect(shouldSkipTemplatePath("tests/docker.test.ts")).toBe(true);
66
+ expect(shouldSkipTemplatePath("tests/support.test.ts")).toBe(false);
67
+ expect(shouldSkipTemplatePath("oke.config.ts")).toBe(false);
68
+ });
69
+ });
70
+
71
+ describe("sanitizeProjectName", () => {
72
+ test("lowercases and slugifies", () => {
73
+ expect(sanitizeProjectName("My App")).toBe("my-app");
74
+ });
75
+ });
76
+
77
+ describe("formatCdPath", () => {
78
+ test("keeps absolute paths outside cwd", () => {
79
+ expect(formatCdPath("/tmp/my-app")).toBe("/tmp/my-app");
80
+ });
81
+ });
82
+
83
+ describe("scaffold structure", () => {
84
+ test("each template produces the example tree (minus skips)", () => {
85
+ for (const id of TEMPLATES) {
86
+ const dir = mkdtempSync(join(tmpdir(), `create-oke-${id}-`));
87
+ try {
88
+ const templateDir = resolveTemplateDir(id);
89
+ const expected = listTemplateFiles(templateDir);
90
+ const result = scaffold({
91
+ targetDir: join(dir, id),
92
+ name: `app-${id}`,
93
+ template: id,
94
+ });
95
+ expect([...result.files].sort()).toEqual(expected);
96
+ const pkg = JSON.parse(
97
+ readFileSync(join(result.targetDir, "package.json"), "utf8"),
98
+ ) as { name: string; dependencies: { okengine: string } };
99
+ expect(pkg.name).toBe(`app-${id}`);
100
+ expect(pkg.dependencies.okengine).not.toBe("file:../..");
101
+ expect(
102
+ pkg.dependencies.okengine.startsWith("file:") ||
103
+ /^\d+\.\d+\.\d+/.test(pkg.dependencies.okengine),
104
+ ).toBe(true);
105
+ } finally {
106
+ rmSync(dir, { recursive: true, force: true });
107
+ }
108
+ }
109
+ });
110
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `create-oke` CLI — one command, sensible default, done.
3
+ *
4
+ * ```bash
5
+ * bunx create-oke@latest <name> [--template notes|linkly|provisions|skyport]
6
+ * ```
7
+ */
8
+
9
+ import { basename, relative, resolve } from "node:path";
10
+ import { scaffold } from "./scaffold.ts";
11
+ import {
12
+ DEFAULT_TEMPLATE,
13
+ TEMPLATES,
14
+ isTemplateId,
15
+ type TemplateId,
16
+ } from "./templates.ts";
17
+
18
+ /** Parsed CLI arguments. */
19
+ export type CliArgs = {
20
+ readonly name: string | undefined;
21
+ readonly template: TemplateId;
22
+ readonly help: boolean;
23
+ readonly targetDir: string | undefined;
24
+ };
25
+
26
+ /**
27
+ * Parse argv after the binary name.
28
+ *
29
+ * @param argv - Process arguments (no node/bun binary)
30
+ */
31
+ export function parseArgs(argv: readonly string[]): CliArgs {
32
+ let name: string | undefined;
33
+ let template: TemplateId = DEFAULT_TEMPLATE;
34
+ let help = false;
35
+ let targetDir: string | undefined;
36
+
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const a = argv[i]!;
39
+ if (a === "--help" || a === "-h") {
40
+ help = true;
41
+ continue;
42
+ }
43
+ if (a === "--template" || a === "-t") {
44
+ const next = argv[++i];
45
+ if (!next || !isTemplateId(next)) {
46
+ throw new Error(
47
+ `create-oke: --template must be one of ${TEMPLATES.join("|")}`,
48
+ );
49
+ }
50
+ template = next;
51
+ continue;
52
+ }
53
+ if (a.startsWith("--template=")) {
54
+ const value = a.slice("--template=".length);
55
+ if (!isTemplateId(value)) {
56
+ throw new Error(
57
+ `create-oke: --template must be one of ${TEMPLATES.join("|")}`,
58
+ );
59
+ }
60
+ template = value;
61
+ continue;
62
+ }
63
+ if (a.startsWith("-")) {
64
+ throw new Error(`create-oke: unknown option ${a}`);
65
+ }
66
+ if (name === undefined) {
67
+ name = a;
68
+ continue;
69
+ }
70
+ throw new Error(`create-oke: unexpected argument ${a}`);
71
+ }
72
+
73
+ if (name !== undefined) {
74
+ targetDir = resolve(name);
75
+ }
76
+
77
+ return { name, template, help, targetDir };
78
+ }
79
+
80
+ /**
81
+ * Path for the post-scaffold `cd` line — relative when under cwd, else absolute.
82
+ *
83
+ * @param targetDir - Absolute project directory
84
+ */
85
+ export function formatCdPath(targetDir: string): string {
86
+ const rel = relative(process.cwd(), targetDir);
87
+ if (!rel || rel.startsWith("..") || rel.includes("..")) return targetDir;
88
+ return rel;
89
+ }
90
+
91
+ /**
92
+ * Help text — next steps match four-applications.md (`bun install` · `oke dev`).
93
+ */
94
+ export function helpText(): string {
95
+ return `create-oke — scaffold an okengine app
96
+
97
+ Usage:
98
+ bunx create-oke@latest <name> [--template notes|linkly|provisions|skyport]
99
+
100
+ Templates (from examples/):
101
+ notes basic — Flow · Store (default)
102
+ linkly intermediate — + Signal · Clock · Gate
103
+ provisions advanced — + Vault · Channel · plugins
104
+ skyport complex — all eight elements · AI · tenancy
105
+
106
+ No telemetry. Bun only. No interactive wizard.
107
+ `;
108
+ }
109
+
110
+ /**
111
+ * Run the CLI.
112
+ *
113
+ * @param argv - Args after the binary
114
+ * @returns Exit code
115
+ */
116
+ export async function run(argv: readonly string[]): Promise<number> {
117
+ let args: CliArgs;
118
+ try {
119
+ args = parseArgs(argv);
120
+ } catch (e) {
121
+ console.error(e instanceof Error ? e.message : e);
122
+ return 1;
123
+ }
124
+
125
+ if (args.help || args.name === undefined) {
126
+ console.log(helpText());
127
+ if (args.name === undefined && !args.help) {
128
+ console.error("create-oke: missing <name>");
129
+ return 1;
130
+ }
131
+ return args.help ? 0 : 1;
132
+ }
133
+
134
+ const projectName = basename(resolve(args.name));
135
+ try {
136
+ const result = scaffold({
137
+ targetDir: args.targetDir!,
138
+ name: projectName,
139
+ template: args.template,
140
+ });
141
+
142
+ console.log(`
143
+ Scaffolded ${result.template} → ${result.targetDir}
144
+
145
+ Next steps:
146
+
147
+ cd ${formatCdPath(result.targetDir)}
148
+ bun install
149
+ oke dev # app :6530 · Console :6533 · MCP :6535
150
+ `);
151
+ return 0;
152
+ } catch (e) {
153
+ console.error(e instanceof Error ? e.message : e);
154
+ return 1;
155
+ }
156
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * `create-oke` binary entry.
4
+ */
5
+
6
+ import { run } from "./cli.ts";
7
+
8
+ process.exit(await run(process.argv.slice(2)));
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Copy an `examples/<template>` tree and apply the package.json transform.
3
+ */
4
+
5
+ import {
6
+ cpSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { join, relative, resolve } from "node:path";
15
+ import {
16
+ resolveLocalOkengineRoot,
17
+ resolveTemplateDir,
18
+ type TemplateId,
19
+ } from "./templates.ts";
20
+ import {
21
+ sanitizeProjectName,
22
+ shouldSkipTemplatePath,
23
+ transformPackageJson,
24
+ resolveOkengineDependency,
25
+ type ScaffoldPackageJson,
26
+ } from "./transform.ts";
27
+
28
+ /** Options for {@link scaffold}. */
29
+ export type ScaffoldOptions = {
30
+ /** Destination directory (created). Absolute or cwd-relative. */
31
+ readonly targetDir: string;
32
+ /** npm package / folder name. */
33
+ readonly name: string;
34
+ /** Template id (default: notes). */
35
+ readonly template: TemplateId;
36
+ };
37
+
38
+ /** Result of a successful scaffold. */
39
+ export type ScaffoldResult = {
40
+ readonly targetDir: string;
41
+ readonly name: string;
42
+ readonly template: TemplateId;
43
+ readonly okengineDependency: string;
44
+ /** Relative paths written (POSIX), sorted. */
45
+ readonly files: readonly string[];
46
+ };
47
+
48
+ /**
49
+ * Scaffold a new okengine project from `examples/<template>`.
50
+ *
51
+ * @param options - Name, template, destination
52
+ */
53
+ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
54
+ const name = sanitizeProjectName(options.name);
55
+ const targetDir = resolve(options.targetDir);
56
+ const templateDir = resolveTemplateDir(options.template);
57
+
58
+ if (existsSync(targetDir)) {
59
+ const entries = readdirSync(targetDir);
60
+ if (entries.length > 0) {
61
+ throw new Error(
62
+ `create-oke: target directory is not empty: ${targetDir}`,
63
+ );
64
+ }
65
+ } else {
66
+ mkdirSync(targetDir, { recursive: true });
67
+ }
68
+
69
+ const okengineDependency = resolveOkengineDependency(
70
+ resolveLocalOkengineRoot(),
71
+ );
72
+ const written: string[] = [];
73
+
74
+ copyTree(templateDir, targetDir, "", written);
75
+
76
+ const pkgPath = join(targetDir, "package.json");
77
+ if (!existsSync(pkgPath)) {
78
+ throw new Error(
79
+ `create-oke: template "${options.template}" has no package.json`,
80
+ );
81
+ }
82
+ const sourcePkg = JSON.parse(
83
+ readFileSync(pkgPath, "utf8"),
84
+ ) as ScaffoldPackageJson;
85
+ const nextPkg = transformPackageJson(sourcePkg, name, okengineDependency);
86
+ writeFileSync(pkgPath, `${JSON.stringify(nextPkg, null, 2)}\n`, "utf8");
87
+
88
+ written.sort();
89
+ return {
90
+ targetDir,
91
+ name,
92
+ template: options.template,
93
+ okengineDependency,
94
+ files: written,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Recursively copy template files, applying {@link shouldSkipTemplatePath}.
100
+ *
101
+ * @param srcRoot - Template root
102
+ * @param dstRoot - Destination root
103
+ * @param rel - Relative path under the roots
104
+ * @param written - Accumulator for relative paths written
105
+ */
106
+ function copyTree(
107
+ srcRoot: string,
108
+ dstRoot: string,
109
+ rel: string,
110
+ written: string[],
111
+ ): void {
112
+ const src = rel ? join(srcRoot, rel) : srcRoot;
113
+ const st = statSync(src);
114
+ if (st.isDirectory()) {
115
+ if (rel && shouldSkipTemplatePath(rel)) return;
116
+ if (rel) mkdirSync(join(dstRoot, rel), { recursive: true });
117
+ for (const entry of readdirSync(src)) {
118
+ copyTree(srcRoot, dstRoot, rel ? join(rel, entry) : entry, written);
119
+ }
120
+ return;
121
+ }
122
+ if (!st.isFile()) return;
123
+ const posixRel = rel.split(/[/\\]/).join("/");
124
+ if (shouldSkipTemplatePath(posixRel)) return;
125
+ const dst = join(dstRoot, rel);
126
+ mkdirSync(join(dst, ".."), { recursive: true });
127
+ cpSync(src, dst);
128
+ written.push(posixRel);
129
+ }
130
+
131
+ /**
132
+ * List relative file paths in an example tree using the same skip rules as scaffold.
133
+ *
134
+ * @param templateDir - Absolute template directory
135
+ */
136
+ export function listTemplateFiles(templateDir: string): string[] {
137
+ const out: string[] = [];
138
+ walk(templateDir, templateDir, out);
139
+ out.sort();
140
+ return out;
141
+ }
142
+
143
+ /**
144
+ * @param root - Template root
145
+ * @param dir - Current directory
146
+ * @param out - Accumulator
147
+ */
148
+ function walk(root: string, dir: string, out: string[]): void {
149
+ for (const entry of readdirSync(dir)) {
150
+ const abs = join(dir, entry);
151
+ const rel = relative(root, abs).split(/[/\\]/).join("/");
152
+ const st = statSync(abs);
153
+ if (st.isDirectory()) {
154
+ if (shouldSkipTemplatePath(rel)) continue;
155
+ walk(root, abs, out);
156
+ continue;
157
+ }
158
+ if (st.isFile() && !shouldSkipTemplatePath(rel)) out.push(rel);
159
+ }
160
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * prepack: copy `examples/<template>` into `templates/<template>` so the
4
+ * published npm package can scaffold without the monorepo checkout.
5
+ *
6
+ * Applies the same skip rules as runtime scaffold (no node_modules, no
7
+ * monorepo-only docker.test.ts).
8
+ */
9
+
10
+ import {
11
+ cpSync,
12
+ existsSync,
13
+ mkdirSync,
14
+ readdirSync,
15
+ rmSync,
16
+ statSync,
17
+ } from "node:fs";
18
+ import { join, relative, resolve } from "node:path";
19
+ import { packageRoot, TEMPLATES } from "./templates.ts";
20
+ import { shouldSkipTemplatePath } from "./transform.ts";
21
+
22
+ const pkg = packageRoot();
23
+ const examplesRoot = resolve(pkg, "../../examples");
24
+ const outRoot = join(pkg, "templates");
25
+
26
+ if (!existsSync(examplesRoot)) {
27
+ console.error(`create-oke prepack: examples/ not found at ${examplesRoot}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ rmSync(outRoot, { recursive: true, force: true });
32
+ mkdirSync(outRoot, { recursive: true });
33
+
34
+ for (const id of TEMPLATES) {
35
+ const src = join(examplesRoot, id);
36
+ const dst = join(outRoot, id);
37
+ if (!existsSync(src)) {
38
+ console.error(`create-oke prepack: missing example ${src}`);
39
+ process.exit(1);
40
+ }
41
+ copyFiltered(src, dst, src);
42
+ console.log(`create-oke prepack: ${id}`);
43
+ }
44
+
45
+ /**
46
+ * @param src - Source file or directory
47
+ * @param dst - Destination path
48
+ * @param root - Template root for relative skip paths
49
+ */
50
+ function copyFiltered(src: string, dst: string, root: string): void {
51
+ const st = statSync(src);
52
+ const rel = relative(root, src).split(/[/\\]/).join("/");
53
+ if (rel && shouldSkipTemplatePath(rel)) return;
54
+ if (st.isDirectory()) {
55
+ mkdirSync(dst, { recursive: true });
56
+ for (const entry of readdirSync(src)) {
57
+ copyFiltered(join(src, entry), join(dst, entry), root);
58
+ }
59
+ return;
60
+ }
61
+ if (st.isFile()) {
62
+ mkdirSync(join(dst, ".."), { recursive: true });
63
+ cpSync(src, dst);
64
+ }
65
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Template ids and resolution — always sourced from `examples/<id>`,
3
+ * with a published-package fallback at `templates/<id>` (filled by prepack).
4
+ */
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ /** Canonical template ids — match `examples/*` and four-applications.md. */
11
+ export const TEMPLATES = ["notes", "linkly", "provisions", "skyport"] as const;
12
+
13
+ /** A known scaffold template. */
14
+ export type TemplateId = (typeof TEMPLATES)[number];
15
+
16
+ /** Default when `--template` is omitted (smallest quickstart). */
17
+ export const DEFAULT_TEMPLATE: TemplateId = "notes";
18
+
19
+ /**
20
+ * Whether `value` is a known {@link TemplateId}.
21
+ *
22
+ * @param value - Candidate string
23
+ */
24
+ export function isTemplateId(value: string): value is TemplateId {
25
+ return (TEMPLATES as readonly string[]).includes(value);
26
+ }
27
+
28
+ /**
29
+ * Directory that contains this package (`packages/create-oke`).
30
+ */
31
+ export function packageRoot(): string {
32
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..");
33
+ }
34
+
35
+ /**
36
+ * Resolve the directory to copy for `template`.
37
+ *
38
+ * Order:
39
+ * 1. `CREATE_OKE_EXAMPLES` env (absolute path to an `examples/` tree)
40
+ * 2. Monorepo `examples/<template>` (two levels up from this package)
41
+ * 3. Bundled `templates/<template>` (npm prepack copy)
42
+ *
43
+ * @param template - Template id
44
+ * @returns Absolute path to the template source tree
45
+ */
46
+ export function resolveTemplateDir(template: TemplateId): string {
47
+ const envRoot = process.env["CREATE_OKE_EXAMPLES"];
48
+ if (envRoot) {
49
+ const fromEnv = resolve(envRoot, template);
50
+ if (existsSync(fromEnv)) return fromEnv;
51
+ }
52
+
53
+ const pkg = packageRoot();
54
+ const monorepo = resolve(pkg, "../../examples", template);
55
+ if (existsSync(monorepo)) return monorepo;
56
+
57
+ const bundled = resolve(pkg, "templates", template);
58
+ if (existsSync(bundled)) return bundled;
59
+
60
+ throw new Error(
61
+ `create-oke: template "${template}" not found.\n` +
62
+ ` looked at: ${monorepo}\n` +
63
+ ` ${bundled}`,
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Absolute path to the okengine package root when running inside this monorepo.
69
+ *
70
+ * @returns Repo root, or `null` when create-oke is used from a published install
71
+ */
72
+ export function resolveLocalOkengineRoot(): string | null {
73
+ const env = process.env["CREATE_OKE_OKENGINE"];
74
+ if (env && existsSync(join(env, "package.json"))) return resolve(env);
75
+
76
+ const candidate = resolve(packageRoot(), "../..");
77
+ const pkgPath = join(candidate, "package.json");
78
+ if (!existsSync(pkgPath)) return null;
79
+ try {
80
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string };
81
+ return pkg.name === "okengine" ? candidate : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }