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.
- package/package.json +36 -0
- package/src/cli.test.ts +110 -0
- package/src/cli.ts +156 -0
- package/src/index.ts +8 -0
- package/src/scaffold.ts +160 -0
- package/src/sync-templates.ts +65 -0
- package/src/templates.ts +85 -0
- package/src/transform.ts +124 -0
- package/templates/linkly/oke.config.ts +10 -0
- package/templates/linkly/package.json +15 -0
- package/templates/linkly/src/app.ts +21 -0
- package/templates/linkly/src/core.ts +4 -0
- package/templates/linkly/src/flows/analytics/index.ts +24 -0
- package/templates/linkly/src/flows/links/index.ts +59 -0
- package/templates/linkly/src/flows/links/shapes.ts +22 -0
- package/templates/linkly/src/flows/links/signals.ts +13 -0
- package/templates/linkly/src/gates.ts +8 -0
- package/templates/linkly/src/schema.ts +17 -0
- package/templates/linkly/tests/linkly.test.ts +19 -0
- package/templates/notes/oke.config.ts +7 -0
- package/templates/notes/package.json +16 -0
- package/templates/notes/src/app.ts +13 -0
- package/templates/notes/src/core.ts +4 -0
- package/templates/notes/src/flows/notes/index.ts +44 -0
- package/templates/notes/src/schema.ts +9 -0
- package/templates/notes/tests/notes.test.ts +10 -0
- package/templates/provisions/oke.config.ts +17 -0
- package/templates/provisions/package.json +15 -0
- package/templates/provisions/src/app.ts +60 -0
- package/templates/provisions/src/channels.ts +14 -0
- package/templates/provisions/src/core.ts +4 -0
- package/templates/provisions/src/flows/notifications/index.ts +19 -0
- package/templates/provisions/src/flows/orders/index.ts +70 -0
- package/templates/provisions/src/flows/orders/shapes.ts +21 -0
- package/templates/provisions/src/flows/orders/signals.ts +19 -0
- package/templates/provisions/src/flows/payments/index.ts +37 -0
- package/templates/provisions/src/flows/payments/shapes.ts +3 -0
- package/templates/provisions/src/flows/payments/stripe.ts +18 -0
- package/templates/provisions/src/gates.ts +4 -0
- package/templates/provisions/src/locales/ar.ts +1 -0
- package/templates/provisions/src/locales/en.ts +1 -0
- package/templates/provisions/src/plugins/audit-schema.ts +2 -0
- package/templates/provisions/src/plugins/audit.ts +14 -0
- package/templates/provisions/src/schema.ts +16 -0
- package/templates/provisions/src/vault.ts +16 -0
- package/templates/provisions/tests/orders.test.ts +18 -0
- package/templates/skyport/evals/triage.jsonl +1 -0
- package/templates/skyport/oke.config.ts +44 -0
- package/templates/skyport/oke.images.lock +4 -0
- package/templates/skyport/package.json +16 -0
- package/templates/skyport/src/ai.ts +25 -0
- package/templates/skyport/src/app.ts +57 -0
- package/templates/skyport/src/channels.ts +12 -0
- package/templates/skyport/src/core.ts +4 -0
- package/templates/skyport/src/flows/bookings/index.ts +50 -0
- package/templates/skyport/src/flows/bookings/shapes.ts +6 -0
- package/templates/skyport/src/flows/bookings/signals.ts +9 -0
- package/templates/skyport/src/flows/notifications/index.ts +16 -0
- package/templates/skyport/src/flows/payments/index.ts +14 -0
- package/templates/skyport/src/flows/payments/shapes.ts +5 -0
- package/templates/skyport/src/flows/support/index.ts +38 -0
- package/templates/skyport/src/flows/users/elements.ts +2 -0
- package/templates/skyport/src/flows/users/index.ts +12 -0
- package/templates/skyport/src/flows/users/shapes.ts +6 -0
- package/templates/skyport/src/gates.ts +10 -0
- package/templates/skyport/src/journeys.ts +9 -0
- package/templates/skyport/src/locales/ar.ts +4 -0
- package/templates/skyport/src/locales/en.ts +4 -0
- package/templates/skyport/src/plugins/audit-schema.ts +7 -0
- package/templates/skyport/src/plugins/audit.ts +14 -0
- package/templates/skyport/src/plugins/panel.tsx +4 -0
- package/templates/skyport/src/schema.ts +22 -0
- package/templates/skyport/src/vault.ts +24 -0
- 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
|
+
}
|
package/src/cli.test.ts
ADDED
|
@@ -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
package/src/scaffold.ts
ADDED
|
@@ -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
|
+
}
|
package/src/templates.ts
ADDED
|
@@ -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
|
+
}
|