create-ovok 0.0.0-stage → 0.1.0-dev.13
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 +18 -2
- package/dist/adapters/fs-project-files.js +30 -0
- package/dist/adapters/fs-template-source.js +25 -0
- package/dist/adapters/terminal-prompt.js +28 -0
- package/dist/application/add-app.js +50 -0
- package/dist/application/copy-template.js +7 -0
- package/dist/application/create-workspace.js +19 -0
- package/dist/application/ports.js +1 -0
- package/dist/application/refresh-agent-rules.js +24 -0
- package/dist/domain/agent-rules.js +43 -0
- package/dist/domain/app-spec.js +46 -0
- package/dist/domain/template-text.js +12 -0
- package/dist/main.js +86 -0
- package/package.json +20 -4
- package/templates/mobile/AGENTS.md +5 -0
- package/templates/mobile/App.tsx +50 -0
- package/templates/mobile/CLAUDE.md +1 -0
- package/templates/mobile/app.config.ts +30 -0
- package/templates/mobile/index.js +8 -0
- package/templates/mobile/package.json +31 -0
- package/templates/mobile/src/ovok.ts +12 -0
- package/templates/mobile/tsconfig.json +8 -0
- package/templates/package/package.json +14 -0
- package/templates/package/src/index.ts +1 -0
- package/templates/package/tsconfig.json +12 -0
- package/templates/web/AGENTS.md +5 -0
- package/templates/web/CLAUDE.md +1 -0
- package/templates/web/index.html +12 -0
- package/templates/web/package.json +27 -0
- package/templates/web/src/App.tsx +66 -0
- package/templates/web/src/env.d.ts +4 -0
- package/templates/web/src/main.tsx +12 -0
- package/templates/web/src/ovok.ts +7 -0
- package/templates/web/tsconfig.json +15 -0
- package/templates/web/vite.config.ts +16 -0
- package/templates/workspace/.env +2 -0
- package/templates/workspace/.mcp.json +8 -0
- package/templates/workspace/AGENTS.md +11 -0
- package/templates/workspace/CLAUDE.md +1 -0
- package/templates/workspace/README.md +11 -0
- package/templates/workspace/_gitignore +9 -0
- package/templates/workspace/package.json +15 -0
- package/templates/workspace/pnpm-workspace.yaml +5 -0
- package/templates/workspace/turbo/generators/config.ts +57 -0
- package/templates/workspace/turbo.json +18 -0
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-ovok
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
```bash
|
|
4
|
+
pnpm create ovok my-app --platform web|mobile|mobile-web [--type ios|android] [--tenant <code>] [--base-url <url>]
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Missing options are asked for on a terminal. The result is a Turborepo with the chosen apps in
|
|
8
|
+
`apps/`, the Ovok API and tenant in `.env`, and the Ovok MCP server in `.mcp.json`.
|
|
9
|
+
|
|
10
|
+
Inside the workspace:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm turbo gen app # add a web or mobile app
|
|
14
|
+
pnpm turbo gen package # add a shared package
|
|
15
|
+
pnpm turbo gen agents # rewrite the Ovok block of every AGENTS.md
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Every app has an `AGENTS.md`, and a `CLAUDE.md` that imports it. The Ovok rules sit between
|
|
19
|
+
`BEGIN:ovok-agent-rules` and `END:ovok-agent-rules`; the rest of each file is yours.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
export class FsProjectFiles {
|
|
4
|
+
#root;
|
|
5
|
+
constructor(root) {
|
|
6
|
+
this.#root = root;
|
|
7
|
+
}
|
|
8
|
+
async exists(path) {
|
|
9
|
+
return access(join(this.#root, path)).then(() => true, () => false);
|
|
10
|
+
}
|
|
11
|
+
async read(path) {
|
|
12
|
+
return readFile(join(this.#root, path), "utf8").catch(missingAs(null));
|
|
13
|
+
}
|
|
14
|
+
async write(path, content) {
|
|
15
|
+
const absolute = join(this.#root, path);
|
|
16
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
17
|
+
await writeFile(absolute, content);
|
|
18
|
+
}
|
|
19
|
+
async directories(path) {
|
|
20
|
+
const entries = await readdir(join(this.#root, path), { withFileTypes: true }).catch(missingAs([]));
|
|
21
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function missingAs(fallback) {
|
|
25
|
+
return (error) => {
|
|
26
|
+
if (error.code === "ENOENT")
|
|
27
|
+
return fallback;
|
|
28
|
+
throw error;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const TEMPLATES_DIRECTORY = fileURLToPath(new URL("../../templates/", import.meta.url));
|
|
5
|
+
/** npm leaves `.gitignore` out of a published package, so templates carry it as `_gitignore`. */
|
|
6
|
+
const PUBLISHED_NAMES = { _gitignore: ".gitignore" };
|
|
7
|
+
export class FsTemplateSource {
|
|
8
|
+
async files(template) {
|
|
9
|
+
const root = join(TEMPLATES_DIRECTORY, template);
|
|
10
|
+
const entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
11
|
+
const files = entries.filter((entry) => entry.isFile());
|
|
12
|
+
return Promise.all(files.map(async (entry) => {
|
|
13
|
+
const absolute = join(entry.parentPath, entry.name);
|
|
14
|
+
return {
|
|
15
|
+
path: projectPathOf(relative(root, absolute)),
|
|
16
|
+
content: await readFile(absolute, "utf8"),
|
|
17
|
+
};
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function projectPathOf(templatePath) {
|
|
22
|
+
const segments = templatePath.split(sep);
|
|
23
|
+
const name = segments.pop() ?? "";
|
|
24
|
+
return [...segments, PUBLISHED_NAMES[name] ?? name].join("/");
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
/** Asks only on a terminal; anywhere else a missing answer takes its default. */
|
|
3
|
+
export class TerminalPrompt {
|
|
4
|
+
#interactive;
|
|
5
|
+
constructor(interactive = Boolean(process.stdin.isTTY)) {
|
|
6
|
+
this.#interactive = interactive;
|
|
7
|
+
}
|
|
8
|
+
async ask(question, fallback) {
|
|
9
|
+
if (!this.#interactive)
|
|
10
|
+
return fallback;
|
|
11
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
try {
|
|
13
|
+
const answer = (await prompt.question(`${question} (${fallback}): `)).trim();
|
|
14
|
+
return answer === "" ? fallback : answer;
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
prompt.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async choose(question, choices, fallback) {
|
|
21
|
+
for (;;) {
|
|
22
|
+
const answer = await this.ask(`${question} [${choices.join(" | ")}]`, fallback);
|
|
23
|
+
const choice = choices.find((candidate) => candidate === answer);
|
|
24
|
+
if (choice !== undefined)
|
|
25
|
+
return choice;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { appRulesBlock } from "../domain/agent-rules.js";
|
|
2
|
+
import { bundleIdFor, MOBILE_OSES } from "../domain/app-spec.js";
|
|
3
|
+
import { copyTemplate } from "./copy-template.js";
|
|
4
|
+
export class NotAWorkspaceError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export class AlreadyExistsError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export async function addApp(app, deps) {
|
|
9
|
+
const workspace = await workspaceNameOf(deps.files);
|
|
10
|
+
const directory = await freeDirectory(`apps/${app.name}`, deps.files);
|
|
11
|
+
await copyTemplate(app.platform, directory, {
|
|
12
|
+
workspace,
|
|
13
|
+
folder: app.name,
|
|
14
|
+
bundleId: bundleIdFor(workspace, app.name),
|
|
15
|
+
platforms: app.os.map((os) => `"${os}"`).join(", "),
|
|
16
|
+
agentRules: appRulesBlock(app.platform),
|
|
17
|
+
}, deps);
|
|
18
|
+
if (app.platform === "mobile") {
|
|
19
|
+
await keepRunScriptsOf(app.os, `${directory}/package.json`, deps.files);
|
|
20
|
+
}
|
|
21
|
+
return directory;
|
|
22
|
+
}
|
|
23
|
+
export async function addPackage(name, deps) {
|
|
24
|
+
const workspace = await workspaceNameOf(deps.files);
|
|
25
|
+
const directory = await freeDirectory(`packages/${name}`, deps.files);
|
|
26
|
+
await copyTemplate("package", directory, { workspace, folder: name }, deps);
|
|
27
|
+
return directory;
|
|
28
|
+
}
|
|
29
|
+
async function workspaceNameOf(files) {
|
|
30
|
+
const manifest = await files.read("package.json");
|
|
31
|
+
const isWorkspace = manifest !== null && (await files.exists("pnpm-workspace.yaml"));
|
|
32
|
+
const name = isWorkspace ? JSON.parse(manifest).name : undefined;
|
|
33
|
+
if (typeof name !== "string") {
|
|
34
|
+
throw new NotAWorkspaceError("Run this at the root of a workspace made by create-ovok.");
|
|
35
|
+
}
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
async function freeDirectory(directory, files) {
|
|
39
|
+
if (await files.exists(directory))
|
|
40
|
+
throw new AlreadyExistsError(`${directory} already exists.`);
|
|
41
|
+
return directory;
|
|
42
|
+
}
|
|
43
|
+
/** Drops the run script of each OS the app leaves out, so `pnpm android` is not offered to an iOS-only app. */
|
|
44
|
+
async function keepRunScriptsOf(os, path, files) {
|
|
45
|
+
const manifest = JSON.parse((await files.read(path)) ?? "{}");
|
|
46
|
+
for (const dropped of MOBILE_OSES.filter((candidate) => !os.includes(candidate))) {
|
|
47
|
+
delete manifest.scripts?.[dropped];
|
|
48
|
+
}
|
|
49
|
+
await files.write(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { renderTemplate } from "../domain/template-text.js";
|
|
3
|
+
export async function copyTemplate(template, directory, variables, deps) {
|
|
4
|
+
for (const file of await deps.templates.files(template)) {
|
|
5
|
+
await deps.files.write(posix.join(directory, file.path), renderTemplate(file.content, variables));
|
|
6
|
+
}
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { workspaceRulesBlock } from "../domain/agent-rules.js";
|
|
2
|
+
import { AlreadyExistsError, addApp } from "./add-app.js";
|
|
3
|
+
import { copyTemplate } from "./copy-template.js";
|
|
4
|
+
export async function createWorkspace(spec, deps) {
|
|
5
|
+
if (await deps.files.exists("package.json")) {
|
|
6
|
+
throw new AlreadyExistsError("The folder already holds a project.");
|
|
7
|
+
}
|
|
8
|
+
await copyTemplate("workspace", "", {
|
|
9
|
+
workspace: spec.name,
|
|
10
|
+
baseUrl: spec.baseUrl,
|
|
11
|
+
tenantCode: spec.tenantCode,
|
|
12
|
+
createOvokVersion: spec.createOvokVersion,
|
|
13
|
+
agentRules: workspaceRulesBlock(),
|
|
14
|
+
}, deps);
|
|
15
|
+
const directories = [];
|
|
16
|
+
for (const app of spec.apps)
|
|
17
|
+
directories.push(await addApp(app, deps));
|
|
18
|
+
return directories;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { appRulesBlock, replaceRulesBlock, workspaceRulesBlock } from "../domain/agent-rules.js";
|
|
2
|
+
import { PLATFORMS } from "../domain/app-spec.js";
|
|
3
|
+
const AGENTS_FILE = "AGENTS.md";
|
|
4
|
+
/** Rewrites the Ovok block of every AGENTS.md to this create-ovok's rules; the rest is the team's. */
|
|
5
|
+
export async function refreshAgentRules(files) {
|
|
6
|
+
const refreshed = [await refresh(AGENTS_FILE, workspaceRulesBlock(), files)];
|
|
7
|
+
for (const app of await files.directories("apps")) {
|
|
8
|
+
const platform = await platformOf(`apps/${app}/package.json`, files);
|
|
9
|
+
if (platform !== null) {
|
|
10
|
+
refreshed.push(await refresh(`apps/${app}/${AGENTS_FILE}`, appRulesBlock(platform), files));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return refreshed;
|
|
14
|
+
}
|
|
15
|
+
async function refresh(path, block, files) {
|
|
16
|
+
await files.write(path, replaceRulesBlock((await files.read(path)) ?? "", block));
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
/** Apps made by create-ovok name their platform in package.json; other apps are left alone. */
|
|
20
|
+
async function platformOf(path, files) {
|
|
21
|
+
const manifest = await files.read(path);
|
|
22
|
+
const platform = manifest === null ? undefined : JSON.parse(manifest)?.ovok?.platform;
|
|
23
|
+
return PLATFORMS.find((candidate) => candidate === platform) ?? null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const RULES_BEGIN = "<!-- BEGIN:ovok-agent-rules -->";
|
|
2
|
+
export const RULES_END = "<!-- END:ovok-agent-rules -->";
|
|
3
|
+
const WORKSPACE_RULES = `## This is NOT the Ovok you know
|
|
4
|
+
|
|
5
|
+
Your training data is out of date for Ovok. Before writing code that touches it, read the guide in
|
|
6
|
+
\`node_modules/@ovok/core/docs/\` (web) or \`node_modules/@ovok/native/docs/\` (mobile), resolved
|
|
7
|
+
from the app's own directory: they match the installed version. If that folder is missing, read
|
|
8
|
+
https://docs.ovok.com/llms.txt instead. Platform concepts (FHIR, settings, Signals) are there too.
|
|
9
|
+
|
|
10
|
+
- Check live data and settings with the \`ovok\` MCP server in \`.mcp.json\` instead of guessing.
|
|
11
|
+
- Never evaluate vital-sign thresholds in app code: Signals decides alerts; you send or edit its rules.
|
|
12
|
+
- Add apps and packages with \`pnpm turbo gen\`, never by copying a folder.`;
|
|
13
|
+
const PLATFORM_RULES = {
|
|
14
|
+
web: `## Ovok in this web app
|
|
15
|
+
|
|
16
|
+
- Vite and React on \`@ovok/core\`. Use the one \`Ovok\` client in \`src/ovok.ts\`; never create another.
|
|
17
|
+
- \`OVOK_BASE_URL\` and \`OVOK_TENANT_CODE\` come from the root \`.env\` through \`vite.config.ts\`.
|
|
18
|
+
Only those two reach the browser; never widen that list, as everything in this app ships to users.`,
|
|
19
|
+
mobile: `## Ovok in this mobile app
|
|
20
|
+
|
|
21
|
+
- Expo with a development build: Bluetooth needs native code, so Expo Go cannot run it. Use
|
|
22
|
+
\`pnpm ios\` or \`pnpm android\`.
|
|
23
|
+
- Read \`node_modules/@ovok/native/docs/\` before touching devices or Bluetooth.
|
|
24
|
+
- Use the one \`Ovok\` client in \`src/ovok.ts\`; its settings come from the root \`.env\` through
|
|
25
|
+
\`app.config.ts\`.`,
|
|
26
|
+
};
|
|
27
|
+
/** The block `create-ovok agents` owns: it rewrites what is between the markers and nothing else. */
|
|
28
|
+
export function workspaceRulesBlock() {
|
|
29
|
+
return block(WORKSPACE_RULES);
|
|
30
|
+
}
|
|
31
|
+
export function appRulesBlock(platform) {
|
|
32
|
+
return block(PLATFORM_RULES[platform]);
|
|
33
|
+
}
|
|
34
|
+
export function replaceRulesBlock(document, replacement) {
|
|
35
|
+
const start = document.indexOf(RULES_BEGIN);
|
|
36
|
+
const end = document.indexOf(RULES_END, start);
|
|
37
|
+
if (start === -1 || end === -1)
|
|
38
|
+
return `${replacement}\n\n${document}`;
|
|
39
|
+
return document.slice(0, start) + replacement + document.slice(end + RULES_END.length);
|
|
40
|
+
}
|
|
41
|
+
function block(rules) {
|
|
42
|
+
return `${RULES_BEGIN}\n${rules}\n${RULES_END}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const PLATFORMS = ["web", "mobile"];
|
|
2
|
+
export const MOBILE_OSES = ["ios", "android"];
|
|
3
|
+
export const STARTING_PLATFORMS = ["web", "mobile", "mobile-web"];
|
|
4
|
+
/** The npm rules for an unscoped name, which every app folder and workspace name follows. */
|
|
5
|
+
const PACKAGE_NAME = /^[a-z0-9][a-z0-9._-]*$/;
|
|
6
|
+
const PACKAGE_NAME_MAX_LENGTH = 214;
|
|
7
|
+
export class InvalidSpecError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export function parsePackageName(raw) {
|
|
10
|
+
if (!PACKAGE_NAME.test(raw) || raw.length > PACKAGE_NAME_MAX_LENGTH) {
|
|
11
|
+
throw new InvalidSpecError(`"${raw}" is not a valid name: use lowercase letters, digits, ".", "_" and "-".`);
|
|
12
|
+
}
|
|
13
|
+
return raw;
|
|
14
|
+
}
|
|
15
|
+
export function parseChoice(raw, choices, flag) {
|
|
16
|
+
const choice = choices.find((candidate) => candidate === raw);
|
|
17
|
+
if (choice === undefined)
|
|
18
|
+
throw new InvalidSpecError(`${flag} is one of ${choices.join(", ")}.`);
|
|
19
|
+
return choice;
|
|
20
|
+
}
|
|
21
|
+
/** `--type` narrows a mobile app to one OS; on a web app it means nothing, so it is refused. */
|
|
22
|
+
export function osFor(platform, type) {
|
|
23
|
+
if (platform === "web") {
|
|
24
|
+
if (type !== null)
|
|
25
|
+
throw new InvalidSpecError("--type only applies to mobile apps.");
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return type === null ? MOBILE_OSES : [type];
|
|
29
|
+
}
|
|
30
|
+
/** A reverse-DNS id both stores accept: letters and digits per segment, never a leading digit. */
|
|
31
|
+
export function bundleIdFor(workspace, app) {
|
|
32
|
+
const segment = (raw) => {
|
|
33
|
+
const cleaned = raw.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
34
|
+
return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
|
|
35
|
+
};
|
|
36
|
+
return ["com", "example", segment(workspace), segment(app)].join(".");
|
|
37
|
+
}
|
|
38
|
+
export function startingApps(platforms, type) {
|
|
39
|
+
const web = { name: "web", platform: "web", os: [] };
|
|
40
|
+
const mobile = { name: "mobile", platform: "mobile", os: osFor("mobile", type) };
|
|
41
|
+
if (platforms === "web")
|
|
42
|
+
return [{ ...web, os: osFor("web", type) }];
|
|
43
|
+
if (platforms === "mobile")
|
|
44
|
+
return [mobile];
|
|
45
|
+
return [web, mobile];
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const PLACEHOLDER = /\{\{(\w+)\}\}/g;
|
|
2
|
+
export class MissingVariableError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** Fills every `{{name}}`; a placeholder without a value is a template bug, so it fails loudly. */
|
|
5
|
+
export function renderTemplate(text, variables) {
|
|
6
|
+
return text.replace(PLACEHOLDER, (_match, name) => {
|
|
7
|
+
const value = variables[name];
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
throw new MissingVariableError(`The template needs "${name}".`);
|
|
10
|
+
return value;
|
|
11
|
+
});
|
|
12
|
+
}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { FsProjectFiles } from "./adapters/fs-project-files.js";
|
|
6
|
+
import { FsTemplateSource } from "./adapters/fs-template-source.js";
|
|
7
|
+
import { TerminalPrompt } from "./adapters/terminal-prompt.js";
|
|
8
|
+
import { addApp, addPackage } from "./application/add-app.js";
|
|
9
|
+
import { createWorkspace } from "./application/create-workspace.js";
|
|
10
|
+
import { refreshAgentRules } from "./application/refresh-agent-rules.js";
|
|
11
|
+
import { MOBILE_OSES, osFor, PLATFORMS, parseChoice, parsePackageName, STARTING_PLATFORMS, startingApps, } from "./domain/app-spec.js";
|
|
12
|
+
const EXIT_FAILURE = 1;
|
|
13
|
+
const DEFAULT_BASE_URL = "https://api.sandbox.ovok.com";
|
|
14
|
+
const DEFAULT_TENANT_CODE = "public-example";
|
|
15
|
+
const OPTIONS = {
|
|
16
|
+
platform: { type: "string" },
|
|
17
|
+
type: { type: "string" },
|
|
18
|
+
tenant: { type: "string" },
|
|
19
|
+
"base-url": { type: "string" },
|
|
20
|
+
};
|
|
21
|
+
const SUBCOMMANDS = {
|
|
22
|
+
add: async ([name], values) => {
|
|
23
|
+
const platform = parseChoice(values.platform ?? "", PLATFORMS, "--platform");
|
|
24
|
+
const app = {
|
|
25
|
+
name: parsePackageName(name ?? ""),
|
|
26
|
+
platform,
|
|
27
|
+
os: osFor(platform, typeOf(values)),
|
|
28
|
+
};
|
|
29
|
+
console.log(`Added ${await addApp(app, dependenciesAt(process.cwd()))}.`);
|
|
30
|
+
},
|
|
31
|
+
"add-package": async ([name]) => {
|
|
32
|
+
console.log(`Added ${await addPackage(parsePackageName(name ?? ""), dependenciesAt(process.cwd()))}.`);
|
|
33
|
+
},
|
|
34
|
+
agents: async () => {
|
|
35
|
+
const refreshed = await refreshAgentRules(new FsProjectFiles(process.cwd()));
|
|
36
|
+
console.log(`Refreshed the Ovok rules in ${refreshed.join(", ")}.`);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
async function main(argv) {
|
|
40
|
+
const { values, positionals } = parseArgs({
|
|
41
|
+
args: argv,
|
|
42
|
+
allowPositionals: true,
|
|
43
|
+
options: OPTIONS,
|
|
44
|
+
});
|
|
45
|
+
const [first, ...rest] = positionals;
|
|
46
|
+
const subcommand = first === undefined ? undefined : SUBCOMMANDS[first];
|
|
47
|
+
if (subcommand === undefined)
|
|
48
|
+
return create(first, values);
|
|
49
|
+
return subcommand(rest, values);
|
|
50
|
+
}
|
|
51
|
+
async function create(nameArgument, values) {
|
|
52
|
+
const prompt = new TerminalPrompt();
|
|
53
|
+
const name = parsePackageName(nameArgument ?? (await prompt.ask("Project name", "ovok-app")));
|
|
54
|
+
const platforms = values.platform === undefined
|
|
55
|
+
? await prompt.choose("Platforms", STARTING_PLATFORMS, "mobile-web")
|
|
56
|
+
: parseChoice(values.platform, STARTING_PLATFORMS, "--platform");
|
|
57
|
+
const apps = startingApps(platforms, typeOf(values));
|
|
58
|
+
const directory = resolve(name);
|
|
59
|
+
await createWorkspace({
|
|
60
|
+
name,
|
|
61
|
+
baseUrl: (values["base-url"] ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
62
|
+
tenantCode: values.tenant ?? (await prompt.ask("Tenant code", DEFAULT_TENANT_CODE)),
|
|
63
|
+
createOvokVersion: await ownVersion(),
|
|
64
|
+
apps,
|
|
65
|
+
}, dependenciesAt(directory));
|
|
66
|
+
console.log(`Created ${name} with ${apps.map((app) => `apps/${app.name}`).join(" and ")}.\n`);
|
|
67
|
+
console.log(` cd ${name}\n pnpm install\n pnpm dev\n`);
|
|
68
|
+
console.log("Add another app with `pnpm turbo gen app`.");
|
|
69
|
+
}
|
|
70
|
+
function typeOf(values) {
|
|
71
|
+
return values.type === undefined ? null : parseChoice(values.type, MOBILE_OSES, "--type");
|
|
72
|
+
}
|
|
73
|
+
function dependenciesAt(root) {
|
|
74
|
+
return { templates: new FsTemplateSource(), files: new FsProjectFiles(root) };
|
|
75
|
+
}
|
|
76
|
+
async function ownVersion() {
|
|
77
|
+
const manifest = await readFile(new URL("../package.json", import.meta.url), "utf8");
|
|
78
|
+
return JSON.parse(manifest).version;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await main(process.argv.slice(2));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
85
|
+
process.exitCode = EXIT_FAILURE;
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ovok",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-dev.13",
|
|
4
|
+
"description": "Create a Turborepo with Ovok web and mobile apps, and add more apps through turbo gen.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-ovok": "dist/main.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22.12"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.build.json",
|
|
19
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
20
|
+
"test": "node --test \"src/**/*.test.ts\""
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Button, StyleSheet, Text, TextInput, View } from "react-native";
|
|
3
|
+
import { ovok } from "./src/ovok";
|
|
4
|
+
|
|
5
|
+
export default function App() {
|
|
6
|
+
const [email, setEmail] = useState("");
|
|
7
|
+
const [password, setPassword] = useState("");
|
|
8
|
+
const [status, setStatus] = useState("Sign in with a patient account.");
|
|
9
|
+
|
|
10
|
+
async function signIn() {
|
|
11
|
+
try {
|
|
12
|
+
const result = await ovok.auth.login({ profileType: "Patient", email, password });
|
|
13
|
+
setStatus(result.status === "signed-in" ? "Signed in." : "This account needs a second factor.");
|
|
14
|
+
} catch (error) {
|
|
15
|
+
setStatus(error instanceof Error ? error.message : String(error));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View style={styles.screen}>
|
|
21
|
+
<Text style={styles.status} accessibilityLiveRegion="polite">
|
|
22
|
+
{status}
|
|
23
|
+
</Text>
|
|
24
|
+
<TextInput
|
|
25
|
+
style={styles.input}
|
|
26
|
+
placeholder="Email"
|
|
27
|
+
autoCapitalize="none"
|
|
28
|
+
autoComplete="email"
|
|
29
|
+
keyboardType="email-address"
|
|
30
|
+
value={email}
|
|
31
|
+
onChangeText={setEmail}
|
|
32
|
+
/>
|
|
33
|
+
<TextInput
|
|
34
|
+
style={styles.input}
|
|
35
|
+
placeholder="Password"
|
|
36
|
+
autoComplete="current-password"
|
|
37
|
+
secureTextEntry
|
|
38
|
+
value={password}
|
|
39
|
+
onChangeText={setPassword}
|
|
40
|
+
/>
|
|
41
|
+
<Button title="Sign in" onPress={signIn} />
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
screen: { flex: 1, justifyContent: "center", gap: 12, padding: 24 },
|
|
48
|
+
status: { fontSize: 16 },
|
|
49
|
+
input: { borderWidth: 1, borderRadius: 8, padding: 12 },
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ExpoConfig } from "expo/config";
|
|
2
|
+
|
|
3
|
+
process.loadEnvFile("../../.env");
|
|
4
|
+
|
|
5
|
+
const config: ExpoConfig = {
|
|
6
|
+
name: "{{folder}}",
|
|
7
|
+
slug: "{{workspace}}-{{folder}}",
|
|
8
|
+
version: "1.0.0",
|
|
9
|
+
orientation: "portrait",
|
|
10
|
+
platforms: [{{platforms}}],
|
|
11
|
+
ios: { bundleIdentifier: "{{bundleId}}" },
|
|
12
|
+
android: { package: "{{bundleId}}" },
|
|
13
|
+
plugins: [
|
|
14
|
+
"expo-secure-store",
|
|
15
|
+
[
|
|
16
|
+
"react-native-ble-plx",
|
|
17
|
+
{
|
|
18
|
+
isBackgroundEnabled: false,
|
|
19
|
+
bluetoothAlwaysPermission: "Connect to your measurement devices to receive readings.",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
["react-native-permissions", { iosPermissions: ["Bluetooth"] }],
|
|
23
|
+
],
|
|
24
|
+
extra: {
|
|
25
|
+
ovokBaseUrl: process.env.OVOK_BASE_URL,
|
|
26
|
+
ovokTenantCode: process.env.OVOK_TENANT_CODE,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default config;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { registerRootComponent } from "expo";
|
|
2
|
+
import { polyfillWebCrypto } from "expo-standard-web-crypto";
|
|
3
|
+
|
|
4
|
+
polyfillWebCrypto();
|
|
5
|
+
|
|
6
|
+
// The Ovok client's PKCE helpers need web crypto before they load.
|
|
7
|
+
const App = require("./App").default;
|
|
8
|
+
registerRootComponent(App);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{workspace}}/{{folder}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"ovok": {
|
|
6
|
+
"platform": "mobile"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "expo start --dev-client",
|
|
10
|
+
"ios": "expo run:ios",
|
|
11
|
+
"android": "expo run:android",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@ovok/core": "^0.4.49-rc.4",
|
|
16
|
+
"@ovok/native": "^1.16.1-rc.3",
|
|
17
|
+
"expo": "~57.0.31",
|
|
18
|
+
"expo-constants": "~57.0.22",
|
|
19
|
+
"expo-dev-client": "~57.0.20",
|
|
20
|
+
"expo-secure-store": "~57.0.4",
|
|
21
|
+
"expo-standard-web-crypto": "^57.0.3",
|
|
22
|
+
"react": "19.2.3",
|
|
23
|
+
"react-native": "0.86.3",
|
|
24
|
+
"react-native-ble-plx": "^3.5.1",
|
|
25
|
+
"react-native-permissions": "^5.6.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": "~19.2.0",
|
|
29
|
+
"typescript": "~6.0.3"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Ovok } from "@ovok/core/next";
|
|
2
|
+
import Constants from "expo-constants";
|
|
3
|
+
|
|
4
|
+
type OvokExtra = { ovokBaseUrl: string; ovokTenantCode: string };
|
|
5
|
+
|
|
6
|
+
const extra = Constants.expoConfig?.extra as OvokExtra;
|
|
7
|
+
|
|
8
|
+
export const ovok = new Ovok({
|
|
9
|
+
baseUrl: extra.ovokBaseUrl,
|
|
10
|
+
tenantCode: extra.ovokTenantCode,
|
|
11
|
+
fhirPath: "/fhir/R4/",
|
|
12
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{folder}}</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{workspace}}/{{folder}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"ovok": {
|
|
6
|
+
"platform": "web"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"build": "tsc --noEmit && vite build",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"preview": "vite preview"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@ovok/core": "^0.4.49-rc.4",
|
|
16
|
+
"react": "^19.3.0",
|
|
17
|
+
"react-dom": "^19.3.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@medplum/fhirtypes": "^5.2.11",
|
|
21
|
+
"@types/react": "^19.3.0",
|
|
22
|
+
"@types/react-dom": "^19.3.0",
|
|
23
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
24
|
+
"typescript": "^7.0.2",
|
|
25
|
+
"vite": "^8.3.4"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Patient } from "@medplum/fhirtypes";
|
|
2
|
+
import { type FormEvent, useState } from "react";
|
|
3
|
+
import { ovok } from "./ovok";
|
|
4
|
+
|
|
5
|
+
const PATIENTS_SHOWN = 10;
|
|
6
|
+
|
|
7
|
+
export function App() {
|
|
8
|
+
const [patients, setPatients] = useState<Patient[] | null>(null);
|
|
9
|
+
const [problem, setProblem] = useState<string | null>(null);
|
|
10
|
+
|
|
11
|
+
async function signIn(event: FormEvent<HTMLFormElement>) {
|
|
12
|
+
event.preventDefault();
|
|
13
|
+
const form = new FormData(event.currentTarget);
|
|
14
|
+
setProblem(null);
|
|
15
|
+
try {
|
|
16
|
+
const result = await ovok.auth.login({
|
|
17
|
+
profileType: "Practitioner",
|
|
18
|
+
email: String(form.get("email")),
|
|
19
|
+
password: String(form.get("password")),
|
|
20
|
+
});
|
|
21
|
+
if (result.status !== "signed-in") {
|
|
22
|
+
setProblem("This account needs a second factor: finish with ovok.auth.verifyMfa.");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const page = await ovok.fhir.search("Patient", { _count: String(PATIENTS_SHOWN) });
|
|
26
|
+
setPatients(page.data);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
setProblem(error instanceof Error ? error.message : String(error));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (patients !== null) {
|
|
33
|
+
return (
|
|
34
|
+
<main>
|
|
35
|
+
<h1>Patients</h1>
|
|
36
|
+
<ul>
|
|
37
|
+
{patients.map((patient) => (
|
|
38
|
+
<li key={patient.id}>{nameOf(patient)}</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
</main>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<main>
|
|
47
|
+
<h1>Sign in</h1>
|
|
48
|
+
<form onSubmit={signIn}>
|
|
49
|
+
<label>
|
|
50
|
+
Email <input name="email" type="email" autoComplete="username" required />
|
|
51
|
+
</label>
|
|
52
|
+
<label>
|
|
53
|
+
Password <input name="password" type="password" autoComplete="current-password" required />
|
|
54
|
+
</label>
|
|
55
|
+
<button type="submit">Sign in</button>
|
|
56
|
+
{problem !== null && <p role="alert">{problem}</p>}
|
|
57
|
+
</form>
|
|
58
|
+
</main>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function nameOf(patient: Patient): string {
|
|
63
|
+
const name = patient.name?.[0];
|
|
64
|
+
const parts = [...(name?.given ?? []), name?.family].filter((part) => part !== undefined);
|
|
65
|
+
return parts.length > 0 ? parts.join(" ") : (patient.id ?? "Unnamed patient");
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { App } from "./App";
|
|
4
|
+
|
|
5
|
+
const root = document.getElementById("root");
|
|
6
|
+
if (root === null) throw new Error("index.html has no #root element.");
|
|
7
|
+
|
|
8
|
+
createRoot(root).render(
|
|
9
|
+
<StrictMode>
|
|
10
|
+
<App />
|
|
11
|
+
</StrictMode>,
|
|
12
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"lib": ["es2022", "dom", "dom.iterable"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src", "vite.config.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import react from "@vitejs/plugin-react";
|
|
2
|
+
import { defineConfig, loadEnv } from "vite";
|
|
3
|
+
|
|
4
|
+
const WORKSPACE_ROOT = "../..";
|
|
5
|
+
|
|
6
|
+
export default defineConfig(({ mode }) => {
|
|
7
|
+
const env = loadEnv(mode, WORKSPACE_ROOT, "OVOK_");
|
|
8
|
+
return {
|
|
9
|
+
plugins: [react()],
|
|
10
|
+
// Everything here ships to the browser, so it names each variable rather than a prefix.
|
|
11
|
+
define: {
|
|
12
|
+
"import.meta.env.OVOK_BASE_URL": JSON.stringify(env.OVOK_BASE_URL),
|
|
13
|
+
"import.meta.env.OVOK_TENANT_CODE": JSON.stringify(env.OVOK_TENANT_CODE),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# {{workspace}}
|
|
2
|
+
|
|
3
|
+
{{agentRules}}
|
|
4
|
+
|
|
5
|
+
## This repository
|
|
6
|
+
|
|
7
|
+
- A Turborepo on pnpm workspaces: apps in `apps/`, shared code in `packages/`.
|
|
8
|
+
- `pnpm dev` runs every app; `pnpm turbo run dev --filter <app>` runs one.
|
|
9
|
+
- `OVOK_BASE_URL` and `OVOK_TENANT_CODE` live in the root `.env`, which every app reads.
|
|
10
|
+
- `pnpm turbo gen app` adds a web or mobile app and `pnpm turbo gen package` a shared package.
|
|
11
|
+
`pnpm turbo gen agents` rewrites the Ovok block of every AGENTS.md after `create-ovok` updates.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# {{workspace}}
|
|
2
|
+
|
|
3
|
+
An Ovok workspace made with `create-ovok`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm install
|
|
7
|
+
pnpm dev
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- Apps live in `apps/`. Add one with `pnpm turbo gen app`, a shared package with `pnpm turbo gen package`.
|
|
11
|
+
- The Ovok API and tenant are set in `.env`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{workspace}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"packageManager": "pnpm@10.8.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "turbo run dev",
|
|
7
|
+
"build": "turbo run build",
|
|
8
|
+
"typecheck": "turbo run typecheck"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@turbo/gen": "2.11.10",
|
|
12
|
+
"create-ovok": "^{{createOvokVersion}}",
|
|
13
|
+
"turbo": "2.11.10"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import type { PlopTypes } from "@turbo/gen";
|
|
3
|
+
|
|
4
|
+
type AppAnswers = { name: string; platform: "web" | "mobile"; os?: "both" | "ios" | "android" };
|
|
5
|
+
|
|
6
|
+
/** Each generator hands off to create-ovok, so `turbo gen` and `pnpm create ovok` share one set of templates. */
|
|
7
|
+
function createOvok(args: string[]): void {
|
|
8
|
+
execFileSync("pnpm", ["exec", "create-ovok", ...args], { stdio: "inherit" });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function generator(plop: PlopTypes.NodePlopAPI): void {
|
|
12
|
+
plop.setGenerator("app", {
|
|
13
|
+
description: "Add an Ovok web or mobile app to apps/",
|
|
14
|
+
prompts: [
|
|
15
|
+
{ type: "input", name: "name", message: "App name (its folder under apps/)" },
|
|
16
|
+
{ type: "list", name: "platform", message: "Platform", choices: ["web", "mobile"] },
|
|
17
|
+
{
|
|
18
|
+
type: "list",
|
|
19
|
+
name: "os",
|
|
20
|
+
message: "Mobile OS",
|
|
21
|
+
choices: ["both", "ios", "android"],
|
|
22
|
+
when: (answers: AppAnswers) => answers.platform === "mobile",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
actions: [
|
|
26
|
+
(answers) => {
|
|
27
|
+
const { name, platform, os } = answers as AppAnswers;
|
|
28
|
+
const type = os === undefined || os === "both" ? [] : ["--type", os];
|
|
29
|
+
createOvok(["add", name, "--platform", platform, ...type]);
|
|
30
|
+
return `Added apps/${name}`;
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
plop.setGenerator("package", {
|
|
36
|
+
description: "Add a shared package to packages/",
|
|
37
|
+
prompts: [{ type: "input", name: "name", message: "Package name (its folder under packages/)" }],
|
|
38
|
+
actions: [
|
|
39
|
+
(answers) => {
|
|
40
|
+
const { name } = answers as { name: string };
|
|
41
|
+
createOvok(["add-package", name]);
|
|
42
|
+
return `Added packages/${name}`;
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
plop.setGenerator("agents", {
|
|
48
|
+
description: "Rewrite the Ovok block of every AGENTS.md",
|
|
49
|
+
prompts: [],
|
|
50
|
+
actions: [
|
|
51
|
+
() => {
|
|
52
|
+
createOvok(["agents"]);
|
|
53
|
+
return "Refreshed the Ovok rules";
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turborepo.com/schema.json",
|
|
3
|
+
"globalDependencies": [".env"],
|
|
4
|
+
"globalEnv": ["OVOK_BASE_URL", "OVOK_TENANT_CODE"],
|
|
5
|
+
"tasks": {
|
|
6
|
+
"build": {
|
|
7
|
+
"dependsOn": ["^build"],
|
|
8
|
+
"outputs": ["dist/**"]
|
|
9
|
+
},
|
|
10
|
+
"dev": {
|
|
11
|
+
"cache": false,
|
|
12
|
+
"persistent": true
|
|
13
|
+
},
|
|
14
|
+
"typecheck": {
|
|
15
|
+
"dependsOn": ["^typecheck"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|