Files
superwork/vscode/src/settings/store.ts
T
cruldra 7bac5c8b03 ✨ feat(vscode): 七份提示词统一走 Handlebars,头脑风暴三档合一
- 模板引擎换成 Handlebars(noEscape,提示词是纯文本不能被转义),单花括号写法彻底退场
- brainstorm-continue / brainstorm-continue-prototype 两份并进 brainstorm.md,
  用 {{#if issueNumber}} 分新建 / 续聊,续聊里用 {{#if prototypeFile}} 嵌原型段落,
  设置面板对应的两个 textarea 一并删掉
- 模板上下文 = 工单 state JSON 全量字段 + issueNumber / userRequest / nonce /
  planFile / prNumber / prRef,以后模板想用新字段不用改扩展代码
- 各会话入口共用一条 state 读取路径,读失败只记 warn 按空状态继续,不拦开会话
- 升级后首次启动把存量自定义提示词迁到双花括号并合并成一份,改写前把
  settings.json 备份成 settings.json.bak-<时间戳>,迁移动作全写日志
- 装 handlebars 时给 vsce 传递依赖的老 semver 加 trustPolicyExclude 豁免,
  否则 no-downgrade 策略卡住整个装包

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
2026-09-15 00:44:28 +08:00

565 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Non-secret extension settings persisted as `settings.json` under
* `context.globalStorageUri`. That directory survives VSIX 更新;`globalState`
* 在部分 VS Code profile 下只活在内存里,一重载就丢。
*
* Token stays in `context.secrets` (see `src/auth/secrets.ts`).
* 读:文件 → 旧 globalState(一次性迁移)。写:只写文件。
*
* Empty-string values for prompts are treated as "use the default" so the
* form can save user input verbatim and still fall back when the user clears
* a field.
*/
import type { ExtensionContext } from 'vscode'
import type { MigrationNote } from './promptMigration'
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { seedCommitProfilePath } from '../cc/commitProfile'
import { listClaudeProfiles } from '../cc/profiles'
import { normalizeReleasePrefix } from '../git/release'
import { planPromptMigration } from './promptMigration'
/**
* Fallback prompt strings used when the on-disk markdown can't be read.
* The full editable versions live in `prompts/*.md` at the extension root;
* users can tweak them without rebuilding. These short fallbacks just keep
* the extension usable if the file is missing/corrupt.
*/
const FALLBACK_BRAINSTORM_PROMPT = `{{#if issueNumber}}
/ecc:grilling 讨论下 {{issueNumber}} 号工单。
{{#if prototypeFile}}
工单已有原型插件 {{prototypeFile}},先读它;原型要改就 ListAgents 找到 issue-{{issueNumber}}-原型 会话{{#if prototypeSessionId}}(id {{prototypeSessionId}}){{/if}}并 SendMessage 给它,不要派子代理、不要自己动插件代码。
{{/if}}
达成共识后用 ecc:spec 写 .claude/specs/<stem>.md,我确认后用 ecc:plan 出 .claude/plans/<stem>.md,两个文件在 main 上提交并用 spx issue marker 同步 marker。不要 git checkout / 不要写代码 / 不要建 PR。
{{else}}
我现在有这样一个需求 {{userRequest}},你用 spx 命令创建 gitea 工单。工单 body 末尾必须包含 <!-- spx:nonce={{nonce}} -->。创建完工单立即停下汇报,不要擅自实施。
{{/if}}`
const FALLBACK_IMPLEMENT_PLAN_PROMPT = `/ecc:tdd @{{planFile}} 按计划实施,编码派子代理,发起 PR 时在 body 中包含 "Closes #{{issueNumber}}"。严禁合并 PR。`
const FALLBACK_TEST_PROMPT = `/pr-acceptance-testing 验收 {{issueNumber}} 号工单({{prRef}})。验收目标以工单正文为准,不要再问验收哪个。`
const FALLBACK_REVIEW_PROMPT = `用 tea 拿到 #{{prNumber}} PR,diff 交给 ecc:code-reviewer 子代理审查。审查意见用 spx pr review-comment 发评论,body 第一行写 <!-- spx:review=1 -->。`
const FALLBACK_PROTOTYPE_PROMPT = `/ecc:figma-plugin-prototyping 给 {{issueNumber}} 号工单画原型:先读工单正文锁定要画的界面和状态,再写本地 Figma 插件把画板和点击流做出来。画完汇报插件路径和怎么导入,不要改业务代码。
画完用 \`opencli spx issue marker --issue {{issueNumber}} --type prototype --value <manifest.json 的仓库相对路径>\` 把原型插件挂到工单上,再汇报。`
type PromptName = 'brainstorm' | 'implement-plan' | 'review' | 'test' | 'prototype'
/**
* Read a default prompt template from `${extensionPath}/prompts/${name}.md`.
*
* This is called every time `getSettings` runs, so edits to the markdown
* file take effect on the next cc/codex tab launch without reloading the
* window. If the file is missing or unreadable we fall back to a short
* inline string so the extension stays functional.
*/
export function readDefaultPrompt(extensionPath: string, name: PromptName): string {
const filePath = path.join(extensionPath, 'prompts', `${name}.md`)
try {
return readFileSync(filePath, 'utf-8')
}
catch (err) {
console.warn(`[spx] 读 ${filePath} 失败,使用内联 fallback:`, err)
switch (name) {
case 'brainstorm': return FALLBACK_BRAINSTORM_PROMPT
case 'implement-plan': return FALLBACK_IMPLEMENT_PLAN_PROMPT
case 'review': return FALLBACK_REVIEW_PROMPT
case 'test': return FALLBACK_TEST_PROMPT
case 'prototype': return FALLBACK_PROTOTYPE_PROMPT
}
}
}
export const DEFAULT_WEBHOOK_PORT = 17421
export interface Settings {
/** Local HTTP port for receiving gitea webhook callbacks. */
webhookPort: number
/**
* Gitea webhook 的 secret,用于校验 `X-Gitea-Signature`(HMAC-SHA256)。
* webhook 经 frp 暴露公网,不校验等于任何人都能伪造事件触发 codex。
* 空 = 不校验(未在 Gitea 侧配置 secret 的过渡状态)。
*/
webhookSecret: string
/**
* 头脑风暴模板(Handlebars)。`{{#if issueNumber}}` 走续聊已有工单,
* `{{else}}` 走新建工单(用 `{{userRequest}}` / `{{nonce}}`);续聊分支里
* `{{#if prototypeFile}}` 嵌原型段落。
*/
brainstormPrompt: string
/** 实施模板;常用 `{{planFile}}` / `{{issueNumber}}`。 */
implementPlanPrompt: string
/** Whether to automatically run `codex exec review` when a PR opens. */
autoReview: boolean
/** 审查模板;常用 `{{prNumber}}`。 */
reviewPrompt: string
/** 测试会话首个提示词模板;常用 `{{issueNumber}}` / `{{prRef}}`。 */
testPrompt: string
/** 原型会话首个提示词模板;常用 `{{issueNumber}}`。 */
prototypePrompt: string
/** Day-to-day development branch (e.g. `main`). Jenkins doesn't watch it. */
devBranch: string
/**
* 发布分支名前缀(如 `release/`),远端该前缀下字典序最大的分支即当期发布点。
* 读出时已归一化:trim、空回退默认、末尾补 `/`。
*/
releaseBranchPrefix: string
/** 正式服部署的 Jenkins 任务名;空串 = 不提供「部署到正式服」。 */
jenkinsJob: string
/** Jenkins 任务的 `REMOTE_HOST` 参数取值,空回退 `正式服`。 */
jenkinsProdHost: string
/**
* Directory template for the worktree created when implementing a plan.
* Placeholders: `$project_root` (workspace abs path), `$project_name` (its
* basename), `$feature_name` (the spec/plan slug). A leading `~` expands to
* the user's home dir. Empty string means "use the default
* `~/Sources/worktree/$project_name/$feature_name`".
*/
worktreeDirectory: string
/**
* Which editor opens a worktree when the user clicks it in the detail
* panel. `'pycharm'` shells out to the JetBrains `pycharm` launcher;
* `'vscode'` (default) opens a new VS Code window.
*/
worktreeOpenWith: 'vscode' | 'pycharm'
/**
* Command used to launch PyCharm when `worktreeOpenWith === 'pycharm'`.
* A bare name relies on PATH, which a GUI-launched VS Code often lacks, so
* an absolute path (or a `~`-prefixed one) is the reliable choice. Empty
* means "use the default `pycharm`".
*/
pycharmCommand: string
/**
* Path to a user-provided shell script that runs *after* the extension
* creates a worktree via `git worktree add`. Empty string means use the
* default `<workspaceRoot>/.spx/worktree-post-create.sh`. Absent file
* (default path or user-specified) is treated as a no-op, not an error.
*
* Hook receives env: `WORKTREE_PATH`, `WORKSPACE_ROOT`, `BRANCH`,
* `ISSUE_NUMBER`, `MAIN_BRANCH`.
*/
worktreePostCreateScript: string
/**
* Path to a user-provided shell script that runs *before* the extension
* removes a worktree via `git worktree remove`. Empty string means use
* the default `<workspaceRoot>/.spx/worktree-pre-remove.sh`. Absent file
* is treated as a no-op.
*
* Same env vars as the post-create hook.
*/
worktreePreRemoveScript: string
/**
* Path to a user-provided shell script that runs *before* the extension
* spawns the implement cc terminal tab. Empty string means use the
* default `<workspaceRoot>/.spx/impl-tab-pre-create.sh`. Absent file is
* treated as a no-op. Tab-reuse fast path (already-open impl terminal)
* does NOT trigger this hook.
*
* Same env vars as the worktree hooks.
*/
implTabPreCreateScript: string
/**
* Path to a user-provided shell script that runs *after* the implement
* cc terminal tab is closed (detected via `onDidCloseTerminal`). Empty
* string means use the default
* `<workspaceRoot>/.spx/impl-tab-post-close.sh`. Absent file is treated
* as a no-op. Typically used to tear down an IDE launched by the
* pre-create hook.
*
* Same env vars as the worktree hooks.
*/
implTabPostCloseScript: string
/**
* 新开会话终端是否分到右侧一栏。关掉则在当前编辑组加标签,适合小屏。
*/
splitNewTerminals: boolean
/**
* YouTrack instance base URL (e.g. `https://ziwuxian.youtrack.cloud`). Empty
* string means "YouTrack source disabled" — the loader returns no issues.
*/
youtrackBaseUrl: string
/**
* Short name of the YouTrack project to mirror into the board (e.g. `LXF`).
* Empty string means "disabled". The token lives in SecretStorage keyed by
* the base URL's host.
*/
youtrackProjectShortName: string
/**
* Optional YouTrack command applied when a youtrack card is dropped in the
* 完成 column (e.g. `State Fixed`). Empty string means "auto-detect" — the
* extension finds the project's first `isResolved` state value and uses that.
*/
youtrackCloseCommand: string
/**
* 头脑风暴会话的默认 Claude profile 绝对路径。工单详情面板里的
* 「头脑风暴配置文件」为空时用它;它也为空才落到 DEFAULT_PROFILE_PATH。
*/
defaultBrainstormProfilePath: string
/** 实施会话的默认 Claude profile;工单级「实施配置文件」为空时用它。 */
defaultImplementProfilePath: string
/** 测试会话的默认 Claude profile;工单级「测试配置文件」为空时用它。 */
defaultTestProfilePath: string
/** 原型会话的默认 Claude profile;工单级「原型配置文件」为空时用它。 */
defaultPrototypeProfilePath: string
/**
* Claude settings profile path for the conflict-resolution cc session
* (drag to 完成 when PR merge conflicts). Empty = DEFAULT_PROFILE_PATH.
*/
conflictResolutionProfilePath: string
/**
* 工具栏「提交代码」使用的 Claude profile 绝对路径。
* 空串 = 未配置 → 提交严格失败。升级时 key 缺失会 seed deepseek-v4-pro(若存在)。
*/
commitProfilePath: string
/**
* 「改动」tab 单文件说明 / 工单 PR 变更摘要使用的 Claude profile 绝对路径。
* 空串 = 未配置 → 生成失败。升级时 key 缺失会 seed deepseek-v4-pro(若存在)。
*/
prDiffSummaryProfilePath: string
/**
* codex 审查会话使用的模型(`-c model=`)。空串 = 不传,用 codex
* `config.toml` 默认。
*/
codexModel: string
/**
* codex 审查会话的思考级别(`-c model_reasoning_effort=`)。空串 = 不传,
* 用 codex 默认。合法值 minimal/low/medium/high/xhigh。
*/
codexReasoningEffort: string
/**
* Claude `--settings` profile 所在目录。空串 = 自动探测。
* 支持前导 `~`。保存后 listClaudeProfiles / getDefaultProfilePath 都从这里读。
*/
profilesDirectory: string
}
export const SETTINGS_KEY = 'superpowers.settings'
const SETTINGS_FILE = 'settings.json'
export function settingsFilePath(ctx: ExtensionContext): string {
return path.join(ctx.globalStorageUri.fsPath, SETTINGS_FILE)
}
function parseStoredBlob(raw: unknown): Partial<Settings> | null {
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
return null
return raw as Partial<Settings>
}
/** 文件优先;没有文件时读旧 globalState,供迁移。 */
export function readStoredSettings(ctx: ExtensionContext): Partial<Settings> {
try {
const parsed: unknown = JSON.parse(readFileSync(settingsFilePath(ctx), 'utf8'))
const blob = parseStoredBlob(parsed)
if (blob)
return blob
}
catch {
// 文件不存在或坏掉:回退 memento。
}
return parseStoredBlob(ctx.globalState.get(SETTINGS_KEY)) ?? {}
}
function writeStoredSettings(ctx: ExtensionContext, blob: Partial<Settings>): void {
const dir = ctx.globalStorageUri.fsPath
mkdirSync(dir, { recursive: true })
const file = settingsFilePath(ctx)
const tmp = `${file}.tmp`
writeFileSync(tmp, `${JSON.stringify(blob, null, 2)}\n`)
renameSync(tmp, file)
}
function defaults(ctx: ExtensionContext): Settings {
return {
webhookPort: DEFAULT_WEBHOOK_PORT,
webhookSecret: '',
brainstormPrompt: readDefaultPrompt(ctx.extensionPath, 'brainstorm'),
implementPlanPrompt: readDefaultPrompt(ctx.extensionPath, 'implement-plan'),
autoReview: true,
reviewPrompt: readDefaultPrompt(ctx.extensionPath, 'review'),
testPrompt: readDefaultPrompt(ctx.extensionPath, 'test'),
prototypePrompt: readDefaultPrompt(ctx.extensionPath, 'prototype'),
devBranch: 'main',
releaseBranchPrefix: 'release/',
jenkinsJob: '',
jenkinsProdHost: '正式服',
worktreeDirectory: '~/Sources/worktree/$project_name/$feature_name',
worktreeOpenWith: 'vscode',
pycharmCommand: 'pycharm',
worktreePostCreateScript: '',
worktreePreRemoveScript: '',
implTabPreCreateScript: '',
implTabPostCloseScript: '',
splitNewTerminals: true,
youtrackBaseUrl: '',
youtrackProjectShortName: '',
youtrackCloseCommand: '',
defaultBrainstormProfilePath: '',
defaultImplementProfilePath: '',
defaultTestProfilePath: '',
defaultPrototypeProfilePath: '',
conflictResolutionProfilePath: '',
commitProfilePath: '',
prDiffSummaryProfilePath: '',
codexModel: '',
codexReasoningEffort: '',
profilesDirectory: '',
}
}
export function getSettings(ctx: ExtensionContext): Settings {
const stored = readStoredSettings(ctx)
const base = defaults(ctx)
const webhookPort = typeof stored.webhookPort === 'number' && Number.isInteger(stored.webhookPort)
&& stored.webhookPort >= 1 && stored.webhookPort <= 65535
? stored.webhookPort
: base.webhookPort
// webhook secret:'' 有意义(= 不校验),只认字符串类型。
const webhookSecret = typeof stored.webhookSecret === 'string'
? stored.webhookSecret
: base.webhookSecret
const brainstormPrompt = typeof stored.brainstormPrompt === 'string' && stored.brainstormPrompt.length > 0
? stored.brainstormPrompt
: base.brainstormPrompt
const implementPlanPrompt = typeof stored.implementPlanPrompt === 'string' && stored.implementPlanPrompt.length > 0
? stored.implementPlanPrompt
: base.implementPlanPrompt
const autoReview = typeof stored.autoReview === 'boolean' ? stored.autoReview : base.autoReview
const reviewPrompt = typeof stored.reviewPrompt === 'string' && stored.reviewPrompt.length > 0
? stored.reviewPrompt
: base.reviewPrompt
const testPrompt = typeof stored.testPrompt === 'string' && stored.testPrompt.length > 0
? stored.testPrompt
: base.testPrompt
const prototypePrompt = typeof stored.prototypePrompt === 'string' && stored.prototypePrompt.length > 0
? stored.prototypePrompt
: base.prototypePrompt
// devBranch always has a usable value — fall back to 'main' if missing/blank
// so we never end up trying to fetch the empty string.
const devBranch = typeof stored.devBranch === 'string' && stored.devBranch.length > 0
? stored.devBranch
: base.devBranch
// 发布前缀读出时归一化(空 → 默认、补尾 `/`),下游拼分支名不用再防御。
const releaseBranchPrefix = normalizeReleasePrefix(
typeof stored.releaseBranchPrefix === 'string' ? stored.releaseBranchPrefix : base.releaseBranchPrefix,
)
// jenkinsJob:'' 有意义(= 不提供部署动作),只认字符串类型。
const jenkinsJob = typeof stored.jenkinsJob === 'string'
? stored.jenkinsJob.trim()
: base.jenkinsJob
const jenkinsProdHost = typeof stored.jenkinsProdHost === 'string' && stored.jenkinsProdHost.trim().length > 0
? stored.jenkinsProdHost.trim()
: base.jenkinsProdHost
// worktree dir template: empty/blank means "use the default template", so
// fall back to base (unlike the hook paths below, where '' is meaningful).
const worktreeDirectory = typeof stored.worktreeDirectory === 'string' && stored.worktreeDirectory.length > 0
? stored.worktreeDirectory
: base.worktreeDirectory
// worktreeOpenWith is an enum — only the two known values are valid; anything
// else (legacy installs, garbage) collapses to the default.
const worktreeOpenWith = stored.worktreeOpenWith === 'pycharm' || stored.worktreeOpenWith === 'vscode'
? stored.worktreeOpenWith
: base.worktreeOpenWith
// pycharm launcher command: blank means "use the default `pycharm`", so fall
// back to base rather than preserving an empty string.
const pycharmCommand = typeof stored.pycharmCommand === 'string' && stored.pycharmCommand.length > 0
? stored.pycharmCommand
: base.pycharmCommand
// worktree hook script paths: '' is meaningful (= "use default
// .spx/worktree-*.sh"), so don't coerce. Non-string legacy values fall
// back to ''. The hook runner resolves '' to the default at execution
// time and silently skips when the file doesn't exist.
const worktreePostCreateScript = typeof stored.worktreePostCreateScript === 'string'
? stored.worktreePostCreateScript
: base.worktreePostCreateScript
const worktreePreRemoveScript = typeof stored.worktreePreRemoveScript === 'string'
? stored.worktreePreRemoveScript
: base.worktreePreRemoveScript
// impl-tab hook script paths: same '' = default semantics as worktree hooks.
const implTabPreCreateScript = typeof stored.implTabPreCreateScript === 'string'
? stored.implTabPreCreateScript
: base.implTabPreCreateScript
const implTabPostCloseScript = typeof stored.implTabPostCloseScript === 'string'
? stored.implTabPostCloseScript
: base.implTabPostCloseScript
const splitNewTerminals = typeof stored.splitNewTerminals === 'boolean'
? stored.splitNewTerminals
: base.splitNewTerminals
// YouTrack config: '' is meaningful (= "source disabled" / "auto-detect close
// command"), so don't coerce. Non-string legacy values collapse to ''.
const youtrackBaseUrl = typeof stored.youtrackBaseUrl === 'string'
? stored.youtrackBaseUrl
: base.youtrackBaseUrl
const youtrackProjectShortName = typeof stored.youtrackProjectShortName === 'string'
? stored.youtrackProjectShortName
: base.youtrackProjectShortName
const youtrackCloseCommand = typeof stored.youtrackCloseCommand === 'string'
? stored.youtrackCloseCommand
: base.youtrackCloseCommand
// 四个会话默认 profile:'' 有意义(= 落到 DEFAULT_PROFILE_PATH),不强制回退。
const defaultBrainstormProfilePath = typeof stored.defaultBrainstormProfilePath === 'string'
? stored.defaultBrainstormProfilePath
: base.defaultBrainstormProfilePath
const defaultImplementProfilePath = typeof stored.defaultImplementProfilePath === 'string'
? stored.defaultImplementProfilePath
: base.defaultImplementProfilePath
const defaultTestProfilePath = typeof stored.defaultTestProfilePath === 'string'
? stored.defaultTestProfilePath
: base.defaultTestProfilePath
const defaultPrototypeProfilePath = typeof stored.defaultPrototypeProfilePath === 'string'
? stored.defaultPrototypeProfilePath
: base.defaultPrototypeProfilePath
// conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH at
// launch time), so don't coerce. Non-string legacy values collapse to ''.
const conflictResolutionProfilePath = typeof stored.conflictResolutionProfilePath === 'string'
? stored.conflictResolutionProfilePath
: base.conflictResolutionProfilePath
// 提交 profile:'' 有意义(= 未配置 → 提交失败)。缺 key 时先给 '';
// deepseek seed 在 getEffectiveCommitProfilePath 里做(需异步 list profiles)。
const commitProfilePath = typeof stored.commitProfilePath === 'string'
? stored.commitProfilePath
: base.commitProfilePath
const prDiffSummaryProfilePath = typeof stored.prDiffSummaryProfilePath === 'string'
? stored.prDiffSummaryProfilePath
: base.prDiffSummaryProfilePath
// codex 模型 / 思考级别:'' 有意义(= 用 codex 默认),不强制回退。
const codexModel = typeof stored.codexModel === 'string'
? stored.codexModel
: base.codexModel
const codexReasoningEffort = typeof stored.codexReasoningEffort === 'string'
? stored.codexReasoningEffort
: base.codexReasoningEffort
// profiles 目录:'' 有意义(= 自动探测),不强制回退。
const profilesDirectory = typeof stored.profilesDirectory === 'string'
? stored.profilesDirectory
: base.profilesDirectory
return {
webhookPort,
webhookSecret,
brainstormPrompt,
implementPlanPrompt,
autoReview,
reviewPrompt,
testPrompt,
prototypePrompt,
devBranch,
releaseBranchPrefix,
jenkinsJob,
jenkinsProdHost,
worktreeDirectory,
worktreeOpenWith,
pycharmCommand,
worktreePostCreateScript,
worktreePreRemoveScript,
implTabPreCreateScript,
implTabPostCloseScript,
splitNewTerminals,
youtrackBaseUrl,
youtrackProjectShortName,
youtrackCloseCommand,
defaultBrainstormProfilePath,
defaultImplementProfilePath,
defaultTestProfilePath,
defaultPrototypeProfilePath,
conflictResolutionProfilePath,
commitProfilePath,
prDiffSummaryProfilePath,
codexModel,
codexReasoningEffort,
profilesDirectory,
}
}
/**
* 读取工具栏「提交代码」有效的 profile path。
* ① stored 已有 key(含用户清空的 '')→ 原样返回
* ② key 从未出现 → list profiles 后 seed deepseek-v4-pro(启动时 persistSeededProfiles 会写盘)
*/
export async function getEffectiveCommitProfilePath(ctx: ExtensionContext): Promise<string> {
const stored = readStoredSettings(ctx)
if (typeof stored.commitProfilePath === 'string')
return stored.commitProfilePath
const profiles = await listClaudeProfiles()
return seedCommitProfilePath(undefined, profiles)
}
/**
* 读取改动说明有效的 profile path。规则与提交代码相同:已有 key 原样返回,缺 key 才 seed。
*/
export async function getEffectivePrDiffSummaryProfilePath(ctx: ExtensionContext): Promise<string> {
const stored = readStoredSettings(ctx)
if (typeof stored.prDiffSummaryProfilePath === 'string')
return stored.prDiffSummaryProfilePath
const profiles = await listClaudeProfiles()
return seedCommitProfilePath(undefined, profiles)
}
export async function saveSettings(ctx: ExtensionContext, next: Partial<Settings>): Promise<void> {
const merged: Partial<Settings> = { ...readStoredSettings(ctx), ...next }
writeStoredSettings(ctx, merged)
}
/**
* 一次性把存量自定义提示词迁到 Handlebars:单花括号改写成双花括号,续聊 /
* 原型段落并进头脑风暴模板。写盘前先把 settings.json 备份一份。
*
* 返回日志条目而不是自己写 logger——logger 依赖 vscode 运行时,store 要能在
* 单测里直接 import。
*/
export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] {
const { blob, notes } = planPromptMigration(
readStoredSettings(ctx) as Record<string, unknown>,
readDefaultPrompt(ctx.extensionPath, 'brainstorm'),
)
if (!blob)
return []
const out: MigrationNote[] = []
const file = settingsFilePath(ctx)
if (existsSync(file)) {
const backup = `${file}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`
try {
copyFileSync(file, backup)
out.push({ level: 'info', message: `提示词迁移前备份设置:${backup}` })
}
catch (err) {
return [{
level: 'warn',
message: `settings.json 备份失败,本次不迁移提示词:${err instanceof Error ? err.message : String(err)}`,
}]
}
}
writeStoredSettings(ctx, blob as Partial<Settings>)
out.push(...notes, { level: 'info', message: '提示词模板已迁到 Handlebars' })
return out
}
/**
* 启动时:把旧 memento 迁到文件;commit / 改动说明 profile 缺 key 且能 seed 则写盘。
* 空串视为用户主动清空,不覆盖。
*/
export async function persistSeededProfiles(ctx: ExtensionContext): Promise<void> {
const stored = readStoredSettings(ctx)
const fileExists = existsSync(settingsFilePath(ctx))
const profiles = await listClaudeProfiles()
const patch: Partial<Settings> = {}
if (typeof stored.commitProfilePath !== 'string') {
const seeded = seedCommitProfilePath(undefined, profiles)
if (seeded)
patch.commitProfilePath = seeded
}
if (typeof stored.prDiffSummaryProfilePath !== 'string') {
const seeded = seedCommitProfilePath(undefined, profiles)
if (seeded)
patch.prDiffSummaryProfilePath = seeded
}
const shouldMigrate = !fileExists && Object.keys(stored).length > 0
if (shouldMigrate || Object.keys(patch).length > 0)
await saveSettings(ctx, patch)
}