@sun-asterisk/sungen 3.2.3-beta.3 → 3.2.4

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 (160) hide show
  1. package/dist/cli/commands/feedback.d.ts +11 -0
  2. package/dist/cli/commands/feedback.d.ts.map +1 -1
  3. package/dist/cli/commands/feedback.js +154 -57
  4. package/dist/cli/commands/feedback.js.map +1 -1
  5. package/dist/cli/commands/login.d.ts +19 -0
  6. package/dist/cli/commands/login.d.ts.map +1 -0
  7. package/dist/cli/commands/login.js +76 -0
  8. package/dist/cli/commands/login.js.map +1 -0
  9. package/dist/cli/commands/qa-feedback.d.ts +14 -0
  10. package/dist/cli/commands/qa-feedback.d.ts.map +1 -0
  11. package/dist/cli/commands/qa-feedback.js +83 -0
  12. package/dist/cli/commands/qa-feedback.js.map +1 -0
  13. package/dist/cli/index.js +4 -0
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  16. package/dist/dashboard/snapshot-builder.js +108 -33
  17. package/dist/dashboard/snapshot-builder.js.map +1 -1
  18. package/dist/dashboard/templates/index.html +23 -23
  19. package/dist/dashboard/types.d.ts +4 -0
  20. package/dist/dashboard/types.d.ts.map +1 -1
  21. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  22. package/dist/exporters/csv-exporter.js +11 -4
  23. package/dist/exporters/csv-exporter.js.map +1 -1
  24. package/dist/exporters/feature-parser.d.ts +4 -0
  25. package/dist/exporters/feature-parser.d.ts.map +1 -1
  26. package/dist/exporters/feature-parser.js +7 -2
  27. package/dist/exporters/feature-parser.js.map +1 -1
  28. package/dist/exporters/json-exporter.d.ts.map +1 -1
  29. package/dist/exporters/json-exporter.js +11 -3
  30. package/dist/exporters/json-exporter.js.map +1 -1
  31. package/dist/exporters/playwright-report-parser.d.ts +21 -2
  32. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  33. package/dist/exporters/playwright-report-parser.js +52 -12
  34. package/dist/exporters/playwright-report-parser.js.map +1 -1
  35. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  36. package/dist/exporters/scenario-merger.js +50 -2
  37. package/dist/exporters/scenario-merger.js.map +1 -1
  38. package/dist/exporters/spec-parser.js +1 -1
  39. package/dist/exporters/spec-parser.js.map +1 -1
  40. package/dist/exporters/test-data-resolver.d.ts +7 -0
  41. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  42. package/dist/exporters/test-data-resolver.js +13 -0
  43. package/dist/exporters/test-data-resolver.js.map +1 -1
  44. package/dist/exporters/types.d.ts +6 -0
  45. package/dist/exporters/types.d.ts.map +1 -1
  46. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  47. package/dist/exporters/xlsx-exporter.js +131 -42
  48. package/dist/exporters/xlsx-exporter.js.map +1 -1
  49. package/dist/generators/gherkin-parser/index.d.ts +15 -0
  50. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  51. package/dist/generators/gherkin-parser/index.js +33 -1
  52. package/dist/generators/gherkin-parser/index.js.map +1 -1
  53. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
  54. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
  55. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
  56. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
  57. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
  58. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
  59. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
  60. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
  61. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
  62. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
  63. package/dist/harness/script-check.d.ts.map +1 -1
  64. package/dist/harness/script-check.js +10 -5
  65. package/dist/harness/script-check.js.map +1 -1
  66. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  67. package/dist/orchestrator/ai-skills/writer.js +46 -4
  68. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  69. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
  70. package/dist/orchestrator/templates/ai-src/commands/create-test.md +7 -0
  71. package/dist/orchestrator/templates/ai-src/commands/feedback.md +6 -6
  72. package/dist/orchestrator/templates/ai-src/commands/locale.md +26 -4
  73. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
  74. package/dist/orchestrator/templates/wdio.conf.ts +39 -3
  75. package/dist/tools/sungen-api/edit-in-editor.d.ts +15 -0
  76. package/dist/tools/sungen-api/edit-in-editor.d.ts.map +1 -0
  77. package/dist/tools/sungen-api/edit-in-editor.js +85 -0
  78. package/dist/tools/sungen-api/edit-in-editor.js.map +1 -0
  79. package/dist/tools/sungen-api/feedback-client.d.ts +24 -0
  80. package/dist/tools/sungen-api/feedback-client.d.ts.map +1 -0
  81. package/dist/tools/sungen-api/feedback-client.js +53 -0
  82. package/dist/tools/sungen-api/feedback-client.js.map +1 -0
  83. package/dist/tools/sungen-api/read-stdin.d.ts +6 -0
  84. package/dist/tools/sungen-api/read-stdin.d.ts.map +1 -0
  85. package/dist/tools/sungen-api/read-stdin.js +19 -0
  86. package/dist/tools/sungen-api/read-stdin.js.map +1 -0
  87. package/dist/tools/sungen-auth/api-base-url.d.ts +14 -0
  88. package/dist/tools/sungen-auth/api-base-url.d.ts.map +1 -0
  89. package/dist/tools/sungen-auth/api-base-url.js +28 -0
  90. package/dist/tools/sungen-auth/api-base-url.js.map +1 -0
  91. package/dist/tools/sungen-auth/credentials-store.d.ts +19 -0
  92. package/dist/tools/sungen-auth/credentials-store.d.ts.map +1 -0
  93. package/dist/tools/sungen-auth/credentials-store.js +94 -0
  94. package/dist/tools/sungen-auth/credentials-store.js.map +1 -0
  95. package/dist/tools/sungen-auth/login-flow.d.ts +33 -0
  96. package/dist/tools/sungen-auth/login-flow.d.ts.map +1 -0
  97. package/dist/tools/sungen-auth/login-flow.js +107 -0
  98. package/dist/tools/sungen-auth/login-flow.js.map +1 -0
  99. package/dist/tools/sungen-auth/loopback-server.d.ts +20 -0
  100. package/dist/tools/sungen-auth/loopback-server.d.ts.map +1 -0
  101. package/dist/tools/sungen-auth/loopback-server.js +132 -0
  102. package/dist/tools/sungen-auth/loopback-server.js.map +1 -0
  103. package/dist/tools/sungen-auth/open-browser.d.ts +10 -0
  104. package/dist/tools/sungen-auth/open-browser.d.ts.map +1 -0
  105. package/dist/tools/sungen-auth/open-browser.js +43 -0
  106. package/dist/tools/sungen-auth/open-browser.js.map +1 -0
  107. package/dist/tools/sungen-auth/pkce.d.ts +13 -0
  108. package/dist/tools/sungen-auth/pkce.d.ts.map +1 -0
  109. package/dist/tools/sungen-auth/pkce.js +27 -0
  110. package/dist/tools/sungen-auth/pkce.js.map +1 -0
  111. package/dist/tools/sungen-auth/sungen-auth-types.d.ts +41 -0
  112. package/dist/tools/sungen-auth/sungen-auth-types.d.ts.map +1 -0
  113. package/dist/tools/sungen-auth/sungen-auth-types.js +7 -0
  114. package/dist/tools/sungen-auth/sungen-auth-types.js.map +1 -0
  115. package/package.json +3 -3
  116. package/src/cli/commands/feedback.ts +123 -51
  117. package/src/cli/commands/login.ts +75 -0
  118. package/src/cli/commands/qa-feedback.ts +77 -0
  119. package/src/cli/index.ts +4 -0
  120. package/src/dashboard/snapshot-builder.ts +100 -27
  121. package/src/dashboard/templates/index.html +23 -23
  122. package/src/dashboard/types.ts +5 -1
  123. package/src/exporters/csv-exporter.ts +10 -4
  124. package/src/exporters/feature-parser.ts +7 -2
  125. package/src/exporters/json-exporter.ts +11 -3
  126. package/src/exporters/playwright-report-parser.ts +50 -11
  127. package/src/exporters/scenario-merger.ts +49 -3
  128. package/src/exporters/spec-parser.ts +1 -1
  129. package/src/exporters/test-data-resolver.ts +13 -0
  130. package/src/exporters/types.ts +6 -0
  131. package/src/exporters/xlsx-exporter.ts +155 -49
  132. package/src/generators/gherkin-parser/index.ts +44 -1
  133. package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
  134. package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
  135. package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
  136. package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
  137. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
  138. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
  139. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
  140. package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
  141. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
  142. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
  143. package/src/harness/script-check.ts +10 -5
  144. package/src/orchestrator/ai-skills/writer.ts +39 -4
  145. package/src/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
  146. package/src/orchestrator/templates/ai-src/commands/create-test.md +7 -0
  147. package/src/orchestrator/templates/ai-src/commands/feedback.md +6 -6
  148. package/src/orchestrator/templates/ai-src/commands/locale.md +26 -4
  149. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
  150. package/src/orchestrator/templates/wdio.conf.ts +39 -3
  151. package/src/tools/sungen-api/edit-in-editor.ts +48 -0
  152. package/src/tools/sungen-api/feedback-client.ts +62 -0
  153. package/src/tools/sungen-api/read-stdin.ts +15 -0
  154. package/src/tools/sungen-auth/api-base-url.ts +23 -0
  155. package/src/tools/sungen-auth/credentials-store.ts +57 -0
  156. package/src/tools/sungen-auth/login-flow.ts +135 -0
  157. package/src/tools/sungen-auth/loopback-server.ts +113 -0
  158. package/src/tools/sungen-auth/open-browser.ts +42 -0
  159. package/src/tools/sungen-auth/pkce.ts +27 -0
  160. package/src/tools/sungen-auth/sungen-auth-types.ts +45 -0
@@ -10,10 +10,10 @@ codex-trigger: "Run when the user wants to send FEEDBACK about sungen itself."
10
10
  ## Role
11
11
  {{#cap parallel-subagents}}
12
12
 
13
- You capture QA feedback and store it **locally** (no server) via `sungen feedback record`. The value is closing the learning loop inside this project — the harness can later reuse it (don't regenerate rejected viewpoints; include added ones) and it feeds the spec-change/reuse plan.
13
+ You capture QA feedback and store it **locally** (no server) via `sungen qa-feedback record`. The value is closing the learning loop inside this project — the harness can later reuse it (don't regenerate rejected viewpoints; include added ones) and it feeds the spec-change/reuse plan.
14
14
  {{/cap}}
15
15
  {{^cap parallel-subagents}}
16
- Capture QA feedback and store it **locally** (no server) via `sungen feedback record`. Closes the learning loop inside the project; the harness can reuse it later.
16
+ Capture QA feedback and store it **locally** (no server) via `sungen qa-feedback record`. Closes the learning loop inside the project; the harness can reuse it later.
17
17
  {{/cap}}
18
18
 
19
19
  ## Steps
@@ -39,12 +39,12 @@ Capture QA feedback and store it **locally** (no server) via `sungen feedback re
39
39
  {{/cap}}
40
40
  ```bash
41
41
  {{#cap parallel-subagents}}
42
- sungen feedback record --type test-design --screen <name> \
42
+ sungen qa-feedback record --type test-design --screen <name> \
43
43
  --target "VP-DATA-CONSISTENCY" --decision add \
44
44
  --message "<message>" --reason "<why>"
45
45
  {{/cap}}
46
46
  {{^cap parallel-subagents}}
47
- sungen feedback record --type <type> --screen <name> --target "<ref>" --decision <d> --message "<msg>" --reason "<why>"
47
+ sungen qa-feedback record --type <type> --screen <name> --target "<ref>" --decision <d> --message "<msg>" --reason "<why>"
48
48
  {{/cap}}
49
49
  ```
50
50
  {{#cap parallel-subagents}}
@@ -60,8 +60,8 @@ Capture QA feedback and store it **locally** (no server) via `sungen feedback re
60
60
  {{#cap parallel-subagents}}
61
61
  - **Never send anywhere** — this only writes a local file.
62
62
  - Keep `product` feedback separate from `test-design` so it can route to telemetry vs the viewpoint knowledge later.
63
- - View history: `sungen feedback list [--screen <name>] [--type <t>]`.
63
+ - View history: `sungen qa-feedback list [--screen <name>] [--type <t>]`.
64
64
  {{/cap}}
65
65
  {{^cap parallel-subagents}}
66
- - Never send anywhere — local file only. Keep `product` vs `test-design` distinct. View: `sungen feedback list`.
66
+ - Never send anywhere — local file only. Keep `product` vs `test-design` distinct. View: `sungen qa-feedback list`.
67
67
  {{/cap}}
@@ -1,8 +1,8 @@
1
1
  ---
2
- description: "Bootstrap i18n for a screen/flow — audit selectors, detect locale switch mechanism via Playwright, generate test-data overlay so `sungen:run-test --env <locale>` works."
2
+ description: "Bootstrap i18n for a screen/flow — audit selectors, detect locale switch mechanism via Playwright (web) or device locale (mobile), generate test-data overlay so `sungen:run-test --env <locale>` works."
3
3
  argument-hint: "<name> <locale> [--base-locale <code>] [--offline]"
4
4
  order: 45
5
- claude-tools: "Read, Grep, Bash, Glob, Edit, Write, AskUserQuestion, mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_wait_for, mcp__playwright__browser_evaluate, mcp__playwright__browser_click, mcp__playwright__browser_storage_state, mcp__playwright__browser_set_storage_state"
5
+ claude-tools: "Read, Grep, Bash, Glob, Edit, Write, AskUserQuestion, mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_wait_for, mcp__playwright__browser_evaluate, mcp__playwright__browser_click, mcp__playwright__browser_storage_state, mcp__playwright__browser_set_storage_state, mcp__appium-mcp__select_device, mcp__appium-mcp__appium_session_management, mcp__appium-mcp__appium_get_page_source, mcp__appium-mcp__appium_screenshot, mcp__appium-mcp__generate_locators"
6
6
  copilot-tools: "[read, execute, edit, vscode/askQuestions, playwright/*]"
7
7
  codex-trigger: "Run when the user asks to bootstrap i18n/localization for a screen. Runs before run-test --env <locale>."
8
8
  ---
@@ -33,6 +33,27 @@ Reject if name == locale (common typo).
33
33
  Reject if name == locale.
34
34
  {{/cap}}
35
35
 
36
+ ## Platform detection (do this FIRST)
37
+
38
+ Read `qa/capabilities.yaml` and check the `platform` field.
39
+
40
+ - If `platform: mobile` → switch to the **mobile path** (below).
41
+ - If `platform: web` or field absent → follow the **web path** (existing behavior below, unchanged).
42
+
43
+ ### Mobile path
44
+
45
+ When `platform: mobile`, mobile follows the **device locale** — there is no URL prefix, query param, or browser-storage mechanism to detect. Key differences from web (full detail in the `sungen-locale` skill):
46
+
47
+ - **Live capture**: use **Appium MCP** (`select_device` → `appium_session_management(create)` with the device at base locale → `appium_get_page_source` / `appium_screenshot`) — not Playwright browser tools.
48
+ - **Locale-switch mechanism**: `appium:language` / `appium:locale` capabilities + `noReset:false`, resolved automatically at runtime by `wdio.conf.ts`'s `resolveLocaleCaps()`. No fixture files needed.
49
+ - **Phases skipped**: 3.5/3.6 (browser storage diff/verify) and 6d/6f (Pages URL update, locale-config.json) are web-only — skip them. Phase 6 (mobile) writes only selectors `{{var}}`, the base `test-data/<feature>.yaml`, and the overlay `test-data/<feature>.<locale>.yaml`.
50
+ - **Compile**: `sungen generate --screen <name>` (the Appium adapter auto-selects for a mobile project).
51
+ - **Hand-off**: after a locale run, print `SUNGEN_ENV=<locale> npm run test:mobile` (or the `--flow` equivalent). If the device's locale code differs from the overlay's env name, set `SUNGEN_LOCALE=<code>` — `resolveLocaleCaps()` reads it as an override.
52
+ - **Reset after a locale run**: run once with `SUNGEN_LOCALE=en npm run test:mobile` to bring the device/app back to the base language before the next non-locale run.
53
+ - **iOS data-wipe**: `wdio.conf.ts`'s `beforeSession` hook already wipes the app's data container on iOS before any locale run (XCUITest's `noReset:false` alone doesn't clear a persisted in-app language). If an older mobile project's `wdio.conf.ts` predates this hook and iOS keeps the previous language, re-run `sungen init --mobile` to refresh it.
54
+
55
+ Stop here for mobile — do not continue to the web path below. Everything else (the 7-phase strategy, selector/test-data output shape, `{{var}}` overlay pattern) matches web; see the `sungen-locale` skill for the full mobile phase table.
56
+
36
57
  ## Auto-detect context
37
58
 
38
59
  Same as `/sungen:run-test`:
@@ -114,10 +135,11 @@ Offer the user:
114
135
  {{#cap parallel-subagents}}
115
136
  - Do NOT run tests yourself. `/sungen:run-test` is the executor. This command only PREPS files.
116
137
  - Do NOT modify `.feature` files. Localization happens entirely through `selectors/*.yaml` + `test-data/*.yaml`.
117
- - Auth blocker on SAA staging? Use `--offline`. See [[saa-auth-blocker]] memory.
138
+ - Auth blocker on SAA staging (web)? Use `--offline`. See [[saa-auth-blocker]] memory.
139
+ - Mobile device/emulator unreachable? Use `--offline` — same fallback as web.
118
140
  {{/cap}}
119
141
  {{^cap parallel-subagents}}
120
142
  - Do NOT run tests yourself. `/sungen:run-test` is the executor; this command only PREPS files.
121
143
  - Do NOT modify `.feature` files. Localization lives in `selectors/*.yaml` + `test-data/*.yaml` only.
122
- - Auth blocked? Use `--offline`.
144
+ - Auth blocked (web)? Use `--offline`. Mobile device unreachable? Same fallback.
123
145
  {{/cap}}
@@ -78,19 +78,20 @@ The CLI reads the **per-target result file first** (co-located with `.spec.ts`),
78
78
 
79
79
  ---
80
80
 
81
- ## XLSX sheets — Auto / Manual split
81
+ ## XLSX sheets — Overview + Auto / Manual split
82
82
 
83
- The `.xlsx` is split into two sheets so QA manages the sets separately:
83
+ The `.xlsx` leads with an **`Overview`** roll-up sheet, then splits the test cases into detail sheets so QA manages the sets separately:
84
+ - **`Overview`** — first sheet. Same masthead (Sun\* logo, `<SCREEN> TESTCASE` title, No/Version/Issue-Date, ISO band) as the others, then a summary table: one row per following sheet with `# · Sheet name · Passed · Failed · Pending · N/A · Remain · Total`, plus a grand-total row. `Remain = Total − Passed − N/A` (i.e. Failed + Pending), reconciling with each detail sheet's own summary band.
84
85
  - **`Auto`** — automatable test cases (`Auto` + `Not compiled`).
85
86
  - **`Manual`** — `@manual` test cases (always present, header-only when there are none).
86
87
 
87
- Multi-locale (no `SUNGEN_ENV`): one **`<LOCALE> Auto`** sheet per locale + a single shared **`Manual`** sheet (manual TCs are locale-invariant). The **CSV stays one file with every row** — the `Testcase type` column distinguishes Auto vs Manual.
88
+ Multi-locale (no `SUNGEN_ENV`): `Overview` + one **`<LOCALE> Auto`** sheet per locale + a single shared **`Manual`** sheet (manual TCs are locale-invariant). The **CSV stays one file with every row** — the `Testcase type` column distinguishes Auto vs Manual. The Overview lists every detail sheet, so each locale's counts appear on its own row.
88
89
 
89
90
  ---
90
91
 
91
92
  ## API delivery — extra worksheet
92
93
 
93
- For **api-kind units** (`qa/api/<area>/`), the `.xlsx` gains a third worksheet **`API detail`** (appended after Auto/Manual). The main BM-2-901-13 Testcases layout is unchanged. The CSV is unchanged (16-column, no extra sheet).
94
+ For **api-kind units** (`qa/api/<area>/`), the `.xlsx` gains a final worksheet **`API detail`** (appended after Overview/Auto/Manual). The main BM-2-901-13 Testcases layout is unchanged. The CSV is unchanged (16-column, no extra sheet).
94
95
 
95
96
  ### Required sources (API detail sheet only)
96
97
 
@@ -25,6 +25,7 @@ import dotenv from 'dotenv';
25
25
  dotenv.config({ path: '.env.appium' });
26
26
  import * as fs from 'node:fs';
27
27
  import * as path from 'node:path';
28
+ import { execSync } from 'node:child_process';
28
29
  import PwShapeReporter from './specs/reporters/pw-shape-reporter';
29
30
 
30
31
  /**
@@ -104,13 +105,34 @@ const RUN_ANDROID = MOBILE_PLATFORM === 'android' || MOBILE_PLATFORM === 'both';
104
105
  const RUN_IOS = MOBILE_PLATFORM === 'ios' || MOBILE_PLATFORM === 'both';
105
106
  const IOS_APP = process.env.IOS_APP;
106
107
  const IOS_BUNDLE_ID = process.env.IOS_BUNDLE_ID || '__IOS_BUNDLE_ID__';
107
- const IOS_DEVICE = process.env.IOS_DEVICE || '__IOS_DEVICE__';
108
- const IOS_VERSION = process.env.IOS_VERSION || '__IOS_VERSION__';
108
+ const IOS_UDID = process.env.IOS_UDID;
109
+ // When a specific Simulator is targeted by IOS_UDID, XCUITest uses THAT device and IGNORES the
110
+ // deviceName/platformVersion caps — so a stale IOS_DEVICE/IOS_VERSION would MISLABEL the deliverable's
111
+ // "Test Environment". Resolve the real device name + iOS runtime from the UDID via simctl so the caps
112
+ // (and the reporter's label) reflect reality. Explicit IOS_DEVICE/IOS_VERSION still win; on a real
113
+ // device / non-mac, simctl fails → fall back to the provided/scaffold values.
114
+ function resolveSimDevice(udid: string): { name?: string; version?: string } {
115
+ try {
116
+ const data = JSON.parse(execSync('xcrun simctl list devices --json', { encoding: 'utf-8' })) as {
117
+ devices?: Record<string, Array<{ udid: string; name: string }>>;
118
+ };
119
+ for (const [runtime, devs] of Object.entries(data.devices || {})) {
120
+ const hit = devs.find((d) => d.udid === udid);
121
+ if (hit) {
122
+ const vm = /iOS-(\d+)-(\d+)/.exec(runtime); // com.apple.CoreSimulator.SimRuntime.iOS-26-3
123
+ return { name: hit.name, version: vm ? `${vm[1]}.${vm[2]}` : undefined };
124
+ }
125
+ }
126
+ } catch { /* simctl unavailable (real device / non-mac) → keep provided values */ }
127
+ return {};
128
+ }
129
+ const IOS_SIM = IOS_UDID ? resolveSimDevice(IOS_UDID) : {};
130
+ const IOS_DEVICE = process.env.IOS_DEVICE || IOS_SIM.name || '__IOS_DEVICE__';
131
+ const IOS_VERSION = process.env.IOS_VERSION || IOS_SIM.version || '__IOS_VERSION__';
109
132
  // platformVersion is OPTIONAL on a real device: with IOS_UDID set, XCUITest reads the iOS version off
110
133
  // the connected device. Only pass the cap when IOS_VERSION is a real value (not the unfilled scaffold
111
134
  // placeholder). On a Simulator set it (or IOS_DEVICE) so Appium picks a matching runtime.
112
135
  const HAS_REAL_IOS_VERSION = !!IOS_VERSION && IOS_VERSION !== '__IOS_VERSION__';
113
- const IOS_UDID = process.env.IOS_UDID;
114
136
  // Real iPhone (device) — WebDriverAgent must be code-signed onto it. Set IOS_TEAM_ID (your Apple
115
137
  // Team ID) to switch the iOS cap into real-device mode; the Simulator ignores these (they're only
116
138
  // emitted when IOS_TEAM_ID is present, so Simulator runs are unchanged). For a real device also set
@@ -208,6 +230,13 @@ export const config: WebdriverIO.Config = {
208
230
  {
209
231
  platformName: 'iOS',
210
232
  'appium:automationName': 'XCUITest',
233
+ // Flutter apps NEVER report "idle" to XCUITest, so WDA always waits the FULL
234
+ // waitForIdleTimeout before EVERY interaction. The default (~10s) makes gesture
235
+ // loops blow the mocha timeout; 0 removes the wait entirely but then taps fire
236
+ // mid-transition/mid-launch and randomly miss (measured: ~2 flaky scenarios per
237
+ // run). 2s is the working balance — a universal settle that covers Flutter
238
+ // transitions while keeping bounded scroll/error paths well inside the budget.
239
+ 'appium:waitForIdleTimeout': 2,
211
240
  'appium:deviceName': IOS_DEVICE,
212
241
  // Omit when unknown (real device, no IOS_VERSION) → XCUITest reads it from the device by udid.
213
242
  ...(HAS_REAL_IOS_VERSION ? { 'appium:platformVersion': IOS_VERSION } : {}),
@@ -253,6 +282,13 @@ export const config: WebdriverIO.Config = {
253
282
  mochaOpts: {
254
283
  ui: 'bdd',
255
284
  timeout: 120000,
285
+ // One retry per test: the relaunch-per-scenario pattern on a Flutter sim has a residual
286
+ // launch race (~10%/scenario — the first interaction can fire before the app routes
287
+ // touches), with a RANDOM victim each run. A retry re-runs the full beforeEach (fresh
288
+ // relaunch), so race victims recover on attempt 2 while real failures (wrong expected
289
+ // value, missing element) fail both attempts and still report Failed. The reporter and
290
+ // delivery already take the LAST attempt.
291
+ retries: 1,
256
292
  },
257
293
 
258
294
  // iOS + locale: XCUITest's `noReset:false` does NOT actually clear the app's data container
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Open the user's editor to compose feedback text, à la `git commit`.
3
+ *
4
+ * The feedback body is MARKDOWN, so the editor buffer is left completely
5
+ * untouched — no comment/instruction lines are injected or stripped (that would
6
+ * clobber markdown `#` headings). Guidance is printed to the terminal before the
7
+ * editor launches instead. Returns the trimmed body, or null when it is empty
8
+ * (caller treats that as "abort").
9
+ */
10
+ /**
11
+ * Open the editor and return the composed feedback (verbatim, trimmed), or null
12
+ * if empty/aborted. Throws if the editor cannot be launched or exits non-zero.
13
+ */
14
+ export declare function composeInEditor(): string | null;
15
+ //# sourceMappingURL=edit-in-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-in-editor.d.ts","sourceRoot":"","sources":["../../../src/tools/sungen-api/edit-in-editor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAmB/C"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * Open the user's editor to compose feedback text, à la `git commit`.
4
+ *
5
+ * The feedback body is MARKDOWN, so the editor buffer is left completely
6
+ * untouched — no comment/instruction lines are injected or stripped (that would
7
+ * clobber markdown `#` headings). Guidance is printed to the terminal before the
8
+ * editor launches instead. Returns the trimmed body, or null when it is empty
9
+ * (caller treats that as "abort").
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.composeInEditor = composeInEditor;
46
+ const child_process_1 = require("child_process");
47
+ const fs = __importStar(require("fs"));
48
+ const os = __importStar(require("os"));
49
+ const path = __importStar(require("path"));
50
+ /** Resolve the editor command, mirroring git's precedence. */
51
+ function resolveEditor() {
52
+ return (process.env.VISUAL ||
53
+ process.env.EDITOR ||
54
+ (process.platform === 'win32' ? 'notepad' : 'vi'));
55
+ }
56
+ /**
57
+ * Open the editor and return the composed feedback (verbatim, trimmed), or null
58
+ * if empty/aborted. Throws if the editor cannot be launched or exits non-zero.
59
+ */
60
+ function composeInEditor() {
61
+ const file = path.join(os.tmpdir(), `sungen-feedback-${process.pid}-${Date.now()}.md`);
62
+ fs.writeFileSync(file, '', 'utf8');
63
+ try {
64
+ const editor = resolveEditor();
65
+ console.error('Composing feedback in your editor (markdown supported). Save & close to submit; leave empty to abort.');
66
+ // shell:true so editors with args work (e.g. VISUAL="code --wait").
67
+ const result = (0, child_process_1.spawnSync)(`${editor} "${file}"`, { shell: true, stdio: 'inherit' });
68
+ if (result.error)
69
+ throw new Error(`Could not launch editor "${editor}": ${result.error.message}`);
70
+ if (typeof result.status === 'number' && result.status !== 0) {
71
+ throw new Error(`Editor "${editor}" exited with code ${result.status}.`);
72
+ }
73
+ // Verbatim — only trim outer whitespace so markdown (headings, lists, code
74
+ // fences, HTML comments) is preserved exactly as written.
75
+ const body = fs.readFileSync(file, 'utf8').trim();
76
+ return body.length > 0 ? body : null;
77
+ }
78
+ finally {
79
+ try {
80
+ fs.unlinkSync(file);
81
+ }
82
+ catch { /* best effort */ }
83
+ }
84
+ }
85
+ //# sourceMappingURL=edit-in-editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-in-editor.js","sourceRoot":"","sources":["../../../src/tools/sungen-api/edit-in-editor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBH,0CAmBC;AArCD,iDAA0C;AAC1C,uCAAyB;AACzB,uCAAyB;AACzB,2CAA6B;AAE7B,8DAA8D;AAC9D,SAAS,aAAa;IACpB,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,MAAM;QAClB,OAAO,CAAC,GAAG,CAAC,MAAM;QAClB,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAClD,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe;IAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,mBAAmB,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACvF,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,uGAAuG,CAAC,CAAC;QACvH,oEAAoE;QACpE,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,GAAG,MAAM,KAAK,IAAI,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QACnF,IAAI,MAAM,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,MAAM,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAClG,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7D,MAAM,IAAI,KAAK,CAAC,WAAW,MAAM,sBAAsB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QAC3E,CAAC;QACD,2EAA2E;QAC3E,0DAA0D;QAC1D,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACvC,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Thin client for submitting product feedback to the FastSchema backend.
3
+ * Raw fetch (the fastschema npm SDK is browser/cookie-oriented and has no
4
+ * bearer-token content client). author is NEVER sent — the server stamps it.
5
+ */
6
+ /** Thrown on 401 so the caller can prompt a re-login and retry once. */
7
+ export declare class AuthExpiredError extends Error {
8
+ constructor(message?: string);
9
+ }
10
+ export interface FeedbackPayload {
11
+ content: string;
12
+ category: string;
13
+ sungen_version?: string;
14
+ os?: string;
15
+ source: string;
16
+ }
17
+ /**
18
+ * POST one feedback record. Returns the created row id.
19
+ * @throws AuthExpiredError on 401; Error (scrubbed) on any other non-2xx.
20
+ */
21
+ export declare function submitFeedback(apiBaseUrl: string, token: string, payload: FeedbackPayload): Promise<{
22
+ id: number | string;
23
+ }>;
24
+ //# sourceMappingURL=feedback-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback-client.d.ts","sourceRoot":"","sources":["../../../src/tools/sungen-api/feedback-client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wEAAwE;AACxE,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,SAA4B;CAIhD;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;;GAGG;AACH,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC,CA2BlC"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * Thin client for submitting product feedback to the FastSchema backend.
4
+ * Raw fetch (the fastschema npm SDK is browser/cookie-oriented and has no
5
+ * bearer-token content client). author is NEVER sent — the server stamps it.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.AuthExpiredError = void 0;
9
+ exports.submitFeedback = submitFeedback;
10
+ /** Thrown on 401 so the caller can prompt a re-login and retry once. */
11
+ class AuthExpiredError extends Error {
12
+ constructor(message = 'Authentication expired.') {
13
+ super(message);
14
+ this.name = 'AuthExpiredError';
15
+ }
16
+ }
17
+ exports.AuthExpiredError = AuthExpiredError;
18
+ function normalizeBase(url) {
19
+ return url.replace(/\/+$/, '');
20
+ }
21
+ /**
22
+ * POST one feedback record. Returns the created row id.
23
+ * @throws AuthExpiredError on 401; Error (scrubbed) on any other non-2xx.
24
+ */
25
+ async function submitFeedback(apiBaseUrl, token, payload) {
26
+ let res;
27
+ try {
28
+ res = await fetch(`${normalizeBase(apiBaseUrl)}/api/content/feedback`, {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ Authorization: `Bearer ${token}`,
33
+ },
34
+ body: JSON.stringify(payload),
35
+ });
36
+ }
37
+ catch (err) {
38
+ throw new Error(`Network error reaching the feedback backend: ${err?.message ?? 'unknown'}`);
39
+ }
40
+ if (res.status === 401)
41
+ throw new AuthExpiredError();
42
+ if (!res.ok) {
43
+ // Scrub the body — never echo a raw server error that might carry internals.
44
+ throw new Error(`Feedback submission failed (HTTP ${res.status}).`);
45
+ }
46
+ const body = (await res.json().catch(() => ({})));
47
+ // FastSchema content create returns the row (id at top level or under .data).
48
+ // Only accept an actual id value — never fall back to the whole object (would
49
+ // print "[object Object]").
50
+ const id = body?.id ?? body?.data?.id ?? 'unknown';
51
+ return { id };
52
+ }
53
+ //# sourceMappingURL=feedback-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback-client.js","sourceRoot":"","sources":["../../../src/tools/sungen-api/feedback-client.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AA0BH,wCA+BC;AAvDD,wEAAwE;AACxE,MAAa,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAO,GAAG,yBAAyB;QAC7C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AALD,4CAKC;AAUD,SAAS,aAAa,CAAC,GAAW;IAChC,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAClC,UAAkB,EAClB,KAAa,EACb,OAAwB;IAExB,IAAI,GAAa,CAAC;IAClB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,uBAAuB,EAAE;YACrE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,KAAK,EAAE;aACjC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;SAC9B,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,gDAAgD,GAAG,EAAE,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC;IAC/F,CAAC;IAED,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,MAAM,IAAI,gBAAgB,EAAE,CAAC;IACrD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,6EAA6E;QAC7E,MAAM,IAAI,KAAK,CAAC,oCAAoC,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAQ,CAAC;IACzD,8EAA8E;IAC9E,8EAA8E;IAC9E,4BAA4B;IAC5B,MAAM,EAAE,GAAG,IAAI,EAAE,EAAE,IAAI,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,SAAS,CAAC;IACnD,OAAO,EAAE,EAAE,EAAE,CAAC;AAChB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Read piped stdin to a string. Returns null when stdin is a TTY (interactive),
3
+ * so callers can distinguish "no piped input" from "empty pipe".
4
+ */
5
+ export declare function readStdin(): Promise<string | null>;
6
+ //# sourceMappingURL=read-stdin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-stdin.d.ts","sourceRoot":"","sources":["../../../src/tools/sungen-api/read-stdin.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,SAAS,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASlD"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ /**
3
+ * Read piped stdin to a string. Returns null when stdin is a TTY (interactive),
4
+ * so callers can distinguish "no piped input" from "empty pipe".
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.readStdin = readStdin;
8
+ function readStdin() {
9
+ if (process.stdin.isTTY)
10
+ return Promise.resolve(null);
11
+ return new Promise((resolve, reject) => {
12
+ let data = '';
13
+ process.stdin.setEncoding('utf8');
14
+ process.stdin.on('data', (chunk) => { data += chunk; });
15
+ process.stdin.on('end', () => resolve(data));
16
+ process.stdin.on('error', reject);
17
+ });
18
+ }
19
+ //# sourceMappingURL=read-stdin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-stdin.js","sourceRoot":"","sources":["../../../src/tools/sungen-api/read-stdin.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAEH,8BASC;AATD,SAAgB,SAAS;IACvB,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAClC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7C,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolves the feedback backend base URL with a fixed precedence so `login` and
3
+ * `feedback` agree on where to talk.
4
+ *
5
+ * Precedence: explicit flag > SUNGEN_API_BASE_URL env > stored credential's
6
+ * api_base_url > DEFAULT_API_BASE_URL.
7
+ *
8
+ * DEFAULT_API_BASE_URL is the deployed production host, so the common case needs
9
+ * no config; override via --api-base-url / SUNGEN_API_BASE_URL for dev/staging.
10
+ */
11
+ /** Deployed feedback backend (production). Override with SUNGEN_API_BASE_URL locally. */
12
+ export declare const DEFAULT_API_BASE_URL = "https://sungen.sun-asterisk.vn";
13
+ export declare function resolveApiBaseUrl(flagValue?: string): string;
14
+ //# sourceMappingURL=api-base-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-base-url.d.ts","sourceRoot":"","sources":["../../../src/tools/sungen-auth/api-base-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,yFAAyF;AACzF,eAAO,MAAM,oBAAoB,mCAAmC,CAAC;AAErE,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAM5D"}
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * Resolves the feedback backend base URL with a fixed precedence so `login` and
4
+ * `feedback` agree on where to talk.
5
+ *
6
+ * Precedence: explicit flag > SUNGEN_API_BASE_URL env > stored credential's
7
+ * api_base_url > DEFAULT_API_BASE_URL.
8
+ *
9
+ * DEFAULT_API_BASE_URL is the deployed production host, so the common case needs
10
+ * no config; override via --api-base-url / SUNGEN_API_BASE_URL for dev/staging.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.DEFAULT_API_BASE_URL = void 0;
14
+ exports.resolveApiBaseUrl = resolveApiBaseUrl;
15
+ const credentials_store_1 = require("./credentials-store");
16
+ /** Deployed feedback backend (production). Override with SUNGEN_API_BASE_URL locally. */
17
+ exports.DEFAULT_API_BASE_URL = 'https://sungen.sun-asterisk.vn';
18
+ function resolveApiBaseUrl(flagValue) {
19
+ if (flagValue)
20
+ return flagValue;
21
+ if (process.env.SUNGEN_API_BASE_URL)
22
+ return process.env.SUNGEN_API_BASE_URL;
23
+ const stored = (0, credentials_store_1.loadCredentials)();
24
+ if (stored?.api_base_url)
25
+ return stored.api_base_url;
26
+ return exports.DEFAULT_API_BASE_URL;
27
+ }
28
+ //# sourceMappingURL=api-base-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-base-url.js","sourceRoot":"","sources":["../../../src/tools/sungen-auth/api-base-url.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAOH,8CAMC;AAXD,2DAAsD;AAEtD,yFAAyF;AAC5E,QAAA,oBAAoB,GAAG,gCAAgC,CAAC;AAErE,SAAgB,iBAAiB,CAAC,SAAkB;IAClD,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAChC,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IAC5E,MAAM,MAAM,GAAG,IAAA,mCAAe,GAAE,CAAC;IACjC,IAAI,MAAM,EAAE,YAAY;QAAE,OAAO,MAAM,CAAC,YAAY,CAAC;IACrD,OAAO,4BAAoB,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * User-level credential storage at ~/.sungen/credentials.json.
3
+ *
4
+ * Deliberately user-scoped (not project-local like the Figma PAT in .env): the
5
+ * login token is tied to the person and works across every sungen project.
6
+ * File mode 0600, parent dir 0700 — the token is a bearer secret.
7
+ */
8
+ import type { Credentials } from './sungen-auth-types';
9
+ /** Absolute path to the credentials file (~/.sungen/credentials.json). */
10
+ export declare function credentialsPath(): string;
11
+ /** Load stored credentials, or null when absent / unreadable / malformed. */
12
+ export declare function loadCredentials(): Credentials | null;
13
+ /** Persist credentials with restrictive permissions (dir 0700, file 0600). */
14
+ export declare function saveCredentials(creds: Credentials): void;
15
+ /** Remove the credentials file; no-op when absent. */
16
+ export declare function clearCredentials(): void;
17
+ /** True when the credential carries an expiry that is already past (no refresh token — prompt re-login on expiry). */
18
+ export declare function isExpired(creds: Credentials): boolean;
19
+ //# sourceMappingURL=credentials-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials-store.d.ts","sourceRoot":"","sources":["../../../src/tools/sungen-auth/credentials-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAKvD,0EAA0E;AAC1E,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CASpD;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAOxD;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,IAAI,IAAI,CAMvC;AAED,sHAAsH;AACtH,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAGrD"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * User-level credential storage at ~/.sungen/credentials.json.
4
+ *
5
+ * Deliberately user-scoped (not project-local like the Figma PAT in .env): the
6
+ * login token is tied to the person and works across every sungen project.
7
+ * File mode 0600, parent dir 0700 — the token is a bearer secret.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.credentialsPath = credentialsPath;
44
+ exports.loadCredentials = loadCredentials;
45
+ exports.saveCredentials = saveCredentials;
46
+ exports.clearCredentials = clearCredentials;
47
+ exports.isExpired = isExpired;
48
+ const fs = __importStar(require("fs"));
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ const DIR_NAME = '.sungen';
52
+ const FILE_NAME = 'credentials.json';
53
+ /** Absolute path to the credentials file (~/.sungen/credentials.json). */
54
+ function credentialsPath() {
55
+ return path.join(os.homedir(), DIR_NAME, FILE_NAME);
56
+ }
57
+ /** Load stored credentials, or null when absent / unreadable / malformed. */
58
+ function loadCredentials() {
59
+ try {
60
+ const raw = fs.readFileSync(credentialsPath(), 'utf8');
61
+ const parsed = JSON.parse(raw);
62
+ if (!parsed || typeof parsed.token !== 'string')
63
+ return null;
64
+ return parsed;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /** Persist credentials with restrictive permissions (dir 0700, file 0600). */
71
+ function saveCredentials(creds) {
72
+ const dir = path.join(os.homedir(), DIR_NAME);
73
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
74
+ const fp = credentialsPath();
75
+ // Write then chmod so an existing looser-mode file is tightened too.
76
+ fs.writeFileSync(fp, JSON.stringify(creds, null, 2), { mode: 0o600 });
77
+ fs.chmodSync(fp, 0o600);
78
+ }
79
+ /** Remove the credentials file; no-op when absent. */
80
+ function clearCredentials() {
81
+ try {
82
+ fs.unlinkSync(credentialsPath());
83
+ }
84
+ catch {
85
+ /* already gone */
86
+ }
87
+ }
88
+ /** True when the credential carries an expiry that is already past (no refresh token — prompt re-login on expiry). */
89
+ function isExpired(creds) {
90
+ if (typeof creds.expires_at !== 'number')
91
+ return false;
92
+ return creds.expires_at * 1000 <= Date.now();
93
+ }
94
+ //# sourceMappingURL=credentials-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials-store.js","sourceRoot":"","sources":["../../../src/tools/sungen-auth/credentials-store.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWH,0CAEC;AAGD,0CASC;AAGD,0CAOC;AAGD,4CAMC;AAGD,8BAGC;AAhDD,uCAAyB;AACzB,uCAAyB;AACzB,2CAA6B;AAG7B,MAAM,QAAQ,GAAG,SAAS,CAAC;AAC3B,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAErC,0EAA0E;AAC1E,SAAgB,eAAe;IAC7B,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;AACtD,CAAC;AAED,6EAA6E;AAC7E,SAAgB,eAAe;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,EAAE,MAAM,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgB,CAAC;QAC9C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC7D,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,SAAgB,eAAe,CAAC,KAAkB;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;IAC9C,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACpD,MAAM,EAAE,GAAG,eAAe,EAAE,CAAC;IAC7B,qEAAqE;IACrE,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACtE,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,sDAAsD;AACtD,SAAgB,gBAAgB;IAC9B,IAAI,CAAC;QACH,EAAE,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,kBAAkB;IACpB,CAAC;AACH,CAAC;AAED,sHAAsH;AACtH,SAAgB,SAAS,CAAC,KAAkB;IAC1C,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACvD,OAAO,KAAK,CAAC,UAAU,GAAG,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;AAC/C,CAAC"}