@outfitter/presets 0.2.1 → 0.3.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/package.json +2 -2
- package/presets/_base/AGENTS.md.template +3 -0
- package/presets/_base/CLAUDE.md.template +35 -0
- package/presets/_examples/cli-todo/src/program.ts.template +202 -0
- package/presets/_examples/mcp-files/src/mcp.ts.template +181 -0
- package/presets/basic/README.md.template +22 -0
- package/presets/basic/package.json.template +41 -37
- package/presets/basic/src/index.test.ts.template +26 -0
- package/presets/basic/src/index.ts.template +32 -15
- package/presets/basic/tsconfig.json.template +28 -29
- package/presets/cli/CLAUDE.md.template +60 -0
- package/presets/cli/README.md.template +5 -1
- package/presets/cli/package.json.template +47 -44
- package/presets/cli/src/commands/hello.ts.template +26 -0
- package/presets/cli/src/index.test.ts.template +38 -0
- package/presets/cli/src/index.ts.template +2 -0
- package/presets/cli/src/program.ts.template +17 -19
- package/presets/cli/src/types.ts.template +13 -0
- package/presets/cli/tsconfig.json.template +29 -29
- package/presets/daemon/CLAUDE.md.template +53 -0
- package/presets/daemon/README.md.template +6 -7
- package/presets/daemon/package.json.template +50 -47
- package/presets/daemon/src/cli.ts.template +73 -66
- package/presets/daemon/src/daemon-main.ts.template +56 -55
- package/presets/daemon/src/daemon.ts.template +7 -3
- package/presets/daemon/src/index.test.ts.template +9 -0
- package/presets/daemon/tsconfig.json.template +21 -21
- package/presets/full-stack/CLAUDE.md.template +66 -0
- package/presets/full-stack/apps/cli/package.json.template +34 -33
- package/presets/full-stack/apps/cli/src/cli.ts.template +16 -15
- package/presets/full-stack/apps/cli/src/index.test.ts.template +12 -11
- package/presets/full-stack/apps/cli/tsconfig.json.template +32 -32
- package/presets/full-stack/apps/mcp/package.json.template +35 -34
- package/presets/full-stack/apps/mcp/src/index.test.ts.template +12 -11
- package/presets/full-stack/apps/mcp/src/mcp.ts.template +10 -10
- package/presets/full-stack/apps/mcp/src/server.ts.template +3 -2
- package/presets/full-stack/apps/mcp/tsconfig.json.template +32 -32
- package/presets/full-stack/package.json.template +20 -14
- package/presets/full-stack/packages/core/package.json.template +31 -30
- package/presets/full-stack/packages/core/src/handlers.ts.template +29 -24
- package/presets/full-stack/packages/core/src/index.test.ts.template +23 -21
- package/presets/full-stack/packages/core/src/types.ts.template +11 -8
- package/presets/full-stack/packages/core/tsconfig.json.template +29 -29
- package/presets/library/CLAUDE.md.template +68 -0
- package/presets/library/bunup.config.ts.template +16 -16
- package/presets/library/package.json.template +51 -50
- package/presets/library/src/handlers.ts.template +51 -27
- package/presets/library/src/index.test.ts.template +40 -29
- package/presets/library/src/types.ts.template +14 -8
- package/presets/library/tsconfig.json.template +29 -29
- package/presets/mcp/CLAUDE.md.template +97 -0
- package/presets/mcp/README.md.template +12 -9
- package/presets/mcp/package.json.template +48 -44
- package/presets/mcp/src/index.test.ts.template +49 -0
- package/presets/mcp/src/index.ts.template +2 -0
- package/presets/mcp/src/mcp.ts.template +16 -16
- package/presets/mcp/src/server.ts.template +8 -1
- package/presets/mcp/src/tools/hello.ts.template +48 -0
- package/presets/mcp/tsconfig.json.template +21 -21
- package/presets/minimal/README.md.template +22 -0
- package/presets/minimal/package.json.template +47 -44
- package/presets/minimal/src/index.test.ts.template +19 -0
- package/presets/minimal/src/index.ts.template +10 -15
- package/presets/minimal/tsconfig.json.template +28 -29
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* {{projectName}} CLI entry point
|
|
4
4
|
*
|
|
5
|
-
* Commands: start, stop,
|
|
5
|
+
* Commands: start, stop, status
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createCLI, command } from "@outfitter/cli";
|
|
9
|
-
import { createLogger } from "@outfitter/logging";
|
|
10
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
import { createCLI, command } from "@outfitter/cli/command";
|
|
11
11
|
import { getSocketPath, getLockPath, isDaemonAlive } from "@outfitter/daemon";
|
|
12
|
+
import { createLogger } from "@outfitter/logging";
|
|
12
13
|
|
|
13
14
|
const logger = createLogger({ name: "{{binName}}" });
|
|
14
15
|
|
|
@@ -17,80 +18,86 @@ const socketPath = getSocketPath(TOOL_NAME);
|
|
|
17
18
|
const lockPath = getLockPath(TOOL_NAME);
|
|
18
19
|
|
|
19
20
|
const program = createCLI({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
name: "{{binName}}",
|
|
22
|
+
version: "{{version}}",
|
|
23
|
+
description: "{{description}}",
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
program.register(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
command("start")
|
|
28
|
+
.description("Start the daemon")
|
|
29
|
+
.option("-f, --foreground", "Run in foreground")
|
|
30
|
+
.action(async ({ flags }) => {
|
|
31
|
+
const foreground = Boolean(
|
|
32
|
+
(flags as { foreground?: boolean }).foreground
|
|
33
|
+
);
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
if (await isDaemonAlive(lockPath)) {
|
|
36
|
+
logger.warn("Daemon is already running");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
if (foreground) {
|
|
41
|
+
// Run in foreground - import and run daemon directly
|
|
42
|
+
const { runDaemon } = await import("./daemon-main.js");
|
|
43
|
+
await runDaemon();
|
|
44
|
+
} else {
|
|
45
|
+
// Spawn daemon in background
|
|
46
|
+
const daemon = spawn(
|
|
47
|
+
process.execPath,
|
|
48
|
+
[import.meta.dir + "/daemon.js"],
|
|
49
|
+
{
|
|
50
|
+
detached: true,
|
|
51
|
+
stdio: "ignore",
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
daemon.unref();
|
|
55
|
+
logger.info(`Daemon started with PID ${daemon.pid}`);
|
|
56
|
+
}
|
|
57
|
+
})
|
|
51
58
|
);
|
|
52
59
|
|
|
53
60
|
program.register(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
command("stop")
|
|
62
|
+
.description("Stop the daemon")
|
|
63
|
+
.action(async () => {
|
|
64
|
+
if (!(await isDaemonAlive(lockPath))) {
|
|
65
|
+
logger.warn("Daemon is not running");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// Signal daemon to stop via HTTP
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`http://unix:${socketPath}:/shutdown`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
});
|
|
73
|
+
if (response.ok) {
|
|
74
|
+
logger.info("Daemon stopped");
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
logger.error("Failed to stop daemon");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
})
|
|
74
81
|
);
|
|
75
82
|
|
|
76
83
|
program.register(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
command("status")
|
|
85
|
+
.description("Check daemon status")
|
|
86
|
+
.action(async () => {
|
|
87
|
+
if (await isDaemonAlive(lockPath)) {
|
|
88
|
+
logger.info("Daemon is running");
|
|
89
|
+
// Fetch health info
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(`http://unix:${socketPath}:/health`);
|
|
92
|
+
const health = await response.json();
|
|
93
|
+
console.log(JSON.stringify(health, null, 2));
|
|
94
|
+
} catch {
|
|
95
|
+
logger.warn("Could not fetch health info");
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
logger.info("Daemon is not running");
|
|
99
|
+
}
|
|
100
|
+
})
|
|
94
101
|
);
|
|
95
102
|
|
|
96
103
|
program.parse(process.argv);
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
* {{projectName}} daemon main logic
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
6
|
+
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
getSocketPath,
|
|
9
|
+
getLockPath,
|
|
10
|
+
getDaemonDir,
|
|
11
|
+
acquireDaemonLock,
|
|
12
|
+
releaseDaemonLock,
|
|
12
13
|
} from "@outfitter/daemon";
|
|
13
|
-
import {
|
|
14
|
+
import { createLogger } from "@outfitter/logging";
|
|
14
15
|
|
|
15
16
|
const logger = createLogger({ name: "{{binName}}d" });
|
|
16
17
|
|
|
@@ -18,62 +19,62 @@ const TOOL_NAME = "{{binName}}";
|
|
|
18
19
|
const startTime = Date.now();
|
|
19
20
|
|
|
20
21
|
export async function runDaemon(): Promise<void> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const socketPath = getSocketPath(TOOL_NAME);
|
|
23
|
+
const lockPath = getLockPath(TOOL_NAME);
|
|
24
|
+
const daemonDir = getDaemonDir(TOOL_NAME);
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// Ensure daemon directory exists
|
|
27
|
+
await mkdir(daemonDir, { recursive: true });
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// Acquire lock
|
|
30
|
+
const lockResult = await acquireDaemonLock(lockPath);
|
|
31
|
+
if (!lockResult.isOk()) {
|
|
32
|
+
logger.error(`Failed to acquire lock: ${lockResult.error.message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const lock = lockResult.value;
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
logger.info(`Daemon starting on ${socketPath}`);
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
// Create HTTP server on Unix socket
|
|
40
|
+
const server = Bun.serve({
|
|
41
|
+
unix: socketPath,
|
|
42
|
+
fetch(request) {
|
|
43
|
+
const url = new URL(request.url);
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
if (url.pathname === "/health") {
|
|
46
|
+
return Response.json({
|
|
47
|
+
status: "ok",
|
|
48
|
+
uptime: Date.now() - startTime,
|
|
49
|
+
version: "{{version}}",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
if (url.pathname === "/shutdown" && request.method === "POST") {
|
|
54
|
+
logger.info("Shutdown requested");
|
|
55
|
+
// Schedule shutdown
|
|
56
|
+
setTimeout(async () => {
|
|
57
|
+
await releaseDaemonLock(lock);
|
|
58
|
+
server.stop();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}, 100);
|
|
61
|
+
return new Response("Shutting down");
|
|
62
|
+
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
return new Response("Not found", { status: 404 });
|
|
65
|
+
},
|
|
66
|
+
});
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
// Handle signals
|
|
69
|
+
const shutdown = async () => {
|
|
70
|
+
logger.info("Received shutdown signal");
|
|
71
|
+
await releaseDaemonLock(lock);
|
|
72
|
+
server.stop();
|
|
73
|
+
process.exit(0);
|
|
74
|
+
};
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
process.on("SIGTERM", shutdown);
|
|
77
|
+
process.on("SIGINT", shutdown);
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
logger.info("Daemon running");
|
|
79
80
|
}
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* {{projectName}} daemon entry point
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createLogger } from "@outfitter/logging";
|
|
7
|
+
|
|
6
8
|
import { runDaemon } from "./daemon-main.js";
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const logger = createLogger({ name: "{{binName}}d" });
|
|
11
|
+
|
|
12
|
+
runDaemon().catch((error: unknown) => {
|
|
13
|
+
logger.error("Daemon failed", { error });
|
|
14
|
+
process.exit(1);
|
|
11
15
|
});
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationDir": "dist",
|
|
12
|
+
"outDir": "dist",
|
|
13
|
+
"rootDir": "src",
|
|
14
|
+
"types": ["bun-types"],
|
|
15
|
+
"noImplicitAny": true,
|
|
16
|
+
"strictNullChecks": true,
|
|
17
|
+
"noUncheckedIndexedAccess": true,
|
|
18
|
+
"exactOptionalPropertyTypes": true,
|
|
19
|
+
"noPropertyAccessFromIndexSignature": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["src"],
|
|
22
|
+
"exclude": ["node_modules", "dist"]
|
|
23
23
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Bun-first TypeScript monorepo. Tests before code. Result types, not exceptions.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Root (all packages)
|
|
9
|
+
bun run build # Build all packages (Bun workspaces)
|
|
10
|
+
bun run dev # Dev mode for all packages
|
|
11
|
+
bun run test # Test all packages
|
|
12
|
+
bun run typecheck # TypeScript validation
|
|
13
|
+
bun run check # Lint checks (all packages)
|
|
14
|
+
bun run lint:fix # Auto-fix lint issues (all packages)
|
|
15
|
+
bun run format # Auto-fix formatting (all packages)
|
|
16
|
+
bun run verify:ci # Full CI validation (typecheck + check + build + test)
|
|
17
|
+
|
|
18
|
+
# Single package
|
|
19
|
+
bun run --filter={{packageName}}-core build
|
|
20
|
+
bun run --filter={{packageName}}-core test
|
|
21
|
+
cd packages/core && bun test
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
Monorepo with shared core library, CLI app, and MCP server.
|
|
27
|
+
|
|
28
|
+
### Project Structure
|
|
29
|
+
|
|
30
|
+
- `packages/core/` — Shared handlers and types (library)
|
|
31
|
+
- `apps/cli/` — CLI application (thin adapter over core handlers)
|
|
32
|
+
- `apps/mcp/` — MCP server (thin adapter over core handlers)
|
|
33
|
+
|
|
34
|
+
### Handler Contract
|
|
35
|
+
|
|
36
|
+
All domain logic lives in `packages/core/` as transport-agnostic handlers returning `Result<T, E>`:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
async function handler(
|
|
40
|
+
input: unknown,
|
|
41
|
+
ctx: HandlerContext
|
|
42
|
+
): Promise<Result<Output, ValidationError>>;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
CLI and MCP are thin adapters over shared handlers. Write the handler once in core, expose it everywhere.
|
|
46
|
+
|
|
47
|
+
### Adding a Feature
|
|
48
|
+
|
|
49
|
+
1. Define types and Zod schema in `packages/core/src/types.ts`
|
|
50
|
+
2. Implement handler in `packages/core/src/handlers.ts` returning `Result<T, E>`
|
|
51
|
+
3. Add tests in `packages/core/src/<name>.test.ts`
|
|
52
|
+
4. Wire CLI command in `apps/cli/src/` using CommandBuilder
|
|
53
|
+
5. Wire MCP tool in `apps/mcp/src/` using `defineTool()`
|
|
54
|
+
|
|
55
|
+
## Development Principles
|
|
56
|
+
|
|
57
|
+
- **TDD-First** — Write the test before the code (Red / Green / Refactor)
|
|
58
|
+
- **Result Types** — Handlers return `Result<T, E>`, not exceptions
|
|
59
|
+
- **Bun-First** — Use Bun-native APIs before npm packages
|
|
60
|
+
- **Strict TypeScript** — No `any`, no `as` casts; narrow instead of assert
|
|
61
|
+
|
|
62
|
+
## Testing
|
|
63
|
+
|
|
64
|
+
- Runner: Bun test runner
|
|
65
|
+
- Files: `src/*.test.ts` within each package
|
|
66
|
+
- Run: `bun test` (per package) or `bun run test` (all packages from root)
|
|
@@ -1,35 +1,36 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
2
|
+
"name": "{{packageName}}-cli",
|
|
3
|
+
"version": "{{version}}",
|
|
4
|
+
"description": "CLI surface for {{projectName}}",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"{{projectName}}": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "bun build src/cli.ts --outdir dist --target bun && bun build src/index.ts --outdir dist --target bun --sourcemap",
|
|
20
|
+
"dev": "bun --watch src/cli.ts",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"test:watch": "bun test --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"lint": "oxlint .",
|
|
25
|
+
"lint:fix": "oxlint --fix .",
|
|
26
|
+
"format": "oxfmt --write .",
|
|
27
|
+
"check": "ultracite check",
|
|
28
|
+
"clean:artifacts": "rm -rf dist .turbo"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@outfitter/cli": "workspace:*",
|
|
32
|
+
"@outfitter/contracts": "workspace:*",
|
|
33
|
+
"{{packageName}}-core": "workspace:*"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {}
|
|
35
36
|
}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import { createContext } from "@outfitter/contracts";
|
|
2
1
|
import { output } from "@outfitter/cli";
|
|
2
|
+
import { createContext } from "@outfitter/contracts";
|
|
3
3
|
import { createGreeting } from "{{packageName}}-core";
|
|
4
4
|
|
|
5
|
-
export async function runCli(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
export async function runCli(
|
|
6
|
+
argv: readonly string[] = process.argv
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
const args = argv.slice(2);
|
|
9
|
+
const name = args.find((arg) => !arg.startsWith("-")) ?? "World";
|
|
10
|
+
const excited = args.includes("--excited");
|
|
11
|
+
const result = await createGreeting(
|
|
12
|
+
{ name, excited },
|
|
13
|
+
createContext({ cwd: process.cwd(), env: process.env })
|
|
14
|
+
);
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
if (result.isErr()) {
|
|
17
|
+
throw result.error;
|
|
18
|
+
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
await output(result.value.message, { mode: "human" });
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
if (import.meta.main) {
|
|
23
|
-
|
|
24
|
+
await runCli();
|
|
24
25
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
2
3
|
import { createContext } from "@outfitter/contracts";
|
|
3
4
|
import { createGreeting } from "{{packageName}}-core";
|
|
4
5
|
|
|
5
6
|
describe("cli surface", () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
test("can use shared core handler", async () => {
|
|
8
|
+
const result = await createGreeting(
|
|
9
|
+
{ name: "CLI", excited: true },
|
|
10
|
+
createContext({ cwd: process.cwd(), env: process.env })
|
|
11
|
+
);
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
expect(result.isOk()).toBe(true);
|
|
14
|
+
if (result.isErr()) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
expect(result.value.message).toBe("Hello, CLI!");
|
|
18
|
+
});
|
|
18
19
|
});
|