@tac0de/project-bootstrap-mcp 1.0.1 → 1.1.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 CHANGED
@@ -45,7 +45,40 @@ This project turns the MCP (Management Control Plane) bootstrap documentation in
45
45
  - **Can the project goal be explained in one sentence?** Yes — “Expose MCP bootstrap docs as runnable API/CLI tooling ready for npm/GitHub distribution.”
46
46
  - **Can success be evaluated without subjective judgment?** Yes — API functionality and `npm test` results provide objective verification.
47
47
 
48
- ## Implementation & Usage
48
+ ## MCP Server Usage
49
+
50
+ ### Installation
51
+
52
+ - `npm install -g project-bootstrap-mcp` (or `npm install project-bootstrap-mcp` for local use).
53
+
54
+ ### Run (stdio)
55
+
56
+ - `project-bootstrap-mcp` (or `npm start`) launches the server on port 4000 unless overridden.
57
+ - Flags: `--port <number>` or `-p <number>` (or set the `PORT` env var) to change the listening port.
58
+
59
+ ### Example MCP Client Config
60
+
61
+ ```json
62
+ {
63
+ "command": "npx",
64
+ "args": ["-y", "project-bootstrap-mcp"]
65
+ }
66
+ ```
67
+
68
+ ### Tools
69
+
70
+ - `bootstrap_list` → `GET /api/bootstraps`
71
+ - `bootstrap_get` → `GET /api/bootstraps/:id`
72
+ - `bootstrap_create` → `POST /api/bootstraps`
73
+ - `service_status` → `GET /api/status`
74
+
75
+ ### Documents
76
+
77
+ - `scope` → documents the allowed MCP actions and active MCPs (mirrors `examples/AGENTS.md`).
78
+ - `behavior` → describes feature behavior expectations in the README PRD section.
79
+ - `forbidden` → clarifies forbidden actions via the Agents contract (`contracts/agents.contract.md`).
80
+ - `contract` → references the PRD/TRD contract requirements.
81
+ - `bug_report_template` → placeholder for future debugging templates (add once defined).
49
82
 
50
83
  ### API Overview
51
84
 
@@ -56,7 +89,7 @@ This project turns the MCP (Management Control Plane) bootstrap documentation in
56
89
 
57
90
  ### CLI / Execution
58
91
 
59
- - `npm start` or the `project-bootstrap-mcp` binary starts the server on port 4000 by default.
92
+ - `project-bootstrap-mcp` binary (installed globally) or `npm start` starts the server on port 4000 by default.
60
93
  - Override the port with `--port` or `-p` (e.g., `project-bootstrap-mcp --port 5000`), or set the `PORT` environment variable.
61
94
 
62
95
  ### Testing & Validation
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const startServer = require('../src/server');
2
+ const startServer = require('../dist/server').default;
3
3
 
4
4
  function parsePort(args, envPort) {
5
5
  let portValue = envPort || '4000';
package/dist/app.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import express from 'express';
2
+ import BootstrapStore from './store';
3
+ interface CreateAppOptions {
4
+ store?: BootstrapStore;
5
+ }
6
+ export default function createApp(options?: CreateAppOptions): {
7
+ app: express.Express;
8
+ store: BootstrapStore;
9
+ };
10
+ export {};
11
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAqD,MAAM,SAAS,CAAC;AAG5E,OAAO,cAAc,MAAM,SAAS,CAAC;AAGrC,UAAU,gBAAgB;IACxB,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,OAAO,GAAE,gBAAqB;;;EA8D/D"}
package/dist/app.js ADDED
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = createApp;
7
+ const express_1 = __importDefault(require("express"));
8
+ const package_json_1 = __importDefault(require("../package.json"));
9
+ const validators_1 = require("./validators");
10
+ const store_1 = __importDefault(require("./store"));
11
+ function createApp(options = {}) {
12
+ const store = options.store ?? new store_1.default();
13
+ const app = (0, express_1.default)();
14
+ app.use(express_1.default.json());
15
+ app.get('/api/status', (_req, res) => {
16
+ res.json({
17
+ service: package_json_1.default.name,
18
+ version: package_json_1.default.version,
19
+ uptime: process.uptime(),
20
+ timestamp: new Date().toISOString(),
21
+ });
22
+ });
23
+ app.get('/api/bootstraps', (_req, res) => {
24
+ res.json({ items: store.list() });
25
+ });
26
+ app.post('/api/bootstraps', (req, res) => {
27
+ const validation = (0, validators_1.validateBootstrapPayload)(req.body);
28
+ if (!validation.valid) {
29
+ return res.status(400).json({ errors: validation.errors });
30
+ }
31
+ const payload = {
32
+ name: req.body.name,
33
+ description: req.body.description,
34
+ primaryAction: req.body.primaryAction,
35
+ targetPlatforms: Array.isArray(req.body.targetPlatforms)
36
+ ? req.body.targetPlatforms.map(String)
37
+ : [],
38
+ successCriteria: Array.isArray(req.body.successCriteria)
39
+ ? req.body.successCriteria.map(String)
40
+ : [],
41
+ steps: Array.isArray(req.body.steps)
42
+ ? req.body.steps
43
+ : [],
44
+ };
45
+ const saved = store.add(payload);
46
+ res.status(201).json(saved);
47
+ });
48
+ app.get('/api/bootstraps/:id', (req, res) => {
49
+ const record = store.find(req.params.id);
50
+ if (!record) {
51
+ return res.status(404).json({ error: 'bootstrap not found' });
52
+ }
53
+ res.json(record);
54
+ });
55
+ app.use((_req, res) => {
56
+ res.status(404).json({ error: 'not found' });
57
+ });
58
+ app.use((err, _req, res, next) => {
59
+ // eslint-disable-next-line no-console
60
+ console.error('internal error', err);
61
+ res.status(500).json({ error: 'internal server error' });
62
+ next(err);
63
+ });
64
+ return { app, store };
65
+ }
@@ -0,0 +1,5 @@
1
+ import BootstrapStore from './store';
2
+ import createApp from './app';
3
+ import startServer from './server';
4
+ export { BootstrapStore, createApp, startServer };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,SAAS,CAAC;AACrC,OAAO,SAAS,MAAM,OAAO,CAAC;AAC9B,OAAO,WAAW,MAAM,UAAU,CAAC;AAEnC,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startServer = exports.createApp = exports.BootstrapStore = void 0;
7
+ const store_1 = __importDefault(require("./store"));
8
+ exports.BootstrapStore = store_1.default;
9
+ const app_1 = __importDefault(require("./app"));
10
+ exports.createApp = app_1.default;
11
+ const server_1 = __importDefault(require("./server"));
12
+ exports.startServer = server_1.default;
@@ -0,0 +1,12 @@
1
+ import type { Express } from 'express';
2
+ import type { Server } from 'http';
3
+ export interface StartServerOptions {
4
+ port?: number;
5
+ onStart?: (message: string, port: number) => void;
6
+ }
7
+ export interface StartServerResult {
8
+ app: Express;
9
+ server: Server;
10
+ }
11
+ export default function startServer({ port, onStart, }?: StartServerOptions): StartServerResult;
12
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnD;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,IAAI,EACJ,OAAO,GACR,GAAE,kBAAuB,GAAG,iBAAiB,CA0B7C"}
package/dist/server.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = startServer;
7
+ const app_1 = __importDefault(require("./app"));
8
+ function startServer({ port, onStart, } = {}) {
9
+ const defaultPort = Number(process.env.PORT) || 4000;
10
+ const resolvedPort = port ?? defaultPort;
11
+ const { app } = (0, app_1.default)();
12
+ const server = app.listen(resolvedPort, () => {
13
+ const message = `Project Bootstrap MCP listening on port ${resolvedPort}`;
14
+ if (typeof onStart === 'function') {
15
+ onStart(message, resolvedPort);
16
+ }
17
+ else {
18
+ // eslint-disable-next-line no-console
19
+ console.log(message);
20
+ }
21
+ });
22
+ const graceful = () => {
23
+ server.close(() => {
24
+ // eslint-disable-next-line no-console
25
+ console.log('Project Bootstrap MCP shutting down');
26
+ process.exit(0);
27
+ });
28
+ };
29
+ process.on('SIGINT', graceful);
30
+ process.on('SIGTERM', graceful);
31
+ return { app, server };
32
+ }
@@ -0,0 +1,9 @@
1
+ import type { BootstrapPayload, BootstrapRecord } from './types';
2
+ export default class BootstrapStore {
3
+ private readonly items;
4
+ private counter;
5
+ list(): BootstrapRecord[];
6
+ add(payload: BootstrapPayload): BootstrapRecord;
7
+ find(id: string): BootstrapRecord | null;
8
+ }
9
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEjE,MAAM,CAAC,OAAO,OAAO,cAAc;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsC;IAC5D,OAAO,CAAC,OAAO,CAAK;IAEb,IAAI,IAAI,eAAe,EAAE;IAIzB,GAAG,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe;IAW/C,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;CAGhD"}
package/dist/store.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class BootstrapStore {
4
+ constructor() {
5
+ this.items = new Map();
6
+ this.counter = 1;
7
+ }
8
+ list() {
9
+ return Array.from(this.items.values());
10
+ }
11
+ add(payload) {
12
+ const id = String(this.counter++);
13
+ const record = {
14
+ id,
15
+ createdAt: new Date().toISOString(),
16
+ ...payload,
17
+ };
18
+ this.items.set(id, record);
19
+ return record;
20
+ }
21
+ find(id) {
22
+ return this.items.get(id) ?? null;
23
+ }
24
+ }
25
+ exports.default = BootstrapStore;
@@ -0,0 +1,17 @@
1
+ export interface BootstrapStep {
2
+ title: string;
3
+ detail: string;
4
+ }
5
+ export interface BootstrapPayload {
6
+ name: string;
7
+ description: string;
8
+ primaryAction: string;
9
+ targetPlatforms?: string[];
10
+ successCriteria?: string[];
11
+ steps: BootstrapStep[];
12
+ }
13
+ export interface BootstrapRecord extends BootstrapPayload {
14
+ id: string;
15
+ createdAt: string;
16
+ }
17
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ export interface ValidationResult {
2
+ valid: boolean;
3
+ errors: string[];
4
+ }
5
+ export declare function validateBootstrapPayload(payload: unknown): ValidationResult;
6
+ //# sourceMappingURL=validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,CAmC3E"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateBootstrapPayload = validateBootstrapPayload;
4
+ function validateBootstrapPayload(payload) {
5
+ const errors = [];
6
+ if (!payload || typeof payload !== 'object') {
7
+ errors.push('payload must be an object');
8
+ return { valid: false, errors };
9
+ }
10
+ const typedPayload = payload;
11
+ const { name, description, primaryAction, steps } = typedPayload;
12
+ if (!name || typeof name !== 'string') {
13
+ errors.push('name is required and must be a string');
14
+ }
15
+ if (!description || typeof description !== 'string') {
16
+ errors.push('description is required and must be a string');
17
+ }
18
+ if (!primaryAction || typeof primaryAction !== 'string') {
19
+ errors.push('primaryAction is required and must be a string');
20
+ }
21
+ if (!Array.isArray(steps) || steps.length === 0) {
22
+ errors.push('steps must be a non-empty array');
23
+ }
24
+ else {
25
+ steps.forEach((step, index) => {
26
+ const currentStep = step;
27
+ if (!currentStep || typeof currentStep.title !== 'string') {
28
+ errors.push(`steps[${index}].title is required`);
29
+ }
30
+ if (!currentStep || typeof currentStep.detail !== 'string') {
31
+ errors.push(`steps[${index}].detail is required`);
32
+ }
33
+ });
34
+ }
35
+ return { valid: errors.length === 0, errors };
36
+ }
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@tac0de/project-bootstrap-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Project Bootstrap MCP API tooling for launching MCP bootstrap definitions and documentation.",
5
- "main": "src/index.js",
5
+ "main": "dist/index.js",
6
6
  "bin": {
7
7
  "project-bootstrap-mcp": "bin/project-bootstrap-mcp.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node ./bin/project-bootstrap-mcp.js",
11
- "test": "node --test test/store.test.js"
10
+ "build": "tsc",
11
+ "start": "npm run build && node ./bin/project-bootstrap-mcp.js",
12
+ "test": "node --test -r ts-node/register test/store.test.ts"
12
13
  },
13
14
  "keywords": [
14
15
  "mcp",
@@ -28,17 +29,25 @@
28
29
  },
29
30
  "files": [
30
31
  "bin",
32
+ "dist",
31
33
  "src",
32
34
  "test",
33
35
  "LICENSE",
34
36
  "README.md",
35
37
  "TRD.md"
36
38
  ],
39
+ "types": "dist/index.d.ts",
37
40
  "repository": {
38
41
  "type": "git",
39
42
  "url": "https://github.com/tac0de/project-bootstrap-mcp"
40
43
  },
41
44
  "dependencies": {
42
45
  "express": "^4.19.2"
46
+ },
47
+ "devDependencies": {
48
+ "@types/express": "^5.0.6",
49
+ "@types/node": "^25.0.3",
50
+ "ts-node": "^10.9.2",
51
+ "typescript": "^5.9.3"
43
52
  }
44
53
  }
package/src/app.ts ADDED
@@ -0,0 +1,73 @@
1
+ import express, { Express, Request, Response, NextFunction } from 'express';
2
+ import pkg from '../package.json';
3
+ import { validateBootstrapPayload } from './validators';
4
+ import BootstrapStore from './store';
5
+ import type { BootstrapPayload } from './types';
6
+
7
+ interface CreateAppOptions {
8
+ store?: BootstrapStore;
9
+ }
10
+
11
+ export default function createApp(options: CreateAppOptions = {}) {
12
+ const store = options.store ?? new BootstrapStore();
13
+ const app: Express = express();
14
+
15
+ app.use(express.json());
16
+
17
+ app.get('/api/status', (_req: Request, res: Response) => {
18
+ res.json({
19
+ service: pkg.name,
20
+ version: pkg.version,
21
+ uptime: process.uptime(),
22
+ timestamp: new Date().toISOString(),
23
+ });
24
+ });
25
+
26
+ app.get('/api/bootstraps', (_req: Request, res: Response) => {
27
+ res.json({ items: store.list() });
28
+ });
29
+
30
+ app.post('/api/bootstraps', (req: Request, res: Response) => {
31
+ const validation = validateBootstrapPayload(req.body);
32
+ if (!validation.valid) {
33
+ return res.status(400).json({ errors: validation.errors });
34
+ }
35
+ const payload: BootstrapPayload = {
36
+ name: req.body.name,
37
+ description: req.body.description,
38
+ primaryAction: req.body.primaryAction,
39
+ targetPlatforms: Array.isArray(req.body.targetPlatforms)
40
+ ? req.body.targetPlatforms.map(String)
41
+ : [],
42
+ successCriteria: Array.isArray(req.body.successCriteria)
43
+ ? req.body.successCriteria.map(String)
44
+ : [],
45
+ steps: Array.isArray(req.body.steps)
46
+ ? (req.body.steps as BootstrapPayload['steps'])
47
+ : [],
48
+ };
49
+ const saved = store.add(payload);
50
+ res.status(201).json(saved);
51
+ });
52
+
53
+ app.get('/api/bootstraps/:id', (req: Request, res: Response) => {
54
+ const record = store.find(req.params.id);
55
+ if (!record) {
56
+ return res.status(404).json({ error: 'bootstrap not found' });
57
+ }
58
+ res.json(record);
59
+ });
60
+
61
+ app.use((_req: Request, res: Response) => {
62
+ res.status(404).json({ error: 'not found' });
63
+ });
64
+
65
+ app.use((err: Error, _req: Request, res: Response, next: NextFunction) => {
66
+ // eslint-disable-next-line no-console
67
+ console.error('internal error', err);
68
+ res.status(500).json({ error: 'internal server error' });
69
+ next(err);
70
+ });
71
+
72
+ return { app, store };
73
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import BootstrapStore from './store';
2
+ import createApp from './app';
3
+ import startServer from './server';
4
+
5
+ export { BootstrapStore, createApp, startServer };
package/src/server.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { Express } from 'express';
2
+ import type { Server } from 'http';
3
+ import createApp from './app';
4
+
5
+ export interface StartServerOptions {
6
+ port?: number;
7
+ onStart?: (message: string, port: number) => void;
8
+ }
9
+
10
+ export interface StartServerResult {
11
+ app: Express;
12
+ server: Server;
13
+ }
14
+
15
+ export default function startServer({
16
+ port,
17
+ onStart,
18
+ }: StartServerOptions = {}): StartServerResult {
19
+ const defaultPort = Number(process.env.PORT) || 4000;
20
+ const resolvedPort = port ?? defaultPort;
21
+ const { app } = createApp();
22
+ const server = app.listen(resolvedPort, () => {
23
+ const message = `Project Bootstrap MCP listening on port ${resolvedPort}`;
24
+ if (typeof onStart === 'function') {
25
+ onStart(message, resolvedPort);
26
+ } else {
27
+ // eslint-disable-next-line no-console
28
+ console.log(message);
29
+ }
30
+ });
31
+
32
+ const graceful = () => {
33
+ server.close(() => {
34
+ // eslint-disable-next-line no-console
35
+ console.log('Project Bootstrap MCP shutting down');
36
+ process.exit(0);
37
+ });
38
+ };
39
+
40
+ process.on('SIGINT', graceful);
41
+ process.on('SIGTERM', graceful);
42
+
43
+ return { app, server };
44
+ }
package/src/store.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { BootstrapPayload, BootstrapRecord } from './types';
2
+
3
+ export default class BootstrapStore {
4
+ private readonly items = new Map<string, BootstrapRecord>();
5
+ private counter = 1;
6
+
7
+ public list(): BootstrapRecord[] {
8
+ return Array.from(this.items.values());
9
+ }
10
+
11
+ public add(payload: BootstrapPayload): BootstrapRecord {
12
+ const id = String(this.counter++);
13
+ const record: BootstrapRecord = {
14
+ id,
15
+ createdAt: new Date().toISOString(),
16
+ ...payload,
17
+ };
18
+ this.items.set(id, record);
19
+ return record;
20
+ }
21
+
22
+ public find(id: string): BootstrapRecord | null {
23
+ return this.items.get(id) ?? null;
24
+ }
25
+ }
package/src/types.ts ADDED
@@ -0,0 +1,18 @@
1
+ export interface BootstrapStep {
2
+ title: string;
3
+ detail: string;
4
+ }
5
+
6
+ export interface BootstrapPayload {
7
+ name: string;
8
+ description: string;
9
+ primaryAction: string;
10
+ targetPlatforms?: string[];
11
+ successCriteria?: string[];
12
+ steps: BootstrapStep[];
13
+ }
14
+
15
+ export interface BootstrapRecord extends BootstrapPayload {
16
+ id: string;
17
+ createdAt: string;
18
+ }
@@ -1,11 +1,20 @@
1
- function validateBootstrapPayload(payload) {
2
- const errors = [];
1
+ import type { BootstrapPayload, BootstrapStep } from './types';
2
+
3
+ export interface ValidationResult {
4
+ valid: boolean;
5
+ errors: string[];
6
+ }
7
+
8
+ export function validateBootstrapPayload(payload: unknown): ValidationResult {
9
+ const errors: string[] = [];
3
10
  if (!payload || typeof payload !== 'object') {
4
11
  errors.push('payload must be an object');
5
12
  return { valid: false, errors };
6
13
  }
7
14
 
8
- const { name, description, primaryAction, steps } = payload;
15
+ const typedPayload = payload as BootstrapPayload;
16
+ const { name, description, primaryAction, steps } = typedPayload;
17
+
9
18
  if (!name || typeof name !== 'string') {
10
19
  errors.push('name is required and must be a string');
11
20
  }
@@ -15,14 +24,16 @@ function validateBootstrapPayload(payload) {
15
24
  if (!primaryAction || typeof primaryAction !== 'string') {
16
25
  errors.push('primaryAction is required and must be a string');
17
26
  }
27
+
18
28
  if (!Array.isArray(steps) || steps.length === 0) {
19
29
  errors.push('steps must be a non-empty array');
20
30
  } else {
21
31
  steps.forEach((step, index) => {
22
- if (!step || typeof step.title !== 'string') {
32
+ const currentStep = step as BootstrapStep | undefined;
33
+ if (!currentStep || typeof currentStep.title !== 'string') {
23
34
  errors.push(`steps[${index}].title is required`);
24
35
  }
25
- if (!step || typeof step.detail !== 'string') {
36
+ if (!currentStep || typeof currentStep.detail !== 'string') {
26
37
  errors.push(`steps[${index}].detail is required`);
27
38
  }
28
39
  });
@@ -30,5 +41,3 @@ function validateBootstrapPayload(payload) {
30
41
 
31
42
  return { valid: errors.length === 0, errors };
32
43
  }
33
-
34
- module.exports = { validateBootstrapPayload };
@@ -1,7 +1,7 @@
1
- const test = require('node:test');
2
- const assert = require('node:assert');
3
- const BootstrapStore = require('../src/store');
4
- const { validateBootstrapPayload } = require('../src/validators');
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import BootstrapStore from '../src/store';
4
+ import { validateBootstrapPayload } from '../src/validators';
5
5
 
6
6
  test('BootstrapStore should add and list records', () => {
7
7
  const store = new BootstrapStore();
@@ -9,9 +9,7 @@ test('BootstrapStore should add and list records', () => {
9
9
  name: 'bootstrap-a',
10
10
  description: 'test',
11
11
  primaryAction: 'deploy',
12
- steps: [
13
- { title: 'step1', detail: 'run' },
14
- ],
12
+ steps: [{ title: 'step1', detail: 'run' }],
15
13
  };
16
14
 
17
15
  const record = store.add(payload);
package/src/app.js DELETED
@@ -1,71 +0,0 @@
1
- const express = require('express');
2
- const path = require('path');
3
- const { validateBootstrapPayload } = require('./validators');
4
- const BootstrapStore = require('./store');
5
-
6
- const pkg = require(path.join(__dirname, '..', 'package.json'));
7
-
8
- function createApp(options = {}) {
9
- const store = options.store || new BootstrapStore();
10
- const app = express();
11
-
12
- app.use(express.json());
13
-
14
- app.get('/api/status', (req, res) => {
15
- res.json({
16
- service: pkg.name,
17
- version: pkg.version,
18
- uptime: process.uptime(),
19
- timestamp: new Date().toISOString(),
20
- });
21
- });
22
-
23
- app.get('/api/bootstraps', (req, res) => {
24
- res.json({ items: store.list() });
25
- });
26
-
27
- app.post('/api/bootstraps', (req, res) => {
28
- const validation = validateBootstrapPayload(req.body);
29
- if (!validation.valid) {
30
- return res.status(400).json({ errors: validation.errors });
31
- }
32
- const payload = {
33
- name: req.body.name,
34
- description: req.body.description,
35
- primaryAction: req.body.primaryAction,
36
- targetPlatforms: Array.isArray(req.body.targetPlatforms)
37
- ? req.body.targetPlatforms
38
- : [],
39
- successCriteria: Array.isArray(req.body.successCriteria)
40
- ? req.body.successCriteria
41
- : [],
42
- steps: req.body.steps,
43
- };
44
- const saved = store.add(payload);
45
- res.status(201).json(saved);
46
- });
47
-
48
- app.get('/api/bootstraps/:id', (req, res) => {
49
- const record = store.find(req.params.id);
50
- if (!record) {
51
- return res.status(404).json({ error: 'bootstrap not found' });
52
- }
53
- res.json(record);
54
- });
55
-
56
- app.use((req, res) => {
57
- res.status(404).json({ error: 'not found' });
58
- });
59
-
60
- app.use((err, req, res, next) => {
61
- /* eslint-disable no-console */
62
- console.error('internal error', err);
63
- /* eslint-enable no-console */
64
- res.status(500).json({ error: 'internal server error' });
65
- next(err);
66
- });
67
-
68
- return { app, store };
69
- }
70
-
71
- module.exports = createApp;
package/src/index.js DELETED
@@ -1,9 +0,0 @@
1
- const BootstrapStore = require('./store');
2
- const createApp = require('./app');
3
- const startServer = require('./server');
4
-
5
- module.exports = {
6
- BootstrapStore,
7
- createApp,
8
- startServer,
9
- };
package/src/store.js DELETED
@@ -1,27 +0,0 @@
1
- class BootstrapStore {
2
- constructor() {
3
- this.items = new Map();
4
- this.counter = 1;
5
- }
6
-
7
- list() {
8
- return Array.from(this.items.values());
9
- }
10
-
11
- add(payload) {
12
- const id = String(this.counter++);
13
- const record = {
14
- id,
15
- createdAt: new Date().toISOString(),
16
- ...payload,
17
- };
18
- this.items.set(id, record);
19
- return record;
20
- }
21
-
22
- find(id) {
23
- return this.items.get(id) ?? null;
24
- }
25
- }
26
-
27
- module.exports = BootstrapStore;