@knpkv/jira-cli 1.2.3 → 1.4.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 (147) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +64 -0
  3. package/dist/AttachmentService.d.ts +2 -2
  4. package/dist/AttachmentService.d.ts.map +1 -1
  5. package/dist/AttachmentService.js +1 -1
  6. package/dist/AttachmentService.js.map +1 -1
  7. package/dist/IssueService.d.ts +82 -3
  8. package/dist/IssueService.d.ts.map +1 -1
  9. package/dist/IssueService.js +111 -16
  10. package/dist/IssueService.js.map +1 -1
  11. package/dist/JiraAuth.d.ts +3 -3
  12. package/dist/JiraAuth.d.ts.map +1 -1
  13. package/dist/JiraAuth.js +87 -23
  14. package/dist/JiraAuth.js.map +1 -1
  15. package/dist/MarkdownWriter.d.ts +4 -4
  16. package/dist/MarkdownWriter.d.ts.map +1 -1
  17. package/dist/MarkdownWriter.js +2 -2
  18. package/dist/MarkdownWriter.js.map +1 -1
  19. package/dist/SyncWorkspace.d.ts +2 -2
  20. package/dist/SyncWorkspace.d.ts.map +1 -1
  21. package/dist/SyncWorkspace.js.map +1 -1
  22. package/dist/VersionService.d.ts +81 -9
  23. package/dist/VersionService.d.ts.map +1 -1
  24. package/dist/VersionService.js +113 -17
  25. package/dist/VersionService.js.map +1 -1
  26. package/dist/bin.js +1 -1
  27. package/dist/bin.js.map +1 -1
  28. package/dist/commands/auth.d.ts +13 -2
  29. package/dist/commands/auth.d.ts.map +1 -1
  30. package/dist/commands/auth.js +7 -7
  31. package/dist/commands/auth.js.map +1 -1
  32. package/dist/commands/edit.d.ts +22 -0
  33. package/dist/commands/edit.d.ts.map +1 -0
  34. package/dist/commands/edit.js +49 -0
  35. package/dist/commands/edit.js.map +1 -0
  36. package/dist/commands/errorHandler.d.ts.map +1 -1
  37. package/dist/commands/get.d.ts +6 -1
  38. package/dist/commands/get.d.ts.map +1 -1
  39. package/dist/commands/get.js +3 -3
  40. package/dist/commands/get.js.map +1 -1
  41. package/dist/commands/issue.d.ts +2 -2
  42. package/dist/commands/issue.d.ts.map +1 -1
  43. package/dist/commands/issue.js +3 -2
  44. package/dist/commands/issue.js.map +1 -1
  45. package/dist/commands/issueAttachment.d.ts +1 -1
  46. package/dist/commands/issueAttachment.d.ts.map +1 -1
  47. package/dist/commands/issueAttachment.js +13 -11
  48. package/dist/commands/issueAttachment.js.map +1 -1
  49. package/dist/commands/layers.d.ts +3 -3
  50. package/dist/commands/layers.d.ts.map +1 -1
  51. package/dist/commands/layers.js +5 -2
  52. package/dist/commands/layers.js.map +1 -1
  53. package/dist/commands/search.d.ts +6 -1
  54. package/dist/commands/search.d.ts.map +1 -1
  55. package/dist/commands/search.js +7 -7
  56. package/dist/commands/search.js.map +1 -1
  57. package/dist/commands/version.d.ts +17 -2
  58. package/dist/commands/version.d.ts.map +1 -1
  59. package/dist/commands/version.js +131 -19
  60. package/dist/commands/version.js.map +1 -1
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/internal/NodeLayers.js +1 -1
  66. package/dist/internal/NodeLayers.js.map +1 -1
  67. package/dist/internal/attachmentInsertion.d.ts.map +1 -1
  68. package/dist/internal/frontmatter.d.ts.map +1 -1
  69. package/dist/internal/frontmatter.js +2 -1
  70. package/dist/internal/frontmatter.js.map +1 -1
  71. package/dist/internal/jqlBuilder.d.ts.map +1 -1
  72. package/dist/internal/oauthServer.d.ts +10 -4
  73. package/dist/internal/oauthServer.d.ts.map +1 -1
  74. package/dist/internal/oauthServer.js +13 -8
  75. package/dist/internal/oauthServer.js.map +1 -1
  76. package/dist/internal/openBrowser.d.ts +1 -1
  77. package/dist/internal/openBrowser.d.ts.map +1 -1
  78. package/dist/internal/openBrowser.js +1 -1
  79. package/dist/internal/openBrowser.js.map +1 -1
  80. package/dist/internal/sync/baseline.d.ts.map +1 -1
  81. package/dist/internal/sync/baseline.js +2 -2
  82. package/dist/internal/sync/baseline.js.map +1 -1
  83. package/dist/internal/sync/changes.d.ts.map +1 -1
  84. package/dist/internal/sync/config.d.ts.map +1 -1
  85. package/dist/internal/sync/config.js +1 -1
  86. package/dist/internal/sync/config.js.map +1 -1
  87. package/dist/internal/sync/document.d.ts.map +1 -1
  88. package/dist/internal/sync/document.js +8 -8
  89. package/dist/internal/sync/document.js.map +1 -1
  90. package/dist/internal/sync/fieldValues.d.ts +9 -9
  91. package/dist/internal/sync/fieldValues.d.ts.map +1 -1
  92. package/dist/internal/sync/fieldValues.js +12 -16
  93. package/dist/internal/sync/fieldValues.js.map +1 -1
  94. package/dist/internal/sync/manifest.d.ts.map +1 -1
  95. package/dist/internal/sync/manifest.js +1 -1
  96. package/dist/internal/sync/manifest.js.map +1 -1
  97. package/dist/internal/sync/paths.d.ts.map +1 -1
  98. package/dist/internal/sync/schemas.d.ts +17 -9
  99. package/dist/internal/sync/schemas.d.ts.map +1 -1
  100. package/dist/internal/sync/schemas.js +6 -6
  101. package/dist/internal/sync/schemas.js.map +1 -1
  102. package/dist/internal/sync/types.d.ts +4 -4
  103. package/dist/internal/sync/types.d.ts.map +1 -1
  104. package/dist/internal/sync/types.js +1 -1
  105. package/dist/internal/sync/types.js.map +1 -1
  106. package/package.json +15 -11
  107. package/skills/jira/SKILL.md +17 -1
  108. package/src/AttachmentService.ts +6 -6
  109. package/src/IssueService.ts +214 -27
  110. package/src/JiraAuth.ts +106 -42
  111. package/src/MarkdownWriter.ts +6 -6
  112. package/src/SyncWorkspace.ts +12 -11
  113. package/src/VersionService.ts +199 -27
  114. package/src/bin.ts +1 -1
  115. package/src/commands/auth.ts +7 -7
  116. package/src/commands/edit.ts +88 -0
  117. package/src/commands/get.ts +3 -3
  118. package/src/commands/issue.ts +3 -2
  119. package/src/commands/issueAttachment.ts +21 -20
  120. package/src/commands/layers.ts +5 -2
  121. package/src/commands/search.ts +7 -7
  122. package/src/commands/version.ts +176 -23
  123. package/src/index.ts +13 -3
  124. package/src/internal/NodeLayers.ts +1 -1
  125. package/src/internal/frontmatter.ts +3 -2
  126. package/src/internal/oauthServer.ts +29 -16
  127. package/src/internal/openBrowser.ts +1 -1
  128. package/src/internal/sync/baseline.ts +3 -2
  129. package/src/internal/sync/config.ts +2 -1
  130. package/src/internal/sync/document.ts +15 -14
  131. package/src/internal/sync/fieldValues.ts +21 -22
  132. package/src/internal/sync/manifest.ts +1 -1
  133. package/src/internal/sync/schemas.ts +6 -6
  134. package/src/internal/sync/types.ts +4 -4
  135. package/test/IssueService.test.ts +2 -2
  136. package/test/JiraAuthRefresh.test.ts +269 -0
  137. package/test/VersionService.test.ts +2 -2
  138. package/test/commandTree.test.ts +158 -2
  139. package/test/editIssuePayload.test.ts +103 -0
  140. package/test/frontmatter.test.ts +4 -2
  141. package/test/integration.test.ts +12 -10
  142. package/test/oauthServer.test.ts +77 -4
  143. package/test/publicApi.test.ts +34 -0
  144. package/test/relatedWorkSync.test.ts +179 -0
  145. package/test/remoteWrites.test.ts +220 -0
  146. package/test/syncChanges.test.ts +2 -2
  147. package/test/syncConfig.test.ts +2 -2
@@ -14,11 +14,12 @@ import { OAuthError } from "@knpkv/atlassian-common/auth"
14
14
  import * as Context from "effect/Context"
15
15
  import * as Deferred from "effect/Deferred"
16
16
  import * as Effect from "effect/Effect"
17
+ import { HttpRouter, HttpServer, HttpServerResponse } from "effect/http"
18
+ import type * as HttpServerError from "effect/http/HttpServerError"
17
19
  import * as Layer from "effect/Layer"
20
+ import { NetAddress } from "effect/net"
18
21
  import * as Schema from "effect/Schema"
19
22
  import type * as Scope from "effect/Scope"
20
- import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http"
21
- import type * as HttpServerError from "effect/unstable/http/HttpServerError"
22
23
 
23
24
  const DEFAULT_PORT = 8585
24
25
  const MAX_PORT = 8594
@@ -30,13 +31,25 @@ type HttpServerInstance = Effect.Success<typeof HttpServer.HttpServer>
30
31
  * @category Services
31
32
  */
32
33
  export interface HttpServerFactory {
33
- readonly createServerLayer: (port: number) => Layer.Layer<
34
+ readonly createServerLayer: (options: CallbackServerListenOptions) => Layer.Layer<
34
35
  HttpServer.HttpServer,
35
36
  HttpServerError.ServeError,
36
37
  never
37
38
  >
38
39
  }
39
40
 
41
+ export interface CallbackServerListenOptions {
42
+ readonly host: "localhost"
43
+ readonly port: number
44
+ }
45
+
46
+ export const callbackServerListenOptions = (port: number): CallbackServerListenOptions => ({
47
+ host: "localhost",
48
+ port
49
+ })
50
+
51
+ export const callbackUrl = (port: number): string => `http://localhost:${port}/callback`
52
+
40
53
  /**
41
54
  * Tag for the HttpServerFactory service.
42
55
  *
@@ -57,7 +70,9 @@ export class HttpServerFactoryTag extends Context.Service<
57
70
  * @category Layers
58
71
  */
59
72
  export const makeHttpServerFactory = (
60
- createLayerFn: (port: number) => Layer.Layer<HttpServer.HttpServer, HttpServerError.ServeError, never>
73
+ createLayerFn: (
74
+ options: CallbackServerListenOptions
75
+ ) => Layer.Layer<HttpServer.HttpServer, HttpServerError.ServeError, never>
61
76
  ): Layer.Layer<HttpServerFactoryTag> =>
62
77
  Layer.succeed(HttpServerFactoryTag, {
63
78
  createServerLayer: createLayerFn
@@ -98,7 +113,7 @@ export const startCallbackServer = (
98
113
  { readonly context: Context.Context<HttpServer.HttpServer>; readonly port: number },
99
114
  OAuthError
100
115
  > =>
101
- Layer.buildWithScope(factory.createServerLayer(port), serverScope).pipe(
116
+ Layer.buildWithScope(factory.createServerLayer(callbackServerListenOptions(port)), serverScope).pipe(
102
117
  Effect.map((context) => ({ context, port })),
103
118
  Effect.catchIf(
104
119
  (error) => isAddressInUse(error) && port < MAX_PORT,
@@ -108,7 +123,7 @@ export const startCallbackServer = (
108
123
  )
109
124
  const { context: serverContext } = yield* buildServerContext(DEFAULT_PORT)
110
125
  const server: HttpServerInstance = Context.get(serverContext, HttpServer.HttpServer)
111
- const port = yield* (server.address._tag === "TcpAddress"
126
+ const port = yield* (NetAddress.isInetAddress(server.address)
112
127
  ? Effect.succeed(server.address.port)
113
128
  : Effect.fail(new OAuthError({ step: "authorize", cause: "OAuth callback server must listen on a TCP port" })))
114
129
 
@@ -118,29 +133,27 @@ export const startCallbackServer = (
118
133
  "/callback",
119
134
  (req) =>
120
135
  Effect.gen(function*() {
121
- const url = new URL(req.url, `http://localhost:${port}`)
136
+ const url = new URL(req.url, callbackUrl(port))
122
137
  const code = url.searchParams.get("code")
123
138
  const state = url.searchParams.get("state")
124
139
  const error = url.searchParams.get("error")
125
140
  const errorDescription = url.searchParams.get("error_description")
126
141
 
127
- if (error) {
128
- yield* Deferred.fail(
129
- deferred,
130
- new OAuthError({ step: "authorize", cause: errorDescription ?? error })
131
- )
142
+ if (state !== expectedState) {
132
143
  return HttpServerResponse.html(
133
- "<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>"
144
+ "<html><body><h1>Security Error</h1><p>State verification failed.</p></body></html>"
145
+ ).pipe(
146
+ HttpServerResponse.setStatus(403)
134
147
  )
135
148
  }
136
149
 
137
- if (state !== expectedState) {
150
+ if (error) {
138
151
  yield* Deferred.fail(
139
152
  deferred,
140
- new OAuthError({ step: "authorize", cause: "State mismatch - possible CSRF attack" })
153
+ new OAuthError({ step: "authorize", cause: errorDescription ?? error })
141
154
  )
142
155
  return HttpServerResponse.html(
143
- "<html><body><h1>Security Error</h1><p>State verification failed.</p></body></html>"
156
+ "<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>"
144
157
  )
145
158
  }
146
159
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import * as Effect from "effect/Effect"
7
7
  import type * as PlatformError from "effect/PlatformError"
8
- import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
8
+ import { ChildProcess, ChildProcessSpawner } from "effect/process"
9
9
 
10
10
  const run = (
11
11
  command: string,
@@ -15,7 +15,7 @@ export const parseSyncBaseline = (
15
15
  ): Effect.Effect<SyncBaseline, SyncWorkspaceError | SyncValidationError> =>
16
16
  Effect.gen(function*() {
17
17
  const raw = yield* Effect.try({
18
- try: (): unknown => JSON.parse(content),
18
+ try: () => Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json))(content),
19
19
  catch: (cause) => new SyncWorkspaceError({ message: "Failed to parse Sync Baseline JSON", path, cause })
20
20
  })
21
21
  const baseline: SyncBaseline = yield* Schema.decodeUnknownEffect(SyncBaselineSchema)(raw).pipe(
@@ -24,4 +24,5 @@ export const parseSyncBaseline = (
24
24
  return baseline
25
25
  })
26
26
 
27
- export const serializeSyncBaseline = (baseline: SyncBaseline): string => `${JSON.stringify(baseline, null, 2)}\n`
27
+ export const serializeSyncBaseline = (baseline: SyncBaseline): string =>
28
+ `${JSON.stringify(Schema.encodeSync(SyncBaselineSchema)(baseline), null, 2)}\n`
@@ -28,7 +28,8 @@ export const parseWorkspaceConfig = (
28
28
  return config
29
29
  })
30
30
 
31
- export const serializeWorkspaceConfig = (config: WorkspaceConfig): string => yaml.dump(config, { lineWidth: 100 })
31
+ export const serializeWorkspaceConfig = (config: WorkspaceConfig): string =>
32
+ yaml.dump(Schema.encodeSync(WorkspaceConfigSchema)(config), { lineWidth: 100 })
32
33
 
33
34
  export const makeDefaultWorkspaceConfig = (siteUrl: string): WorkspaceConfig => ({
34
35
  version: 1,
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { isPreviewableAttachment } from "@knpkv/atlassian-common/attachments"
7
7
  import * as Predicate from "effect/Predicate"
8
+ import type * as Schema from "effect/Schema"
8
9
  import matter from "gray-matter"
9
10
  import * as yaml from "js-yaml"
10
11
  import { SyncValidationError } from "../../JiraCliError.js"
@@ -17,19 +18,19 @@ import type {
17
18
  UserFieldValue
18
19
  } from "./types.js"
19
20
 
20
- const isStringArray = (value: unknown): value is ReadonlyArray<string> =>
21
- Array.isArray(value) && value.every((item) => typeof item === "string")
21
+ const isStringArray = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & ReadonlyArray<string> =>
22
+ Array.isArray(value) && value.every((item) => Predicate.isString(item))
22
23
 
23
- const isFrontMatterCustomFields = (
24
- value: unknown
25
- ): value is IssueDocumentFrontMatter["customFields"] => Predicate.isReadonlyObject(value)
24
+ const isFrontMatterCustomFields = <UnparsedInput>(
25
+ value: UnparsedInput
26
+ ): value is UnparsedInput & IssueDocumentFrontMatter["customFields"] => Predicate.isReadonlyObject(value)
26
27
 
27
28
  const yamlEngine = {
28
29
  parse: (str: string): object => {
29
30
  const value = yaml.load(str)
30
31
  return Predicate.isReadonlyObject(value) ? value : {}
31
32
  },
32
- stringify: (data: object): string => yaml.dump(data)
33
+ stringify: <Data extends object>(data: Data): string => yaml.dump(data)
33
34
  }
34
35
 
35
36
  export const DESCRIPTION_SECTION = "Description"
@@ -88,17 +89,17 @@ const section = (name: string, content: string): string => {
88
89
  return normalized.length > 0 ? `## ${name}\n\n${normalized}\n` : `## ${name}\n`
89
90
  }
90
91
 
91
- const parseFrontMatter = (path: string, data: Record<string, unknown>): IssueDocumentFrontMatter => {
92
+ const parseFrontMatter = (path: string, data: Record<string, Schema.Json>): IssueDocumentFrontMatter => {
92
93
  const requiredString = (key: string): string => {
93
94
  const value = data[key]
94
- if (typeof value === "string") return value
95
+ if (Predicate.isString(value)) return value
95
96
  return fail(path, `Missing or invalid front matter field "${key}"`)
96
97
  }
97
98
 
98
99
  const nullableString = (key: string): string | null => {
99
100
  const value = data[key]
100
101
  if (value === null || value === undefined) return null
101
- if (typeof value === "string") return value
102
+ if (Predicate.isString(value)) return value
102
103
  return fail(path, `Invalid front matter field "${key}"`)
103
104
  }
104
105
 
@@ -108,7 +109,7 @@ const parseFrontMatter = (path: string, data: Record<string, unknown>): IssueDoc
108
109
  const record = Predicate.isReadonlyObject(value) ? value : fail(path, `Invalid user field "${key}"`)
109
110
  const accountId = record["accountId"]
110
111
  const displayName = record["displayName"]
111
- if (typeof accountId === "string" && typeof displayName === "string") {
112
+ if (Predicate.isString(accountId) && Predicate.isString(displayName)) {
112
113
  return {
113
114
  accountId,
114
115
  displayName
@@ -244,13 +245,13 @@ const parseAttachmentMetadata = (
244
245
  const parsed = JSON.parse(raw)
245
246
  if (!Predicate.isReadonlyObject(parsed)) return {}
246
247
  return {
247
- id: typeof parsed["jiraAttachmentId"] === "string"
248
+ id: Predicate.isString(parsed["jiraAttachmentId"])
248
249
  ? parsed["jiraAttachmentId"]
249
- : typeof parsed["id"] === "string"
250
+ : Predicate.isString(parsed["id"])
250
251
  ? parsed["id"]
251
252
  : "",
252
- mediaType: typeof parsed["mediaType"] === "string" ? parsed["mediaType"] : null,
253
- size: typeof parsed["size"] === "number" ? parsed["size"] : null
253
+ mediaType: Predicate.isString(parsed["mediaType"]) ? parsed["mediaType"] : null,
254
+ size: Predicate.isNumber(parsed["size"]) ? parsed["size"] : null
254
255
  }
255
256
  } catch {
256
257
  return {}
@@ -16,7 +16,7 @@ export interface CanonicalFieldValueOptions {
16
16
 
17
17
  export const explicitClear = null
18
18
 
19
- export const isExplicitClear = (value: unknown): value is null => value === null
19
+ export const isExplicitClear = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & null => value === null
20
20
 
21
21
  export const userFieldValue = (accountId: string, displayName: string): UserFieldValue => ({
22
22
  accountId,
@@ -44,24 +44,26 @@ export const completeListValue = (
44
44
 
45
45
  export const makeCompleteListValue = completeListValue
46
46
 
47
- export const isUserFieldValue = (value: unknown): value is UserFieldValue => {
47
+ export const isUserFieldValue = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & UserFieldValue => {
48
48
  if (!Predicate.isReadonlyObject(value)) return false
49
- return typeof value["accountId"] === "string" && typeof value["displayName"] === "string"
49
+ return Predicate.isString(value["accountId"]) && Predicate.isString(value["displayName"])
50
50
  }
51
51
 
52
- export const isOptionFieldValue = (value: unknown): value is OptionFieldValue => {
53
- if (!Predicate.isReadonlyObject(value) || typeof value["value"] !== "string") return false
52
+ export const isOptionFieldValue = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & OptionFieldValue => {
53
+ if (!Predicate.isReadonlyObject(value) || !Predicate.isString(value["value"])) return false
54
54
  const id = value["id"]
55
- return id === undefined || typeof id === "string"
55
+ return id === undefined || Predicate.isString(id)
56
56
  }
57
57
 
58
- export const isCascadingFieldValue = (value: unknown): value is CascadingFieldValue => {
58
+ export const isCascadingFieldValue = <UnparsedInput>(
59
+ value: UnparsedInput
60
+ ): value is UnparsedInput & CascadingFieldValue => {
59
61
  if (!Predicate.isReadonlyObject(value) || !isOptionFieldValue(value["parent"])) return false
60
62
  const child = value["child"]
61
63
  return child === undefined || isOptionFieldValue(child)
62
64
  }
63
65
 
64
- export const isCompleteListValue = (value: unknown): value is CompleteListValue =>
66
+ export const isCompleteListValue = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & CompleteListValue =>
65
67
  Array.isArray(value) && value.every(isCompleteListItem)
66
68
 
67
69
  export const canonicalFieldOrder = <A extends CompleteListItem>(
@@ -104,7 +106,7 @@ const canonicalFieldValueKey = (
104
106
  options: CanonicalFieldValueOptions
105
107
  ): string => {
106
108
  if (value === null) return "clear:"
107
- if (Array.isArray(value)) {
109
+ if (isCompleteListValue(value)) {
108
110
  return `list:${canonicalFieldOrder(value, options).map(completeListItemOrderKey).join("\u0000")}`
109
111
  }
110
112
  if (isCascadingFieldValue(value)) {
@@ -114,13 +116,13 @@ const canonicalFieldValueKey = (
114
116
  }
115
117
  if (isUserFieldValue(value)) return userOrderKey(value)
116
118
  if (isOptionFieldValue(value)) return optionOrderKey(value)
117
- return `${typeof value}:${String(value)}`
119
+ return scalarOrderKey(value)
118
120
  }
119
121
 
120
122
  const completeListItemOrderKey = (value: CompleteListItem): string => {
121
123
  if (isUserFieldValue(value)) return userOrderKey(value)
122
124
  if (isOptionFieldValue(value)) return optionOrderKey(value)
123
- return `${typeof value}:${String(value)}`
125
+ return scalarOrderKey(value)
124
126
  }
125
127
 
126
128
  const userOrderKey = (value: UserFieldValue): string =>
@@ -129,15 +131,12 @@ const userOrderKey = (value: UserFieldValue): string =>
129
131
  const optionOrderKey = (value: OptionFieldValue): string =>
130
132
  `option:${value.value.toLocaleLowerCase("en")}\u0000${value.id ?? ""}`
131
133
 
132
- const isCompleteListItem = (value: unknown): value is CompleteListItem => {
133
- switch (typeof value) {
134
- case "string":
135
- case "number":
136
- case "boolean":
137
- return true
138
- case "object":
139
- return isUserFieldValue(value) || isOptionFieldValue(value)
140
- default:
141
- return false
142
- }
134
+ const scalarOrderKey = (value: string | number | boolean): string => {
135
+ const kind = Predicate.isString(value) ? "string" : Predicate.isNumber(value) ? "number" : "boolean"
136
+ return `${kind}:${String(value)}`
137
+ }
138
+
139
+ const isCompleteListItem = <UnparsedInput>(value: UnparsedInput): value is UnparsedInput & CompleteListItem => {
140
+ return Predicate.isString(value) || Predicate.isNumber(value) || Predicate.isBoolean(value) ||
141
+ isUserFieldValue(value) || isOptionFieldValue(value)
143
142
  }
@@ -21,7 +21,7 @@ export const parseSyncManifest = (
21
21
  ): Effect.Effect<SyncManifest, SyncWorkspaceError | SyncValidationError> =>
22
22
  Effect.gen(function*() {
23
23
  const raw = yield* Effect.try({
24
- try: (): unknown => JSON.parse(content),
24
+ try: () => Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json))(content),
25
25
  catch: (cause) => new SyncWorkspaceError({ message: "Failed to parse Sync Manifest JSON", path, cause })
26
26
  })
27
27
  return yield* Schema.decodeUnknownEffect(SyncManifestSchema)(raw).pipe(
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import * as Effect from "effect/Effect"
7
7
  import * as Schema from "effect/Schema"
8
- import { FIELD_SHAPES } from "./types.js"
8
+ import { FIELD_CONTRACTS } from "./types.js"
9
9
 
10
10
  const SiteUrl = Schema.String.pipe(
11
11
  Schema.check(Schema.isPattern(/^https:\/\/[a-z0-9][a-z0-9-]*\.atlassian\.net$/))
@@ -14,14 +14,14 @@ const SiteUrl = Schema.String.pipe(
14
14
  const NonEmptyString = Schema.String.pipe(Schema.check(Schema.isMinLength(1)))
15
15
  const FilenameModes: readonly ["convention", "custom"] = ["convention", "custom"]
16
16
 
17
- export const FieldShapeSchema = Schema.Literals(FIELD_SHAPES)
17
+ export const FieldContractSchema = Schema.Literals(FIELD_CONTRACTS)
18
18
 
19
19
  export const RequestedCustomFieldSchema = Schema.Struct({
20
20
  displayName: NonEmptyString,
21
21
  fieldId: Schema.optional(NonEmptyString),
22
- shape: FieldShapeSchema,
22
+ form: FieldContractSchema,
23
23
  ordered: Schema.optional(Schema.Boolean)
24
- })
24
+ }).pipe(Schema.encodeKeys({ form: "shape" }))
25
25
 
26
26
  export const WorkspaceConfigSchema = Schema.Struct({
27
27
  version: Schema.Literal(1).pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1))),
@@ -82,9 +82,9 @@ export const SyncFieldValueSchema = Schema.Union([
82
82
  export const BaselineCustomFieldSchema = Schema.Struct({
83
83
  fieldId: NonEmptyString,
84
84
  displayName: NonEmptyString,
85
- shape: FieldShapeSchema,
85
+ form: FieldContractSchema,
86
86
  value: SyncFieldValueSchema
87
- })
87
+ }).pipe(Schema.encodeKeys({ form: "shape" }))
88
88
 
89
89
  export const SyncBaselineSchema = Schema.Struct({
90
90
  version: Schema.Literal(1),
@@ -4,7 +4,7 @@
4
4
  * @internal
5
5
  */
6
6
 
7
- export type FieldShape =
7
+ export type FieldContract =
8
8
  | "text"
9
9
  | "multilineText"
10
10
  | "number"
@@ -15,7 +15,7 @@ export type FieldShape =
15
15
  | "user"
16
16
  | "cascadingSelect"
17
17
 
18
- export const FIELD_SHAPES: ReadonlyArray<FieldShape> = [
18
+ export const FIELD_CONTRACTS: ReadonlyArray<FieldContract> = [
19
19
  "text",
20
20
  "multilineText",
21
21
  "number",
@@ -30,7 +30,7 @@ export const FIELD_SHAPES: ReadonlyArray<FieldShape> = [
30
30
  export interface RequestedCustomField {
31
31
  readonly displayName: string
32
32
  readonly fieldId?: string | undefined
33
- readonly shape: FieldShape
33
+ readonly form: FieldContract
34
34
  readonly ordered?: boolean | undefined
35
35
  }
36
36
 
@@ -83,7 +83,7 @@ export type SyncFieldValue =
83
83
  export interface BaselineCustomField {
84
84
  readonly fieldId: string
85
85
  readonly displayName: string
86
- readonly shape: FieldShape
86
+ readonly form: FieldContract
87
87
  readonly value: SyncFieldValue
88
88
  }
89
89
 
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, it } from "@effect/vitest"
2
2
  import { JiraApiClient, make } from "@knpkv/jira-api-client"
3
3
  import * as Effect from "effect/Effect"
4
+ import * as HttpClient from "effect/http/HttpClient"
5
+ import * as HttpClientResponse from "effect/http/HttpClientResponse"
4
6
  import * as Layer from "effect/Layer"
5
7
  import * as Redacted from "effect/Redacted"
6
- import * as HttpClient from "effect/unstable/http/HttpClient"
7
- import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
8
8
  import { IssueService, layer as IssueServiceLayer, SiteUrl } from "../src/IssueService.js"
9
9
 
10
10
  const httpClient = HttpClient.make((request) =>