照测试会话那条链路补一份 prototype:
- 消息与路由:session/start-prototype、session/resume-prototype、
issue/update-prototype-profile-path,都进只读门禁
- 会话:handleStartPrototypeSession / handleResumePrototypeSession,
终端名 issue-N-原型;cwd 有 worktree 用 worktree、没有退主仓库根
(原 resolveTestSessionCwd 改名 resolveWorktreeOrRootCwd 两种会话共用)
- 不要求有 PR,任何阶段都能起;前置工单锁仍然拦
- 提示词:新增 prompts/prototype.md,设置里可改,占位符 {issueNumber}
- profile:工单级 prototypeProfilePath > 设置里的原型默认 profile >
内置默认,与其它会话互不回退
- 状态字段 prototypeSessionId / prototypeProfilePath / prototypeTabOpen
贯通 gitea、youtrack、本机 state 与移交包
- 详情面板加「原型会话id」「原型配置文件」两行,设置面板加对应两项
Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
533 lines
23 KiB
TypeScript
533 lines
23 KiB
TypeScript
/**
|
||
* 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 { 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'
|
||
|
||
/**
|
||
* 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 = `我现在有这样一个需求 {userRequest},你用 spx 命令创建 gitea 工单。工单 body 末尾必须包含 <!-- spx:nonce={nonce} -->。创建完工单立即停下汇报,不要擅自实施。`
|
||
|
||
const FALLBACK_BRAINSTORM_CONTINUE_PROMPT = `/ecc:grilling 讨论下 {issueNumber} 号工单。达成共识后用 ecc:spec 写 .claude/specs/<stem>.md,我确认后用 ecc:plan 出 .claude/plans/<stem>.md,两个文件在 main 上提交并用 spx issue marker 同步 marker。不要 git checkout / 不要写代码 / 不要建 PR。`
|
||
|
||
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 插件把画板和点击流做出来。画完汇报插件路径和怎么导入,不要改业务代码。`
|
||
|
||
type PromptName = 'brainstorm' | 'brainstorm-continue' | '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 'brainstorm-continue': return FALLBACK_BRAINSTORM_CONTINUE_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
|
||
/**
|
||
* Prompt template for the brainstorming flow (issue creation + ongoing
|
||
* session conventions for spec/plan body annotations). `{userRequest}`
|
||
* and `{nonce}` placeholders.
|
||
*/
|
||
brainstormPrompt: string
|
||
/**
|
||
* Prompt template for continuing brainstorm on an already-existing issue
|
||
* (when the user later wants to discuss / write spec/plan in the panel).
|
||
* `{issueNumber}` placeholder.
|
||
*/
|
||
brainstormContinuePrompt: string
|
||
/** Prompt template for the implement-plan flow. `{planFile}` placeholder. */
|
||
implementPlanPrompt: string
|
||
/** Whether to automatically run `codex exec review` when a PR opens. */
|
||
autoReview: boolean
|
||
/** Prompt template for the auto-review flow. `{prNumber}` placeholder. */
|
||
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'),
|
||
brainstormContinuePrompt: readDefaultPrompt(ctx.extensionPath, 'brainstorm-continue'),
|
||
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 brainstormContinuePrompt = typeof stored.brainstormContinuePrompt === 'string' && stored.brainstormContinuePrompt.length > 0
|
||
? stored.brainstormContinuePrompt
|
||
: base.brainstormContinuePrompt
|
||
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,
|
||
brainstormContinuePrompt,
|
||
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)
|
||
}
|
||
|
||
/**
|
||
* 启动时:把旧 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)
|
||
}
|