oxe-cc 0.3.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/.cursor/commands/oxe-discuss.md +5 -0
- package/.cursor/commands/oxe-execute.md +5 -0
- package/.cursor/commands/oxe-help.md +5 -0
- package/.cursor/commands/oxe-next.md +5 -0
- package/.cursor/commands/oxe-plan.md +5 -0
- package/.cursor/commands/oxe-quick.md +5 -0
- package/.cursor/commands/oxe-scan.md +5 -0
- package/.cursor/commands/oxe-spec.md +5 -0
- package/.cursor/commands/oxe-verify.md +5 -0
- package/.cursor/rules/oxe-workflow.mdc +15 -0
- package/.github/copilot-instructions.md +37 -0
- package/.github/prompts/oxe-discuss.prompt.md +10 -0
- package/.github/prompts/oxe-execute.prompt.md +12 -0
- package/.github/prompts/oxe-help.prompt.md +9 -0
- package/.github/prompts/oxe-next.prompt.md +9 -0
- package/.github/prompts/oxe-plan.prompt.md +12 -0
- package/.github/prompts/oxe-quick.prompt.md +12 -0
- package/.github/prompts/oxe-scan.prompt.md +12 -0
- package/.github/prompts/oxe-spec.prompt.md +12 -0
- package/.github/prompts/oxe-verify.prompt.md +12 -0
- package/.github/workflows/ci.yml +18 -0
- package/AGENTS.md +11 -0
- package/LICENSE +674 -0
- package/README.md +206 -0
- package/bin/banner.txt +5 -0
- package/bin/oxe-cc.js +473 -0
- package/commands/oxe/discuss.md +16 -0
- package/commands/oxe/execute.md +16 -0
- package/commands/oxe/help.md +11 -0
- package/commands/oxe/next.md +12 -0
- package/commands/oxe/plan.md +15 -0
- package/commands/oxe/quick.md +16 -0
- package/commands/oxe/scan.md +16 -0
- package/commands/oxe/spec.md +14 -0
- package/commands/oxe/verify.md +15 -0
- package/oxe/templates/CONFIG.md +12 -0
- package/oxe/templates/PLAN.template.md +38 -0
- package/oxe/templates/SPEC.template.md +39 -0
- package/oxe/templates/STATE.md +30 -0
- package/oxe/templates/SUMMARY.template.md +20 -0
- package/oxe/templates/config.template.json +6 -0
- package/oxe/workflows/discuss.md +31 -0
- package/oxe/workflows/execute.md +28 -0
- package/oxe/workflows/help.md +50 -0
- package/oxe/workflows/next.md +22 -0
- package/oxe/workflows/plan.md +52 -0
- package/oxe/workflows/quick.md +32 -0
- package/oxe/workflows/scan.md +36 -0
- package/oxe/workflows/spec.md +37 -0
- package/oxe/workflows/verify.md +34 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# oxe-build
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/oxe-cc)
|
|
4
|
+
[](https://www.npmjs.com/package/oxe-cc)
|
|
5
|
+
|
|
6
|
+
*(O badge de versão só reflete uma release real depois de `npm publish` concluído com sucesso.)*
|
|
7
|
+
|
|
8
|
+
Fluxo **spec-driven** enxuto (inspirado em context engineering / GSD), com prefixo **OXE** e artefatos em **`.oxe/`**.
|
|
9
|
+
|
|
10
|
+
Os alvos **iniciais** são **Cursor** e **GitHub Copilot** (VS Code e IDEs compatíveis). Outros clientes podem usar os mesmos workflows em Markdown.
|
|
11
|
+
|
|
12
|
+
O nome previsto no npm é **`oxe-cc`**. Confirma se já está publicado: `npm view oxe-cc version`. Se responder **404**, o pacote **ainda não existe** no registry — usa uma das opções abaixo até publicares com sucesso.
|
|
13
|
+
|
|
14
|
+
## Instalação (escolhe uma)
|
|
15
|
+
|
|
16
|
+
Precisas de **Node.js 18+**. O executável chama-se **`oxe-cc`**.
|
|
17
|
+
|
|
18
|
+
### A) Repositório local (sem npm registry)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd C:\caminho\para\oxe-build
|
|
22
|
+
npm link
|
|
23
|
+
cd C:\caminho\para\teu-projeto
|
|
24
|
+
npm link oxe-cc
|
|
25
|
+
oxe-cc
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Ou, sem `link`, apontando ao script:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node C:\caminho\para\oxe-build\bin\oxe-cc.js
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### B) Depois de publicado no npm
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd /caminho/do/teu-projeto
|
|
38
|
+
npx oxe-cc@latest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Página do pacote (quando existir): [npmjs.com/package/oxe-cc](https://www.npmjs.com/package/oxe-cc). Versões: [aba Versions](https://www.npmjs.com/package/oxe-cc?activeTab=versions).
|
|
42
|
+
|
|
43
|
+
### C) Erro `npm error 404` / `'oxe-cc@latest' is not in this registry`
|
|
44
|
+
|
|
45
|
+
Significa que **nenhuma versão** de `oxe-cc` foi aceite no `registry.npmjs.org`. Causas frequentes:
|
|
46
|
+
|
|
47
|
+
1. **`npm publish` falhou** (ex. **403** — conta com **2FA obrigatória** para publicar). Ativa 2FA em [npm → Security](https://www.npmjs.com/settings/~/security) e volta a correr `npm publish --access public` dentro da pasta do repo, já autenticado (`npm login`).
|
|
48
|
+
2. **Nome diferente** — publicaste com **scope** (ex. `@propagno/oxe-cc`). Nesse caso usa `npx @propagno/oxe-cc@latest` (ajusta ao `name` real do `package.json`).
|
|
49
|
+
3. **Outro registry** — confirma: `npm config get registry` → deve ser `https://registry.npmjs.org/` para o público.
|
|
50
|
+
|
|
51
|
+
Até o 404 desaparecer, usa a opção **A** (`npm link` ou `node …/bin/oxe-cc.js`).
|
|
52
|
+
|
|
53
|
+
Opções úteis:
|
|
54
|
+
|
|
55
|
+
| Opção | Efeito |
|
|
56
|
+
|--------|--------|
|
|
57
|
+
| *(omissão)* | **Cursor + Copilot** + `oxe/` + `commands/oxe` + `AGENTS.md` |
|
|
58
|
+
| `--all`, `-a` | Garante Cursor e Copilot (mesmo efeito que omitir `--cursor` e `--copilot`) |
|
|
59
|
+
| `--dir <pasta>` | Instala noutro diretório em vez do cwd |
|
|
60
|
+
| `--cursor` | Só `.cursor/commands` e `.cursor/rules` |
|
|
61
|
+
| `--copilot` | Só `.github/copilot-instructions.md` e `.github/prompts` |
|
|
62
|
+
| `--vscode` | Copia também `.vscode/settings.json` (`chat.promptFiles`) |
|
|
63
|
+
| `--no-commands` | Não copia `commands/oxe/` |
|
|
64
|
+
| `--no-agents` | Não copia `AGENTS.md` |
|
|
65
|
+
| `--force` / `-f` | Sobrescreve ficheiros já existentes |
|
|
66
|
+
| `--dry-run` | Mostra o que faria sem escrever |
|
|
67
|
+
| `--no-init-oxe` | Não cria `.oxe/` (STATE, config, codebase) após instalar |
|
|
68
|
+
| `--oxe-only` | Copia só a pasta `oxe/` (sem Cursor, Copilot, `commands/oxe`, `AGENTS.md`) |
|
|
69
|
+
| `-h` / `--help`, `-v` / `--version` | Ajuda e versão |
|
|
70
|
+
|
|
71
|
+
**Subcomandos:**
|
|
72
|
+
|
|
73
|
+
| Comando | Efeito |
|
|
74
|
+
|---------|--------|
|
|
75
|
+
| `oxe-cc doctor [dir]` | Node, workflows vs pacote, JSON válido em `.oxe/config.json`, mapa scan (7 ficheiros em `codebase/`) |
|
|
76
|
+
| `oxe-cc init-oxe [dir]` | Só `.oxe/`: STATE, `config.json` (template), pasta `codebase/` |
|
|
77
|
+
|
|
78
|
+
A pasta **`oxe/`** (workflows + templates) é **sempre** copiada na instalação normal. `--cursor` / `--copilot` apenas controlam se entram ficheiros em `.cursor/` e `.github/`.
|
|
79
|
+
|
|
80
|
+
Instalação global (só depois do pacote existir no npm): `npm install -g oxe-cc`.
|
|
81
|
+
|
|
82
|
+
### Nova versão no npm (mantenedores)
|
|
83
|
+
|
|
84
|
+
1. Atualiza **`version`** em `package.json` (semver).
|
|
85
|
+
2. Garante **`repository`**, **`homepage`** e **`bugs`** corretos para o teu GitHub.
|
|
86
|
+
3. Conta npm com **2FA** ativa: `npm login`, depois **`npm publish --access public`**.
|
|
87
|
+
4. O script **`prepublishOnly`** corre testes + `scan:assets` + `--version` antes do upload.
|
|
88
|
+
|
|
89
|
+
### Fork com outro nome no npm
|
|
90
|
+
|
|
91
|
+
1. Edita `package.json`: **`name`** (ex. `@tua-org/oxe-cc`) e **`repository.url`**.
|
|
92
|
+
2. `npm publish --access public` (scopes `@org/...` precisam de `--access public` na primeira publicação).
|
|
93
|
+
|
|
94
|
+
### CLI com nome personalizado
|
|
95
|
+
|
|
96
|
+
No `package.json`, o binário é o mapa **`bin`**: a chave é o comando no terminal, o valor é o script.
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
"bin": {
|
|
100
|
+
"oxe-cc": "bin/oxe-cc.js",
|
|
101
|
+
"meu-oxe": "bin/oxe-cc.js"
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Depois de publicar, o comando extra fica disponível **globalmente** com `npm i -g nome-do-pacote` como `meu-oxe`. Com **npx**, o pacote continua a identificar-se pelo **`name`** em `package.json`; para correr um binário que não é o nome do pacote, usa por exemplo `npx -p oxe-cc meu-oxe` (ajusta `oxe-cc` / `meu-oxe` aos teus nomes).
|
|
106
|
+
|
|
107
|
+
O ficheiro do script (`bin/oxe-cc.js`) pode ser renomeado desde que atualizes o caminho no mapa `bin`.
|
|
108
|
+
|
|
109
|
+
**Desenvolvimento local sem publicar:** na pasta `oxe-build`, `npm link` e no projeto alvo `npm link oxe-cc`, depois `oxe-cc`; ou `node /caminho/para/oxe-build/bin/oxe-cc.js`. **Testes:** `npm test`. **Scan de assets (padrões tipo segredo em markdown):** `npm run scan:assets`.
|
|
110
|
+
|
|
111
|
+
### Imagem / banner no início do CLI
|
|
112
|
+
|
|
113
|
+
Ao correr `oxe-cc` (instalar, `doctor`, `init-oxe`, `--help`), aparece um **cabeçalho ASCII** vindo de [`bin/banner.txt`](bin/banner.txt). Podes **personalizar** editando esse ficheiro no teu fork antes de publicar no npm.
|
|
114
|
+
|
|
115
|
+
- Placeholder **`{version}`** — substituído pela versão do `package.json` (ex. `v0.3.0`).
|
|
116
|
+
- **Cores:** em terminal TTY, o corpo usa ciano + negrito; a linha com a versão fica mais discreta. Define **`NO_COLOR`** (ou `FORCE_COLOR=0`) para saída só texto.
|
|
117
|
+
- **Desligar o banner:** variável de ambiente **`OXE_NO_BANNER=1`** (útil em CI ou testes).
|
|
118
|
+
- **`--version`** não mostra banner — só uma linha `oxe-cc v…` para scripts.
|
|
119
|
+
|
|
120
|
+
Para um “logo” mais elaborado, substitui o conteúdo de `banner.txt` pelo teu ASCII art (várias linhas, largura ~60 colunas costuma caber bem). Não é suportada imagem bitmap no terminal; para isso seria preciso outra ferramenta (ex. `terminal-image`) e dependências extra.
|
|
121
|
+
|
|
122
|
+
## Fonte única
|
|
123
|
+
|
|
124
|
+
Os passos detalhados vivem em **`oxe/workflows/`** (`scan.md`, `spec.md`, `discuss.md`, `plan.md`, `quick.md`, `execute.md`, `verify.md`, `next.md`, `help.md`). Comandos Cursor, prompts Copilot e `commands/oxe/*` **delegam** para esses ficheiros.
|
|
125
|
+
|
|
126
|
+
Por omissão, após instalar, o CLI cria **`.oxe/`** mínimo: `STATE.md`, **`config.json`** (a partir de `oxe/templates/config.template.json`) e pasta **`codebase/`** — exceto com `--no-init-oxe`. Documentação das chaves: **`oxe/templates/CONFIG.md`**.
|
|
127
|
+
|
|
128
|
+
## Cursor
|
|
129
|
+
|
|
130
|
+
| Slash | Workflow |
|
|
131
|
+
|-------|----------|
|
|
132
|
+
| `/oxe-scan` | `oxe/workflows/scan.md` |
|
|
133
|
+
| `/oxe-spec` | `oxe/workflows/spec.md` |
|
|
134
|
+
| `/oxe-discuss` | `oxe/workflows/discuss.md` |
|
|
135
|
+
| `/oxe-plan` | `oxe/workflows/plan.md` |
|
|
136
|
+
| `/oxe-quick` | `oxe/workflows/quick.md` |
|
|
137
|
+
| `/oxe-execute` | `oxe/workflows/execute.md` |
|
|
138
|
+
| `/oxe-verify` | `oxe/workflows/verify.md` |
|
|
139
|
+
| `/oxe-next` | `oxe/workflows/next.md` |
|
|
140
|
+
| `/oxe-help` | `oxe/workflows/help.md` |
|
|
141
|
+
|
|
142
|
+
Ficheiros: `.cursor/commands/oxe-*.md` e regra `.cursor/rules/oxe-workflow.mdc`.
|
|
143
|
+
|
|
144
|
+
## GitHub Copilot
|
|
145
|
+
|
|
146
|
+
1. **Instruções do repositório** — [`.github/copilot-instructions.md`](.github/copilot-instructions.md): ativadas automaticamente no chat quando o repositório está em contexto (ver [documentação](https://docs.github.com/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot)).
|
|
147
|
+
2. **Prompt files** — [`.github/prompts/*.prompt.md`](.github/prompts/): no chat, escreve `/` e escolhe por exemplo **`oxe-scan`**, **`oxe-quick`**, **`oxe-execute`**, etc. (o `name` no frontmatter define o comando). Cada prompt referencia `oxe/workflows/<passo>.md` na **raiz do repo em contexto**.
|
|
148
|
+
|
|
149
|
+
Este repo inclui [`.vscode/settings.json`](.vscode/settings.json) com `"chat.promptFiles": true` para expor a pasta `.github/prompts`. Podes copiar essa definição para o teu `settings.json` global se preferires.
|
|
150
|
+
|
|
151
|
+
3. **Agentes** — [`AGENTS.md`](AGENTS.md) resume o pacote para modos que leem instruções de agente no repo.
|
|
152
|
+
|
|
153
|
+
## Fluxo recomendado
|
|
154
|
+
|
|
155
|
+
**Completo:** 1. **scan** (7 ficheiros em `codebase/`, incl. CONVENTIONS + CONCERNS) → 2. **spec** → 3. **discuss** (opcional; recomendado se `discuss_before_plan` em `.oxe/config.json`) → 4. **plan** (secção **Replanejamento** em `--replan`) → 5. **execute** (opcional) → 6. implementar → 7. **verify** (rascunho de commit + checklist PR se ativo em config) → 8. **next**.
|
|
156
|
+
|
|
157
|
+
**Rápido (tarefas pequenas):** **quick** gera `.oxe/QUICK.md` com passos curtos + verificação; depois **execute** (sobre o QUICK) ou implementação direta e **verify**. Promover a spec/plan se o trabalho crescer (muitos ficheiros, API pública, segurança).
|
|
158
|
+
|
|
159
|
+
## Artefatos (`.oxe/` no projeto alvo)
|
|
160
|
+
|
|
161
|
+
| Caminho | Conteúdo |
|
|
162
|
+
|---------|----------|
|
|
163
|
+
| `.oxe/STATE.md` | Fase, último scan, próximo passo |
|
|
164
|
+
| `.oxe/config.json` | Opcional (criado no bootstrap): `discuss_before_plan`, texto de verify pós-sucesso, comando de teste por defeito — ver `oxe/templates/CONFIG.md` |
|
|
165
|
+
| `.oxe/codebase/*.md` | Mapa: OVERVIEW, STACK, STRUCTURE, TESTING, INTEGRATIONS, **CONVENTIONS**, **CONCERNS** |
|
|
166
|
+
| `.oxe/SPEC.md` | Especificação |
|
|
167
|
+
| `.oxe/DISCUSS.md` | Perguntas e decisões antes do plano (opcional) |
|
|
168
|
+
| `.oxe/PLAN.md` | Plano com **Verificar** por tarefa + secção **Replanejamento** |
|
|
169
|
+
| `.oxe/QUICK.md` | Modo rápido: passos curtos + verificar |
|
|
170
|
+
| `.oxe/VERIFY.md` | Resultado das verificações |
|
|
171
|
+
| `.oxe/SUMMARY.md` | Resumo de sessão / contexto para replan (opcional) |
|
|
172
|
+
|
|
173
|
+
Templates: **`oxe/templates/`** (`STATE.md`, `config.template.json`, `CONFIG.md`, `SPEC.template.md`, `PLAN.template.md`, `SUMMARY.template.md`).
|
|
174
|
+
|
|
175
|
+
Neste repositório, **`.oxe/` está no `.gitignore`** para não versionar scans locais do *oxe-build*. No teu produto, remove ou ajusta essa regra se quiseres commitar `.oxe/` com a equipa.
|
|
176
|
+
|
|
177
|
+
## Usar noutro projeto
|
|
178
|
+
|
|
179
|
+
- Com pacote no npm: **`npx oxe-cc@latest`** na raiz do repo alvo, ou **`npx oxe-cc@<versão>`** (versões na [página do pacote](https://www.npmjs.com/package/oxe-cc?activeTab=versions) quando existir).
|
|
180
|
+
- Sem pacote no npm: **`npm link oxe-cc`** (após `npm link` no clone do `oxe-build`) ou **`node …/oxe-build/bin/oxe-cc.js`**.
|
|
181
|
+
|
|
182
|
+
Alternativa manual: copia os mesmos caminhos que o instalador usa (`oxe/`, `.cursor/`, `.github/`, `commands/oxe`, `AGENTS.md`, opcionalmente `.vscode/settings.json` ou só `"chat.promptFiles": true` nas definições).
|
|
183
|
+
|
|
184
|
+
`commands/oxe/*.md` mantém frontmatter estilo GSD (`name: oxe:scan`, …) para ferramentas que importam comandos nesse formato.
|
|
185
|
+
|
|
186
|
+
## Estrutura deste repositório
|
|
187
|
+
|
|
188
|
+
| Pasta / ficheiro | Função |
|
|
189
|
+
|------------------|--------|
|
|
190
|
+
| `bin/oxe-cc.js` | CLI: instalar, `doctor`, `init-oxe` |
|
|
191
|
+
| `bin/banner.txt` | Cabeçalho ASCII ao arrancar o CLI (`{version}`) |
|
|
192
|
+
| `package.json` | Metadados npm (`oxe-cc`, `files`, `bin`) |
|
|
193
|
+
| `oxe/workflows/` | Workflows canónicos (fonte única) |
|
|
194
|
+
| `oxe/templates/` | Modelos (STATE, config, SPEC, PLAN, SUMMARY, CONFIG.md) |
|
|
195
|
+
| `scripts/oxe-assets-scan.cjs` | Verificação leve de padrões sensíveis em markdown (CI / `npm run scan:assets`) |
|
|
196
|
+
| `.github/workflows/ci.yml` | `npm test` + `scan:assets` |
|
|
197
|
+
| `.cursor/commands/` | Slash commands Cursor |
|
|
198
|
+
| `.cursor/rules/` | Regras do projeto Cursor |
|
|
199
|
+
| `.github/copilot-instructions.md` | Instruções Copilot no repo |
|
|
200
|
+
| `.github/prompts/` | Ficheiros `*.prompt.md` (`/oxe-scan`, …) |
|
|
201
|
+
| `commands/oxe/` | Comandos com frontmatter estilo GSD |
|
|
202
|
+
| `AGENTS.md` | Resumo para agentes (ex. Copilot) |
|
|
203
|
+
|
|
204
|
+
## Licença
|
|
205
|
+
|
|
206
|
+
[GPL-3.0](LICENSE) — ver [LICENSE](LICENSE).
|
package/bin/banner.txt
ADDED
package/bin/oxe-cc.js
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OXE — install workflows into a target project; bootstrap `.oxe/`; doctor.
|
|
4
|
+
* Usage:
|
|
5
|
+
* npx oxe-cc [options] [target-dir]
|
|
6
|
+
* npx oxe-cc doctor [options] [target-dir]
|
|
7
|
+
* npx oxe-cc init-oxe [options] [target-dir]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
const cyan = '\x1b[36m';
|
|
16
|
+
const green = '\x1b[32m';
|
|
17
|
+
const yellow = '\x1b[33m';
|
|
18
|
+
const dim = '\x1b[2m';
|
|
19
|
+
const red = '\x1b[31m';
|
|
20
|
+
const bold = '\x1b[1m';
|
|
21
|
+
const reset = '\x1b[0m';
|
|
22
|
+
|
|
23
|
+
/** Plain banner if banner.txt is missing (keep in sync with bin/banner.txt style). */
|
|
24
|
+
const DEFAULT_BANNER = ` .============================================.
|
|
25
|
+
| OXE · spec-driven workflow CLI |
|
|
26
|
+
| Cursor · GitHub Copilot |
|
|
27
|
+
'============================================'
|
|
28
|
+
v{version}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
function useAnsiColors() {
|
|
32
|
+
if (process.env.NO_COLOR) return false;
|
|
33
|
+
if (process.env.FORCE_COLOR === '0') return false;
|
|
34
|
+
return process.stdout.isTTY === true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Print branded header; skip with OXE_NO_BANNER=1. Not used for --version (scripts). */
|
|
38
|
+
function printBanner() {
|
|
39
|
+
if (process.env.OXE_NO_BANNER === '1' || process.env.OXE_NO_BANNER === 'true') return;
|
|
40
|
+
const color = useAnsiColors();
|
|
41
|
+
const ver = readPkgVersion();
|
|
42
|
+
const bannerPath = path.join(PKG_ROOT, 'bin', 'banner.txt');
|
|
43
|
+
let raw = DEFAULT_BANNER;
|
|
44
|
+
if (fs.existsSync(bannerPath)) {
|
|
45
|
+
try {
|
|
46
|
+
raw = fs.readFileSync(bannerPath, 'utf8');
|
|
47
|
+
} catch {
|
|
48
|
+
/* keep default */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const text = raw.replace(/\{version\}/g, ver).replace(/\r\n/g, '\n').trimEnd();
|
|
52
|
+
if (!color) {
|
|
53
|
+
console.log(text + '\n');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const lines = text.split('\n');
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.includes(`v${ver}`)) console.log(`${dim}${line}${reset}`);
|
|
59
|
+
else console.log(`${cyan}${bold}${line}${reset}`);
|
|
60
|
+
}
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @typedef {{ help: boolean, version: boolean, cursor: boolean, copilot: boolean, vscode: boolean, commands: boolean, agents: boolean, force: boolean, dryRun: boolean, dir: string, all: boolean, noInitOxe: boolean, oxeOnly: boolean, parseError: boolean, unknownFlag: string }} InstallOpts */
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string[]} argv
|
|
68
|
+
* @returns {InstallOpts & { restPositional: string[] }}
|
|
69
|
+
*/
|
|
70
|
+
function parseInstallArgs(argv) {
|
|
71
|
+
/** @type {InstallOpts & { restPositional: string[] }} */
|
|
72
|
+
const out = {
|
|
73
|
+
help: false,
|
|
74
|
+
version: false,
|
|
75
|
+
cursor: false,
|
|
76
|
+
copilot: false,
|
|
77
|
+
vscode: false,
|
|
78
|
+
commands: true,
|
|
79
|
+
agents: true,
|
|
80
|
+
force: false,
|
|
81
|
+
dryRun: false,
|
|
82
|
+
dir: process.cwd(),
|
|
83
|
+
all: false,
|
|
84
|
+
noInitOxe: false,
|
|
85
|
+
oxeOnly: false,
|
|
86
|
+
parseError: false,
|
|
87
|
+
unknownFlag: '',
|
|
88
|
+
restPositional: [],
|
|
89
|
+
};
|
|
90
|
+
for (let i = 0; i < argv.length; i++) {
|
|
91
|
+
const a = argv[i];
|
|
92
|
+
if (a === '-h' || a === '--help') out.help = true;
|
|
93
|
+
else if (a === '-v' || a === '--version') out.version = true;
|
|
94
|
+
else if (a === '--cursor') out.cursor = true;
|
|
95
|
+
else if (a === '--copilot') out.copilot = true;
|
|
96
|
+
else if (a === '--vscode') out.vscode = true;
|
|
97
|
+
else if (a === '--no-commands') out.commands = false;
|
|
98
|
+
else if (a === '--no-agents') out.agents = false;
|
|
99
|
+
else if (a === '--force' || a === '-f') out.force = true;
|
|
100
|
+
else if (a === '--dry-run') out.dryRun = true;
|
|
101
|
+
else if (a === '--all' || a === '-a') out.all = true;
|
|
102
|
+
else if (a === '--no-init-oxe') out.noInitOxe = true;
|
|
103
|
+
else if (a === '--oxe-only') out.oxeOnly = true;
|
|
104
|
+
else if (a === '--dir' && argv[i + 1]) {
|
|
105
|
+
out.dir = path.resolve(argv[++i]);
|
|
106
|
+
} else if (!a.startsWith('-')) out.restPositional.push(a);
|
|
107
|
+
else {
|
|
108
|
+
out.parseError = true;
|
|
109
|
+
out.unknownFlag = a;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (out.oxeOnly) {
|
|
114
|
+
out.cursor = false;
|
|
115
|
+
out.copilot = false;
|
|
116
|
+
out.vscode = false;
|
|
117
|
+
out.commands = false;
|
|
118
|
+
out.agents = false;
|
|
119
|
+
} else if (out.all || (!out.cursor && !out.copilot)) {
|
|
120
|
+
out.cursor = true;
|
|
121
|
+
out.copilot = true;
|
|
122
|
+
}
|
|
123
|
+
if (out.restPositional.length) out.dir = path.resolve(out.restPositional[0]);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readPkgVersion() {
|
|
128
|
+
try {
|
|
129
|
+
const p = path.join(PKG_ROOT, 'package.json');
|
|
130
|
+
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
131
|
+
return j.version || '0.0.0';
|
|
132
|
+
} catch {
|
|
133
|
+
return '0.0.0';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readMinNode() {
|
|
138
|
+
try {
|
|
139
|
+
const p = path.join(PKG_ROOT, 'package.json');
|
|
140
|
+
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
141
|
+
const eng = j.engines && j.engines.node;
|
|
142
|
+
if (!eng || typeof eng !== 'string') return 18;
|
|
143
|
+
const m = eng.match(/>=?\s*(\d+)/);
|
|
144
|
+
return m ? parseInt(m[1], 10) : 18;
|
|
145
|
+
} catch {
|
|
146
|
+
return 18;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ensureDir(p) {
|
|
151
|
+
fs.mkdirSync(p, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @param {string} src @param {string} dest @param {{ dryRun: boolean }} opts */
|
|
155
|
+
function copyFile(src, dest, opts) {
|
|
156
|
+
if (opts.dryRun) {
|
|
157
|
+
console.log(`${dim}file${reset} ${src} → ${dest}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
ensureDir(path.dirname(dest));
|
|
161
|
+
fs.copyFileSync(src, dest);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @param {string} srcDir @param {string} destDir @param {{ dryRun: boolean, force: boolean }} opts */
|
|
165
|
+
function copyDir(srcDir, destDir, opts) {
|
|
166
|
+
if (!fs.existsSync(srcDir)) return;
|
|
167
|
+
ensureDir(destDir);
|
|
168
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
169
|
+
for (const e of entries) {
|
|
170
|
+
const s = path.join(srcDir, e.name);
|
|
171
|
+
const d = path.join(destDir, e.name);
|
|
172
|
+
if (e.isDirectory()) copyDir(s, d, opts);
|
|
173
|
+
else {
|
|
174
|
+
if (fs.existsSync(d) && !opts.force) {
|
|
175
|
+
console.log(`${dim}skip${reset} ${d} (exists, use --force)`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (opts.dryRun) console.log(`${dim}file${reset} ${s} → ${d}`);
|
|
179
|
+
else {
|
|
180
|
+
ensureDir(path.dirname(d));
|
|
181
|
+
fs.copyFileSync(s, d);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create `.oxe/STATE.md` from template and ensure `.oxe/codebase/` exists.
|
|
189
|
+
* @param {string} target
|
|
190
|
+
* @param {{ dryRun: boolean, force: boolean }} opts
|
|
191
|
+
*/
|
|
192
|
+
function bootstrapOxe(target, opts) {
|
|
193
|
+
const oxeDir = path.join(target, '.oxe');
|
|
194
|
+
const codebaseDir = path.join(oxeDir, 'codebase');
|
|
195
|
+
const stateSrc = path.join(PKG_ROOT, 'oxe', 'templates', 'STATE.md');
|
|
196
|
+
const stateDest = path.join(oxeDir, 'STATE.md');
|
|
197
|
+
const configSrc = path.join(PKG_ROOT, 'oxe', 'templates', 'config.template.json');
|
|
198
|
+
const configDest = path.join(oxeDir, 'config.json');
|
|
199
|
+
|
|
200
|
+
if (!fs.existsSync(stateSrc)) {
|
|
201
|
+
console.error(`${yellow}warn:${reset} template missing: ${stateSrc}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (opts.dryRun) {
|
|
206
|
+
console.log(`${dim}init${reset} ${oxeDir}/ (STATE.md, config.json, codebase/)`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ensureDir(codebaseDir);
|
|
211
|
+
|
|
212
|
+
if (!fs.existsSync(stateDest) || opts.force) {
|
|
213
|
+
copyFile(stateSrc, stateDest, { dryRun: false });
|
|
214
|
+
console.log(`${green}init${reset} ${stateDest}`);
|
|
215
|
+
} else {
|
|
216
|
+
console.log(`${dim}skip${reset} ${stateDest} (exists, use --force to replace)`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (fs.existsSync(configSrc)) {
|
|
220
|
+
if (!fs.existsSync(configDest) || opts.force) {
|
|
221
|
+
copyFile(configSrc, configDest, { dryRun: false });
|
|
222
|
+
console.log(`${green}init${reset} ${configDest}`);
|
|
223
|
+
} else {
|
|
224
|
+
console.log(`${dim}skip${reset} ${configDest} (exists, use --force to replace)`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @param {string} target */
|
|
230
|
+
function runDoctor(target) {
|
|
231
|
+
const v = process.versions.node;
|
|
232
|
+
const major = parseInt(v.split('.')[0], 10);
|
|
233
|
+
const minNode = readMinNode();
|
|
234
|
+
console.log(`${cyan}oxe-cc doctor${reset} — ${target}`);
|
|
235
|
+
console.log(`Node.js ${v} (require >= ${minNode})`);
|
|
236
|
+
if (major < minNode) {
|
|
237
|
+
console.log(`${red}FAIL${reset} Node.js version below package engines`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
console.log(`${green}OK${reset} Node.js`);
|
|
241
|
+
|
|
242
|
+
const wfPkg = path.join(PKG_ROOT, 'oxe', 'workflows');
|
|
243
|
+
const wfTgt = path.join(target, 'oxe', 'workflows');
|
|
244
|
+
if (!fs.existsSync(wfPkg)) {
|
|
245
|
+
console.log(`${red}FAIL${reset} package workflows missing: ${wfPkg}`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
const expected = fs
|
|
249
|
+
.readdirSync(wfPkg)
|
|
250
|
+
.filter((f) => f.endsWith('.md'))
|
|
251
|
+
.sort();
|
|
252
|
+
|
|
253
|
+
if (!fs.existsSync(wfTgt)) {
|
|
254
|
+
console.log(`${yellow}WARN${reset} Target has no oxe/workflows/ — run ${cyan}oxe-cc${reset} to install.`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const actual = fs
|
|
259
|
+
.readdirSync(wfTgt)
|
|
260
|
+
.filter((f) => f.endsWith('.md'))
|
|
261
|
+
.sort();
|
|
262
|
+
const missing = expected.filter((f) => !actual.includes(f));
|
|
263
|
+
const extra = actual.filter((f) => !expected.includes(f));
|
|
264
|
+
|
|
265
|
+
if (missing.length) {
|
|
266
|
+
console.log(`${red}FAIL${reset} Missing workflows vs package: ${missing.join(', ')}`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
if (extra.length) console.log(`${dim}Note:${reset} Extra workflows in target (ok for forks): ${extra.join(', ')}`);
|
|
270
|
+
console.log(`${green}OK${reset} oxe/workflows has all ${expected.length} package files`);
|
|
271
|
+
|
|
272
|
+
const oxeState = path.join(target, '.oxe', 'STATE.md');
|
|
273
|
+
if (fs.existsSync(oxeState)) console.log(`${green}OK${reset} .oxe/STATE.md present`);
|
|
274
|
+
else console.log(`${dim}Note:${reset} .oxe/STATE.md absent — run ${cyan}oxe-cc init-oxe${reset} or install without ${cyan}--no-init-oxe${reset}`);
|
|
275
|
+
|
|
276
|
+
const cfgPath = path.join(target, '.oxe', 'config.json');
|
|
277
|
+
if (fs.existsSync(cfgPath)) {
|
|
278
|
+
try {
|
|
279
|
+
JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
280
|
+
console.log(`${green}OK${reset} .oxe/config.json (valid JSON)`);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
console.log(`${red}FAIL${reset} .oxe/config.json invalid JSON: ${e.message}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
console.log(`${dim}Note:${reset} .oxe/config.json absent (optional — see oxe/templates/CONFIG.md)`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const cbDir = path.join(target, '.oxe', 'codebase');
|
|
290
|
+
const expectedMaps = [
|
|
291
|
+
'OVERVIEW.md',
|
|
292
|
+
'STACK.md',
|
|
293
|
+
'STRUCTURE.md',
|
|
294
|
+
'TESTING.md',
|
|
295
|
+
'INTEGRATIONS.md',
|
|
296
|
+
'CONVENTIONS.md',
|
|
297
|
+
'CONCERNS.md',
|
|
298
|
+
];
|
|
299
|
+
if (fs.existsSync(cbDir)) {
|
|
300
|
+
const missingMaps = expectedMaps.filter((f) => !fs.existsSync(path.join(cbDir, f)));
|
|
301
|
+
if (missingMaps.length) {
|
|
302
|
+
console.log(
|
|
303
|
+
`${yellow}Note:${reset} scan incomplete — missing under .oxe/codebase/: ${missingMaps.join(', ')} (run ${cyan}/oxe-scan${reset})`
|
|
304
|
+
);
|
|
305
|
+
} else {
|
|
306
|
+
console.log(`${green}OK${reset} .oxe/codebase/ has all ${expectedMaps.length} map files`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(`\n${green}Doctor finished.${reset}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function usage() {
|
|
314
|
+
console.log(`
|
|
315
|
+
${cyan}oxe-cc${reset} — install OXE workflows (Cursor + GitHub Copilot) into a project
|
|
316
|
+
|
|
317
|
+
${green}Usage:${reset}
|
|
318
|
+
npx oxe-cc@latest [options] [target-dir]
|
|
319
|
+
npx oxe-cc@latest --dir /path/to/project
|
|
320
|
+
npx oxe-cc doctor [options] [target-dir]
|
|
321
|
+
npx oxe-cc init-oxe [options] [target-dir]
|
|
322
|
+
|
|
323
|
+
${green}Install options:${reset}
|
|
324
|
+
--cursor Install .cursor/commands and .cursor/rules (default with --all)
|
|
325
|
+
--copilot Install .github/copilot-instructions.md and .github/prompts
|
|
326
|
+
--vscode Also copy .vscode/settings.json (chat.promptFiles)
|
|
327
|
+
--all, -a Cursor + Copilot (default when neither --cursor nor --copilot)
|
|
328
|
+
--no-commands Skip commands/oxe (Claude-style frontmatter)
|
|
329
|
+
--no-agents Skip AGENTS.md
|
|
330
|
+
--no-init-oxe Do not create .oxe/STATE.md + .oxe/codebase/ after install
|
|
331
|
+
--oxe-only Only copy oxe/ (skip Cursor, Copilot, commands, AGENTS.md)
|
|
332
|
+
--force, -f Overwrite existing files
|
|
333
|
+
--dry-run Print actions without writing
|
|
334
|
+
--dir <path> Target directory (default: cwd)
|
|
335
|
+
-h, --help
|
|
336
|
+
-v, --version
|
|
337
|
+
|
|
338
|
+
${green}Examples:${reset}
|
|
339
|
+
npx oxe-cc@latest
|
|
340
|
+
npx oxe-cc@latest ./my-app
|
|
341
|
+
npx oxe-cc@latest --cursor --dry-run
|
|
342
|
+
npx oxe-cc doctor
|
|
343
|
+
npx oxe-cc init-oxe --dir ./my-app
|
|
344
|
+
`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function runInstall(opts) {
|
|
348
|
+
const target = opts.dir;
|
|
349
|
+
if (!opts.dryRun && !fs.existsSync(target)) {
|
|
350
|
+
console.error(`${yellow}Target directory does not exist: ${target}${reset}`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
console.log(`${cyan}OXE${reset} install → ${green}${target}${reset}`);
|
|
355
|
+
if (opts.dryRun) console.log(`${yellow}(dry-run)${reset}`);
|
|
356
|
+
|
|
357
|
+
const copyOpts = { dryRun: opts.dryRun, force: opts.force };
|
|
358
|
+
|
|
359
|
+
copyDir(path.join(PKG_ROOT, 'oxe'), path.join(target, 'oxe'), copyOpts);
|
|
360
|
+
|
|
361
|
+
if (opts.cursor) {
|
|
362
|
+
const cCmd = path.join(PKG_ROOT, '.cursor', 'commands');
|
|
363
|
+
const cRules = path.join(PKG_ROOT, '.cursor', 'rules');
|
|
364
|
+
if (fs.existsSync(cCmd)) copyDir(cCmd, path.join(target, '.cursor', 'commands'), copyOpts);
|
|
365
|
+
if (fs.existsSync(cRules)) copyDir(cRules, path.join(target, '.cursor', 'rules'), copyOpts);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (opts.copilot) {
|
|
369
|
+
const gh = path.join(PKG_ROOT, '.github');
|
|
370
|
+
const inst = path.join(gh, 'copilot-instructions.md');
|
|
371
|
+
const prompts = path.join(gh, 'prompts');
|
|
372
|
+
if (fs.existsSync(inst)) {
|
|
373
|
+
const dest = path.join(target, '.github', 'copilot-instructions.md');
|
|
374
|
+
if (opts.dryRun) console.log(`${dim}file${reset} ${inst} → ${dest}`);
|
|
375
|
+
else {
|
|
376
|
+
if (fs.existsSync(dest) && !opts.force) console.log(`${dim}skip${reset} ${dest} (exists)`);
|
|
377
|
+
else copyFile(inst, dest, copyOpts);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (fs.existsSync(prompts)) copyDir(prompts, path.join(target, '.github', 'prompts'), copyOpts);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (opts.vscode) {
|
|
384
|
+
const vs = path.join(PKG_ROOT, '.vscode', 'settings.json');
|
|
385
|
+
if (fs.existsSync(vs)) {
|
|
386
|
+
const dest = path.join(target, '.vscode', 'settings.json');
|
|
387
|
+
if (opts.dryRun) console.log(`${dim}file${reset} ${vs} → ${dest}`);
|
|
388
|
+
else {
|
|
389
|
+
if (fs.existsSync(dest) && !opts.force) console.log(`${dim}skip${reset} ${dest} (exists)`);
|
|
390
|
+
else copyFile(vs, dest, copyOpts);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (opts.commands) {
|
|
396
|
+
const cmdSrc = path.join(PKG_ROOT, 'commands', 'oxe');
|
|
397
|
+
const cmdDest = path.join(target, 'commands', 'oxe');
|
|
398
|
+
if (fs.existsSync(cmdSrc)) copyDir(cmdSrc, cmdDest, copyOpts);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (opts.agents) {
|
|
402
|
+
const agents = path.join(PKG_ROOT, 'AGENTS.md');
|
|
403
|
+
if (fs.existsSync(agents)) {
|
|
404
|
+
const dest = path.join(target, 'AGENTS.md');
|
|
405
|
+
if (opts.dryRun) console.log(`${dim}file${reset} ${agents} → ${dest}`);
|
|
406
|
+
else if (fs.existsSync(dest) && !opts.force) console.log(`${dim}skip${reset} ${dest} (exists)`);
|
|
407
|
+
else copyFile(agents, dest, copyOpts);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!opts.noInitOxe) bootstrapOxe(target, { dryRun: opts.dryRun, force: opts.force });
|
|
412
|
+
|
|
413
|
+
console.log(
|
|
414
|
+
`\n${green}Done.${reset} Open the project in Cursor (${cyan}/oxe-scan${reset}) or VS Code + Copilot (prompt ${cyan}/oxe-scan${reset}).`
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function main() {
|
|
419
|
+
const argv = process.argv.slice(2);
|
|
420
|
+
let command = 'install';
|
|
421
|
+
if (argv[0] === 'doctor' || argv[0] === 'init-oxe') {
|
|
422
|
+
command = argv[0];
|
|
423
|
+
argv.shift();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const opts = parseInstallArgs(argv);
|
|
427
|
+
|
|
428
|
+
if (opts.version) {
|
|
429
|
+
console.log(`oxe-cc v${readPkgVersion()}`);
|
|
430
|
+
process.exit(0);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (opts.parseError) {
|
|
434
|
+
printBanner();
|
|
435
|
+
console.error(`${red}Unknown option:${reset} ${opts.unknownFlag}`);
|
|
436
|
+
usage();
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (opts.help) {
|
|
441
|
+
printBanner();
|
|
442
|
+
usage();
|
|
443
|
+
process.exit(0);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
printBanner();
|
|
447
|
+
|
|
448
|
+
const target = opts.dir;
|
|
449
|
+
if (command === 'doctor') {
|
|
450
|
+
if (!fs.existsSync(target)) {
|
|
451
|
+
console.error(`${yellow}Target directory does not exist: ${target}${reset}`);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
runDoctor(target);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (command === 'init-oxe') {
|
|
459
|
+
if (!opts.dryRun && !fs.existsSync(target)) {
|
|
460
|
+
console.error(`${yellow}Target directory does not exist: ${target}${reset}`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
console.log(`${cyan}OXE${reset} init-oxe → ${green}${target}${reset}`);
|
|
464
|
+
if (opts.dryRun) console.log(`${yellow}(dry-run)${reset}`);
|
|
465
|
+
bootstrapOxe(target, { dryRun: opts.dryRun, force: opts.force });
|
|
466
|
+
console.log(`\n${green}Done.${reset}`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
runInstall(opts);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
main();
|