sauron-cli 1.1.0 → 1.2.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/.sauron/.manifest.json +6 -0
- package/dist/index.js +193 -136
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
6
|
+
// src/features/init/init.command.ts
|
|
7
|
+
import path3 from "path";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import * as p from "@clack/prompts";
|
|
11
|
+
import * as Diff from "diff";
|
|
12
|
+
|
|
13
|
+
// src/features/init/init.service.ts
|
|
7
14
|
import fs2 from "fs-extra";
|
|
8
15
|
import path2 from "path";
|
|
9
|
-
import pc2 from "picocolors";
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
|
-
import * as p2 from "@clack/prompts";
|
|
12
16
|
|
|
13
|
-
// src/
|
|
17
|
+
// src/core/manifest.service.ts
|
|
14
18
|
import crypto from "crypto";
|
|
15
19
|
import fs from "fs-extra";
|
|
16
20
|
import path from "path";
|
|
@@ -35,80 +39,127 @@ async function saveManifest(targetDir, manifest) {
|
|
|
35
39
|
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
// src/
|
|
39
|
-
|
|
40
|
-
import pc from "picocolors";
|
|
41
|
-
import * as Diff from "diff";
|
|
42
|
-
async function resolveConflict(filePath, localContent, newContent, manifestHash) {
|
|
42
|
+
// src/core/merge.service.ts
|
|
43
|
+
function checkConflict(localContent, newContent, manifestHash) {
|
|
43
44
|
const localHash = generateHash(localContent);
|
|
44
45
|
if (manifestHash && localHash === manifestHash) {
|
|
45
|
-
return
|
|
46
|
+
return false;
|
|
46
47
|
}
|
|
47
48
|
if (localContent === newContent) {
|
|
48
|
-
return
|
|
49
|
+
return false;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
O seu agente de IA ou voc\xEA modificou este arquivo desde a \xFAltima instala\xE7\xE3o.`, "\u26A0\uFE0F CONFLITO DETECTADO");
|
|
52
|
-
let resolved = null;
|
|
53
|
-
while (!resolved) {
|
|
54
|
-
const action = await p.select({
|
|
55
|
-
message: `Como deseja proceder com ${pc.cyan(filePath)}?`,
|
|
56
|
-
options: [
|
|
57
|
-
{ value: "ours", label: "Preservar Local (Ours)", hint: "Mant\xE9m sua vers\xE3o e ignora o novo template" },
|
|
58
|
-
{ value: "theirs", label: "Sobrescrever (Theirs)", hint: "Destr\xF3i a sua vers\xE3o e aplica o template novo" },
|
|
59
|
-
{ value: "diff", label: "Auditar Diferen\xE7as (Diff Mode)", hint: "Mostra o que vai mudar visualmente" }
|
|
60
|
-
]
|
|
61
|
-
});
|
|
62
|
-
if (p.isCancel(action)) {
|
|
63
|
-
p.cancel("Opera\xE7\xE3o cancelada pelo usu\xE1rio durante a resolu\xE7\xE3o de conflito.");
|
|
64
|
-
process.exit(0);
|
|
65
|
-
}
|
|
66
|
-
if (action === "diff") {
|
|
67
|
-
const diffStr = renderDiff(localContent, newContent, filePath);
|
|
68
|
-
console.log("\n" + diffStr + "\n");
|
|
69
|
-
} else {
|
|
70
|
-
resolved = action;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return resolved;
|
|
51
|
+
return true;
|
|
74
52
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
53
|
+
|
|
54
|
+
// src/features/init/templates.ts
|
|
55
|
+
function generateAgentsMarkdown(aiTargets, severity, projectContext, projectStack) {
|
|
56
|
+
return `# Diretrizes Globais de Agentes (Sauron CLI)
|
|
57
|
+
|
|
58
|
+
**Alvos:** ${aiTargets.join(", ")}
|
|
59
|
+
**Severidade:** ${severity}
|
|
60
|
+
|
|
61
|
+
## Contexto do Projeto
|
|
62
|
+
${projectContext}
|
|
63
|
+
|
|
64
|
+
## Stack Tecnol\xF3gica
|
|
65
|
+
${projectStack}
|
|
66
|
+
|
|
67
|
+
## Regra de Ouro (Write Obligation)
|
|
68
|
+
Todos os agentes operando neste reposit\xF3rio est\xE3o estritamente obrigados a documentar qualquer altera\xE7\xE3o arquitetural, de regra de neg\xF3cio ou muta\xE7\xE3o de estado nas pastas \`.sauron\` e \`.agents\` correspondentes. A leitura passiva sem documenta\xE7\xE3o \xE9 considerada quebra de compliance.
|
|
69
|
+
|
|
70
|
+
*Gerado automaticamente pelo Sauron CLI.*
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/features/init/init.service.ts
|
|
75
|
+
var InitService = class {
|
|
76
|
+
async execute(options, onConflict) {
|
|
77
|
+
const { cwd, templatesDir } = options;
|
|
78
|
+
const manifest = await getManifest(cwd) || { version: "1.0.0", files: {} };
|
|
79
|
+
async function processDirectory(source, target) {
|
|
80
|
+
if (!await fs2.pathExists(source)) return;
|
|
81
|
+
const files = await fs2.readdir(source);
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const sourcePath = path2.join(source, file);
|
|
84
|
+
const targetPath = path2.join(target, file);
|
|
85
|
+
const stat = await fs2.stat(sourcePath);
|
|
86
|
+
if (stat.isDirectory()) {
|
|
87
|
+
await fs2.ensureDir(targetPath);
|
|
88
|
+
await processDirectory(sourcePath, targetPath);
|
|
89
|
+
} else {
|
|
90
|
+
const content = await fs2.readFile(sourcePath, "utf8");
|
|
91
|
+
const relPath = path2.relative(cwd, targetPath).replace(/\\/g, "/");
|
|
92
|
+
let shouldWrite = true;
|
|
93
|
+
if (await fs2.pathExists(targetPath)) {
|
|
94
|
+
const localContent = await fs2.readFile(targetPath, "utf8");
|
|
95
|
+
const hasConflict = checkConflict(localContent, content, manifest.files[relPath]);
|
|
96
|
+
if (hasConflict) {
|
|
97
|
+
const decision = await onConflict(relPath, localContent, content);
|
|
98
|
+
if (decision === "ours") {
|
|
99
|
+
shouldWrite = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
await fs2.ensureDir(path2.dirname(targetPath));
|
|
104
|
+
}
|
|
105
|
+
if (shouldWrite) {
|
|
106
|
+
await fs2.writeFile(targetPath, content, "utf8");
|
|
107
|
+
}
|
|
108
|
+
manifest.files[relPath] = generateHash(content);
|
|
109
|
+
}
|
|
92
110
|
}
|
|
93
111
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
await processDirectory(path2.join(templatesDir, ".sauron"), path2.join(cwd, ".sauron"));
|
|
113
|
+
await processDirectory(path2.join(templatesDir, ".agents"), path2.join(cwd, ".agents"));
|
|
114
|
+
const agentsMdPath = path2.join(cwd, "AGENTS.md");
|
|
115
|
+
const agentsMdContent = generateAgentsMarkdown(
|
|
116
|
+
options.aiTargets,
|
|
117
|
+
options.severity,
|
|
118
|
+
options.projectContext,
|
|
119
|
+
options.projectStack
|
|
120
|
+
);
|
|
121
|
+
let shouldWriteAgents = true;
|
|
122
|
+
if (await fs2.pathExists(agentsMdPath)) {
|
|
123
|
+
const localAgents = await fs2.readFile(agentsMdPath, "utf8");
|
|
124
|
+
const hasConflict = checkConflict(localAgents, agentsMdContent, manifest.files["AGENTS.md"]);
|
|
125
|
+
if (hasConflict) {
|
|
126
|
+
const decision = await onConflict("AGENTS.md", localAgents, agentsMdContent);
|
|
127
|
+
if (decision === "ours") {
|
|
128
|
+
shouldWriteAgents = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (shouldWriteAgents) {
|
|
133
|
+
await fs2.writeFile(agentsMdPath, agentsMdContent, "utf8");
|
|
134
|
+
}
|
|
135
|
+
manifest.files["AGENTS.md"] = generateHash(agentsMdContent);
|
|
136
|
+
await saveManifest(cwd, manifest);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
97
139
|
|
|
98
|
-
// src/
|
|
140
|
+
// src/features/init/init.command.ts
|
|
99
141
|
var __filename = fileURLToPath(import.meta.url);
|
|
100
|
-
var __dirname =
|
|
101
|
-
async function
|
|
142
|
+
var __dirname = path3.dirname(__filename);
|
|
143
|
+
async function runInitCommand(options) {
|
|
102
144
|
const cwd = process.cwd();
|
|
103
|
-
|
|
145
|
+
const logo = `
|
|
146
|
+
\x1B[38;2;255;106;0m \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\x1B[0m
|
|
147
|
+
\x1B[38;2;255;179;0m\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588 \u2588\u2588\x1B[0m
|
|
148
|
+
\x1B[38;2;255;215;0m\u255A\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\x1B[0m
|
|
149
|
+
\x1B[38;2;255;140;0m \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\x1B[0m
|
|
150
|
+
\x1B[38;2;204;51;0m\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588 \u2588\u2588\u2588\u2588\x1B[0m
|
|
151
|
+
`;
|
|
152
|
+
console.log(pc.bold(logo));
|
|
153
|
+
console.log(pc.dim(" CLI de Idempot\xEAncia e Mem\xF3ria para IAs\n"));
|
|
154
|
+
p.intro(pc.bgRed(pc.white(" Sauron Memory System - Inicializa\xE7\xE3o ")));
|
|
104
155
|
let aiTargets = ["Cursor", "Windsurf", "Aider", "Antigravity"];
|
|
105
156
|
let severity = "Observacional";
|
|
106
157
|
let projectContext = "Projeto Gen\xE9rico";
|
|
107
158
|
let projectStack = "Node.js, TypeScript";
|
|
108
159
|
if (!options.yes) {
|
|
109
|
-
const config = await
|
|
160
|
+
const config = await p.group(
|
|
110
161
|
{
|
|
111
|
-
targets: () =>
|
|
162
|
+
targets: () => p.multiselect({
|
|
112
163
|
message: "Qual(is) IA(s) voc\xEA usar\xE1 neste projeto?",
|
|
113
164
|
options: [
|
|
114
165
|
{ value: "Cursor", label: "Cursor", hint: "Recomendado" },
|
|
@@ -118,19 +169,19 @@ async function runInit(options) {
|
|
|
118
169
|
],
|
|
119
170
|
required: false
|
|
120
171
|
}),
|
|
121
|
-
severity: () =>
|
|
172
|
+
severity: () => p.select({
|
|
122
173
|
message: "Qual o n\xEDvel de severidade das regras da IA?",
|
|
123
174
|
options: [
|
|
124
175
|
{ value: "Observacional", label: "Observacional (A IA documenta quando julgar necess\xE1rio)" },
|
|
125
176
|
{ value: "Estrito", label: "Estrito (Bloqueia altera\xE7\xF5es sem documenta\xE7\xE3o expl\xEDcita)" }
|
|
126
177
|
]
|
|
127
178
|
}),
|
|
128
|
-
context: () =>
|
|
179
|
+
context: () => p.text({
|
|
129
180
|
message: "Descreva brevemente o contexto do seu projeto:",
|
|
130
181
|
placeholder: "Ex: App de agendamento de pilates",
|
|
131
182
|
defaultValue: "Projeto Gen\xE9rico"
|
|
132
183
|
}),
|
|
133
|
-
stack: () =>
|
|
184
|
+
stack: () => p.text({
|
|
134
185
|
message: "Qual a stack tecnol\xF3gica principal do seu projeto?",
|
|
135
186
|
placeholder: "Ex: Next.js 15, Tailwind, Firebase",
|
|
136
187
|
defaultValue: "Node.js, TypeScript"
|
|
@@ -138,7 +189,7 @@ async function runInit(options) {
|
|
|
138
189
|
},
|
|
139
190
|
{
|
|
140
191
|
onCancel: () => {
|
|
141
|
-
|
|
192
|
+
p.cancel("Inicializa\xE7\xE3o abortada.");
|
|
142
193
|
process.exit(0);
|
|
143
194
|
}
|
|
144
195
|
}
|
|
@@ -148,83 +199,89 @@ async function runInit(options) {
|
|
|
148
199
|
projectContext = config.context;
|
|
149
200
|
projectStack = config.stack;
|
|
150
201
|
}
|
|
151
|
-
const s =
|
|
202
|
+
const s = p.spinner();
|
|
152
203
|
s.start("Injetando o C\xE9rebro da IA no reposit\xF3rio...");
|
|
153
|
-
const packageRoot =
|
|
154
|
-
const templatesDir =
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const localContent = await fs2.readFile(targetPath, "utf8");
|
|
172
|
-
s.stop(`Conflito ou atualiza\xE7\xE3o em ${relPath}`);
|
|
173
|
-
const decision = await resolveConflict(relPath, localContent, content, manifest.files[relPath]);
|
|
174
|
-
s.start("Continuando inje\xE7\xE3o...");
|
|
175
|
-
if (decision === "ours") {
|
|
176
|
-
shouldWrite = false;
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
await fs2.ensureDir(path2.dirname(targetPath));
|
|
180
|
-
}
|
|
181
|
-
if (shouldWrite) {
|
|
182
|
-
await fs2.writeFile(targetPath, content, "utf8");
|
|
183
|
-
}
|
|
184
|
-
manifest.files[relPath] = generateHash(content);
|
|
204
|
+
const packageRoot = path3.join(__dirname, "..", "..", "..");
|
|
205
|
+
const templatesDir = path3.join(packageRoot, "templates");
|
|
206
|
+
const initService = new InitService();
|
|
207
|
+
try {
|
|
208
|
+
await initService.execute(
|
|
209
|
+
{
|
|
210
|
+
aiTargets,
|
|
211
|
+
severity,
|
|
212
|
+
projectContext,
|
|
213
|
+
projectStack,
|
|
214
|
+
cwd,
|
|
215
|
+
templatesDir
|
|
216
|
+
},
|
|
217
|
+
async (filePath, localContent, newContent) => {
|
|
218
|
+
s.stop(`Conflito em ${filePath}`);
|
|
219
|
+
const decision = await showConflictUI(filePath, localContent, newContent);
|
|
220
|
+
s.start("Continuando inje\xE7\xE3o...");
|
|
221
|
+
return decision;
|
|
185
222
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
## Contexto do Projeto
|
|
197
|
-
${projectContext}
|
|
198
|
-
|
|
199
|
-
## Stack Tecnol\xF3gica
|
|
200
|
-
${projectStack}
|
|
201
|
-
|
|
202
|
-
## Regra de Ouro (Write Obligation)
|
|
203
|
-
Todos os agentes operando neste reposit\xF3rio est\xE3o estritamente obrigados a documentar qualquer altera\xE7\xE3o arquitetural, de regra de neg\xF3cio ou muta\xE7\xE3o de estado nas pastas \`.sauron\` e \`.agents\` correspondentes. A leitura passiva sem documenta\xE7\xE3o \xE9 considerada quebra de compliance.
|
|
204
|
-
|
|
205
|
-
*Gerado automaticamente pelo Sauron CLI.*
|
|
206
|
-
`;
|
|
207
|
-
let shouldWriteAgents = true;
|
|
208
|
-
if (await fs2.pathExists(agentsMdPath)) {
|
|
209
|
-
const localAgents = await fs2.readFile(agentsMdPath, "utf8");
|
|
210
|
-
s.stop(`Conflito em AGENTS.md`);
|
|
211
|
-
const decision = await resolveConflict("AGENTS.md", localAgents, agentsMdContent, manifest.files["AGENTS.md"]);
|
|
212
|
-
s.start("Finalizando...");
|
|
213
|
-
if (decision === "ours") shouldWriteAgents = false;
|
|
223
|
+
);
|
|
224
|
+
s.stop("Inje\xE7\xE3o finalizada.");
|
|
225
|
+
p.outro(
|
|
226
|
+
pc.green(pc.bold("Sauron Memory System instalado com sucesso!\n\n")) + pc.white("O C\xE9rebro da IA foi injetado e protegido pelo motor de integridade.\n") + pc.cyan("A\xE7\xF5es Recomendadas:\n") + pc.dim("Copie o comando abaixo e envie para a sua IA testar a nova arquitetura:\n") + pc.yellow('"Analise a estrutura .sauron/ e .agents/ rec\xE9m injetada e sugira quais regras cr\xEDticas eu devo documentar agora para nosso projeto."')
|
|
227
|
+
);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
s.stop("Falha na instala\xE7\xE3o.");
|
|
230
|
+
p.cancel(error.message);
|
|
231
|
+
process.exit(1);
|
|
214
232
|
}
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
}
|
|
234
|
+
async function showConflictUI(filePath, localContent, newContent) {
|
|
235
|
+
p.note(`Foi detectada uma muta\xE7\xE3o no arquivo: ${pc.cyan(filePath)}
|
|
236
|
+
O seu agente de IA ou voc\xEA modificou este arquivo desde a \xFAltima instala\xE7\xE3o.`, "\u26A0\uFE0F CONFLITO DETECTADO");
|
|
237
|
+
let resolved = null;
|
|
238
|
+
while (!resolved) {
|
|
239
|
+
const action = await p.select({
|
|
240
|
+
message: `Como deseja proceder com ${pc.cyan(filePath)}?`,
|
|
241
|
+
options: [
|
|
242
|
+
{ value: "ours", label: "Preservar Local (Ours)", hint: "Mant\xE9m sua vers\xE3o e ignora o novo template" },
|
|
243
|
+
{ value: "theirs", label: "Sobrescrever (Theirs)", hint: "Destr\xF3i a sua vers\xE3o e aplica o template novo" },
|
|
244
|
+
{ value: "diff", label: "Auditar Diferen\xE7as (Diff Mode)", hint: "Mostra o que vai mudar visualmente" }
|
|
245
|
+
]
|
|
246
|
+
});
|
|
247
|
+
if (p.isCancel(action)) {
|
|
248
|
+
p.cancel("Opera\xE7\xE3o cancelada pelo usu\xE1rio durante a resolu\xE7\xE3o de conflito.");
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
if (action === "diff") {
|
|
252
|
+
const diffStr = renderDiff(localContent, newContent, filePath);
|
|
253
|
+
console.log("\n" + diffStr + "\n");
|
|
254
|
+
} else {
|
|
255
|
+
resolved = action;
|
|
256
|
+
}
|
|
217
257
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
return resolved;
|
|
259
|
+
}
|
|
260
|
+
function renderDiff(oldStr, newStr, fileName) {
|
|
261
|
+
const diffs = Diff.diffLines(oldStr, newStr);
|
|
262
|
+
let output = pc.bold(`--- a/${fileName} (Local)
|
|
263
|
+
+++ b/${fileName} (Novo Template)
|
|
264
|
+
`);
|
|
265
|
+
diffs.forEach((part) => {
|
|
266
|
+
const lines = part.value.replace(/\n$/, "").split("\n");
|
|
267
|
+
for (const line of lines) {
|
|
268
|
+
if (part.added) {
|
|
269
|
+
output += pc.green(`+ ${line}
|
|
270
|
+
`);
|
|
271
|
+
} else if (part.removed) {
|
|
272
|
+
output += pc.red(`- ${line}
|
|
273
|
+
`);
|
|
274
|
+
} else {
|
|
275
|
+
output += pc.dim(` ${line}
|
|
276
|
+
`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
return output;
|
|
224
281
|
}
|
|
225
282
|
|
|
226
283
|
// src/index.ts
|
|
227
284
|
var program = new Command();
|
|
228
285
|
program.name("sauron").description("Sauron CLI - Framework para resolu\xE7\xE3o de Amn\xE9sia de Contexto em IAs").version("1.0.0");
|
|
229
|
-
program.command("init").description("Inicializa o Sauron Memory System no projeto atual").option("-y, --yes", "Pula os prompts interativos e usa os valores padr\xE3o (N\xE3o-interativo)").action((options) =>
|
|
286
|
+
program.command("init").description("Inicializa o Sauron Memory System no projeto atual").option("-y, --yes", "Pula os prompts interativos e usa os valores padr\xE3o (N\xE3o-interativo)").action((options) => runInitCommand(options));
|
|
230
287
|
program.parse();
|