fg-devkit 1.14.4 → 1.15.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.
@@ -11,6 +11,13 @@ alwaysApply: true
11
11
  - Avoid unrelated refactors in the same task.
12
12
  - Preserve existing behavior unless a behavior change is requested.
13
13
 
14
+ ## Code comments
15
+
16
+ - Do not add comments in code unless the user explicitly asks for them.
17
+ - Prefer self-explanatory naming and structure over inline comments.
18
+ - When updating existing code, do not add new comments or expand existing ones unless requested.
19
+ - Exception: update comments only when they are required to keep an existing comment accurate after a behavior change in the same task.
20
+
14
21
  ## Communication efficiency
15
22
 
16
23
  - Do not waste tokens: keep responses concise, high-signal, and scoped to the request.
@@ -33,7 +40,7 @@ alwaysApply: true
33
40
  - Keep TypeScript and Vue template typing strict (`strict: true`, `strictTemplates: true`).
34
41
  - Do not introduce `any`; use `unknown` for unsafe input and narrow before use.
35
42
  - Type API boundaries explicitly (request payloads, response DTOs, mutation variables, callback contracts).
36
- - Prefer named reusable types (for example under `src/types`) for shared domain shapes.
43
+ - Prefer named reusable types under `src/types/<domain>/` for shared domain shapes; re-export from `src/types/index.ts`. Do not keep shared interfaces in views, stores, or query files.
37
44
  - Avoid non-null assertions unless guarded by `enabled` checks or equivalent runtime guarantees.
38
45
 
39
46
  ## API boundaries
@@ -44,9 +51,10 @@ alwaysApply: true
44
51
 
45
52
  ## State-management boundaries
46
53
 
47
- - Use TanStack Query (or the project’s chosen data library) for server state and caching.
48
- - Use Pinia/Vuex only for client/app state that is not owned by the server.
49
- - Do not duplicate the same state across the query cache and global stores unless the project explicitly documents that pattern.
54
+ - Use TanStack Query for server state and caching. Do not add Vuex for new work.
55
+ - Use Pinia only for client/app state that is not owned by the server (for example `src/pinia/*`).
56
+ - Do not duplicate the same state across the query cache and Pinia unless the project explicitly documents that pattern.
57
+ - When a project is migrating Vuex → Query/Pinia, follow that repo’s local migration rules (if present) instead of expanding Vuex.
50
58
 
51
59
  ## TanStack Query server-state conventions
52
60
 
@@ -85,6 +93,7 @@ alwaysApply: true
85
93
  ## FDS-first component usage
86
94
 
87
95
  - Use `fds-vue-core` components as the default choice for UI building blocks.
96
+ - Multi-step wizards use `FdsWizard` from `fds-vue-core`. Follow the project’s wizard rule file when one exists (for example `.cursor/rules/fds-wizard.md`).
88
97
  - Before adding custom UI, check whether an FDS component already solves the use case.
89
98
  - Prefer extending composition around FDS components over re-implementing design-system behavior.
90
99
  - Keep visual consistency by matching FDS variants, spacing patterns, and interaction styles.
@@ -113,6 +122,7 @@ alwaysApply: true
113
122
  - Keep spacing and typography explicit in class names; avoid hidden styling in deep selectors.
114
123
  - Use `!` utility overrides sparingly and only when a component default cannot be changed safely.
115
124
  - For Tailwind v4 important modifiers, use postfix-important utility syntax (for example `grid-cols-1!`) instead of prefix syntax (for example `!grid-cols-1`).
125
+ - Stack repeated items with parent `flex flex-col gap-*` (or `space-y-*`). Do not use `mb-* last:mb-0` or `last-of-type:mb-*` — those variants often miss the last `v-for` item (extra Vue/FDS nodes) and trip Tailwind conflict lint.
116
126
 
117
127
  ## Figma MCP implementation workflow
118
128
 
@@ -166,9 +176,11 @@ alwaysApply: true
166
176
 
167
177
  ## Testing expectations
168
178
 
169
- - For non-trivial logic changes, add or update automated tests close to the affected feature.
170
- - Validate critical flows for changed views: loading, empty, error, and success states.
179
+ - Do not add new automated tests unless the user explicitly asks for them.
180
+ - Update existing tests when your change requires it (for example broken assertions, renamed APIs, or changed behavior covered by current tests).
181
+ - When new tests are requested, add them close to the affected feature.
171
182
  - Keep tests behavior-focused and avoid brittle implementation-detail assertions.
183
+ - When no new tests are requested, rely on lint, typecheck, and manual verification of critical flows where practical.
172
184
 
173
185
  ## CI/CD
174
186
 
@@ -178,7 +190,8 @@ alwaysApply: true
178
190
 
179
191
  ## Quality checks before finishing
180
192
 
181
- - Run relevant checks for touched code (lint, typecheck, or tests when practical).
193
+ - Run relevant checks for touched code (lint and typecheck when practical).
194
+ - Run and update existing tests when your change affects them; add new tests only when the user explicitly requests them.
182
195
  - Fix issues introduced by your change before handing off.
183
196
  - Call out anything you could not verify.
184
197
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.14.4",
3
+ "version": "1.15.0",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -33,7 +33,7 @@
33
33
  "@tanstack/vue-query-devtools": "6.1.38",
34
34
  "@types/node": "22.16.5",
35
35
  "@vitejs/plugin-vue": "6.0.8",
36
- "fg-devkit": "1.14.4",
36
+ "fg-devkit": "1.15.0",
37
37
  "tailwindcss": "4.3.3",
38
38
  "tsx": "4.23.12",
39
39
  "vite": "7.3.5"
@@ -559,7 +559,7 @@ Commands:
559
559
  fallow [fallow-args...] Run Fallow using fg-devkit bundled package
560
560
  dead-code [fallow-args...] Run Fallow dead code analysis (shortcut for: fallow dead-code)
561
561
  poeditor-download [args...] Download locale files via PoEditor (reads poeditor/.poeditor.json by default)
562
- poeditor-upload [args...] Upload locale file via PoEditor (reads poeditor/.poeditor.json by default)
562
+ poeditor-upload [args...] Upload locale file via PoEditor (reads poeditor/.poeditor.json by default; --sync-terms deletes missing terms)
563
563
  scaffold-vue <target-dir> [--name …] [--slug …] [--force] …
564
564
  Copy the bundled Vue scaffold into target-dir (see script header)
565
565
  sync-scaffold-vue-deps [policy-file]
@@ -589,11 +589,14 @@ function isFgDevkitMaintainerContext() {
589
589
 
590
590
  async function runInteractiveMenu() {
591
591
  let selectPrompt
592
+ let confirmPrompt
592
593
  try {
593
594
  const inquirerSelect = await import('@inquirer/select')
595
+ const inquirerConfirm = await import('@inquirer/confirm')
594
596
  selectPrompt = inquirerSelect.default
597
+ confirmPrompt = inquirerConfirm.default
595
598
  } catch {
596
- console.error('[fg-devkit] Interactive menu requires @inquirer/select but it is not available.')
599
+ console.error('[fg-devkit] Interactive menu requires @inquirer/select and @inquirer/confirm but they are not available.')
597
600
  console.error('[fg-devkit] Run pnpm install in your project and try again.')
598
601
  return 1
599
602
  }
@@ -769,7 +772,34 @@ async function runInteractiveMenu() {
769
772
  loop: false,
770
773
  })
771
774
  if (choice === 'cancel') continue
772
- return runSelectedAction(() => runNode(choice === 'download' ? 'poeditor-download.mjs' : 'poeditor-upload.mjs'))
775
+ if (choice === 'download') {
776
+ return runSelectedAction(() => runNode('poeditor-download.mjs'))
777
+ }
778
+
779
+ const uploadMode = await selectPrompt({
780
+ message: 'Upload mode',
781
+ choices: [
782
+ { name: 'Upload (keep terms missing from file)', value: 'upload' },
783
+ {
784
+ name: 'Upload + sync terms (delete terms missing from file)',
785
+ value: 'upload-sync',
786
+ },
787
+ { name: 'Cancel', value: 'cancel' },
788
+ ],
789
+ default: 'upload',
790
+ loop: false,
791
+ })
792
+ if (uploadMode === 'cancel') continue
793
+ if (uploadMode === 'upload-sync') {
794
+ const confirmed = await confirmPrompt({
795
+ message:
796
+ 'This is a destructive POEditor action: terms not present in the uploaded file will be permanently deleted from the project (including all language translations). Continue?',
797
+ default: false,
798
+ })
799
+ if (!confirmed) continue
800
+ }
801
+ const uploadArgs = uploadMode === 'upload-sync' ? ['--sync-terms'] : []
802
+ return runSelectedAction(() => runNode('poeditor-upload.mjs', uploadArgs))
773
803
  }
774
804
 
775
805
  if (group === 'dependency-policy') {
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * Usage:
6
6
  * pnpm exec fg-devkit poeditor-upload [--config ./poeditor/.poeditor.json] [--project-id 123]
7
- * [--language sv] [--file ./src/lang/sv.json] [--api-token xxx]
7
+ * [--language sv] [--file ./src/lang/sv.json] [--api-token xxx] [--sync-terms]
8
+ *
9
+ * --sync-terms: Pass sync_terms=1 to PoEditor so terms missing from the uploaded file
10
+ * are deleted from the project (destructive; use with care).
8
11
  */
9
12
 
10
13
  import fs from 'node:fs'
@@ -22,6 +25,12 @@ function sleep(ms) {
22
25
  })
23
26
  }
24
27
 
28
+ function isTruthyFlag(value) {
29
+ if (value === undefined) return false
30
+ const normalized = String(value).trim().toLowerCase()
31
+ return normalized === 'true' || normalized === '1' || normalized === 'yes'
32
+ }
33
+
25
34
  function parsePoeditorError(err) {
26
35
  const message = err instanceof Error ? err.message : String(err)
27
36
  const match = message.match(/Upload failed:\s*(\{.*\})$/)
@@ -46,7 +55,7 @@ function getUploadType(filePath) {
46
55
  return extension
47
56
  }
48
57
 
49
- async function uploadFile({ token, projectId, language, filePath }) {
58
+ async function uploadFile({ token, projectId, language, filePath, syncTerms = false }) {
50
59
  const fileBuffer = fs.readFileSync(filePath)
51
60
  const fileName = path.basename(filePath)
52
61
  const uploadType = getUploadType(filePath)
@@ -56,6 +65,7 @@ async function uploadFile({ token, projectId, language, filePath }) {
56
65
  form.append('updating', 'terms_translations')
57
66
  form.append('language', language)
58
67
  form.append('overwrite', '1')
68
+ form.append('sync_terms', syncTerms ? '1' : '0')
59
69
  form.append('type', uploadType)
60
70
  form.append('file', new Blob([fileBuffer]), fileName)
61
71
 
@@ -70,18 +80,20 @@ async function uploadFile({ token, projectId, language, filePath }) {
70
80
  if (data?.response?.status !== 'success') {
71
81
  throw new Error(`Upload failed: ${JSON.stringify(data)}`)
72
82
  }
83
+ const terms = data?.result?.terms
73
84
  const translations = data?.result?.translations
85
+ const syncNote = syncTerms ? `, terms deleted: ${terms?.deleted ?? 0}` : ''
74
86
  console.log(
75
- `[poeditor-upload] Upload complete for "${language}" (parsed: ${translations?.parsed ?? 0}, updated: ${translations?.updated ?? 0}, added: ${translations?.added ?? 0})`,
87
+ `[poeditor-upload] Upload complete for "${language}" (parsed: ${translations?.parsed ?? 0}, updated: ${translations?.updated ?? 0}, added: ${translations?.added ?? 0}${syncNote})`,
76
88
  )
77
89
  }
78
90
 
79
- async function uploadFileWithRetry({ token, projectId, language, filePath }) {
91
+ async function uploadFileWithRetry({ token, projectId, language, filePath, syncTerms = false }) {
80
92
  let attempt = 0
81
93
  while (true) {
82
94
  attempt += 1
83
95
  try {
84
- await uploadFile({ token, projectId, language, filePath })
96
+ await uploadFile({ token, projectId, language, filePath, syncTerms })
85
97
  return
86
98
  } catch (err) {
87
99
  const errorCode = parsePoeditorError(err)
@@ -111,20 +123,32 @@ async function main() {
111
123
  }
112
124
 
113
125
  const projectId = getFlagValue(flags, 'project-id', cfg.projectId)
126
+ const syncTerms = isTruthyFlag(getFlagValue(flags, 'sync-terms'))
127
+ if (syncTerms) {
128
+ console.warn(
129
+ '[poeditor-upload] --sync-terms enabled: terms not present in the uploaded file will be deleted from PoEditor.',
130
+ )
131
+ }
132
+
114
133
  const hasExplicitLanguage = flags.has('language')
115
134
  const hasExplicitFile = flags.has('file')
116
135
  if (hasExplicitLanguage || hasExplicitFile) {
117
136
  const language = getFlagValue(flags, 'language', cfg.defaultLanguage)
118
137
  const filePath = resolvePathFromCwd(getFlagValue(flags, 'file', cfg.uploadFile))
119
- await uploadFile({ token, projectId, language, filePath })
138
+ await uploadFile({ token, projectId, language, filePath, syncTerms })
120
139
  console.log(`[poeditor-upload] Upload completed for ${language}: ${filePath}`)
121
140
  return
122
141
  }
123
142
 
124
143
  const uploads = cfg.uploadFiles && typeof cfg.uploadFiles === 'object' ? Object.entries(cfg.uploadFiles) : []
125
144
  if (uploads.length > 0) {
145
+ if (syncTerms && uploads.length > 1) {
146
+ console.warn(
147
+ '[poeditor-upload] --sync-terms with multiple uploadFiles: syncing only on the first file to avoid deleting terms present in other language files.',
148
+ )
149
+ }
126
150
  let nextUploadAt = 0
127
- for (const [language, file] of uploads) {
151
+ for (const [index, [language, file]] of uploads.entries()) {
128
152
  const now = Date.now()
129
153
  const waitBeforeUploadMs = Math.max(0, nextUploadAt - now)
130
154
  if (waitBeforeUploadMs > 0) {
@@ -133,7 +157,8 @@ async function main() {
133
157
  }
134
158
 
135
159
  const filePath = resolvePathFromCwd(file)
136
- await uploadFileWithRetry({ token, projectId, language, filePath })
160
+ const shouldSyncTerms = syncTerms && index === 0
161
+ await uploadFileWithRetry({ token, projectId, language, filePath, syncTerms: shouldSyncTerms })
137
162
  console.log(`[poeditor-upload] Upload completed for ${language}: ${filePath}`)
138
163
  nextUploadAt = Date.now() + MIN_UPLOAD_INTERVAL_MS
139
164
  }
@@ -142,7 +167,7 @@ async function main() {
142
167
 
143
168
  const language = cfg.defaultLanguage
144
169
  const filePath = resolvePathFromCwd(cfg.uploadFile)
145
- await uploadFileWithRetry({ token, projectId, language, filePath })
170
+ await uploadFileWithRetry({ token, projectId, language, filePath, syncTerms })
146
171
  console.log(`[poeditor-upload] Upload completed for ${language}: ${filePath}`)
147
172
  }
148
173