@dynamicworks/br-openspec 2.0.0 → 2.1.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.
Files changed (46) hide show
  1. package/README.md +11 -2
  2. package/README.pt-BR.md +11 -2
  3. package/dist/commands/config.js +4 -0
  4. package/dist/commands/schema.js +21 -21
  5. package/dist/core/artifact-graph/instruction-loader.js +4 -4
  6. package/dist/core/artifact-graph/schema.js +5 -4
  7. package/dist/core/completions/factory.js +3 -2
  8. package/dist/core/completions/installers/fish-installer.js +13 -12
  9. package/dist/core/completions/installers/powershell-installer.js +16 -17
  10. package/dist/core/completions/installers/zsh-installer.d.ts +0 -8
  11. package/dist/core/completions/installers/zsh-installer.js +4 -32
  12. package/dist/core/config.js +3 -2
  13. package/dist/core/global-config.d.ts +6 -1
  14. package/dist/core/global-config.js +15 -16
  15. package/dist/core/parsers/change-parser.js +7 -6
  16. package/dist/core/parsers/requirement-blocks.js +5 -5
  17. package/dist/core/parsers/spec-structure.js +1 -1
  18. package/dist/core/profile-sync-drift.js +1 -0
  19. package/dist/core/profiles.d.ts +2 -2
  20. package/dist/core/profiles.js +2 -1
  21. package/dist/core/project-config.js +12 -13
  22. package/dist/core/shared/skill-generation.js +3 -1
  23. package/dist/core/shared/tool-detection.d.ts +2 -2
  24. package/dist/core/shared/tool-detection.js +2 -0
  25. package/dist/core/specs-apply.js +38 -39
  26. package/dist/core/templates/skill-templates.d.ts +1 -1
  27. package/dist/core/templates/skill-templates.js +1 -1
  28. package/dist/core/templates/workflows/code-review.d.ts +10 -0
  29. package/dist/core/templates/workflows/code-review.js +21 -0
  30. package/dist/core/templates/workflows/sync-specs.js +2 -2
  31. package/dist/core/tools-manager.js +3 -2
  32. package/dist/core/update.d.ts +6 -0
  33. package/dist/core/update.js +21 -0
  34. package/dist/core/validation/validator.js +2 -2
  35. package/dist/messages/index.d.ts +145 -2
  36. package/dist/messages/index.js +320 -12
  37. package/dist/utils/change-metadata.js +8 -7
  38. package/dist/utils/change-utils.js +12 -11
  39. package/package.json +1 -1
  40. package/schemas/spec-driven/schema.yaml +78 -78
  41. package/schemas/spec-driven/templates/design.md +5 -5
  42. package/schemas/spec-driven/templates/proposal.md +9 -9
  43. package/schemas/spec-driven/templates/spec.md +5 -5
  44. package/schemas/spec-driven/templates/tasks.md +6 -6
  45. package/dist/core/templates/workflows/upstream-sync.d.ts +0 -10
  46. package/dist/core/templates/workflows/upstream-sync.js +0 -116
package/README.md CHANGED
@@ -97,7 +97,7 @@ openspec init
97
97
 
98
98
  Now tell your AI: `/opsx:propose <what-you-want-to-build>`
99
99
 
100
- If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
100
+ If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
101
101
 
102
102
  > [!NOTE]
103
103
  > Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
@@ -116,6 +116,13 @@ If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/
116
116
  → **[Customization](docs/customization.md)**: make it yours
117
117
 
118
118
 
119
+ ## Community schemas
120
+
121
+ Third-party schema bundles distributed via standalone repositories — these provide opinionated workflows that integrate BR-OpenSpec with other tools, similar to how [github/spec-kit's community extension catalog](https://github.com/github/spec-kit/tree/main/extensions) handles tool integrations.
122
+
123
+ → **[Browse the catalog](docs/customization.md#community-schemas)** in the customization docs.
124
+
125
+
119
126
  ## Why BR-OpenSpec?
120
127
 
121
128
  AI coding assistants are powerful but unpredictable when requirements live only in chat history. BR-OpenSpec adds a lightweight spec layer so you agree on what to build before any code is written.
@@ -161,10 +168,12 @@ openspec tools --remove windsurf
161
168
 
162
169
  ## Usage Notes
163
170
 
164
- **Model selection**: BR-OpenSpec works best with high-reasoning models. We recommend Opus 4.5 and GPT 5.2 for both planning and implementation.
171
+ **Model selection**: BR-OpenSpec works best with high-reasoning models. We recommend Codex 5.5 and Opus 4.8 for both planning and implementation.
165
172
 
166
173
  **Context hygiene**: BR-OpenSpec benefits from a clean context window. Clear your context before starting implementation and maintain good context hygiene throughout your session.
167
174
 
175
+ **Spec keywords stay in English**: BR-OpenSpec is PT-BR first, but the spec format is a protocol read by the tooling. Structural markers (`## ADDED Requirements`, `### Requirement:`, `#### Scenario:`) and normative/scenario keywords (RFC 2119 — `MUST`, `SHALL`, `SHOULD`, `MAY`, … — plus `WHEN`, `THEN`, `AND`, `GIVEN`, `ELSE`) are always written in English and UPPERCASE; only the descriptive text is in Portuguese. Translating these keywords breaks `openspec validate`. See the full list in [AGENTS.md](AGENTS.md#reserved-english-terms-never-translate).
176
+
168
177
  ## Contributing
169
178
 
170
179
  **Small fixes** — Bug fixes, typo corrections, and minor improvements can be submitted directly as PRs.
package/README.pt-BR.md CHANGED
@@ -97,7 +97,7 @@ openspec init
97
97
 
98
98
  Agora diga à sua IA: `/opsx:propose <o-que-você-quer-construir>`
99
99
 
100
- Se você quiser o fluxo de trabalho expandido (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), selecione-o com `openspec config profile` e aplique com `openspec update`.
100
+ Se você quiser o fluxo de trabalho expandido (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:bulk-archive`, `/opsx:onboard`), selecione-o com `openspec config profile` e aplique com `openspec update`.
101
101
 
102
102
  > [!NOTE]
103
103
  > Não tem certeza se sua ferramenta é suportada? [Veja a lista completa](docs/pt-BR/supported-tools.md) – suportamos mais de 25 ferramentas e crescendo.
@@ -116,6 +116,13 @@ Se você quiser o fluxo de trabalho expandido (`/opsx:new`, `/opsx:continue`, `/
116
116
  → **[Personalização](docs/pt-BR/customization.md)**: faça do seu jeito
117
117
 
118
118
 
119
+ ## Schemas da comunidade
120
+
121
+ Bundles de schema de terceiros distribuídos em repositórios independentes — oferecem fluxos de trabalho opinativos que integram o BR-OpenSpec a outras ferramentas, de forma semelhante a como o [catálogo de extensões da comunidade do github/spec-kit](https://github.com/github/spec-kit/tree/main/extensions) trata integrações de ferramentas.
122
+
123
+ → **[Veja o catálogo](docs/pt-BR/customization.md#schemas-da-comunidade)** na documentação de personalização.
124
+
125
+
119
126
  ## Por que o BR-OpenSpec?
120
127
 
121
128
  Assistentes de codificação com IA são poderosos, mas imprevisíveis quando os requisitos vivem apenas no histórico do chat. O BR-OpenSpec adiciona uma camada leve de especificação para que você concorde sobre o que construir antes de qualquer código ser escrito.
@@ -163,10 +170,12 @@ openspec tools --remove windsurf
163
170
 
164
171
  ## Notas de Uso
165
172
 
166
- **Seleção de modelo**: O BR-OpenSpec funciona melhor com modelos de alto raciocínio. Recomendamos Opus 4.5 e GPT 5.2 tanto para planejamento quanto para implementação.
173
+ **Seleção de modelo**: O BR-OpenSpec funciona melhor com modelos de alto raciocínio. Recomendamos Codex 5.5 e Opus 4.8 tanto para planejamento quanto para implementação.
167
174
 
168
175
  **Higiene de contexto**: O BR-OpenSpec se beneficia de uma janela de contexto limpa. Limpe seu contexto antes de iniciar a implementação e mantenha uma boa higiene de contexto ao longo da sua sessão.
169
176
 
177
+ **Palavras-chave do formato de spec ficam em inglês**: O BR-OpenSpec é PT-BR first, mas o formato de spec é um protocolo lido pelas ferramentas. Os marcadores estruturais (`## ADDED Requirements`, `### Requirement:`, `#### Scenario:`) e as palavras-chave normativas/de cenário (RFC 2119 — `MUST`, `SHALL`, `SHOULD`, `MAY`, … — além de `WHEN`, `THEN`, `AND`, `GIVEN`, `ELSE`) são sempre escritas em inglês e em CAIXA ALTA; apenas o texto descritivo fica em português. Traduzir essas palavras-chave quebra o `openspec validate`. Veja a lista completa em [AGENTS.md](AGENTS.md#reserved-english-terms-never-translate).
178
+
170
179
  ## Contribuindo
171
180
 
172
181
  **Pequenas correções** — Correções de bugs, erros de digitação e melhorias menores podem ser enviadas diretamente como PRs.
@@ -48,6 +48,10 @@ const WORKFLOW_PROMPT_META = {
48
48
  name: CONFIG_MESSAGES.workflowVerifyName,
49
49
  description: CONFIG_MESSAGES.workflowVerifyDesc,
50
50
  },
51
+ 'code-review': {
52
+ name: CONFIG_MESSAGES.workflowCodeReviewName,
53
+ description: CONFIG_MESSAGES.workflowCodeReviewDesc,
54
+ },
51
55
  onboard: {
52
56
  name: CONFIG_MESSAGES.workflowOnboardName,
53
57
  description: CONFIG_MESSAGES.workflowOnboardDesc,
@@ -797,72 +797,72 @@ function createDefaultTemplate(artifactId) {
797
797
  case 'proposal':
798
798
  return `## Why
799
799
 
800
- <!-- Describe the motivation for this change -->
800
+ <!-- Descreva a motivação para esta mudança -->
801
801
 
802
802
  ## What Changes
803
803
 
804
- <!-- Describe what will change -->
804
+ <!-- Descreva o que vai mudar -->
805
805
 
806
806
  ## Capabilities
807
807
 
808
808
  ### New Capabilities
809
- <!-- List new capabilities -->
809
+ <!-- Liste as novas capabilities -->
810
810
 
811
811
  ### Modified Capabilities
812
- <!-- List modified capabilities -->
812
+ <!-- Liste as capabilities modificadas -->
813
813
 
814
814
  ## Impact
815
815
 
816
- <!-- Describe the impact on existing functionality -->
816
+ <!-- Descreva o impacto sobre a funcionalidade existente -->
817
817
  `;
818
818
  case 'specs':
819
819
  return `## ADDED Requirements
820
820
 
821
- ### Requirement: Example requirement
821
+ ### Requirement: Requisito de exemplo
822
822
 
823
- Description of the requirement.
823
+ Descrição do requisito.
824
824
 
825
- #### Scenario: Example scenario
826
- - **WHEN** some condition
827
- - **THEN** some outcome
825
+ #### Scenario: Cenário de exemplo
826
+ - **WHEN** alguma condição
827
+ - **THEN** algum resultado
828
828
  `;
829
829
  case 'design':
830
830
  return `## Context
831
831
 
832
- <!-- Background and context -->
832
+ <!-- Contexto e antecedentes -->
833
833
 
834
834
  ## Goals / Non-Goals
835
835
 
836
836
  **Goals:**
837
- <!-- List goals -->
837
+ <!-- Liste os objetivos -->
838
838
 
839
839
  **Non-Goals:**
840
- <!-- List non-goals -->
840
+ <!-- Liste os não-objetivos -->
841
841
 
842
842
  ## Decisions
843
843
 
844
- ### 1. Decision Name
844
+ ### 1. Nome da Decisão
845
845
 
846
- Description and rationale.
846
+ Descrição e justificativa.
847
847
 
848
848
  **Alternatives considered:**
849
- - Alternative 1: Rejected because...
849
+ - Alternativa 1: Rejeitada porque...
850
850
 
851
851
  ## Risks / Trade-offs
852
852
 
853
- <!-- List risks and trade-offs -->
853
+ <!-- Liste os riscos e trade-offs -->
854
854
  `;
855
855
  case 'tasks':
856
856
  return `## Implementation Tasks
857
857
 
858
- - [ ] Task 1
859
- - [ ] Task 2
860
- - [ ] Task 3
858
+ - [ ] Tarefa 1
859
+ - [ ] Tarefa 2
860
+ - [ ] Tarefa 3
861
861
  `;
862
862
  default:
863
863
  return `## ${artifactId}
864
864
 
865
- <!-- Add content here -->
865
+ <!-- Adicione o conteúdo aqui -->
866
866
  `;
867
867
  }
868
868
  }
@@ -4,7 +4,7 @@ import { getSchemaDir, resolveSchema } from './resolver.js';
4
4
  import { ArtifactGraph } from './graph.js';
5
5
  import { detectCompleted } from './state.js';
6
6
  import { resolveSchemaForChange } from '../../utils/change-metadata.js';
7
- import { WORKFLOW_MESSAGES } from '../../messages/index.js';
7
+ import { WORKFLOW_MESSAGES, ARTIFACT_GRAPH_MESSAGES } from '../../messages/index.js';
8
8
  import { FileSystemUtils } from '../../utils/file-system.js';
9
9
  import { readProjectConfig, validateConfigRules } from '../project-config.js';
10
10
  // Session-level cache for validation warnings (avoid repeating same warnings)
@@ -36,7 +36,7 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
36
36
  }
37
37
  const templatePathOnDisk = path.join(schemaDir, 'templates', templatePath);
38
38
  if (!fs.existsSync(templatePathOnDisk)) {
39
- throw new TemplateLoadError(`Template not found: ${templatePathOnDisk}`, templatePathOnDisk);
39
+ throw new TemplateLoadError(ARTIFACT_GRAPH_MESSAGES.templateNotFound(templatePathOnDisk), templatePathOnDisk);
40
40
  }
41
41
  const fullPath = FileSystemUtils.canonicalizeExistingPath(templatePathOnDisk);
42
42
  try {
@@ -44,7 +44,7 @@ export function loadTemplate(schemaName, templatePath, projectRoot) {
44
44
  }
45
45
  catch (err) {
46
46
  const ioError = err instanceof Error ? err : new Error(String(err));
47
- throw new TemplateLoadError(`Failed to read template: ${ioError.message}`, fullPath);
47
+ throw new TemplateLoadError(ARTIFACT_GRAPH_MESSAGES.failedToReadTemplate(ioError.message), fullPath);
48
48
  }
49
49
  }
50
50
  /**
@@ -93,7 +93,7 @@ export function loadChangeContext(projectRoot, changeName, schemaName) {
93
93
  export function generateInstructions(context, artifactId, projectRoot) {
94
94
  const artifact = context.graph.getArtifact(artifactId);
95
95
  if (!artifact) {
96
- throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
96
+ throw new Error(ARTIFACT_GRAPH_MESSAGES.artifactNotFound(artifactId, context.schemaName));
97
97
  }
98
98
  const templateContent = loadTemplate(context.schemaName, artifact.template, context.projectRoot);
99
99
  const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import { parse as parseYaml } from 'yaml';
3
3
  import { SchemaYamlSchema } from './types.js';
4
+ import { ARTIFACT_GRAPH_MESSAGES } from '../../messages/index.js';
4
5
  export class SchemaValidationError extends Error {
5
6
  constructor(message) {
6
7
  super(message);
@@ -23,7 +24,7 @@ export function parseSchema(yamlContent) {
23
24
  const result = SchemaYamlSchema.safeParse(parsed);
24
25
  if (!result.success) {
25
26
  const errors = result.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
26
- throw new SchemaValidationError(`Invalid schema: ${errors}`);
27
+ throw new SchemaValidationError(ARTIFACT_GRAPH_MESSAGES.invalidSchema(errors));
27
28
  }
28
29
  const schema = result.data;
29
30
  // Check for duplicate artifact IDs
@@ -41,7 +42,7 @@ function validateNoDuplicateIds(artifacts) {
41
42
  const seen = new Set();
42
43
  for (const artifact of artifacts) {
43
44
  if (seen.has(artifact.id)) {
44
- throw new SchemaValidationError(`Duplicate artifact ID: ${artifact.id}`);
45
+ throw new SchemaValidationError(ARTIFACT_GRAPH_MESSAGES.duplicateArtifactId(artifact.id));
45
46
  }
46
47
  seen.add(artifact.id);
47
48
  }
@@ -54,7 +55,7 @@ function validateRequiresReferences(artifacts) {
54
55
  for (const artifact of artifacts) {
55
56
  for (const req of artifact.requires) {
56
57
  if (!validIds.has(req)) {
57
- throw new SchemaValidationError(`Invalid dependency reference in artifact '${artifact.id}': '${req}' does not exist`);
58
+ throw new SchemaValidationError(ARTIFACT_GRAPH_MESSAGES.invalidDependencyReference(artifact.id, req));
58
59
  }
59
60
  }
60
61
  }
@@ -100,7 +101,7 @@ function validateNoCycles(artifacts) {
100
101
  if (!visited.has(artifact.id)) {
101
102
  const cycle = dfs(artifact.id);
102
103
  if (cycle) {
103
- throw new SchemaValidationError(`Cyclic dependency detected: ${cycle}`);
104
+ throw new SchemaValidationError(ARTIFACT_GRAPH_MESSAGES.cyclicDependency(cycle));
104
105
  }
105
106
  }
106
107
  }
@@ -6,6 +6,7 @@ import { ZshInstaller } from './installers/zsh-installer.js';
6
6
  import { BashInstaller } from './installers/bash-installer.js';
7
7
  import { FishInstaller } from './installers/fish-installer.js';
8
8
  import { PowerShellInstaller } from './installers/powershell-installer.js';
9
+ import { COMPLETIONS_FACTORY_MESSAGES } from '../../messages/index.js';
9
10
  /**
10
11
  * Factory for creating completion generators and installers
11
12
  * This design makes it easy to add support for additional shells
@@ -30,7 +31,7 @@ export class CompletionFactory {
30
31
  case 'powershell':
31
32
  return new PowerShellGenerator();
32
33
  default:
33
- throw new Error(`Unsupported shell: ${shell}`);
34
+ throw new Error(COMPLETIONS_FACTORY_MESSAGES.unsupportedShell(shell));
34
35
  }
35
36
  }
36
37
  /**
@@ -51,7 +52,7 @@ export class CompletionFactory {
51
52
  case 'powershell':
52
53
  return new PowerShellInstaller();
53
54
  default:
54
- throw new Error(`Unsupported shell: ${shell}`);
55
+ throw new Error(COMPLETIONS_FACTORY_MESSAGES.unsupportedShell(shell));
55
56
  }
56
57
  }
57
58
  /**
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { COMPLETION_MESSAGES } from '../../../messages/index.js';
4
5
  /**
5
6
  * Installer for Fish completion scripts.
6
7
  * Fish automatically loads completions from ~/.config/fish/completions/
@@ -56,10 +57,10 @@ export class FishInstaller {
56
57
  return {
57
58
  success: true,
58
59
  installedPath: targetPath,
59
- message: 'Completion script is already installed (up to date)',
60
+ message: COMPLETION_MESSAGES.fishAlreadyInstalled,
60
61
  instructions: [
61
- 'The completion script is already installed and up to date.',
62
- 'Fish automatically loads completions - they should be available immediately.',
62
+ COMPLETION_MESSAGES.fishAlreadyInstalledDetail,
63
+ COMPLETION_MESSAGES.fishAutoLoadsHint,
63
64
  ],
64
65
  };
65
66
  }
@@ -81,11 +82,11 @@ export class FishInstaller {
81
82
  let message;
82
83
  if (isUpdate) {
83
84
  message = backupPath
84
- ? 'Completion script updated successfully (previous version backed up)'
85
- : 'Completion script updated successfully';
85
+ ? COMPLETION_MESSAGES.fishUpdatedWithBackup
86
+ : COMPLETION_MESSAGES.fishUpdated;
86
87
  }
87
88
  else {
88
- message = 'Completion script installed successfully for Fish';
89
+ message = COMPLETION_MESSAGES.fishInstalled;
89
90
  }
90
91
  return {
91
92
  success: true,
@@ -93,15 +94,15 @@ export class FishInstaller {
93
94
  backupPath,
94
95
  message,
95
96
  instructions: [
96
- 'Fish automatically loads completions from ~/.config/fish/completions/',
97
- 'Completions are available immediately - no shell restart needed.',
97
+ COMPLETION_MESSAGES.fishAutoLoadsDir,
98
+ COMPLETION_MESSAGES.fishAvailableImmediately,
98
99
  ],
99
100
  };
100
101
  }
101
102
  catch (error) {
102
103
  return {
103
104
  success: false,
104
- message: `Failed to install completion script: ${error instanceof Error ? error.message : String(error)}`,
105
+ message: COMPLETION_MESSAGES.fishFailedToInstall(error instanceof Error ? error.message : String(error)),
105
106
  };
106
107
  }
107
108
  }
@@ -122,20 +123,20 @@ export class FishInstaller {
122
123
  catch {
123
124
  return {
124
125
  success: false,
125
- message: 'Completion script is not installed',
126
+ message: COMPLETION_MESSAGES.fishNotInstalled,
126
127
  };
127
128
  }
128
129
  // Remove the completion script
129
130
  await fs.unlink(targetPath);
130
131
  return {
131
132
  success: true,
132
- message: 'Completion script uninstalled successfully',
133
+ message: COMPLETION_MESSAGES.fishUninstalled,
133
134
  };
134
135
  }
135
136
  catch (error) {
136
137
  return {
137
138
  success: false,
138
- message: `Failed to uninstall completion script: ${error instanceof Error ? error.message : String(error)}`,
139
+ message: COMPLETION_MESSAGES.fishFailedToUninstall(error instanceof Error ? error.message : String(error)),
139
140
  };
140
141
  }
141
142
  }
@@ -29,8 +29,7 @@ export class PowerShellInstaller {
29
29
  }
30
30
  // UTF-16 BE BOM: FE FF — not natively supported by Node
31
31
  if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
32
- throw new Error('File is encoded as UTF-16 BE which is not supported. ' +
33
- 'Please re-save as UTF-8 or UTF-16 LE, then retry.');
32
+ throw new Error(COMPLETION_MESSAGES.powershellUtf16BEUnsupported);
34
33
  }
35
34
  // UTF-8 BOM: EF BB BF
36
35
  if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
@@ -274,10 +273,10 @@ export class PowerShellInstaller {
274
273
  return {
275
274
  success: true,
276
275
  installedPath: targetPath,
277
- message: 'Completion script is already installed (up to date)',
276
+ message: COMPLETION_MESSAGES.powershellAlreadyInstalled,
278
277
  instructions: [
279
- 'The completion script is already installed and up to date.',
280
- 'If completions are not working, try restarting PowerShell or run: . $PROFILE',
278
+ COMPLETION_MESSAGES.powershellAlreadyInstalledDetail,
279
+ COMPLETION_MESSAGES.powershellAlreadyInstalledHint,
281
280
  ],
282
281
  };
283
282
  }
@@ -315,13 +314,13 @@ export class PowerShellInstaller {
315
314
  let message;
316
315
  if (isUpdate) {
317
316
  message = backupPath
318
- ? 'Completion script updated successfully (previous version backed up)'
319
- : 'Completion script updated successfully';
317
+ ? COMPLETION_MESSAGES.powershellUpdatedWithBackup
318
+ : COMPLETION_MESSAGES.powershellUpdated;
320
319
  }
321
320
  else {
322
321
  message = profileConfigured
323
- ? 'Completion script installed and PowerShell profile configured successfully'
324
- : 'Completion script installed successfully for PowerShell';
322
+ ? COMPLETION_MESSAGES.powershellInstalledWithProfile
323
+ : COMPLETION_MESSAGES.powershellInstalled;
325
324
  }
326
325
  return {
327
326
  success: true,
@@ -335,7 +334,7 @@ export class PowerShellInstaller {
335
334
  catch (error) {
336
335
  return {
337
336
  success: false,
338
- message: `Failed to install completion script: ${error instanceof Error ? error.message : String(error)}`,
337
+ message: COMPLETION_MESSAGES.powershellFailedToInstall(error instanceof Error ? error.message : String(error)),
339
338
  };
340
339
  }
341
340
  }
@@ -348,16 +347,16 @@ export class PowerShellInstaller {
348
347
  generateInstructions(installedPath) {
349
348
  const profilePath = this.getProfilePath();
350
349
  return [
351
- 'Completion script installed successfully.',
350
+ COMPLETION_MESSAGES.powershellScriptInstalled,
352
351
  '',
353
- `To enable completions, add the following to your PowerShell profile (${profilePath}):`,
352
+ COMPLETION_MESSAGES.powershellEnableCompletions(profilePath),
354
353
  '',
355
- ' # Source BR-OpenSpec completions',
354
+ ` ${COMPLETION_MESSAGES.powershellSourceComment}`,
356
355
  ` if (Test-Path "${installedPath}") {`,
357
356
  ` . "${installedPath}"`,
358
357
  ' }',
359
358
  '',
360
- 'Then restart PowerShell or run: . $PROFILE',
359
+ COMPLETION_MESSAGES.powershellThenRestart,
361
360
  ];
362
361
  }
363
362
  /**
@@ -377,7 +376,7 @@ export class PowerShellInstaller {
377
376
  catch {
378
377
  return {
379
378
  success: false,
380
- message: 'Completion script is not installed',
379
+ message: COMPLETION_MESSAGES.powershellNotInstalled,
381
380
  };
382
381
  }
383
382
  // Remove the completion script
@@ -386,13 +385,13 @@ export class PowerShellInstaller {
386
385
  await this.removeProfileConfig();
387
386
  return {
388
387
  success: true,
389
- message: 'Completion script uninstalled successfully',
388
+ message: COMPLETION_MESSAGES.powershellUninstalled,
390
389
  };
391
390
  }
392
391
  catch (error) {
393
392
  return {
394
393
  success: false,
395
- message: `Failed to uninstall completion script: ${error instanceof Error ? error.message : String(error)}`,
394
+ message: COMPLETION_MESSAGES.powershellFailedToUninstall(error instanceof Error ? error.message : String(error)),
396
395
  };
397
396
  }
398
397
  }
@@ -59,14 +59,6 @@ export declare class ZshInstaller {
59
59
  * @returns true if .zshrc exists and has markers
60
60
  */
61
61
  private hasZshrcConfig;
62
- /**
63
- * Check if fpath configuration is needed for a given directory
64
- * Used to verify if Oh My Zsh (or other) completions directory is already in fpath
65
- *
66
- * @param completionsDir - Directory to check for in fpath
67
- * @returns true if configuration is needed, false if directory is already referenced
68
- */
69
- private needsFpathConfig;
70
62
  /**
71
63
  * Remove .zshrc configuration
72
64
  * Used during uninstallation
@@ -148,26 +148,6 @@ export class ZshInstaller {
148
148
  return false;
149
149
  }
150
150
  }
151
- /**
152
- * Check if fpath configuration is needed for a given directory
153
- * Used to verify if Oh My Zsh (or other) completions directory is already in fpath
154
- *
155
- * @param completionsDir - Directory to check for in fpath
156
- * @returns true if configuration is needed, false if directory is already referenced
157
- */
158
- async needsFpathConfig(completionsDir) {
159
- try {
160
- const zshrcPath = this.getZshrcPath();
161
- const content = await fs.readFile(zshrcPath, 'utf-8');
162
- // Check if fpath already includes this directory
163
- return !content.includes(completionsDir);
164
- }
165
- catch (error) {
166
- // If we can't read .zshrc, assume config is needed
167
- console.debug(`Unable to read .zshrc to check fpath config: ${error instanceof Error ? error.message : String(error)}`);
168
- return true;
169
- }
170
- }
171
151
  /**
172
152
  * Remove .zshrc configuration
173
153
  * Used during uninstallation
@@ -256,18 +236,10 @@ export class ZshInstaller {
256
236
  const backupPath = isUpdate ? await this.backupExistingFile(targetPath) : undefined;
257
237
  // Write the completion script
258
238
  await fs.writeFile(targetPath, completionScript, 'utf-8');
259
- // Auto-configure .zshrc
239
+ // Auto-configure .zshrc for standard Zsh only.
240
+ // Oh My Zsh loads custom/completions and runs compinit itself.
260
241
  let zshrcConfigured = false;
261
- if (isOhMyZsh) {
262
- // For Oh My Zsh, verify that custom/completions is in fpath
263
- // If not, add it to .zshrc
264
- const needsConfig = await this.needsFpathConfig(targetDir);
265
- if (needsConfig) {
266
- zshrcConfigured = await this.configureZshrc(targetDir);
267
- }
268
- }
269
- else {
270
- // Standard Zsh always needs .zshrc configuration
242
+ if (!isOhMyZsh) {
271
243
  zshrcConfigured = await this.configureZshrc(targetDir);
272
244
  }
273
245
  // Generate instructions (only if .zshrc wasn't auto-configured)
@@ -392,7 +364,7 @@ export class ZshInstaller {
392
364
  if (!scriptRemoved && !zshrcWasPresent) {
393
365
  return {
394
366
  success: false,
395
- message: 'Completion script is not installed',
367
+ message: COMPLETION_MESSAGES.zshNotInstalled,
396
368
  };
397
369
  }
398
370
  const messages = [];
@@ -23,12 +23,13 @@ export const AI_TOOLS = [
23
23
  { name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
24
24
  { name: 'Junie', value: 'junie', available: true, successLabel: 'Junie', skillsDir: '.junie' },
25
25
  { name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
26
- { name: 'Kimi Code CLI', value: 'kimi', available: true, successLabel: 'Kimi Code CLI', skillsDir: '.kimi' },
26
+ { name: 'Kimi CLI', value: 'kimi', available: true, successLabel: 'Kimi CLI', skillsDir: '.kimi' },
27
27
  { name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
28
+ { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
29
+ { name: 'Mistral Vibe', value: 'vibe', available: true, successLabel: 'Mistral Vibe', skillsDir: '.vibe' },
28
30
  { name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
29
31
  { name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
30
32
  { name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
31
- { name: 'Lingma', value: 'lingma', available: true, successLabel: 'Lingma', skillsDir: '.lingma' },
32
33
  { name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
33
34
  { name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
34
35
  { name: 'Trae', value: 'trae', available: true, successLabel: 'Trae', skillsDir: '.trae' },
@@ -25,7 +25,12 @@ export declare function getGlobalConfigDir(): string;
25
25
  * - Unix/macOS fallback: ~/.local/share/openspec/
26
26
  * - Windows fallback: %LOCALAPPDATA%/openspec/
27
27
  */
28
- export declare function getGlobalDataDir(): string;
28
+ export interface GlobalDataDirOptions {
29
+ env?: NodeJS.ProcessEnv;
30
+ platform?: NodeJS.Platform;
31
+ homedir?: string;
32
+ }
33
+ export declare function getGlobalDataDir(options?: GlobalDataDirOptions): string;
29
34
  /**
30
35
  * Gets the path to the global config file.
31
36
  */
@@ -36,32 +36,31 @@ export function getGlobalConfigDir() {
36
36
  // Unix/macOS fallback: ~/.config
37
37
  return path.join(os.homedir(), '.config', GLOBAL_CONFIG_DIR_NAME);
38
38
  }
39
- /**
40
- * Gets the global data directory path following XDG Base Directory Specification.
41
- * Used for user data like schema overrides.
42
- *
43
- * - All platforms: $XDG_DATA_HOME/openspec/ if XDG_DATA_HOME is set
44
- * - Unix/macOS fallback: ~/.local/share/openspec/
45
- * - Windows fallback: %LOCALAPPDATA%/openspec/
46
- */
47
- export function getGlobalDataDir() {
39
+ function joinGlobalDataPath(platform, ...segments) {
40
+ return platform === 'win32'
41
+ ? path.win32.join(...segments)
42
+ : path.posix.join(...segments);
43
+ }
44
+ export function getGlobalDataDir(options = {}) {
45
+ const env = options.env ?? process.env;
46
+ const platform = options.platform ?? os.platform();
48
47
  // XDG_DATA_HOME takes precedence on all platforms when explicitly set
49
- const xdgDataHome = process.env.XDG_DATA_HOME;
48
+ const xdgDataHome = env.XDG_DATA_HOME;
50
49
  if (xdgDataHome) {
51
- return path.join(xdgDataHome, GLOBAL_DATA_DIR_NAME);
50
+ return joinGlobalDataPath(platform, xdgDataHome, GLOBAL_DATA_DIR_NAME);
52
51
  }
53
- const platform = os.platform();
52
+ const homedir = options.homedir ?? os.homedir();
54
53
  if (platform === 'win32') {
55
54
  // Windows: use %LOCALAPPDATA%
56
- const localAppData = process.env.LOCALAPPDATA;
55
+ const localAppData = env.LOCALAPPDATA;
57
56
  if (localAppData) {
58
- return path.join(localAppData, GLOBAL_DATA_DIR_NAME);
57
+ return joinGlobalDataPath(platform, localAppData, GLOBAL_DATA_DIR_NAME);
59
58
  }
60
59
  // Fallback for Windows if LOCALAPPDATA is not set
61
- return path.join(os.homedir(), 'AppData', 'Local', GLOBAL_DATA_DIR_NAME);
60
+ return joinGlobalDataPath(platform, homedir, 'AppData', 'Local', GLOBAL_DATA_DIR_NAME);
62
61
  }
63
62
  // Unix/macOS fallback: ~/.local/share
64
- return path.join(os.homedir(), '.local', 'share', GLOBAL_DATA_DIR_NAME);
63
+ return joinGlobalDataPath(platform, homedir, '.local', 'share', GLOBAL_DATA_DIR_NAME);
65
64
  }
66
65
  /**
67
66
  * Gets the path to the global config file.