@getstrata/cli 0.1.0 → 0.2.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 +25 -5
- package/cli.ts +4 -0
- package/package.json +8 -11
- package/src/commands/dev.ts +20 -0
- package/src/commands/help.ts +7 -0
- package/src/commands/migrate.ts +48 -0
- package/src/commands/run.ts +19 -0
- package/src/commands/start.ts +18 -0
- package/src/index.ts +10 -0
- package/src/resolveApp.ts +77 -0
- package/src/runCli.ts +89 -0
- package/src/spawnBun.ts +19 -0
- package/src/types.ts +39 -0
- package/dist/cli.js +0 -8308
- package/dist/index-sfreg6q3.js +0 -0
package/README.md
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
# @getstrata/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework CLI for Strata apps. Bun stays the runtime, installer, and test runner. `strata` owns app lifecycle and framework commands.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
strata dev
|
|
7
|
+
strata start
|
|
8
|
+
strata migrate
|
|
9
|
+
strata run src/scripts/backfill.ts
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
## How it finds your app
|
|
13
|
+
|
|
14
|
+
From the current working directory, `strata` loads `strata.config.ts` if present, then falls back to conventions:
|
|
15
|
+
|
|
16
|
+
| Setting | Convention |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `preload` | `src/bootstrap/preload.ts` or `src/bootstrap/preloadModules.ts` |
|
|
19
|
+
| `server` | `src/bootstrap/server.ts` |
|
|
20
|
+
| `modulesDirectory` | `src/modules` |
|
|
21
|
+
| `commands` | `src/cli/register.ts` |
|
|
22
|
+
| `migrate` | `src/db/migrate.ts` |
|
|
23
|
+
| `fresh` | `src/db/fresh.ts` |
|
|
24
|
+
|
|
25
|
+
`src/cli/register.ts` can export `commands` or `registerCommands()` to add app-specific commands such as WorkHub's `make:*`, `queue:work`, and `openapi:*`.
|
|
26
|
+
|
|
27
|
+
`strata run <file>` executes a file with the app preload. It is not an alias for `bun run <package.json script>`.
|
|
28
|
+
|
|
29
|
+
## WorkHub
|
|
30
|
+
|
|
31
|
+
In this monorepo, `bun run cli <command>` is an alias for `strata <command>`.
|
package/cli.ts
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Strata CLI —
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Strata CLI — framework commands for Bun apps (dev, start, migrate, run)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -10,29 +10,26 @@
|
|
|
10
10
|
"directory": "packages/strata-cli"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"strata": "./
|
|
13
|
+
"strata": "./cli.ts"
|
|
14
14
|
},
|
|
15
15
|
"exports": {
|
|
16
16
|
".": {
|
|
17
|
-
"import": "./
|
|
18
|
-
"default": "./
|
|
17
|
+
"import": "./src/index.ts",
|
|
18
|
+
"default": "./src/index.ts"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
-
"
|
|
22
|
+
"cli.ts",
|
|
23
|
+
"src",
|
|
23
24
|
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
|
-
"build": "bun build cli.ts --outdir dist --target bun --external bun
|
|
27
|
+
"build": "bun build cli.ts --outdir dist --target bun --external bun",
|
|
27
28
|
"prepublishOnly": "bun run build"
|
|
28
29
|
},
|
|
29
30
|
"publishConfig": {
|
|
30
31
|
"access": "public"
|
|
31
32
|
},
|
|
32
|
-
"peerDependencies": {
|
|
33
|
-
"@getstrata/core": "^0.5.0",
|
|
34
|
-
"typescript": "^5.9.0"
|
|
35
|
-
},
|
|
36
33
|
"engines": {
|
|
37
34
|
"bun": ">=1.1.0"
|
|
38
35
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawnBun } from "../spawnBun.ts";
|
|
2
|
+
import type { StrataAppConfig } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
async function devCommand(app: StrataAppConfig): Promise<void> {
|
|
5
|
+
if (!app.server) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"No server entry found. Add src/bootstrap/server.ts or set server in strata.config.ts.",
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const args = app.preload
|
|
12
|
+
? ["--preload", app.preload, "--watch", app.server]
|
|
13
|
+
: ["--watch", app.server];
|
|
14
|
+
const code = await spawnBun(app, args);
|
|
15
|
+
if (code !== 0) {
|
|
16
|
+
process.exitCode = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { devCommand };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import type { StrataAppConfig } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
type MigrateModule = {
|
|
5
|
+
migrate?: (...args: string[]) => Promise<void> | void;
|
|
6
|
+
seed?: (...args: string[]) => Promise<void> | void;
|
|
7
|
+
fresh?: (...args: string[]) => Promise<void> | void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
async function importEntry(path: string): Promise<MigrateModule> {
|
|
11
|
+
return (await import(pathToFileURL(path).href)) as MigrateModule;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function migrateCommand(app: StrataAppConfig, args: string[]): Promise<void> {
|
|
15
|
+
if (!app.migrate) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"No migrate entry found. Add src/db/migrate.ts or set migrate in strata.config.ts.",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const entry = await importEntry(app.migrate);
|
|
22
|
+
if (typeof entry.migrate !== "function") {
|
|
23
|
+
throw new Error(`${app.migrate} must export a migrate() function.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await entry.migrate(...args);
|
|
27
|
+
|
|
28
|
+
if (typeof entry.seed === "function") {
|
|
29
|
+
await entry.seed(...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function migrateFreshCommand(app: StrataAppConfig, args: string[]): Promise<void> {
|
|
34
|
+
if (app.fresh) {
|
|
35
|
+
const entry = await importEntry(app.fresh);
|
|
36
|
+
if (typeof entry.fresh !== "function") {
|
|
37
|
+
throw new Error(`${app.fresh} must export a fresh() function.`);
|
|
38
|
+
}
|
|
39
|
+
await entry.fresh(...args);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw new Error(
|
|
44
|
+
"No migrate:fresh entry found. Add src/db/fresh.ts or register a migrate:fresh command.",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { migrateCommand, migrateFreshCommand };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { spawnBun } from "../spawnBun.ts";
|
|
3
|
+
import type { StrataAppConfig } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
async function runCommand(app: StrataAppConfig, args: string[]): Promise<void> {
|
|
6
|
+
const [file, ...rest] = args;
|
|
7
|
+
if (!file) {
|
|
8
|
+
throw new Error("run requires a file path. Example: strata run src/scripts/backfill.ts");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const filePath = resolve(app.root, file);
|
|
12
|
+
const bunArgs = app.preload ? ["--preload", app.preload, filePath, ...rest] : [filePath, ...rest];
|
|
13
|
+
const code = await spawnBun(app, bunArgs);
|
|
14
|
+
if (code !== 0) {
|
|
15
|
+
process.exitCode = code;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { runCommand };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { spawnBun } from "../spawnBun.ts";
|
|
2
|
+
import type { StrataAppConfig } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
async function startCommand(app: StrataAppConfig): Promise<void> {
|
|
5
|
+
if (!app.server) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"No server entry found. Add src/bootstrap/server.ts or set server in strata.config.ts.",
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const args = app.preload ? ["--preload", app.preload, app.server] : [app.server];
|
|
12
|
+
const code = await spawnBun(app, args);
|
|
13
|
+
if (code !== 0) {
|
|
14
|
+
process.exitCode = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { startCommand };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { StrataAppConfig, StrataCommandMap, StrataFileConfig } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
function firstExisting(root: string, candidates: string[]): string | undefined {
|
|
7
|
+
for (const candidate of candidates) {
|
|
8
|
+
const absolutePath = join(root, candidate);
|
|
9
|
+
if (existsSync(absolutePath)) {
|
|
10
|
+
return absolutePath;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolvePath(
|
|
18
|
+
root: string,
|
|
19
|
+
value: string | undefined,
|
|
20
|
+
fallbacks: string[],
|
|
21
|
+
): string | undefined {
|
|
22
|
+
if (value) {
|
|
23
|
+
return isAbsolute(value) ? value : join(root, value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return firstExisting(root, fallbacks);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadFileConfig(root: string): Promise<StrataFileConfig> {
|
|
30
|
+
const configPath = firstExisting(root, ["strata.config.ts", "strata.config.js"]);
|
|
31
|
+
if (!configPath) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const loaded = (await import(pathToFileURL(configPath).href)) as {
|
|
36
|
+
default?: StrataFileConfig;
|
|
37
|
+
} & StrataFileConfig;
|
|
38
|
+
|
|
39
|
+
return loaded.default ?? loaded;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function resolveApp(cwd = process.cwd()): Promise<StrataAppConfig> {
|
|
43
|
+
const root = resolve(cwd);
|
|
44
|
+
const file = await loadFileConfig(root);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
root,
|
|
48
|
+
preload: resolvePath(root, file.preload, [
|
|
49
|
+
"src/bootstrap/preload.ts",
|
|
50
|
+
"src/bootstrap/preloadModules.ts",
|
|
51
|
+
]),
|
|
52
|
+
server: resolvePath(root, file.server, ["src/bootstrap/server.ts"]),
|
|
53
|
+
modulesDirectory: resolvePath(root, file.modulesDirectory, ["src/modules"]),
|
|
54
|
+
commandsModule: resolvePath(root, file.commands, ["src/cli/register.ts"]),
|
|
55
|
+
migrate: resolvePath(root, file.migrate, ["src/db/migrate.ts"]),
|
|
56
|
+
fresh: resolvePath(root, file.fresh, ["src/db/fresh.ts"]),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function loadAppCommands(app: StrataAppConfig): Promise<StrataCommandMap> {
|
|
61
|
+
if (!app.commandsModule) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const loaded = (await import(pathToFileURL(app.commandsModule).href)) as {
|
|
66
|
+
commands?: StrataCommandMap;
|
|
67
|
+
registerCommands?: () => StrataCommandMap | Promise<StrataCommandMap>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (typeof loaded.registerCommands === "function") {
|
|
71
|
+
return await loaded.registerCommands();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return loaded.commands ?? {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { loadAppCommands, resolveApp };
|
package/src/runCli.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { devCommand } from "./commands/dev.ts";
|
|
3
|
+
import { printHelp } from "./commands/help.ts";
|
|
4
|
+
import { migrateCommand, migrateFreshCommand } from "./commands/migrate.ts";
|
|
5
|
+
import { runCommand } from "./commands/run.ts";
|
|
6
|
+
import { startCommand } from "./commands/start.ts";
|
|
7
|
+
import { loadAppCommands, resolveApp } from "./resolveApp.ts";
|
|
8
|
+
import type { RunCliOptions, StrataAppConfig, StrataCommandMap } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
function builtinCommands(app: StrataAppConfig): StrataCommandMap {
|
|
11
|
+
const commands: StrataCommandMap = {
|
|
12
|
+
dev: async () => async () => {
|
|
13
|
+
await devCommand(app);
|
|
14
|
+
},
|
|
15
|
+
start: async () => async () => {
|
|
16
|
+
await startCommand(app);
|
|
17
|
+
},
|
|
18
|
+
run:
|
|
19
|
+
async () =>
|
|
20
|
+
async (...args: string[]) => {
|
|
21
|
+
await runCommand(app, args);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (app.migrate) {
|
|
26
|
+
commands.migrate =
|
|
27
|
+
async () =>
|
|
28
|
+
async (...args: string[]) => {
|
|
29
|
+
await migrateCommand(app, args);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (app.fresh) {
|
|
34
|
+
commands["migrate:fresh"] =
|
|
35
|
+
async () =>
|
|
36
|
+
async (...args: string[]) => {
|
|
37
|
+
await migrateFreshCommand(app, args);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return commands;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runCli(options: RunCliOptions = {}): Promise<number> {
|
|
45
|
+
const exitProcess = options.exitProcess ?? true;
|
|
46
|
+
const app = await resolveApp(options.cwd ?? process.cwd());
|
|
47
|
+
|
|
48
|
+
if (!options.skipBoot && app.preload) {
|
|
49
|
+
await import(pathToFileURL(app.preload).href);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const appCommands = options.commands ?? (await loadAppCommands(app));
|
|
53
|
+
const registry: StrataCommandMap = {
|
|
54
|
+
...builtinCommands(app),
|
|
55
|
+
...appCommands,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const commandNames = [...new Set(["help", ...Object.keys(registry)])];
|
|
59
|
+
registry.help = async () => async () => {
|
|
60
|
+
printHelp(commandNames);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const [command = "help", ...args] = options.argv ?? process.argv.slice(2);
|
|
64
|
+
const loadHandler = registry[command];
|
|
65
|
+
|
|
66
|
+
const fail = (code: number): number => {
|
|
67
|
+
if (exitProcess) {
|
|
68
|
+
process.exit(code);
|
|
69
|
+
}
|
|
70
|
+
return code;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (!loadHandler) {
|
|
74
|
+
console.error(`Unknown command: ${command}`);
|
|
75
|
+
printHelp(commandNames);
|
|
76
|
+
return fail(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const handler = await loadHandler();
|
|
81
|
+
await handler(...args);
|
|
82
|
+
return typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(error);
|
|
85
|
+
return fail(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { runCli };
|
package/src/spawnBun.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StrataAppConfig } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
function bunExecutable(): string {
|
|
4
|
+
return process.execPath.includes("bun") ? process.execPath : "bun";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function spawnBun(app: StrataAppConfig, bunArgs: string[]): Promise<number> {
|
|
8
|
+
const proc = Bun.spawn([bunExecutable(), ...bunArgs], {
|
|
9
|
+
cwd: app.root,
|
|
10
|
+
stdout: "inherit",
|
|
11
|
+
stderr: "inherit",
|
|
12
|
+
stdin: "inherit",
|
|
13
|
+
env: process.env,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return await proc.exited;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { bunExecutable, spawnBun };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type StrataCommand = (...args: string[]) => Promise<void> | void;
|
|
2
|
+
type StrataCommandLoader = () => Promise<StrataCommand>;
|
|
3
|
+
type StrataCommandMap = Record<string, StrataCommandLoader>;
|
|
4
|
+
|
|
5
|
+
type StrataFileConfig = {
|
|
6
|
+
preload?: string;
|
|
7
|
+
server?: string;
|
|
8
|
+
modulesDirectory?: string;
|
|
9
|
+
commands?: string;
|
|
10
|
+
migrate?: string;
|
|
11
|
+
fresh?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type StrataAppConfig = {
|
|
15
|
+
root: string;
|
|
16
|
+
preload?: string;
|
|
17
|
+
server?: string;
|
|
18
|
+
modulesDirectory?: string;
|
|
19
|
+
commandsModule?: string;
|
|
20
|
+
migrate?: string;
|
|
21
|
+
fresh?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type RunCliOptions = {
|
|
25
|
+
argv?: string[];
|
|
26
|
+
cwd?: string;
|
|
27
|
+
commands?: StrataCommandMap;
|
|
28
|
+
skipBoot?: boolean;
|
|
29
|
+
exitProcess?: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type {
|
|
33
|
+
RunCliOptions,
|
|
34
|
+
StrataAppConfig,
|
|
35
|
+
StrataCommand,
|
|
36
|
+
StrataCommandLoader,
|
|
37
|
+
StrataCommandMap,
|
|
38
|
+
StrataFileConfig,
|
|
39
|
+
};
|