@knpkv/clockify-api-client 0.2.0 → 1.0.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 (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +92 -80
  3. package/dist/ClockifyApiClient.d.ts +45 -46
  4. package/dist/ClockifyApiClient.d.ts.map +1 -1
  5. package/dist/ClockifyApiClient.js +53 -122
  6. package/dist/ClockifyApiClient.js.map +1 -1
  7. package/dist/ClockifyApiConfig.d.ts +1 -1
  8. package/dist/ClockifyApiConfig.d.ts.map +1 -1
  9. package/dist/ClockifyApiConfig.js +1 -1
  10. package/dist/ClockifyApiConfig.js.map +1 -1
  11. package/dist/generated/ClockifyApi.d.ts +14432 -0
  12. package/dist/generated/ClockifyApi.d.ts.map +1 -0
  13. package/dist/generated/ClockifyApi.js +1239 -0
  14. package/dist/generated/ClockifyApi.js.map +1 -0
  15. package/dist/index.d.ts +4 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -4
  18. package/dist/index.js.map +1 -1
  19. package/package.json +16 -21
  20. package/scripts/regenerate.ts +208 -116
  21. package/scripts/tsconfig.json +13 -0
  22. package/src/ClockifyApiClient.ts +127 -228
  23. package/src/ClockifyApiConfig.ts +2 -2
  24. package/src/generated/ClockifyApi.ts +3020 -0
  25. package/src/index.ts +10 -14
  26. package/test/ClockifyApiClient.test.ts +129 -0
  27. package/vitest.config.ts +7 -0
  28. package/.specs/VERSION +0 -1
  29. package/.specs/clockify-v1.json +0 -31670
  30. package/.specs/clockify-v1.patch.json +0 -48
  31. package/dist/ClockifyApiError.d.ts +0 -11
  32. package/dist/ClockifyApiError.d.ts.map +0 -1
  33. package/dist/ClockifyApiError.js +0 -14
  34. package/dist/ClockifyApiError.js.map +0 -1
  35. package/dist/OpenApiFetchClient.d.ts +0 -55
  36. package/dist/OpenApiFetchClient.d.ts.map +0 -1
  37. package/dist/OpenApiFetchClient.js +0 -63
  38. package/dist/OpenApiFetchClient.js.map +0 -1
  39. package/dist/generated/index.d.ts +0 -2
  40. package/dist/generated/index.d.ts.map +0 -1
  41. package/dist/generated/index.js +0 -2
  42. package/dist/generated/index.js.map +0 -1
  43. package/dist/generated/schema.d.ts +0 -16204
  44. package/src/ClockifyApiError.ts +0 -17
  45. package/src/OpenApiFetchClient.ts +0 -92
  46. package/src/generated/index.ts +0 -1
  47. package/src/generated/schema.d.ts +0 -16204
  48. package/tsconfig.tsbuildinfo +0 -1
package/dist/index.d.ts CHANGED
@@ -1,12 +1,10 @@
1
1
  /**
2
- * Root barrel export for `@knpkv/clockify-api-client` -- openapi-fetch + Effect Clockify REST client.
2
+ * Root barrel export for the Schema-validated Effect Clockify client.
3
3
  *
4
4
  * @packageDocumentation
5
5
  */
6
- export { ClockifyApiClient, type ClockifyApiClientShape, layer } from "./ClockifyApiClient.js";
7
- export type { CreateTimeEntryParams, GetTimeEntriesParams, Project, StopTimeEntryParams, Tag, TimeEntry, TimeInterval, UpdateTimeEntryParams, User, Workspace } from "./ClockifyApiClient.js";
6
+ export { ClockifyApiClient, type ClockifyApiClientShape, type ClockifyClientError, layer, make } from "./ClockifyApiClient.js";
7
+ export type { AuthenticatedClockifyApi, CreateTimeEntryParams, GetTimeEntriesParams, Project, StopTimeEntryParams, Tag, TimeEntry, TimeInterval, UpdateTimeEntryParams, User, Workspace } from "./ClockifyApiClient.js";
8
8
  export { ClockifyApiConfig, type ClockifyApiConfigShape } from "./ClockifyApiConfig.js";
9
- export { ClockifyApiError } from "./ClockifyApiError.js";
10
- export { FetchClientError, makeOpenApiFetchClient, type OpenApiFetchClient, type SuccessData, toEffect } from "./OpenApiFetchClient.js";
11
- export type * as V1 from "./generated/index.js";
9
+ export * as ClockifyApi from "./generated/ClockifyApi.js";
12
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAE9F,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,OAAO,EACP,mBAAmB,EACnB,GAAG,EACH,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,IAAI,EACJ,SAAS,EACV,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,QAAQ,EACT,MAAM,yBAAyB,CAAA;AAGhC,YAAY,KAAK,EAAE,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,EACL,IAAI,EACL,MAAM,wBAAwB,CAAA;AAE/B,YAAY,EACV,wBAAwB,EACxB,qBAAqB,EACrB,oBAAoB,EACpB,OAAO,EACP,mBAAmB,EACnB,GAAG,EACH,SAAS,EACT,YAAY,EACZ,qBAAqB,EACrB,IAAI,EACJ,SAAS,EACV,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEvF,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAA"}
package/dist/index.js CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
- * Root barrel export for `@knpkv/clockify-api-client` -- openapi-fetch + Effect Clockify REST client.
2
+ * Root barrel export for the Schema-validated Effect Clockify client.
3
3
  *
4
4
  * @packageDocumentation
5
5
  */
6
- export { ClockifyApiClient, layer } from "./ClockifyApiClient.js";
6
+ export { ClockifyApiClient, layer, make } from "./ClockifyApiClient.js";
7
7
  export { ClockifyApiConfig } from "./ClockifyApiConfig.js";
8
- export { ClockifyApiError } from "./ClockifyApiError.js";
9
- export { FetchClientError, makeOpenApiFetchClient, toEffect } from "./OpenApiFetchClient.js";
8
+ export * as ClockifyApi from "./generated/ClockifyApi.js";
10
9
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,iBAAiB,EAA+B,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAe9F,OAAO,EAAE,iBAAiB,EAA+B,MAAM,wBAAwB,CAAA;AAEvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAExD,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EAGtB,QAAQ,EACT,MAAM,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,iBAAiB,EAGjB,KAAK,EACL,IAAI,EACL,MAAM,wBAAwB,CAAA;AAgB/B,OAAO,EAAE,iBAAiB,EAA+B,MAAM,wBAAwB,CAAA;AAEvF,OAAO,KAAK,WAAW,MAAM,4BAA4B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knpkv/clockify-api-client",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "Effect-based Clockify REST API client",
5
5
  "license": "MIT",
6
6
  "author": "knpkv",
@@ -26,33 +26,27 @@
26
26
  "import": "./dist/ClockifyApiConfig.js",
27
27
  "types": "./dist/ClockifyApiConfig.d.ts"
28
28
  },
29
- "./ClockifyApiError": {
30
- "import": "./dist/ClockifyApiError.js",
31
- "types": "./dist/ClockifyApiError.d.ts"
32
- },
33
29
  "./generated": {
34
- "import": "./dist/generated/index.js",
35
- "types": "./dist/generated/index.d.ts"
30
+ "import": "./dist/generated/ClockifyApi.js",
31
+ "types": "./dist/generated/ClockifyApi.d.ts"
36
32
  }
37
33
  },
38
34
  "publishConfig": {
39
35
  "access": "public"
40
36
  },
41
37
  "peerDependencies": {
42
- "effect": "^3.19.3"
43
- },
44
- "dependencies": {
45
- "openapi-fetch": "^0.17.0"
38
+ "effect": "4.0.0-beta.98"
46
39
  },
47
40
  "devDependencies": {
48
- "@effect/vitest": "latest",
41
+ "@effect/openapi-generator": "4.0.0-beta.98",
42
+ "@effect/platform-node": "4.0.0-beta.98",
43
+ "@effect/vitest": "4.0.0-beta.98",
49
44
  "@types/node": "latest",
50
- "effect": "latest",
51
- "openapi-typescript": "^7.13.0",
52
- "openapi-typescript-helpers": "^0.1.0",
53
- "tsx": "^4.19.4",
54
- "typescript": "~5.9.0",
55
- "vitest": "^4.0.13"
45
+ "effect": "4.0.0-beta.98",
46
+ "prettier": "^3.9.5",
47
+ "tsx": "^4.23.0",
48
+ "typescript": "~6.0.3",
49
+ "vitest": "^4.1.10"
56
50
  },
57
51
  "keywords": [
58
52
  "effect",
@@ -63,11 +57,12 @@
63
57
  "openapi"
64
58
  ],
65
59
  "scripts": {
66
- "build": "tsc && cp src/generated/schema.d.ts dist/generated/schema.d.ts",
67
- "check": "tsc --noEmit",
60
+ "build": "tsc",
61
+ "check": "tsc --noEmit && tsc -p scripts/tsconfig.json",
68
62
  "test": "vitest run",
69
63
  "test:watch": "vitest",
70
64
  "lint": "eslint src",
71
- "regenerate": "tsx scripts/regenerate.ts"
65
+ "regenerate": "tsx scripts/regenerate.ts",
66
+ "regenerate:check": "tsx scripts/regenerate.ts --check"
72
67
  }
73
68
  }
@@ -1,138 +1,230 @@
1
1
  #!/usr/bin/env tsx
2
2
  /**
3
- * Regeneration script for Clockify API client.
3
+ * Fetch, compare, patch, and regenerate the Clockify API client.
4
4
  *
5
- * 1. Fetches latest OpenAPI spec from Clockify docs
6
- * 2. Applies local patches (.specs/clockify-v1.patch.json) to fix incomplete DTOs
7
- * 3. Generates TypeScript types via openapi-typescript
5
+ * The committed OpenAPI document is the unmodified upstream document. RFC 6902
6
+ * patches are applied in memory before Effect's OpenAPI generator runs.
8
7
  */
8
+ import * as OpenApiGenerator from "@effect/openapi-generator/OpenApiGenerator"
9
+ import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch"
10
+ import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node"
9
11
  import * as Console from "effect/Console"
12
+ import * as Data from "effect/Data"
10
13
  import * as Effect from "effect/Effect"
11
- import { execSync } from "node:child_process"
12
- import * as fs from "node:fs"
13
- import * as path from "node:path"
14
- import { fileURLToPath } from "node:url"
15
-
16
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
17
- const SPECS_DIR = path.join(__dirname, "..", ".specs")
18
- const SPEC_FILE = path.join(SPECS_DIR, "clockify-v1.json")
19
- const PATCH_FILE = path.join(SPECS_DIR, "clockify-v1.patch.json")
20
- const OUTPUT_FILE = path.join(__dirname, "..", "src", "generated", "schema.d.ts")
21
- const VERSION_FILE = path.join(SPECS_DIR, "VERSION")
14
+ import * as FileSystem from "effect/FileSystem"
15
+ import type * as JsonSchema from "effect/JsonSchema"
16
+ import * as Layer from "effect/Layer"
17
+ import * as Path from "effect/Path"
18
+ import * as Predicate from "effect/Predicate"
19
+ import * as Schema from "effect/Schema"
20
+ import * as Command from "effect/unstable/cli/Command"
21
+ import * as Flag from "effect/unstable/cli/Flag"
22
+ import * as HttpClient from "effect/unstable/http/HttpClient"
23
+ import type { OpenAPISpec } from "effect/unstable/httpapi/OpenApi"
24
+ import { format as formatSource } from "prettier"
25
+
22
26
  const SPEC_URL = "https://docs.clockify.me/openapi.json"
23
27
 
24
- interface SpecInfo {
25
- version: string
26
- title: string
27
- }
28
+ class RegenerateError extends Data.TaggedError("RegenerateError")<{
29
+ readonly message: string
30
+ readonly cause?: unknown
31
+ }> {}
28
32
 
29
- const fetchSpecInfo = (): Effect.Effect<SpecInfo, Error> =>
30
- Effect.tryPromise({
31
- try: async () => {
32
- const response = await fetch(SPEC_URL)
33
- if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`)
34
- const spec = (await response.json()) as { info: { version: string; title: string } }
35
- return { version: spec.info.version, title: spec.info.title }
36
- },
37
- catch: (e) => new Error(`Fetch failed: ${e}`)
38
- })
33
+ const JsonString = Schema.fromJsonString(Schema.Json)
39
34
 
40
- const fetchAndSaveSpec = (): Effect.Effect<void, Error> =>
41
- Effect.tryPromise({
42
- try: async () => {
43
- const response = await fetch(SPEC_URL)
44
- if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`)
45
- fs.writeFileSync(SPEC_FILE, await response.text(), "utf-8")
46
- },
47
- catch: (e) => new Error(`Fetch/save failed: ${e}`)
48
- })
35
+ const OpenApiDocument = Schema.declare<OpenAPISpec>(
36
+ (value): value is OpenAPISpec =>
37
+ Predicate.hasProperty(value, "paths") &&
38
+ Predicate.isObject(value.paths) &&
39
+ (Predicate.hasProperty(value, "openapi") || Predicate.hasProperty(value, "swagger")),
40
+ { identifier: "OpenApiDocument" }
41
+ )
49
42
 
50
- /**
51
- * Apply patches from clockify-v1.patch.json to the spec in-place.
52
- * Keeps original spec clean — patches add missing fields and required arrays.
53
- */
54
- const applyPatches = (): Effect.Effect<void, Error> =>
55
- Effect.try({
56
- try: () => {
57
- if (!fs.existsSync(PATCH_FILE)) return
58
-
59
- const spec = JSON.parse(fs.readFileSync(SPEC_FILE, "utf-8"))
60
- const patches = JSON.parse(fs.readFileSync(PATCH_FILE, "utf-8"))
61
- const schemas = spec.components?.schemas ?? {}
62
-
63
- for (const [schemaName, patch] of Object.entries(patches.schemas ?? {}) as Array<[string, Record<string, unknown>]>) {
64
- const schema = schemas[schemaName]
65
- if (!schema) continue
66
-
67
- // Rename properties (e.g. get_id → id)
68
- if (patch.renameProperties) {
69
- for (const [from, to] of Object.entries(patch.renameProperties as Record<string, string>)) {
70
- if (schema.properties?.[from]) {
71
- schema.properties[to] = schema.properties[from]
72
- delete schema.properties[from]
73
- }
74
- }
75
- }
76
-
77
- // Add new properties
78
- if (patch.addProperties) {
79
- schema.properties = { ...schema.properties, ...patch.addProperties }
80
- }
81
-
82
- // Patch existing properties (overwrite)
83
- if (patch.patchProperties) {
84
- for (const [name, value] of Object.entries(patch.patchProperties as Record<string, unknown>)) {
85
- schema.properties[name] = value
86
- }
87
- }
43
+ const SpecInfo = Schema.Struct({
44
+ info: Schema.Struct({ version: Schema.String })
45
+ })
88
46
 
89
- // Set required fields
90
- if (patch.required) {
91
- schema.required = patch.required
92
- }
93
- }
47
+ interface ScriptPaths {
48
+ readonly packageDir: string
49
+ readonly specFile: string
50
+ readonly patchFile: string
51
+ readonly outputFile: string
52
+ readonly versionFile: string
53
+ }
94
54
 
95
- fs.writeFileSync(SPEC_FILE, JSON.stringify(spec, null, 2), "utf-8")
96
- },
97
- catch: (e) => new Error(`Patch failed: ${e}`)
98
- })
55
+ const paths = Effect.gen(function*() {
56
+ const path = yield* Path.Path
57
+ const scriptFile = yield* path.fromFileUrl(new URL(import.meta.url)).pipe(
58
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not resolve script path", cause }))
59
+ )
60
+ const packageDir = path.join(path.dirname(scriptFile), "..")
61
+ const specsDir = path.join(packageDir, ".specs")
62
+ return {
63
+ packageDir,
64
+ specFile: path.join(specsDir, "clockify-v1.json"),
65
+ patchFile: path.join(specsDir, "clockify-v1.patch.json"),
66
+ outputFile: path.join(packageDir, "src", "generated", "ClockifyApi.ts"),
67
+ versionFile: path.join(specsDir, "VERSION")
68
+ } satisfies ScriptPaths
69
+ })
99
70
 
100
- const generateTypes = (): Effect.Effect<void, Error> =>
101
- Effect.try({
102
- try: () => {
103
- execSync(`npx openapi-typescript "${SPEC_FILE}" -o "${OUTPUT_FILE}"`, {
104
- cwd: path.join(__dirname, ".."),
105
- stdio: "inherit"
71
+ const fetchUpstream = HttpClient.get(SPEC_URL).pipe(
72
+ Effect.flatMap((response) =>
73
+ response.status >= 200 && response.status < 300
74
+ ? response.text.pipe(
75
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not read Clockify response", cause }))
76
+ )
77
+ : Effect.fail(
78
+ new RegenerateError({
79
+ message: `Clockify spec request failed with status ${response.status}`
80
+ })
81
+ )
82
+ ),
83
+ Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
84
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not fetch or decode Clockify spec", cause }))
85
+ )
86
+
87
+ const canonicalJson = (value: Schema.Json) =>
88
+ Schema.encodeEffect(JsonString)(value).pipe(
89
+ Effect.map((json) => `${json}\n`),
90
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not encode Clockify spec", cause }))
91
+ )
92
+
93
+ const formattedJson = (value: Schema.Json) =>
94
+ Schema.encodeEffect(JsonString)(value).pipe(
95
+ Effect.flatMap((source) =>
96
+ Effect.tryPromise({
97
+ try: () => formatSource(source, { parser: "json", printWidth: 120, trailingComma: "none", semi: false }),
98
+ catch: (cause) => new RegenerateError({ message: "Could not format Clockify spec", cause })
106
99
  })
107
- },
108
- catch: (e) => new Error(`Generation failed: ${e}`)
109
- })
110
-
111
- const program = Effect.gen(function*() {
112
- yield* Console.log("Fetching Clockify API spec version...")
100
+ ),
101
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not format Clockify spec", cause }))
102
+ )
113
103
 
114
- const info = yield* fetchSpecInfo()
115
- const current = fs.existsSync(VERSION_FILE) ? fs.readFileSync(VERSION_FILE, "utf-8").trim() : null
116
-
117
- yield* Console.log(`Current: ${current ?? "none"}, Remote: ${info.version}`)
104
+ const stripUnreliableMetadata = (schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema => {
105
+ const { default: _default, examples: _examples, ...normalized } = schema
106
+ return normalized
107
+ }
118
108
 
119
- if (current === info.version) {
120
- yield* Console.log("Spec up to date, regenerating types...")
121
- } else {
122
- if (!fs.existsSync(SPECS_DIR)) fs.mkdirSync(SPECS_DIR, { recursive: true })
109
+ const stripTrailingWhitespace = (source: string): string => source.replace(/[ \t]+$/gm, "")
123
110
 
124
- yield* Console.log(`Fetching spec (${info.version})...`)
125
- yield* fetchAndSaveSpec()
126
- fs.writeFileSync(VERSION_FILE, info.version, "utf-8")
111
+ /**
112
+ * The pinned Effect generator emits an unused decoder when a spec declares no typed error bodies.
113
+ * Remove only that exact unused block; fail if the generator shape changes.
114
+ */
115
+ const removeUnusedErrorDecoder = (source: string): string => {
116
+ const marker = " const decodeError ="
117
+ if (!source.includes(marker)) return source
118
+ const cleaned = source.replace(/\n {2}const decodeError =[\s\S]*?\n {2}return \{/m, "\n return {")
119
+ if (cleaned === source || cleaned.includes(marker)) {
120
+ throw new RegenerateError({ message: "Effect generator output changed around decodeError" })
127
121
  }
122
+ return cleaned
123
+ }
128
124
 
129
- yield* Console.log("Applying patches...")
130
- yield* applyPatches()
131
-
132
- yield* Console.log("Generating types...")
133
- yield* generateTypes()
134
-
135
- yield* Console.log("Done.")
125
+ /**
126
+ * The generator currently passes record-shaped multipart payloads to
127
+ * `bodyFormData`, which expects an already-built Web FormData value. Use the
128
+ * record-aware Effect helper so strings and Blob/File values are serialized
129
+ * with a real multipart boundary by the runtime.
130
+ */
131
+ const normalizeMultipartBodies = (source: string): string =>
132
+ source.replaceAll("HttpClientRequest.bodyFormData(", "HttpClientRequest.bodyFormDataRecord(")
133
+
134
+ const generate = Effect.fn("Clockify.regenerate.generate")(function*(
135
+ scriptPaths: ScriptPaths,
136
+ upstream: Schema.Json
137
+ ) {
138
+ const fs = yield* FileSystem.FileSystem
139
+ const patch = yield* OpenApiPatch.parsePatchInput(scriptPaths.patchFile).pipe(
140
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not parse Clockify JSON patch", cause }))
141
+ )
142
+ const patched = yield* OpenApiPatch.applyPatches(
143
+ [{ source: scriptPaths.patchFile, patch }],
144
+ upstream
145
+ ).pipe(
146
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not apply Clockify JSON patch", cause }))
147
+ )
148
+ const document = yield* Schema.decodeUnknownEffect(OpenApiDocument)(patched).pipe(
149
+ Effect.mapError((cause) => new RegenerateError({ message: "Patched Clockify document is not OpenAPI", cause }))
150
+ )
151
+ const generator = yield* OpenApiGenerator.OpenApiGenerator
152
+ const warnings: Array<OpenApiGenerator.OpenApiGeneratorWarning> = []
153
+ const generated = yield* generator.generate(document, {
154
+ name: "ClockifyApi",
155
+ format: "httpclient",
156
+ onEnter: stripUnreliableMetadata,
157
+ onWarning: (warning) => warnings.push(warning)
158
+ })
159
+ yield* Effect.forEach(
160
+ warnings,
161
+ (warning) => Console.warn(`[${warning.code}] ${warning.method ?? ""} ${warning.path ?? ""}: ${warning.message}`),
162
+ { discard: true }
163
+ )
164
+ yield* fs.writeFileString(
165
+ scriptPaths.outputFile,
166
+ stripTrailingWhitespace(normalizeMultipartBodies(removeUnusedErrorDecoder(generated)))
167
+ ).pipe(
168
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not write generated Clockify client", cause }))
169
+ )
136
170
  })
137
171
 
138
- Effect.runPromise(program).catch(console.error)
172
+ const check = Flag.boolean("check").pipe(
173
+ Flag.withDescription("Exit non-zero when the committed upstream spec differs from Clockify")
174
+ )
175
+
176
+ const local = Flag.boolean("local").pipe(
177
+ Flag.withDescription("Regenerate from the committed spec without contacting Clockify")
178
+ )
179
+
180
+ const root = Command.make("clockify-api-regenerate", { check, local }).pipe(
181
+ Command.withHandler(
182
+ Effect.fn("Clockify.regenerate")(function*({ check, local }) {
183
+ const fs = yield* FileSystem.FileSystem
184
+ const scriptPaths = yield* paths
185
+ const upstream = local
186
+ ? yield* fs.readFileString(scriptPaths.specFile).pipe(
187
+ Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
188
+ Effect.mapError((cause) => new RegenerateError({ message: "Could not read committed Clockify spec", cause }))
189
+ )
190
+ : yield* fetchUpstream
191
+ const encoded = yield* canonicalJson(upstream)
192
+ const current = yield* fs.readFileString(scriptPaths.specFile).pipe(
193
+ Effect.flatMap(Schema.decodeUnknownEffect(JsonString)),
194
+ Effect.flatMap(canonicalJson),
195
+ Effect.orElseSucceed(() => "")
196
+ )
197
+
198
+ if (check) {
199
+ if (local) {
200
+ return yield* new RegenerateError({ message: "--check and --local cannot be combined" })
201
+ }
202
+ if (current === encoded) {
203
+ return yield* Console.log("Clockify OpenAPI spec is current.")
204
+ }
205
+ return yield* new RegenerateError({
206
+ message: "Clockify OpenAPI spec changed; run `pnpm --filter @knpkv/clockify-api-client regenerate`"
207
+ })
208
+ }
209
+
210
+ const { info } = yield* Schema.decodeUnknownEffect(SpecInfo)(upstream).pipe(
211
+ Effect.mapError((cause) => new RegenerateError({ message: "Clockify spec has no version", cause }))
212
+ )
213
+ yield* fs.writeFileString(scriptPaths.specFile, yield* formattedJson(upstream))
214
+ yield* fs.writeFileString(scriptPaths.versionFile, `${info.version}\n`)
215
+ yield* generate(scriptPaths, upstream)
216
+ yield* Console.log(`Generated Clockify ${info.version} client from ${SPEC_URL}`)
217
+ })
218
+ )
219
+ )
220
+
221
+ const MainLayer = Layer.mergeAll(
222
+ OpenApiGenerator.layerTransformerSchema,
223
+ NodeHttpClient.layerFetch,
224
+ NodeServices.layer
225
+ )
226
+
227
+ Command.run(root, { version: "1.0.0" }).pipe(
228
+ Effect.provide(MainLayer),
229
+ NodeRuntime.runMain
230
+ )
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.jsonc",
3
+ "compilerOptions": {
4
+ "composite": false,
5
+ "declaration": false,
6
+ "declarationMap": false,
7
+ "incremental": false,
8
+ "noEmit": true,
9
+ "rootDir": "..",
10
+ "types": ["node"]
11
+ },
12
+ "include": ["./**/*.ts"]
13
+ }