@tiqora/tiqora 0.0.2-dev
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/README.md +184 -0
- package/_tiqora/agents/dev.md +49 -0
- package/_tiqora/agents/pm.md +50 -0
- package/_tiqora/agents/sm.md +50 -0
- package/_tiqora/core/tasks/workflow.xml +235 -0
- package/_tiqora/workflows/4-implementation/dev-story/checklist.md +47 -0
- package/_tiqora/workflows/4-implementation/dev-story/instructions.xml +112 -0
- package/_tiqora/workflows/4-implementation/dev-story/workflow.yaml +25 -0
- package/dist/index.cjs +1551 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +1565 -0
- package/package.json +35 -0
- package/templates/commands/.gitkeep +0 -0
- package/templates/commands/tiq-agent-dev.md +17 -0
- package/templates/commands/tiq-agent-pm.md +18 -0
- package/templates/commands/tiq-agent-sm.md +18 -0
- package/templates/commands/tiq-workflow-create-story.md +21 -0
- package/templates/commands/tiq-workflow-create-ticket.md +21 -0
- package/templates/commands/tiq-workflow-dev-story.md +16 -0
- package/templates/commands/tiq-workflow-fetch-project-context.md +21 -0
- package/templates/config/.gitkeep +0 -0
- package/templates/config/tiqora.yaml.tpl +3 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1551 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var import_commander = require("commander");
|
|
6
|
+
var import_node_fs6 = require("fs");
|
|
7
|
+
var import_node_path7 = require("path");
|
|
8
|
+
|
|
9
|
+
// src/commands/init.ts
|
|
10
|
+
var import_prompts3 = require("@clack/prompts");
|
|
11
|
+
|
|
12
|
+
// src/utils/env-detect.ts
|
|
13
|
+
var import_node_fs = require("fs");
|
|
14
|
+
var import_node_os = require("os");
|
|
15
|
+
var import_node_path = require("path");
|
|
16
|
+
var import_prompts = require("@clack/prompts");
|
|
17
|
+
var ENVIRONMENT_PROBES = [
|
|
18
|
+
{
|
|
19
|
+
envId: "claude-code",
|
|
20
|
+
label: "Claude Code",
|
|
21
|
+
targetRelativePath: ".claude/commands",
|
|
22
|
+
indicatorRelativePaths: [".claude/commands", ".claude"]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
envId: "codex",
|
|
26
|
+
label: "Codex",
|
|
27
|
+
targetRelativePath: ".codex/prompts",
|
|
28
|
+
indicatorRelativePaths: [".codex/prompts", ".codex"]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
envId: "cursor",
|
|
32
|
+
label: "Cursor",
|
|
33
|
+
targetRelativePath: ".cursor/commands",
|
|
34
|
+
indicatorRelativePaths: [".cursor/commands", ".cursor"]
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
var SOURCE_ORDER = ["project", "home"];
|
|
38
|
+
var INSTALLED_COMMAND_FILES = [
|
|
39
|
+
"tiq-agent-dev.md",
|
|
40
|
+
"tiq-agent-sm.md",
|
|
41
|
+
"tiq-agent-pm.md",
|
|
42
|
+
"tiq-workflow-create-story.md",
|
|
43
|
+
"tiq-workflow-dev-story.md",
|
|
44
|
+
"tiq-workflow-fetch-project-context.md",
|
|
45
|
+
"tiq-workflow-create-ticket.md"
|
|
46
|
+
];
|
|
47
|
+
function detectEnvironmentCandidates(options = {}) {
|
|
48
|
+
const cwd = (0, import_node_path.resolve)(options.cwd ?? process.cwd());
|
|
49
|
+
const homeDir = (0, import_node_path.resolve)(options.homeDir ?? (0, import_node_os.homedir)());
|
|
50
|
+
const candidates = [];
|
|
51
|
+
for (const source of SOURCE_ORDER) {
|
|
52
|
+
const sourceRoot = source === "project" ? cwd : homeDir;
|
|
53
|
+
for (const probe of ENVIRONMENT_PROBES) {
|
|
54
|
+
const hasIndicator = probe.indicatorRelativePaths.some(
|
|
55
|
+
(indicatorPath) => isExistingDirectory((0, import_node_path.resolve)(sourceRoot, indicatorPath))
|
|
56
|
+
);
|
|
57
|
+
if (hasIndicator) {
|
|
58
|
+
const targetPath = (0, import_node_path.resolve)(sourceRoot, probe.targetRelativePath);
|
|
59
|
+
if (!isCreatableDirectoryTarget(targetPath)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
candidates.push({
|
|
63
|
+
envId: probe.envId,
|
|
64
|
+
label: probe.label,
|
|
65
|
+
targetPath,
|
|
66
|
+
source
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return candidates;
|
|
72
|
+
}
|
|
73
|
+
async function resolveEnvironmentTarget(options = {}) {
|
|
74
|
+
const detected = detectEnvironmentCandidates(options);
|
|
75
|
+
const allowMultipleSelections = options.allowMultipleSelections ?? false;
|
|
76
|
+
if (detected.length === 1) {
|
|
77
|
+
const selected2 = toSelectedEnvironment(detected[0]);
|
|
78
|
+
return {
|
|
79
|
+
detected,
|
|
80
|
+
selectedTargets: [selected2],
|
|
81
|
+
selected: selected2,
|
|
82
|
+
needsManualPath: false
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (detected.length > 1) {
|
|
86
|
+
if (allowMultipleSelections) {
|
|
87
|
+
const selectEnvironments = options.selectEnvironments ?? defaultSelectEnvironments;
|
|
88
|
+
const selectedKeys = await selectEnvironments(detected);
|
|
89
|
+
if (!selectedKeys || selectedKeys.length === 0) {
|
|
90
|
+
throw new Error("Environment selection was cancelled.");
|
|
91
|
+
}
|
|
92
|
+
const selectedTargets = detected.filter((candidate) => selectedKeys.includes(candidateKey(candidate))).map(toSelectedEnvironment);
|
|
93
|
+
if (selectedTargets.length === 0) {
|
|
94
|
+
throw new Error("Selected environments do not match detected candidates.");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
detected,
|
|
98
|
+
selectedTargets,
|
|
99
|
+
selected: selectedTargets[0],
|
|
100
|
+
needsManualPath: false
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const selectEnvironment = options.selectEnvironment ?? defaultSelectEnvironment;
|
|
104
|
+
const selectedKey = await selectEnvironment(detected);
|
|
105
|
+
if (!selectedKey) {
|
|
106
|
+
throw new Error("Environment selection was cancelled.");
|
|
107
|
+
}
|
|
108
|
+
const selectedCandidate = detected.find((candidate) => candidateKey(candidate) === selectedKey);
|
|
109
|
+
if (!selectedCandidate) {
|
|
110
|
+
throw new Error(`Selected environment does not match detected candidates: ${selectedKey}`);
|
|
111
|
+
}
|
|
112
|
+
const selected2 = toSelectedEnvironment(selectedCandidate);
|
|
113
|
+
return {
|
|
114
|
+
detected,
|
|
115
|
+
selectedTargets: [selected2],
|
|
116
|
+
selected: selected2,
|
|
117
|
+
needsManualPath: false
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const promptManualPath = options.promptManualPath ?? defaultManualPathPrompt;
|
|
121
|
+
const manualInput = await promptManualPath();
|
|
122
|
+
if (!manualInput) {
|
|
123
|
+
throw new Error("Manual deployment path prompt was cancelled.");
|
|
124
|
+
}
|
|
125
|
+
const normalizedPath = normalizeManualTargetPath(manualInput, {
|
|
126
|
+
baseDir: options.manualPathBaseDir ?? options.cwd ?? process.cwd()
|
|
127
|
+
});
|
|
128
|
+
const validateTargetPath = options.validateTargetPath ?? validateDeploymentTargetPath;
|
|
129
|
+
validateTargetPath(normalizedPath);
|
|
130
|
+
const selected = {
|
|
131
|
+
envId: "manual",
|
|
132
|
+
label: "Manual path",
|
|
133
|
+
targetPath: normalizedPath,
|
|
134
|
+
source: "manual"
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
detected,
|
|
138
|
+
selectedTargets: [selected],
|
|
139
|
+
selected,
|
|
140
|
+
needsManualPath: true
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function normalizeManualTargetPath(inputPath, options = {}) {
|
|
144
|
+
const trimmed = inputPath.trim();
|
|
145
|
+
if (trimmed.length === 0) {
|
|
146
|
+
throw new Error("Manual deployment path cannot be empty.");
|
|
147
|
+
}
|
|
148
|
+
const baseDir = (0, import_node_path.resolve)(options.baseDir ?? process.cwd());
|
|
149
|
+
return (0, import_node_path.resolve)(baseDir, trimmed);
|
|
150
|
+
}
|
|
151
|
+
function validateDeploymentTargetPath(targetPath) {
|
|
152
|
+
if ((0, import_node_fs.existsSync)(targetPath)) {
|
|
153
|
+
const stats = (0, import_node_fs.statSync)(targetPath);
|
|
154
|
+
if (!stats.isDirectory()) {
|
|
155
|
+
throw new Error(`Deployment target is not a directory: ${targetPath}`);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
(0, import_node_fs.mkdirSync)(targetPath, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
(0, import_node_fs.accessSync)(targetPath, import_node_fs.constants.W_OK);
|
|
161
|
+
}
|
|
162
|
+
function candidateKey(candidate) {
|
|
163
|
+
return `${candidate.source}:${candidate.envId}`;
|
|
164
|
+
}
|
|
165
|
+
function toSelectedEnvironment(candidate) {
|
|
166
|
+
return {
|
|
167
|
+
envId: candidate.envId,
|
|
168
|
+
label: candidate.label,
|
|
169
|
+
targetPath: candidate.targetPath,
|
|
170
|
+
source: candidate.source
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async function defaultSelectEnvironment(candidates) {
|
|
174
|
+
const answer = await (0, import_prompts.select)({
|
|
175
|
+
message: "Multiple AI environments detected. Choose deployment target:",
|
|
176
|
+
options: candidates.map((candidate) => ({
|
|
177
|
+
value: candidateKey(candidate),
|
|
178
|
+
label: `${candidate.label} (${candidate.source})`,
|
|
179
|
+
hint: candidate.targetPath
|
|
180
|
+
}))
|
|
181
|
+
});
|
|
182
|
+
if ((0, import_prompts.isCancel)(answer)) {
|
|
183
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return String(answer);
|
|
187
|
+
}
|
|
188
|
+
async function defaultSelectEnvironments(candidates) {
|
|
189
|
+
const initialValues = getDefaultSelectedEnvironmentKeys(candidates);
|
|
190
|
+
const answer = await (0, import_prompts.multiselect)({
|
|
191
|
+
message: "Multiple AI environments detected. Choose deployment targets:",
|
|
192
|
+
initialValues,
|
|
193
|
+
options: candidates.map((candidate) => ({
|
|
194
|
+
value: candidateKey(candidate),
|
|
195
|
+
label: `${candidate.label} (${candidate.source})`,
|
|
196
|
+
hint: candidate.targetPath
|
|
197
|
+
}))
|
|
198
|
+
});
|
|
199
|
+
if ((0, import_prompts.isCancel)(answer)) {
|
|
200
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return Array.from(answer).map((value) => String(value));
|
|
204
|
+
}
|
|
205
|
+
function getDefaultSelectedEnvironmentKeys(candidates) {
|
|
206
|
+
return candidates.filter(
|
|
207
|
+
(candidate) => hasInstalledCommandFiles(candidate.targetPath) || hasAnyTiqCommandFiles(candidate.targetPath)
|
|
208
|
+
).map(candidateKey);
|
|
209
|
+
}
|
|
210
|
+
async function defaultManualPathPrompt() {
|
|
211
|
+
const answer = await (0, import_prompts.text)({
|
|
212
|
+
message: "No known AI environment found. Enter deployment directory:"
|
|
213
|
+
});
|
|
214
|
+
if ((0, import_prompts.isCancel)(answer)) {
|
|
215
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return String(answer);
|
|
219
|
+
}
|
|
220
|
+
function isExistingDirectory(path) {
|
|
221
|
+
if (!(0, import_node_fs.existsSync)(path)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return (0, import_node_fs.statSync)(path).isDirectory();
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function isCreatableDirectoryTarget(path) {
|
|
231
|
+
if (!(0, import_node_fs.existsSync)(path)) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
return (0, import_node_fs.statSync)(path).isDirectory();
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function hasInstalledCommandFiles(targetPath) {
|
|
241
|
+
return INSTALLED_COMMAND_FILES.every(
|
|
242
|
+
(fileName) => (0, import_node_fs.existsSync)((0, import_node_path.resolve)(targetPath, fileName))
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
function hasAnyTiqCommandFiles(targetPath) {
|
|
246
|
+
try {
|
|
247
|
+
const entries = (0, import_node_fs.readdirSync)(targetPath, { withFileTypes: true });
|
|
248
|
+
return entries.some(
|
|
249
|
+
(entry) => entry.isFile() && /^tiq-[a-z0-9-]+\.md$/iu.test(entry.name)
|
|
250
|
+
);
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/utils/file-ops.ts
|
|
257
|
+
var import_node_fs2 = require("fs");
|
|
258
|
+
var import_node_path2 = require("path");
|
|
259
|
+
var import_prompts2 = require("@clack/prompts");
|
|
260
|
+
var COMMAND_TEMPLATE_FILES = [
|
|
261
|
+
"tiq-agent-dev.md",
|
|
262
|
+
"tiq-agent-sm.md",
|
|
263
|
+
"tiq-agent-pm.md",
|
|
264
|
+
"tiq-workflow-create-story.md",
|
|
265
|
+
"tiq-workflow-dev-story.md",
|
|
266
|
+
"tiq-workflow-fetch-project-context.md",
|
|
267
|
+
"tiq-workflow-create-ticket.md"
|
|
268
|
+
];
|
|
269
|
+
var TIQORA_WORKSPACE_DIRECTORIES = [
|
|
270
|
+
"config",
|
|
271
|
+
"state",
|
|
272
|
+
"agents/sessions",
|
|
273
|
+
"workflows/runs",
|
|
274
|
+
"workflows/steps",
|
|
275
|
+
"sessions",
|
|
276
|
+
"sprints",
|
|
277
|
+
"reports/daily",
|
|
278
|
+
"reports/retrospective",
|
|
279
|
+
"reports/mr-context",
|
|
280
|
+
"sync",
|
|
281
|
+
"migrations"
|
|
282
|
+
];
|
|
283
|
+
var TIQORA_WORKSPACE_ROOT = ".tiqora";
|
|
284
|
+
var TIQORA_RUNTIME_ROOT = "_tiqora";
|
|
285
|
+
var MissingTemplateFileError = class extends Error {
|
|
286
|
+
constructor(templateFile, templatePath) {
|
|
287
|
+
super(`Missing required template file: ${templatePath}`);
|
|
288
|
+
this.templateFile = templateFile;
|
|
289
|
+
this.templatePath = templatePath;
|
|
290
|
+
this.name = "MissingTemplateFileError";
|
|
291
|
+
}
|
|
292
|
+
code = "MISSING_TEMPLATE_FILE";
|
|
293
|
+
};
|
|
294
|
+
function resolveTemplateSourceDir(startDir) {
|
|
295
|
+
const startDirs = getSearchRoots(startDir);
|
|
296
|
+
for (const initialCursor of startDirs) {
|
|
297
|
+
let cursor = initialCursor;
|
|
298
|
+
for (let level = 0; level <= 6; level += 1) {
|
|
299
|
+
const candidate = (0, import_node_path2.resolve)(cursor, "templates", "commands");
|
|
300
|
+
if ((0, import_node_fs2.existsSync)(candidate)) {
|
|
301
|
+
return candidate;
|
|
302
|
+
}
|
|
303
|
+
const parent = (0, import_node_path2.dirname)(cursor);
|
|
304
|
+
if (parent === cursor) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
cursor = parent;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return (0, import_node_path2.resolve)(process.cwd(), "templates", "commands");
|
|
311
|
+
}
|
|
312
|
+
function getCommandTemplateManifest(templateSourceDir) {
|
|
313
|
+
const sourceDir = (0, import_node_path2.resolve)(templateSourceDir ?? resolveTemplateSourceDir());
|
|
314
|
+
return COMMAND_TEMPLATE_FILES.map((fileName) => {
|
|
315
|
+
const sourcePath = (0, import_node_path2.resolve)(sourceDir, fileName);
|
|
316
|
+
if (!(0, import_node_fs2.existsSync)(sourcePath)) {
|
|
317
|
+
throw new MissingTemplateFileError(fileName, sourcePath);
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
fileName,
|
|
321
|
+
sourcePath
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async function deployCommandTemplates(options) {
|
|
326
|
+
if (!options.selectedTargets.length) {
|
|
327
|
+
throw new Error("No deployment targets were provided.");
|
|
328
|
+
}
|
|
329
|
+
const manifest = getCommandTemplateManifest(options.templateSourceDir);
|
|
330
|
+
const askOverwrite = options.confirmOverwrite ?? defaultConfirmOverwrite;
|
|
331
|
+
const force = options.force ?? false;
|
|
332
|
+
const summary = {
|
|
333
|
+
deployedTargets: [],
|
|
334
|
+
filesCopied: [],
|
|
335
|
+
filesOverwritten: [],
|
|
336
|
+
filesSkipped: []
|
|
337
|
+
};
|
|
338
|
+
let overwriteDecision;
|
|
339
|
+
for (const selectedTarget of options.selectedTargets) {
|
|
340
|
+
const targetPath = (0, import_node_path2.resolve)(selectedTarget.targetPath);
|
|
341
|
+
(0, import_node_fs2.mkdirSync)(targetPath, { recursive: true });
|
|
342
|
+
summary.deployedTargets.push(targetPath);
|
|
343
|
+
for (const templateEntry of manifest) {
|
|
344
|
+
const outputPath = (0, import_node_path2.resolve)(targetPath, templateEntry.fileName);
|
|
345
|
+
if (!(0, import_node_fs2.existsSync)(outputPath)) {
|
|
346
|
+
(0, import_node_fs2.copyFileSync)(templateEntry.sourcePath, outputPath);
|
|
347
|
+
summary.filesCopied.push(outputPath);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (force) {
|
|
351
|
+
(0, import_node_fs2.copyFileSync)(templateEntry.sourcePath, outputPath);
|
|
352
|
+
summary.filesOverwritten.push(outputPath);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (overwriteDecision === void 0) {
|
|
356
|
+
overwriteDecision = await askOverwrite({
|
|
357
|
+
targetPath: outputPath,
|
|
358
|
+
templateFile: templateEntry.fileName
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (overwriteDecision) {
|
|
362
|
+
(0, import_node_fs2.copyFileSync)(templateEntry.sourcePath, outputPath);
|
|
363
|
+
summary.filesOverwritten.push(outputPath);
|
|
364
|
+
} else {
|
|
365
|
+
summary.filesSkipped.push(outputPath);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return summary;
|
|
370
|
+
}
|
|
371
|
+
function patchGitignoreWithTiqora(options = {}) {
|
|
372
|
+
const projectRoot = (0, import_node_path2.resolve)(options.projectRoot ?? process.cwd());
|
|
373
|
+
const gitignorePath = (0, import_node_path2.resolve)(projectRoot, ".gitignore");
|
|
374
|
+
if (!(0, import_node_fs2.existsSync)(gitignorePath)) {
|
|
375
|
+
(0, import_node_fs2.writeFileSync)(gitignorePath, ".tiqora/\n", "utf8");
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
const current = (0, import_node_fs2.readFileSync)(gitignorePath, "utf8");
|
|
379
|
+
const normalized = current.replace(/\r\n/g, "\n");
|
|
380
|
+
const lines = normalized.split("\n");
|
|
381
|
+
if (lines.includes(".tiqora/")) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
const withTrailingNewline = normalized.endsWith("\n") ? normalized : `${normalized}
|
|
385
|
+
`;
|
|
386
|
+
const nextContent = `${withTrailingNewline}.tiqora/
|
|
387
|
+
`;
|
|
388
|
+
(0, import_node_fs2.writeFileSync)(gitignorePath, nextContent, "utf8");
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
async function deployAgentAssets(options) {
|
|
392
|
+
const deployment = await deployCommandTemplates(options);
|
|
393
|
+
const workspaceSummary = bootstrapTiqoraWorkspace({
|
|
394
|
+
projectRoot: options.projectRoot
|
|
395
|
+
});
|
|
396
|
+
const runtimeSummary = deployTiqoraRuntimeAssets({
|
|
397
|
+
projectRoot: options.projectRoot,
|
|
398
|
+
runtimeSourceDir: options.runtimeSourceDir,
|
|
399
|
+
force: options.force
|
|
400
|
+
});
|
|
401
|
+
const gitignorePatched = patchGitignoreWithTiqora({
|
|
402
|
+
projectRoot: options.projectRoot
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
...deployment,
|
|
406
|
+
gitignorePatched,
|
|
407
|
+
runtimeSummary,
|
|
408
|
+
workspaceSummary
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function deployTiqoraRuntimeAssets(options = {}) {
|
|
412
|
+
const projectRoot = (0, import_node_path2.resolve)(options.projectRoot ?? process.cwd());
|
|
413
|
+
const runtimeRoot = (0, import_node_path2.resolve)(projectRoot, TIQORA_RUNTIME_ROOT);
|
|
414
|
+
const sourcePath = options.runtimeSourceDir ? (0, import_node_path2.resolve)(options.runtimeSourceDir) : resolveTiqoraRuntimeSourceDir(projectRoot);
|
|
415
|
+
const force = options.force ?? false;
|
|
416
|
+
const summary = {
|
|
417
|
+
sourceFound: false,
|
|
418
|
+
sourcePath,
|
|
419
|
+
runtimeRoot,
|
|
420
|
+
filesCopied: [],
|
|
421
|
+
filesOverwritten: [],
|
|
422
|
+
filesSkipped: []
|
|
423
|
+
};
|
|
424
|
+
if (!sourcePath) {
|
|
425
|
+
return summary;
|
|
426
|
+
}
|
|
427
|
+
const sourceWorkflowEngine = (0, import_node_path2.resolve)(sourcePath, "core", "tasks", "workflow.xml");
|
|
428
|
+
if (!(0, import_node_fs2.existsSync)(sourceWorkflowEngine)) {
|
|
429
|
+
return summary;
|
|
430
|
+
}
|
|
431
|
+
summary.sourceFound = true;
|
|
432
|
+
syncRuntimeTree({
|
|
433
|
+
sourcePath,
|
|
434
|
+
targetPath: runtimeRoot,
|
|
435
|
+
projectRoot,
|
|
436
|
+
force,
|
|
437
|
+
summary
|
|
438
|
+
});
|
|
439
|
+
return summary;
|
|
440
|
+
}
|
|
441
|
+
function bootstrapTiqoraWorkspace(options = {}) {
|
|
442
|
+
const projectRoot = (0, import_node_path2.resolve)(options.projectRoot ?? process.cwd());
|
|
443
|
+
const workspaceRoot = (0, import_node_path2.resolve)(projectRoot, TIQORA_WORKSPACE_ROOT);
|
|
444
|
+
const directoriesCreated = [];
|
|
445
|
+
for (const relativeDir of TIQORA_WORKSPACE_DIRECTORIES) {
|
|
446
|
+
const directoryPath = (0, import_node_path2.resolve)(workspaceRoot, relativeDir);
|
|
447
|
+
if (!(0, import_node_fs2.existsSync)(directoryPath)) {
|
|
448
|
+
directoriesCreated.push(toProjectRelativePath(projectRoot, directoryPath));
|
|
449
|
+
}
|
|
450
|
+
(0, import_node_fs2.mkdirSync)(directoryPath, { recursive: true });
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
workspaceRoot,
|
|
454
|
+
directoriesCreated
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
async function defaultConfirmOverwrite(args) {
|
|
458
|
+
const answer = await (0, import_prompts2.confirm)({
|
|
459
|
+
message: "Existing command files were found. Do you want to update all command files for this run?",
|
|
460
|
+
initialValue: false,
|
|
461
|
+
active: "Yes, update all",
|
|
462
|
+
inactive: "No, keep existing"
|
|
463
|
+
});
|
|
464
|
+
if ((0, import_prompts2.isCancel)(answer)) {
|
|
465
|
+
(0, import_prompts2.cancel)("Operation cancelled.");
|
|
466
|
+
throw new Error("Overwrite confirmation was cancelled.");
|
|
467
|
+
}
|
|
468
|
+
return Boolean(answer);
|
|
469
|
+
}
|
|
470
|
+
function syncRuntimeTree(options) {
|
|
471
|
+
const sourceStats = (0, import_node_fs2.lstatSync)(options.sourcePath);
|
|
472
|
+
if (sourceStats.isDirectory()) {
|
|
473
|
+
(0, import_node_fs2.mkdirSync)(options.targetPath, { recursive: true });
|
|
474
|
+
const entries = (0, import_node_fs2.readdirSync)(options.sourcePath, { withFileTypes: true });
|
|
475
|
+
for (const entry of entries) {
|
|
476
|
+
syncRuntimeTree({
|
|
477
|
+
...options,
|
|
478
|
+
sourcePath: (0, import_node_path2.resolve)(options.sourcePath, entry.name),
|
|
479
|
+
targetPath: (0, import_node_path2.resolve)(options.targetPath, entry.name)
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (!sourceStats.isFile()) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const relativeTargetPath = toProjectRelativePath(options.projectRoot, options.targetPath);
|
|
488
|
+
if ((0, import_node_fs2.existsSync)(options.targetPath)) {
|
|
489
|
+
if (!options.force) {
|
|
490
|
+
options.summary.filesSkipped.push(relativeTargetPath);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(options.targetPath), { recursive: true });
|
|
494
|
+
(0, import_node_fs2.copyFileSync)(options.sourcePath, options.targetPath);
|
|
495
|
+
options.summary.filesOverwritten.push(relativeTargetPath);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(options.targetPath), { recursive: true });
|
|
499
|
+
(0, import_node_fs2.copyFileSync)(options.sourcePath, options.targetPath);
|
|
500
|
+
options.summary.filesCopied.push(relativeTargetPath);
|
|
501
|
+
}
|
|
502
|
+
function resolveTiqoraRuntimeSourceDir(startDir) {
|
|
503
|
+
const startDirs = getSearchRoots(startDir);
|
|
504
|
+
for (const initialCursor of startDirs) {
|
|
505
|
+
let cursor = initialCursor;
|
|
506
|
+
for (let level = 0; level <= 6; level += 1) {
|
|
507
|
+
const candidate = (0, import_node_path2.resolve)(cursor, TIQORA_RUNTIME_ROOT);
|
|
508
|
+
const workflowEngineCandidate = (0, import_node_path2.resolve)(candidate, "core", "tasks", "workflow.xml");
|
|
509
|
+
if ((0, import_node_fs2.existsSync)(workflowEngineCandidate)) {
|
|
510
|
+
return candidate;
|
|
511
|
+
}
|
|
512
|
+
const parent = (0, import_node_path2.dirname)(cursor);
|
|
513
|
+
if (parent === cursor) {
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
cursor = parent;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
function getSearchRoots(startDir) {
|
|
522
|
+
const startDirs = /* @__PURE__ */ new Set();
|
|
523
|
+
addProcessEntrySearchRoots(startDirs);
|
|
524
|
+
addModuleSearchRoots(startDirs);
|
|
525
|
+
if (startDir) {
|
|
526
|
+
startDirs.add((0, import_node_path2.resolve)(startDir));
|
|
527
|
+
}
|
|
528
|
+
startDirs.add((0, import_node_path2.resolve)(process.cwd()));
|
|
529
|
+
return startDirs;
|
|
530
|
+
}
|
|
531
|
+
function addProcessEntrySearchRoots(startDirs) {
|
|
532
|
+
const entryPath = process.argv[1];
|
|
533
|
+
if (!entryPath) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
addPathAndParent(startDirs, (0, import_node_path2.resolve)(entryPath));
|
|
537
|
+
try {
|
|
538
|
+
const realEntryPath = (0, import_node_fs2.realpathSync)(entryPath);
|
|
539
|
+
addPathAndParent(startDirs, (0, import_node_path2.resolve)(realEntryPath));
|
|
540
|
+
} catch {
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function addModuleSearchRoots(startDirs) {
|
|
544
|
+
if (typeof __filename !== "string" || __filename.length === 0) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
addPathAndParent(startDirs, (0, import_node_path2.resolve)(__filename));
|
|
548
|
+
}
|
|
549
|
+
function addPathAndParent(startDirs, path) {
|
|
550
|
+
const pathDir = (0, import_node_path2.dirname)(path);
|
|
551
|
+
startDirs.add(pathDir);
|
|
552
|
+
startDirs.add((0, import_node_path2.resolve)(pathDir, ".."));
|
|
553
|
+
}
|
|
554
|
+
function toProjectRelativePath(projectRoot, absolutePath) {
|
|
555
|
+
const relativePath = (0, import_node_path2.relative)(projectRoot, absolutePath);
|
|
556
|
+
if (relativePath.length === 0) {
|
|
557
|
+
return ".";
|
|
558
|
+
}
|
|
559
|
+
return relativePath.replace(/\\/gu, "/");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/utils/git-branch-pattern.ts
|
|
563
|
+
var import_node_child_process = require("child_process");
|
|
564
|
+
var import_node_path3 = require("path");
|
|
565
|
+
var DEFAULT_BRANCH_PATTERN = "feature/*";
|
|
566
|
+
var IGNORED_BRANCH_NAMES = /* @__PURE__ */ new Set([
|
|
567
|
+
"main",
|
|
568
|
+
"master",
|
|
569
|
+
"develop",
|
|
570
|
+
"development",
|
|
571
|
+
"dev",
|
|
572
|
+
"trunk",
|
|
573
|
+
"staging",
|
|
574
|
+
"production",
|
|
575
|
+
"prod"
|
|
576
|
+
]);
|
|
577
|
+
var PATTERN_PRIORITY = [
|
|
578
|
+
"feature/*",
|
|
579
|
+
"feat/*",
|
|
580
|
+
"story/*",
|
|
581
|
+
"chore/*",
|
|
582
|
+
"bugfix/*",
|
|
583
|
+
"fix/*",
|
|
584
|
+
"hotfix/*",
|
|
585
|
+
"task/*",
|
|
586
|
+
"release/*"
|
|
587
|
+
];
|
|
588
|
+
function suggestBranchPattern(options = {}) {
|
|
589
|
+
const cwd = (0, import_node_path3.resolve)(options.cwd ?? process.cwd());
|
|
590
|
+
try {
|
|
591
|
+
const listBranches = options.listBranches ?? listLocalBranches;
|
|
592
|
+
return inferBranchPatternFromBranches(listBranches(cwd));
|
|
593
|
+
} catch {
|
|
594
|
+
return DEFAULT_BRANCH_PATTERN;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function inferBranchPatternFromBranches(branches) {
|
|
598
|
+
const counts = /* @__PURE__ */ new Map();
|
|
599
|
+
for (const rawBranch of branches) {
|
|
600
|
+
const pattern = toPattern(rawBranch);
|
|
601
|
+
if (!pattern) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
counts.set(pattern, (counts.get(pattern) ?? 0) + 1);
|
|
605
|
+
}
|
|
606
|
+
if (counts.size === 0) {
|
|
607
|
+
return DEFAULT_BRANCH_PATTERN;
|
|
608
|
+
}
|
|
609
|
+
return Array.from(counts.entries()).sort((left, right) => {
|
|
610
|
+
const countDelta = right[1] - left[1];
|
|
611
|
+
if (countDelta !== 0) {
|
|
612
|
+
return countDelta;
|
|
613
|
+
}
|
|
614
|
+
const leftPriority = patternPriorityIndex(left[0]);
|
|
615
|
+
const rightPriority = patternPriorityIndex(right[0]);
|
|
616
|
+
if (leftPriority !== rightPriority) {
|
|
617
|
+
return leftPriority - rightPriority;
|
|
618
|
+
}
|
|
619
|
+
return left[0].localeCompare(right[0]);
|
|
620
|
+
})[0][0];
|
|
621
|
+
}
|
|
622
|
+
function listLocalBranches(cwd) {
|
|
623
|
+
const output = (0, import_node_child_process.execFileSync)(
|
|
624
|
+
"git",
|
|
625
|
+
["for-each-ref", "--format=%(refname:short)", "refs/heads"],
|
|
626
|
+
{
|
|
627
|
+
cwd,
|
|
628
|
+
encoding: "utf8",
|
|
629
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
630
|
+
}
|
|
631
|
+
);
|
|
632
|
+
return output.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
633
|
+
}
|
|
634
|
+
function toPattern(branchName) {
|
|
635
|
+
const normalized = branchName.trim().replace(/^remotes\/origin\//u, "");
|
|
636
|
+
if (normalized.length === 0 || IGNORED_BRANCH_NAMES.has(normalized)) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
if (normalized.includes("/")) {
|
|
640
|
+
const prefix = normalized.split("/")[0].toLowerCase();
|
|
641
|
+
if (prefix.length === 0 || IGNORED_BRANCH_NAMES.has(prefix)) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
return `${prefix}/*`;
|
|
645
|
+
}
|
|
646
|
+
const dashedPrefixMatch = normalized.match(/^([a-z0-9._-]+)-/iu);
|
|
647
|
+
if (dashedPrefixMatch) {
|
|
648
|
+
const prefix = dashedPrefixMatch[1].toLowerCase();
|
|
649
|
+
if (!IGNORED_BRANCH_NAMES.has(prefix)) {
|
|
650
|
+
return `${prefix}/*`;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
function patternPriorityIndex(pattern) {
|
|
656
|
+
const index = PATTERN_PRIORITY.indexOf(pattern);
|
|
657
|
+
return index === -1 ? PATTERN_PRIORITY.length : index;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/utils/init-config.ts
|
|
661
|
+
var import_node_fs3 = require("fs");
|
|
662
|
+
var import_node_os2 = require("os");
|
|
663
|
+
var import_node_path4 = require("path");
|
|
664
|
+
var DEFAULT_IDLE_THRESHOLD_MINUTES = 15;
|
|
665
|
+
var DEFAULT_USER_NAME = "Developer";
|
|
666
|
+
var DEFAULT_COMMUNICATION_LANGUAGE = "fr";
|
|
667
|
+
var DEFAULT_DOCUMENT_LANGUAGE = "fr";
|
|
668
|
+
var PROJECT_INIT_CONFIG_FILE = ".tiqora.yaml";
|
|
669
|
+
var GLOBAL_USER_CONFIG_FILE = ".tiqora/config.yaml";
|
|
670
|
+
function createProjectInitConfigYaml(answers) {
|
|
671
|
+
const branchPattern = answers.branchPattern.trim();
|
|
672
|
+
if (branchPattern.length === 0) {
|
|
673
|
+
throw new Error("Branch pattern cannot be empty.");
|
|
674
|
+
}
|
|
675
|
+
const lines = [
|
|
676
|
+
`pm_tool: ${answers.pmTool}`,
|
|
677
|
+
`git_host: ${answers.gitHost}`,
|
|
678
|
+
`branch_pattern: '${escapeSingleQuotes(branchPattern)}'`
|
|
679
|
+
];
|
|
680
|
+
if (answers.pmTool === "jira") {
|
|
681
|
+
const jira = answers.jira;
|
|
682
|
+
if (!jira) {
|
|
683
|
+
throw new Error("Jira configuration is required when pm_tool is jira.");
|
|
684
|
+
}
|
|
685
|
+
const projectKey = jira.projectKey.trim();
|
|
686
|
+
const boardId = jira.boardId.trim();
|
|
687
|
+
if (!projectKey || !boardId) {
|
|
688
|
+
throw new Error("jira.project_key and jira.board_id are required.");
|
|
689
|
+
}
|
|
690
|
+
lines.push("jira:");
|
|
691
|
+
lines.push(` project_key: ${projectKey}`);
|
|
692
|
+
lines.push(` board_id: '${escapeSingleQuotes(boardId)}'`);
|
|
693
|
+
}
|
|
694
|
+
return `${lines.join("\n")}
|
|
695
|
+
`;
|
|
696
|
+
}
|
|
697
|
+
function createGlobalUserConfigYaml(answers) {
|
|
698
|
+
const idleThresholdMinutes = answers.idleThresholdMinutes ?? DEFAULT_IDLE_THRESHOLD_MINUTES;
|
|
699
|
+
if (!Number.isInteger(idleThresholdMinutes) || idleThresholdMinutes <= 0) {
|
|
700
|
+
throw new Error("idle_threshold_minutes must be a positive integer.");
|
|
701
|
+
}
|
|
702
|
+
const userName = normalizeRequiredValue(
|
|
703
|
+
answers.userName ?? DEFAULT_USER_NAME,
|
|
704
|
+
"user_name"
|
|
705
|
+
);
|
|
706
|
+
const communicationLanguage = normalizeLanguageValue(
|
|
707
|
+
answers.communicationLanguage ?? DEFAULT_COMMUNICATION_LANGUAGE,
|
|
708
|
+
"communication_language"
|
|
709
|
+
);
|
|
710
|
+
const documentLanguage = normalizeLanguageValue(
|
|
711
|
+
answers.documentLanguage ?? DEFAULT_DOCUMENT_LANGUAGE,
|
|
712
|
+
"document_language"
|
|
713
|
+
);
|
|
714
|
+
return [
|
|
715
|
+
`user_name: '${escapeSingleQuotes(userName)}'`,
|
|
716
|
+
`idle_threshold_minutes: ${idleThresholdMinutes}`,
|
|
717
|
+
`communication_language: '${escapeSingleQuotes(communicationLanguage)}'`,
|
|
718
|
+
`document_language: '${escapeSingleQuotes(documentLanguage)}'`,
|
|
719
|
+
""
|
|
720
|
+
].join("\n");
|
|
721
|
+
}
|
|
722
|
+
function writeProjectInitConfig(options) {
|
|
723
|
+
const projectRoot = (0, import_node_path4.resolve)(options.projectRoot ?? process.cwd());
|
|
724
|
+
const configPath = (0, import_node_path4.resolve)(projectRoot, PROJECT_INIT_CONFIG_FILE);
|
|
725
|
+
const allowOverwrite = options.allowOverwrite ?? false;
|
|
726
|
+
if ((0, import_node_fs3.existsSync)(configPath) && !allowOverwrite) {
|
|
727
|
+
throw new Error(
|
|
728
|
+
`${PROJECT_INIT_CONFIG_FILE} already exists at ${configPath}. Remove it before running init.`
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const content = createProjectInitConfigYaml(options.answers);
|
|
732
|
+
(0, import_node_fs3.writeFileSync)(configPath, content, "utf8");
|
|
733
|
+
return configPath;
|
|
734
|
+
}
|
|
735
|
+
function writeGlobalUserConfig(options) {
|
|
736
|
+
const home = (0, import_node_path4.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
|
|
737
|
+
const configPath = (0, import_node_path4.resolve)(home, GLOBAL_USER_CONFIG_FILE);
|
|
738
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path4.resolve)(home, ".tiqora"), { recursive: true });
|
|
739
|
+
const content = createGlobalUserConfigYaml(options.answers);
|
|
740
|
+
(0, import_node_fs3.writeFileSync)(configPath, content, "utf8");
|
|
741
|
+
return configPath;
|
|
742
|
+
}
|
|
743
|
+
function readExistingInitConfig(options = {}) {
|
|
744
|
+
const projectRoot = (0, import_node_path4.resolve)(options.projectRoot ?? process.cwd());
|
|
745
|
+
const projectConfigPath = (0, import_node_path4.resolve)(projectRoot, PROJECT_INIT_CONFIG_FILE);
|
|
746
|
+
const home = (0, import_node_path4.resolve)(options.homeDir ?? (0, import_node_os2.homedir)());
|
|
747
|
+
const globalConfigPath = (0, import_node_path4.resolve)(home, GLOBAL_USER_CONFIG_FILE);
|
|
748
|
+
if (!(0, import_node_fs3.existsSync)(projectConfigPath) && !(0, import_node_fs3.existsSync)(globalConfigPath)) {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
const result = {};
|
|
752
|
+
const mergeFrom = (path) => {
|
|
753
|
+
if (!(0, import_node_fs3.existsSync)(path)) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const parsed = parseExistingInitConfigFile(path);
|
|
757
|
+
Object.assign(result, parsed);
|
|
758
|
+
};
|
|
759
|
+
mergeFrom(globalConfigPath);
|
|
760
|
+
mergeFrom(projectConfigPath);
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
763
|
+
function parseExistingInitConfigFile(configPath) {
|
|
764
|
+
const content = (0, import_node_fs3.readFileSync)(configPath, "utf8");
|
|
765
|
+
const lines = content.split(/\r?\n/u);
|
|
766
|
+
const result = {};
|
|
767
|
+
let inJiraBlock = false;
|
|
768
|
+
for (const rawLine of lines) {
|
|
769
|
+
const line = rawLine.trim();
|
|
770
|
+
if (!line || line.startsWith("#")) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (line === "jira:") {
|
|
774
|
+
inJiraBlock = true;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (!rawLine.startsWith(" ") && !rawLine.startsWith(" ")) {
|
|
778
|
+
inJiraBlock = false;
|
|
779
|
+
}
|
|
780
|
+
const separatorIndex = line.indexOf(":");
|
|
781
|
+
if (separatorIndex <= 0) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
785
|
+
const value = stripWrappingQuotes(line.slice(separatorIndex + 1).trim());
|
|
786
|
+
if (!inJiraBlock) {
|
|
787
|
+
if (key === "pm_tool" && isInitPmTool(value)) {
|
|
788
|
+
result.pmTool = value;
|
|
789
|
+
} else if (key === "git_host" && isInitGitHost(value)) {
|
|
790
|
+
result.gitHost = value;
|
|
791
|
+
} else if (key === "branch_pattern" && value.length > 0) {
|
|
792
|
+
result.branchPattern = value;
|
|
793
|
+
} else if (key === "idle_threshold_minutes") {
|
|
794
|
+
const parsed = Number(value);
|
|
795
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
796
|
+
result.idleThresholdMinutes = parsed;
|
|
797
|
+
}
|
|
798
|
+
} else if (key === "user_name" && value.length > 0) {
|
|
799
|
+
result.userName = value;
|
|
800
|
+
} else if (key === "communication_language" && value.length > 0) {
|
|
801
|
+
result.communicationLanguage = value;
|
|
802
|
+
} else if (key === "document_language" && value.length > 0) {
|
|
803
|
+
result.documentLanguage = value;
|
|
804
|
+
}
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (key === "project_key" && value.length > 0) {
|
|
808
|
+
result.jira = {
|
|
809
|
+
projectKey: value,
|
|
810
|
+
boardId: result.jira?.boardId ?? ""
|
|
811
|
+
};
|
|
812
|
+
} else if (key === "board_id" && value.length > 0) {
|
|
813
|
+
result.jira = {
|
|
814
|
+
projectKey: result.jira?.projectKey ?? "",
|
|
815
|
+
boardId: value
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (result.jira && (result.jira.projectKey.length === 0 || result.jira.boardId.length === 0)) {
|
|
820
|
+
delete result.jira;
|
|
821
|
+
}
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
function escapeSingleQuotes(value) {
|
|
825
|
+
return value.replace(/'/gu, "''");
|
|
826
|
+
}
|
|
827
|
+
function stripWrappingQuotes(value) {
|
|
828
|
+
if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
|
|
829
|
+
return value.slice(1, -1);
|
|
830
|
+
}
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
833
|
+
function normalizeLanguageValue(value, key) {
|
|
834
|
+
return normalizeRequiredValue(value, key);
|
|
835
|
+
}
|
|
836
|
+
function normalizeRequiredValue(value, key) {
|
|
837
|
+
const normalized = value.trim();
|
|
838
|
+
if (normalized.length === 0) {
|
|
839
|
+
throw new Error(`${key} cannot be empty.`);
|
|
840
|
+
}
|
|
841
|
+
return normalized;
|
|
842
|
+
}
|
|
843
|
+
function isInitPmTool(value) {
|
|
844
|
+
return value === "jira" || value === "gitlab" || value === "none";
|
|
845
|
+
}
|
|
846
|
+
function isInitGitHost(value) {
|
|
847
|
+
return value === "gitlab" || value === "github";
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/utils/jira-mcp.ts
|
|
851
|
+
var import_node_fs4 = require("fs");
|
|
852
|
+
var import_node_os3 = require("os");
|
|
853
|
+
var import_node_path5 = require("path");
|
|
854
|
+
var ATLASSIAN_MCP_URL = "https://mcp.atlassian.com/v1/mcp";
|
|
855
|
+
function ensureJiraMcpConfigured(options) {
|
|
856
|
+
const projectRoot = (0, import_node_path5.resolve)(options.projectRoot ?? process.cwd());
|
|
857
|
+
const homeDir = (0, import_node_path5.resolve)(options.homeDir ?? (0, import_node_os3.homedir)());
|
|
858
|
+
const envIds = new Set(options.selectedTargets.map((target) => target.envId));
|
|
859
|
+
for (const envId of envIds) {
|
|
860
|
+
if (envId === "manual") {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (envId === "codex") {
|
|
864
|
+
ensureCodexJiraMcp((0, import_node_path5.resolve)(homeDir, ".codex", "config.toml"));
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (envId === "claude-code") {
|
|
868
|
+
ensureClaudeJiraMcp((0, import_node_path5.resolve)(homeDir, ".claude.json"), projectRoot);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (envId === "cursor") {
|
|
872
|
+
ensureCursorJiraMcp((0, import_node_path5.resolve)(homeDir, ".cursor", "mcp.json"));
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function ensureCodexJiraMcp(configPath) {
|
|
878
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
879
|
+
const rawContent = (0, import_node_fs4.existsSync)(configPath) ? (0, import_node_fs4.readFileSync)(configPath, "utf8") : "";
|
|
880
|
+
const content = normalizeInlineSectionFormatting(rawContent);
|
|
881
|
+
const sectionName = getTomlSectionName(content, ["mcp_servers.jira", "mcp_servers.atlassian"]);
|
|
882
|
+
if (!sectionName) {
|
|
883
|
+
const nextContent2 = appendTomlSection(content, "mcp_servers.jira", [
|
|
884
|
+
"enabled = true",
|
|
885
|
+
`url = "${ATLASSIAN_MCP_URL}"`
|
|
886
|
+
]);
|
|
887
|
+
(0, import_node_fs4.writeFileSync)(configPath, nextContent2, "utf8");
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const sectionRange = getTomlSectionRange(content, sectionName);
|
|
891
|
+
if (!sectionRange) {
|
|
892
|
+
const nextContent2 = appendTomlSection(content, sectionName, [
|
|
893
|
+
"enabled = true",
|
|
894
|
+
`url = "${ATLASSIAN_MCP_URL}"`
|
|
895
|
+
]);
|
|
896
|
+
(0, import_node_fs4.writeFileSync)(configPath, nextContent2, "utf8");
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const updatedSectionBody = upsertTomlKey(
|
|
900
|
+
upsertTomlKey(sectionRange.body, "enabled", "true"),
|
|
901
|
+
"url",
|
|
902
|
+
`"${ATLASSIAN_MCP_URL}"`
|
|
903
|
+
);
|
|
904
|
+
const formattedSectionBody = ensureTomlSectionBodyStartsOnNewLine(updatedSectionBody);
|
|
905
|
+
const nextContent = `${content.slice(0, sectionRange.start)}${formattedSectionBody}${content.slice(
|
|
906
|
+
sectionRange.end
|
|
907
|
+
)}`;
|
|
908
|
+
(0, import_node_fs4.writeFileSync)(configPath, nextContent, "utf8");
|
|
909
|
+
}
|
|
910
|
+
function ensureClaudeJiraMcp(configPath, projectRoot) {
|
|
911
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
912
|
+
const parsed = readJsonOrDefault(configPath, {});
|
|
913
|
+
const rootRecord = asRecord(parsed);
|
|
914
|
+
if (!rootRecord) {
|
|
915
|
+
throw new Error(`\u2717 ${configPath} must contain a JSON object`);
|
|
916
|
+
}
|
|
917
|
+
const projects = asRecord(rootRecord.projects) ?? {};
|
|
918
|
+
const bestProject = findBestProjectMatch(projects, projectRoot);
|
|
919
|
+
const projectKey = bestProject?.key ?? (0, import_node_path5.resolve)(projectRoot);
|
|
920
|
+
const projectConfig = asRecord(projects[projectKey]) ?? {};
|
|
921
|
+
const mcpServers = asRecord(projectConfig.mcpServers) ?? {};
|
|
922
|
+
const atlassianKey = Object.keys(mcpServers).find((key) => key.toLowerCase() === "atlassian") ?? "atlassian";
|
|
923
|
+
const atlassianServer = asRecord(mcpServers[atlassianKey]) ?? {};
|
|
924
|
+
mcpServers[atlassianKey] = {
|
|
925
|
+
...atlassianServer,
|
|
926
|
+
type: "http",
|
|
927
|
+
url: ATLASSIAN_MCP_URL
|
|
928
|
+
};
|
|
929
|
+
projectConfig.mcpServers = mcpServers;
|
|
930
|
+
projects[projectKey] = projectConfig;
|
|
931
|
+
rootRecord.projects = projects;
|
|
932
|
+
(0, import_node_fs4.writeFileSync)(configPath, `${JSON.stringify(rootRecord, null, 2)}
|
|
933
|
+
`, "utf8");
|
|
934
|
+
}
|
|
935
|
+
function ensureCursorJiraMcp(configPath) {
|
|
936
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path5.dirname)(configPath), { recursive: true });
|
|
937
|
+
const parsed = readJsonOrDefault(configPath, {});
|
|
938
|
+
const rootRecord = asRecord(parsed);
|
|
939
|
+
if (!rootRecord) {
|
|
940
|
+
throw new Error(`\u2717 ${configPath} must contain a JSON object`);
|
|
941
|
+
}
|
|
942
|
+
const mcpServers = asRecord(rootRecord.mcpServers) ?? {};
|
|
943
|
+
const atlassianKey = Object.keys(mcpServers).find((key) => key.toLowerCase() === "atlassian") ?? "Atlassian";
|
|
944
|
+
const atlassianServer = asRecord(mcpServers[atlassianKey]) ?? {};
|
|
945
|
+
mcpServers[atlassianKey] = {
|
|
946
|
+
...atlassianServer,
|
|
947
|
+
url: ATLASSIAN_MCP_URL,
|
|
948
|
+
headers: asRecord(atlassianServer.headers) ?? {}
|
|
949
|
+
};
|
|
950
|
+
rootRecord.mcpServers = mcpServers;
|
|
951
|
+
(0, import_node_fs4.writeFileSync)(configPath, `${JSON.stringify(rootRecord, null, 2)}
|
|
952
|
+
`, "utf8");
|
|
953
|
+
}
|
|
954
|
+
function appendTomlSection(content, sectionName, lines) {
|
|
955
|
+
const prefix = content.trim().length === 0 ? "" : content.endsWith("\n") ? content : `${content}
|
|
956
|
+
`;
|
|
957
|
+
return `${prefix}[${sectionName}]
|
|
958
|
+
${lines.join("\n")}
|
|
959
|
+
`;
|
|
960
|
+
}
|
|
961
|
+
function getTomlSectionName(content, sectionNames) {
|
|
962
|
+
for (const sectionName of sectionNames) {
|
|
963
|
+
const sectionHeader = new RegExp(`^\\s*\\[${escapeForRegex(sectionName)}\\]\\s*$`, "m");
|
|
964
|
+
if (sectionHeader.test(content)) {
|
|
965
|
+
return sectionName;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
function getTomlSectionRange(content, sectionName) {
|
|
971
|
+
const sectionHeader = new RegExp(`^\\s*\\[${escapeForRegex(sectionName)}\\]\\s*$`, "m");
|
|
972
|
+
const match = sectionHeader.exec(content);
|
|
973
|
+
if (!match || match.index === void 0) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
const start = match.index + match[0].length;
|
|
977
|
+
const remaining = content.slice(start);
|
|
978
|
+
const nextSection = /^\s*\[[^\]]+\]\s*$/m.exec(remaining);
|
|
979
|
+
const end = nextSection?.index !== void 0 ? start + nextSection.index : content.length;
|
|
980
|
+
return {
|
|
981
|
+
start,
|
|
982
|
+
end,
|
|
983
|
+
body: content.slice(start, end)
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function upsertTomlKey(sectionBody, key, valueLiteral) {
|
|
987
|
+
const keyPattern = new RegExp(`^\\s*${escapeForRegex(key)}\\s*=.*$`, "m");
|
|
988
|
+
const line = `${key} = ${valueLiteral}`;
|
|
989
|
+
if (keyPattern.test(sectionBody)) {
|
|
990
|
+
return sectionBody.replace(keyPattern, line);
|
|
991
|
+
}
|
|
992
|
+
if (sectionBody.trim().length === 0) {
|
|
993
|
+
return `
|
|
994
|
+
${line}
|
|
995
|
+
`;
|
|
996
|
+
}
|
|
997
|
+
const suffix = sectionBody.endsWith("\n") ? "" : "\n";
|
|
998
|
+
return `${sectionBody}${suffix}${line}
|
|
999
|
+
`;
|
|
1000
|
+
}
|
|
1001
|
+
function ensureTomlSectionBodyStartsOnNewLine(sectionBody) {
|
|
1002
|
+
if (sectionBody.startsWith("\n")) {
|
|
1003
|
+
return sectionBody;
|
|
1004
|
+
}
|
|
1005
|
+
return `
|
|
1006
|
+
${sectionBody}`;
|
|
1007
|
+
}
|
|
1008
|
+
function normalizeInlineSectionFormatting(content) {
|
|
1009
|
+
return content.replace(
|
|
1010
|
+
/(\[mcp_servers\.(?:jira|atlassian)\])\s*(enabled\s*=)/giu,
|
|
1011
|
+
"$1\n$2"
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
function readJsonOrDefault(path, fallback) {
|
|
1015
|
+
if (!(0, import_node_fs4.existsSync)(path)) {
|
|
1016
|
+
return fallback;
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
return JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
|
|
1020
|
+
} catch {
|
|
1021
|
+
throw new Error(`\u2717 ${path} is not valid JSON`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function asRecord(value) {
|
|
1025
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
return value;
|
|
1029
|
+
}
|
|
1030
|
+
function findBestProjectMatch(projects, projectRoot) {
|
|
1031
|
+
const normalizedRoot = normalizePath(projectRoot);
|
|
1032
|
+
let best = null;
|
|
1033
|
+
let bestLength = -1;
|
|
1034
|
+
for (const [key, value] of Object.entries(projects)) {
|
|
1035
|
+
const normalizedKey = normalizePath(key);
|
|
1036
|
+
const isExactMatch = normalizedRoot === normalizedKey;
|
|
1037
|
+
const isPrefixMatch = normalizedRoot.startsWith(`${normalizedKey}${import_node_path5.sep}`);
|
|
1038
|
+
if (!isExactMatch && !isPrefixMatch) {
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (normalizedKey.length > bestLength) {
|
|
1042
|
+
best = { key, value };
|
|
1043
|
+
bestLength = normalizedKey.length;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return best;
|
|
1047
|
+
}
|
|
1048
|
+
function escapeForRegex(value) {
|
|
1049
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1050
|
+
}
|
|
1051
|
+
function normalizePath(pathValue) {
|
|
1052
|
+
const resolved = (0, import_node_path5.resolve)(pathValue);
|
|
1053
|
+
try {
|
|
1054
|
+
return (0, import_node_fs4.realpathSync)(resolved);
|
|
1055
|
+
} catch {
|
|
1056
|
+
return resolved;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/commands/init.ts
|
|
1061
|
+
async function runInitCommand(options = {}) {
|
|
1062
|
+
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
1063
|
+
const resolution = await resolveEnvironmentTarget({
|
|
1064
|
+
cwd: options.cwd,
|
|
1065
|
+
homeDir: options.homeDir,
|
|
1066
|
+
allowMultipleSelections: options.allowMultipleSelections ?? true,
|
|
1067
|
+
selectEnvironment: options.selectEnvironment,
|
|
1068
|
+
selectEnvironments: options.selectEnvironments,
|
|
1069
|
+
promptManualPath: options.promptManualPath,
|
|
1070
|
+
validateTargetPath: options.validateTargetPath,
|
|
1071
|
+
manualPathBaseDir: options.manualPathBaseDir
|
|
1072
|
+
});
|
|
1073
|
+
const branchPatternSuggester = options.branchPatternSuggester ?? suggestBranchPattern;
|
|
1074
|
+
const existingConfig = readExistingInitConfig({
|
|
1075
|
+
projectRoot,
|
|
1076
|
+
homeDir: options.homeDir
|
|
1077
|
+
});
|
|
1078
|
+
const suggestedBranchPattern = existingConfig?.branchPattern ?? branchPatternSuggester({ cwd: projectRoot });
|
|
1079
|
+
const collectInitAnswers = options.collectInitAnswers ?? defaultCollectInitAnswers;
|
|
1080
|
+
const answers = await collectInitAnswers({
|
|
1081
|
+
cwd: projectRoot,
|
|
1082
|
+
suggestedBranchPattern,
|
|
1083
|
+
existingConfig
|
|
1084
|
+
});
|
|
1085
|
+
if (!answers) {
|
|
1086
|
+
throw new Error("Init wizard cancelled.");
|
|
1087
|
+
}
|
|
1088
|
+
if (answers.pmTool === "jira") {
|
|
1089
|
+
ensureJiraMcpConfigured({
|
|
1090
|
+
projectRoot,
|
|
1091
|
+
homeDir: options.homeDir,
|
|
1092
|
+
selectedTargets: resolution.selectedTargets
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const configPath = writeProjectInitConfig({
|
|
1096
|
+
projectRoot,
|
|
1097
|
+
answers,
|
|
1098
|
+
allowOverwrite: true
|
|
1099
|
+
});
|
|
1100
|
+
const userConfigPath = writeGlobalUserConfig({
|
|
1101
|
+
homeDir: options.homeDir,
|
|
1102
|
+
answers
|
|
1103
|
+
});
|
|
1104
|
+
const deploymentSummary = await deployAgentAssets({
|
|
1105
|
+
selectedTargets: resolution.selectedTargets,
|
|
1106
|
+
templateSourceDir: options.templateSourceDir,
|
|
1107
|
+
runtimeSourceDir: options.runtimeSourceDir,
|
|
1108
|
+
projectRoot,
|
|
1109
|
+
force: options.force ?? false,
|
|
1110
|
+
confirmOverwrite: options.confirmOverwrite
|
|
1111
|
+
});
|
|
1112
|
+
return {
|
|
1113
|
+
...resolution,
|
|
1114
|
+
deploymentSummary,
|
|
1115
|
+
configPath,
|
|
1116
|
+
userConfigPath
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function registerInitCommand(program) {
|
|
1120
|
+
program.command("init").description("Initialize tiqora in this repository.").option("-f, --force", "Overwrite existing command files without prompting.").action((commandOptions) => {
|
|
1121
|
+
void runInitCommand({ force: Boolean(commandOptions.force) }).then((result) => {
|
|
1122
|
+
printInitSummary(result);
|
|
1123
|
+
}).catch(handleCommandError);
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
function printInitSummary(result) {
|
|
1127
|
+
console.log(`Created config: ${result.configPath}`);
|
|
1128
|
+
console.log(`Updated user profile: ${result.userConfigPath}`);
|
|
1129
|
+
printDeploymentSummary(result.deploymentSummary);
|
|
1130
|
+
}
|
|
1131
|
+
function printDeploymentSummary(summary) {
|
|
1132
|
+
if (summary.deployedTargets.length === 1) {
|
|
1133
|
+
console.log(`Deployment target: ${summary.deployedTargets[0]}`);
|
|
1134
|
+
} else {
|
|
1135
|
+
console.log(
|
|
1136
|
+
`Deployment targets (${summary.deployedTargets.length}):
|
|
1137
|
+
- ${summary.deployedTargets.join(
|
|
1138
|
+
"\n- "
|
|
1139
|
+
)}`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
console.log(
|
|
1143
|
+
`Deployment summary: copied=${summary.filesCopied.length}, overwritten=${summary.filesOverwritten.length}, skipped=${summary.filesSkipped.length}`
|
|
1144
|
+
);
|
|
1145
|
+
console.log(
|
|
1146
|
+
summary.gitignorePatched ? "Patched .gitignore with .tiqora/." : ".gitignore already contains .tiqora/."
|
|
1147
|
+
);
|
|
1148
|
+
console.log(
|
|
1149
|
+
summary.runtimeSummary.sourceFound ? `Runtime summary (_tiqora): copied=${summary.runtimeSummary.filesCopied.length}, overwritten=${summary.runtimeSummary.filesOverwritten.length}, skipped=${summary.runtimeSummary.filesSkipped.length}` : "Runtime summary (_tiqora): source not found, skipped."
|
|
1150
|
+
);
|
|
1151
|
+
console.log(
|
|
1152
|
+
`Workspace summary: created=${summary.workspaceSummary.directoriesCreated.length}`
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
async function defaultCollectInitAnswers(context) {
|
|
1156
|
+
const pmToolAnswer = await (0, import_prompts3.select)({
|
|
1157
|
+
message: "Project management tool:",
|
|
1158
|
+
options: [
|
|
1159
|
+
{ value: "jira", label: "Jira" },
|
|
1160
|
+
{ value: "gitlab", label: "GitLab" },
|
|
1161
|
+
{ value: "none", label: "None" }
|
|
1162
|
+
],
|
|
1163
|
+
initialValue: context.existingConfig?.pmTool
|
|
1164
|
+
});
|
|
1165
|
+
if ((0, import_prompts3.isCancel)(pmToolAnswer)) {
|
|
1166
|
+
(0, import_prompts3.cancel)("Operation cancelled.");
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
const gitHostAnswer = await (0, import_prompts3.select)({
|
|
1170
|
+
message: "Git host:",
|
|
1171
|
+
options: [
|
|
1172
|
+
{ value: "gitlab", label: "GitLab" },
|
|
1173
|
+
{ value: "github", label: "GitHub" }
|
|
1174
|
+
],
|
|
1175
|
+
initialValue: context.existingConfig?.gitHost
|
|
1176
|
+
});
|
|
1177
|
+
if ((0, import_prompts3.isCancel)(gitHostAnswer)) {
|
|
1178
|
+
(0, import_prompts3.cancel)("Operation cancelled.");
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
const branchPatternAnswer = await (0, import_prompts3.text)({
|
|
1182
|
+
message: "Branch naming pattern:",
|
|
1183
|
+
defaultValue: context.suggestedBranchPattern,
|
|
1184
|
+
placeholder: "feature/*",
|
|
1185
|
+
validate(value) {
|
|
1186
|
+
if (value.trim().length === 0) {
|
|
1187
|
+
return "Branch pattern cannot be empty.";
|
|
1188
|
+
}
|
|
1189
|
+
return void 0;
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
if ((0, import_prompts3.isCancel)(branchPatternAnswer)) {
|
|
1193
|
+
(0, import_prompts3.cancel)("Operation cancelled.");
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
const pmTool = String(pmToolAnswer);
|
|
1197
|
+
const answers = {
|
|
1198
|
+
pmTool,
|
|
1199
|
+
gitHost: String(gitHostAnswer),
|
|
1200
|
+
branchPattern: String(branchPatternAnswer).trim()
|
|
1201
|
+
};
|
|
1202
|
+
const userNameAnswer = await (0, import_prompts3.text)({
|
|
1203
|
+
message: "Your name (for agent interactions):",
|
|
1204
|
+
defaultValue: context.existingConfig?.userName ?? process.env.USER ?? DEFAULT_USER_NAME,
|
|
1205
|
+
placeholder: "Developer",
|
|
1206
|
+
validate(value) {
|
|
1207
|
+
if (value.trim().length === 0) {
|
|
1208
|
+
return "Name cannot be empty.";
|
|
1209
|
+
}
|
|
1210
|
+
return void 0;
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
if ((0, import_prompts3.isCancel)(userNameAnswer)) {
|
|
1214
|
+
(0, import_prompts3.cancel)("Operation cancelled.");
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
const communicationLanguageAnswer = await (0, import_prompts3.text)({
|
|
1218
|
+
message: "Agent response language:",
|
|
1219
|
+
defaultValue: context.existingConfig?.communicationLanguage ?? DEFAULT_COMMUNICATION_LANGUAGE,
|
|
1220
|
+
placeholder: "fr",
|
|
1221
|
+
validate(value) {
|
|
1222
|
+
if (value.trim().length === 0) {
|
|
1223
|
+
return "Language cannot be empty.";
|
|
1224
|
+
}
|
|
1225
|
+
return void 0;
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
if ((0, import_prompts3.isCancel)(communicationLanguageAnswer)) {
|
|
1229
|
+
(0, import_prompts3.cancel)("Operation cancelled.");
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
const documentLanguageAnswer = await (0, import_prompts3.text)({
|
|
1233
|
+
message: "Document output language:",
|
|
1234
|
+
defaultValue: context.existingConfig?.documentLanguage ?? DEFAULT_DOCUMENT_LANGUAGE,
|
|
1235
|
+
placeholder: "fr",
|
|
1236
|
+
validate(value) {
|
|
1237
|
+
if (value.trim().length === 0) {
|
|
1238
|
+
return "Language cannot be empty.";
|
|
1239
|
+
}
|
|
1240
|
+
return void 0;
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
if ((0, import_prompts3.isCancel)(documentLanguageAnswer)) {
|
|
1244
|
+
(0, import_prompts3.cancel)("Operation cancelled.");
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
answers.communicationLanguage = String(communicationLanguageAnswer).trim();
|
|
1248
|
+
answers.documentLanguage = String(documentLanguageAnswer).trim();
|
|
1249
|
+
answers.userName = String(userNameAnswer).trim();
|
|
1250
|
+
if (pmTool === "jira") {
|
|
1251
|
+
const projectKeyAnswer = await (0, import_prompts3.text)({
|
|
1252
|
+
message: "Jira project key:",
|
|
1253
|
+
placeholder: "PROJ",
|
|
1254
|
+
initialValue: context.existingConfig?.jira?.projectKey,
|
|
1255
|
+
validate(value) {
|
|
1256
|
+
if (value.trim().length === 0) {
|
|
1257
|
+
return "Project key is required.";
|
|
1258
|
+
}
|
|
1259
|
+
return void 0;
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
if ((0, import_prompts3.isCancel)(projectKeyAnswer)) {
|
|
1263
|
+
(0, import_prompts3.cancel)("Operation cancelled.");
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
const boardIdAnswer = await (0, import_prompts3.text)({
|
|
1267
|
+
message: "Jira board id:",
|
|
1268
|
+
placeholder: "123",
|
|
1269
|
+
initialValue: context.existingConfig?.jira?.boardId,
|
|
1270
|
+
validate(value) {
|
|
1271
|
+
if (value.trim().length === 0) {
|
|
1272
|
+
return "Board id is required.";
|
|
1273
|
+
}
|
|
1274
|
+
return void 0;
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
if ((0, import_prompts3.isCancel)(boardIdAnswer)) {
|
|
1278
|
+
(0, import_prompts3.cancel)("Operation cancelled.");
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
answers.jira = {
|
|
1282
|
+
projectKey: String(projectKeyAnswer).trim(),
|
|
1283
|
+
boardId: String(boardIdAnswer).trim()
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
return answers;
|
|
1287
|
+
}
|
|
1288
|
+
function handleCommandError(error) {
|
|
1289
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1290
|
+
console.error(message.startsWith("\u2717") ? message : `\u2717 ${message}`);
|
|
1291
|
+
return process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/utils/config.ts
|
|
1295
|
+
var import_node_fs5 = require("fs");
|
|
1296
|
+
var import_node_os4 = require("os");
|
|
1297
|
+
var import_node_path6 = require("path");
|
|
1298
|
+
var PROJECT_CONFIG_FILE = ".tiqora.yaml";
|
|
1299
|
+
var GLOBAL_CONFIG_FILE = ".tiqora/config.yaml";
|
|
1300
|
+
var MISSING_CONFIG_ERROR_MESSAGE = "\u2717 No .tiqora.yaml found. Run npx tiqora init first.";
|
|
1301
|
+
var DEFAULT_USER_NAME2 = "Developer";
|
|
1302
|
+
var DEFAULT_COMMUNICATION_LANGUAGE2 = "fr";
|
|
1303
|
+
var DEFAULT_DOCUMENT_LANGUAGE2 = "fr";
|
|
1304
|
+
var REQUIRED_CONFIG_KEYS = [
|
|
1305
|
+
"pm_tool",
|
|
1306
|
+
"git_host",
|
|
1307
|
+
"branch_pattern"
|
|
1308
|
+
];
|
|
1309
|
+
var ConfigNotFoundError = class extends Error {
|
|
1310
|
+
code = "CONFIG_NOT_FOUND";
|
|
1311
|
+
constructor(message = MISSING_CONFIG_ERROR_MESSAGE) {
|
|
1312
|
+
super(message);
|
|
1313
|
+
this.name = "ConfigNotFoundError";
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
function discoverProjectConfigPath(startDir = process.cwd(), maxParentLevels = 3) {
|
|
1317
|
+
let cursor = (0, import_node_path6.resolve)(startDir);
|
|
1318
|
+
for (let level = 0; level <= maxParentLevels; level += 1) {
|
|
1319
|
+
const candidate = (0, import_node_path6.resolve)(cursor, PROJECT_CONFIG_FILE);
|
|
1320
|
+
if ((0, import_node_fs5.existsSync)(candidate)) {
|
|
1321
|
+
return candidate;
|
|
1322
|
+
}
|
|
1323
|
+
const parent = (0, import_node_path6.dirname)(cursor);
|
|
1324
|
+
if (parent === cursor) {
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
cursor = parent;
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
function getGlobalConfigPath() {
|
|
1332
|
+
return (0, import_node_path6.resolve)((0, import_node_os4.homedir)(), GLOBAL_CONFIG_FILE);
|
|
1333
|
+
}
|
|
1334
|
+
function loadRuntimeConfig(options = {}) {
|
|
1335
|
+
const projectPath = discoverProjectConfigPath(
|
|
1336
|
+
options.startDir ?? process.cwd(),
|
|
1337
|
+
options.maxParentLevels ?? 3
|
|
1338
|
+
);
|
|
1339
|
+
const globalPath = options.globalConfigPath ?? getGlobalConfigPath();
|
|
1340
|
+
const hasGlobal = (0, import_node_fs5.existsSync)(globalPath);
|
|
1341
|
+
if (!projectPath && !hasGlobal) {
|
|
1342
|
+
throw new ConfigNotFoundError();
|
|
1343
|
+
}
|
|
1344
|
+
const projectValues = projectPath ? parseYamlFlatMap((0, import_node_fs5.readFileSync)(projectPath, "utf8")) : {};
|
|
1345
|
+
const globalValues = hasGlobal ? parseYamlFlatMap((0, import_node_fs5.readFileSync)(globalPath, "utf8")) : {};
|
|
1346
|
+
return parseAndValidateConfig(
|
|
1347
|
+
{
|
|
1348
|
+
...globalValues,
|
|
1349
|
+
...projectValues
|
|
1350
|
+
},
|
|
1351
|
+
projectPath ?? globalPath
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
function loadRuntimeConfigOrExit(options = {}) {
|
|
1355
|
+
try {
|
|
1356
|
+
return loadRuntimeConfig(options);
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
if (error instanceof ConfigNotFoundError) {
|
|
1359
|
+
const log = options.log ?? console.error;
|
|
1360
|
+
const exit = options.exit ?? process.exit;
|
|
1361
|
+
log(MISSING_CONFIG_ERROR_MESSAGE);
|
|
1362
|
+
return exit(1);
|
|
1363
|
+
}
|
|
1364
|
+
throw error;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function parseAndValidateConfig(parsed, configPath) {
|
|
1368
|
+
const missingKey = REQUIRED_CONFIG_KEYS.find((key) => !(key in parsed));
|
|
1369
|
+
if (missingKey) {
|
|
1370
|
+
throw new Error(`Missing required key "${missingKey}" in ${configPath}`);
|
|
1371
|
+
}
|
|
1372
|
+
const idleThreshold = Number(parsed.idle_threshold_minutes ?? "15");
|
|
1373
|
+
if (!Number.isFinite(idleThreshold) || idleThreshold <= 0) {
|
|
1374
|
+
throw new Error(`Invalid idle_threshold_minutes in ${configPath}`);
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
user_name: parseOptionalRequiredValue(parsed.user_name, DEFAULT_USER_NAME2),
|
|
1378
|
+
pm_tool: String(parsed.pm_tool),
|
|
1379
|
+
git_host: String(parsed.git_host),
|
|
1380
|
+
branch_pattern: String(parsed.branch_pattern),
|
|
1381
|
+
idle_threshold_minutes: idleThreshold,
|
|
1382
|
+
communication_language: parseOptionalLanguage(
|
|
1383
|
+
parsed.communication_language,
|
|
1384
|
+
DEFAULT_COMMUNICATION_LANGUAGE2
|
|
1385
|
+
),
|
|
1386
|
+
document_language: parseOptionalLanguage(
|
|
1387
|
+
parsed.document_language,
|
|
1388
|
+
DEFAULT_DOCUMENT_LANGUAGE2
|
|
1389
|
+
)
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function parseYamlFlatMap(content) {
|
|
1393
|
+
const result = {};
|
|
1394
|
+
for (const rawLine of content.split(/\r?\n/u)) {
|
|
1395
|
+
const line = rawLine.trim();
|
|
1396
|
+
if (!line || line.startsWith("#")) {
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
const separatorIndex = line.indexOf(":");
|
|
1400
|
+
if (separatorIndex <= 0) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
1404
|
+
const rawValue = line.slice(separatorIndex + 1).trim();
|
|
1405
|
+
result[key] = stripWrappingQuotes2(rawValue);
|
|
1406
|
+
}
|
|
1407
|
+
return result;
|
|
1408
|
+
}
|
|
1409
|
+
function stripWrappingQuotes2(value) {
|
|
1410
|
+
if (value.length < 2) {
|
|
1411
|
+
return value;
|
|
1412
|
+
}
|
|
1413
|
+
const startsWithSingle = value.startsWith("'");
|
|
1414
|
+
const startsWithDouble = value.startsWith('"');
|
|
1415
|
+
if (startsWithSingle && value.endsWith("'")) {
|
|
1416
|
+
return value.slice(1, -1);
|
|
1417
|
+
}
|
|
1418
|
+
if (startsWithDouble && value.endsWith('"')) {
|
|
1419
|
+
return value.slice(1, -1);
|
|
1420
|
+
}
|
|
1421
|
+
return value;
|
|
1422
|
+
}
|
|
1423
|
+
function parseOptionalLanguage(value, fallback) {
|
|
1424
|
+
return parseOptionalRequiredValue(value, fallback);
|
|
1425
|
+
}
|
|
1426
|
+
function parseOptionalRequiredValue(value, fallback) {
|
|
1427
|
+
if (value === void 0) {
|
|
1428
|
+
return fallback;
|
|
1429
|
+
}
|
|
1430
|
+
const normalized = value.trim();
|
|
1431
|
+
return normalized.length === 0 ? fallback : normalized;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/commands/install.ts
|
|
1435
|
+
var INVALID_INSTALL_CONFIG_ERROR_MESSAGE = "\u2717 No valid .tiqora.yaml found. Run npx tiqora init first.";
|
|
1436
|
+
async function runInstallCommand(options = {}) {
|
|
1437
|
+
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
1438
|
+
const runtimeConfig = ensureValidInstallConfig(projectRoot);
|
|
1439
|
+
const resolution = await resolveEnvironmentTarget({
|
|
1440
|
+
cwd: options.cwd,
|
|
1441
|
+
homeDir: options.homeDir,
|
|
1442
|
+
allowMultipleSelections: options.allowMultipleSelections ?? true,
|
|
1443
|
+
selectEnvironment: options.selectEnvironment,
|
|
1444
|
+
selectEnvironments: options.selectEnvironments,
|
|
1445
|
+
promptManualPath: options.promptManualPath,
|
|
1446
|
+
validateTargetPath: options.validateTargetPath,
|
|
1447
|
+
manualPathBaseDir: options.manualPathBaseDir
|
|
1448
|
+
});
|
|
1449
|
+
if (runtimeConfig.pm_tool === "jira") {
|
|
1450
|
+
ensureJiraMcpConfigured({
|
|
1451
|
+
projectRoot,
|
|
1452
|
+
homeDir: options.homeDir,
|
|
1453
|
+
selectedTargets: resolution.selectedTargets
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
const deploymentSummary = await deployAgentAssets({
|
|
1457
|
+
selectedTargets: resolution.selectedTargets,
|
|
1458
|
+
templateSourceDir: options.templateSourceDir,
|
|
1459
|
+
runtimeSourceDir: options.runtimeSourceDir,
|
|
1460
|
+
projectRoot,
|
|
1461
|
+
force: options.force ?? false,
|
|
1462
|
+
confirmOverwrite: options.confirmOverwrite
|
|
1463
|
+
});
|
|
1464
|
+
return {
|
|
1465
|
+
...resolution,
|
|
1466
|
+
deploymentSummary
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function registerInstallCommand(program) {
|
|
1470
|
+
program.command("install").description("Install tiqora assets into the selected environment.").option("-f, --force", "Overwrite existing command files without prompting.").action((commandOptions) => {
|
|
1471
|
+
void runInstallCommand({ force: Boolean(commandOptions.force) }).then((result) => {
|
|
1472
|
+
printDeploymentSummary2(result.deploymentSummary);
|
|
1473
|
+
console.log("\u2713 tiqora installed");
|
|
1474
|
+
}).catch(handleCommandError2);
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
function ensureValidInstallConfig(projectRoot) {
|
|
1478
|
+
try {
|
|
1479
|
+
return loadRuntimeConfig({ startDir: projectRoot });
|
|
1480
|
+
} catch {
|
|
1481
|
+
throw new Error(INVALID_INSTALL_CONFIG_ERROR_MESSAGE);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
function printDeploymentSummary2(summary) {
|
|
1485
|
+
if (summary.deployedTargets.length === 1) {
|
|
1486
|
+
console.log(`Deployment target: ${summary.deployedTargets[0]}`);
|
|
1487
|
+
} else {
|
|
1488
|
+
console.log(
|
|
1489
|
+
`Deployment targets (${summary.deployedTargets.length}):
|
|
1490
|
+
- ${summary.deployedTargets.join(
|
|
1491
|
+
"\n- "
|
|
1492
|
+
)}`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
console.log(
|
|
1496
|
+
`Deployment summary: copied=${summary.filesCopied.length}, overwritten=${summary.filesOverwritten.length}, skipped=${summary.filesSkipped.length}`
|
|
1497
|
+
);
|
|
1498
|
+
console.log(
|
|
1499
|
+
summary.gitignorePatched ? "Patched .gitignore with .tiqora/." : ".gitignore already contains .tiqora/."
|
|
1500
|
+
);
|
|
1501
|
+
console.log(
|
|
1502
|
+
summary.runtimeSummary.sourceFound ? `Runtime summary (_tiqora): copied=${summary.runtimeSummary.filesCopied.length}, overwritten=${summary.runtimeSummary.filesOverwritten.length}, skipped=${summary.runtimeSummary.filesSkipped.length}` : "Runtime summary (_tiqora): source not found, skipped."
|
|
1503
|
+
);
|
|
1504
|
+
console.log(
|
|
1505
|
+
`Workspace summary: created=${summary.workspaceSummary.directoriesCreated.length}`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
function handleCommandError2(error) {
|
|
1509
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1510
|
+
console.error(message.startsWith("\u2717") ? message : `\u2717 ${message}`);
|
|
1511
|
+
return process.exit(1);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/index.ts
|
|
1515
|
+
var cli = new import_commander.Command();
|
|
1516
|
+
cli.name("tiqora");
|
|
1517
|
+
cli.description("Tiqora CLI scaffold");
|
|
1518
|
+
cli.version(readPackageVersion(), "-v, --version", "output the current version");
|
|
1519
|
+
registerInitCommand(cli);
|
|
1520
|
+
registerInstallCommand(cli);
|
|
1521
|
+
if (shouldLoadRuntimeConfig(process.argv.slice(2))) {
|
|
1522
|
+
loadRuntimeConfigOrExit();
|
|
1523
|
+
}
|
|
1524
|
+
cli.parse();
|
|
1525
|
+
function readPackageVersion() {
|
|
1526
|
+
try {
|
|
1527
|
+
const packageJsonPath = (0, import_node_path7.resolve)(process.cwd(), "package.json");
|
|
1528
|
+
const content = (0, import_node_fs6.readFileSync)(packageJsonPath, "utf8");
|
|
1529
|
+
const packageJson = JSON.parse(content);
|
|
1530
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
1531
|
+
return packageJson.version;
|
|
1532
|
+
}
|
|
1533
|
+
} catch {
|
|
1534
|
+
}
|
|
1535
|
+
return "0.1.0";
|
|
1536
|
+
}
|
|
1537
|
+
function shouldLoadRuntimeConfig(args) {
|
|
1538
|
+
if (args.length === 0) {
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
const bypassFlags = /* @__PURE__ */ new Set(["-v", "--version", "-h", "--help"]);
|
|
1542
|
+
if (args.some((arg) => bypassFlags.has(arg))) {
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
const firstCommand = args.find((arg) => !arg.startsWith("-"));
|
|
1546
|
+
const installerCommands = /* @__PURE__ */ new Set(["init", "install"]);
|
|
1547
|
+
if (firstCommand && installerCommands.has(firstCommand)) {
|
|
1548
|
+
return false;
|
|
1549
|
+
}
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|