project-auto-wizard 0.16.2 → 0.17.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/LICENSE CHANGED
@@ -1,5 +1,6 @@
1
1
  MIT License
2
2
 
3
+ Copyright (c) 2025 Cassiiopeia
3
4
  Copyright (c) 2026 Twin-Fang
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
package/README.ja.md CHANGED
@@ -49,7 +49,7 @@ npx project-auto-wizard --mode full --force --type node --dry-run # preview on
49
49
 
50
50
  生成されたファイルをコミットして push してください。`npx project-auto-wizard --mode doctor` は、ワークフローが依存するリポジトリ設定を確認します。
51
51
 
52
- メッセージの言語は英語または韓国語(`en` | `ko`)です。`--lang`、環境変数 `PROJECT_AUTO_WIZARD_LANG`、`version.yml` の `language` の順に優先されます。環境変数はその実行にのみ適用され、有効な保存済み `language` は上書きしません(有効な値がない場合のみ保存)。上書きするには `--lang` を使います。
52
+ メッセージの言語は英語または韓国語(`en` | `ko`)です。使う言語が決まっていない場合、対話型ウィザードは最初に言語を尋ねます。`--lang` でも指定できます。`--lang`、環境変数 `PROJECT_AUTO_WIZARD_LANG`、`version.yml` の `language` の順に優先されます。環境変数はその実行にのみ適用され、有効な保存済み `language` は上書きしません(有効な値がない場合のみ保存)。上書きするには `--lang` を使います。
53
53
 
54
54
  ## インストールされるもの
55
55
 
@@ -162,6 +162,10 @@ Flutter のストアデプロイ(Play Store、Firebase、TestFlight)の設
162
162
 
163
163
  issue と pull request を歓迎します。`develop` からブランチを作り、`develop` に対して PR を開いてください。セットアップとテスト(`npm test`)は [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。このリポジトリは、自身がインストールするワークフローで自分自身をリリースしています。
164
164
 
165
+ ## 由来と謝辞
166
+
167
+ このプロジェクトは [Cassiiopeia/projectops](https://github.com/Cassiiopeia/projectops)(MIT、Copyright (c) 2025 Cassiiopeia)の GitHub Actions 自動化テンプレートから発展しました。ワークフローの構成や名前の一部(例: `PROJECT-COMMON-*` ワークフロー)はその成果を引き継いでおり、project-auto-wizard はそれを `npx` インストーラーとして再構成し拡張したものです。原作の著作権表示は [LICENSE](LICENSE) に残しています。
168
+
165
169
  ## ライセンス
166
170
 
167
171
  [MIT](LICENSE)
package/README.ko.md CHANGED
@@ -49,7 +49,7 @@ npx project-auto-wizard --mode full --force --type node --dry-run # preview on
49
49
 
50
50
  생성된 파일을 커밋하고 push하세요. `npx project-auto-wizard --mode doctor`는 워크플로우가 의존하는 레포 설정을 점검합니다.
51
51
 
52
- 메시지 언어는 영어 또는 한국어(`en` | `ko`)입니다. `--lang`, 환경변수 `PROJECT_AUTO_WIZARD_LANG`, `version.yml`의 `language` 순서로 우선 적용됩니다. 환경변수는 그 실행에만 적용되며 저장된 유효한 `language`를 덮어쓰지 않습니다(유효한 값이 없을 때만 저장). 덮어쓰려면 `--lang`을 쓰세요.
52
+ 메시지 언어는 영어 또는 한국어(`en` | `ko`)입니다. 어떤 언어를 쓸지 정해진 것이 없으면 대화형 마법사가 가장 먼저 언어를 묻고, `--lang`으로도 지정할 수 있습니다. `--lang`, 환경변수 `PROJECT_AUTO_WIZARD_LANG`, `version.yml`의 `language` 순서로 우선 적용됩니다. 환경변수는 그 실행에만 적용되며 저장된 유효한 `language`를 덮어쓰지 않습니다(유효한 값이 없을 때만 저장). 덮어쓰려면 `--lang`을 쓰세요.
53
53
 
54
54
  ## 설치되는 것
55
55
 
@@ -162,6 +162,10 @@ Flutter 스토어 배포(Play Store, Firebase, TestFlight) 설정은 [문서 사
162
162
 
163
163
  이슈와 풀 리퀘스트를 환영합니다. `develop`에서 브랜치를 만들고 `develop`을 대상으로 PR을 여세요. 설정과 테스트(`npm test`)는 [CONTRIBUTING.md](CONTRIBUTING.md)를 참고하세요. 이 레포는 마법사가 설치하는 워크플로우로 스스로 릴리스됩니다.
164
164
 
165
+ ## 출처와 감사
166
+
167
+ 이 프로젝트는 [Cassiiopeia/projectops](https://github.com/Cassiiopeia/projectops)(MIT, Copyright (c) 2025 Cassiiopeia)의 GitHub Actions 자동화 템플릿에서 출발했습니다. 워크플로우 구조와 이름 일부(예: `PROJECT-COMMON-*` 워크플로우)는 해당 작업에서 이어졌고, project-auto-wizard는 이를 `npx` 설치기로 다시 구성하고 확장했습니다. 원작의 저작권 고지는 [LICENSE](LICENSE)에 유지합니다.
168
+
165
169
  ## 라이선스
166
170
 
167
171
  [MIT](LICENSE)
package/README.md CHANGED
@@ -21,7 +21,7 @@ Everything it installs is plain GitHub Actions in your own repository. No API ke
21
21
  </div>
22
22
 
23
23
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
24
- ## Latest Version : v0.16.2 (2026-10-01)
24
+ ## Latest Version : v0.17.0 (2026-10-01)
25
25
 
26
26
  [Full release history](CHANGELOG.md)
27
27
 
@@ -50,7 +50,7 @@ npx project-auto-wizard --mode full --force --type node --dry-run # preview on
50
50
 
51
51
  Commit the generated files and push. `npx project-auto-wizard --mode doctor` checks the repository settings the workflows depend on.
52
52
 
53
- Messages are in English or Korean (`en` | `ko`). Pick one with `--lang`, the `PROJECT_AUTO_WIZARD_LANG` environment variable, or `language` in `version.yml`, in that order of precedence. The environment variable applies to that run only and does not overwrite a valid saved `language` (it is saved only when there is none); `--lang` does.
53
+ Messages are in English or Korean (`en` | `ko`). The interactive wizard asks for the language first when nothing says which one to use; you can also pick one with `--lang`, the `PROJECT_AUTO_WIZARD_LANG` environment variable, or `language` in `version.yml`, in that order of precedence. The environment variable applies to that run only and does not overwrite a valid saved `language` (it is saved only when there is none); `--lang` does.
54
54
 
55
55
  ## What you get
56
56
 
@@ -167,6 +167,10 @@ Issues and pull requests are welcome. Branch from `develop` and open PRs against
167
167
  - [ADDING-A-PROJECT-TYPE.md](ADDING-A-PROJECT-TYPE.md) — step-by-step guide to adding a project type
168
168
  - [CONTRIBUTING.md](CONTRIBUTING.md) — dev setup and pull request rules
169
169
 
170
+ ## Origin & Acknowledgements
171
+
172
+ This project grew out of the GitHub Actions automation templates of [Cassiiopeia/projectops](https://github.com/Cassiiopeia/projectops) (MIT, Copyright (c) 2025 Cassiiopeia). Some of the workflow structure and naming (for example the `PROJECT-COMMON-*` workflows) carries over from that work; project-auto-wizard repackages and extends it as an `npx` installer. The original copyright notice is kept in [LICENSE](LICENSE).
173
+
170
174
  ## License
171
175
 
172
176
  [MIT](LICENSE)
package/README.zh-CN.md CHANGED
@@ -49,7 +49,7 @@ npx project-auto-wizard --mode full --force --type node --dry-run # preview on
49
49
 
50
50
  提交生成的文件并 push。`npx project-auto-wizard --mode doctor` 会检查工作流所依赖的仓库设置。
51
51
 
52
- 消息语言为英语或韩语(`en` | `ko`)。可通过 `--lang`、环境变量 `PROJECT_AUTO_WIZARD_LANG` 或 `version.yml` 中的 `language` 指定,优先级依次降低。环境变量仅对该次运行生效,不会覆盖已保存的有效 `language`(仅在没有有效值时才保存);如需覆盖请使用 `--lang`。
52
+ 消息语言为英语或韩语(`en` | `ko`)。如果没有任何信息指明语言,交互式向导会首先询问语言,也可以通过 `--lang`、环境变量 `PROJECT_AUTO_WIZARD_LANG` 或 `version.yml` 中的 `language` 指定,优先级依次降低。环境变量仅对该次运行生效,不会覆盖已保存的有效 `language`(仅在没有有效值时才保存);如需覆盖请使用 `--lang`。
53
53
 
54
54
  ## 安装内容
55
55
 
@@ -162,6 +162,10 @@ Flutter 商店部署(Play Store、Firebase、TestFlight)的设置见[文档
162
162
 
163
163
  欢迎提交 issue 和 pull request。请从 `develop` 创建分支,并向 `develop` 发起 PR;环境搭建和测试(`npm test`)见 [CONTRIBUTING.md](CONTRIBUTING.md)。本仓库使用它自己安装的工作流来发布自身。
164
164
 
165
+ ## 来源与致谢
166
+
167
+ 本项目源自 [Cassiiopeia/projectops](https://github.com/Cassiiopeia/projectops)(MIT,Copyright (c) 2025 Cassiiopeia)的 GitHub Actions 自动化模板。工作流的部分结构和命名(例如 `PROJECT-COMMON-*` 工作流)沿用自该项目;project-auto-wizard 将其重新整理并扩展为 `npx` 安装器。原作的版权声明保留在 [LICENSE](LICENSE) 中。
168
+
165
169
  ## 许可证
166
170
 
167
171
  [MIT](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-auto-wizard",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "description": "One command DevOps: npx wizard that installs GitHub-native Release Automation (optional GitHub Copilot AI summaries) into any project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,11 +44,11 @@ metadata:
44
44
  last_updated: "{{NOW}}"
45
45
  last_updated_by: "{{LAST_UPDATED_BY}}"
46
46
  default_branch: "{{DEFAULT_BRANCH}}"
47
- integration_date: "{{TODAY}}"
47
+ integration_date: "{{INTEGRATION_DATE}}"
48
48
  template:
49
49
  source: "project-auto-wizard"
50
50
  version: "{{TEMPLATE_VERSION}}"
51
- integrated_date: "{{TODAY}}"
51
+ integrated_date: "{{INTEGRATED_DATE}}"
52
52
  last_update_date: "{{TODAY}}"
53
53
  branches:
54
54
  main: "{{MAIN_BRANCH}}"
@@ -371,7 +371,8 @@ jobs:
371
371
  owner: context.repo.owner,
372
372
  repo: context.repo.repo,
373
373
  name: 'Build Verification',
374
- head_sha: context.sha,
374
+ // On a PR, context.sha is the temporary merge commit; attach the check run to the PR head commit
375
+ head_sha: context.payload.pull_request?.head?.sha ?? context.sha,
375
376
  status: 'completed',
376
377
  conclusion: conclusion,
377
378
  output: {
@@ -29,6 +29,7 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
29
29
  // Use the same render function as the real install; assembling separately drifts the preview whenever an option is added.
30
30
  const wouldBe = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), {
31
31
  pathMarkers, extraTopLevel, lastUpdatedBy: prevParsed?.lastUpdatedBy || "",
32
+ integrationDate: prevParsed?.integrationDate || "", integratedDate: prevParsed?.integratedDate || "",
32
33
  });
33
34
  // Compare by the same rule as the real install: if only the timestamp lines differ, the install does not rewrite the file either.
34
35
  return { existed: existingRaw !== null, changed: existingRaw === null || !sameIgnoringTimestamps(existingRaw, wouldBe) };
@@ -78,7 +78,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
78
78
 
79
79
  // 2. Generate version.yml (render payload/version.yml.template; full regeneration)
80
80
  // A re-run where only the date lines differ does not rewrite it (idempotent).
81
- const vyText = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel, lastUpdatedBy: prevParsed?.lastUpdatedBy || "" });
81
+ const vyText = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel, lastUpdatedBy: prevParsed?.lastUpdatedBy || "", integrationDate: prevParsed?.integrationDate || "", integratedDate: prevParsed?.integratedDate || "" });
82
82
  if (prevVy == null || !sameIgnoringTimestamps(prevVy, vyText)) {
83
83
  writeText(vyPath, vyText);
84
84
  log.info("version", "write", `version.yml (v${version}, code=${versionCode})`);
@@ -29,7 +29,7 @@ import * as prompts from "../ui/prompts.js";
29
29
  import { runStatus, printStatus } from "./status.js";
30
30
  import { runDoctor, printDoctorReport } from "./doctor.js";
31
31
  import { currentLogPath, hasLegacyMdLogs } from "../core/logger.js";
32
- import { t, getLanguage } from "../i18n/index.js";
32
+ import { t, getLanguage, setLanguage, isSupportedLanguage, DEFAULT_LANGUAGE } from "../i18n/index.js";
33
33
 
34
34
  const CANCEL = prompts.CANCEL;
35
35
 
@@ -40,6 +40,15 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
40
40
  const payload = assertPayload(payloadRoot ?? resolvePayloadRoot());
41
41
  const templateVersion = readTemplateVersion();
42
42
 
43
+ // No language is known yet (no --lang, env var or saved value): ask before anything is printed so the banner
44
+ // already uses the pick. ESC or an unusable answer keeps the default. Stubs without the prompt skip it.
45
+ let language = baseCtx.language ?? null;
46
+ if (baseCtx.askLanguage && io.selectLanguage) {
47
+ const picked = await io.selectLanguage();
48
+ language = isSupportedLanguage(picked) ? picked : DEFAULT_LANGUAGE;
49
+ setLanguage(language);
50
+ }
51
+
43
52
  // Start banner. Stubs have no banner -> fall back to intro.
44
53
  if (io.banner) io.banner({ version: templateVersion, modeLabel: t("interactive.banner.modeLabel") });
45
54
  else io.intro?.(t("interactive.intro"));
@@ -324,7 +333,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
324
333
  releaseOptions: Object.fromEntries(OPTIONS.map((o) => [o.ctxField, optionState[o.name]])),
325
334
  mode, force: true, version, versionCode, branch, branches, paths,
326
335
  repoName, resolvers, envValues, envUseDefaults, now, today,
327
- language: baseCtx.language ?? existing?.language ?? getLanguage(),
336
+ language: language ?? existing?.language ?? getLanguage(),
328
337
  // Extra context used by the install log and completion summary - does not change the install behaviour itself.
329
338
  markers, envAnswers, detectWarnings,
330
339
  });
@@ -145,6 +145,10 @@ export function parseExisting(content) {
145
145
  // (quoted values are unescaped; an unquoted one ends at whitespace or a comment)
146
146
  const byMatch = /^[ \t]+last_updated_by:[ \t]*(?:"((?:[^"\\]|\\.)*)"|([^\s#"']+))/m.exec(text);
147
147
  const lastUpdatedBy = byMatch ? (byMatch[1] !== undefined ? byMatch[1].replace(/\\(.)/g, "$1") : byMatch[2]) : null;
148
+ // The original install dates: a re-install keeps them instead of overwriting them with the re-install date
149
+ const dateLine = (key) => line(new RegExp(`^\\s+${key}:\\s*["']?(\\d{4}-\\d{2}-\\d{2})["']?\\s*(?:#.*)?$`));
150
+ const integrationDate = dateLine("integration_date");
151
+ const integratedDate = dateLine("integrated_date");
148
152
  // project_paths block: ` type: "path"`
149
153
  const paths = new Map();
150
154
  // Entries folded into another entry of the same canonical type with a different folder (react: client + next: web).
@@ -196,7 +200,7 @@ export function parseExisting(content) {
196
200
  // metadata.template.branches - main/develop/mode (to skip re-asking in update mode)
197
201
  const branches = parseTemplateBranches(text);
198
202
  return {
199
- version, versionCode, types, language, languageUnsupported, lastUpdatedBy, paths, droppedPaths, templateVersion, options, invalidOptions, branches,
203
+ version, versionCode, types, language, languageUnsupported, lastUpdatedBy, integrationDate, integratedDate, paths, droppedPaths, templateVersion, options, invalidOptions, branches,
200
204
  deploy: parseDeployBlock(text), extraTopLevel: parseExtraTopLevel(text),
201
205
  };
202
206
  }
@@ -268,7 +272,7 @@ export function buildVersionYml({
268
272
  templateText, version, types = [], paths = new Map(), pathMarkers = new Map(),
269
273
  branch = "main", branches = null, versionCode = 1, now, today,
270
274
  templateOptions = null, deployValues = new Map(), extraTopLevel = [], typeOptions = {}, language = DEFAULT_LANGUAGE,
271
- lastUpdatedBy = "",
275
+ lastUpdatedBy = "", integrationDate = "", integratedDate = "",
272
276
  }) {
273
277
  if (!templateText) throw new Error(tr("core.versionYml.error.templateRequired"));
274
278
  const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(", ")}]` : `["basic"]`;
@@ -313,6 +317,8 @@ export function buildVersionYml({
313
317
  PROJECT_TYPES: typesJson,
314
318
  LANGUAGE: isSupportedLanguage(language) ? language : DEFAULT_LANGUAGE,
315
319
  NOW: now, TODAY: today || optionsDate, DEFAULT_BRANCH: branch,
320
+ // The original install dates win; today is written only when the key is absent (fresh install)
321
+ INTEGRATION_DATE: integrationDate || today || optionsDate, INTEGRATED_DATE: integratedDate || today || optionsDate,
316
322
  // A previously recorded value (e.g. the user who last bumped the version) wins over the installer's own name
317
323
  LAST_UPDATED_BY: escapeYamlDoubleQuoted(lastUpdatedBy || "project-auto-wizard"),
318
324
  TEMPLATE_VERSION: templateVersion,
@@ -353,13 +359,13 @@ export function sameIgnoringTimestamps(a, b) {
353
359
  // Builds the final version.yml from a single context - the real install (full) and the preview (dry-run)
354
360
  // use the same function, structurally preventing "the preview differs from the result".
355
361
  // deployValues exist only in a real install (the preview performs no substitution, so it is an empty Map).
356
- export function renderVersionYml(context, templateText, { pathMarkers, deployValues = new Map(), extraTopLevel = [], lastUpdatedBy = "" }) {
362
+ export function renderVersionYml(context, templateText, { pathMarkers, deployValues = new Map(), extraTopLevel = [], lastUpdatedBy = "", integrationDate = "", integratedDate = "" }) {
357
363
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
358
364
  now, today, templateVersion = "unknown", branches = null, language,
359
365
  deployStyle } = context;
360
366
  return buildVersionYml({
361
367
  templateText, version, types, paths, pathMarkers, branch, branches, versionCode, now, today,
362
- deployValues, extraTopLevel, language, lastUpdatedBy,
368
+ deployValues, extraTopLevel, language, lastUpdatedBy, integrationDate, integratedDate,
363
369
  typeOptions: mergeHookResults(types, "optionsFromContext", context),
364
370
  templateOptions: {
365
371
  templateVersion,
@@ -116,6 +116,8 @@ export default {
116
116
  "ui.readline-engine.yes": "Yes",
117
117
  "ui.readline-engine.no": "No",
118
118
  "ui.readline-engine.cancelled": "Cancelled.",
119
+ "ui.prompts.language.message": "Select language",
120
+ "ui.prompts.language.label": "English",
119
121
  "ui.prompts.mode.messageAgain": "What would you like to do next?",
120
122
  "ui.prompts.mode.message": "What would you like to install?",
121
123
  "ui.prompts.mode.full": "Install / update — version management + automation workflows (recommended for first time)",
@@ -116,6 +116,8 @@ export default {
116
116
  "ui.readline-engine.yes": "예",
117
117
  "ui.readline-engine.no": "아니오",
118
118
  "ui.readline-engine.cancelled": "취소했습니다.",
119
+ "ui.prompts.language.message": "언어 선택",
120
+ "ui.prompts.language.label": "한국어",
119
121
  "ui.prompts.mode.messageAgain": "다음으로 무엇을 할까요?",
120
122
  "ui.prompts.mode.message": "무엇을 설치할까요?",
121
123
  "ui.prompts.mode.full": "설치 / 업데이트 — 버전관리 + 자동화 워크플로우 (처음이라면 추천)",
package/src/index.js CHANGED
@@ -103,6 +103,8 @@ async function runInner(argv, {
103
103
  // Language written to version.yml: the environment variable only decides this run's output, so it must not
104
104
  // overwrite a saved choice. Only --lang (or a first install / an unusable saved value) sets the stored language.
105
105
  let storedLanguage;
106
+ // Interactive mode asks for the language itself when nothing says which one to use.
107
+ let askLanguage = false;
106
108
  try {
107
109
  const savedVy = join(cwd, "version.yml");
108
110
  const savedParsed = existsSync(savedVy) ? parseExisting(readFileSync(savedVy, "utf8")) : null;
@@ -114,11 +116,15 @@ async function runInner(argv, {
114
116
  // falls back to English, but it was never the old default, so it gets its own wording.
115
117
  const existingWithoutLanguage = existsSync(savedVy) && !saved;
116
118
  const unspecified = !normalizeLanguage(opts.lang) && !normalizeLanguage(process.env[LANG_ENV_VAR]);
119
+ askLanguage = opts.mode === "interactive" && unspecified && !saved;
117
120
  if (existingWithoutLanguage && unspecified && ["full", "interactive"].includes(opts.mode)) {
118
121
  const unknown = savedParsed?.languageUnsupported;
119
- console.error(unknown
120
- ? t("cli.lang.unsupportedSavedNotice", { value: unknown, supported: SUPPORTED_LANGUAGES.join(", ") }, DEFAULT_LANGUAGE)
121
- : t("cli.lang.defaultNotice", {}, DEFAULT_LANGUAGE));
122
+ if (unknown) {
123
+ console.error(t("cli.lang.unsupportedSavedNotice", { value: unknown, supported: SUPPORTED_LANGUAGES.join(", ") }, DEFAULT_LANGUAGE));
124
+ } else if (!askLanguage) {
125
+ // When the user is asked instead, there is no silent fallback left to explain.
126
+ console.error(t("cli.lang.defaultNotice", {}, DEFAULT_LANGUAGE));
127
+ }
122
128
  }
123
129
  } catch (e) {
124
130
  if (e instanceof CliError) { console.error(e.message); return 1; }
@@ -159,7 +165,7 @@ async function runInner(argv, {
159
165
  // The log file is created on the first write, so viewing only status/doctor from the menu leaves nothing behind.
160
166
  startLog("install");
161
167
  return await runInteractive(
162
- { ...optionContextFields(opts), language: storedLanguage },
168
+ { ...optionContextFields(opts), language: askLanguage ? null : storedLanguage, askLanguage },
163
169
  { cwd, payloadRoot: payload, clock },
164
170
  );
165
171
  }
package/src/ui/prompts.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // On ESC each function returns the CANCEL symbol -> the caller interprets it as default/stay.
4
4
  // Ctrl+C / Ctrl+D make the engine reject with PromptAbortError -> run() catches it and exits with code 130.
5
5
  import * as engine from "./readline-engine.js";
6
- import { t } from "../i18n/index.js";
6
+ import { t, SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE } from "../i18n/index.js";
7
7
  import { DEPLOY_STYLES, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
8
8
  import { TYPE_IDS, typeInfo } from "../core/types.js";
9
9
  import { askableOptions } from "../core/options.js";
@@ -11,6 +11,16 @@ import { ENV_MODES, DEFAULT_ENV_MODE, STORE_PLATFORMS, DEPLOY_MODES, DEFAULT_DEP
11
11
 
12
12
  export const CANCEL = engine.CANCEL;
13
13
 
14
+ // Language selection - asked before any language is known, so the message and every label are rendered
15
+ // once per supported language instead of in the active one. Returns the language code. CANCEL on cancel.
16
+ export async function selectLanguage() {
17
+ return engine.select({
18
+ message: SUPPORTED_LANGUAGES.map((lang) => t("ui.prompts.language.message", {}, lang)).join(" / "),
19
+ options: SUPPORTED_LANGUAGES.map((lang) => ({ value: lang, label: t("ui.prompts.language.label", {}, lang) })),
20
+ initialIndex: Math.max(0, SUPPORTED_LANGUAGES.indexOf(DEFAULT_LANGUAGE)),
21
+ });
22
+ }
23
+
14
24
  // Mode selection - localized labels, returns the internal key. CANCEL on cancel.
15
25
  // again=true is a re-entry after running a read-only mode (status/doctor) and returning to the menu -
16
26
  // asking "what should we install?" again would feel odd, so the wording changes.