mikoshi-construct 0.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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/dist/cli.js +1492 -0
  4. package/package.json +73 -0
  5. package/templates/ai/claude/CLAUDE.md.eta +18 -0
  6. package/templates/ai/claude/CLAUDE.md.existing.eta +15 -0
  7. package/templates/ai/claude/_claude/agents/architect.md +38 -0
  8. package/templates/ai/claude/_claude/agents/harness.md +40 -0
  9. package/templates/ai/claude/_claude/agents/implementer.md +40 -0
  10. package/templates/ai/claude/_claude/commands/plan.md +24 -0
  11. package/templates/ai/claude/_claude/skills/implement/SKILL.md +42 -0
  12. package/templates/ai/claude/scripts/construct/implement.workflow.mjs +185 -0
  13. package/templates/ai/cursor/_cursor/rules/construct.mdc +19 -0
  14. package/templates/ai/review/_github/workflows/claude-review.yml.eta +51 -0
  15. package/templates/ai/shared/AGENTS.md.eta +123 -0
  16. package/templates/ai/shared/AGENTS.md.existing.eta +50 -0
  17. package/templates/ai/shared/_claude/commands/construct-discover.md +75 -0
  18. package/templates/ai/shared/_claude/rules/conventions.md +63 -0
  19. package/templates/ai/shared/_claude/rules/secrets.md +8 -0
  20. package/templates/ai/shared/_claude/rules/tests.md +6 -0
  21. package/templates/base/_editorconfig +12 -0
  22. package/templates/base/_github/workflows/security.yml +41 -0
  23. package/templates/base/_gitignore +11 -0
  24. package/templates/base/_gitleaks.toml +2 -0
  25. package/templates/base/_nvmrc.eta +1 -0
  26. package/templates/base/_vscode/settings.json +22 -0
  27. package/templates/base/architecture/checklists.md +42 -0
  28. package/templates/base/architecture/principles.md +113 -0
  29. package/templates/base/architecture/security-invariants.md +17 -0
  30. package/templates/harness/_github/workflows/ci.yml.eta +31 -0
  31. package/templates/harness/eslint.config.mjs +13 -0
  32. package/templates/harness/package.json.eta +29 -0
  33. package/templates/harness/pnpm-workspace.yaml +11 -0
  34. package/templates/harness/scripts/composition/check.ts +19 -0
  35. package/templates/harness/scripts/composition/files.ts +34 -0
  36. package/templates/harness/scripts/composition/model.ts +137 -0
  37. package/templates/harness/scripts/composition/render.ts +72 -0
  38. package/templates/harness/scripts/composition/sync-docs.ts +11 -0
  39. package/templates/harness/scripts/tests/composition/files.test.ts +16 -0
  40. package/templates/harness/scripts/tests/composition/model.test.ts +41 -0
  41. package/templates/harness/scripts/tests/composition/render.test.ts +54 -0
  42. package/templates/harness/tsconfig.base.json +14 -0
  43. package/templates/harness/tsconfig.json +5 -0
  44. package/templates/harness/vitest.config.ts +8 -0
  45. package/templates/presets/monorepo/baseline/eslint.config.mjs.eta +100 -0
  46. package/templates/presets/monorepo/baseline/package.json.eta +18 -0
  47. package/templates/presets/monorepo/baseline/packages/shared/package.json.eta +22 -0
  48. package/templates/presets/monorepo/baseline/packages/shared/src/api/openapi.ts +81 -0
  49. package/templates/presets/monorepo/baseline/packages/shared/src/index.ts +1 -0
  50. package/templates/presets/monorepo/baseline/packages/shared/tsconfig.build.json +10 -0
  51. package/templates/presets/monorepo/baseline/packages/shared/tsconfig.json +4 -0
  52. package/templates/presets/monorepo/baseline/pnpm-workspace.yaml.eta +31 -0
  53. package/templates/presets/monorepo/baseline/tsconfig.json +12 -0
  54. package/templates/presets/monorepo/baseline/vitest.config.ts +8 -0
  55. package/templates/presets/monorepo/sample/apps/api/package.json.eta +23 -0
  56. package/templates/presets/monorepo/sample/apps/api/tsconfig.build.json +10 -0
  57. package/templates/presets/monorepo/sample/apps/api/tsconfig.json +4 -0
  58. package/templates/presets/monorepo/sample/scripts/tests/lint/syntax-policy.test.ts.eta +36 -0
  59. package/templates/presets/node-backend/baseline/eslint.config.mjs +59 -0
  60. package/templates/presets/node-backend/baseline/package.json.eta +20 -0
  61. package/templates/presets/node-backend/baseline/src/contracts/openapi.ts +81 -0
  62. package/templates/presets/node-backend/baseline/tsconfig.build.json +10 -0
  63. package/templates/presets/node-backend/sample/scripts/tests/lint/syntax-policy.test.ts +31 -0
  64. package/templates/presets/node-frontend/baseline/_claude/rules/css.md +158 -0
  65. package/templates/presets/node-frontend/baseline/eslint.config.mjs +44 -0
  66. package/templates/presets/node-frontend/baseline/package.json.eta +15 -0
  67. package/templates/presets/node-frontend/baseline/tsconfig.json +11 -0
  68. package/templates/presets/node-frontend/baseline/vitest.config.ts +10 -0
  69. package/templates/presets/node-frontend/sample/architecture/app.md +37 -0
  70. package/templates/presets/node-frontend/sample/architecture/composition/app.yaml +41 -0
  71. package/templates/presets/node-frontend/sample/index.html.eta +12 -0
  72. package/templates/presets/node-frontend/sample/src/app.ts +32 -0
  73. package/templates/presets/node-frontend/sample/src/main.ts +9 -0
  74. package/templates/presets/node-frontend/sample/src/styles/app.css +43 -0
  75. package/templates/presets/node-frontend/sample/src/styles/tokens.css +16 -0
  76. package/templates/presets/node-frontend/sample/tests/app.test.ts +21 -0
  77. package/templates/stacks/express-api/app/src/app.ts +27 -0
  78. package/templates/stacks/express-api/app/src/config.ts.eta +30 -0
  79. package/templates/stacks/express-api/app/src/contracts/types.ts.eta +1 -0
  80. package/templates/stacks/express-api/app/src/health/health.controller.ts +12 -0
  81. package/templates/stacks/express-api/app/src/health/health.service.ts +23 -0
  82. package/templates/stacks/express-api/app/src/http/api-contract.middleware.ts +13 -0
  83. package/templates/stacks/express-api/app/src/http/app-error.ts +14 -0
  84. package/templates/stacks/express-api/app/src/http/async-handler.middleware.ts +7 -0
  85. package/templates/stacks/express-api/app/src/http/error-handler.middleware.ts +50 -0
  86. package/templates/stacks/express-api/app/src/server.ts +9 -0
  87. package/templates/stacks/express-api/app/tests/health.test.ts +28 -0
  88. package/templates/stacks/express-api/app/tests/http/error-handler.middleware.test.ts +48 -0
  89. package/templates/stacks/express-api/repo/architecture/composition/http.yaml.eta +45 -0
  90. package/templates/stacks/express-api/repo/architecture/http.md +39 -0
  91. package/templates/stacks/http-contract/_github/workflows/api-contract.yml +36 -0
  92. package/templates/stacks/http-contract/contracts/api/openapi.yaml.eta +62 -0
  93. package/templates/stacks/http-contract/package.json.eta +13 -0
  94. package/templates/stacks/http-contract/redocly.yaml.eta +8 -0
  95. package/templates/stacks/http-contract/scripts/contracts/types.mjs.eta +20 -0
  96. package/templates/stacks/http-contract/scripts/tests/contracts/security.test.ts.eta +86 -0
@@ -0,0 +1,62 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: {{projectName}} API
4
+ version: 0.1.0
5
+ description: What this service promises to its consumers. API changes start here; the implementation conforms to the contract and consumers read the generated types.
6
+ security:
7
+ - {}
8
+ servers:
9
+ - url: /
10
+ description: Same origin. Local development serves the contract on the port PORT names, 3000 by default.
11
+ tags:
12
+ - name: public
13
+ description: Anonymous endpoints.
14
+ paths:
15
+ /health:
16
+ get:
17
+ operationId: getHealth
18
+ tags: [public]
19
+ summary: Liveness
20
+ responses:
21
+ '200':
22
+ description: The service is up.
23
+ content:
24
+ application/json:
25
+ schema:
26
+ $ref: '#/components/schemas/HealthResponse'
27
+ '503':
28
+ description: A dependency did not answer.
29
+ content:
30
+ application/json:
31
+ schema:
32
+ $ref: '#/components/schemas/HealthResponse'
33
+ default:
34
+ description: Unexpected error.
35
+ content:
36
+ application/json:
37
+ schema:
38
+ $ref: '#/components/schemas/ErrorResponse'
39
+ components:
40
+ schemas:
41
+ HealthResponse:
42
+ type: object
43
+ additionalProperties: false
44
+ required: [status, service, time]
45
+ properties:
46
+ status:
47
+ type: string
48
+ enum: [ok, error]
49
+ service:
50
+ type: string
51
+ time:
52
+ type: string
53
+ format: date-time
54
+ ErrorResponse:
55
+ type: object
56
+ additionalProperties: false
57
+ required: [error, message]
58
+ properties:
59
+ error:
60
+ type: string
61
+ message:
62
+ type: string
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "scripts": {
4
+ "contracts:lint": "redocly lint",
5
+ "contracts:types": "node scripts/contracts/types.mjs",
6
+ "contracts:check": "pnpm contracts:lint && node scripts/contracts/types.mjs --check",
7
+ "quality": "pnpm contracts:check && pnpm composition:check && pnpm lint && pnpm typecheck && pnpm test"
8
+ },
9
+ "devDependencies": {
10
+ "@redocly/cli": "^2.52.0",
11
+ "openapi-typescript": "^7.13.0"
12
+ }
13
+ }
@@ -0,0 +1,8 @@
1
+ apis:
2
+ {{projectName}}@v1:
3
+ root: {{contractPath}}
4
+ extends:
5
+ - recommended
6
+ rules:
7
+ operation-4xx-response: off
8
+ info-license: off
@@ -0,0 +1,20 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs'
2
+ import process from 'node:process'
3
+ import { pathToFileURL } from 'node:url'
4
+ import openapiTS, { astToString } from 'openapi-typescript'
5
+
6
+ const CONTRACT = '{{contractPath}}'
7
+ const OUTPUT = '{{contractTypesOutput}}'
8
+
9
+ const generated = astToString(await openapiTS(pathToFileURL(CONTRACT)))
10
+
11
+ if (process.argv.includes('--check')) {
12
+ const committed = readFileSync(OUTPUT, 'utf8')
13
+ if (committed !== generated) {
14
+ console.error(`${OUTPUT} is out of date with ${CONTRACT}; run pnpm contracts:types`)
15
+ process.exit(1)
16
+ }
17
+ }
18
+ else {
19
+ writeFileSync(OUTPUT, generated)
20
+ }
@@ -0,0 +1,86 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { parse } from 'yaml'
4
+
5
+ interface SchemaNode {
6
+ $ref?: string
7
+ additionalProperties?: boolean | SchemaNode
8
+ }
9
+
10
+ interface MediaTypes {
11
+ content?: Record<string, { schema: SchemaNode }>
12
+ }
13
+
14
+ interface Operation {
15
+ operationId: string
16
+ security?: Array<Record<string, unknown>>
17
+ requestBody?: MediaTypes
18
+ responses?: Record<string, MediaTypes>
19
+ }
20
+
21
+ interface ContractDocument {
22
+ paths: Record<string, Record<string, Operation>>
23
+ components: { schemas: Record<string, SchemaNode> }
24
+ }
25
+
26
+ const contract = parse(readFileSync(new URL('../../../{{contractPath}}', import.meta.url), 'utf8')) as ContractDocument
27
+
28
+ const PROTECTED_PREFIXES = ['/v1/admin', '/v1/me']
29
+
30
+ function operations(prefix: string): Array<{ route: string, method: string, operation: Operation }> {
31
+ return Object.entries(contract.paths)
32
+ .filter(([route]) => route.startsWith(prefix))
33
+ .flatMap(([route, methods]) => Object.entries(methods).map(([method, operation]) => ({ route, method, operation })))
34
+ }
35
+
36
+ function resolveSchema(schema: SchemaNode): SchemaNode {
37
+ return schema.$ref == null
38
+ ? schema
39
+ : resolveSchema(contract.components.schemas[schema.$ref.replace('#/components/schemas/', '')])
40
+ }
41
+
42
+ function jsonRequestBodies(): Array<{ route: string, method: string, schema: SchemaNode }> {
43
+ return Object.entries(contract.paths).flatMap(([route, methods]) =>
44
+ Object.entries(methods).flatMap(([method, operation]) => {
45
+ const schema = operation.requestBody?.content?.['application/json']?.schema
46
+ return schema == null ? [] : [{ route, method, schema: resolveSchema(schema) }]
47
+ }))
48
+ }
49
+
50
+ function jsonResponseBodies(): Array<{ route: string, method: string, status: string, schema: SchemaNode }> {
51
+ return Object.entries(contract.paths).flatMap(([route, methods]) =>
52
+ Object.entries(methods).flatMap(([method, operation]) =>
53
+ Object.entries(operation.responses ?? {}).flatMap(([status, response]) => {
54
+ const schema = response.content?.['application/json']?.schema
55
+ return schema == null ? [] : [{ route, method, status, schema: resolveSchema(schema) }]
56
+ })))
57
+ }
58
+
59
+ function requires(operation: Operation, scheme: string): boolean {
60
+ const schemes = operation.security ?? []
61
+ return schemes.length > 0 && schemes.every(entry => scheme in entry)
62
+ }
63
+
64
+ describe('security invariants stated by the contract', () => {
65
+ it('declares every operation with an operationId', () => {
66
+ for (const { route, method, operation } of operations('/'))
67
+ expect({ route, method, operationId: typeof operation.operationId }).toEqual({ route, method, operationId: 'string' })
68
+ })
69
+
70
+ it('requires a bearer session on every operation under a protected prefix', () => {
71
+ for (const prefix of PROTECTED_PREFIXES) {
72
+ for (const { route, method, operation } of operations(prefix))
73
+ expect({ route, method, bearer: requires(operation, 'bearer') }).toEqual({ route, method, bearer: true })
74
+ }
75
+ })
76
+
77
+ it('closes every documented JSON request body, so no write can set a field the contract does not expose', () => {
78
+ for (const { route, method, schema } of jsonRequestBodies())
79
+ expect({ route, method, closed: schema.additionalProperties }).toEqual({ route, method, closed: false })
80
+ })
81
+
82
+ it('closes every documented JSON response body, so no response can leak a field the contract does not expose', () => {
83
+ for (const { route, method, status, schema } of jsonResponseBodies())
84
+ expect({ route, method, status, closed: schema.additionalProperties }).toEqual({ route, method, status, closed: false })
85
+ })
86
+ })