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