@knpkv/jira-cli 0.1.1 → 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.
- package/CHANGELOG.md +67 -0
- package/README.md +66 -4
- package/dist/IssueService.d.ts +2 -2
- package/dist/IssueService.d.ts.map +1 -1
- package/dist/IssueService.js +3 -3
- package/dist/IssueService.js.map +1 -1
- package/dist/JiraAuth.d.ts +14 -14
- package/dist/JiraAuth.d.ts.map +1 -1
- package/dist/JiraAuth.js +18 -10
- package/dist/JiraAuth.js.map +1 -1
- package/dist/JiraCliError.d.ts +30 -0
- package/dist/JiraCliError.d.ts.map +1 -1
- package/dist/JiraCliError.js +14 -0
- package/dist/JiraCliError.js.map +1 -1
- package/dist/MarkdownWriter.d.ts +4 -4
- package/dist/MarkdownWriter.d.ts.map +1 -1
- package/dist/MarkdownWriter.js +6 -6
- package/dist/MarkdownWriter.js.map +1 -1
- package/dist/SyncWorkspace.d.ts +34 -0
- package/dist/SyncWorkspace.d.ts.map +1 -0
- package/dist/SyncWorkspace.js +105 -0
- package/dist/SyncWorkspace.js.map +1 -0
- package/dist/VersionService.d.ts +206 -0
- package/dist/VersionService.d.ts.map +1 -0
- package/dist/VersionService.js +426 -0
- package/dist/VersionService.js.map +1 -0
- package/dist/bin.js +29 -22
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +2 -21
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +6 -6
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/get.d.ts +3 -8
- package/dist/commands/get.d.ts.map +1 -1
- package/dist/commands/get.js +4 -4
- package/dist/commands/get.js.map +1 -1
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -2
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/issue.d.ts +8 -0
- package/dist/commands/issue.d.ts.map +1 -0
- package/dist/commands/issue.js +10 -0
- package/dist/commands/issue.js.map +1 -0
- package/dist/commands/layers.d.ts +6 -18
- package/dist/commands/layers.d.ts.map +1 -1
- package/dist/commands/layers.js +35 -24
- package/dist/commands/layers.js.map +1 -1
- package/dist/commands/search.d.ts +3 -8
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +8 -8
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/version.d.ts +12 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +179 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +14 -1
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/oauthServer.d.ts +17 -5
- package/dist/internal/oauthServer.d.ts.map +1 -1
- package/dist/internal/oauthServer.js +23 -40
- package/dist/internal/oauthServer.js.map +1 -1
- package/dist/internal/openBrowser.d.ts +10 -0
- package/dist/internal/openBrowser.d.ts.map +1 -0
- package/dist/internal/openBrowser.js +17 -0
- package/dist/internal/openBrowser.js.map +1 -0
- package/dist/internal/sync/baseline.d.ts +11 -0
- package/dist/internal/sync/baseline.d.ts.map +1 -0
- package/dist/internal/sync/baseline.js +18 -0
- package/dist/internal/sync/baseline.js.map +1 -0
- package/dist/internal/sync/changes.d.ts +15 -0
- package/dist/internal/sync/changes.d.ts.map +1 -0
- package/dist/internal/sync/changes.js +72 -0
- package/dist/internal/sync/changes.js.map +1 -0
- package/dist/internal/sync/config.d.ts +12 -0
- package/dist/internal/sync/config.d.ts.map +1 -0
- package/dist/internal/sync/config.js +53 -0
- package/dist/internal/sync/config.js.map +1 -0
- package/dist/internal/sync/document.d.ts +9 -0
- package/dist/internal/sync/document.d.ts.map +1 -0
- package/dist/internal/sync/document.js +173 -0
- package/dist/internal/sync/document.js.map +1 -0
- package/dist/internal/sync/fieldValues.d.ts +30 -0
- package/dist/internal/sync/fieldValues.d.ts.map +1 -0
- package/dist/internal/sync/fieldValues.js +91 -0
- package/dist/internal/sync/fieldValues.js.map +1 -0
- package/dist/internal/sync/manifest.d.ts +12 -0
- package/dist/internal/sync/manifest.d.ts.map +1 -0
- package/dist/internal/sync/manifest.js +23 -0
- package/dist/internal/sync/manifest.js.map +1 -0
- package/dist/internal/sync/paths.d.ts +26 -0
- package/dist/internal/sync/paths.d.ts.map +1 -0
- package/dist/internal/sync/paths.js +22 -0
- package/dist/internal/sync/paths.js.map +1 -0
- package/dist/internal/sync/schemas.d.ts +128 -0
- package/dist/internal/sync/schemas.d.ts.map +1 -0
- package/dist/internal/sync/schemas.js +82 -0
- package/dist/internal/sync/schemas.js.map +1 -0
- package/dist/internal/sync/types.d.ts +144 -0
- package/dist/internal/sync/types.d.ts.map +1 -0
- package/dist/internal/sync/types.js +17 -0
- package/dist/internal/sync/types.js.map +1 -0
- package/package.json +13 -12
- package/skills/jira/SKILL.md +90 -0
- package/skills/jira/agents/openai.yaml +4 -0
- package/src/IssueService.ts +34 -28
- package/src/JiraAuth.ts +53 -39
- package/src/JiraCliError.ts +24 -0
- package/src/MarkdownWriter.ts +7 -11
- package/src/SyncWorkspace.ts +185 -0
- package/src/VersionService.ts +647 -0
- package/src/bin.ts +39 -29
- package/src/commands/auth.ts +6 -12
- package/src/commands/get.ts +4 -4
- package/src/commands/index.ts +2 -2
- package/src/commands/issue.ts +13 -0
- package/src/commands/layers.ts +44 -26
- package/src/commands/search.ts +8 -8
- package/src/commands/version.ts +267 -0
- package/src/internal/frontmatter.ts +15 -1
- package/src/internal/oauthServer.ts +43 -70
- package/src/internal/openBrowser.ts +31 -0
- package/src/internal/sync/baseline.ts +27 -0
- package/src/internal/sync/changes.ts +118 -0
- package/src/internal/sync/config.ts +76 -0
- package/src/internal/sync/document.ts +201 -0
- package/src/internal/sync/fieldValues.ts +145 -0
- package/src/internal/sync/manifest.ts +32 -0
- package/src/internal/sync/paths.ts +48 -0
- package/src/internal/sync/schemas.ts +103 -0
- package/src/internal/sync/types.ts +192 -0
- package/test/SyncWorkspace.test.ts +76 -0
- package/test/VersionService.test.ts +266 -0
- package/test/commandTree.test.ts +266 -0
- package/test/frontmatter.test.ts +69 -0
- package/test/integration.test.ts +187 -0
- package/test/syncChanges.test.ts +106 -0
- package/test/syncConfig.test.ts +138 -0
- package/test/syncDocument.test.ts +69 -0
- package/test/syncFieldValues.test.ts +101 -0
- package/vitest.config.integration.ts +17 -0
- package/vitest.config.ts +6 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field value helpers for Jira Markdown Sync reconciliation.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import type { CascadingFieldValue, OptionFieldValue, SyncFieldValue, UserFieldValue } from "./types.js"
|
|
7
|
+
|
|
8
|
+
export type CompleteListItem = string | number | boolean | UserFieldValue | OptionFieldValue
|
|
9
|
+
|
|
10
|
+
export type CompleteListValue = ReadonlyArray<CompleteListItem>
|
|
11
|
+
|
|
12
|
+
export interface CanonicalFieldValueOptions {
|
|
13
|
+
readonly ordered?: boolean | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const explicitClear = null
|
|
17
|
+
|
|
18
|
+
export const isExplicitClear = (value: unknown): value is null => value === null
|
|
19
|
+
|
|
20
|
+
export const userFieldValue = (accountId: string, displayName: string): UserFieldValue => ({
|
|
21
|
+
accountId,
|
|
22
|
+
displayName
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const makeUserFieldValue = userFieldValue
|
|
26
|
+
|
|
27
|
+
export const optionFieldValue = (value: string, id?: string): OptionFieldValue =>
|
|
28
|
+
id === undefined ? { value } : { id, value }
|
|
29
|
+
|
|
30
|
+
export const makeOptionFieldValue = optionFieldValue
|
|
31
|
+
|
|
32
|
+
export const cascadingFieldValue = (
|
|
33
|
+
parent: OptionFieldValue,
|
|
34
|
+
child?: OptionFieldValue
|
|
35
|
+
): CascadingFieldValue => child === undefined ? { parent } : { parent, child }
|
|
36
|
+
|
|
37
|
+
export const makeCascadingFieldValue = cascadingFieldValue
|
|
38
|
+
|
|
39
|
+
export const completeListValue = (
|
|
40
|
+
items: Iterable<CompleteListItem>,
|
|
41
|
+
options: CanonicalFieldValueOptions = {}
|
|
42
|
+
): CompleteListValue => canonicalFieldOrder(Array.from(items), options)
|
|
43
|
+
|
|
44
|
+
export const makeCompleteListValue = completeListValue
|
|
45
|
+
|
|
46
|
+
export const isUserFieldValue = (value: unknown): value is UserFieldValue => {
|
|
47
|
+
if (!isRecord(value)) return false
|
|
48
|
+
return typeof value["accountId"] === "string" && typeof value["displayName"] === "string"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const isOptionFieldValue = (value: unknown): value is OptionFieldValue => {
|
|
52
|
+
if (!isRecord(value) || typeof value["value"] !== "string") return false
|
|
53
|
+
const id = value["id"]
|
|
54
|
+
return id === undefined || typeof id === "string"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const isCascadingFieldValue = (value: unknown): value is CascadingFieldValue => {
|
|
58
|
+
if (!isRecord(value) || !isOptionFieldValue(value["parent"])) return false
|
|
59
|
+
const child = value["child"]
|
|
60
|
+
return child === undefined || isOptionFieldValue(child)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const isCompleteListValue = (value: unknown): value is CompleteListValue =>
|
|
64
|
+
Array.isArray(value) && value.every(isCompleteListItem)
|
|
65
|
+
|
|
66
|
+
export const canonicalFieldOrder = <A extends CompleteListItem>(
|
|
67
|
+
items: ReadonlyArray<A>,
|
|
68
|
+
options: CanonicalFieldValueOptions = {}
|
|
69
|
+
): ReadonlyArray<A> => options.ordered === true ? [...items] : [...items].sort(compareCompleteListItems)
|
|
70
|
+
|
|
71
|
+
export const canonicalizeFieldValue = (
|
|
72
|
+
value: SyncFieldValue,
|
|
73
|
+
options: CanonicalFieldValueOptions = {}
|
|
74
|
+
): SyncFieldValue => {
|
|
75
|
+
if (Array.isArray(value)) return canonicalFieldOrder(value, options)
|
|
76
|
+
if (isCascadingFieldValue(value)) {
|
|
77
|
+
return value.child === undefined
|
|
78
|
+
? { parent: canonicalizeOption(value.parent) }
|
|
79
|
+
: { parent: canonicalizeOption(value.parent), child: canonicalizeOption(value.child) }
|
|
80
|
+
}
|
|
81
|
+
if (isUserFieldValue(value)) return { accountId: value.accountId, displayName: value.displayName }
|
|
82
|
+
if (isOptionFieldValue(value)) return canonicalizeOption(value)
|
|
83
|
+
return value
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const fieldValuesEqual = (
|
|
87
|
+
left: SyncFieldValue,
|
|
88
|
+
right: SyncFieldValue,
|
|
89
|
+
options: CanonicalFieldValueOptions = {}
|
|
90
|
+
): boolean => canonicalFieldValueKey(left, options) === canonicalFieldValueKey(right, options)
|
|
91
|
+
|
|
92
|
+
export const compareCompleteListItems = (left: CompleteListItem, right: CompleteListItem): number => {
|
|
93
|
+
const leftKey = completeListItemOrderKey(left)
|
|
94
|
+
const rightKey = completeListItemOrderKey(right)
|
|
95
|
+
return leftKey.localeCompare(rightKey, "en", { numeric: true })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const canonicalizeOption = (value: OptionFieldValue): OptionFieldValue =>
|
|
99
|
+
value.id === undefined ? { value: value.value } : { id: value.id, value: value.value }
|
|
100
|
+
|
|
101
|
+
const canonicalFieldValueKey = (
|
|
102
|
+
value: SyncFieldValue,
|
|
103
|
+
options: CanonicalFieldValueOptions
|
|
104
|
+
): string => {
|
|
105
|
+
if (value === null) return "clear:"
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
return `list:${canonicalFieldOrder(value, options).map(completeListItemOrderKey).join("\u0000")}`
|
|
108
|
+
}
|
|
109
|
+
if (isCascadingFieldValue(value)) {
|
|
110
|
+
return `cascading:${optionOrderKey(value.parent)}\u0000${
|
|
111
|
+
value.child === undefined ? "" : optionOrderKey(value.child)
|
|
112
|
+
}`
|
|
113
|
+
}
|
|
114
|
+
if (isUserFieldValue(value)) return userOrderKey(value)
|
|
115
|
+
if (isOptionFieldValue(value)) return optionOrderKey(value)
|
|
116
|
+
return `${typeof value}:${String(value)}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const completeListItemOrderKey = (value: CompleteListItem): string => {
|
|
120
|
+
if (isUserFieldValue(value)) return userOrderKey(value)
|
|
121
|
+
if (isOptionFieldValue(value)) return optionOrderKey(value)
|
|
122
|
+
return `${typeof value}:${String(value)}`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const userOrderKey = (value: UserFieldValue): string =>
|
|
126
|
+
`user:${value.displayName.toLocaleLowerCase("en")}\u0000${value.accountId}`
|
|
127
|
+
|
|
128
|
+
const optionOrderKey = (value: OptionFieldValue): string =>
|
|
129
|
+
`option:${value.value.toLocaleLowerCase("en")}\u0000${value.id ?? ""}`
|
|
130
|
+
|
|
131
|
+
const isCompleteListItem = (value: unknown): value is CompleteListItem => {
|
|
132
|
+
switch (typeof value) {
|
|
133
|
+
case "string":
|
|
134
|
+
case "number":
|
|
135
|
+
case "boolean":
|
|
136
|
+
return true
|
|
137
|
+
case "object":
|
|
138
|
+
return isUserFieldValue(value) || isOptionFieldValue(value)
|
|
139
|
+
default:
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
145
|
+
value !== null && typeof value === "object" && !Array.isArray(value)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Manifest helpers.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
import { SyncValidationError, SyncWorkspaceError } from "../../JiraCliError.js"
|
|
9
|
+
import { SyncManifestSchema } from "./schemas.js"
|
|
10
|
+
import type { SyncManifest } from "./types.js"
|
|
11
|
+
|
|
12
|
+
export const makeEmptyManifest = (siteUrl: string): SyncManifest => ({
|
|
13
|
+
version: 1,
|
|
14
|
+
siteUrl,
|
|
15
|
+
issues: []
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const parseSyncManifest = (
|
|
19
|
+
path: string,
|
|
20
|
+
content: string
|
|
21
|
+
): Effect.Effect<SyncManifest, SyncWorkspaceError | SyncValidationError> =>
|
|
22
|
+
Effect.gen(function*() {
|
|
23
|
+
const raw = yield* Effect.try({
|
|
24
|
+
try: () => JSON.parse(content) as unknown,
|
|
25
|
+
catch: (cause) => new SyncWorkspaceError({ message: "Failed to parse Sync Manifest JSON", path, cause })
|
|
26
|
+
})
|
|
27
|
+
return yield* Schema.decodeUnknownEffect(SyncManifestSchema)(raw).pipe(
|
|
28
|
+
Effect.mapError((cause) => new SyncValidationError({ message: "Invalid Sync Manifest", path, cause }))
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const serializeSyncManifest = (manifest: SyncManifest): string => `${JSON.stringify(manifest, null, 2)}\n`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path helpers for Jira Markdown Sync workspace files.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import type * as Path from "effect/Path"
|
|
7
|
+
import type { WorkspaceConfig } from "./types.js"
|
|
8
|
+
|
|
9
|
+
export const METADATA_DIR = ".jira-md"
|
|
10
|
+
export const DEFAULT_DOCUMENTS_DIR = "issues"
|
|
11
|
+
export const CONFIG_FILE = "config.yaml"
|
|
12
|
+
export const MANIFEST_FILE = "manifest.json"
|
|
13
|
+
export const BASELINES_DIR = "baselines"
|
|
14
|
+
export const HISTORY_DIR = "history"
|
|
15
|
+
|
|
16
|
+
export interface SyncWorkspacePaths {
|
|
17
|
+
readonly root: string
|
|
18
|
+
readonly documentsDir: string
|
|
19
|
+
readonly metadataDir: string
|
|
20
|
+
readonly configFile: string
|
|
21
|
+
readonly manifestFile: string
|
|
22
|
+
readonly baselinesDir: string
|
|
23
|
+
readonly historyDir: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const resolveWorkspacePaths = (
|
|
27
|
+
path: Path.Path,
|
|
28
|
+
root: string,
|
|
29
|
+
config?: Pick<WorkspaceConfig, "documentsDir">
|
|
30
|
+
): SyncWorkspacePaths => {
|
|
31
|
+
const documentsDir = path.resolve(root, config?.documentsDir ?? DEFAULT_DOCUMENTS_DIR)
|
|
32
|
+
const metadataDir = path.resolve(root, METADATA_DIR)
|
|
33
|
+
return {
|
|
34
|
+
root: path.resolve(root),
|
|
35
|
+
documentsDir,
|
|
36
|
+
metadataDir,
|
|
37
|
+
configFile: path.join(metadataDir, CONFIG_FILE),
|
|
38
|
+
manifestFile: path.join(metadataDir, MANIFEST_FILE),
|
|
39
|
+
baselinesDir: path.join(metadataDir, BASELINES_DIR),
|
|
40
|
+
historyDir: path.join(metadataDir, HISTORY_DIR)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const baselineFilePath = (path: Path.Path, paths: SyncWorkspacePaths, issueId: string): string =>
|
|
45
|
+
path.join(paths.baselinesDir, `${issueId}.json`)
|
|
46
|
+
|
|
47
|
+
export const conventionDocumentPath = (path: Path.Path, paths: SyncWorkspacePaths, issueKey: string): string =>
|
|
48
|
+
path.join(paths.documentsDir, `${issueKey}.md`)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime schemas for Jira Markdown Sync local workspace files.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
import { FIELD_SHAPES } from "./types.js"
|
|
9
|
+
|
|
10
|
+
const SiteUrl = Schema.String.pipe(
|
|
11
|
+
Schema.check(Schema.isPattern(/^https:\/\/[a-z0-9][a-z0-9-]*\.atlassian\.net$/))
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const NonEmptyString = Schema.String.pipe(Schema.check(Schema.isMinLength(1)))
|
|
15
|
+
|
|
16
|
+
export const FieldShapeSchema = Schema.Literals(FIELD_SHAPES)
|
|
17
|
+
|
|
18
|
+
export const RequestedCustomFieldSchema = Schema.Struct({
|
|
19
|
+
displayName: NonEmptyString,
|
|
20
|
+
fieldId: Schema.optional(NonEmptyString),
|
|
21
|
+
shape: FieldShapeSchema,
|
|
22
|
+
ordered: Schema.optional(Schema.Boolean)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const WorkspaceConfigSchema = Schema.Struct({
|
|
26
|
+
version: Schema.Literal(1).pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1 as const))),
|
|
27
|
+
siteUrl: SiteUrl,
|
|
28
|
+
documentsDir: NonEmptyString.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed("issues"))),
|
|
29
|
+
customFields: Schema.Array(RequestedCustomFieldSchema).pipe(
|
|
30
|
+
Schema.withDecodingDefaultTypeKey(Effect.succeed([]))
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const ManifestIssueSchema = Schema.Struct({
|
|
35
|
+
issueId: NonEmptyString,
|
|
36
|
+
issueKey: NonEmptyString,
|
|
37
|
+
documentPath: NonEmptyString,
|
|
38
|
+
filenameMode: Schema.Literals(["convention", "custom"] as const)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const SyncManifestSchema = Schema.Struct({
|
|
42
|
+
version: Schema.Literal(1),
|
|
43
|
+
siteUrl: SiteUrl,
|
|
44
|
+
issues: Schema.Array(ManifestIssueSchema)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const UserFieldValueSchema = Schema.Struct({
|
|
48
|
+
accountId: NonEmptyString,
|
|
49
|
+
displayName: NonEmptyString
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const OptionFieldValueSchema = Schema.Struct({
|
|
53
|
+
id: Schema.optional(NonEmptyString),
|
|
54
|
+
value: NonEmptyString
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const CascadingFieldValueSchema = Schema.Struct({
|
|
58
|
+
parent: OptionFieldValueSchema,
|
|
59
|
+
child: Schema.optional(OptionFieldValueSchema)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const SyncFieldValueItemSchema = Schema.Union([
|
|
63
|
+
Schema.String,
|
|
64
|
+
Schema.Number,
|
|
65
|
+
Schema.Boolean,
|
|
66
|
+
UserFieldValueSchema,
|
|
67
|
+
OptionFieldValueSchema
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
export const SyncFieldValueSchema = Schema.Union([
|
|
71
|
+
Schema.String,
|
|
72
|
+
Schema.Number,
|
|
73
|
+
Schema.Boolean,
|
|
74
|
+
Schema.Null,
|
|
75
|
+
UserFieldValueSchema,
|
|
76
|
+
OptionFieldValueSchema,
|
|
77
|
+
CascadingFieldValueSchema,
|
|
78
|
+
Schema.Array(SyncFieldValueItemSchema)
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
export const BaselineCustomFieldSchema = Schema.Struct({
|
|
82
|
+
fieldId: NonEmptyString,
|
|
83
|
+
displayName: NonEmptyString,
|
|
84
|
+
shape: FieldShapeSchema,
|
|
85
|
+
value: SyncFieldValueSchema
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
export const SyncBaselineSchema = Schema.Struct({
|
|
89
|
+
version: Schema.Literal(1),
|
|
90
|
+
issueId: NonEmptyString,
|
|
91
|
+
issueKey: NonEmptyString,
|
|
92
|
+
fields: Schema.Struct({
|
|
93
|
+
summary: Schema.String,
|
|
94
|
+
description: Schema.String,
|
|
95
|
+
labels: Schema.Array(Schema.String),
|
|
96
|
+
customFields: Schema.Record(Schema.String, BaselineCustomFieldSchema)
|
|
97
|
+
}),
|
|
98
|
+
comments: Schema.Array(Schema.Struct({ id: NonEmptyString }))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export type WorkspaceConfig = Schema.Schema.Type<typeof WorkspaceConfigSchema>
|
|
102
|
+
export type SyncManifest = Schema.Schema.Type<typeof SyncManifestSchema>
|
|
103
|
+
export type SyncBaseline = Schema.Schema.Type<typeof SyncBaselineSchema>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core domain types for Jira Markdown Sync local workspace state.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const FIELD_SHAPES = [
|
|
8
|
+
"text",
|
|
9
|
+
"multilineText",
|
|
10
|
+
"number",
|
|
11
|
+
"boolean",
|
|
12
|
+
"date",
|
|
13
|
+
"singleSelect",
|
|
14
|
+
"multiSelect",
|
|
15
|
+
"user",
|
|
16
|
+
"cascadingSelect"
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
export type FieldShape = typeof FIELD_SHAPES[number]
|
|
20
|
+
|
|
21
|
+
export interface RequestedCustomField {
|
|
22
|
+
readonly displayName: string
|
|
23
|
+
readonly fieldId?: string | undefined
|
|
24
|
+
readonly shape: FieldShape
|
|
25
|
+
readonly ordered?: boolean | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface WorkspaceConfig {
|
|
29
|
+
readonly version: 1
|
|
30
|
+
readonly siteUrl: string
|
|
31
|
+
readonly documentsDir: string
|
|
32
|
+
readonly customFields: ReadonlyArray<RequestedCustomField>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type FilenameMode = "convention" | "custom"
|
|
36
|
+
|
|
37
|
+
export interface ManifestIssue {
|
|
38
|
+
readonly issueId: string
|
|
39
|
+
readonly issueKey: string
|
|
40
|
+
readonly documentPath: string
|
|
41
|
+
readonly filenameMode: FilenameMode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SyncManifest {
|
|
45
|
+
readonly version: 1
|
|
46
|
+
readonly siteUrl: string
|
|
47
|
+
readonly issues: ReadonlyArray<ManifestIssue>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UserFieldValue {
|
|
51
|
+
readonly accountId: string
|
|
52
|
+
readonly displayName: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OptionFieldValue {
|
|
56
|
+
readonly id?: string | undefined
|
|
57
|
+
readonly value: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CascadingFieldValue {
|
|
61
|
+
readonly parent: OptionFieldValue
|
|
62
|
+
readonly child?: OptionFieldValue | undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type SyncFieldScalar = string | number | boolean | null
|
|
66
|
+
|
|
67
|
+
export type SyncFieldValue =
|
|
68
|
+
| SyncFieldScalar
|
|
69
|
+
| UserFieldValue
|
|
70
|
+
| OptionFieldValue
|
|
71
|
+
| CascadingFieldValue
|
|
72
|
+
| ReadonlyArray<string | number | boolean | UserFieldValue | OptionFieldValue>
|
|
73
|
+
|
|
74
|
+
export interface BaselineCustomField {
|
|
75
|
+
readonly fieldId: string
|
|
76
|
+
readonly displayName: string
|
|
77
|
+
readonly shape: FieldShape
|
|
78
|
+
readonly value: SyncFieldValue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SyncBaselineFields {
|
|
82
|
+
readonly summary: string
|
|
83
|
+
readonly description: string
|
|
84
|
+
readonly labels: ReadonlyArray<string>
|
|
85
|
+
readonly customFields: Readonly<Record<string, BaselineCustomField>>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SyncBaselineComment {
|
|
89
|
+
readonly id: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface SyncBaseline {
|
|
93
|
+
readonly version: 1
|
|
94
|
+
readonly issueId: string
|
|
95
|
+
readonly issueKey: string
|
|
96
|
+
readonly fields: SyncBaselineFields
|
|
97
|
+
readonly comments: ReadonlyArray<SyncBaselineComment>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface IssueDocumentFrontMatter {
|
|
101
|
+
readonly issueId: string
|
|
102
|
+
readonly issueKey: string
|
|
103
|
+
readonly summary: string
|
|
104
|
+
readonly status: string
|
|
105
|
+
readonly issueType: string
|
|
106
|
+
readonly priority: string | null
|
|
107
|
+
readonly assignee: UserFieldValue | null
|
|
108
|
+
readonly reporter: UserFieldValue | null
|
|
109
|
+
readonly labels: ReadonlyArray<string>
|
|
110
|
+
readonly customFields: Readonly<Record<string, SyncFieldValue>>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CommentDraft {
|
|
114
|
+
readonly draftId: string
|
|
115
|
+
readonly body: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface AcceptedComment {
|
|
119
|
+
readonly id: string
|
|
120
|
+
readonly author: string
|
|
121
|
+
readonly created: string
|
|
122
|
+
readonly body: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface AttachmentReference {
|
|
126
|
+
readonly filename: string
|
|
127
|
+
readonly url: string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface IssueDocument {
|
|
131
|
+
readonly frontMatter: IssueDocumentFrontMatter
|
|
132
|
+
readonly description: string
|
|
133
|
+
readonly multilineCustomFields: Readonly<Record<string, string>>
|
|
134
|
+
readonly commentDrafts: ReadonlyArray<CommentDraft>
|
|
135
|
+
readonly acceptedComments: ReadonlyArray<AcceptedComment>
|
|
136
|
+
readonly attachments: ReadonlyArray<AttachmentReference>
|
|
137
|
+
readonly localNotes: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type SyncFieldPath =
|
|
141
|
+
| "summary"
|
|
142
|
+
| "description"
|
|
143
|
+
| "labels"
|
|
144
|
+
| `customFields.${string}`
|
|
145
|
+
| `readOnly.${string}`
|
|
146
|
+
|
|
147
|
+
export interface SyncValidationFailure {
|
|
148
|
+
readonly _tag: "ValidationFailure"
|
|
149
|
+
readonly message: string
|
|
150
|
+
readonly issueKey?: string
|
|
151
|
+
readonly field?: SyncFieldPath
|
|
152
|
+
readonly path?: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface SyncConflict {
|
|
156
|
+
readonly issueId: string
|
|
157
|
+
readonly issueKey: string
|
|
158
|
+
readonly field: SyncFieldPath
|
|
159
|
+
readonly baselineValue: SyncFieldValue
|
|
160
|
+
readonly jiraValue: SyncFieldValue
|
|
161
|
+
readonly documentValue: SyncFieldValue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type PlannedFieldChange =
|
|
165
|
+
| {
|
|
166
|
+
readonly _tag: "RemoteOnly"
|
|
167
|
+
readonly issueId: string
|
|
168
|
+
readonly issueKey: string
|
|
169
|
+
readonly field: SyncFieldPath
|
|
170
|
+
readonly jiraValue: SyncFieldValue
|
|
171
|
+
}
|
|
172
|
+
| {
|
|
173
|
+
readonly _tag: "LocalOnly"
|
|
174
|
+
readonly issueId: string
|
|
175
|
+
readonly issueKey: string
|
|
176
|
+
readonly field: SyncFieldPath
|
|
177
|
+
readonly documentValue: SyncFieldValue
|
|
178
|
+
}
|
|
179
|
+
| {
|
|
180
|
+
readonly _tag: "Conflict"
|
|
181
|
+
readonly issueId: string
|
|
182
|
+
readonly issueKey: string
|
|
183
|
+
readonly field: SyncFieldPath
|
|
184
|
+
readonly baselineValue: SyncFieldValue
|
|
185
|
+
readonly jiraValue: SyncFieldValue
|
|
186
|
+
readonly documentValue: SyncFieldValue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface SyncPlan {
|
|
190
|
+
readonly changes: ReadonlyArray<PlannedFieldChange>
|
|
191
|
+
readonly validationFailures: ReadonlyArray<SyncValidationFailure>
|
|
192
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as NodeServices from "@effect/platform-node/NodeServices"
|
|
2
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as FileSystem from "effect/FileSystem"
|
|
5
|
+
import * as Layer from "effect/Layer"
|
|
6
|
+
import * as Path from "effect/Path"
|
|
7
|
+
import type { SyncBaseline } from "../src/internal/sync/types.js"
|
|
8
|
+
import { layer as SyncWorkspaceLayer, SyncWorkspace } from "../src/SyncWorkspace.js"
|
|
9
|
+
|
|
10
|
+
const TestLayer = SyncWorkspaceLayer.pipe(
|
|
11
|
+
Layer.provide(NodeServices.layer),
|
|
12
|
+
Layer.provideMerge(NodeServices.layer)
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const makeTempRoot = Effect.gen(function*() {
|
|
16
|
+
const fs = yield* FileSystem.FileSystem
|
|
17
|
+
return yield* fs.makeTempDirectoryScoped()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe("SyncWorkspace", () => {
|
|
21
|
+
it.effect("initializes workspace metadata and visible documents directory", () =>
|
|
22
|
+
Effect.gen(function*() {
|
|
23
|
+
const root = yield* makeTempRoot
|
|
24
|
+
const workspace = yield* SyncWorkspace
|
|
25
|
+
const paths = yield* workspace.init({ root, siteUrl: "https://example.atlassian.net" })
|
|
26
|
+
const fs = yield* FileSystem.FileSystem
|
|
27
|
+
|
|
28
|
+
expect(yield* fs.exists(paths.documentsDir)).toBe(true)
|
|
29
|
+
expect(yield* fs.exists(paths.configFile)).toBe(true)
|
|
30
|
+
expect(yield* fs.exists(paths.manifestFile)).toBe(true)
|
|
31
|
+
expect(yield* fs.exists(paths.baselinesDir)).toBe(true)
|
|
32
|
+
expect(yield* fs.exists(paths.historyDir)).toBe(true)
|
|
33
|
+
|
|
34
|
+
const config = yield* workspace.readConfig(root)
|
|
35
|
+
const manifest = yield* workspace.readManifest(root)
|
|
36
|
+
expect(config.siteUrl).toBe("https://example.atlassian.net")
|
|
37
|
+
expect(config.documentsDir).toBe("issues")
|
|
38
|
+
expect(manifest.issues).toEqual([])
|
|
39
|
+
}).pipe(Effect.provide(TestLayer), Effect.scoped))
|
|
40
|
+
|
|
41
|
+
it.effect("writes and reads baselines by issue id", () =>
|
|
42
|
+
Effect.gen(function*() {
|
|
43
|
+
const root = yield* makeTempRoot
|
|
44
|
+
const workspace = yield* SyncWorkspace
|
|
45
|
+
yield* workspace.init({ root, siteUrl: "https://example.atlassian.net" })
|
|
46
|
+
|
|
47
|
+
const baseline: SyncBaseline = {
|
|
48
|
+
version: 1,
|
|
49
|
+
issueId: "100123",
|
|
50
|
+
issueKey: "PROJ-123",
|
|
51
|
+
fields: {
|
|
52
|
+
summary: "Fix checkout copy",
|
|
53
|
+
description: "Description",
|
|
54
|
+
labels: ["copy"],
|
|
55
|
+
customFields: {}
|
|
56
|
+
},
|
|
57
|
+
comments: []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
yield* workspace.writeBaseline(root, baseline)
|
|
61
|
+
expect(yield* workspace.readBaseline(root, "100123")).toEqual(baseline)
|
|
62
|
+
}).pipe(Effect.provide(TestLayer), Effect.scoped))
|
|
63
|
+
|
|
64
|
+
it.effect("uses configured documents directory for convention document paths", () =>
|
|
65
|
+
Effect.gen(function*() {
|
|
66
|
+
const root = yield* makeTempRoot
|
|
67
|
+
const workspace = yield* SyncWorkspace
|
|
68
|
+
const path = yield* Path.Path
|
|
69
|
+
yield* workspace.init({ root, siteUrl: "https://example.atlassian.net" })
|
|
70
|
+
const config = yield* workspace.readConfig(root)
|
|
71
|
+
yield* workspace.writeConfig(root, { ...config, documentsDir: "issue-docs" })
|
|
72
|
+
|
|
73
|
+
const filePath = yield* workspace.conventionDocumentPath(root, "PROJ-123")
|
|
74
|
+
expect(filePath).toBe(path.join(root, "issue-docs", "PROJ-123.md"))
|
|
75
|
+
}).pipe(Effect.provide(TestLayer), Effect.scoped))
|
|
76
|
+
})
|