@kitsy/coop 2.2.2 → 2.2.3
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/dist/index.js +828 -127
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -32,7 +32,31 @@ var SEQ_MARKER = "COOPSEQTOKEN";
|
|
|
32
32
|
var DEFAULT_ID_NAMING_TEMPLATE = "<TYPE>-<TITLE16>-<SEQ>";
|
|
33
33
|
var DEFAULT_ARTIFACTS_DIR = "docs";
|
|
34
34
|
var DEFAULT_TITLE_TOKEN_LENGTH = 16;
|
|
35
|
+
var DEFAULT_NAMING_TEMPLATES = {
|
|
36
|
+
task: DEFAULT_ID_NAMING_TEMPLATE,
|
|
37
|
+
idea: DEFAULT_ID_NAMING_TEMPLATE,
|
|
38
|
+
track: "<NAME_SLUG>",
|
|
39
|
+
delivery: "<NAME_SLUG>",
|
|
40
|
+
run: "<TYPE>-<YYMMDD>-<RAND>"
|
|
41
|
+
};
|
|
35
42
|
var SEMANTIC_WORD_MAX = 4;
|
|
43
|
+
var BUILT_IN_NAMING_TOKENS = /* @__PURE__ */ new Set([
|
|
44
|
+
"TYPE",
|
|
45
|
+
"ENTITY",
|
|
46
|
+
"TITLE",
|
|
47
|
+
"TITLE16",
|
|
48
|
+
"TITLE24",
|
|
49
|
+
"TRACK",
|
|
50
|
+
"STATUS",
|
|
51
|
+
"TASK_TYPE",
|
|
52
|
+
"PREFIX",
|
|
53
|
+
"USER",
|
|
54
|
+
"YYMMDD",
|
|
55
|
+
"RAND",
|
|
56
|
+
"SEQ",
|
|
57
|
+
"NAME",
|
|
58
|
+
"NAME_SLUG"
|
|
59
|
+
]);
|
|
36
60
|
var SEMANTIC_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
37
61
|
"A",
|
|
38
62
|
"AN",
|
|
@@ -60,8 +84,12 @@ function resolveCoopHome() {
|
|
|
60
84
|
}
|
|
61
85
|
function resolveRepoRoot(start = process.cwd()) {
|
|
62
86
|
let current = path.resolve(start);
|
|
87
|
+
const configuredCoopHome = path.resolve(resolveCoopHome());
|
|
88
|
+
const defaultCoopHome = path.resolve(path.join(os.homedir(), ".coop"));
|
|
63
89
|
while (true) {
|
|
64
|
-
|
|
90
|
+
const coopDir2 = path.join(current, ".coop");
|
|
91
|
+
const isGlobalCoopHome = path.resolve(coopDir2) === configuredCoopHome || path.resolve(coopDir2) === defaultCoopHome;
|
|
92
|
+
if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(coopDir2) && !isGlobalCoopHome) {
|
|
65
93
|
return current;
|
|
66
94
|
}
|
|
67
95
|
const parent = path.dirname(current);
|
|
@@ -126,6 +154,8 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
|
|
|
126
154
|
taskPrefix: "PM",
|
|
127
155
|
indexDataFormat: "yaml",
|
|
128
156
|
idNamingTemplate: DEFAULT_ID_NAMING_TEMPLATE,
|
|
157
|
+
idNamingTemplates: { ...DEFAULT_NAMING_TEMPLATES },
|
|
158
|
+
idTokens: {},
|
|
129
159
|
idSeqPadding: 0,
|
|
130
160
|
artifactsDir: DEFAULT_ARTIFACTS_DIR,
|
|
131
161
|
projectName: repoName || "COOP Workspace",
|
|
@@ -147,9 +177,10 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
|
|
|
147
177
|
const indexRaw = typeof config.index === "object" && config.index !== null ? config.index : {};
|
|
148
178
|
const indexDataRaw = indexRaw.data;
|
|
149
179
|
const indexDataFormat = indexDataRaw === "json" ? "json" : "yaml";
|
|
180
|
+
const idNamingTemplates = readNamingTemplates(config);
|
|
181
|
+
const idNamingTemplate = idNamingTemplates.task;
|
|
182
|
+
const idTokens = readNamingTokens(config);
|
|
150
183
|
const idRaw = typeof config.id === "object" && config.id !== null ? config.id : {};
|
|
151
|
-
const idNamingTemplateRaw = idRaw.naming;
|
|
152
|
-
const idNamingTemplate = typeof idNamingTemplateRaw === "string" && idNamingTemplateRaw.trim().length > 0 ? idNamingTemplateRaw.trim() : DEFAULT_ID_NAMING_TEMPLATE;
|
|
153
184
|
const idSeqPaddingRaw = idRaw.seq_padding;
|
|
154
185
|
const idSeqPadding = Number.isInteger(idSeqPaddingRaw) && Number(idSeqPaddingRaw) >= 0 ? Number(idSeqPaddingRaw) : 0;
|
|
155
186
|
const artifactsRaw = typeof config.artifacts === "object" && config.artifacts !== null ? config.artifacts : {};
|
|
@@ -159,6 +190,8 @@ function readCoopConfig(root, projectId = resolveRequestedProject()) {
|
|
|
159
190
|
taskPrefix,
|
|
160
191
|
indexDataFormat,
|
|
161
192
|
idNamingTemplate,
|
|
193
|
+
idNamingTemplates,
|
|
194
|
+
idTokens,
|
|
162
195
|
idSeqPadding,
|
|
163
196
|
artifactsDir,
|
|
164
197
|
projectName: projectName || "COOP Workspace",
|
|
@@ -176,6 +209,59 @@ function sanitizeIdentityPart(value, fallback) {
|
|
|
176
209
|
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
177
210
|
return normalized || fallback;
|
|
178
211
|
}
|
|
212
|
+
function slugifyLower(value, fallback = "item") {
|
|
213
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
214
|
+
return normalized || fallback;
|
|
215
|
+
}
|
|
216
|
+
function normalizeNamingTokenName(value) {
|
|
217
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
|
|
218
|
+
if (!normalized) {
|
|
219
|
+
throw new Error("Naming token names must contain letters, numbers, or underscores.");
|
|
220
|
+
}
|
|
221
|
+
return normalized;
|
|
222
|
+
}
|
|
223
|
+
function normalizeNamingTokenValue(value) {
|
|
224
|
+
const normalized = value.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
|
|
225
|
+
if (!normalized) {
|
|
226
|
+
throw new Error("Naming token values must contain letters or numbers.");
|
|
227
|
+
}
|
|
228
|
+
return normalized;
|
|
229
|
+
}
|
|
230
|
+
function readNamingTemplates(rawConfig) {
|
|
231
|
+
const idRaw = typeof rawConfig.id === "object" && rawConfig.id !== null ? rawConfig.id : {};
|
|
232
|
+
const namingRaw = idRaw.naming;
|
|
233
|
+
if (typeof namingRaw === "string" && namingRaw.trim().length > 0) {
|
|
234
|
+
return {
|
|
235
|
+
...DEFAULT_NAMING_TEMPLATES,
|
|
236
|
+
task: namingRaw.trim(),
|
|
237
|
+
idea: namingRaw.trim()
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const namingRecord = typeof namingRaw === "object" && namingRaw !== null ? namingRaw : {};
|
|
241
|
+
return {
|
|
242
|
+
task: typeof namingRecord.task === "string" && namingRecord.task.trim().length > 0 ? namingRecord.task.trim() : DEFAULT_NAMING_TEMPLATES.task,
|
|
243
|
+
idea: typeof namingRecord.idea === "string" && namingRecord.idea.trim().length > 0 ? namingRecord.idea.trim() : DEFAULT_NAMING_TEMPLATES.idea,
|
|
244
|
+
track: typeof namingRecord.track === "string" && namingRecord.track.trim().length > 0 ? namingRecord.track.trim() : DEFAULT_NAMING_TEMPLATES.track,
|
|
245
|
+
delivery: typeof namingRecord.delivery === "string" && namingRecord.delivery.trim().length > 0 ? namingRecord.delivery.trim() : DEFAULT_NAMING_TEMPLATES.delivery,
|
|
246
|
+
run: typeof namingRecord.run === "string" && namingRecord.run.trim().length > 0 ? namingRecord.run.trim() : DEFAULT_NAMING_TEMPLATES.run
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function readNamingTokens(rawConfig) {
|
|
250
|
+
const idRaw = typeof rawConfig.id === "object" && rawConfig.id !== null ? rawConfig.id : {};
|
|
251
|
+
const tokensRaw = typeof idRaw.tokens === "object" && idRaw.tokens !== null ? idRaw.tokens : {};
|
|
252
|
+
const tokens = {};
|
|
253
|
+
for (const [rawName, rawValue] of Object.entries(tokensRaw)) {
|
|
254
|
+
const name = normalizeNamingTokenName(rawName);
|
|
255
|
+
const valuesRecord = typeof rawValue === "object" && rawValue !== null ? rawValue : {};
|
|
256
|
+
const values = Array.isArray(valuesRecord.values) ? Array.from(
|
|
257
|
+
new Set(
|
|
258
|
+
valuesRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
|
|
259
|
+
)
|
|
260
|
+
).sort((a, b) => a.localeCompare(b)) : [];
|
|
261
|
+
tokens[name] = { values };
|
|
262
|
+
}
|
|
263
|
+
return tokens;
|
|
264
|
+
}
|
|
179
265
|
function repoDisplayName(root) {
|
|
180
266
|
const base = path.basename(path.resolve(root)).trim();
|
|
181
267
|
return base || "COOP Workspace";
|
|
@@ -278,6 +364,19 @@ function sanitizeTemplateValue(input2, fallback = "X") {
|
|
|
278
364
|
const normalized = input2.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
279
365
|
return normalized || fallback;
|
|
280
366
|
}
|
|
367
|
+
function normalizeEntityIdValue(entityType, input2, fallback = "COOP-ID") {
|
|
368
|
+
if (entityType === "track" || entityType === "delivery") {
|
|
369
|
+
return slugifyLower(input2, slugifyLower(fallback, "item"));
|
|
370
|
+
}
|
|
371
|
+
return sanitizeTemplateValue(input2, fallback);
|
|
372
|
+
}
|
|
373
|
+
function extractTemplateTokens(template) {
|
|
374
|
+
return Array.from(
|
|
375
|
+
new Set(
|
|
376
|
+
Array.from(template.matchAll(/<([^>]+)>/g)).map((match) => match[1]?.trim().toUpperCase()).filter((token) => Boolean(token))
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
}
|
|
281
380
|
function sanitizeSemanticWord(input2) {
|
|
282
381
|
return input2.toUpperCase().replace(/[^A-Z0-9]+/g, "").trim();
|
|
283
382
|
}
|
|
@@ -332,8 +431,8 @@ function namingTokenExamples(title = "Natural-language COOP command recommender"
|
|
|
332
431
|
};
|
|
333
432
|
}
|
|
334
433
|
function sequenceForPattern(existingIds, prefix, suffix) {
|
|
335
|
-
const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
336
|
-
const escapedSuffix = suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
434
|
+
const escapedPrefix = prefix.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
435
|
+
const escapedSuffix = suffix.toUpperCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
337
436
|
const regex = new RegExp(`^${escapedPrefix}(\\d+)${escapedSuffix}$`);
|
|
338
437
|
let max = 0;
|
|
339
438
|
for (const id of existingIds) {
|
|
@@ -359,6 +458,7 @@ function buildIdContext(root, config, context) {
|
|
|
359
458
|
const track = context.track?.trim() || "";
|
|
360
459
|
const status = context.status?.trim() || "";
|
|
361
460
|
const title = context.title?.trim() || "";
|
|
461
|
+
const name = context.name?.trim() || context.title?.trim() || "";
|
|
362
462
|
const prefix = context.prefix?.trim() || (context.entityType === "idea" ? config.ideaPrefix : config.taskPrefix);
|
|
363
463
|
const actor = inferActor(root);
|
|
364
464
|
const map = {
|
|
@@ -373,13 +473,26 @@ function buildIdContext(root, config, context) {
|
|
|
373
473
|
STATUS: sanitizeTemplateValue(status || "TODO", "TODO"),
|
|
374
474
|
TASK_TYPE: sanitizeTemplateValue(taskType || "FEATURE", "FEATURE"),
|
|
375
475
|
PREFIX: sanitizeTemplateValue(prefix, "COOP"),
|
|
476
|
+
NAME: name || "item",
|
|
477
|
+
NAME_SLUG: slugifyLower(name || "item"),
|
|
376
478
|
RAND: randomToken()
|
|
377
479
|
};
|
|
378
480
|
for (const [key, value] of Object.entries(fields)) {
|
|
379
481
|
if (!value || !value.trim()) continue;
|
|
380
|
-
const
|
|
482
|
+
const tokenName = normalizeNamingTokenName(key);
|
|
483
|
+
const upper = tokenName.toUpperCase();
|
|
381
484
|
if (Object.prototype.hasOwnProperty.call(map, upper)) continue;
|
|
382
|
-
|
|
485
|
+
const tokenConfig = config.idTokens[tokenName];
|
|
486
|
+
if (!tokenConfig) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const normalizedValue = normalizeNamingTokenValue(value);
|
|
490
|
+
if (tokenConfig.values.length > 0 && !tokenConfig.values.includes(normalizedValue)) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Invalid value '${value}' for naming token '${tokenName}'. Allowed values: ${tokenConfig.values.join(", ")}.`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
map[upper] = normalizedValue;
|
|
383
496
|
}
|
|
384
497
|
return map;
|
|
385
498
|
}
|
|
@@ -398,52 +511,137 @@ function replaceTemplateToken(token, contextMap) {
|
|
|
398
511
|
if (upper === "TASK_TYPE") return contextMap.TASK_TYPE;
|
|
399
512
|
if (upper === "STATUS") return contextMap.STATUS;
|
|
400
513
|
if (upper === "PREFIX") return contextMap.PREFIX;
|
|
514
|
+
if (upper === "NAME") return contextMap.NAME;
|
|
515
|
+
if (upper === "NAME_SLUG") return contextMap.NAME_SLUG;
|
|
401
516
|
const dynamic = contextMap[upper];
|
|
402
517
|
if (dynamic) return dynamic;
|
|
403
518
|
return sanitizeTemplateValue(upper);
|
|
404
519
|
}
|
|
405
|
-
function renderNamingTemplate(template, contextMap) {
|
|
520
|
+
function renderNamingTemplate(template, contextMap, entityType) {
|
|
406
521
|
const normalizedTemplate = template.trim().length > 0 ? template : DEFAULT_ID_NAMING_TEMPLATE;
|
|
407
522
|
const replaced = normalizedTemplate.replace(/<([^>]+)>/g, (_, token) => replaceTemplateToken(token, contextMap));
|
|
408
|
-
return
|
|
523
|
+
return normalizeEntityIdValue(entityType, replaced, "COOP-ID");
|
|
409
524
|
}
|
|
410
525
|
function defaultCoopAuthor(root) {
|
|
411
526
|
const actor = inferActor(root);
|
|
412
527
|
return actor.trim() || "unknown";
|
|
413
528
|
}
|
|
529
|
+
function normalizeEntityId(entityType, value) {
|
|
530
|
+
const trimmed = value.trim();
|
|
531
|
+
if (!trimmed) {
|
|
532
|
+
throw new Error(`Invalid ${entityType} id. Provide a non-empty value.`);
|
|
533
|
+
}
|
|
534
|
+
return normalizeEntityIdValue(entityType, trimmed, entityType);
|
|
535
|
+
}
|
|
536
|
+
function namingTemplatesForRoot(root) {
|
|
537
|
+
return readCoopConfig(root).idNamingTemplates;
|
|
538
|
+
}
|
|
539
|
+
function namingTokensForRoot(root) {
|
|
540
|
+
return readCoopConfig(root).idTokens;
|
|
541
|
+
}
|
|
542
|
+
function generateStableShortId(root, entityType, primaryId, existingShortIds = []) {
|
|
543
|
+
const config = readCoopConfig(root);
|
|
544
|
+
const digest = crypto.createHash("sha256").update(`${config.projectId}:${entityType}:${primaryId}`).digest("hex").toLowerCase();
|
|
545
|
+
const existing = new Set(existingShortIds.map((value) => value.toLowerCase()));
|
|
546
|
+
for (let width = 12; width <= digest.length; width += 2) {
|
|
547
|
+
const candidate = digest.slice(0, width);
|
|
548
|
+
if (!existing.has(candidate)) {
|
|
549
|
+
return candidate;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
throw new Error(`Unable to generate a unique short id for ${entityType} '${primaryId}'.`);
|
|
553
|
+
}
|
|
554
|
+
function extractDynamicTokenFlags(commandPath, knownValueOptions, knownBooleanOptions = [], argv = process.argv.slice(2)) {
|
|
555
|
+
const start = argv.findIndex((_, index) => commandPath.every((segment, offset) => argv[index + offset] === segment));
|
|
556
|
+
if (start < 0) {
|
|
557
|
+
return {};
|
|
558
|
+
}
|
|
559
|
+
const result = {};
|
|
560
|
+
const valueOptions = new Set(knownValueOptions);
|
|
561
|
+
const booleanOptions = new Set(knownBooleanOptions);
|
|
562
|
+
for (let index = start + commandPath.length; index < argv.length; index += 1) {
|
|
563
|
+
const token = argv[index] ?? "";
|
|
564
|
+
if (!token.startsWith("--")) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const [rawName, inlineValue] = token.slice(2).split("=", 2);
|
|
568
|
+
const name = rawName.trim();
|
|
569
|
+
if (!name) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (valueOptions.has(name)) {
|
|
573
|
+
if (inlineValue === void 0) {
|
|
574
|
+
index += 1;
|
|
575
|
+
}
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (booleanOptions.has(name) || name.startsWith("no-")) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const value = inlineValue ?? argv[index + 1];
|
|
582
|
+
if (!value || value.startsWith("-")) {
|
|
583
|
+
throw new Error(`Dynamic naming flag '--${name}' requires a value.`);
|
|
584
|
+
}
|
|
585
|
+
result[name] = value;
|
|
586
|
+
if (inlineValue === void 0) {
|
|
587
|
+
index += 1;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
414
592
|
function previewNamingTemplate(template, context, root = process.cwd()) {
|
|
415
593
|
const config = readCoopConfig(root);
|
|
594
|
+
const usedTemplate = template.trim().length > 0 ? template : config.idNamingTemplates[context.entityType];
|
|
595
|
+
const referencedTokens = extractTemplateTokens(usedTemplate);
|
|
596
|
+
for (const token of referencedTokens) {
|
|
597
|
+
if (!BUILT_IN_NAMING_TOKENS.has(token) && !config.idTokens[token.toLowerCase()]) {
|
|
598
|
+
throw new Error(`Naming template references unknown token <${token}>.`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
416
601
|
const contextMap = buildIdContext(root, config, context);
|
|
417
|
-
const rendered = renderNamingTemplate(
|
|
602
|
+
const rendered = renderNamingTemplate(usedTemplate, {
|
|
418
603
|
...contextMap,
|
|
419
604
|
RAND: "AB12CD34",
|
|
420
605
|
YYMMDD: "260320",
|
|
421
606
|
USER: "PKVSI"
|
|
422
|
-
});
|
|
607
|
+
}, context.entityType);
|
|
423
608
|
return rendered.replace(SEQ_MARKER, "1");
|
|
424
609
|
}
|
|
425
610
|
function generateConfiguredId(root, existingIds, context) {
|
|
426
611
|
const config = readCoopConfig(root);
|
|
612
|
+
const template = config.idNamingTemplates[context.entityType];
|
|
613
|
+
const referencedTokens = extractTemplateTokens(template);
|
|
614
|
+
for (const token of referencedTokens) {
|
|
615
|
+
if (BUILT_IN_NAMING_TOKENS.has(token)) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (!config.idTokens[token.toLowerCase()]) {
|
|
619
|
+
throw new Error(`Naming template references unknown token <${token}>.`);
|
|
620
|
+
}
|
|
621
|
+
if (!context.fields || !Object.keys(context.fields).some((key) => normalizeNamingTokenName(key) === token.toLowerCase())) {
|
|
622
|
+
throw new Error(`Naming template requires token <${token}>. Pass --${token.toLowerCase()} <value>.`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
427
625
|
const contextMap = buildIdContext(root, config, context);
|
|
428
626
|
const existing = existingIds.map((id) => id.toUpperCase());
|
|
429
627
|
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
430
|
-
const rendered = renderNamingTemplate(
|
|
628
|
+
const rendered = renderNamingTemplate(template, {
|
|
431
629
|
...contextMap,
|
|
432
630
|
RAND: randomToken()
|
|
433
|
-
});
|
|
631
|
+
}, context.entityType);
|
|
434
632
|
const seqMarker = SEQ_MARKER;
|
|
435
|
-
const
|
|
436
|
-
if (!
|
|
437
|
-
if (!existing.includes(
|
|
438
|
-
return
|
|
633
|
+
const normalizedRendered = context.entityType === "track" || context.entityType === "delivery" ? rendered : rendered.toUpperCase();
|
|
634
|
+
if (!normalizedRendered.includes(seqMarker)) {
|
|
635
|
+
if (!existing.includes(normalizedRendered.toUpperCase())) {
|
|
636
|
+
return normalizedRendered;
|
|
439
637
|
}
|
|
440
638
|
continue;
|
|
441
639
|
}
|
|
442
|
-
const [prefix, suffix] =
|
|
640
|
+
const [prefix, suffix] = normalizedRendered.split(seqMarker);
|
|
443
641
|
const nextSeq = sequenceForPattern(existing, prefix ?? "", suffix ?? "");
|
|
444
642
|
const seq = padSequence(nextSeq, config.idSeqPadding);
|
|
445
643
|
const candidate = `${prefix ?? ""}${seq}${suffix ?? ""}`.replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
446
|
-
if (!existing.includes(candidate)) {
|
|
644
|
+
if (!existing.includes(candidate.toUpperCase())) {
|
|
447
645
|
return candidate;
|
|
448
646
|
}
|
|
449
647
|
}
|
|
@@ -551,32 +749,58 @@ function rebuildAliasIndex(root) {
|
|
|
551
749
|
ensureCoopInitialized(root);
|
|
552
750
|
const items = {};
|
|
553
751
|
const aliases = {};
|
|
752
|
+
const shortIds = {};
|
|
554
753
|
const ids = /* @__PURE__ */ new Set();
|
|
555
754
|
const taskFiles = listTaskFiles(root);
|
|
755
|
+
const usedTaskShortIds = taskFiles.map((filePath) => parseTaskFile2(filePath)).map((parsed) => parsed.task.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
556
756
|
for (const filePath of taskFiles) {
|
|
557
757
|
const parsed = parseTaskFile2(filePath);
|
|
558
758
|
const id = parsed.task.id.toUpperCase();
|
|
559
759
|
const source = path2.relative(root, filePath);
|
|
760
|
+
const shortId = parsed.task.short_id?.trim() ? parsed.task.short_id.trim().toLowerCase() : generateStableShortId(root, "task", parsed.task.id, usedTaskShortIds);
|
|
761
|
+
if (!parsed.task.short_id?.trim()) {
|
|
762
|
+
usedTaskShortIds.push(shortId);
|
|
763
|
+
const nextRaw = { ...parsed.raw, short_id: shortId };
|
|
764
|
+
const nextTask = { ...parsed.task, short_id: shortId };
|
|
765
|
+
writeTask(nextTask, { body: parsed.body, raw: nextRaw, filePath });
|
|
766
|
+
}
|
|
560
767
|
ids.add(id);
|
|
561
768
|
items[id] = {
|
|
562
769
|
type: "task",
|
|
563
770
|
aliases: parseAliases(parsed.raw, source),
|
|
564
|
-
file: toPosixPath(path2.relative(root, filePath))
|
|
771
|
+
file: toPosixPath(path2.relative(root, filePath)),
|
|
772
|
+
short_id: shortId
|
|
565
773
|
};
|
|
566
774
|
}
|
|
567
775
|
const ideaFiles = listIdeaFiles(root);
|
|
776
|
+
const usedIdeaShortIds = ideaFiles.map((filePath) => parseIdeaFile(filePath)).map((parsed) => parsed.idea.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
568
777
|
for (const filePath of ideaFiles) {
|
|
569
778
|
const parsed = parseIdeaFile(filePath);
|
|
570
779
|
const id = parsed.idea.id.toUpperCase();
|
|
571
780
|
const source = path2.relative(root, filePath);
|
|
781
|
+
const shortId = parsed.idea.short_id?.trim() ? parsed.idea.short_id.trim().toLowerCase() : generateStableShortId(root, "idea", parsed.idea.id, usedIdeaShortIds);
|
|
782
|
+
if (!parsed.idea.short_id?.trim()) {
|
|
783
|
+
usedIdeaShortIds.push(shortId);
|
|
784
|
+
const nextRaw = { ...parsed.raw, short_id: shortId };
|
|
785
|
+
const output2 = stringifyFrontmatter(nextRaw, parsed.body);
|
|
786
|
+
fs2.writeFileSync(filePath, output2, "utf8");
|
|
787
|
+
}
|
|
572
788
|
ids.add(id);
|
|
573
789
|
items[id] = {
|
|
574
790
|
type: "idea",
|
|
575
791
|
aliases: parseAliases(parsed.raw, source),
|
|
576
|
-
file: toPosixPath(path2.relative(root, filePath))
|
|
792
|
+
file: toPosixPath(path2.relative(root, filePath)),
|
|
793
|
+
short_id: shortId
|
|
577
794
|
};
|
|
578
795
|
}
|
|
579
796
|
for (const [id, item] of Object.entries(items)) {
|
|
797
|
+
if (item.short_id) {
|
|
798
|
+
const existingShort = shortIds[item.short_id];
|
|
799
|
+
if (existingShort && existingShort.id !== id) {
|
|
800
|
+
throw new Error(`Short id '${item.short_id}' is already mapped to '${existingShort.id}'.`);
|
|
801
|
+
}
|
|
802
|
+
shortIds[item.short_id] = { id, type: item.type, file: item.file, short_id: item.short_id };
|
|
803
|
+
}
|
|
580
804
|
for (const alias of item.aliases) {
|
|
581
805
|
if (ids.has(alias)) {
|
|
582
806
|
throw new Error(`Alias '${alias}' conflicts with existing item id '${alias}'.`);
|
|
@@ -592,6 +816,7 @@ function rebuildAliasIndex(root) {
|
|
|
592
816
|
version: 1,
|
|
593
817
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
594
818
|
aliases,
|
|
819
|
+
short_ids: shortIds,
|
|
595
820
|
items
|
|
596
821
|
};
|
|
597
822
|
writeIndexFile(root, data);
|
|
@@ -604,7 +829,7 @@ function loadAliasIndex(root) {
|
|
|
604
829
|
return rebuildAliasIndex(root);
|
|
605
830
|
}
|
|
606
831
|
const parsed = readIndexFile(root, aliasIndexPath);
|
|
607
|
-
if (!parsed || typeof parsed !== "object" || !parsed.aliases || !parsed.items) {
|
|
832
|
+
if (!parsed || typeof parsed !== "object" || !parsed.aliases || !parsed.items || !parsed.short_ids) {
|
|
608
833
|
return rebuildAliasIndex(root);
|
|
609
834
|
}
|
|
610
835
|
return parsed;
|
|
@@ -619,6 +844,27 @@ function resolveReference(root, idOrAlias, expectedType) {
|
|
|
619
844
|
}
|
|
620
845
|
return { id: idCandidate, type: item.type, file: item.file };
|
|
621
846
|
}
|
|
847
|
+
const shortCandidate = idOrAlias.trim().toLowerCase();
|
|
848
|
+
const shortMatch = index.short_ids[shortCandidate];
|
|
849
|
+
if (shortMatch) {
|
|
850
|
+
if (expectedType && shortMatch.type !== expectedType) {
|
|
851
|
+
throw new Error(`'${idOrAlias}' resolves to ${shortMatch.type} '${shortMatch.id}', expected ${expectedType}.`);
|
|
852
|
+
}
|
|
853
|
+
return shortMatch;
|
|
854
|
+
}
|
|
855
|
+
if (shortCandidate.length >= 6) {
|
|
856
|
+
const prefixMatches = Object.entries(index.short_ids).filter(([shortId]) => shortId.startsWith(shortCandidate)).map(([, target]) => target);
|
|
857
|
+
if (prefixMatches.length === 1) {
|
|
858
|
+
const match2 = prefixMatches[0];
|
|
859
|
+
if (expectedType && match2.type !== expectedType) {
|
|
860
|
+
throw new Error(`'${idOrAlias}' resolves to ${match2.type} '${match2.id}', expected ${expectedType}.`);
|
|
861
|
+
}
|
|
862
|
+
return match2;
|
|
863
|
+
}
|
|
864
|
+
if (prefixMatches.length > 1) {
|
|
865
|
+
throw new Error(`Ambiguous short id '${idOrAlias}'. Candidates: ${prefixMatches.map((entry) => entry.id).join(", ")}.`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
622
868
|
const alias = normalizeAlias(idOrAlias);
|
|
623
869
|
const match = index.aliases[alias];
|
|
624
870
|
if (!match) {
|
|
@@ -725,17 +971,28 @@ function removeAliases(root, idOrAlias, values) {
|
|
|
725
971
|
}
|
|
726
972
|
|
|
727
973
|
// src/utils/table.ts
|
|
974
|
+
var ANSI_PATTERN = /\u001B\[[0-9;]*m/g;
|
|
975
|
+
function visibleLength(value) {
|
|
976
|
+
return value.replace(ANSI_PATTERN, "").length;
|
|
977
|
+
}
|
|
978
|
+
function padEndAnsi(value, width) {
|
|
979
|
+
const visible = visibleLength(value);
|
|
980
|
+
if (visible >= width) {
|
|
981
|
+
return value;
|
|
982
|
+
}
|
|
983
|
+
return `${value}${" ".repeat(width - visible)}`;
|
|
984
|
+
}
|
|
728
985
|
function formatTable(headers, rows) {
|
|
729
986
|
const table = [headers, ...rows];
|
|
730
987
|
const widths = [];
|
|
731
988
|
for (let col = 0; col < headers.length; col += 1) {
|
|
732
|
-
let width = headers[col]
|
|
989
|
+
let width = visibleLength(headers[col] ?? "");
|
|
733
990
|
for (const row of table) {
|
|
734
|
-
width = Math.max(width, (row[col] ?? "")
|
|
991
|
+
width = Math.max(width, visibleLength(row[col] ?? ""));
|
|
735
992
|
}
|
|
736
993
|
widths.push(width + 2);
|
|
737
994
|
}
|
|
738
|
-
return table.map((row) => row.map((cell, index) => (cell ?? ""
|
|
995
|
+
return table.map((row) => row.map((cell, index) => padEndAnsi(cell ?? "", widths[index])).join("").trimEnd()).join("\n");
|
|
739
996
|
}
|
|
740
997
|
|
|
741
998
|
// src/commands/alias.ts
|
|
@@ -1398,38 +1655,101 @@ import {
|
|
|
1398
1655
|
stringifyFrontmatter as stringifyFrontmatter2,
|
|
1399
1656
|
validateStructural as validateStructural2,
|
|
1400
1657
|
validateSemantic,
|
|
1658
|
+
writeYamlFile as writeYamlFile3,
|
|
1401
1659
|
writeTask as writeTask3
|
|
1402
1660
|
} from "@kitsy/coop-core";
|
|
1403
|
-
function
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1661
|
+
function writeTrackFile(filePath, track) {
|
|
1662
|
+
writeYamlFile3(filePath, track);
|
|
1663
|
+
}
|
|
1664
|
+
function writeDeliveryFile(filePath, delivery, raw, body) {
|
|
1665
|
+
const payload = { ...raw, ...delivery };
|
|
1666
|
+
if (body.trim().length > 0) {
|
|
1667
|
+
fs3.writeFileSync(filePath, stringifyFrontmatter2(payload, body), "utf8");
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
writeYamlFile3(filePath, payload);
|
|
1671
|
+
}
|
|
1672
|
+
function loadTrackEntries(root) {
|
|
1673
|
+
const rawEntries = listTrackFiles(root).map((filePath) => ({ track: parseYamlFile2(filePath), filePath })).sort((a, b) => a.track.id.localeCompare(b.track.id));
|
|
1674
|
+
const used = rawEntries.map((entry) => entry.track.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
1675
|
+
let mutated = false;
|
|
1676
|
+
for (const entry of rawEntries) {
|
|
1677
|
+
if (entry.track.short_id?.trim()) {
|
|
1678
|
+
continue;
|
|
1409
1679
|
}
|
|
1680
|
+
entry.track.short_id = generateStableShortId(root, "track", entry.track.id, used);
|
|
1681
|
+
used.push(entry.track.short_id);
|
|
1682
|
+
writeTrackFile(entry.filePath, entry.track);
|
|
1683
|
+
mutated = true;
|
|
1410
1684
|
}
|
|
1411
|
-
return map;
|
|
1685
|
+
return mutated ? rawEntries.map((entry) => ({ track: parseYamlFile2(entry.filePath), filePath: entry.filePath })) : rawEntries;
|
|
1412
1686
|
}
|
|
1413
|
-
function
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1687
|
+
function loadDeliveryEntries(root) {
|
|
1688
|
+
const rawEntries = listDeliveryFiles(root).map((filePath) => {
|
|
1689
|
+
const parsed = parseDeliveryFile(filePath);
|
|
1690
|
+
return { delivery: parsed.delivery, filePath, body: parsed.body, raw: parsed.raw };
|
|
1691
|
+
}).sort((a, b) => a.delivery.id.localeCompare(b.delivery.id));
|
|
1692
|
+
const used = rawEntries.map((entry) => entry.delivery.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
1693
|
+
let mutated = false;
|
|
1694
|
+
for (const entry of rawEntries) {
|
|
1695
|
+
if (entry.delivery.short_id?.trim()) {
|
|
1696
|
+
continue;
|
|
1419
1697
|
}
|
|
1698
|
+
entry.delivery.short_id = generateStableShortId(root, "delivery", entry.delivery.id, used);
|
|
1699
|
+
used.push(entry.delivery.short_id);
|
|
1700
|
+
writeDeliveryFile(entry.filePath, entry.delivery, entry.raw, entry.body);
|
|
1701
|
+
mutated = true;
|
|
1420
1702
|
}
|
|
1421
|
-
return map
|
|
1703
|
+
return mutated ? rawEntries.map((entry) => {
|
|
1704
|
+
const parsed = parseDeliveryFile(entry.filePath);
|
|
1705
|
+
return { delivery: parsed.delivery, filePath: entry.filePath, body: parsed.body, raw: parsed.raw };
|
|
1706
|
+
}) : rawEntries;
|
|
1707
|
+
}
|
|
1708
|
+
function resolveUniqueShortId(entries, value) {
|
|
1709
|
+
const normalized = value.trim().toLowerCase();
|
|
1710
|
+
if (!normalized) return void 0;
|
|
1711
|
+
const exact = entries.find((entry) => entry.short_id?.toLowerCase() === normalized);
|
|
1712
|
+
if (exact) return exact;
|
|
1713
|
+
if (normalized.length < 6) return void 0;
|
|
1714
|
+
const matches = entries.filter((entry) => entry.short_id?.toLowerCase().startsWith(normalized));
|
|
1715
|
+
if (matches.length === 1) {
|
|
1716
|
+
return matches[0];
|
|
1717
|
+
}
|
|
1718
|
+
if (matches.length > 1) {
|
|
1719
|
+
throw new Error(`Ambiguous short id '${value}'. Candidates: ${matches.map((entry) => entry.id).join(", ")}.`);
|
|
1720
|
+
}
|
|
1721
|
+
return void 0;
|
|
1422
1722
|
}
|
|
1423
1723
|
function resolveExistingTrackId(root, trackId) {
|
|
1424
1724
|
const normalized = trackId.trim().toLowerCase();
|
|
1425
1725
|
if (!normalized) return void 0;
|
|
1426
1726
|
if (normalized === "unassigned") return "unassigned";
|
|
1427
|
-
|
|
1727
|
+
const entries = loadTrackEntries(root);
|
|
1728
|
+
const byId = entries.find((entry) => entry.track.id.trim().toLowerCase() === normalized);
|
|
1729
|
+
if (byId) return byId.track.id.trim();
|
|
1730
|
+
const byShort = resolveUniqueShortId(entries.map((entry) => entry.track), normalized);
|
|
1731
|
+
if (byShort) return byShort.id.trim();
|
|
1732
|
+
const byName = entries.filter((entry) => entry.track.name.trim().toLowerCase() === normalized);
|
|
1733
|
+
if (byName.length === 1) return byName[0].track.id.trim();
|
|
1734
|
+
if (byName.length > 1) {
|
|
1735
|
+
throw new Error(`Multiple tracks match '${trackId}'. Use the track id or short id.`);
|
|
1736
|
+
}
|
|
1737
|
+
return void 0;
|
|
1428
1738
|
}
|
|
1429
1739
|
function resolveExistingDeliveryId(root, deliveryId) {
|
|
1430
1740
|
const normalized = deliveryId.trim().toLowerCase();
|
|
1431
1741
|
if (!normalized) return void 0;
|
|
1432
|
-
|
|
1742
|
+
const entries = loadDeliveryEntries(root);
|
|
1743
|
+
const byId = entries.find((entry) => entry.delivery.id.trim().toLowerCase() === normalized);
|
|
1744
|
+
if (byId) return byId.delivery.id.trim();
|
|
1745
|
+
const byShort = resolveUniqueShortId(entries.map((entry) => entry.delivery), normalized);
|
|
1746
|
+
if (byShort) return byShort.id.trim();
|
|
1747
|
+
const byName = entries.filter((entry) => entry.delivery.name.trim().toLowerCase() === normalized);
|
|
1748
|
+
if (byName.length === 1) return byName[0].delivery.id.trim();
|
|
1749
|
+
if (byName.length > 1) {
|
|
1750
|
+
throw new Error(`Multiple deliveries match '${deliveryId}'. Use the delivery id or short id.`);
|
|
1751
|
+
}
|
|
1752
|
+
return void 0;
|
|
1433
1753
|
}
|
|
1434
1754
|
function assertExistingTrackId(root, trackId) {
|
|
1435
1755
|
const resolved = resolveExistingTrackId(root, trackId);
|
|
@@ -1480,11 +1800,27 @@ function resolveIdeaFile(root, idOrAlias) {
|
|
|
1480
1800
|
}
|
|
1481
1801
|
function loadTaskEntry(root, idOrAlias) {
|
|
1482
1802
|
const filePath = resolveTaskFile(root, idOrAlias);
|
|
1483
|
-
|
|
1803
|
+
const parsed = parseTaskFile4(filePath);
|
|
1804
|
+
if (!parsed.task.short_id?.trim()) {
|
|
1805
|
+
const shortId = generateStableShortId(root, "task", parsed.task.id);
|
|
1806
|
+
const nextTask = { ...parsed.task, short_id: shortId };
|
|
1807
|
+
const nextRaw = { ...parsed.raw, short_id: shortId };
|
|
1808
|
+
writeTask3(nextTask, { body: parsed.body, raw: nextRaw, filePath });
|
|
1809
|
+
return { filePath, parsed: parseTaskFile4(filePath) };
|
|
1810
|
+
}
|
|
1811
|
+
return { filePath, parsed };
|
|
1484
1812
|
}
|
|
1485
1813
|
function loadIdeaEntry(root, idOrAlias) {
|
|
1486
1814
|
const filePath = resolveIdeaFile(root, idOrAlias);
|
|
1487
|
-
|
|
1815
|
+
const parsed = parseIdeaFile2(filePath);
|
|
1816
|
+
if (!parsed.idea.short_id?.trim()) {
|
|
1817
|
+
const shortId = generateStableShortId(root, "idea", parsed.idea.id);
|
|
1818
|
+
const nextRaw = { ...parsed.raw, short_id: shortId };
|
|
1819
|
+
const output2 = stringifyFrontmatter2(nextRaw, parsed.body);
|
|
1820
|
+
fs3.writeFileSync(filePath, output2, "utf8");
|
|
1821
|
+
return { filePath, parsed: parseIdeaFile2(filePath) };
|
|
1822
|
+
}
|
|
1823
|
+
return { filePath, parsed };
|
|
1488
1824
|
}
|
|
1489
1825
|
function writeIdeaFile(filePath, parsed, idea, body = parsed.body) {
|
|
1490
1826
|
const output2 = stringifyFrontmatter2({ ...parsed.raw, ...idea }, body);
|
|
@@ -1566,18 +1902,18 @@ function taskEffectivePriority(task, track) {
|
|
|
1566
1902
|
return effective_priority(task, track);
|
|
1567
1903
|
}
|
|
1568
1904
|
function resolveDeliveryEntry(root, ref) {
|
|
1569
|
-
const files = listDeliveryFiles(root);
|
|
1570
1905
|
const target = ref.trim().toLowerCase();
|
|
1571
|
-
const entries =
|
|
1572
|
-
const parsed = parseDeliveryFile(filePath);
|
|
1573
|
-
return { filePath, delivery: parsed.delivery, body: parsed.body };
|
|
1574
|
-
});
|
|
1906
|
+
const entries = loadDeliveryEntries(root);
|
|
1575
1907
|
const direct = entries.find((entry) => entry.delivery.id.toLowerCase() === target);
|
|
1576
1908
|
if (direct) return direct;
|
|
1909
|
+
const byShort = resolveUniqueShortId(entries.map((entry) => entry.delivery), target);
|
|
1910
|
+
if (byShort) {
|
|
1911
|
+
return entries.find((entry) => entry.delivery.id === byShort.id);
|
|
1912
|
+
}
|
|
1577
1913
|
const byName = entries.filter((entry) => entry.delivery.name.toLowerCase() === target);
|
|
1578
1914
|
if (byName.length === 1) return byName[0];
|
|
1579
1915
|
if (byName.length > 1) {
|
|
1580
|
-
throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id.`);
|
|
1916
|
+
throw new Error(`Multiple deliveries match '${ref}'. Use the delivery id or short id.`);
|
|
1581
1917
|
}
|
|
1582
1918
|
throw new Error(`Delivery '${ref}' not found.`);
|
|
1583
1919
|
}
|
|
@@ -1716,6 +2052,7 @@ function selectTopReadyTask(root = resolveRepoRoot(), options = {}) {
|
|
|
1716
2052
|
function formatSelectedTask(entry, selection = {}) {
|
|
1717
2053
|
const lines = [
|
|
1718
2054
|
`Selected task: ${entry.task.id}`,
|
|
2055
|
+
`Short ID: ${entry.task.short_id ?? "-"}`,
|
|
1719
2056
|
`Title: ${entry.task.title}`,
|
|
1720
2057
|
`Priority: ${selection.track && entry.task.priority_context?.[selection.track] ? `${entry.task.priority ?? "-"} -> ${entry.task.priority_context[selection.track]}` : entry.task.priority ?? "-"}`,
|
|
1721
2058
|
`Track: ${entry.task.track ?? "-"}`,
|
|
@@ -2038,7 +2375,9 @@ function writeIdNamingValue(root, value) {
|
|
|
2038
2375
|
const config = readCoopConfig(root).raw;
|
|
2039
2376
|
const next = { ...config };
|
|
2040
2377
|
const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
|
|
2041
|
-
idRaw.naming
|
|
2378
|
+
const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
|
|
2379
|
+
namingRaw.task = nextValue;
|
|
2380
|
+
idRaw.naming = namingRaw;
|
|
2042
2381
|
next.id = idRaw;
|
|
2043
2382
|
writeCoopConfig(root, next);
|
|
2044
2383
|
}
|
|
@@ -2209,8 +2548,10 @@ import {
|
|
|
2209
2548
|
TaskType as TaskType2,
|
|
2210
2549
|
check_permission as check_permission2,
|
|
2211
2550
|
load_auth_config as load_auth_config2,
|
|
2551
|
+
parseDeliveryFile as parseDeliveryFile2,
|
|
2212
2552
|
parseTaskFile as parseTaskFile7,
|
|
2213
|
-
|
|
2553
|
+
parseYamlFile as parseYamlFile3,
|
|
2554
|
+
writeYamlFile as writeYamlFile4,
|
|
2214
2555
|
stringifyFrontmatter as stringifyFrontmatter4,
|
|
2215
2556
|
writeTask as writeTask6
|
|
2216
2557
|
} from "@kitsy/coop-core";
|
|
@@ -2713,6 +3054,41 @@ function plusDaysIso(days) {
|
|
|
2713
3054
|
function unique2(values) {
|
|
2714
3055
|
return Array.from(new Set(values));
|
|
2715
3056
|
}
|
|
3057
|
+
function assertNoCaseInsensitiveNameConflict(kind, entries, candidateId, candidateName) {
|
|
3058
|
+
const normalizedName = candidateName.trim().toLowerCase();
|
|
3059
|
+
if (!normalizedName) {
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
const conflict = entries.find(
|
|
3063
|
+
(entry) => entry.id.trim().toLowerCase() !== candidateId.trim().toLowerCase() && entry.name.trim().toLowerCase() === normalizedName
|
|
3064
|
+
);
|
|
3065
|
+
if (!conflict) {
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
`${kind === "track" ? "Track" : "Delivery"} name '${candidateName}' conflicts with existing ${kind} '${conflict.id}' (${conflict.name}). Names are matched case-insensitively.`
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
function taskShortIds(root) {
|
|
3073
|
+
return listTaskFiles(root).map((filePath) => parseTaskFile7(filePath).task.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
3074
|
+
}
|
|
3075
|
+
function ideaShortIds(root) {
|
|
3076
|
+
return listIdeaFiles(root).map((filePath) => parseIdeaFile3(filePath).idea.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
3077
|
+
}
|
|
3078
|
+
function trackShortIds(root) {
|
|
3079
|
+
return listTrackFiles(root).map((filePath) => parseYamlFile3(filePath).short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
3080
|
+
}
|
|
3081
|
+
function deliveryShortIds(root) {
|
|
3082
|
+
return listDeliveryFiles(root).map((filePath) => parseDeliveryFile2(filePath).delivery.short_id?.trim().toLowerCase()).filter((value) => Boolean(value));
|
|
3083
|
+
}
|
|
3084
|
+
function assertKnownDynamicFields(root, fields) {
|
|
3085
|
+
const tokens = namingTokensForRoot(root);
|
|
3086
|
+
for (const key of Object.keys(fields)) {
|
|
3087
|
+
if (!tokens[key]) {
|
|
3088
|
+
throw new Error(`Unknown naming token '${key}'. Define it first with \`coop naming token create ${key}\`.`);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
2716
3092
|
function resolveIdeaFile2(root, idOrAlias) {
|
|
2717
3093
|
const target = resolveReference(root, idOrAlias, "idea");
|
|
2718
3094
|
return path8.join(root, ...target.file.split("/"));
|
|
@@ -2741,10 +3117,33 @@ function makeTaskDraft(input2) {
|
|
|
2741
3117
|
}
|
|
2742
3118
|
function registerCreateCommand(program) {
|
|
2743
3119
|
const create = program.command("create").description("Create COOP entities");
|
|
2744
|
-
create.command("task").description("Create a task").argument("[title]", "Task title").option("--id <id>", "Task id").option("--from <idea>", "Create task(s) from an idea id/alias").option("--ai", "Use AI-assisted decomposition for --from").option("--title <title>", "Task title").option("--type <type>", `Task type (${Object.values(TaskType2).join(", ")})`).option("--status <status>", `Task status (${Object.values(TaskStatus3).join(", ")})`).option("--track <track>", "Home/origin track id").option("--delivery <delivery>", "Primary delivery id").option("--priority <priority>", "Task priority").option("--body <body>", "Markdown body").option("--acceptance <items>", "Comma-separated acceptance criteria", collectMultiValue, []).option("--tests-required <items>", "Comma-separated required tests", collectMultiValue, []).option("--authority-ref <ref>", "Authority document reference", collectMultiValue, []).option("--derived-ref <ref>", "Derived planning document reference", collectMultiValue, []).option("--from-file <path>", "Create task(s) from task draft/refinement draft file").option("--stdin", "Read task draft/refinement draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
|
|
3120
|
+
create.command("task").description("Create a task").allowUnknownOption().allowExcessArguments().argument("[title]", "Task title").option("--id <id>", "Task id").option("--from <idea>", "Create task(s) from an idea id/alias").option("--ai", "Use AI-assisted decomposition for --from").option("--title <title>", "Task title").option("--type <type>", `Task type (${Object.values(TaskType2).join(", ")})`).option("--status <status>", `Task status (${Object.values(TaskStatus3).join(", ")})`).option("--track <track>", "Home/origin track id").option("--delivery <delivery>", "Primary delivery id").option("--priority <priority>", "Task priority").option("--body <body>", "Markdown body").option("--acceptance <items>", "Comma-separated acceptance criteria", collectMultiValue, []).option("--tests-required <items>", "Comma-separated required tests", collectMultiValue, []).option("--authority-ref <ref>", "Authority document reference", collectMultiValue, []).option("--derived-ref <ref>", "Derived planning document reference", collectMultiValue, []).option("--from-file <path>", "Create task(s) from task draft/refinement draft file").option("--stdin", "Read task draft/refinement draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
|
|
2745
3121
|
const root = resolveRepoRoot();
|
|
2746
3122
|
const coop = ensureCoopInitialized(root);
|
|
2747
3123
|
const interactive = Boolean(options.interactive);
|
|
3124
|
+
const dynamicFields = extractDynamicTokenFlags(
|
|
3125
|
+
["create", "task"],
|
|
3126
|
+
[
|
|
3127
|
+
"id",
|
|
3128
|
+
"from",
|
|
3129
|
+
"title",
|
|
3130
|
+
"type",
|
|
3131
|
+
"status",
|
|
3132
|
+
"track",
|
|
3133
|
+
"delivery",
|
|
3134
|
+
"priority",
|
|
3135
|
+
"body",
|
|
3136
|
+
"acceptance",
|
|
3137
|
+
"tests-required",
|
|
3138
|
+
"authority-ref",
|
|
3139
|
+
"derived-ref",
|
|
3140
|
+
"from-file",
|
|
3141
|
+
"stdin",
|
|
3142
|
+
"interactive",
|
|
3143
|
+
"ai"
|
|
3144
|
+
]
|
|
3145
|
+
);
|
|
3146
|
+
assertKnownDynamicFields(root, dynamicFields);
|
|
2748
3147
|
if (options.fromFile?.trim() || options.stdin) {
|
|
2749
3148
|
if (options.id || options.from || options.ai || options.title || titleArg || options.type || options.status || options.track || options.delivery || options.priority || options.body || (options.acceptance?.length ?? 0) > 0 || (options.testsRequired?.length ?? 0) > 0 || (options.authorityRef?.length ?? 0) > 0 || (options.derivedRef?.length ?? 0) > 0) {
|
|
2750
3149
|
throw new Error("Cannot combine --from-file/--stdin with direct task field flags. Use one input mode.");
|
|
@@ -2854,23 +3253,28 @@ function registerCreateCommand(program) {
|
|
|
2854
3253
|
}
|
|
2855
3254
|
const existingIds = listTaskFiles(root).map((filePath) => path8.basename(filePath, ".md"));
|
|
2856
3255
|
const createdIds = [];
|
|
3256
|
+
const existingShortIds = taskShortIds(root);
|
|
3257
|
+
const createdShortIds = [];
|
|
2857
3258
|
for (let index = 0; index < drafts.length; index += 1) {
|
|
2858
3259
|
const draft = drafts[index];
|
|
2859
3260
|
if (!draft) continue;
|
|
2860
|
-
const id = options.id?.trim()
|
|
3261
|
+
const id = (options.id?.trim() ? normalizeEntityId("task", options.id) : void 0) || generateConfiguredId(root, [...existingIds, ...createdIds], {
|
|
2861
3262
|
entityType: "task",
|
|
2862
3263
|
title: draft.title,
|
|
2863
3264
|
taskType: draft.type,
|
|
2864
3265
|
track: draft.track,
|
|
2865
3266
|
status: draft.status,
|
|
3267
|
+
name: draft.title,
|
|
2866
3268
|
fields: {
|
|
2867
3269
|
track: draft.track,
|
|
2868
3270
|
type: draft.type,
|
|
2869
|
-
feature: draft.track || draft.type
|
|
3271
|
+
feature: draft.track || draft.type,
|
|
3272
|
+
...dynamicFields
|
|
2870
3273
|
}
|
|
2871
3274
|
});
|
|
2872
3275
|
const task = {
|
|
2873
3276
|
id,
|
|
3277
|
+
short_id: generateStableShortId(root, "task", id, [...existingShortIds, ...createdShortIds]),
|
|
2874
3278
|
title: draft.title,
|
|
2875
3279
|
type: draft.type,
|
|
2876
3280
|
status: draft.status,
|
|
@@ -2895,6 +3299,9 @@ function registerCreateCommand(program) {
|
|
|
2895
3299
|
filePath
|
|
2896
3300
|
});
|
|
2897
3301
|
createdIds.push(id);
|
|
3302
|
+
if (normalizedTask.short_id) {
|
|
3303
|
+
createdShortIds.push(normalizedTask.short_id);
|
|
3304
|
+
}
|
|
2898
3305
|
console.log(`Created task: ${path8.relative(root, filePath)}`);
|
|
2899
3306
|
}
|
|
2900
3307
|
if (sourceIdeaPath && sourceIdeaParsed && createdIds.length > 0) {
|
|
@@ -2908,10 +3315,15 @@ function registerCreateCommand(program) {
|
|
|
2908
3315
|
console.log(`Linked ${createdIds.length} task(s) to idea: ${sourceIdeaParsed.idea.id}`);
|
|
2909
3316
|
}
|
|
2910
3317
|
});
|
|
2911
|
-
create.command("idea").description("Create an idea").argument("[title]", "Idea title").option("--id <id>", "Idea id").option("--title <title>", "Idea title").option("--author <author>", "Idea author").option("--source <source>", "Idea source").option("--status <status>", `Idea status (${Object.values(IdeaStatus2).join(", ")})`).option("--tags <tags>", "Comma-separated tags").option("--body <body>", "Markdown body").option("--from-file <path>", "Create an idea from an idea draft file").option("--stdin", "Read idea draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
|
|
3318
|
+
create.command("idea").description("Create an idea").allowUnknownOption().allowExcessArguments().argument("[title]", "Idea title").option("--id <id>", "Idea id").option("--title <title>", "Idea title").option("--author <author>", "Idea author").option("--source <source>", "Idea source").option("--status <status>", `Idea status (${Object.values(IdeaStatus2).join(", ")})`).option("--tags <tags>", "Comma-separated tags").option("--body <body>", "Markdown body").option("--from-file <path>", "Create an idea from an idea draft file").option("--stdin", "Read idea draft from stdin").option("--interactive", "Prompt for optional fields").action(async (titleArg, options) => {
|
|
2912
3319
|
const root = resolveRepoRoot();
|
|
2913
3320
|
const coop = ensureCoopInitialized(root);
|
|
2914
3321
|
const interactive = Boolean(options.interactive);
|
|
3322
|
+
const dynamicFields = extractDynamicTokenFlags(
|
|
3323
|
+
["create", "idea"],
|
|
3324
|
+
["id", "title", "author", "source", "status", "tags", "body", "from-file", "stdin", "interactive"]
|
|
3325
|
+
);
|
|
3326
|
+
assertKnownDynamicFields(root, dynamicFields);
|
|
2915
3327
|
if (options.fromFile?.trim() || options.stdin) {
|
|
2916
3328
|
if (options.id || options.title || titleArg || options.author || options.source || options.status || options.tags || options.body) {
|
|
2917
3329
|
throw new Error("Cannot combine --from-file/--stdin with direct idea field flags. Use one input mode.");
|
|
@@ -2933,17 +3345,21 @@ function registerCreateCommand(program) {
|
|
|
2933
3345
|
const tags = options.tags ? parseCsv(options.tags) : interactive ? parseCsv(await ask("Tags (comma-separated)", "")) : [];
|
|
2934
3346
|
const body = options.body ?? (interactive ? await ask("Idea body (optional)", "") : "");
|
|
2935
3347
|
const existingIds = listIdeaFiles(root).map((filePath2) => path8.basename(filePath2, ".md"));
|
|
2936
|
-
const id = options.id?.trim()
|
|
3348
|
+
const id = (options.id?.trim() ? normalizeEntityId("idea", options.id) : void 0) || generateConfiguredId(root, existingIds, {
|
|
2937
3349
|
entityType: "idea",
|
|
2938
3350
|
title,
|
|
3351
|
+
name: title,
|
|
2939
3352
|
status,
|
|
2940
3353
|
fields: {
|
|
2941
3354
|
source,
|
|
2942
|
-
author
|
|
3355
|
+
author,
|
|
3356
|
+
...dynamicFields
|
|
2943
3357
|
}
|
|
2944
3358
|
});
|
|
3359
|
+
const shortId = generateStableShortId(root, "idea", id, ideaShortIds(root));
|
|
2945
3360
|
const frontmatter = {
|
|
2946
3361
|
id,
|
|
3362
|
+
short_id: shortId,
|
|
2947
3363
|
title,
|
|
2948
3364
|
created: todayIsoDate(),
|
|
2949
3365
|
aliases: [],
|
|
@@ -2960,10 +3376,15 @@ function registerCreateCommand(program) {
|
|
|
2960
3376
|
fs7.writeFileSync(filePath, stringifyFrontmatter4(frontmatter, body), "utf8");
|
|
2961
3377
|
console.log(`Created idea: ${path8.relative(root, filePath)}`);
|
|
2962
3378
|
});
|
|
2963
|
-
create.command("track").description("Create a track").argument("[name]", "Track name").option("--id <id>", "Track id").option("--name <name>", "Track name").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--max-wip <number>", "Max concurrent tasks").option("--allowed-types <types>", "Comma-separated allowed task types").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
|
|
3379
|
+
create.command("track").description("Create a track").allowUnknownOption().allowExcessArguments().argument("[name]", "Track name").option("--id <id>", "Track id").option("--name <name>", "Track name").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--max-wip <number>", "Max concurrent tasks").option("--allowed-types <types>", "Comma-separated allowed task types").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
|
|
2964
3380
|
const root = resolveRepoRoot();
|
|
2965
3381
|
const coop = ensureCoopInitialized(root);
|
|
2966
3382
|
const interactive = Boolean(options.interactive);
|
|
3383
|
+
const dynamicFields = extractDynamicTokenFlags(
|
|
3384
|
+
["create", "track"],
|
|
3385
|
+
["id", "name", "profiles", "max-wip", "allowed-types", "interactive"]
|
|
3386
|
+
);
|
|
3387
|
+
assertKnownDynamicFields(root, dynamicFields);
|
|
2967
3388
|
const name = options.name?.trim() || nameArg?.trim() || await ask("Track name");
|
|
2968
3389
|
if (!name) throw new Error("Track name is required.");
|
|
2969
3390
|
const capacityProfiles = unique2(
|
|
@@ -2985,19 +3406,21 @@ function registerCreateCommand(program) {
|
|
|
2985
3406
|
}
|
|
2986
3407
|
}
|
|
2987
3408
|
const existingIds = listTrackFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml)$/i, ""));
|
|
2988
|
-
const
|
|
2989
|
-
const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
|
|
2990
|
-
const prefix = typeof idPrefixesRaw.track === "string" ? idPrefixesRaw.track : "TRK";
|
|
2991
|
-
const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
|
|
3409
|
+
const id = (options.id?.trim() ? normalizeEntityId("track", options.id) : void 0) || generateConfiguredId(root, existingIds, {
|
|
2992
3410
|
entityType: "track",
|
|
2993
|
-
prefix,
|
|
2994
3411
|
title: name,
|
|
3412
|
+
name,
|
|
2995
3413
|
fields: {
|
|
2996
|
-
name
|
|
3414
|
+
name,
|
|
3415
|
+
...dynamicFields
|
|
2997
3416
|
}
|
|
2998
3417
|
});
|
|
3418
|
+
const existingTracks = listTrackFiles(root).map((filePath2) => parseYamlFile3(filePath2));
|
|
3419
|
+
assertNoCaseInsensitiveNameConflict("track", existingTracks, id, name);
|
|
3420
|
+
const shortId = generateStableShortId(root, "track", id, trackShortIds(root));
|
|
2999
3421
|
const payload = {
|
|
3000
3422
|
id,
|
|
3423
|
+
short_id: shortId,
|
|
3001
3424
|
name,
|
|
3002
3425
|
capacity_profiles: capacityProfiles,
|
|
3003
3426
|
constraints: {
|
|
@@ -3006,13 +3429,32 @@ function registerCreateCommand(program) {
|
|
|
3006
3429
|
}
|
|
3007
3430
|
};
|
|
3008
3431
|
const filePath = path8.join(coop, "tracks", `${id}.yml`);
|
|
3009
|
-
|
|
3432
|
+
writeYamlFile4(filePath, payload);
|
|
3010
3433
|
console.log(`Created track: ${path8.relative(root, filePath)}`);
|
|
3011
3434
|
});
|
|
3012
|
-
create.command("delivery").description("Create a delivery").argument("[name]", "Delivery name").option("--id <id>", "Delivery id").option("--name <name>", "Delivery name").option("--status <status>", `Delivery status (${Object.values(DeliveryStatus).join(", ")})`).option("--commit", "Create delivery directly in committed state").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--budget-hours <hours>", "Engineering budget hours").option("--budget-cost <usd>", "Cost budget in USD").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--scope <ids>", "Comma-separated task ids to include in scope").option("--exclude <ids>", "Comma-separated task ids to exclude from scope").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
|
|
3435
|
+
create.command("delivery").description("Create a delivery").allowUnknownOption().allowExcessArguments().argument("[name]", "Delivery name").option("--id <id>", "Delivery id").option("--name <name>", "Delivery name").option("--status <status>", `Delivery status (${Object.values(DeliveryStatus).join(", ")})`).option("--commit", "Create delivery directly in committed state").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--budget-hours <hours>", "Engineering budget hours").option("--budget-cost <usd>", "Cost budget in USD").option("--profiles <profiles>", "Comma-separated capacity profile ids").option("--scope <ids>", "Comma-separated task ids to include in scope").option("--exclude <ids>", "Comma-separated task ids to exclude from scope").option("--user <user>", "Current user for advisory authorization checks").option("--force", "Override advisory authorization checks").option("--interactive", "Prompt for optional fields").action(async (nameArg, options) => {
|
|
3013
3436
|
const root = resolveRepoRoot();
|
|
3014
3437
|
const coop = ensureCoopInitialized(root);
|
|
3015
3438
|
const interactive = Boolean(options.interactive);
|
|
3439
|
+
const dynamicFields = extractDynamicTokenFlags(
|
|
3440
|
+
["create", "delivery"],
|
|
3441
|
+
[
|
|
3442
|
+
"id",
|
|
3443
|
+
"name",
|
|
3444
|
+
"status",
|
|
3445
|
+
"commit",
|
|
3446
|
+
"target-date",
|
|
3447
|
+
"budget-hours",
|
|
3448
|
+
"budget-cost",
|
|
3449
|
+
"profiles",
|
|
3450
|
+
"scope",
|
|
3451
|
+
"exclude",
|
|
3452
|
+
"user",
|
|
3453
|
+
"force",
|
|
3454
|
+
"interactive"
|
|
3455
|
+
]
|
|
3456
|
+
);
|
|
3457
|
+
assertKnownDynamicFields(root, dynamicFields);
|
|
3016
3458
|
const user = options.user?.trim() || defaultCoopAuthor(root);
|
|
3017
3459
|
const config = readCoopConfig(root);
|
|
3018
3460
|
const auth = load_auth_config2(config.raw);
|
|
@@ -3088,21 +3530,22 @@ function registerCreateCommand(program) {
|
|
|
3088
3530
|
throw new Error(`Scope exclude task '${id2}' does not exist.`);
|
|
3089
3531
|
}
|
|
3090
3532
|
}
|
|
3091
|
-
const existingIds = listDeliveryFiles(root).map(
|
|
3092
|
-
|
|
3093
|
-
);
|
|
3094
|
-
const idPrefixesRaw = typeof config.raw.id_prefixes === "object" && config.raw.id_prefixes !== null ? config.raw.id_prefixes : {};
|
|
3095
|
-
const prefix = typeof idPrefixesRaw.delivery === "string" ? idPrefixesRaw.delivery : "DEL";
|
|
3096
|
-
const id = options.id?.trim()?.toUpperCase() || generateConfiguredId(root, existingIds, {
|
|
3533
|
+
const existingIds = listDeliveryFiles(root).map((filePath2) => path8.basename(filePath2).replace(/\.(yml|yaml|md)$/i, ""));
|
|
3534
|
+
const id = (options.id?.trim() ? normalizeEntityId("delivery", options.id) : void 0) || generateConfiguredId(root, existingIds, {
|
|
3097
3535
|
entityType: "delivery",
|
|
3098
|
-
prefix,
|
|
3099
3536
|
title: name,
|
|
3537
|
+
name,
|
|
3100
3538
|
fields: {
|
|
3101
|
-
status
|
|
3539
|
+
status,
|
|
3540
|
+
...dynamicFields
|
|
3102
3541
|
}
|
|
3103
3542
|
});
|
|
3543
|
+
const existingDeliveries = listDeliveryFiles(root).map((filePath2) => parseDeliveryFile2(filePath2).delivery);
|
|
3544
|
+
assertNoCaseInsensitiveNameConflict("delivery", existingDeliveries, id, name);
|
|
3545
|
+
const shortId = generateStableShortId(root, "delivery", id, deliveryShortIds(root));
|
|
3104
3546
|
const payload = {
|
|
3105
3547
|
id,
|
|
3548
|
+
short_id: shortId,
|
|
3106
3549
|
name,
|
|
3107
3550
|
status,
|
|
3108
3551
|
target_date: targetDate,
|
|
@@ -3119,7 +3562,7 @@ function registerCreateCommand(program) {
|
|
|
3119
3562
|
}
|
|
3120
3563
|
};
|
|
3121
3564
|
const filePath = path8.join(coop, "deliveries", `${id}.yml`);
|
|
3122
|
-
|
|
3565
|
+
writeYamlFile4(filePath, payload);
|
|
3123
3566
|
console.log(`Created delivery: ${path8.relative(root, filePath)}`);
|
|
3124
3567
|
});
|
|
3125
3568
|
}
|
|
@@ -3411,6 +3854,7 @@ var catalog = {
|
|
|
3411
3854
|
"Use `coop project show` first to confirm the active workspace and project.",
|
|
3412
3855
|
"Use `coop use show` to inspect the current user-local working defaults for track, delivery, and version.",
|
|
3413
3856
|
"Before assigning `track` or `delivery` values to tasks, inspect or create named entities with `coop list tracks`, `coop list deliveries`, `coop create track`, and `coop create delivery`.",
|
|
3857
|
+
"Track and delivery references accept exact ids, stable short ids, and unique case-insensitive names.",
|
|
3414
3858
|
"Use `coop graph next --delivery <delivery>` or `coop next task` to choose work. Do not reprioritize outside COOP unless the user explicitly overrides it.",
|
|
3415
3859
|
"Commands resolve selection scope from: explicit CLI arg, then `coop use` working context, then shared project defaults.",
|
|
3416
3860
|
"Use `--track` for the workstream lens (home track or delivery_tracks). Use `--delivery` for release/scope membership.",
|
|
@@ -3471,8 +3915,11 @@ var catalog = {
|
|
|
3471
3915
|
{ usage: "coop list tracks", purpose: "List valid named tracks before assigning or updating task track values." },
|
|
3472
3916
|
{ usage: "coop list deliveries", purpose: "List valid named deliveries before assigning or updating task delivery values." },
|
|
3473
3917
|
{ usage: "coop current", purpose: "Show the active project, working context, my active tasks, and the next ready task." },
|
|
3474
|
-
{ usage: "coop naming", purpose: "Explain the
|
|
3475
|
-
{ usage: 'coop naming preview "Natural-language COOP command recommender"', purpose: "Preview
|
|
3918
|
+
{ usage: "coop naming", purpose: "Explain the effective per-entity naming rules, custom tokens, and examples." },
|
|
3919
|
+
{ usage: 'coop naming preview "Natural-language COOP command recommender" --entity task', purpose: "Preview the generated ID before creating an item." },
|
|
3920
|
+
{ usage: "coop naming set task <TYPE>-<TITLE16>-<SEQ>", purpose: "Set one entity's naming template without editing config by hand." },
|
|
3921
|
+
{ usage: "coop naming token create proj", purpose: "Create a custom naming token." },
|
|
3922
|
+
{ usage: "coop naming token value add proj UX", purpose: "Register an allowed value for a naming token." }
|
|
3476
3923
|
]
|
|
3477
3924
|
},
|
|
3478
3925
|
{
|
|
@@ -3483,8 +3930,10 @@ var catalog = {
|
|
|
3483
3930
|
{ usage: "coop create idea --from-file idea-draft.yml", purpose: "Ingest a structured idea draft file." },
|
|
3484
3931
|
{ usage: "cat idea.md | coop create idea --stdin", purpose: "Ingest an idea draft from stdin." },
|
|
3485
3932
|
{ usage: 'coop create task "Implement webhook pipeline"', purpose: "Create a task with defaults." },
|
|
3933
|
+
{ usage: 'coop create task "UX: Auth user journey" --id UX-AUTH-1', purpose: "Create a task with an explicit primary ID." },
|
|
3934
|
+
{ usage: 'coop create task "UX: Auth user journey" --proj UX --feat AUTH', purpose: "Create a task using configured naming tokens." },
|
|
3486
3935
|
{ usage: 'coop create task --title "Lock auth contract" --track MVP --delivery MVP', purpose: "Create a task directly inside a track and delivery scope." },
|
|
3487
|
-
{ usage: "coop create track
|
|
3936
|
+
{ usage: "coop create track MVP", purpose: "Create a named track with slug-style default ID." },
|
|
3488
3937
|
{
|
|
3489
3938
|
usage: 'coop create task --title "Lock auth contract" --acceptance "Contract approved,Client mapping documented" --tests-required "Contract fixture test" --authority-ref docs/webapp-mvp-plan.md#auth',
|
|
3490
3939
|
purpose: "Create a planning-grade task with acceptance, tests, and origin refs."
|
|
@@ -3628,9 +4077,10 @@ function renderTopicPayload(topic) {
|
|
|
3628
4077
|
return {
|
|
3629
4078
|
topic,
|
|
3630
4079
|
naming_guidance: [
|
|
3631
|
-
"Use `coop naming` to inspect
|
|
3632
|
-
'Use `coop naming preview "<title>"
|
|
3633
|
-
"Use `coop
|
|
4080
|
+
"Use `coop naming` to inspect per-entity templates, custom tokens, and examples.",
|
|
4081
|
+
'Use `coop naming preview "<title>" --entity <entity>` before creating a new item if predictable IDs matter.',
|
|
4082
|
+
"Use `coop naming set <entity> <template>` to update one entity's naming rule.",
|
|
4083
|
+
"Use `coop naming token create <token>` and `coop naming token value add <token> <value>` before passing custom token flags like `--proj UX`."
|
|
3634
4084
|
]
|
|
3635
4085
|
};
|
|
3636
4086
|
}
|
|
@@ -4358,7 +4808,13 @@ id_prefixes:
|
|
|
4358
4808
|
run: "RUN"
|
|
4359
4809
|
|
|
4360
4810
|
id:
|
|
4361
|
-
naming:
|
|
4811
|
+
naming:
|
|
4812
|
+
task: ${JSON.stringify(namingTemplate)}
|
|
4813
|
+
idea: ${JSON.stringify(namingTemplate)}
|
|
4814
|
+
track: "<NAME_SLUG>"
|
|
4815
|
+
delivery: "<NAME_SLUG>"
|
|
4816
|
+
run: "<TYPE>-<YYMMDD>-<RAND>"
|
|
4817
|
+
tokens: {}
|
|
4362
4818
|
seq_padding: 0
|
|
4363
4819
|
|
|
4364
4820
|
defaults:
|
|
@@ -4720,15 +5176,14 @@ import path15 from "path";
|
|
|
4720
5176
|
import {
|
|
4721
5177
|
effective_priority as effective_priority3,
|
|
4722
5178
|
load_graph as load_graph6,
|
|
4723
|
-
parseDeliveryFile as parseDeliveryFile2,
|
|
4724
5179
|
parseIdeaFile as parseIdeaFile4,
|
|
4725
5180
|
parseTaskFile as parseTaskFile10,
|
|
4726
|
-
parseYamlFile as parseYamlFile3,
|
|
4727
5181
|
schedule_next as schedule_next3
|
|
4728
5182
|
} from "@kitsy/coop-core";
|
|
4729
5183
|
import chalk2 from "chalk";
|
|
4730
5184
|
var TASK_COLUMN_WIDTHS = {
|
|
4731
5185
|
id: 24,
|
|
5186
|
+
short: 12,
|
|
4732
5187
|
title: 30,
|
|
4733
5188
|
status: 12,
|
|
4734
5189
|
priority: 4,
|
|
@@ -4740,6 +5195,7 @@ var TASK_COLUMN_WIDTHS = {
|
|
|
4740
5195
|
};
|
|
4741
5196
|
var IDEA_COLUMN_WIDTHS = {
|
|
4742
5197
|
id: 24,
|
|
5198
|
+
short: 12,
|
|
4743
5199
|
title: 30,
|
|
4744
5200
|
status: 12,
|
|
4745
5201
|
file: 30
|
|
@@ -4786,36 +5242,36 @@ function parseColumns(input2) {
|
|
|
4786
5242
|
}
|
|
4787
5243
|
function normalizeTaskColumns(value, ready) {
|
|
4788
5244
|
if (!value?.trim()) {
|
|
4789
|
-
return ready ? ["id", "title", "priority", "status", "assignee", "score"] : ["id", "title", "priority", "status", "assignee"];
|
|
5245
|
+
return ready ? ["id", "title", "priority", "status", "assignee", "track", "delivery", "score"] : ["id", "title", "priority", "status", "assignee", "track", "delivery"];
|
|
4790
5246
|
}
|
|
4791
5247
|
const raw = parseColumns(value);
|
|
4792
5248
|
if (raw.length === 1 && raw[0] === "all") {
|
|
4793
|
-
return ["id", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
|
|
5249
|
+
return ["id", "short", "title", "priority", "status", "assignee", "track", "delivery", "score", "file"];
|
|
4794
5250
|
}
|
|
4795
5251
|
const normalized = raw.map((column) => {
|
|
4796
5252
|
if (column === "p") return "priority";
|
|
4797
5253
|
return column;
|
|
4798
5254
|
});
|
|
4799
|
-
const valid = /* @__PURE__ */ new Set(["id", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
|
|
5255
|
+
const valid = /* @__PURE__ */ new Set(["id", "short", "title", "status", "priority", "assignee", "track", "delivery", "score", "file"]);
|
|
4800
5256
|
for (const column of normalized) {
|
|
4801
5257
|
if (!valid.has(column)) {
|
|
4802
|
-
throw new Error(`Invalid task column '${column}'. Expected id|title|status|priority|assignee|track|delivery|score|file|all.`);
|
|
5258
|
+
throw new Error(`Invalid task column '${column}'. Expected id|short|title|status|priority|assignee|track|delivery|score|file|all.`);
|
|
4803
5259
|
}
|
|
4804
5260
|
}
|
|
4805
5261
|
return normalized;
|
|
4806
5262
|
}
|
|
4807
5263
|
function normalizeIdeaColumns(value) {
|
|
4808
5264
|
if (!value?.trim()) {
|
|
4809
|
-
return ["id", "title", "status"];
|
|
5265
|
+
return ["id", "short", "title", "status"];
|
|
4810
5266
|
}
|
|
4811
5267
|
const raw = parseColumns(value);
|
|
4812
5268
|
if (raw.length === 1 && raw[0] === "all") {
|
|
4813
|
-
return ["id", "title", "status", "file"];
|
|
5269
|
+
return ["id", "short", "title", "status", "file"];
|
|
4814
5270
|
}
|
|
4815
|
-
const valid = /* @__PURE__ */ new Set(["id", "title", "status", "file"]);
|
|
5271
|
+
const valid = /* @__PURE__ */ new Set(["id", "short", "title", "status", "file"]);
|
|
4816
5272
|
for (const column of raw) {
|
|
4817
5273
|
if (!valid.has(column)) {
|
|
4818
|
-
throw new Error(`Invalid idea column '${column}'. Expected id|title|status|file|all.`);
|
|
5274
|
+
throw new Error(`Invalid idea column '${column}'. Expected id|short|title|status|file|all.`);
|
|
4819
5275
|
}
|
|
4820
5276
|
}
|
|
4821
5277
|
return raw;
|
|
@@ -4952,6 +5408,8 @@ function sortTaskRows(rows, sortMode, readyOrder, track) {
|
|
|
4952
5408
|
}
|
|
4953
5409
|
function taskColumnHeader(column) {
|
|
4954
5410
|
switch (column) {
|
|
5411
|
+
case "short":
|
|
5412
|
+
return "Short";
|
|
4955
5413
|
case "priority":
|
|
4956
5414
|
return "P";
|
|
4957
5415
|
case "assignee":
|
|
@@ -4975,6 +5433,8 @@ function taskColumnHeader(column) {
|
|
|
4975
5433
|
}
|
|
4976
5434
|
function ideaColumnHeader(column) {
|
|
4977
5435
|
switch (column) {
|
|
5436
|
+
case "short":
|
|
5437
|
+
return "Short";
|
|
4978
5438
|
case "file":
|
|
4979
5439
|
return "File";
|
|
4980
5440
|
case "status":
|
|
@@ -5051,6 +5511,7 @@ function listTasks(options) {
|
|
|
5051
5511
|
}).map(({ task, filePath }) => ({
|
|
5052
5512
|
task,
|
|
5053
5513
|
id: task.id,
|
|
5514
|
+
shortId: task.short_id ?? "-",
|
|
5054
5515
|
title: task.title,
|
|
5055
5516
|
status: task.status,
|
|
5056
5517
|
priority: taskEffectivePriority(task, resolvedTrack.value),
|
|
@@ -5071,10 +5532,12 @@ function listTasks(options) {
|
|
|
5071
5532
|
(entry) => columns.map((column) => {
|
|
5072
5533
|
const rawValue = (() => {
|
|
5073
5534
|
switch (column) {
|
|
5535
|
+
case "short":
|
|
5536
|
+
return entry.shortId;
|
|
5074
5537
|
case "title":
|
|
5075
5538
|
return entry.title;
|
|
5076
5539
|
case "status":
|
|
5077
|
-
return
|
|
5540
|
+
return entry.status;
|
|
5078
5541
|
case "priority":
|
|
5079
5542
|
return entry.priority;
|
|
5080
5543
|
case "assignee":
|
|
@@ -5092,7 +5555,8 @@ function listTasks(options) {
|
|
|
5092
5555
|
return entry.id;
|
|
5093
5556
|
}
|
|
5094
5557
|
})();
|
|
5095
|
-
|
|
5558
|
+
const truncated = column === "file" ? truncateMiddleCell(rawValue, TASK_COLUMN_WIDTHS[column]) : truncateCell(rawValue, TASK_COLUMN_WIDTHS[column]);
|
|
5559
|
+
return column === "status" ? statusColor(truncated) : truncated;
|
|
5096
5560
|
})
|
|
5097
5561
|
)
|
|
5098
5562
|
)
|
|
@@ -5110,6 +5574,7 @@ function listIdeas(options) {
|
|
|
5110
5574
|
return true;
|
|
5111
5575
|
}).map(({ idea, filePath }) => ({
|
|
5112
5576
|
id: idea.id,
|
|
5577
|
+
shortId: idea.short_id ?? "-",
|
|
5113
5578
|
title: idea.title,
|
|
5114
5579
|
status: idea.status,
|
|
5115
5580
|
priority: "-",
|
|
@@ -5142,8 +5607,9 @@ function listIdeas(options) {
|
|
|
5142
5607
|
columns.map(ideaColumnHeader),
|
|
5143
5608
|
sorted.map(
|
|
5144
5609
|
(entry) => columns.map((column) => {
|
|
5145
|
-
const rawValue = column === "title" ? entry.title : column === "status" ?
|
|
5146
|
-
|
|
5610
|
+
const rawValue = column === "short" ? entry.shortId : column === "title" ? entry.title : column === "status" ? entry.status : column === "file" ? path15.relative(root, entry.filePath) : entry.id;
|
|
5611
|
+
const truncated = column === "file" ? truncateMiddleCell(rawValue, IDEA_COLUMN_WIDTHS[column]) : truncateCell(rawValue, IDEA_COLUMN_WIDTHS[column]);
|
|
5612
|
+
return column === "status" ? statusColor(truncated) : truncated;
|
|
5147
5613
|
})
|
|
5148
5614
|
)
|
|
5149
5615
|
)
|
|
@@ -5153,10 +5619,11 @@ Total ideas: ${sorted.length}`);
|
|
|
5153
5619
|
}
|
|
5154
5620
|
function listDeliveries() {
|
|
5155
5621
|
const root = resolveRepoRoot();
|
|
5156
|
-
const rows =
|
|
5622
|
+
const rows = loadDeliveryEntries(root).map(({ delivery, filePath }) => [
|
|
5157
5623
|
truncateCell(delivery.id, 24),
|
|
5624
|
+
truncateCell(delivery.short_id ?? "-", 12),
|
|
5158
5625
|
truncateCell(delivery.name, 30),
|
|
5159
|
-
truncateCell(
|
|
5626
|
+
statusColor(truncateCell(delivery.status, 12)),
|
|
5160
5627
|
truncateCell(delivery.target_date ?? "-", 16),
|
|
5161
5628
|
truncateMiddleCell(path15.relative(root, filePath), 30)
|
|
5162
5629
|
]);
|
|
@@ -5164,12 +5631,13 @@ function listDeliveries() {
|
|
|
5164
5631
|
console.log("No deliveries found.");
|
|
5165
5632
|
return;
|
|
5166
5633
|
}
|
|
5167
|
-
console.log(formatTable(["ID", "Name", "Status", "Target", "File"], rows));
|
|
5634
|
+
console.log(formatTable(["ID", "Short", "Name", "Status", "Target", "File"], rows));
|
|
5168
5635
|
}
|
|
5169
5636
|
function listTracks() {
|
|
5170
5637
|
const root = resolveRepoRoot();
|
|
5171
|
-
const rows =
|
|
5638
|
+
const rows = loadTrackEntries(root).map(({ track, filePath }) => [
|
|
5172
5639
|
truncateCell(track.id, 24),
|
|
5640
|
+
truncateCell(track.short_id ?? "-", 12),
|
|
5173
5641
|
truncateCell(track.name, 30),
|
|
5174
5642
|
truncateCell((track.capacity_profiles ?? []).join(", ") || "-", 24),
|
|
5175
5643
|
truncateCell(String(track.constraints?.max_concurrent_tasks ?? "-"), 8),
|
|
@@ -5179,14 +5647,14 @@ function listTracks() {
|
|
|
5179
5647
|
console.log("No tracks found.");
|
|
5180
5648
|
return;
|
|
5181
5649
|
}
|
|
5182
|
-
console.log(formatTable(["ID", "Name", "Profiles", "Max WIP", "File"], rows));
|
|
5650
|
+
console.log(formatTable(["ID", "Short", "Name", "Profiles", "Max WIP", "File"], rows));
|
|
5183
5651
|
}
|
|
5184
5652
|
function registerListCommand(program) {
|
|
5185
5653
|
const list = program.command("list").description("List COOP entities");
|
|
5186
|
-
list.command("tasks").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by home/contributing track lens, using `coop use track` if omitted").option("--delivery <delivery>", "Filter by delivery membership, using `coop use delivery` if omitted").option("--priority <priority>", "Filter by effective priority").option("--assignee <assignee>", "Filter by assignee").option("--version <version>", "Filter by fix/released version, using `coop use version` if omitted").option("--mine", "Filter to the current default COOP author").option("--ready", "Only list ready tasks in scored order").option("--sort <sort>", "Sort by id|priority|status|title|updated|created|score").option("--columns <columns>", "Columns: id,title,status,priority,assignee,track,delivery,score,file or all").action((options) => {
|
|
5654
|
+
list.command("tasks").alias("task").description("List tasks").option("--status <status>", "Filter by status").option("--track <track>", "Filter by home/contributing track lens, using `coop use track` if omitted").option("--delivery <delivery>", "Filter by delivery membership, using `coop use delivery` if omitted").option("--priority <priority>", "Filter by effective priority").option("--assignee <assignee>", "Filter by assignee").option("--version <version>", "Filter by fix/released version, using `coop use version` if omitted").option("--mine", "Filter to the current default COOP author").option("--ready", "Only list ready tasks in scored order").option("--sort <sort>", "Sort by id|priority|status|title|updated|created|score").option("--columns <columns>", "Columns: id,short,title,status,priority,assignee,track,delivery,score,file or all").action((options) => {
|
|
5187
5655
|
listTasks(options);
|
|
5188
5656
|
});
|
|
5189
|
-
list.command("ideas").description("List ideas").option("--status <status>", "Filter by status").option("--sort <sort>", "Sort by id|status|title|updated|created").option("--columns <columns>", "Columns: id,title,status,file or all").action((options) => {
|
|
5657
|
+
list.command("ideas").alias("idea").description("List ideas").option("--status <status>", "Filter by status").option("--sort <sort>", "Sort by id|status|title|updated|created").option("--columns <columns>", "Columns: id,short,title,status,file or all").action((options) => {
|
|
5190
5658
|
listIdeas(options);
|
|
5191
5659
|
});
|
|
5192
5660
|
list.command("alias").description("List aliases").argument("[pattern]", "Wildcard pattern, e.g. PAY*").action((pattern) => {
|
|
@@ -5202,18 +5670,21 @@ function registerListCommand(program) {
|
|
|
5202
5670
|
|
|
5203
5671
|
// src/utils/logger.ts
|
|
5204
5672
|
import fs11 from "fs";
|
|
5673
|
+
import os2 from "os";
|
|
5205
5674
|
import path16 from "path";
|
|
5206
5675
|
function resolveWorkspaceRoot(start = process.cwd()) {
|
|
5207
5676
|
let current = path16.resolve(start);
|
|
5677
|
+
const configuredCoopHome = path16.resolve(resolveCoopHome());
|
|
5678
|
+
const defaultCoopHome = path16.resolve(path16.join(os2.homedir(), ".coop"));
|
|
5208
5679
|
while (true) {
|
|
5209
5680
|
const gitDir = path16.join(current, ".git");
|
|
5210
5681
|
const coopDir2 = coopWorkspaceDir(current);
|
|
5211
5682
|
const workspaceConfig = path16.join(coopDir2, "config.yml");
|
|
5212
|
-
const projectsDir = path16.join(coopDir2, "projects");
|
|
5213
5683
|
if (fs11.existsSync(gitDir)) {
|
|
5214
5684
|
return current;
|
|
5215
5685
|
}
|
|
5216
|
-
|
|
5686
|
+
const resolvedCoopDir = path16.resolve(coopDir2);
|
|
5687
|
+
if (resolvedCoopDir !== configuredCoopHome && resolvedCoopDir !== defaultCoopHome && fs11.existsSync(workspaceConfig)) {
|
|
5217
5688
|
return current;
|
|
5218
5689
|
}
|
|
5219
5690
|
const parent = path16.dirname(current);
|
|
@@ -5340,7 +5811,7 @@ import fs12 from "fs";
|
|
|
5340
5811
|
import path17 from "path";
|
|
5341
5812
|
import { createInterface } from "readline/promises";
|
|
5342
5813
|
import { stdin as input, stdout as output } from "process";
|
|
5343
|
-
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as
|
|
5814
|
+
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager3, migrate_repository, parseYamlFile as parseYamlFile5, writeYamlFile as writeYamlFile5 } from "@kitsy/coop-core";
|
|
5344
5815
|
var COOP_IGNORE_TEMPLATE2 = `.index/
|
|
5345
5816
|
logs/
|
|
5346
5817
|
tmp/
|
|
@@ -5507,7 +5978,7 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
5507
5978
|
}
|
|
5508
5979
|
const movedConfigPath = path17.join(projectRoot, "config.yml");
|
|
5509
5980
|
if (fs12.existsSync(movedConfigPath)) {
|
|
5510
|
-
const movedConfig =
|
|
5981
|
+
const movedConfig = parseYamlFile5(movedConfigPath);
|
|
5511
5982
|
const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
|
|
5512
5983
|
nextProject.name = identity.projectName;
|
|
5513
5984
|
nextProject.id = projectId;
|
|
@@ -5515,7 +5986,7 @@ async function migrateWorkspaceLayout(root, options) {
|
|
|
5515
5986
|
const nextHooks = typeof movedConfig.hooks === "object" && movedConfig.hooks !== null ? { ...movedConfig.hooks } : {};
|
|
5516
5987
|
nextHooks.on_task_transition = `.coop/projects/${projectId}/hooks/on-task-transition.sh`;
|
|
5517
5988
|
nextHooks.on_delivery_complete = `.coop/projects/${projectId}/hooks/on-delivery-complete.sh`;
|
|
5518
|
-
|
|
5989
|
+
writeYamlFile5(movedConfigPath, {
|
|
5519
5990
|
...movedConfig,
|
|
5520
5991
|
project: nextProject,
|
|
5521
5992
|
hooks: nextHooks
|
|
@@ -5569,25 +6040,55 @@ function registerMigrateCommand(program) {
|
|
|
5569
6040
|
}
|
|
5570
6041
|
|
|
5571
6042
|
// src/commands/naming.ts
|
|
6043
|
+
var ENTITY_NAMES = ["task", "idea", "track", "delivery", "run"];
|
|
6044
|
+
function assertNamingEntity(value) {
|
|
6045
|
+
const entity = value?.trim().toLowerCase() || "task";
|
|
6046
|
+
if (!ENTITY_NAMES.includes(entity)) {
|
|
6047
|
+
throw new Error(`Invalid entity '${value}'. Expected ${ENTITY_NAMES.join("|")}.`);
|
|
6048
|
+
}
|
|
6049
|
+
return entity;
|
|
6050
|
+
}
|
|
6051
|
+
function readConfigRecord(root) {
|
|
6052
|
+
return readCoopConfig(root).raw;
|
|
6053
|
+
}
|
|
6054
|
+
function writeNamingConfig(root, update) {
|
|
6055
|
+
writeCoopConfig(root, update(readConfigRecord(root)));
|
|
6056
|
+
}
|
|
5572
6057
|
function printNamingOverview() {
|
|
5573
6058
|
const root = resolveRepoRoot();
|
|
5574
6059
|
const config = readCoopConfig(root);
|
|
6060
|
+
const templates = namingTemplatesForRoot(root);
|
|
6061
|
+
const tokens = namingTokensForRoot(root);
|
|
5575
6062
|
const sampleTitle = "Natural-language COOP command recommender";
|
|
5576
6063
|
const sampleTaskTitle = "Implement billing payment contract review";
|
|
5577
6064
|
console.log("COOP Naming");
|
|
5578
|
-
console.log(`
|
|
5579
|
-
console.log(`Default template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
|
|
5580
|
-
console.log("
|
|
6065
|
+
console.log(`Legacy task template: ${config.idNamingTemplate}`);
|
|
6066
|
+
console.log(`Default task template: ${DEFAULT_ID_NAMING_TEMPLATE}`);
|
|
6067
|
+
console.log("Per-entity templates:");
|
|
6068
|
+
for (const entity of ENTITY_NAMES) {
|
|
6069
|
+
console.log(`- ${entity}: ${templates[entity]}`);
|
|
6070
|
+
}
|
|
6071
|
+
console.log("Built-in tokens:");
|
|
5581
6072
|
console.log(" <TYPE> entity type such as IDEA, TASK, DELIVERY");
|
|
5582
6073
|
console.log(" <TITLE> semantic title token (defaults to TITLE16)");
|
|
5583
6074
|
console.log(" <TITLE16> semantic title token capped to 16 chars");
|
|
5584
6075
|
console.log(" <TITLE24> semantic title token capped to 24 chars");
|
|
5585
6076
|
console.log(" <TRACK> task track");
|
|
6077
|
+
console.log(" <NAME> entity name/title");
|
|
6078
|
+
console.log(" <NAME_SLUG> lower-case slug of the entity name");
|
|
5586
6079
|
console.log(" <SEQ> sequential number within the rendered pattern");
|
|
5587
6080
|
console.log(" <USER> actor/user namespace");
|
|
5588
6081
|
console.log(" <YYMMDD> short date token");
|
|
5589
6082
|
console.log(" <RAND> random uniqueness token");
|
|
5590
6083
|
console.log(" <PREFIX> entity prefix override");
|
|
6084
|
+
console.log("Custom tokens:");
|
|
6085
|
+
if (Object.keys(tokens).length === 0) {
|
|
6086
|
+
console.log("- none");
|
|
6087
|
+
} else {
|
|
6088
|
+
for (const [token, definition] of Object.entries(tokens)) {
|
|
6089
|
+
console.log(`- <${token.toUpperCase()}>: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
5591
6092
|
console.log("Examples:");
|
|
5592
6093
|
const tokenExamples = namingTokenExamples(sampleTitle);
|
|
5593
6094
|
for (const [token, value] of Object.entries(tokenExamples)) {
|
|
@@ -5597,7 +6098,7 @@ function printNamingOverview() {
|
|
|
5597
6098
|
entityType: "idea",
|
|
5598
6099
|
title: sampleTitle
|
|
5599
6100
|
}, root)}`);
|
|
5600
|
-
console.log(`Preview (${
|
|
6101
|
+
console.log(`Preview (${templates.task}): ${previewNamingTemplate(templates.task, {
|
|
5601
6102
|
entityType: "task",
|
|
5602
6103
|
title: sampleTaskTitle,
|
|
5603
6104
|
track: "mvp",
|
|
@@ -5606,35 +6107,187 @@ function printNamingOverview() {
|
|
|
5606
6107
|
}, root)}`);
|
|
5607
6108
|
console.log("Try:");
|
|
5608
6109
|
console.log(` coop naming preview "${sampleTitle}"`);
|
|
5609
|
-
console.log(
|
|
5610
|
-
console.log(
|
|
6110
|
+
console.log(" coop naming set task <TYPE>-<TITLE24>-<SEQ>");
|
|
6111
|
+
console.log(" coop naming token create proj");
|
|
6112
|
+
console.log(" coop naming token value add proj UX");
|
|
6113
|
+
}
|
|
6114
|
+
function setEntityNamingTemplate(root, entity, template) {
|
|
6115
|
+
const nextTemplate = template.trim();
|
|
6116
|
+
if (!nextTemplate) {
|
|
6117
|
+
throw new Error("Naming template must be non-empty.");
|
|
6118
|
+
}
|
|
6119
|
+
writeNamingConfig(root, (config) => {
|
|
6120
|
+
const next = { ...config };
|
|
6121
|
+
const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
|
|
6122
|
+
const namingRaw = typeof idRaw.naming === "object" && idRaw.naming !== null ? { ...idRaw.naming } : typeof idRaw.naming === "string" ? { task: idRaw.naming, idea: idRaw.naming } : {};
|
|
6123
|
+
namingRaw[entity] = nextTemplate;
|
|
6124
|
+
idRaw.naming = namingRaw;
|
|
6125
|
+
next.id = idRaw;
|
|
6126
|
+
return next;
|
|
6127
|
+
});
|
|
6128
|
+
}
|
|
6129
|
+
function ensureTokenRecord(config) {
|
|
6130
|
+
const next = { ...config };
|
|
6131
|
+
const idRaw = typeof next.id === "object" && next.id !== null ? { ...next.id } : {};
|
|
6132
|
+
const tokensRaw = typeof idRaw.tokens === "object" && idRaw.tokens !== null ? { ...idRaw.tokens } : {};
|
|
6133
|
+
idRaw.tokens = tokensRaw;
|
|
6134
|
+
next.id = idRaw;
|
|
6135
|
+
return { next, tokens: tokensRaw };
|
|
6136
|
+
}
|
|
6137
|
+
function listTokens() {
|
|
6138
|
+
const root = resolveRepoRoot();
|
|
6139
|
+
const tokens = namingTokensForRoot(root);
|
|
6140
|
+
if (Object.keys(tokens).length === 0) {
|
|
6141
|
+
console.log("No naming tokens defined.");
|
|
6142
|
+
return;
|
|
6143
|
+
}
|
|
6144
|
+
for (const [token, definition] of Object.entries(tokens)) {
|
|
6145
|
+
console.log(`${token}: ${definition.values.length > 0 ? definition.values.join(", ") : "(no values)"}`);
|
|
6146
|
+
}
|
|
5611
6147
|
}
|
|
5612
6148
|
function registerNamingCommand(program) {
|
|
5613
|
-
const naming = program.command("naming").description("
|
|
6149
|
+
const naming = program.command("naming").description("Manage COOP naming templates, tokens, and previews");
|
|
5614
6150
|
naming.action(() => {
|
|
5615
6151
|
printNamingOverview();
|
|
5616
6152
|
});
|
|
5617
|
-
naming.command("preview").description("Preview the
|
|
6153
|
+
naming.command("preview").description("Preview the effective or supplied naming template for a sample title").allowUnknownOption().allowExcessArguments().argument("<title>", "Sample title to render").option("--template <template>", "Override naming template").option("--entity <entity>", "Entity type: idea|task|track|delivery|run", "task").option("--track <track>", "Track token value").option("--status <status>", "Status token value").option("--task-type <taskType>", "Task type token value").action(
|
|
5618
6154
|
(title, options) => {
|
|
5619
6155
|
const root = resolveRepoRoot();
|
|
5620
|
-
const
|
|
5621
|
-
const
|
|
5622
|
-
const
|
|
6156
|
+
const entity = assertNamingEntity(options.entity);
|
|
6157
|
+
const templates = namingTemplatesForRoot(root);
|
|
6158
|
+
const dynamicFields = extractDynamicTokenFlags(
|
|
6159
|
+
["naming", "preview"],
|
|
6160
|
+
["template", "entity", "track", "status", "task-type"]
|
|
6161
|
+
);
|
|
6162
|
+
const tokens = namingTokensForRoot(root);
|
|
6163
|
+
for (const key of Object.keys(dynamicFields)) {
|
|
6164
|
+
if (!tokens[key]) {
|
|
6165
|
+
throw new Error(`Unknown naming token '${key}'. Define it first with \`coop naming token create ${key}\`.`);
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
5623
6168
|
console.log(
|
|
5624
6169
|
previewNamingTemplate(
|
|
5625
|
-
template,
|
|
6170
|
+
options.template?.trim() || templates[entity],
|
|
5626
6171
|
{
|
|
5627
6172
|
entityType: entity,
|
|
5628
6173
|
title,
|
|
6174
|
+
name: title,
|
|
5629
6175
|
track: options.track,
|
|
5630
6176
|
status: options.status,
|
|
5631
|
-
taskType: options.taskType
|
|
6177
|
+
taskType: options.taskType,
|
|
6178
|
+
fields: dynamicFields
|
|
5632
6179
|
},
|
|
5633
6180
|
root
|
|
5634
6181
|
)
|
|
5635
6182
|
);
|
|
5636
6183
|
}
|
|
5637
6184
|
);
|
|
6185
|
+
naming.command("set").description("Set the naming template for one entity type").argument("<entity>", "Entity: task|idea|track|delivery|run").argument("<template>", "Naming template").action((entity, template) => {
|
|
6186
|
+
const root = resolveRepoRoot();
|
|
6187
|
+
setEntityNamingTemplate(root, assertNamingEntity(entity), template);
|
|
6188
|
+
console.log(`${assertNamingEntity(entity)}=${namingTemplatesForRoot(root)[assertNamingEntity(entity)]}`);
|
|
6189
|
+
});
|
|
6190
|
+
const token = naming.command("token").description("Manage custom naming tokens");
|
|
6191
|
+
token.command("list").description("List naming tokens").action(() => {
|
|
6192
|
+
listTokens();
|
|
6193
|
+
});
|
|
6194
|
+
token.command("create").description("Create a custom naming token").argument("<token>", "Token name").action((tokenName) => {
|
|
6195
|
+
const root = resolveRepoRoot();
|
|
6196
|
+
const normalizedToken = normalizeNamingTokenName(tokenName);
|
|
6197
|
+
writeNamingConfig(root, (config) => {
|
|
6198
|
+
const { next, tokens } = ensureTokenRecord(config);
|
|
6199
|
+
if (tokens[normalizedToken]) {
|
|
6200
|
+
throw new Error(`Naming token '${normalizedToken}' already exists.`);
|
|
6201
|
+
}
|
|
6202
|
+
tokens[normalizedToken] = { values: [] };
|
|
6203
|
+
return next;
|
|
6204
|
+
});
|
|
6205
|
+
console.log(`Created naming token: ${normalizedToken}`);
|
|
6206
|
+
});
|
|
6207
|
+
token.command("rename").description("Rename a custom naming token").argument("<token>", "Existing token name").argument("<next>", "New token name").action((tokenName, nextToken) => {
|
|
6208
|
+
const root = resolveRepoRoot();
|
|
6209
|
+
const normalizedToken = normalizeNamingTokenName(tokenName);
|
|
6210
|
+
const normalizedNext = normalizeNamingTokenName(nextToken);
|
|
6211
|
+
const tokens = namingTokensForRoot(root);
|
|
6212
|
+
if (!tokens[normalizedToken]) {
|
|
6213
|
+
throw new Error(`Naming token '${normalizedToken}' does not exist.`);
|
|
6214
|
+
}
|
|
6215
|
+
if (tokens[normalizedNext]) {
|
|
6216
|
+
throw new Error(`Naming token '${normalizedNext}' already exists.`);
|
|
6217
|
+
}
|
|
6218
|
+
writeNamingConfig(root, (config) => {
|
|
6219
|
+
const { next, tokens: tokensRaw } = ensureTokenRecord(config);
|
|
6220
|
+
tokensRaw[normalizedNext] = tokensRaw[normalizedToken];
|
|
6221
|
+
delete tokensRaw[normalizedToken];
|
|
6222
|
+
return next;
|
|
6223
|
+
});
|
|
6224
|
+
console.log(`Renamed naming token: ${normalizedToken} -> ${normalizedNext}`);
|
|
6225
|
+
});
|
|
6226
|
+
token.command("delete").description("Delete a custom naming token").argument("<token>", "Token name").action((tokenName) => {
|
|
6227
|
+
const root = resolveRepoRoot();
|
|
6228
|
+
const normalizedToken = normalizeNamingTokenName(tokenName);
|
|
6229
|
+
writeNamingConfig(root, (config) => {
|
|
6230
|
+
const { next, tokens } = ensureTokenRecord(config);
|
|
6231
|
+
delete tokens[normalizedToken];
|
|
6232
|
+
return next;
|
|
6233
|
+
});
|
|
6234
|
+
console.log(`Deleted naming token: ${normalizedToken}`);
|
|
6235
|
+
});
|
|
6236
|
+
const tokenValue = token.command("value").description("Manage allowed values for a naming token");
|
|
6237
|
+
tokenValue.command("list").description("List allowed values for a naming token").argument("<token>", "Token name").action((tokenName) => {
|
|
6238
|
+
const normalizedToken = normalizeNamingTokenName(tokenName);
|
|
6239
|
+
const tokens = namingTokensForRoot(resolveRepoRoot());
|
|
6240
|
+
const values = tokens[normalizedToken]?.values;
|
|
6241
|
+
if (!values) {
|
|
6242
|
+
throw new Error(`Naming token '${normalizedToken}' does not exist.`);
|
|
6243
|
+
}
|
|
6244
|
+
if (values.length === 0) {
|
|
6245
|
+
console.log("(no values)");
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
for (const value of values) {
|
|
6249
|
+
console.log(value);
|
|
6250
|
+
}
|
|
6251
|
+
});
|
|
6252
|
+
tokenValue.command("add").description("Add an allowed value for a naming token").argument("<token>", "Token name").argument("<value>", "Token value").action((tokenName, value) => {
|
|
6253
|
+
const root = resolveRepoRoot();
|
|
6254
|
+
const normalizedToken = normalizeNamingTokenName(tokenName);
|
|
6255
|
+
const normalizedValue = normalizeNamingTokenValue(value);
|
|
6256
|
+
writeNamingConfig(root, (config) => {
|
|
6257
|
+
const { next, tokens } = ensureTokenRecord(config);
|
|
6258
|
+
const tokenRecord = typeof tokens[normalizedToken] === "object" && tokens[normalizedToken] !== null ? { ...tokens[normalizedToken] } : null;
|
|
6259
|
+
if (!tokenRecord) {
|
|
6260
|
+
throw new Error(`Naming token '${normalizedToken}' does not exist.`);
|
|
6261
|
+
}
|
|
6262
|
+
const values = Array.isArray(tokenRecord.values) ? Array.from(
|
|
6263
|
+
new Set(
|
|
6264
|
+
tokenRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry))
|
|
6265
|
+
)
|
|
6266
|
+
) : [];
|
|
6267
|
+
values.push(normalizedValue);
|
|
6268
|
+
tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
6269
|
+
tokens[normalizedToken] = tokenRecord;
|
|
6270
|
+
return next;
|
|
6271
|
+
});
|
|
6272
|
+
console.log(`Added naming token value: ${normalizedToken}=${normalizedValue}`);
|
|
6273
|
+
});
|
|
6274
|
+
tokenValue.command("remove").description("Remove an allowed value for a naming token").argument("<token>", "Token name").argument("<value>", "Token value").action((tokenName, value) => {
|
|
6275
|
+
const root = resolveRepoRoot();
|
|
6276
|
+
const normalizedToken = normalizeNamingTokenName(tokenName);
|
|
6277
|
+
const normalizedValue = normalizeNamingTokenValue(value);
|
|
6278
|
+
writeNamingConfig(root, (config) => {
|
|
6279
|
+
const { next, tokens } = ensureTokenRecord(config);
|
|
6280
|
+
const tokenRecord = typeof tokens[normalizedToken] === "object" && tokens[normalizedToken] !== null ? { ...tokens[normalizedToken] } : null;
|
|
6281
|
+
if (!tokenRecord) {
|
|
6282
|
+
throw new Error(`Naming token '${normalizedToken}' does not exist.`);
|
|
6283
|
+
}
|
|
6284
|
+
const values = Array.isArray(tokenRecord.values) ? tokenRecord.values.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => normalizeNamingTokenValue(entry)).filter((entry) => entry !== normalizedValue) : [];
|
|
6285
|
+
tokenRecord.values = Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
6286
|
+
tokens[normalizedToken] = tokenRecord;
|
|
6287
|
+
return next;
|
|
6288
|
+
});
|
|
6289
|
+
console.log(`Removed naming token value: ${normalizedToken}=${normalizedValue}`);
|
|
6290
|
+
});
|
|
5638
6291
|
}
|
|
5639
6292
|
|
|
5640
6293
|
// src/commands/plan.ts
|
|
@@ -5955,7 +6608,13 @@ id_prefixes:
|
|
|
5955
6608
|
run: "RUN"
|
|
5956
6609
|
|
|
5957
6610
|
id:
|
|
5958
|
-
naming:
|
|
6611
|
+
naming:
|
|
6612
|
+
task: ${JSON.stringify(namingTemplate)}
|
|
6613
|
+
idea: ${JSON.stringify(namingTemplate)}
|
|
6614
|
+
track: "<NAME_SLUG>"
|
|
6615
|
+
delivery: "<NAME_SLUG>"
|
|
6616
|
+
run: "<TYPE>-<YYMMDD>-<RAND>"
|
|
6617
|
+
tokens: {}
|
|
5959
6618
|
seq_padding: 0
|
|
5960
6619
|
|
|
5961
6620
|
defaults:
|
|
@@ -6375,7 +7034,7 @@ function registerRunCommand(program) {
|
|
|
6375
7034
|
}
|
|
6376
7035
|
|
|
6377
7036
|
// src/commands/search.ts
|
|
6378
|
-
import { load_graph as load_graph9, parseDeliveryFile as
|
|
7037
|
+
import { load_graph as load_graph9, parseDeliveryFile as parseDeliveryFile4, parseIdeaFile as parseIdeaFile6, parseTaskFile as parseTaskFile13 } from "@kitsy/coop-core";
|
|
6379
7038
|
function haystackForTask(task) {
|
|
6380
7039
|
return [
|
|
6381
7040
|
task.id,
|
|
@@ -6454,7 +7113,7 @@ ${parsed.body}`, query)) continue;
|
|
|
6454
7113
|
}
|
|
6455
7114
|
if (options.kind === "all" || options.kind === "delivery") {
|
|
6456
7115
|
for (const filePath of listDeliveryFiles(root)) {
|
|
6457
|
-
const parsed =
|
|
7116
|
+
const parsed = parseDeliveryFile4(filePath);
|
|
6458
7117
|
if (options.status && parsed.delivery.status !== options.status) continue;
|
|
6459
7118
|
if (!includesQuery(haystackForDelivery(parsed.delivery, parsed.body), query)) continue;
|
|
6460
7119
|
rows.push({
|
|
@@ -6798,6 +7457,7 @@ function showTask(taskId, options = {}) {
|
|
|
6798
7457
|
if (options.compact) {
|
|
6799
7458
|
const compactLines = [
|
|
6800
7459
|
`Task: ${task.id}`,
|
|
7460
|
+
`Short ID: ${task.short_id ?? "-"}`,
|
|
6801
7461
|
`Title: ${task.title}`,
|
|
6802
7462
|
`Status: ${task.status}`,
|
|
6803
7463
|
`Priority: ${task.priority ?? "-"}`,
|
|
@@ -6815,6 +7475,7 @@ function showTask(taskId, options = {}) {
|
|
|
6815
7475
|
}
|
|
6816
7476
|
const lines = [
|
|
6817
7477
|
`Task: ${task.id}`,
|
|
7478
|
+
`Short ID: ${task.short_id ?? "-"}`,
|
|
6818
7479
|
`Title: ${task.title}`,
|
|
6819
7480
|
`Status: ${task.status}`,
|
|
6820
7481
|
`Type: ${task.type}`,
|
|
@@ -6888,6 +7549,7 @@ function showIdea(ideaId, options = {}) {
|
|
|
6888
7549
|
console.log(
|
|
6889
7550
|
[
|
|
6890
7551
|
`Idea: ${idea.id}`,
|
|
7552
|
+
`Short ID: ${idea.short_id ?? "-"}`,
|
|
6891
7553
|
`Title: ${idea.title}`,
|
|
6892
7554
|
`Status: ${idea.status}`,
|
|
6893
7555
|
`Tags: ${stringify(idea.tags)}`,
|
|
@@ -6899,6 +7561,7 @@ function showIdea(ideaId, options = {}) {
|
|
|
6899
7561
|
}
|
|
6900
7562
|
const lines = [
|
|
6901
7563
|
`Idea: ${idea.id}`,
|
|
7564
|
+
`Short ID: ${idea.short_id ?? "-"}`,
|
|
6902
7565
|
`Title: ${idea.title}`,
|
|
6903
7566
|
`Status: ${idea.status}`,
|
|
6904
7567
|
`Author: ${idea.author}`,
|
|
@@ -6921,6 +7584,7 @@ function showDelivery(ref, options = {}) {
|
|
|
6921
7584
|
console.log(
|
|
6922
7585
|
[
|
|
6923
7586
|
`Delivery: ${delivery.id}`,
|
|
7587
|
+
`Short ID: ${delivery.short_id ?? "-"}`,
|
|
6924
7588
|
`Name: ${delivery.name}`,
|
|
6925
7589
|
`Status: ${delivery.status}`,
|
|
6926
7590
|
`Target Date: ${delivery.target_date ?? "-"}`,
|
|
@@ -6932,6 +7596,7 @@ function showDelivery(ref, options = {}) {
|
|
|
6932
7596
|
}
|
|
6933
7597
|
const lines = [
|
|
6934
7598
|
`Delivery: ${delivery.id}`,
|
|
7599
|
+
`Short ID: ${delivery.short_id ?? "-"}`,
|
|
6935
7600
|
`Name: ${delivery.name}`,
|
|
6936
7601
|
`Status: ${delivery.status}`,
|
|
6937
7602
|
`Target Date: ${delivery.target_date ?? "-"}`,
|
|
@@ -6947,6 +7612,28 @@ function showDelivery(ref, options = {}) {
|
|
|
6947
7612
|
];
|
|
6948
7613
|
console.log(lines.join("\n"));
|
|
6949
7614
|
}
|
|
7615
|
+
function showTrack(ref, options = {}) {
|
|
7616
|
+
const root = resolveRepoRoot();
|
|
7617
|
+
const resolvedId = resolveExistingTrackId(root, ref);
|
|
7618
|
+
if (!resolvedId) {
|
|
7619
|
+
throw new Error(`Track '${ref}' not found.`);
|
|
7620
|
+
}
|
|
7621
|
+
const entry = loadTrackEntries(root).find((candidate) => candidate.track.id === resolvedId);
|
|
7622
|
+
if (!entry) {
|
|
7623
|
+
throw new Error(`Track '${ref}' not found.`);
|
|
7624
|
+
}
|
|
7625
|
+
const track = entry.track;
|
|
7626
|
+
const base = [
|
|
7627
|
+
`Track: ${track.id}`,
|
|
7628
|
+
`Short ID: ${track.short_id ?? "-"}`,
|
|
7629
|
+
`Name: ${track.name}`,
|
|
7630
|
+
`Profiles: ${(track.capacity_profiles ?? []).join(", ") || "-"}`,
|
|
7631
|
+
`Max WIP: ${track.constraints?.max_concurrent_tasks ?? "-"}`,
|
|
7632
|
+
`Allowed Types: ${(track.constraints?.allowed_types ?? []).join(", ") || "-"}`,
|
|
7633
|
+
`File: ${path22.relative(root, entry.filePath)}`
|
|
7634
|
+
];
|
|
7635
|
+
console.log((options.compact ? base.filter((line) => !line.startsWith("Allowed Types")) : base).join("\n"));
|
|
7636
|
+
}
|
|
6950
7637
|
function showByReference(ref, options = {}) {
|
|
6951
7638
|
const root = resolveRepoRoot();
|
|
6952
7639
|
try {
|
|
@@ -6958,7 +7645,11 @@ function showByReference(ref, options = {}) {
|
|
|
6958
7645
|
showIdea(ref, options);
|
|
6959
7646
|
return;
|
|
6960
7647
|
} catch {
|
|
6961
|
-
|
|
7648
|
+
try {
|
|
7649
|
+
showDelivery(ref, options);
|
|
7650
|
+
} catch {
|
|
7651
|
+
showTrack(ref, options);
|
|
7652
|
+
}
|
|
6962
7653
|
}
|
|
6963
7654
|
}
|
|
6964
7655
|
function registerShowCommand(program) {
|
|
@@ -6974,6 +7665,9 @@ function registerShowCommand(program) {
|
|
|
6974
7665
|
show.command("idea").description("Show idea details").argument("<id>", "Idea ID").option("--compact", "Show a smaller summary view").action((id, options) => {
|
|
6975
7666
|
showIdea(id, options);
|
|
6976
7667
|
});
|
|
7668
|
+
show.command("track").description("Show track details").argument("<id>", "Track id, short id, or unique name").option("--compact", "Show a smaller summary view").action((id, options) => {
|
|
7669
|
+
showTrack(id, options);
|
|
7670
|
+
});
|
|
6977
7671
|
show.command("delivery").description("Show delivery details").argument("<id>", "Delivery id or name").option("--compact", "Show a smaller summary view").action((id, options) => {
|
|
6978
7672
|
showDelivery(id, options);
|
|
6979
7673
|
});
|
|
@@ -7556,6 +8250,10 @@ function registerUseCommand(program) {
|
|
|
7556
8250
|
const root = resolveRepoRoot();
|
|
7557
8251
|
printContext(clearWorkingContext(root, resolveCoopHome(), scope));
|
|
7558
8252
|
});
|
|
8253
|
+
use.command("reset").description("Clear all working-context values").action(() => {
|
|
8254
|
+
const root = resolveRepoRoot();
|
|
8255
|
+
printContext(clearWorkingContext(root, resolveCoopHome(), "all"));
|
|
8256
|
+
});
|
|
7559
8257
|
}
|
|
7560
8258
|
|
|
7561
8259
|
// src/commands/view.ts
|
|
@@ -7896,7 +8594,7 @@ function registerWebhookCommand(program) {
|
|
|
7896
8594
|
|
|
7897
8595
|
// src/merge-driver/merge-driver.ts
|
|
7898
8596
|
import fs21 from "fs";
|
|
7899
|
-
import
|
|
8597
|
+
import os3 from "os";
|
|
7900
8598
|
import path25 from "path";
|
|
7901
8599
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
7902
8600
|
import { stringifyFrontmatter as stringifyFrontmatter6, parseFrontmatterContent as parseFrontmatterContent3, parseYamlContent as parseYamlContent3, stringifyYamlContent as stringifyYamlContent2 } from "@kitsy/coop-core";
|
|
@@ -7987,7 +8685,7 @@ function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
|
|
|
7987
8685
|
const ours = parseTaskDocument(oursRaw, oursPath);
|
|
7988
8686
|
const theirs = parseTaskDocument(theirsRaw, theirsPath);
|
|
7989
8687
|
const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
|
|
7990
|
-
const tempDir = fs21.mkdtempSync(path25.join(
|
|
8688
|
+
const tempDir = fs21.mkdtempSync(path25.join(os3.tmpdir(), "coop-merge-body-"));
|
|
7991
8689
|
try {
|
|
7992
8690
|
const ancestorBody = path25.join(tempDir, "ancestor.md");
|
|
7993
8691
|
const oursBody = path25.join(tempDir, "ours.md");
|
|
@@ -8035,6 +8733,8 @@ function renderBasicHelp() {
|
|
|
8035
8733
|
"- `coop current`: show working context, active work, and the next ready task",
|
|
8036
8734
|
"- `coop use track <id>` / `coop use delivery <id>`: set working scope defaults",
|
|
8037
8735
|
"- `coop list tracks` / `coop list deliveries`: inspect valid named values before assigning them",
|
|
8736
|
+
"- `coop naming`: inspect per-entity ID rules and naming tokens",
|
|
8737
|
+
'- `coop naming preview "Title" --entity task`: preview the generated ID before creating an item',
|
|
8038
8738
|
"- `coop next task` or `coop pick task`: choose work from COOP",
|
|
8039
8739
|
"- `coop show <id>`: inspect a task, idea, or delivery",
|
|
8040
8740
|
"- `coop list tasks --track <id>`: browse scoped work",
|
|
@@ -8082,6 +8782,7 @@ Common day-to-day commands:
|
|
|
8082
8782
|
coop current
|
|
8083
8783
|
coop next task
|
|
8084
8784
|
coop show <id>
|
|
8785
|
+
coop naming
|
|
8085
8786
|
`);
|
|
8086
8787
|
registerInitCommand(program);
|
|
8087
8788
|
registerCreateCommand(program);
|