@kitsy/coop 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/dist/index.js +732 -188
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs15 from "fs";
|
|
5
|
+
import path19 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/utils/aliases.ts
|
|
10
10
|
import fs2 from "fs";
|
|
11
11
|
import path2 from "path";
|
|
12
|
-
import { parseIdeaFile, parseTaskFile as parseTaskFile2, parseYamlFile
|
|
12
|
+
import { parseIdeaFile, parseTaskFile as parseTaskFile2, parseYamlFile, stringifyFrontmatter, writeTask, writeYamlFile as writeYamlFile2 } from "@kitsy/coop-core";
|
|
13
13
|
|
|
14
14
|
// src/utils/repo.ts
|
|
15
15
|
import fs from "fs";
|
|
@@ -17,7 +17,17 @@ import os from "os";
|
|
|
17
17
|
import path from "path";
|
|
18
18
|
import { spawnSync } from "child_process";
|
|
19
19
|
import crypto from "crypto";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
coop_project_config_path,
|
|
22
|
+
coop_workspace_config_path,
|
|
23
|
+
list_projects as listWorkspaceProjects,
|
|
24
|
+
parseTaskFile,
|
|
25
|
+
read_project_config,
|
|
26
|
+
read_workspace_config,
|
|
27
|
+
resolve_project,
|
|
28
|
+
write_workspace_config,
|
|
29
|
+
writeYamlFile
|
|
30
|
+
} from "@kitsy/coop-core";
|
|
21
31
|
var SEQ_MARKER = "COOPSEQTOKEN";
|
|
22
32
|
function resolveCoopHome() {
|
|
23
33
|
const configured = process.env.COOP_HOME?.trim();
|
|
@@ -39,21 +49,54 @@ function resolveRepoRoot(start = process.cwd()) {
|
|
|
39
49
|
current = parent;
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
|
-
function
|
|
52
|
+
function resolveRequestedProject(argv = process.argv) {
|
|
53
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
54
|
+
const current = argv[i]?.trim();
|
|
55
|
+
if (current === "--project" || current === "-p") {
|
|
56
|
+
const next = argv[i + 1]?.trim();
|
|
57
|
+
return next || void 0;
|
|
58
|
+
}
|
|
59
|
+
if (current?.startsWith("--project=")) {
|
|
60
|
+
return current.slice("--project=".length).trim() || void 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
function coopWorkspaceDir(root) {
|
|
43
66
|
return path.join(root, ".coop");
|
|
44
67
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
function coopDir(root, projectId = resolveRequestedProject()) {
|
|
69
|
+
return resolve_project(root, { project: projectId, require: true }).root;
|
|
70
|
+
}
|
|
71
|
+
function ensureCoopInitialized(root, projectId = resolveRequestedProject()) {
|
|
72
|
+
const workspaceDir = coopWorkspaceDir(root);
|
|
73
|
+
if (!fs.existsSync(workspaceDir)) {
|
|
74
|
+
throw new Error(`Missing .coop directory at ${workspaceDir}. Run 'coop init'.`);
|
|
75
|
+
}
|
|
76
|
+
const project = resolve_project(root, { project: projectId, require: true });
|
|
77
|
+
if (!fs.existsSync(project.root)) {
|
|
78
|
+
throw new Error(`Missing COOP project directory at ${project.root}. Run 'coop init'.`);
|
|
49
79
|
}
|
|
50
|
-
return
|
|
80
|
+
return project.root;
|
|
51
81
|
}
|
|
52
|
-
function coopConfigPath(root) {
|
|
53
|
-
return
|
|
82
|
+
function coopConfigPath(root, projectId = resolveRequestedProject()) {
|
|
83
|
+
return coop_project_config_path(ensureCoopInitialized(root, projectId));
|
|
54
84
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
85
|
+
function readWorkspaceConfig(root) {
|
|
86
|
+
return read_workspace_config(root);
|
|
87
|
+
}
|
|
88
|
+
function writeWorkspaceConfig(root, config) {
|
|
89
|
+
write_workspace_config(root, config);
|
|
90
|
+
}
|
|
91
|
+
function listProjects(root) {
|
|
92
|
+
return listWorkspaceProjects(root);
|
|
93
|
+
}
|
|
94
|
+
function resolveProject(root, projectId = resolveRequestedProject()) {
|
|
95
|
+
return resolve_project(root, { project: projectId, require: true });
|
|
96
|
+
}
|
|
97
|
+
function readCoopConfig(root, projectId = resolveRequestedProject()) {
|
|
98
|
+
const project = resolve_project(root, { project: projectId, require: false });
|
|
99
|
+
const configPath = coop_project_config_path(project.root);
|
|
57
100
|
const repoName = path.basename(path.resolve(root));
|
|
58
101
|
if (!fs.existsSync(configPath)) {
|
|
59
102
|
return {
|
|
@@ -69,10 +112,10 @@ function readCoopConfig(root) {
|
|
|
69
112
|
filePath: configPath
|
|
70
113
|
};
|
|
71
114
|
}
|
|
72
|
-
const config =
|
|
115
|
+
const config = read_project_config(project.root);
|
|
73
116
|
const projectRaw = typeof config.project === "object" && config.project !== null ? config.project : {};
|
|
74
117
|
const projectName = typeof projectRaw.name === "string" && projectRaw.name.trim().length > 0 ? projectRaw.name.trim() : repoName;
|
|
75
|
-
const
|
|
118
|
+
const resolvedProjectId = typeof projectRaw.id === "string" && projectRaw.id.trim().length > 0 ? projectRaw.id.trim() : repoName;
|
|
76
119
|
const projectAliases = Array.isArray(projectRaw.aliases) ? projectRaw.aliases.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
|
|
77
120
|
const idPrefixesRaw = config.id_prefixes;
|
|
78
121
|
const idPrefixes = typeof idPrefixesRaw === "object" && idPrefixesRaw !== null ? idPrefixesRaw : {};
|
|
@@ -93,7 +136,7 @@ function readCoopConfig(root) {
|
|
|
93
136
|
idNamingTemplate,
|
|
94
137
|
idSeqPadding,
|
|
95
138
|
projectName: projectName || "COOP Workspace",
|
|
96
|
-
projectId:
|
|
139
|
+
projectId: resolvedProjectId || "workspace",
|
|
97
140
|
projectAliases,
|
|
98
141
|
raw: config,
|
|
99
142
|
filePath: configPath
|
|
@@ -158,15 +201,15 @@ function findTaskFileById(root, id) {
|
|
|
158
201
|
const target = `${id}.md`.toLowerCase();
|
|
159
202
|
const match = listTaskFiles(root).find((filePath) => path.basename(filePath).toLowerCase() === target);
|
|
160
203
|
if (!match) {
|
|
161
|
-
throw new Error(`Task '${id}' not found in
|
|
204
|
+
throw new Error(`Task '${id}' not found in the active COOP project.`);
|
|
162
205
|
}
|
|
163
206
|
return match;
|
|
164
207
|
}
|
|
165
208
|
function todayIsoDate() {
|
|
166
209
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
167
210
|
}
|
|
168
|
-
function normalizeIdPart(
|
|
169
|
-
const normalized =
|
|
211
|
+
function normalizeIdPart(input3, fallback, maxLength = 12) {
|
|
212
|
+
const normalized = input3.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-").replace(/-/g, "");
|
|
170
213
|
if (!normalized) return fallback;
|
|
171
214
|
return normalized.slice(0, maxLength);
|
|
172
215
|
}
|
|
@@ -202,8 +245,8 @@ function shortDateToken(now = /* @__PURE__ */ new Date()) {
|
|
|
202
245
|
function randomToken() {
|
|
203
246
|
return crypto.randomBytes(4).toString("hex").toUpperCase();
|
|
204
247
|
}
|
|
205
|
-
function sanitizeTemplateValue(
|
|
206
|
-
const normalized =
|
|
248
|
+
function sanitizeTemplateValue(input3, fallback = "X") {
|
|
249
|
+
const normalized = input3.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
207
250
|
return normalized || fallback;
|
|
208
251
|
}
|
|
209
252
|
function sequenceForPattern(existingIds, prefix, suffix) {
|
|
@@ -311,27 +354,27 @@ function generateConfiguredId(root, existingIds, context) {
|
|
|
311
354
|
|
|
312
355
|
// src/utils/aliases.ts
|
|
313
356
|
var ALIAS_PATTERN = /^[A-Z0-9]+(?:[.-][A-Z0-9]+)*$/;
|
|
314
|
-
function toPosixPath(
|
|
315
|
-
return
|
|
357
|
+
function toPosixPath(input3) {
|
|
358
|
+
return input3.replace(/\\/g, "/");
|
|
316
359
|
}
|
|
317
360
|
function indexFilePath(root) {
|
|
318
361
|
const { indexDataFormat } = readCoopConfig(root);
|
|
319
362
|
const extension = indexDataFormat === "json" ? "json" : "yml";
|
|
320
|
-
return path2.join(root, ".
|
|
363
|
+
return path2.join(ensureCoopInitialized(root), ".index", `aliases.${extension}`);
|
|
321
364
|
}
|
|
322
|
-
function normalizeAliasValue(
|
|
323
|
-
return
|
|
365
|
+
function normalizeAliasValue(input3) {
|
|
366
|
+
return input3.trim().toUpperCase().replace(/_/g, ".").replace(/\.+/g, ".");
|
|
324
367
|
}
|
|
325
|
-
function normalizePatternValue(
|
|
326
|
-
return
|
|
368
|
+
function normalizePatternValue(input3) {
|
|
369
|
+
return input3.trim().toUpperCase().replace(/_/g, ".");
|
|
327
370
|
}
|
|
328
|
-
function normalizeAlias(
|
|
329
|
-
const normalized = normalizeAliasValue(
|
|
371
|
+
function normalizeAlias(input3) {
|
|
372
|
+
const normalized = normalizeAliasValue(input3);
|
|
330
373
|
if (!normalized) {
|
|
331
374
|
throw new Error("Alias cannot be empty.");
|
|
332
375
|
}
|
|
333
376
|
if (!ALIAS_PATTERN.test(normalized)) {
|
|
334
|
-
throw new Error(`Invalid alias '${
|
|
377
|
+
throw new Error(`Invalid alias '${input3}'. Use letters/numbers with '.' and '-'.`);
|
|
335
378
|
}
|
|
336
379
|
return normalized;
|
|
337
380
|
}
|
|
@@ -383,7 +426,7 @@ function readIndexFile(root, aliasIndexPath) {
|
|
|
383
426
|
const parsed = JSON.parse(fs2.readFileSync(aliasIndexPath, "utf8"));
|
|
384
427
|
return parsed;
|
|
385
428
|
}
|
|
386
|
-
return
|
|
429
|
+
return parseYamlFile(aliasIndexPath);
|
|
387
430
|
} catch {
|
|
388
431
|
return null;
|
|
389
432
|
}
|
|
@@ -394,14 +437,14 @@ function writeIndexFile(root, data) {
|
|
|
394
437
|
if (aliasIndexPath.endsWith(".json")) {
|
|
395
438
|
fs2.writeFileSync(aliasIndexPath, `${JSON.stringify(data, null, 2)}
|
|
396
439
|
`, "utf8");
|
|
397
|
-
const yamlPath = path2.join(root, ".
|
|
440
|
+
const yamlPath = path2.join(ensureCoopInitialized(root), ".index", "aliases.yml");
|
|
398
441
|
if (fs2.existsSync(yamlPath)) {
|
|
399
442
|
fs2.rmSync(yamlPath, { force: true });
|
|
400
443
|
}
|
|
401
444
|
return;
|
|
402
445
|
}
|
|
403
446
|
writeYamlFile2(aliasIndexPath, data);
|
|
404
|
-
const jsonPath = path2.join(root, ".
|
|
447
|
+
const jsonPath = path2.join(ensureCoopInitialized(root), ".index", "aliases.json");
|
|
405
448
|
if (fs2.existsSync(jsonPath)) {
|
|
406
449
|
fs2.rmSync(jsonPath, { force: true });
|
|
407
450
|
}
|
|
@@ -518,8 +561,8 @@ function updateTaskAliases(filePath, aliases) {
|
|
|
518
561
|
function updateIdeaAliases(filePath, aliases) {
|
|
519
562
|
const parsed = parseIdeaFile(filePath);
|
|
520
563
|
const nextRaw = { ...parsed.raw, aliases };
|
|
521
|
-
const
|
|
522
|
-
fs2.writeFileSync(filePath,
|
|
564
|
+
const output3 = stringifyFrontmatter(nextRaw, parsed.body);
|
|
565
|
+
fs2.writeFileSync(filePath, output3, "utf8");
|
|
523
566
|
}
|
|
524
567
|
function resolveFilePath(root, target) {
|
|
525
568
|
return path2.join(root, ...target.file.split("/"));
|
|
@@ -1061,14 +1104,14 @@ function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
|
|
|
1061
1104
|
};
|
|
1062
1105
|
fs3.writeFileSync(filePath, stringifyFrontmatter2(nextRaw, body), "utf8");
|
|
1063
1106
|
}
|
|
1064
|
-
function makeTaskDraft(
|
|
1107
|
+
function makeTaskDraft(input3) {
|
|
1065
1108
|
return {
|
|
1066
|
-
title:
|
|
1067
|
-
type:
|
|
1068
|
-
status:
|
|
1069
|
-
track:
|
|
1070
|
-
priority:
|
|
1071
|
-
body:
|
|
1109
|
+
title: input3.title,
|
|
1110
|
+
type: input3.type,
|
|
1111
|
+
status: input3.status,
|
|
1112
|
+
track: input3.track,
|
|
1113
|
+
priority: input3.priority,
|
|
1114
|
+
body: input3.body
|
|
1072
1115
|
};
|
|
1073
1116
|
}
|
|
1074
1117
|
function registerCreateCommand(program) {
|
|
@@ -1419,10 +1462,10 @@ import {
|
|
|
1419
1462
|
function normalize(value) {
|
|
1420
1463
|
return value.trim().toLowerCase();
|
|
1421
1464
|
}
|
|
1422
|
-
function resolveDelivery(graph,
|
|
1423
|
-
const direct = graph.deliveries.get(
|
|
1465
|
+
function resolveDelivery(graph, input3) {
|
|
1466
|
+
const direct = graph.deliveries.get(input3);
|
|
1424
1467
|
if (direct) return direct;
|
|
1425
|
-
const target = normalize(
|
|
1468
|
+
const target = normalize(input3);
|
|
1426
1469
|
const byId = Array.from(graph.deliveries.values()).find((delivery) => normalize(delivery.id) === target);
|
|
1427
1470
|
if (byId) return byId;
|
|
1428
1471
|
const byName = Array.from(graph.deliveries.values()).filter((delivery) => normalize(delivery.name) === target);
|
|
@@ -1430,9 +1473,9 @@ function resolveDelivery(graph, input) {
|
|
|
1430
1473
|
return byName[0];
|
|
1431
1474
|
}
|
|
1432
1475
|
if (byName.length > 1) {
|
|
1433
|
-
throw new Error(`Multiple deliveries match '${
|
|
1476
|
+
throw new Error(`Multiple deliveries match '${input3}'. Use delivery id instead.`);
|
|
1434
1477
|
}
|
|
1435
|
-
throw new Error(`Delivery '${
|
|
1478
|
+
throw new Error(`Delivery '${input3}' not found.`);
|
|
1436
1479
|
}
|
|
1437
1480
|
|
|
1438
1481
|
// src/commands/graph.ts
|
|
@@ -1623,6 +1666,8 @@ function registerIndexCommand(program) {
|
|
|
1623
1666
|
import fs6 from "fs";
|
|
1624
1667
|
import path8 from "path";
|
|
1625
1668
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1669
|
+
import { createInterface } from "readline/promises";
|
|
1670
|
+
import { stdin as input, stdout as output } from "process";
|
|
1626
1671
|
import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
|
|
1627
1672
|
|
|
1628
1673
|
// src/hooks/pre-commit.ts
|
|
@@ -1650,7 +1695,41 @@ function toPosixPath2(filePath) {
|
|
|
1650
1695
|
}
|
|
1651
1696
|
function stagedTaskFiles(repoRoot) {
|
|
1652
1697
|
const { stdout } = runGit(repoRoot, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
|
|
1653
|
-
return stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean).map(toPosixPath2).filter(
|
|
1698
|
+
return stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean).map(toPosixPath2).filter(
|
|
1699
|
+
(entry) => (entry.startsWith(".coop/tasks/") || /^\.coop\/projects\/[^/]+\/tasks\//.test(entry)) && entry.endsWith(".md")
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
function projectRootFromRelativePath(repoRoot, relativePath) {
|
|
1703
|
+
const normalized = toPosixPath2(relativePath);
|
|
1704
|
+
const projectMatch = /^\.coop\/projects\/([^/]+)\/tasks\/.+\.md$/i.exec(normalized);
|
|
1705
|
+
if (projectMatch?.[1]) {
|
|
1706
|
+
return path6.join(repoRoot, ".coop", "projects", projectMatch[1]);
|
|
1707
|
+
}
|
|
1708
|
+
if (normalized.startsWith(".coop/tasks/")) {
|
|
1709
|
+
return path6.join(repoRoot, ".coop");
|
|
1710
|
+
}
|
|
1711
|
+
throw new Error(`Unsupported staged COOP task path '${relativePath}'.`);
|
|
1712
|
+
}
|
|
1713
|
+
function listTaskFilesForProject(projectRoot) {
|
|
1714
|
+
const tasksDir = path6.join(projectRoot, "tasks");
|
|
1715
|
+
if (!fs4.existsSync(tasksDir)) return [];
|
|
1716
|
+
const out = [];
|
|
1717
|
+
const stack = [tasksDir];
|
|
1718
|
+
while (stack.length > 0) {
|
|
1719
|
+
const current = stack.pop();
|
|
1720
|
+
const entries = fs4.readdirSync(current, { withFileTypes: true });
|
|
1721
|
+
for (const entry of entries) {
|
|
1722
|
+
const fullPath = path6.join(current, entry.name);
|
|
1723
|
+
if (entry.isDirectory()) {
|
|
1724
|
+
stack.push(fullPath);
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
if (entry.isFile() && path6.extname(entry.name).toLowerCase() === ".md") {
|
|
1728
|
+
out.push(fullPath);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
1654
1733
|
}
|
|
1655
1734
|
function readGitBlob(repoRoot, spec) {
|
|
1656
1735
|
const result = runGit(repoRoot, ["show", spec], true);
|
|
@@ -1665,6 +1744,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
1665
1744
|
const staged = [];
|
|
1666
1745
|
for (const relativePath of relativePaths) {
|
|
1667
1746
|
const absolutePath = path6.join(repoRoot, ...relativePath.split("/"));
|
|
1747
|
+
const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
|
|
1668
1748
|
const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
|
|
1669
1749
|
if (!stagedBlob) {
|
|
1670
1750
|
errors.push(`[COOP] Unable to read staged task '${relativePath}' from git index.`);
|
|
@@ -1685,6 +1765,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
1685
1765
|
staged.push({
|
|
1686
1766
|
relativePath,
|
|
1687
1767
|
absolutePath,
|
|
1768
|
+
projectRoot,
|
|
1688
1769
|
task
|
|
1689
1770
|
});
|
|
1690
1771
|
}
|
|
@@ -1719,13 +1800,13 @@ function buildGraphForCycleCheck(tasks) {
|
|
|
1719
1800
|
deliveries: /* @__PURE__ */ new Map()
|
|
1720
1801
|
};
|
|
1721
1802
|
}
|
|
1722
|
-
function collectTasksForCycleCheck(
|
|
1803
|
+
function collectTasksForCycleCheck(projectRoot, stagedTasks) {
|
|
1723
1804
|
const stagedByPath = /* @__PURE__ */ new Map();
|
|
1724
1805
|
for (const staged of stagedTasks) {
|
|
1725
1806
|
stagedByPath.set(toPosixPath2(staged.absolutePath), staged.task);
|
|
1726
1807
|
}
|
|
1727
1808
|
const tasks = [];
|
|
1728
|
-
for (const filePath of
|
|
1809
|
+
for (const filePath of listTaskFilesForProject(projectRoot)) {
|
|
1729
1810
|
const normalized = toPosixPath2(path6.resolve(filePath));
|
|
1730
1811
|
const stagedTask = stagedByPath.get(normalized);
|
|
1731
1812
|
if (stagedTask) {
|
|
@@ -1744,16 +1825,25 @@ function runPreCommitChecks(repoRoot) {
|
|
|
1744
1825
|
const parsed = parseStagedTasks(repoRoot, relativeTaskFiles);
|
|
1745
1826
|
const errors = [...parsed.errors];
|
|
1746
1827
|
if (parsed.staged.length > 0 && errors.length === 0) {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1828
|
+
const stagedByProject = /* @__PURE__ */ new Map();
|
|
1829
|
+
for (const staged of parsed.staged) {
|
|
1830
|
+
const existing = stagedByProject.get(staged.projectRoot) ?? [];
|
|
1831
|
+
existing.push(staged);
|
|
1832
|
+
stagedByProject.set(staged.projectRoot, existing);
|
|
1833
|
+
}
|
|
1834
|
+
for (const [projectRoot, stagedTasks] of stagedByProject.entries()) {
|
|
1835
|
+
try {
|
|
1836
|
+
const tasks = collectTasksForCycleCheck(projectRoot, stagedTasks);
|
|
1837
|
+
const graph = buildGraphForCycleCheck(tasks);
|
|
1838
|
+
const cycle = detect_cycle(graph);
|
|
1839
|
+
if (cycle) {
|
|
1840
|
+
const projectLabel = toPosixPath2(path6.relative(repoRoot, projectRoot));
|
|
1841
|
+
errors.push(`[COOP] Dependency cycle detected in ${projectLabel}: ${cycle.join(" -> ")}.`);
|
|
1842
|
+
}
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1845
|
+
errors.push(`[COOP] Failed to run dependency cycle check: ${message}`);
|
|
1753
1846
|
}
|
|
1754
|
-
} catch (error) {
|
|
1755
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1756
|
-
errors.push(`[COOP] Failed to run dependency cycle check: ${message}`);
|
|
1757
1847
|
}
|
|
1758
1848
|
}
|
|
1759
1849
|
return {
|
|
@@ -1813,28 +1903,37 @@ function installPreCommitHook(repoRoot) {
|
|
|
1813
1903
|
// src/hooks/post-merge-validate.ts
|
|
1814
1904
|
import fs5 from "fs";
|
|
1815
1905
|
import path7 from "path";
|
|
1906
|
+
import { list_projects } from "@kitsy/coop-core";
|
|
1816
1907
|
import { load_graph as load_graph3, validate_graph as validate_graph2 } from "@kitsy/coop-core";
|
|
1817
1908
|
var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
|
|
1818
1909
|
var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
|
|
1819
1910
|
function runPostMergeValidate(repoRoot) {
|
|
1820
|
-
const
|
|
1821
|
-
if (!fs5.existsSync(
|
|
1911
|
+
const workspaceDir = path7.join(repoRoot, ".coop");
|
|
1912
|
+
if (!fs5.existsSync(workspaceDir)) {
|
|
1822
1913
|
return {
|
|
1823
1914
|
ok: true,
|
|
1824
1915
|
warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
|
|
1825
1916
|
};
|
|
1826
1917
|
}
|
|
1827
1918
|
try {
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1919
|
+
const projects = list_projects(repoRoot);
|
|
1920
|
+
if (projects.length === 0) {
|
|
1921
|
+
return {
|
|
1922
|
+
ok: true,
|
|
1923
|
+
warnings: ["[COOP] Skipped post-merge validation (no COOP projects found)."]
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
const warnings = [];
|
|
1927
|
+
for (const project of projects) {
|
|
1928
|
+
const graph = load_graph3(project.root);
|
|
1929
|
+
const issues = validate_graph2(graph);
|
|
1930
|
+
for (const issue of issues) {
|
|
1931
|
+
warnings.push(`[COOP] post-merge warning [${project.id}] [${issue.invariant}] ${issue.message}`);
|
|
1932
|
+
}
|
|
1832
1933
|
}
|
|
1833
1934
|
return {
|
|
1834
1935
|
ok: true,
|
|
1835
|
-
warnings
|
|
1836
|
-
(issue) => `[COOP] post-merge warning [${issue.invariant}] ${issue.message}`
|
|
1837
|
-
)
|
|
1936
|
+
warnings
|
|
1838
1937
|
};
|
|
1839
1938
|
} catch (error) {
|
|
1840
1939
|
return {
|
|
@@ -1893,14 +1992,32 @@ function installPostMergeHook(repoRoot) {
|
|
|
1893
1992
|
}
|
|
1894
1993
|
|
|
1895
1994
|
// src/commands/init.ts
|
|
1896
|
-
function
|
|
1897
|
-
|
|
1898
|
-
|
|
1995
|
+
function normalizeProjectId(value) {
|
|
1996
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
1997
|
+
}
|
|
1998
|
+
function parseAliases2(value) {
|
|
1999
|
+
if (!value) return [];
|
|
2000
|
+
return Array.from(
|
|
2001
|
+
new Set(
|
|
2002
|
+
value.split(",").map((entry) => entry.trim()).filter(Boolean).map(
|
|
2003
|
+
(entry) => entry.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-")
|
|
2004
|
+
).filter(Boolean)
|
|
2005
|
+
)
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
function aliasesYaml(aliases) {
|
|
2009
|
+
if (aliases.length === 0) {
|
|
2010
|
+
return " []";
|
|
2011
|
+
}
|
|
2012
|
+
return `
|
|
2013
|
+
${aliases.map((alias) => ` - "${alias}"`).join("\n")}`;
|
|
2014
|
+
}
|
|
2015
|
+
function buildProjectConfig(projectId, projectName, projectAliases) {
|
|
1899
2016
|
return `version: 2
|
|
1900
2017
|
project:
|
|
1901
|
-
name:
|
|
1902
|
-
id: "${
|
|
1903
|
-
aliases
|
|
2018
|
+
name: ${JSON.stringify(projectName)}
|
|
2019
|
+
id: "${projectId}"
|
|
2020
|
+
aliases:${aliasesYaml(projectAliases)}
|
|
1904
2021
|
|
|
1905
2022
|
id_prefixes:
|
|
1906
2023
|
idea: "IDEA"
|
|
@@ -1950,8 +2067,8 @@ api:
|
|
|
1950
2067
|
remotes: {}
|
|
1951
2068
|
|
|
1952
2069
|
hooks:
|
|
1953
|
-
on_task_transition: .coop/hooks/on-task-transition.sh
|
|
1954
|
-
on_delivery_complete: .coop/hooks/on-delivery-complete.sh
|
|
2070
|
+
on_task_transition: .coop/projects/${projectId}/hooks/on-task-transition.sh
|
|
2071
|
+
on_delivery_complete: .coop/projects/${projectId}/hooks/on-delivery-complete.sh
|
|
1955
2072
|
|
|
1956
2073
|
index:
|
|
1957
2074
|
data: yaml
|
|
@@ -2077,8 +2194,8 @@ function setGitConfig(root, key, value) {
|
|
|
2077
2194
|
}
|
|
2078
2195
|
function installMergeDrivers(root) {
|
|
2079
2196
|
const entries = [
|
|
2080
|
-
".coop/tasks/*.md merge=coop-task",
|
|
2081
|
-
".coop/deliveries/*.yml merge=coop-delivery"
|
|
2197
|
+
".coop/projects/*/tasks/*.md merge=coop-task",
|
|
2198
|
+
".coop/projects/*/deliveries/*.yml merge=coop-delivery"
|
|
2082
2199
|
];
|
|
2083
2200
|
for (const entry of entries) {
|
|
2084
2201
|
ensureGitattributesEntry(root, entry);
|
|
@@ -2094,10 +2211,58 @@ function installMergeDrivers(root) {
|
|
|
2094
2211
|
}
|
|
2095
2212
|
return "Updated .gitattributes but could not register merge drivers in git config.";
|
|
2096
2213
|
}
|
|
2214
|
+
async function promptInitIdentity(root, options) {
|
|
2215
|
+
const rl = createInterface({ input, output });
|
|
2216
|
+
const ask2 = async (question, fallback) => {
|
|
2217
|
+
try {
|
|
2218
|
+
const answer = await rl.question(`${question} [${fallback}]: `);
|
|
2219
|
+
return answer.trim() || fallback;
|
|
2220
|
+
} catch {
|
|
2221
|
+
return fallback;
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
const defaultName = repoDisplayName(root);
|
|
2225
|
+
try {
|
|
2226
|
+
const initialName = options.name?.trim() || defaultName;
|
|
2227
|
+
const projectName = options.name?.trim() || await ask2("Project name", initialName);
|
|
2228
|
+
const defaultId = options.id?.trim() || normalizeProjectId(projectName) || repoIdentityId(root);
|
|
2229
|
+
const projectIdRaw = options.id?.trim() || await ask2("Project id", defaultId);
|
|
2230
|
+
const projectId = normalizeProjectId(projectIdRaw);
|
|
2231
|
+
if (!projectId) {
|
|
2232
|
+
throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
|
|
2233
|
+
}
|
|
2234
|
+
const aliasesInput = options.aliases !== void 0 ? options.aliases : await ask2("Aliases (csv, optional)", "");
|
|
2235
|
+
return {
|
|
2236
|
+
projectName,
|
|
2237
|
+
projectId,
|
|
2238
|
+
projectAliases: parseAliases2(aliasesInput)
|
|
2239
|
+
};
|
|
2240
|
+
} finally {
|
|
2241
|
+
rl.close();
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
async function resolveInitIdentity(root, options) {
|
|
2245
|
+
const defaultName = repoDisplayName(root);
|
|
2246
|
+
const fallbackName = options.name?.trim() || defaultName;
|
|
2247
|
+
const fallbackId = normalizeProjectId(options.id?.trim() || fallbackName) || repoIdentityId(root);
|
|
2248
|
+
const fallbackAliases = parseAliases2(options.aliases);
|
|
2249
|
+
const interactive = Boolean(options.interactive || !options.yes && input.isTTY && output.isTTY);
|
|
2250
|
+
if (interactive) {
|
|
2251
|
+
return promptInitIdentity(root, options);
|
|
2252
|
+
}
|
|
2253
|
+
return {
|
|
2254
|
+
projectName: fallbackName,
|
|
2255
|
+
projectId: fallbackId,
|
|
2256
|
+
projectAliases: fallbackAliases
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2097
2259
|
function registerInitCommand(program) {
|
|
2098
|
-
program.command("init").description("Initialize .coop/ structure in the current repository").action(() => {
|
|
2260
|
+
program.command("init").description("Initialize .coop/ structure in the current repository").option("--name <name>", "Project display name").option("--id <id>", "Project id").option("--aliases <csv>", "Comma-separated project aliases").option("-y, --yes", "Use defaults without prompting").option("--interactive", "Prompt for missing project identity values").action(async (options) => {
|
|
2099
2261
|
const root = resolveRepoRoot();
|
|
2100
|
-
const
|
|
2262
|
+
const workspaceDir = coopWorkspaceDir(root);
|
|
2263
|
+
const identity = await resolveInitIdentity(root, options);
|
|
2264
|
+
const projectId = identity.projectId;
|
|
2265
|
+
const projectRoot = path8.join(workspaceDir, "projects", projectId);
|
|
2101
2266
|
const dirs = [
|
|
2102
2267
|
"ideas",
|
|
2103
2268
|
"tasks",
|
|
@@ -2113,25 +2278,36 @@ function registerInitCommand(program) {
|
|
|
2113
2278
|
"history/deliveries",
|
|
2114
2279
|
".index"
|
|
2115
2280
|
];
|
|
2281
|
+
ensureDir(path8.join(workspaceDir, "projects"));
|
|
2116
2282
|
for (const dir of dirs) {
|
|
2117
|
-
ensureDir(path8.join(
|
|
2118
|
-
}
|
|
2119
|
-
writeIfMissing(
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
writeIfMissing(path8.join(
|
|
2127
|
-
writeIfMissing(path8.join(
|
|
2128
|
-
writeIfMissing(path8.join(
|
|
2129
|
-
|
|
2283
|
+
ensureDir(path8.join(projectRoot, dir));
|
|
2284
|
+
}
|
|
2285
|
+
writeIfMissing(
|
|
2286
|
+
path8.join(projectRoot, "config.yml"),
|
|
2287
|
+
buildProjectConfig(projectId, identity.projectName, identity.projectAliases)
|
|
2288
|
+
);
|
|
2289
|
+
if (!fs6.existsSync(path8.join(projectRoot, "schema-version"))) {
|
|
2290
|
+
write_schema_version(projectRoot, CURRENT_SCHEMA_VERSION);
|
|
2291
|
+
}
|
|
2292
|
+
writeIfMissing(path8.join(projectRoot, "templates/task.md"), TASK_TEMPLATE);
|
|
2293
|
+
writeIfMissing(path8.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE);
|
|
2294
|
+
writeIfMissing(path8.join(projectRoot, "plugins/console-log.yml"), PLUGIN_CONSOLE_TEMPLATE);
|
|
2295
|
+
writeIfMissing(path8.join(projectRoot, "plugins/github-pr.yml"), PLUGIN_GITHUB_TEMPLATE);
|
|
2296
|
+
writeIfMissing(path8.join(workspaceDir, ".ignore"), COOP_IGNORE_TEMPLATE);
|
|
2297
|
+
writeIfMissing(path8.join(workspaceDir, ".gitignore"), COOP_IGNORE_TEMPLATE);
|
|
2298
|
+
writeWorkspaceConfig(root, { version: 2, current_project: projectId });
|
|
2299
|
+
ensureGitignoreEntry(root, ".coop/logs/");
|
|
2300
|
+
ensureGitignoreEntry(root, ".coop/tmp/");
|
|
2130
2301
|
const preCommitHook = installPreCommitHook(root);
|
|
2131
2302
|
const postMergeHook = installPostMergeHook(root);
|
|
2132
2303
|
const mergeDrivers = installMergeDrivers(root);
|
|
2304
|
+
const project = resolveProject(root, projectId);
|
|
2133
2305
|
console.log("Initialized COOP workspace.");
|
|
2134
2306
|
console.log(`- Root: ${root}`);
|
|
2307
|
+
console.log(`- Workspace: ${path8.relative(root, workspaceDir)}`);
|
|
2308
|
+
console.log(`- Project: ${project.id} (${path8.relative(root, project.root)})`);
|
|
2309
|
+
console.log(`- Name: ${identity.projectName}`);
|
|
2310
|
+
console.log(`- Aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
|
|
2135
2311
|
console.log(`- ${preCommitHook.message}`);
|
|
2136
2312
|
if (preCommitHook.installed) {
|
|
2137
2313
|
console.log(`- Hook: ${path8.relative(root, preCommitHook.hookPath)}`);
|
|
@@ -2290,9 +2466,9 @@ function resolveRepoSafe(start = process.cwd()) {
|
|
|
2290
2466
|
}
|
|
2291
2467
|
function resolveCliLogFile(start = process.cwd()) {
|
|
2292
2468
|
const root = resolveRepoSafe(start);
|
|
2293
|
-
const
|
|
2294
|
-
if (fs7.existsSync(
|
|
2295
|
-
return path10.join(
|
|
2469
|
+
const workspace = coopWorkspaceDir(root);
|
|
2470
|
+
if (fs7.existsSync(workspace)) {
|
|
2471
|
+
return path10.join(workspace, "logs", "cli.log");
|
|
2296
2472
|
}
|
|
2297
2473
|
return path10.join(resolveCoopHome(), "logs", "cli.log");
|
|
2298
2474
|
}
|
|
@@ -2373,8 +2549,11 @@ function registerLogCommand(program) {
|
|
|
2373
2549
|
}
|
|
2374
2550
|
|
|
2375
2551
|
// src/commands/migrate.ts
|
|
2552
|
+
import fs8 from "fs";
|
|
2376
2553
|
import path11 from "path";
|
|
2377
|
-
import {
|
|
2554
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2555
|
+
import { stdin as input2, stdout as output2 } from "process";
|
|
2556
|
+
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION2, IndexManager as IndexManager2, migrate_repository, parseYamlFile as parseYamlFile2, writeYamlFile as writeYamlFile4 } from "@kitsy/coop-core";
|
|
2378
2557
|
function parseTargetVersion(raw) {
|
|
2379
2558
|
if (!raw) return CURRENT_SCHEMA_VERSION2;
|
|
2380
2559
|
const parsed = Number(raw);
|
|
@@ -2383,8 +2562,160 @@ function parseTargetVersion(raw) {
|
|
|
2383
2562
|
}
|
|
2384
2563
|
return parsed;
|
|
2385
2564
|
}
|
|
2565
|
+
function legacyWorkspaceProjectEntries(root) {
|
|
2566
|
+
const workspaceDir = coopWorkspaceDir(root);
|
|
2567
|
+
return [
|
|
2568
|
+
"config.yml",
|
|
2569
|
+
"schema-version",
|
|
2570
|
+
"ideas",
|
|
2571
|
+
"tasks",
|
|
2572
|
+
"tracks",
|
|
2573
|
+
"deliveries",
|
|
2574
|
+
"resources",
|
|
2575
|
+
"templates",
|
|
2576
|
+
"plugins",
|
|
2577
|
+
"hooks",
|
|
2578
|
+
"runs",
|
|
2579
|
+
"decisions",
|
|
2580
|
+
"history",
|
|
2581
|
+
".index",
|
|
2582
|
+
"views",
|
|
2583
|
+
"backlog",
|
|
2584
|
+
"plans",
|
|
2585
|
+
"releases"
|
|
2586
|
+
].filter((entry) => fs8.existsSync(path11.join(workspaceDir, entry)));
|
|
2587
|
+
}
|
|
2588
|
+
function normalizeProjectId2(value) {
|
|
2589
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
2590
|
+
}
|
|
2591
|
+
function parseAliases3(value) {
|
|
2592
|
+
if (!value) return [];
|
|
2593
|
+
return Array.from(
|
|
2594
|
+
new Set(
|
|
2595
|
+
value.split(",").map((entry) => entry.trim()).filter(Boolean).map(
|
|
2596
|
+
(entry) => entry.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-")
|
|
2597
|
+
).filter(Boolean)
|
|
2598
|
+
)
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
async function promptProjectIdentity(defaults, options) {
|
|
2602
|
+
const rl = createInterface2({ input: input2, output: output2 });
|
|
2603
|
+
const ask2 = async (question, fallback) => {
|
|
2604
|
+
try {
|
|
2605
|
+
const answer = await rl.question(`${question} [${fallback}]: `);
|
|
2606
|
+
return answer.trim() || fallback;
|
|
2607
|
+
} catch {
|
|
2608
|
+
return fallback;
|
|
2609
|
+
}
|
|
2610
|
+
};
|
|
2611
|
+
try {
|
|
2612
|
+
const projectName = options.name?.trim() || await ask2("Project name", defaults.projectName);
|
|
2613
|
+
const projectIdRaw = options.id?.trim() || await ask2("Project id", defaults.projectId);
|
|
2614
|
+
const projectId = normalizeProjectId2(projectIdRaw);
|
|
2615
|
+
if (!projectId) {
|
|
2616
|
+
throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
|
|
2617
|
+
}
|
|
2618
|
+
const aliasesInput = options.aliases !== void 0 ? options.aliases : await ask2("Aliases (csv, optional)", defaults.projectAliases.join(","));
|
|
2619
|
+
return {
|
|
2620
|
+
projectName,
|
|
2621
|
+
projectId,
|
|
2622
|
+
projectAliases: parseAliases3(aliasesInput)
|
|
2623
|
+
};
|
|
2624
|
+
} finally {
|
|
2625
|
+
rl.close();
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
async function resolveMigrationIdentity(root, options) {
|
|
2629
|
+
const existing = readCoopConfig(root);
|
|
2630
|
+
const defaults = {
|
|
2631
|
+
projectName: existing.projectName || path11.basename(root),
|
|
2632
|
+
projectId: normalizeProjectId2(existing.projectId || repoIdentityId(root)) || repoIdentityId(root),
|
|
2633
|
+
projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : existing.projectAliases
|
|
2634
|
+
};
|
|
2635
|
+
const interactive = Boolean(options.interactive || !options.yes && input2.isTTY && output2.isTTY);
|
|
2636
|
+
if (interactive) {
|
|
2637
|
+
return promptProjectIdentity(defaults, options);
|
|
2638
|
+
}
|
|
2639
|
+
return {
|
|
2640
|
+
projectName: options.name?.trim() || defaults.projectName,
|
|
2641
|
+
projectId: normalizeProjectId2(options.id?.trim() || defaults.projectId) || defaults.projectId,
|
|
2642
|
+
projectAliases: options.aliases !== void 0 ? parseAliases3(options.aliases) : defaults.projectAliases
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
async function migrateWorkspaceLayout(root, options) {
|
|
2646
|
+
const target = (options.to ?? "").trim().toLowerCase();
|
|
2647
|
+
if (target !== "v2") {
|
|
2648
|
+
throw new Error(`Unsupported workspace-layout target '${options.to ?? ""}'. Expected 'v2'.`);
|
|
2649
|
+
}
|
|
2650
|
+
const workspaceDir = coopWorkspaceDir(root);
|
|
2651
|
+
if (!fs8.existsSync(workspaceDir)) {
|
|
2652
|
+
throw new Error("Missing .coop directory. Run 'coop init' first.");
|
|
2653
|
+
}
|
|
2654
|
+
const projectsDir = path11.join(workspaceDir, "projects");
|
|
2655
|
+
const legacyEntries = legacyWorkspaceProjectEntries(root);
|
|
2656
|
+
if (legacyEntries.length === 0 && fs8.existsSync(projectsDir)) {
|
|
2657
|
+
console.log("[COOP] workspace layout already uses v2.");
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
const identity = await resolveMigrationIdentity(root, options);
|
|
2661
|
+
const projectId = identity.projectId;
|
|
2662
|
+
const projectRoot = path11.join(projectsDir, projectId);
|
|
2663
|
+
if (fs8.existsSync(projectRoot) && !options.force) {
|
|
2664
|
+
throw new Error(`Project destination '${path11.relative(root, projectRoot)}' already exists. Re-run with --force.`);
|
|
2665
|
+
}
|
|
2666
|
+
const changed = legacyEntries.map((entry) => `${path11.join(".coop", entry)} -> ${path11.join(".coop", "projects", projectId, entry)}`);
|
|
2667
|
+
changed.push(`.coop/config.yml -> workspace current_project=${projectId}`);
|
|
2668
|
+
console.log(`Workspace layout migration (${options.dryRun ? "DRY RUN" : "APPLY"})`);
|
|
2669
|
+
console.log(`- from: v1 flat layout`);
|
|
2670
|
+
console.log(`- to: v2 multi-project layout`);
|
|
2671
|
+
console.log(`- project: ${projectId}`);
|
|
2672
|
+
console.log(`- name: ${identity.projectName}`);
|
|
2673
|
+
console.log(`- aliases: ${identity.projectAliases.length > 0 ? identity.projectAliases.join(", ") : "(none)"}`);
|
|
2674
|
+
console.log(`- entries: ${legacyEntries.length}`);
|
|
2675
|
+
for (const entry of changed) {
|
|
2676
|
+
console.log(` - ${entry}`);
|
|
2677
|
+
}
|
|
2678
|
+
if (options.dryRun) {
|
|
2679
|
+
console.log("- no files were modified.");
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
fs8.mkdirSync(projectsDir, { recursive: true });
|
|
2683
|
+
fs8.mkdirSync(projectRoot, { recursive: true });
|
|
2684
|
+
for (const entry of legacyEntries) {
|
|
2685
|
+
const source = path11.join(workspaceDir, entry);
|
|
2686
|
+
const destination = path11.join(projectRoot, entry);
|
|
2687
|
+
if (fs8.existsSync(destination)) {
|
|
2688
|
+
if (!options.force) {
|
|
2689
|
+
throw new Error(`Destination '${path11.relative(root, destination)}' already exists.`);
|
|
2690
|
+
}
|
|
2691
|
+
fs8.rmSync(destination, { recursive: true, force: true });
|
|
2692
|
+
}
|
|
2693
|
+
fs8.renameSync(source, destination);
|
|
2694
|
+
}
|
|
2695
|
+
const movedConfigPath = path11.join(projectRoot, "config.yml");
|
|
2696
|
+
if (fs8.existsSync(movedConfigPath)) {
|
|
2697
|
+
const movedConfig = parseYamlFile2(movedConfigPath);
|
|
2698
|
+
const nextProject = typeof movedConfig.project === "object" && movedConfig.project !== null ? { ...movedConfig.project } : {};
|
|
2699
|
+
nextProject.name = identity.projectName;
|
|
2700
|
+
nextProject.id = projectId;
|
|
2701
|
+
nextProject.aliases = identity.projectAliases;
|
|
2702
|
+
const nextHooks = typeof movedConfig.hooks === "object" && movedConfig.hooks !== null ? { ...movedConfig.hooks } : {};
|
|
2703
|
+
nextHooks.on_task_transition = `.coop/projects/${projectId}/hooks/on-task-transition.sh`;
|
|
2704
|
+
nextHooks.on_delivery_complete = `.coop/projects/${projectId}/hooks/on-delivery-complete.sh`;
|
|
2705
|
+
writeYamlFile4(movedConfigPath, {
|
|
2706
|
+
...movedConfig,
|
|
2707
|
+
project: nextProject,
|
|
2708
|
+
hooks: nextHooks
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
const workspace = readWorkspaceConfig(root);
|
|
2712
|
+
writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: projectId });
|
|
2713
|
+
const manager = new IndexManager2(projectRoot);
|
|
2714
|
+
manager.build_full_index();
|
|
2715
|
+
console.log(`[COOP] migrated workspace to v2 at ${path11.relative(root, projectRoot)}`);
|
|
2716
|
+
}
|
|
2386
2717
|
function registerMigrateCommand(program) {
|
|
2387
|
-
program.command("migrate").description("Migrate
|
|
2718
|
+
const migrate = program.command("migrate").description("Migrate COOP data and workspace layouts").option("--dry-run", "Preview migration without writing files").option("--to <version>", "Target schema version", String(CURRENT_SCHEMA_VERSION2)).action((options) => {
|
|
2388
2719
|
const root = resolveRepoRoot();
|
|
2389
2720
|
ensureCoopInitialized(root);
|
|
2390
2721
|
const target = parseTargetVersion(options.to);
|
|
@@ -2409,6 +2740,15 @@ function registerMigrateCommand(program) {
|
|
|
2409
2740
|
console.log(`- schema-version updated to v${report.to_version}`);
|
|
2410
2741
|
}
|
|
2411
2742
|
});
|
|
2743
|
+
migrate.command("workspace-layout").description("Migrate a flat v1 workspace layout to the v2 multi-project layout").option("--dry-run", "Preview migration without writing files").option("--to <version>", "Target workspace layout version", "v2").option("--force", "Overwrite an existing destination project directory").option("--name <name>", "Project display name").option("--id <id>", "Project id").option("--aliases <csv>", "Comma-separated project aliases").option("-y, --yes", "Use existing project identity defaults without prompting").option("--interactive", "Prompt for project identity during migration").action(async (options) => {
|
|
2744
|
+
const root = resolveRepoRoot();
|
|
2745
|
+
const parentOptions = migrate.opts();
|
|
2746
|
+
await migrateWorkspaceLayout(root, {
|
|
2747
|
+
...parentOptions,
|
|
2748
|
+
...options,
|
|
2749
|
+
dryRun: Boolean(options.dryRun || parentOptions.dryRun)
|
|
2750
|
+
});
|
|
2751
|
+
});
|
|
2412
2752
|
}
|
|
2413
2753
|
|
|
2414
2754
|
// src/commands/plan.ts
|
|
@@ -2644,9 +2984,206 @@ function registerPlanCommand(program) {
|
|
|
2644
2984
|
});
|
|
2645
2985
|
}
|
|
2646
2986
|
|
|
2647
|
-
// src/commands/
|
|
2648
|
-
import
|
|
2987
|
+
// src/commands/project.ts
|
|
2988
|
+
import fs9 from "fs";
|
|
2649
2989
|
import path12 from "path";
|
|
2990
|
+
import { CURRENT_SCHEMA_VERSION as CURRENT_SCHEMA_VERSION3, write_schema_version as write_schema_version2 } from "@kitsy/coop-core";
|
|
2991
|
+
var TASK_TEMPLATE2 = `---
|
|
2992
|
+
id: TASK-001
|
|
2993
|
+
title: "Implement feature"
|
|
2994
|
+
type: feature
|
|
2995
|
+
status: todo
|
|
2996
|
+
created: 2026-03-06
|
|
2997
|
+
updated: 2026-03-06
|
|
2998
|
+
aliases: []
|
|
2999
|
+
priority: p2
|
|
3000
|
+
track: unassigned
|
|
3001
|
+
depends_on: []
|
|
3002
|
+
---
|
|
3003
|
+
|
|
3004
|
+
## Context
|
|
3005
|
+
Why this task exists.
|
|
3006
|
+
`;
|
|
3007
|
+
var IDEA_TEMPLATE2 = `---
|
|
3008
|
+
id: IDEA-001
|
|
3009
|
+
title: "New idea"
|
|
3010
|
+
created: 2026-03-06
|
|
3011
|
+
aliases: []
|
|
3012
|
+
author: your-name
|
|
3013
|
+
status: captured
|
|
3014
|
+
tags: []
|
|
3015
|
+
source: manual
|
|
3016
|
+
linked_tasks: []
|
|
3017
|
+
---
|
|
3018
|
+
|
|
3019
|
+
## Problem
|
|
3020
|
+
What problem are you solving?
|
|
3021
|
+
`;
|
|
3022
|
+
var PROJECT_CONFIG_TEMPLATE = (projectId, projectName) => `version: 2
|
|
3023
|
+
project:
|
|
3024
|
+
name: "${projectName}"
|
|
3025
|
+
id: "${projectId}"
|
|
3026
|
+
aliases: []
|
|
3027
|
+
|
|
3028
|
+
id_prefixes:
|
|
3029
|
+
idea: "IDEA"
|
|
3030
|
+
task: "PM"
|
|
3031
|
+
delivery: "DEL"
|
|
3032
|
+
run: "RUN"
|
|
3033
|
+
|
|
3034
|
+
id:
|
|
3035
|
+
naming: "<TYPE>-<USER>-<YYMMDD>-<RAND>"
|
|
3036
|
+
seq_padding: 0
|
|
3037
|
+
|
|
3038
|
+
defaults:
|
|
3039
|
+
task:
|
|
3040
|
+
type: feature
|
|
3041
|
+
priority: p2
|
|
3042
|
+
complexity: medium
|
|
3043
|
+
determinism: medium
|
|
3044
|
+
track: unassigned
|
|
3045
|
+
|
|
3046
|
+
scheduling:
|
|
3047
|
+
algorithm: critical-path
|
|
3048
|
+
velocity_window_weeks: 4
|
|
3049
|
+
overhead_factor: 1.2
|
|
3050
|
+
|
|
3051
|
+
ai:
|
|
3052
|
+
provider: mock
|
|
3053
|
+
model: mock-local
|
|
3054
|
+
default_executor: claude-sonnet
|
|
3055
|
+
sandbox: true
|
|
3056
|
+
max_concurrent_agents: 4
|
|
3057
|
+
token_budget_per_task: 50000
|
|
3058
|
+
|
|
3059
|
+
plugins:
|
|
3060
|
+
enabled: []
|
|
3061
|
+
|
|
3062
|
+
github:
|
|
3063
|
+
owner: ""
|
|
3064
|
+
repo: ""
|
|
3065
|
+
base_branch: main
|
|
3066
|
+
token_env: GITHUB_TOKEN
|
|
3067
|
+
webhook_secret_env: GITHUB_WEBHOOK_SECRET
|
|
3068
|
+
merge_method: squash
|
|
3069
|
+
|
|
3070
|
+
api:
|
|
3071
|
+
host: 127.0.0.1
|
|
3072
|
+
port: 3847
|
|
3073
|
+
remotes: {}
|
|
3074
|
+
|
|
3075
|
+
hooks:
|
|
3076
|
+
on_task_transition: .coop/projects/${projectId}/hooks/on-task-transition.sh
|
|
3077
|
+
on_delivery_complete: .coop/projects/${projectId}/hooks/on-delivery-complete.sh
|
|
3078
|
+
|
|
3079
|
+
index:
|
|
3080
|
+
data: yaml
|
|
3081
|
+
`;
|
|
3082
|
+
var PROJECT_DIRS = [
|
|
3083
|
+
"ideas",
|
|
3084
|
+
"tasks",
|
|
3085
|
+
"tracks",
|
|
3086
|
+
"deliveries",
|
|
3087
|
+
"resources",
|
|
3088
|
+
"templates",
|
|
3089
|
+
"plugins",
|
|
3090
|
+
"hooks",
|
|
3091
|
+
"runs",
|
|
3092
|
+
"decisions",
|
|
3093
|
+
"history/tasks",
|
|
3094
|
+
"history/deliveries",
|
|
3095
|
+
".index"
|
|
3096
|
+
];
|
|
3097
|
+
function ensureDir2(dirPath) {
|
|
3098
|
+
fs9.mkdirSync(dirPath, { recursive: true });
|
|
3099
|
+
}
|
|
3100
|
+
function writeIfMissing2(filePath, content) {
|
|
3101
|
+
if (!fs9.existsSync(filePath)) {
|
|
3102
|
+
fs9.writeFileSync(filePath, content, "utf8");
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
function normalizeProjectId3(value) {
|
|
3106
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
3107
|
+
}
|
|
3108
|
+
function createProject(root, projectId, projectName) {
|
|
3109
|
+
const workspaceDir = coopWorkspaceDir(root);
|
|
3110
|
+
const projectRoot = path12.join(workspaceDir, "projects", projectId);
|
|
3111
|
+
ensureDir2(path12.join(workspaceDir, "projects"));
|
|
3112
|
+
for (const dir of PROJECT_DIRS) {
|
|
3113
|
+
ensureDir2(path12.join(projectRoot, dir));
|
|
3114
|
+
}
|
|
3115
|
+
writeIfMissing2(path12.join(projectRoot, "config.yml"), PROJECT_CONFIG_TEMPLATE(projectId, projectName));
|
|
3116
|
+
if (!fs9.existsSync(path12.join(projectRoot, "schema-version"))) {
|
|
3117
|
+
write_schema_version2(projectRoot, CURRENT_SCHEMA_VERSION3);
|
|
3118
|
+
}
|
|
3119
|
+
writeIfMissing2(path12.join(projectRoot, "templates/task.md"), TASK_TEMPLATE2);
|
|
3120
|
+
writeIfMissing2(path12.join(projectRoot, "templates/idea.md"), IDEA_TEMPLATE2);
|
|
3121
|
+
return projectRoot;
|
|
3122
|
+
}
|
|
3123
|
+
function registerProjectCommand(program) {
|
|
3124
|
+
const project = program.command("project").description("Manage COOP projects");
|
|
3125
|
+
project.command("list").description("List COOP projects in the current workspace").action(() => {
|
|
3126
|
+
const root = resolveRepoRoot();
|
|
3127
|
+
const projects = listProjects(root);
|
|
3128
|
+
const workspace = readWorkspaceConfig(root);
|
|
3129
|
+
if (projects.length === 0) {
|
|
3130
|
+
console.log("No COOP projects found.");
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
for (const item of projects) {
|
|
3134
|
+
const current = workspace.current_project === item.id ? " *" : "";
|
|
3135
|
+
console.log(`${item.id}${current} ${item.name}`);
|
|
3136
|
+
}
|
|
3137
|
+
});
|
|
3138
|
+
project.command("show").description("Show the active or selected COOP project").argument("[id]", "Optional project id").action((id) => {
|
|
3139
|
+
const root = resolveRepoRoot();
|
|
3140
|
+
const projects = listProjects(root);
|
|
3141
|
+
const workspace = readWorkspaceConfig(root);
|
|
3142
|
+
const active = (id ? projects.find((item) => item.id === id) : void 0) ?? projects.find((item) => item.id === workspace.current_project) ?? projects[0];
|
|
3143
|
+
if (!active) {
|
|
3144
|
+
throw new Error("No COOP projects found.");
|
|
3145
|
+
}
|
|
3146
|
+
console.log(`id=${active.id}`);
|
|
3147
|
+
console.log(`name=${active.name}`);
|
|
3148
|
+
console.log(`path=${path12.relative(root, active.root)}`);
|
|
3149
|
+
console.log(`layout=${active.layout}`);
|
|
3150
|
+
});
|
|
3151
|
+
project.command("use").description("Set the active COOP project").argument("<id>", "Project id").action((id) => {
|
|
3152
|
+
const root = resolveRepoRoot();
|
|
3153
|
+
const projects = listProjects(root);
|
|
3154
|
+
const match = projects.find((item) => item.id === id);
|
|
3155
|
+
if (!match) {
|
|
3156
|
+
throw new Error(`Project '${id}' not found.`);
|
|
3157
|
+
}
|
|
3158
|
+
const workspace = readWorkspaceConfig(root);
|
|
3159
|
+
writeWorkspaceConfig(root, { ...workspace, version: 2, current_project: match.id });
|
|
3160
|
+
console.log(`current_project=${match.id}`);
|
|
3161
|
+
});
|
|
3162
|
+
project.command("create").description("Create a new COOP project in the current workspace").argument("<id>", "Project id").option("--name <name>", "Project display name").action((id, options) => {
|
|
3163
|
+
const root = resolveRepoRoot();
|
|
3164
|
+
const projectId = normalizeProjectId3(id);
|
|
3165
|
+
if (!projectId) {
|
|
3166
|
+
throw new Error("Invalid project id. Use letters, numbers, and hyphens.");
|
|
3167
|
+
}
|
|
3168
|
+
const existing = listProjects(root).find((item) => item.id === projectId);
|
|
3169
|
+
if (existing) {
|
|
3170
|
+
throw new Error(`Project '${projectId}' already exists.`);
|
|
3171
|
+
}
|
|
3172
|
+
const projectName = options.name?.trim() || repoDisplayName(root);
|
|
3173
|
+
const projectRoot = createProject(root, projectId, projectName);
|
|
3174
|
+
const workspace = readWorkspaceConfig(root);
|
|
3175
|
+
writeWorkspaceConfig(root, {
|
|
3176
|
+
...workspace,
|
|
3177
|
+
version: 2,
|
|
3178
|
+
current_project: workspace.current_project || projectId
|
|
3179
|
+
});
|
|
3180
|
+
console.log(`Created project '${projectId}' at ${path12.relative(root, projectRoot)}`);
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
// src/commands/run.ts
|
|
3185
|
+
import fs10 from "fs";
|
|
3186
|
+
import path13 from "path";
|
|
2650
3187
|
import { load_graph as load_graph5, parseTaskFile as parseTaskFile7 } from "@kitsy/coop-core";
|
|
2651
3188
|
import {
|
|
2652
3189
|
build_contract,
|
|
@@ -2656,8 +3193,8 @@ import {
|
|
|
2656
3193
|
} from "@kitsy/coop-ai";
|
|
2657
3194
|
function loadTask(root, idOrAlias) {
|
|
2658
3195
|
const target = resolveReference(root, idOrAlias, "task");
|
|
2659
|
-
const taskFile =
|
|
2660
|
-
if (!
|
|
3196
|
+
const taskFile = path13.join(root, ...target.file.split("/"));
|
|
3197
|
+
if (!fs10.existsSync(taskFile)) {
|
|
2661
3198
|
throw new Error(`Task file not found: ${target.file}`);
|
|
2662
3199
|
}
|
|
2663
3200
|
return parseTaskFile7(taskFile).task;
|
|
@@ -2696,22 +3233,22 @@ function registerRunCommand(program) {
|
|
|
2696
3233
|
on_progress: (message) => console.log(`[COOP] ${message}`)
|
|
2697
3234
|
});
|
|
2698
3235
|
if (result.status === "failed") {
|
|
2699
|
-
throw new Error(`Run failed: ${result.run.id}. Log: ${
|
|
3236
|
+
throw new Error(`Run failed: ${result.run.id}. Log: ${path13.relative(root, result.log_path)}`);
|
|
2700
3237
|
}
|
|
2701
3238
|
if (result.status === "paused") {
|
|
2702
3239
|
console.log(`[COOP] run paused: ${result.run.id}`);
|
|
2703
|
-
console.log(`[COOP] log: ${
|
|
3240
|
+
console.log(`[COOP] log: ${path13.relative(root, result.log_path)}`);
|
|
2704
3241
|
return;
|
|
2705
3242
|
}
|
|
2706
3243
|
console.log(`[COOP] run completed: ${result.run.id}`);
|
|
2707
|
-
console.log(`[COOP] log: ${
|
|
3244
|
+
console.log(`[COOP] log: ${path13.relative(root, result.log_path)}`);
|
|
2708
3245
|
});
|
|
2709
3246
|
}
|
|
2710
3247
|
|
|
2711
3248
|
// src/server/api.ts
|
|
2712
|
-
import
|
|
3249
|
+
import fs11 from "fs";
|
|
2713
3250
|
import http from "http";
|
|
2714
|
-
import
|
|
3251
|
+
import path14 from "path";
|
|
2715
3252
|
import {
|
|
2716
3253
|
analyze_feasibility as analyze_feasibility2,
|
|
2717
3254
|
load_graph as load_graph6,
|
|
@@ -2757,12 +3294,12 @@ function taskSummary(graph, task, external = []) {
|
|
|
2757
3294
|
};
|
|
2758
3295
|
}
|
|
2759
3296
|
function taskFileById(root, id) {
|
|
2760
|
-
const tasksDir =
|
|
2761
|
-
if (!
|
|
2762
|
-
const entries =
|
|
3297
|
+
const tasksDir = path14.join(resolveProject(root).root, "tasks");
|
|
3298
|
+
if (!fs11.existsSync(tasksDir)) return null;
|
|
3299
|
+
const entries = fs11.readdirSync(tasksDir, { withFileTypes: true });
|
|
2763
3300
|
const target = `${id}.md`.toLowerCase();
|
|
2764
3301
|
const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
|
|
2765
|
-
return match ?
|
|
3302
|
+
return match ? path14.join(tasksDir, match.name) : null;
|
|
2766
3303
|
}
|
|
2767
3304
|
function loadRemoteConfig(root) {
|
|
2768
3305
|
const raw = readCoopConfig(root).raw;
|
|
@@ -2826,7 +3363,8 @@ function createApiServer(root, options = {}) {
|
|
|
2826
3363
|
json(res, 200, workspaceMeta(repoRoot));
|
|
2827
3364
|
return;
|
|
2828
3365
|
}
|
|
2829
|
-
const
|
|
3366
|
+
const project = resolveProject(repoRoot);
|
|
3367
|
+
const coopPath = project.root;
|
|
2830
3368
|
const graph = load_graph6(coopPath);
|
|
2831
3369
|
const resolutions = await externalResolutions(repoRoot, graph, options);
|
|
2832
3370
|
if (pathname === "/api/tasks") {
|
|
@@ -2851,7 +3389,7 @@ function createApiServer(root, options = {}) {
|
|
|
2851
3389
|
created: task.created,
|
|
2852
3390
|
updated: task.updated,
|
|
2853
3391
|
body: parsed.body,
|
|
2854
|
-
file_path:
|
|
3392
|
+
file_path: path14.relative(repoRoot, filePath).replace(/\\/g, "/")
|
|
2855
3393
|
}
|
|
2856
3394
|
});
|
|
2857
3395
|
return;
|
|
@@ -2939,8 +3477,8 @@ function registerServeCommand(program) {
|
|
|
2939
3477
|
}
|
|
2940
3478
|
|
|
2941
3479
|
// src/commands/show.ts
|
|
2942
|
-
import
|
|
2943
|
-
import
|
|
3480
|
+
import fs12 from "fs";
|
|
3481
|
+
import path15 from "path";
|
|
2944
3482
|
import { parseIdeaFile as parseIdeaFile4, parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
|
|
2945
3483
|
function stringify(value) {
|
|
2946
3484
|
if (value === null || value === void 0) return "-";
|
|
@@ -2949,13 +3487,13 @@ function stringify(value) {
|
|
|
2949
3487
|
return String(value);
|
|
2950
3488
|
}
|
|
2951
3489
|
function loadComputedFromIndex(root, taskId) {
|
|
2952
|
-
const indexPath =
|
|
2953
|
-
if (!
|
|
3490
|
+
const indexPath = path15.join(ensureCoopInitialized(root), ".index", "tasks.json");
|
|
3491
|
+
if (!fs12.existsSync(indexPath)) {
|
|
2954
3492
|
return null;
|
|
2955
3493
|
}
|
|
2956
3494
|
let parsed;
|
|
2957
3495
|
try {
|
|
2958
|
-
parsed = JSON.parse(
|
|
3496
|
+
parsed = JSON.parse(fs12.readFileSync(indexPath, "utf8"));
|
|
2959
3497
|
} catch {
|
|
2960
3498
|
return null;
|
|
2961
3499
|
}
|
|
@@ -2994,7 +3532,7 @@ function showTask(taskId) {
|
|
|
2994
3532
|
const root = resolveRepoRoot();
|
|
2995
3533
|
const coop = ensureCoopInitialized(root);
|
|
2996
3534
|
const target = resolveReference(root, taskId, "task");
|
|
2997
|
-
const taskFile =
|
|
3535
|
+
const taskFile = path15.join(root, ...target.file.split("/"));
|
|
2998
3536
|
const parsed = parseTaskFile9(taskFile);
|
|
2999
3537
|
const task = parsed.task;
|
|
3000
3538
|
const body = parsed.body.trim();
|
|
@@ -3013,7 +3551,7 @@ function showTask(taskId) {
|
|
|
3013
3551
|
`Tags: ${stringify(task.tags)}`,
|
|
3014
3552
|
`Created: ${task.created}`,
|
|
3015
3553
|
`Updated: ${task.updated}`,
|
|
3016
|
-
`File: ${
|
|
3554
|
+
`File: ${path15.relative(root, taskFile)}`,
|
|
3017
3555
|
"",
|
|
3018
3556
|
"Body:",
|
|
3019
3557
|
body || "-",
|
|
@@ -3021,7 +3559,7 @@ function showTask(taskId) {
|
|
|
3021
3559
|
"Computed:"
|
|
3022
3560
|
];
|
|
3023
3561
|
if (!computed) {
|
|
3024
|
-
lines.push(`index not built (${
|
|
3562
|
+
lines.push(`index not built (${path15.relative(root, path15.join(coop, ".index", "tasks.json"))} missing)`);
|
|
3025
3563
|
} else {
|
|
3026
3564
|
for (const [key, value] of Object.entries(computed)) {
|
|
3027
3565
|
lines.push(`- ${key}: ${stringify(value)}`);
|
|
@@ -3033,7 +3571,7 @@ function showIdea(ideaId) {
|
|
|
3033
3571
|
const root = resolveRepoRoot();
|
|
3034
3572
|
ensureCoopInitialized(root);
|
|
3035
3573
|
const target = resolveReference(root, ideaId, "idea");
|
|
3036
|
-
const ideaFile =
|
|
3574
|
+
const ideaFile = path15.join(root, ...target.file.split("/"));
|
|
3037
3575
|
const parsed = parseIdeaFile4(ideaFile);
|
|
3038
3576
|
const idea = parsed.idea;
|
|
3039
3577
|
const body = parsed.body.trim();
|
|
@@ -3047,7 +3585,7 @@ function showIdea(ideaId) {
|
|
|
3047
3585
|
`Tags: ${stringify(idea.tags)}`,
|
|
3048
3586
|
`Linked Tasks: ${stringify(idea.linked_tasks)}`,
|
|
3049
3587
|
`Created: ${idea.created}`,
|
|
3050
|
-
`File: ${
|
|
3588
|
+
`File: ${path15.relative(root, ideaFile)}`,
|
|
3051
3589
|
"",
|
|
3052
3590
|
"Body:",
|
|
3053
3591
|
body || "-"
|
|
@@ -3154,7 +3692,7 @@ function registerStatusCommand(program) {
|
|
|
3154
3692
|
}
|
|
3155
3693
|
|
|
3156
3694
|
// src/commands/transition.ts
|
|
3157
|
-
import
|
|
3695
|
+
import path16 from "path";
|
|
3158
3696
|
import {
|
|
3159
3697
|
TaskStatus as TaskStatus3,
|
|
3160
3698
|
check_permission as check_permission3,
|
|
@@ -3183,8 +3721,8 @@ import { Octokit } from "octokit";
|
|
|
3183
3721
|
function isObject(value) {
|
|
3184
3722
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3185
3723
|
}
|
|
3186
|
-
function sanitizeBranchPart(
|
|
3187
|
-
const normalized =
|
|
3724
|
+
function sanitizeBranchPart(input3, fallback) {
|
|
3725
|
+
const normalized = input3.trim().toLowerCase().replace(/[^a-z0-9/_-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
3188
3726
|
return normalized || fallback;
|
|
3189
3727
|
}
|
|
3190
3728
|
function readGitRemote(root) {
|
|
@@ -3317,15 +3855,15 @@ function toGitHubClient(config) {
|
|
|
3317
3855
|
baseUrl: config.apiBaseUrl
|
|
3318
3856
|
});
|
|
3319
3857
|
return {
|
|
3320
|
-
async createPullRequest(
|
|
3858
|
+
async createPullRequest(input3) {
|
|
3321
3859
|
const response = await octokit.rest.pulls.create({
|
|
3322
|
-
owner:
|
|
3323
|
-
repo:
|
|
3324
|
-
title:
|
|
3325
|
-
body:
|
|
3326
|
-
head:
|
|
3327
|
-
base:
|
|
3328
|
-
draft:
|
|
3860
|
+
owner: input3.owner,
|
|
3861
|
+
repo: input3.repo,
|
|
3862
|
+
title: input3.title,
|
|
3863
|
+
body: input3.body,
|
|
3864
|
+
head: input3.head,
|
|
3865
|
+
base: input3.base,
|
|
3866
|
+
draft: input3.draft
|
|
3329
3867
|
});
|
|
3330
3868
|
return {
|
|
3331
3869
|
number: response.data.number,
|
|
@@ -3335,14 +3873,14 @@ function toGitHubClient(config) {
|
|
|
3335
3873
|
draft: response.data.draft
|
|
3336
3874
|
};
|
|
3337
3875
|
},
|
|
3338
|
-
async updatePullRequest(
|
|
3876
|
+
async updatePullRequest(input3) {
|
|
3339
3877
|
const response = await octokit.rest.pulls.update({
|
|
3340
|
-
owner:
|
|
3341
|
-
repo:
|
|
3342
|
-
pull_number:
|
|
3343
|
-
title:
|
|
3344
|
-
body:
|
|
3345
|
-
base:
|
|
3878
|
+
owner: input3.owner,
|
|
3879
|
+
repo: input3.repo,
|
|
3880
|
+
pull_number: input3.pull_number,
|
|
3881
|
+
title: input3.title,
|
|
3882
|
+
body: input3.body,
|
|
3883
|
+
base: input3.base
|
|
3346
3884
|
});
|
|
3347
3885
|
return {
|
|
3348
3886
|
number: response.data.number,
|
|
@@ -3352,11 +3890,11 @@ function toGitHubClient(config) {
|
|
|
3352
3890
|
draft: response.data.draft
|
|
3353
3891
|
};
|
|
3354
3892
|
},
|
|
3355
|
-
async getPullRequest(
|
|
3893
|
+
async getPullRequest(input3) {
|
|
3356
3894
|
const response = await octokit.rest.pulls.get({
|
|
3357
|
-
owner:
|
|
3358
|
-
repo:
|
|
3359
|
-
pull_number:
|
|
3895
|
+
owner: input3.owner,
|
|
3896
|
+
repo: input3.repo,
|
|
3897
|
+
pull_number: input3.pull_number
|
|
3360
3898
|
});
|
|
3361
3899
|
return {
|
|
3362
3900
|
number: response.data.number,
|
|
@@ -3366,12 +3904,12 @@ function toGitHubClient(config) {
|
|
|
3366
3904
|
draft: response.data.draft
|
|
3367
3905
|
};
|
|
3368
3906
|
},
|
|
3369
|
-
async mergePullRequest(
|
|
3907
|
+
async mergePullRequest(input3) {
|
|
3370
3908
|
const response = await octokit.rest.pulls.merge({
|
|
3371
|
-
owner:
|
|
3372
|
-
repo:
|
|
3373
|
-
pull_number:
|
|
3374
|
-
merge_method:
|
|
3909
|
+
owner: input3.owner,
|
|
3910
|
+
repo: input3.repo,
|
|
3911
|
+
pull_number: input3.pull_number,
|
|
3912
|
+
merge_method: input3.merge_method
|
|
3375
3913
|
});
|
|
3376
3914
|
return {
|
|
3377
3915
|
merged: response.data.merged,
|
|
@@ -3387,7 +3925,9 @@ function readGitHubConfig(root) {
|
|
|
3387
3925
|
const owner = typeof githubRaw.owner === "string" && githubRaw.owner.trim() ? githubRaw.owner.trim() : inferred?.owner;
|
|
3388
3926
|
const repo = typeof githubRaw.repo === "string" && githubRaw.repo.trim() ? githubRaw.repo.trim() : inferred?.repo;
|
|
3389
3927
|
if (!owner || !repo) {
|
|
3390
|
-
throw new Error(
|
|
3928
|
+
throw new Error(
|
|
3929
|
+
"GitHub integration requires github.owner and github.repo in the active COOP project config or a GitHub origin remote."
|
|
3930
|
+
);
|
|
3391
3931
|
}
|
|
3392
3932
|
const tokenEnv = typeof githubRaw.token_env === "string" && githubRaw.token_env.trim() ? githubRaw.token_env.trim() : "GITHUB_TOKEN";
|
|
3393
3933
|
const token = process.env[tokenEnv];
|
|
@@ -3748,7 +4288,7 @@ function registerTransitionCommand(program) {
|
|
|
3748
4288
|
}
|
|
3749
4289
|
console.warn(`[COOP][auth] override: user '${user}' forced transition_task on '${reference.id}'.`);
|
|
3750
4290
|
}
|
|
3751
|
-
const filePath =
|
|
4291
|
+
const filePath = path16.join(root, ...reference.file.split("/"));
|
|
3752
4292
|
const parsed = parseTaskFile11(filePath);
|
|
3753
4293
|
const result = transition2(parsed.task, target, {
|
|
3754
4294
|
actor: options.actor ?? user,
|
|
@@ -3794,19 +4334,19 @@ ${errors}`);
|
|
|
3794
4334
|
}
|
|
3795
4335
|
|
|
3796
4336
|
// src/commands/ui.ts
|
|
3797
|
-
import
|
|
3798
|
-
import
|
|
4337
|
+
import fs13 from "fs";
|
|
4338
|
+
import path17 from "path";
|
|
3799
4339
|
import { createRequire } from "module";
|
|
3800
4340
|
import { fileURLToPath } from "url";
|
|
3801
4341
|
import { spawn } from "child_process";
|
|
3802
|
-
import { IndexManager as
|
|
4342
|
+
import { IndexManager as IndexManager3 } from "@kitsy/coop-core";
|
|
3803
4343
|
function findPackageRoot(entryPath) {
|
|
3804
|
-
let current =
|
|
4344
|
+
let current = path17.dirname(entryPath);
|
|
3805
4345
|
while (true) {
|
|
3806
|
-
if (
|
|
4346
|
+
if (fs13.existsSync(path17.join(current, "package.json"))) {
|
|
3807
4347
|
return current;
|
|
3808
4348
|
}
|
|
3809
|
-
const parent =
|
|
4349
|
+
const parent = path17.dirname(current);
|
|
3810
4350
|
if (parent === current) {
|
|
3811
4351
|
throw new Error(`Unable to locate package root for ${entryPath}.`);
|
|
3812
4352
|
}
|
|
@@ -3837,7 +4377,7 @@ function openBrowser(url) {
|
|
|
3837
4377
|
}
|
|
3838
4378
|
function ensureIndex(repoRoot) {
|
|
3839
4379
|
const directory = ensureCoopInitialized(repoRoot);
|
|
3840
|
-
const manager = new
|
|
4380
|
+
const manager = new IndexManager3(directory);
|
|
3841
4381
|
if (manager.is_stale() || !manager.load_indexed_graph()) {
|
|
3842
4382
|
manager.build_full_index();
|
|
3843
4383
|
}
|
|
@@ -3852,11 +4392,12 @@ function attachSignalForwarding(child) {
|
|
|
3852
4392
|
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
3853
4393
|
}
|
|
3854
4394
|
async function startUiServer(repoRoot, host, port, shouldOpen) {
|
|
4395
|
+
const project = resolveProject(repoRoot);
|
|
3855
4396
|
ensureIndex(repoRoot);
|
|
3856
4397
|
const uiRoot = resolveUiPackageRoot();
|
|
3857
|
-
const requireFromUi = createRequire(
|
|
4398
|
+
const requireFromUi = createRequire(path17.join(uiRoot, "package.json"));
|
|
3858
4399
|
const vitePackageJson = requireFromUi.resolve("vite/package.json");
|
|
3859
|
-
const viteBin =
|
|
4400
|
+
const viteBin = path17.join(path17.dirname(vitePackageJson), "bin", "vite.js");
|
|
3860
4401
|
const url = `http://${host}:${port}`;
|
|
3861
4402
|
console.log(`COOP UI: ${url}`);
|
|
3862
4403
|
const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
|
|
@@ -3865,6 +4406,7 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
|
|
|
3865
4406
|
env: {
|
|
3866
4407
|
...process.env,
|
|
3867
4408
|
COOP_REPO_ROOT: repoRoot,
|
|
4409
|
+
COOP_PROJECT_ID: project.id,
|
|
3868
4410
|
COOP_UI_HOST: host,
|
|
3869
4411
|
COOP_UI_PORT: String(port)
|
|
3870
4412
|
}
|
|
@@ -4212,9 +4754,9 @@ function registerWebhookCommand(program) {
|
|
|
4212
4754
|
}
|
|
4213
4755
|
|
|
4214
4756
|
// src/merge-driver/merge-driver.ts
|
|
4215
|
-
import
|
|
4757
|
+
import fs14 from "fs";
|
|
4216
4758
|
import os2 from "os";
|
|
4217
|
-
import
|
|
4759
|
+
import path18 from "path";
|
|
4218
4760
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
4219
4761
|
import { stringifyFrontmatter as stringifyFrontmatter3, parseFrontmatterContent, parseYamlContent, stringifyYamlContent } from "@kitsy/coop-core";
|
|
4220
4762
|
var STATUS_RANK = {
|
|
@@ -4273,16 +4815,16 @@ function chooseFieldValue(key, baseValue, oursValue, theirsValue, oursUpdated, t
|
|
|
4273
4815
|
}
|
|
4274
4816
|
function mergeTaskFrontmatter(base, ours, theirs) {
|
|
4275
4817
|
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)]);
|
|
4276
|
-
const
|
|
4818
|
+
const output3 = {};
|
|
4277
4819
|
const oursUpdated = asTimestamp(ours.updated);
|
|
4278
4820
|
const theirsUpdated = asTimestamp(theirs.updated);
|
|
4279
4821
|
for (const key of keys) {
|
|
4280
4822
|
const merged = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
|
|
4281
4823
|
if (merged !== void 0) {
|
|
4282
|
-
|
|
4824
|
+
output3[key] = merged;
|
|
4283
4825
|
}
|
|
4284
4826
|
}
|
|
4285
|
-
return
|
|
4827
|
+
return output3;
|
|
4286
4828
|
}
|
|
4287
4829
|
function mergeTextWithGit(ancestor, ours, theirs) {
|
|
4288
4830
|
const result = spawnSync5("git", ["merge-file", "-p", ours, ancestor, theirs], {
|
|
@@ -4297,33 +4839,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
|
|
|
4297
4839
|
return { ok: false, output: stdout };
|
|
4298
4840
|
}
|
|
4299
4841
|
function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
|
|
4300
|
-
const ancestorRaw =
|
|
4301
|
-
const oursRaw =
|
|
4302
|
-
const theirsRaw =
|
|
4842
|
+
const ancestorRaw = fs14.readFileSync(ancestorPath, "utf8");
|
|
4843
|
+
const oursRaw = fs14.readFileSync(oursPath, "utf8");
|
|
4844
|
+
const theirsRaw = fs14.readFileSync(theirsPath, "utf8");
|
|
4303
4845
|
const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
|
|
4304
4846
|
const ours = parseTaskDocument(oursRaw, oursPath);
|
|
4305
4847
|
const theirs = parseTaskDocument(theirsRaw, theirsPath);
|
|
4306
4848
|
const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
|
|
4307
|
-
const tempDir =
|
|
4849
|
+
const tempDir = fs14.mkdtempSync(path18.join(os2.tmpdir(), "coop-merge-body-"));
|
|
4308
4850
|
try {
|
|
4309
|
-
const ancestorBody =
|
|
4310
|
-
const oursBody =
|
|
4311
|
-
const theirsBody =
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4851
|
+
const ancestorBody = path18.join(tempDir, "ancestor.md");
|
|
4852
|
+
const oursBody = path18.join(tempDir, "ours.md");
|
|
4853
|
+
const theirsBody = path18.join(tempDir, "theirs.md");
|
|
4854
|
+
fs14.writeFileSync(ancestorBody, ancestor.body, "utf8");
|
|
4855
|
+
fs14.writeFileSync(oursBody, ours.body, "utf8");
|
|
4856
|
+
fs14.writeFileSync(theirsBody, theirs.body, "utf8");
|
|
4315
4857
|
const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
|
|
4316
|
-
const
|
|
4317
|
-
|
|
4858
|
+
const output3 = stringifyFrontmatter3(mergedFrontmatter, mergedBody.output);
|
|
4859
|
+
fs14.writeFileSync(oursPath, output3, "utf8");
|
|
4318
4860
|
return mergedBody.ok ? 0 : 1;
|
|
4319
4861
|
} finally {
|
|
4320
|
-
|
|
4862
|
+
fs14.rmSync(tempDir, { recursive: true, force: true });
|
|
4321
4863
|
}
|
|
4322
4864
|
}
|
|
4323
4865
|
function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
4324
|
-
const ancestor = parseYamlContent(
|
|
4325
|
-
const ours = parseYamlContent(
|
|
4326
|
-
const theirs = parseYamlContent(
|
|
4866
|
+
const ancestor = parseYamlContent(fs14.readFileSync(ancestorPath, "utf8"), ancestorPath);
|
|
4867
|
+
const ours = parseYamlContent(fs14.readFileSync(oursPath, "utf8"), oursPath);
|
|
4868
|
+
const theirs = parseYamlContent(fs14.readFileSync(theirsPath, "utf8"), theirsPath);
|
|
4327
4869
|
const oursUpdated = asTimestamp(ours.updated);
|
|
4328
4870
|
const theirsUpdated = asTimestamp(theirs.updated);
|
|
4329
4871
|
const base = ancestor;
|
|
@@ -4333,7 +4875,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
|
4333
4875
|
const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
|
|
4334
4876
|
if (value !== void 0) merged[key] = value;
|
|
4335
4877
|
}
|
|
4336
|
-
|
|
4878
|
+
fs14.writeFileSync(oursPath, stringifyYamlContent(merged), "utf8");
|
|
4337
4879
|
return 0;
|
|
4338
4880
|
}
|
|
4339
4881
|
function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
@@ -4346,9 +4888,9 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
|
4346
4888
|
// src/index.ts
|
|
4347
4889
|
function readVersion() {
|
|
4348
4890
|
const currentFile = fileURLToPath2(import.meta.url);
|
|
4349
|
-
const packageJsonPath =
|
|
4891
|
+
const packageJsonPath = path19.resolve(path19.dirname(currentFile), "..", "package.json");
|
|
4350
4892
|
try {
|
|
4351
|
-
const parsed = JSON.parse(
|
|
4893
|
+
const parsed = JSON.parse(fs15.readFileSync(packageJsonPath, "utf8"));
|
|
4352
4894
|
return parsed.version ?? "0.0.0";
|
|
4353
4895
|
} catch {
|
|
4354
4896
|
return "0.0.0";
|
|
@@ -4365,6 +4907,7 @@ function createProgram() {
|
|
|
4365
4907
|
program.version(readVersion());
|
|
4366
4908
|
program.description("COOP CLI");
|
|
4367
4909
|
program.option("--verbose", "Print stack traces for command errors");
|
|
4910
|
+
program.option("-p, --project <id>", "Select the active COOP project");
|
|
4368
4911
|
registerInitCommand(program);
|
|
4369
4912
|
registerCreateCommand(program);
|
|
4370
4913
|
registerAssignCommand(program);
|
|
@@ -4378,6 +4921,7 @@ function createProgram() {
|
|
|
4378
4921
|
registerLogCommand(program);
|
|
4379
4922
|
registerMigrateCommand(program);
|
|
4380
4923
|
registerPlanCommand(program);
|
|
4924
|
+
registerProjectCommand(program);
|
|
4381
4925
|
registerRunCommand(program);
|
|
4382
4926
|
registerServeCommand(program);
|
|
4383
4927
|
registerStatusCommand(program);
|
|
@@ -4441,7 +4985,7 @@ async function runCli(argv = process.argv) {
|
|
|
4441
4985
|
function isMainModule() {
|
|
4442
4986
|
const entry = process.argv[1];
|
|
4443
4987
|
if (!entry) return false;
|
|
4444
|
-
return
|
|
4988
|
+
return path19.resolve(entry) === fileURLToPath2(import.meta.url);
|
|
4445
4989
|
}
|
|
4446
4990
|
if (isMainModule()) {
|
|
4447
4991
|
await runCli(process.argv);
|