@kitsy/coop 1.0.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 +28 -6
- package/bin/coop.js +4 -0
- package/bin/coop.mjs +12 -0
- package/dist/index.js +746 -187
- package/package.json +6 -5
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
|
|
6
|
-
import { fileURLToPath as fileURLToPath2
|
|
4
|
+
import fs15 from "fs";
|
|
5
|
+
import path19 from "path";
|
|
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,12 +17,29 @@ 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";
|
|
32
|
+
function resolveCoopHome() {
|
|
33
|
+
const configured = process.env.COOP_HOME?.trim();
|
|
34
|
+
if (configured) {
|
|
35
|
+
return path.resolve(configured);
|
|
36
|
+
}
|
|
37
|
+
return path.join(os.homedir(), ".coop");
|
|
38
|
+
}
|
|
22
39
|
function resolveRepoRoot(start = process.cwd()) {
|
|
23
40
|
let current = path.resolve(start);
|
|
24
41
|
while (true) {
|
|
25
|
-
if (fs.existsSync(path.join(current, ".git"))) {
|
|
42
|
+
if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, ".coop"))) {
|
|
26
43
|
return current;
|
|
27
44
|
}
|
|
28
45
|
const parent = path.dirname(current);
|
|
@@ -32,21 +49,54 @@ function resolveRepoRoot(start = process.cwd()) {
|
|
|
32
49
|
current = parent;
|
|
33
50
|
}
|
|
34
51
|
}
|
|
35
|
-
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) {
|
|
36
66
|
return path.join(root, ".coop");
|
|
37
67
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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'.`);
|
|
42
79
|
}
|
|
43
|
-
return
|
|
80
|
+
return project.root;
|
|
44
81
|
}
|
|
45
|
-
function coopConfigPath(root) {
|
|
46
|
-
return
|
|
82
|
+
function coopConfigPath(root, projectId = resolveRequestedProject()) {
|
|
83
|
+
return coop_project_config_path(ensureCoopInitialized(root, projectId));
|
|
47
84
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
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);
|
|
50
100
|
const repoName = path.basename(path.resolve(root));
|
|
51
101
|
if (!fs.existsSync(configPath)) {
|
|
52
102
|
return {
|
|
@@ -62,10 +112,10 @@ function readCoopConfig(root) {
|
|
|
62
112
|
filePath: configPath
|
|
63
113
|
};
|
|
64
114
|
}
|
|
65
|
-
const config =
|
|
115
|
+
const config = read_project_config(project.root);
|
|
66
116
|
const projectRaw = typeof config.project === "object" && config.project !== null ? config.project : {};
|
|
67
117
|
const projectName = typeof projectRaw.name === "string" && projectRaw.name.trim().length > 0 ? projectRaw.name.trim() : repoName;
|
|
68
|
-
const
|
|
118
|
+
const resolvedProjectId = typeof projectRaw.id === "string" && projectRaw.id.trim().length > 0 ? projectRaw.id.trim() : repoName;
|
|
69
119
|
const projectAliases = Array.isArray(projectRaw.aliases) ? projectRaw.aliases.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
|
|
70
120
|
const idPrefixesRaw = config.id_prefixes;
|
|
71
121
|
const idPrefixes = typeof idPrefixesRaw === "object" && idPrefixesRaw !== null ? idPrefixesRaw : {};
|
|
@@ -86,7 +136,7 @@ function readCoopConfig(root) {
|
|
|
86
136
|
idNamingTemplate,
|
|
87
137
|
idSeqPadding,
|
|
88
138
|
projectName: projectName || "COOP Workspace",
|
|
89
|
-
projectId:
|
|
139
|
+
projectId: resolvedProjectId || "workspace",
|
|
90
140
|
projectAliases,
|
|
91
141
|
raw: config,
|
|
92
142
|
filePath: configPath
|
|
@@ -151,15 +201,15 @@ function findTaskFileById(root, id) {
|
|
|
151
201
|
const target = `${id}.md`.toLowerCase();
|
|
152
202
|
const match = listTaskFiles(root).find((filePath) => path.basename(filePath).toLowerCase() === target);
|
|
153
203
|
if (!match) {
|
|
154
|
-
throw new Error(`Task '${id}' not found in
|
|
204
|
+
throw new Error(`Task '${id}' not found in the active COOP project.`);
|
|
155
205
|
}
|
|
156
206
|
return match;
|
|
157
207
|
}
|
|
158
208
|
function todayIsoDate() {
|
|
159
209
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
160
210
|
}
|
|
161
|
-
function normalizeIdPart(
|
|
162
|
-
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, "");
|
|
163
213
|
if (!normalized) return fallback;
|
|
164
214
|
return normalized.slice(0, maxLength);
|
|
165
215
|
}
|
|
@@ -195,8 +245,8 @@ function shortDateToken(now = /* @__PURE__ */ new Date()) {
|
|
|
195
245
|
function randomToken() {
|
|
196
246
|
return crypto.randomBytes(4).toString("hex").toUpperCase();
|
|
197
247
|
}
|
|
198
|
-
function sanitizeTemplateValue(
|
|
199
|
-
const normalized =
|
|
248
|
+
function sanitizeTemplateValue(input3, fallback = "X") {
|
|
249
|
+
const normalized = input3.toUpperCase().replace(/[^A-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
200
250
|
return normalized || fallback;
|
|
201
251
|
}
|
|
202
252
|
function sequenceForPattern(existingIds, prefix, suffix) {
|
|
@@ -304,27 +354,27 @@ function generateConfiguredId(root, existingIds, context) {
|
|
|
304
354
|
|
|
305
355
|
// src/utils/aliases.ts
|
|
306
356
|
var ALIAS_PATTERN = /^[A-Z0-9]+(?:[.-][A-Z0-9]+)*$/;
|
|
307
|
-
function toPosixPath(
|
|
308
|
-
return
|
|
357
|
+
function toPosixPath(input3) {
|
|
358
|
+
return input3.replace(/\\/g, "/");
|
|
309
359
|
}
|
|
310
360
|
function indexFilePath(root) {
|
|
311
361
|
const { indexDataFormat } = readCoopConfig(root);
|
|
312
362
|
const extension = indexDataFormat === "json" ? "json" : "yml";
|
|
313
|
-
return path2.join(root, ".
|
|
363
|
+
return path2.join(ensureCoopInitialized(root), ".index", `aliases.${extension}`);
|
|
314
364
|
}
|
|
315
|
-
function normalizeAliasValue(
|
|
316
|
-
return
|
|
365
|
+
function normalizeAliasValue(input3) {
|
|
366
|
+
return input3.trim().toUpperCase().replace(/_/g, ".").replace(/\.+/g, ".");
|
|
317
367
|
}
|
|
318
|
-
function normalizePatternValue(
|
|
319
|
-
return
|
|
368
|
+
function normalizePatternValue(input3) {
|
|
369
|
+
return input3.trim().toUpperCase().replace(/_/g, ".");
|
|
320
370
|
}
|
|
321
|
-
function normalizeAlias(
|
|
322
|
-
const normalized = normalizeAliasValue(
|
|
371
|
+
function normalizeAlias(input3) {
|
|
372
|
+
const normalized = normalizeAliasValue(input3);
|
|
323
373
|
if (!normalized) {
|
|
324
374
|
throw new Error("Alias cannot be empty.");
|
|
325
375
|
}
|
|
326
376
|
if (!ALIAS_PATTERN.test(normalized)) {
|
|
327
|
-
throw new Error(`Invalid alias '${
|
|
377
|
+
throw new Error(`Invalid alias '${input3}'. Use letters/numbers with '.' and '-'.`);
|
|
328
378
|
}
|
|
329
379
|
return normalized;
|
|
330
380
|
}
|
|
@@ -376,7 +426,7 @@ function readIndexFile(root, aliasIndexPath) {
|
|
|
376
426
|
const parsed = JSON.parse(fs2.readFileSync(aliasIndexPath, "utf8"));
|
|
377
427
|
return parsed;
|
|
378
428
|
}
|
|
379
|
-
return
|
|
429
|
+
return parseYamlFile(aliasIndexPath);
|
|
380
430
|
} catch {
|
|
381
431
|
return null;
|
|
382
432
|
}
|
|
@@ -387,14 +437,14 @@ function writeIndexFile(root, data) {
|
|
|
387
437
|
if (aliasIndexPath.endsWith(".json")) {
|
|
388
438
|
fs2.writeFileSync(aliasIndexPath, `${JSON.stringify(data, null, 2)}
|
|
389
439
|
`, "utf8");
|
|
390
|
-
const yamlPath = path2.join(root, ".
|
|
440
|
+
const yamlPath = path2.join(ensureCoopInitialized(root), ".index", "aliases.yml");
|
|
391
441
|
if (fs2.existsSync(yamlPath)) {
|
|
392
442
|
fs2.rmSync(yamlPath, { force: true });
|
|
393
443
|
}
|
|
394
444
|
return;
|
|
395
445
|
}
|
|
396
446
|
writeYamlFile2(aliasIndexPath, data);
|
|
397
|
-
const jsonPath = path2.join(root, ".
|
|
447
|
+
const jsonPath = path2.join(ensureCoopInitialized(root), ".index", "aliases.json");
|
|
398
448
|
if (fs2.existsSync(jsonPath)) {
|
|
399
449
|
fs2.rmSync(jsonPath, { force: true });
|
|
400
450
|
}
|
|
@@ -511,8 +561,8 @@ function updateTaskAliases(filePath, aliases) {
|
|
|
511
561
|
function updateIdeaAliases(filePath, aliases) {
|
|
512
562
|
const parsed = parseIdeaFile(filePath);
|
|
513
563
|
const nextRaw = { ...parsed.raw, aliases };
|
|
514
|
-
const
|
|
515
|
-
fs2.writeFileSync(filePath,
|
|
564
|
+
const output3 = stringifyFrontmatter(nextRaw, parsed.body);
|
|
565
|
+
fs2.writeFileSync(filePath, output3, "utf8");
|
|
516
566
|
}
|
|
517
567
|
function resolveFilePath(root, target) {
|
|
518
568
|
return path2.join(root, ...target.file.split("/"));
|
|
@@ -1054,14 +1104,14 @@ function updateIdeaLinkedTasks(filePath, idea, raw, body, linked) {
|
|
|
1054
1104
|
};
|
|
1055
1105
|
fs3.writeFileSync(filePath, stringifyFrontmatter2(nextRaw, body), "utf8");
|
|
1056
1106
|
}
|
|
1057
|
-
function makeTaskDraft(
|
|
1107
|
+
function makeTaskDraft(input3) {
|
|
1058
1108
|
return {
|
|
1059
|
-
title:
|
|
1060
|
-
type:
|
|
1061
|
-
status:
|
|
1062
|
-
track:
|
|
1063
|
-
priority:
|
|
1064
|
-
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
|
|
1065
1115
|
};
|
|
1066
1116
|
}
|
|
1067
1117
|
function registerCreateCommand(program) {
|
|
@@ -1412,10 +1462,10 @@ import {
|
|
|
1412
1462
|
function normalize(value) {
|
|
1413
1463
|
return value.trim().toLowerCase();
|
|
1414
1464
|
}
|
|
1415
|
-
function resolveDelivery(graph,
|
|
1416
|
-
const direct = graph.deliveries.get(
|
|
1465
|
+
function resolveDelivery(graph, input3) {
|
|
1466
|
+
const direct = graph.deliveries.get(input3);
|
|
1417
1467
|
if (direct) return direct;
|
|
1418
|
-
const target = normalize(
|
|
1468
|
+
const target = normalize(input3);
|
|
1419
1469
|
const byId = Array.from(graph.deliveries.values()).find((delivery) => normalize(delivery.id) === target);
|
|
1420
1470
|
if (byId) return byId;
|
|
1421
1471
|
const byName = Array.from(graph.deliveries.values()).filter((delivery) => normalize(delivery.name) === target);
|
|
@@ -1423,9 +1473,9 @@ function resolveDelivery(graph, input) {
|
|
|
1423
1473
|
return byName[0];
|
|
1424
1474
|
}
|
|
1425
1475
|
if (byName.length > 1) {
|
|
1426
|
-
throw new Error(`Multiple deliveries match '${
|
|
1476
|
+
throw new Error(`Multiple deliveries match '${input3}'. Use delivery id instead.`);
|
|
1427
1477
|
}
|
|
1428
|
-
throw new Error(`Delivery '${
|
|
1478
|
+
throw new Error(`Delivery '${input3}' not found.`);
|
|
1429
1479
|
}
|
|
1430
1480
|
|
|
1431
1481
|
// src/commands/graph.ts
|
|
@@ -1616,6 +1666,8 @@ function registerIndexCommand(program) {
|
|
|
1616
1666
|
import fs6 from "fs";
|
|
1617
1667
|
import path8 from "path";
|
|
1618
1668
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1669
|
+
import { createInterface } from "readline/promises";
|
|
1670
|
+
import { stdin as input, stdout as output } from "process";
|
|
1619
1671
|
import { CURRENT_SCHEMA_VERSION, write_schema_version } from "@kitsy/coop-core";
|
|
1620
1672
|
|
|
1621
1673
|
// src/hooks/pre-commit.ts
|
|
@@ -1643,7 +1695,41 @@ function toPosixPath2(filePath) {
|
|
|
1643
1695
|
}
|
|
1644
1696
|
function stagedTaskFiles(repoRoot) {
|
|
1645
1697
|
const { stdout } = runGit(repoRoot, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
|
|
1646
|
-
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));
|
|
1647
1733
|
}
|
|
1648
1734
|
function readGitBlob(repoRoot, spec) {
|
|
1649
1735
|
const result = runGit(repoRoot, ["show", spec], true);
|
|
@@ -1658,6 +1744,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
1658
1744
|
const staged = [];
|
|
1659
1745
|
for (const relativePath of relativePaths) {
|
|
1660
1746
|
const absolutePath = path6.join(repoRoot, ...relativePath.split("/"));
|
|
1747
|
+
const projectRoot = projectRootFromRelativePath(repoRoot, relativePath);
|
|
1661
1748
|
const stagedBlob = readGitBlob(repoRoot, `:${relativePath}`);
|
|
1662
1749
|
if (!stagedBlob) {
|
|
1663
1750
|
errors.push(`[COOP] Unable to read staged task '${relativePath}' from git index.`);
|
|
@@ -1678,6 +1765,7 @@ function parseStagedTasks(repoRoot, relativePaths) {
|
|
|
1678
1765
|
staged.push({
|
|
1679
1766
|
relativePath,
|
|
1680
1767
|
absolutePath,
|
|
1768
|
+
projectRoot,
|
|
1681
1769
|
task
|
|
1682
1770
|
});
|
|
1683
1771
|
}
|
|
@@ -1712,13 +1800,13 @@ function buildGraphForCycleCheck(tasks) {
|
|
|
1712
1800
|
deliveries: /* @__PURE__ */ new Map()
|
|
1713
1801
|
};
|
|
1714
1802
|
}
|
|
1715
|
-
function collectTasksForCycleCheck(
|
|
1803
|
+
function collectTasksForCycleCheck(projectRoot, stagedTasks) {
|
|
1716
1804
|
const stagedByPath = /* @__PURE__ */ new Map();
|
|
1717
1805
|
for (const staged of stagedTasks) {
|
|
1718
1806
|
stagedByPath.set(toPosixPath2(staged.absolutePath), staged.task);
|
|
1719
1807
|
}
|
|
1720
1808
|
const tasks = [];
|
|
1721
|
-
for (const filePath of
|
|
1809
|
+
for (const filePath of listTaskFilesForProject(projectRoot)) {
|
|
1722
1810
|
const normalized = toPosixPath2(path6.resolve(filePath));
|
|
1723
1811
|
const stagedTask = stagedByPath.get(normalized);
|
|
1724
1812
|
if (stagedTask) {
|
|
@@ -1737,16 +1825,25 @@ function runPreCommitChecks(repoRoot) {
|
|
|
1737
1825
|
const parsed = parseStagedTasks(repoRoot, relativeTaskFiles);
|
|
1738
1826
|
const errors = [...parsed.errors];
|
|
1739
1827
|
if (parsed.staged.length > 0 && errors.length === 0) {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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}`);
|
|
1746
1846
|
}
|
|
1747
|
-
} catch (error) {
|
|
1748
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1749
|
-
errors.push(`[COOP] Failed to run dependency cycle check: ${message}`);
|
|
1750
1847
|
}
|
|
1751
1848
|
}
|
|
1752
1849
|
return {
|
|
@@ -1806,28 +1903,37 @@ function installPreCommitHook(repoRoot) {
|
|
|
1806
1903
|
// src/hooks/post-merge-validate.ts
|
|
1807
1904
|
import fs5 from "fs";
|
|
1808
1905
|
import path7 from "path";
|
|
1906
|
+
import { list_projects } from "@kitsy/coop-core";
|
|
1809
1907
|
import { load_graph as load_graph3, validate_graph as validate_graph2 } from "@kitsy/coop-core";
|
|
1810
1908
|
var HOOK_BLOCK_START2 = "# COOP_POST_MERGE_START";
|
|
1811
1909
|
var HOOK_BLOCK_END2 = "# COOP_POST_MERGE_END";
|
|
1812
1910
|
function runPostMergeValidate(repoRoot) {
|
|
1813
|
-
const
|
|
1814
|
-
if (!fs5.existsSync(
|
|
1911
|
+
const workspaceDir = path7.join(repoRoot, ".coop");
|
|
1912
|
+
if (!fs5.existsSync(workspaceDir)) {
|
|
1815
1913
|
return {
|
|
1816
1914
|
ok: true,
|
|
1817
1915
|
warnings: ["[COOP] Skipped post-merge validation (.coop not found)."]
|
|
1818
1916
|
};
|
|
1819
1917
|
}
|
|
1820
1918
|
try {
|
|
1821
|
-
const
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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
|
+
}
|
|
1825
1933
|
}
|
|
1826
1934
|
return {
|
|
1827
1935
|
ok: true,
|
|
1828
|
-
warnings
|
|
1829
|
-
(issue) => `[COOP] post-merge warning [${issue.invariant}] ${issue.message}`
|
|
1830
|
-
)
|
|
1936
|
+
warnings
|
|
1831
1937
|
};
|
|
1832
1938
|
} catch (error) {
|
|
1833
1939
|
return {
|
|
@@ -1886,14 +1992,32 @@ function installPostMergeHook(repoRoot) {
|
|
|
1886
1992
|
}
|
|
1887
1993
|
|
|
1888
1994
|
// src/commands/init.ts
|
|
1889
|
-
function
|
|
1890
|
-
|
|
1891
|
-
|
|
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) {
|
|
1892
2016
|
return `version: 2
|
|
1893
2017
|
project:
|
|
1894
|
-
name:
|
|
1895
|
-
id: "${
|
|
1896
|
-
aliases
|
|
2018
|
+
name: ${JSON.stringify(projectName)}
|
|
2019
|
+
id: "${projectId}"
|
|
2020
|
+
aliases:${aliasesYaml(projectAliases)}
|
|
1897
2021
|
|
|
1898
2022
|
id_prefixes:
|
|
1899
2023
|
idea: "IDEA"
|
|
@@ -1943,8 +2067,8 @@ api:
|
|
|
1943
2067
|
remotes: {}
|
|
1944
2068
|
|
|
1945
2069
|
hooks:
|
|
1946
|
-
on_task_transition: .coop/hooks/on-task-transition.sh
|
|
1947
|
-
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
|
|
1948
2072
|
|
|
1949
2073
|
index:
|
|
1950
2074
|
data: yaml
|
|
@@ -2016,6 +2140,12 @@ triggers:
|
|
|
2016
2140
|
type: github_pr
|
|
2017
2141
|
operation: merge
|
|
2018
2142
|
`;
|
|
2143
|
+
var COOP_IGNORE_TEMPLATE = `.index/
|
|
2144
|
+
logs/
|
|
2145
|
+
tmp/
|
|
2146
|
+
*.log
|
|
2147
|
+
*.tmp
|
|
2148
|
+
`;
|
|
2019
2149
|
function ensureDir(dirPath) {
|
|
2020
2150
|
fs6.mkdirSync(dirPath, { recursive: true });
|
|
2021
2151
|
}
|
|
@@ -2064,8 +2194,8 @@ function setGitConfig(root, key, value) {
|
|
|
2064
2194
|
}
|
|
2065
2195
|
function installMergeDrivers(root) {
|
|
2066
2196
|
const entries = [
|
|
2067
|
-
".coop/tasks/*.md merge=coop-task",
|
|
2068
|
-
".coop/deliveries/*.yml merge=coop-delivery"
|
|
2197
|
+
".coop/projects/*/tasks/*.md merge=coop-task",
|
|
2198
|
+
".coop/projects/*/deliveries/*.yml merge=coop-delivery"
|
|
2069
2199
|
];
|
|
2070
2200
|
for (const entry of entries) {
|
|
2071
2201
|
ensureGitattributesEntry(root, entry);
|
|
@@ -2081,10 +2211,58 @@ function installMergeDrivers(root) {
|
|
|
2081
2211
|
}
|
|
2082
2212
|
return "Updated .gitattributes but could not register merge drivers in git config.";
|
|
2083
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
|
+
}
|
|
2084
2259
|
function registerInitCommand(program) {
|
|
2085
|
-
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) => {
|
|
2086
2261
|
const root = resolveRepoRoot();
|
|
2087
|
-
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);
|
|
2088
2266
|
const dirs = [
|
|
2089
2267
|
"ideas",
|
|
2090
2268
|
"tasks",
|
|
@@ -2100,23 +2278,36 @@ function registerInitCommand(program) {
|
|
|
2100
2278
|
"history/deliveries",
|
|
2101
2279
|
".index"
|
|
2102
2280
|
];
|
|
2281
|
+
ensureDir(path8.join(workspaceDir, "projects"));
|
|
2103
2282
|
for (const dir of dirs) {
|
|
2104
|
-
ensureDir(path8.join(
|
|
2283
|
+
ensureDir(path8.join(projectRoot, dir));
|
|
2105
2284
|
}
|
|
2106
|
-
writeIfMissing(
|
|
2107
|
-
|
|
2108
|
-
|
|
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);
|
|
2109
2291
|
}
|
|
2110
|
-
writeIfMissing(path8.join(
|
|
2111
|
-
writeIfMissing(path8.join(
|
|
2112
|
-
writeIfMissing(path8.join(
|
|
2113
|
-
writeIfMissing(path8.join(
|
|
2114
|
-
|
|
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/");
|
|
2115
2301
|
const preCommitHook = installPreCommitHook(root);
|
|
2116
2302
|
const postMergeHook = installPostMergeHook(root);
|
|
2117
2303
|
const mergeDrivers = installMergeDrivers(root);
|
|
2304
|
+
const project = resolveProject(root, projectId);
|
|
2118
2305
|
console.log("Initialized COOP workspace.");
|
|
2119
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)"}`);
|
|
2120
2311
|
console.log(`- ${preCommitHook.message}`);
|
|
2121
2312
|
if (preCommitHook.installed) {
|
|
2122
2313
|
console.log(`- Hook: ${path8.relative(root, preCommitHook.hookPath)}`);
|
|
@@ -2275,11 +2466,11 @@ function resolveRepoSafe(start = process.cwd()) {
|
|
|
2275
2466
|
}
|
|
2276
2467
|
function resolveCliLogFile(start = process.cwd()) {
|
|
2277
2468
|
const root = resolveRepoSafe(start);
|
|
2278
|
-
const
|
|
2279
|
-
if (fs7.existsSync(
|
|
2280
|
-
return path10.join(
|
|
2469
|
+
const workspace = coopWorkspaceDir(root);
|
|
2470
|
+
if (fs7.existsSync(workspace)) {
|
|
2471
|
+
return path10.join(workspace, "logs", "cli.log");
|
|
2281
2472
|
}
|
|
2282
|
-
return path10.join(
|
|
2473
|
+
return path10.join(resolveCoopHome(), "logs", "cli.log");
|
|
2283
2474
|
}
|
|
2284
2475
|
function appendLogEntry(entry, logFile) {
|
|
2285
2476
|
fs7.mkdirSync(path10.dirname(logFile), { recursive: true });
|
|
@@ -2358,8 +2549,11 @@ function registerLogCommand(program) {
|
|
|
2358
2549
|
}
|
|
2359
2550
|
|
|
2360
2551
|
// src/commands/migrate.ts
|
|
2552
|
+
import fs8 from "fs";
|
|
2361
2553
|
import path11 from "path";
|
|
2362
|
-
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";
|
|
2363
2557
|
function parseTargetVersion(raw) {
|
|
2364
2558
|
if (!raw) return CURRENT_SCHEMA_VERSION2;
|
|
2365
2559
|
const parsed = Number(raw);
|
|
@@ -2368,8 +2562,160 @@ function parseTargetVersion(raw) {
|
|
|
2368
2562
|
}
|
|
2369
2563
|
return parsed;
|
|
2370
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
|
+
}
|
|
2371
2717
|
function registerMigrateCommand(program) {
|
|
2372
|
-
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) => {
|
|
2373
2719
|
const root = resolveRepoRoot();
|
|
2374
2720
|
ensureCoopInitialized(root);
|
|
2375
2721
|
const target = parseTargetVersion(options.to);
|
|
@@ -2394,6 +2740,15 @@ function registerMigrateCommand(program) {
|
|
|
2394
2740
|
console.log(`- schema-version updated to v${report.to_version}`);
|
|
2395
2741
|
}
|
|
2396
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
|
+
});
|
|
2397
2752
|
}
|
|
2398
2753
|
|
|
2399
2754
|
// src/commands/plan.ts
|
|
@@ -2629,9 +2984,206 @@ function registerPlanCommand(program) {
|
|
|
2629
2984
|
});
|
|
2630
2985
|
}
|
|
2631
2986
|
|
|
2632
|
-
// src/commands/
|
|
2633
|
-
import
|
|
2987
|
+
// src/commands/project.ts
|
|
2988
|
+
import fs9 from "fs";
|
|
2634
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";
|
|
2635
3187
|
import { load_graph as load_graph5, parseTaskFile as parseTaskFile7 } from "@kitsy/coop-core";
|
|
2636
3188
|
import {
|
|
2637
3189
|
build_contract,
|
|
@@ -2641,8 +3193,8 @@ import {
|
|
|
2641
3193
|
} from "@kitsy/coop-ai";
|
|
2642
3194
|
function loadTask(root, idOrAlias) {
|
|
2643
3195
|
const target = resolveReference(root, idOrAlias, "task");
|
|
2644
|
-
const taskFile =
|
|
2645
|
-
if (!
|
|
3196
|
+
const taskFile = path13.join(root, ...target.file.split("/"));
|
|
3197
|
+
if (!fs10.existsSync(taskFile)) {
|
|
2646
3198
|
throw new Error(`Task file not found: ${target.file}`);
|
|
2647
3199
|
}
|
|
2648
3200
|
return parseTaskFile7(taskFile).task;
|
|
@@ -2681,22 +3233,22 @@ function registerRunCommand(program) {
|
|
|
2681
3233
|
on_progress: (message) => console.log(`[COOP] ${message}`)
|
|
2682
3234
|
});
|
|
2683
3235
|
if (result.status === "failed") {
|
|
2684
|
-
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)}`);
|
|
2685
3237
|
}
|
|
2686
3238
|
if (result.status === "paused") {
|
|
2687
3239
|
console.log(`[COOP] run paused: ${result.run.id}`);
|
|
2688
|
-
console.log(`[COOP] log: ${
|
|
3240
|
+
console.log(`[COOP] log: ${path13.relative(root, result.log_path)}`);
|
|
2689
3241
|
return;
|
|
2690
3242
|
}
|
|
2691
3243
|
console.log(`[COOP] run completed: ${result.run.id}`);
|
|
2692
|
-
console.log(`[COOP] log: ${
|
|
3244
|
+
console.log(`[COOP] log: ${path13.relative(root, result.log_path)}`);
|
|
2693
3245
|
});
|
|
2694
3246
|
}
|
|
2695
3247
|
|
|
2696
3248
|
// src/server/api.ts
|
|
2697
|
-
import
|
|
3249
|
+
import fs11 from "fs";
|
|
2698
3250
|
import http from "http";
|
|
2699
|
-
import
|
|
3251
|
+
import path14 from "path";
|
|
2700
3252
|
import {
|
|
2701
3253
|
analyze_feasibility as analyze_feasibility2,
|
|
2702
3254
|
load_graph as load_graph6,
|
|
@@ -2742,12 +3294,12 @@ function taskSummary(graph, task, external = []) {
|
|
|
2742
3294
|
};
|
|
2743
3295
|
}
|
|
2744
3296
|
function taskFileById(root, id) {
|
|
2745
|
-
const tasksDir =
|
|
2746
|
-
if (!
|
|
2747
|
-
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 });
|
|
2748
3300
|
const target = `${id}.md`.toLowerCase();
|
|
2749
3301
|
const match = entries.find((entry) => entry.isFile() && entry.name.toLowerCase() === target);
|
|
2750
|
-
return match ?
|
|
3302
|
+
return match ? path14.join(tasksDir, match.name) : null;
|
|
2751
3303
|
}
|
|
2752
3304
|
function loadRemoteConfig(root) {
|
|
2753
3305
|
const raw = readCoopConfig(root).raw;
|
|
@@ -2811,7 +3363,8 @@ function createApiServer(root, options = {}) {
|
|
|
2811
3363
|
json(res, 200, workspaceMeta(repoRoot));
|
|
2812
3364
|
return;
|
|
2813
3365
|
}
|
|
2814
|
-
const
|
|
3366
|
+
const project = resolveProject(repoRoot);
|
|
3367
|
+
const coopPath = project.root;
|
|
2815
3368
|
const graph = load_graph6(coopPath);
|
|
2816
3369
|
const resolutions = await externalResolutions(repoRoot, graph, options);
|
|
2817
3370
|
if (pathname === "/api/tasks") {
|
|
@@ -2836,7 +3389,7 @@ function createApiServer(root, options = {}) {
|
|
|
2836
3389
|
created: task.created,
|
|
2837
3390
|
updated: task.updated,
|
|
2838
3391
|
body: parsed.body,
|
|
2839
|
-
file_path:
|
|
3392
|
+
file_path: path14.relative(repoRoot, filePath).replace(/\\/g, "/")
|
|
2840
3393
|
}
|
|
2841
3394
|
});
|
|
2842
3395
|
return;
|
|
@@ -2924,8 +3477,8 @@ function registerServeCommand(program) {
|
|
|
2924
3477
|
}
|
|
2925
3478
|
|
|
2926
3479
|
// src/commands/show.ts
|
|
2927
|
-
import
|
|
2928
|
-
import
|
|
3480
|
+
import fs12 from "fs";
|
|
3481
|
+
import path15 from "path";
|
|
2929
3482
|
import { parseIdeaFile as parseIdeaFile4, parseTaskFile as parseTaskFile9 } from "@kitsy/coop-core";
|
|
2930
3483
|
function stringify(value) {
|
|
2931
3484
|
if (value === null || value === void 0) return "-";
|
|
@@ -2934,13 +3487,13 @@ function stringify(value) {
|
|
|
2934
3487
|
return String(value);
|
|
2935
3488
|
}
|
|
2936
3489
|
function loadComputedFromIndex(root, taskId) {
|
|
2937
|
-
const indexPath =
|
|
2938
|
-
if (!
|
|
3490
|
+
const indexPath = path15.join(ensureCoopInitialized(root), ".index", "tasks.json");
|
|
3491
|
+
if (!fs12.existsSync(indexPath)) {
|
|
2939
3492
|
return null;
|
|
2940
3493
|
}
|
|
2941
3494
|
let parsed;
|
|
2942
3495
|
try {
|
|
2943
|
-
parsed = JSON.parse(
|
|
3496
|
+
parsed = JSON.parse(fs12.readFileSync(indexPath, "utf8"));
|
|
2944
3497
|
} catch {
|
|
2945
3498
|
return null;
|
|
2946
3499
|
}
|
|
@@ -2979,7 +3532,7 @@ function showTask(taskId) {
|
|
|
2979
3532
|
const root = resolveRepoRoot();
|
|
2980
3533
|
const coop = ensureCoopInitialized(root);
|
|
2981
3534
|
const target = resolveReference(root, taskId, "task");
|
|
2982
|
-
const taskFile =
|
|
3535
|
+
const taskFile = path15.join(root, ...target.file.split("/"));
|
|
2983
3536
|
const parsed = parseTaskFile9(taskFile);
|
|
2984
3537
|
const task = parsed.task;
|
|
2985
3538
|
const body = parsed.body.trim();
|
|
@@ -2998,7 +3551,7 @@ function showTask(taskId) {
|
|
|
2998
3551
|
`Tags: ${stringify(task.tags)}`,
|
|
2999
3552
|
`Created: ${task.created}`,
|
|
3000
3553
|
`Updated: ${task.updated}`,
|
|
3001
|
-
`File: ${
|
|
3554
|
+
`File: ${path15.relative(root, taskFile)}`,
|
|
3002
3555
|
"",
|
|
3003
3556
|
"Body:",
|
|
3004
3557
|
body || "-",
|
|
@@ -3006,7 +3559,7 @@ function showTask(taskId) {
|
|
|
3006
3559
|
"Computed:"
|
|
3007
3560
|
];
|
|
3008
3561
|
if (!computed) {
|
|
3009
|
-
lines.push(`index not built (${
|
|
3562
|
+
lines.push(`index not built (${path15.relative(root, path15.join(coop, ".index", "tasks.json"))} missing)`);
|
|
3010
3563
|
} else {
|
|
3011
3564
|
for (const [key, value] of Object.entries(computed)) {
|
|
3012
3565
|
lines.push(`- ${key}: ${stringify(value)}`);
|
|
@@ -3018,7 +3571,7 @@ function showIdea(ideaId) {
|
|
|
3018
3571
|
const root = resolveRepoRoot();
|
|
3019
3572
|
ensureCoopInitialized(root);
|
|
3020
3573
|
const target = resolveReference(root, ideaId, "idea");
|
|
3021
|
-
const ideaFile =
|
|
3574
|
+
const ideaFile = path15.join(root, ...target.file.split("/"));
|
|
3022
3575
|
const parsed = parseIdeaFile4(ideaFile);
|
|
3023
3576
|
const idea = parsed.idea;
|
|
3024
3577
|
const body = parsed.body.trim();
|
|
@@ -3032,7 +3585,7 @@ function showIdea(ideaId) {
|
|
|
3032
3585
|
`Tags: ${stringify(idea.tags)}`,
|
|
3033
3586
|
`Linked Tasks: ${stringify(idea.linked_tasks)}`,
|
|
3034
3587
|
`Created: ${idea.created}`,
|
|
3035
|
-
`File: ${
|
|
3588
|
+
`File: ${path15.relative(root, ideaFile)}`,
|
|
3036
3589
|
"",
|
|
3037
3590
|
"Body:",
|
|
3038
3591
|
body || "-"
|
|
@@ -3139,7 +3692,7 @@ function registerStatusCommand(program) {
|
|
|
3139
3692
|
}
|
|
3140
3693
|
|
|
3141
3694
|
// src/commands/transition.ts
|
|
3142
|
-
import
|
|
3695
|
+
import path16 from "path";
|
|
3143
3696
|
import {
|
|
3144
3697
|
TaskStatus as TaskStatus3,
|
|
3145
3698
|
check_permission as check_permission3,
|
|
@@ -3168,8 +3721,8 @@ import { Octokit } from "octokit";
|
|
|
3168
3721
|
function isObject(value) {
|
|
3169
3722
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3170
3723
|
}
|
|
3171
|
-
function sanitizeBranchPart(
|
|
3172
|
-
const normalized =
|
|
3724
|
+
function sanitizeBranchPart(input3, fallback) {
|
|
3725
|
+
const normalized = input3.trim().toLowerCase().replace(/[^a-z0-9/_-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
3173
3726
|
return normalized || fallback;
|
|
3174
3727
|
}
|
|
3175
3728
|
function readGitRemote(root) {
|
|
@@ -3302,15 +3855,15 @@ function toGitHubClient(config) {
|
|
|
3302
3855
|
baseUrl: config.apiBaseUrl
|
|
3303
3856
|
});
|
|
3304
3857
|
return {
|
|
3305
|
-
async createPullRequest(
|
|
3858
|
+
async createPullRequest(input3) {
|
|
3306
3859
|
const response = await octokit.rest.pulls.create({
|
|
3307
|
-
owner:
|
|
3308
|
-
repo:
|
|
3309
|
-
title:
|
|
3310
|
-
body:
|
|
3311
|
-
head:
|
|
3312
|
-
base:
|
|
3313
|
-
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
|
|
3314
3867
|
});
|
|
3315
3868
|
return {
|
|
3316
3869
|
number: response.data.number,
|
|
@@ -3320,14 +3873,14 @@ function toGitHubClient(config) {
|
|
|
3320
3873
|
draft: response.data.draft
|
|
3321
3874
|
};
|
|
3322
3875
|
},
|
|
3323
|
-
async updatePullRequest(
|
|
3876
|
+
async updatePullRequest(input3) {
|
|
3324
3877
|
const response = await octokit.rest.pulls.update({
|
|
3325
|
-
owner:
|
|
3326
|
-
repo:
|
|
3327
|
-
pull_number:
|
|
3328
|
-
title:
|
|
3329
|
-
body:
|
|
3330
|
-
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
|
|
3331
3884
|
});
|
|
3332
3885
|
return {
|
|
3333
3886
|
number: response.data.number,
|
|
@@ -3337,11 +3890,11 @@ function toGitHubClient(config) {
|
|
|
3337
3890
|
draft: response.data.draft
|
|
3338
3891
|
};
|
|
3339
3892
|
},
|
|
3340
|
-
async getPullRequest(
|
|
3893
|
+
async getPullRequest(input3) {
|
|
3341
3894
|
const response = await octokit.rest.pulls.get({
|
|
3342
|
-
owner:
|
|
3343
|
-
repo:
|
|
3344
|
-
pull_number:
|
|
3895
|
+
owner: input3.owner,
|
|
3896
|
+
repo: input3.repo,
|
|
3897
|
+
pull_number: input3.pull_number
|
|
3345
3898
|
});
|
|
3346
3899
|
return {
|
|
3347
3900
|
number: response.data.number,
|
|
@@ -3351,12 +3904,12 @@ function toGitHubClient(config) {
|
|
|
3351
3904
|
draft: response.data.draft
|
|
3352
3905
|
};
|
|
3353
3906
|
},
|
|
3354
|
-
async mergePullRequest(
|
|
3907
|
+
async mergePullRequest(input3) {
|
|
3355
3908
|
const response = await octokit.rest.pulls.merge({
|
|
3356
|
-
owner:
|
|
3357
|
-
repo:
|
|
3358
|
-
pull_number:
|
|
3359
|
-
merge_method:
|
|
3909
|
+
owner: input3.owner,
|
|
3910
|
+
repo: input3.repo,
|
|
3911
|
+
pull_number: input3.pull_number,
|
|
3912
|
+
merge_method: input3.merge_method
|
|
3360
3913
|
});
|
|
3361
3914
|
return {
|
|
3362
3915
|
merged: response.data.merged,
|
|
@@ -3372,7 +3925,9 @@ function readGitHubConfig(root) {
|
|
|
3372
3925
|
const owner = typeof githubRaw.owner === "string" && githubRaw.owner.trim() ? githubRaw.owner.trim() : inferred?.owner;
|
|
3373
3926
|
const repo = typeof githubRaw.repo === "string" && githubRaw.repo.trim() ? githubRaw.repo.trim() : inferred?.repo;
|
|
3374
3927
|
if (!owner || !repo) {
|
|
3375
|
-
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
|
+
);
|
|
3376
3931
|
}
|
|
3377
3932
|
const tokenEnv = typeof githubRaw.token_env === "string" && githubRaw.token_env.trim() ? githubRaw.token_env.trim() : "GITHUB_TOKEN";
|
|
3378
3933
|
const token = process.env[tokenEnv];
|
|
@@ -3733,7 +4288,7 @@ function registerTransitionCommand(program) {
|
|
|
3733
4288
|
}
|
|
3734
4289
|
console.warn(`[COOP][auth] override: user '${user}' forced transition_task on '${reference.id}'.`);
|
|
3735
4290
|
}
|
|
3736
|
-
const filePath =
|
|
4291
|
+
const filePath = path16.join(root, ...reference.file.split("/"));
|
|
3737
4292
|
const parsed = parseTaskFile11(filePath);
|
|
3738
4293
|
const result = transition2(parsed.task, target, {
|
|
3739
4294
|
actor: options.actor ?? user,
|
|
@@ -3779,19 +4334,19 @@ ${errors}`);
|
|
|
3779
4334
|
}
|
|
3780
4335
|
|
|
3781
4336
|
// src/commands/ui.ts
|
|
3782
|
-
import
|
|
3783
|
-
import
|
|
4337
|
+
import fs13 from "fs";
|
|
4338
|
+
import path17 from "path";
|
|
3784
4339
|
import { createRequire } from "module";
|
|
3785
4340
|
import { fileURLToPath } from "url";
|
|
3786
4341
|
import { spawn } from "child_process";
|
|
3787
|
-
import { IndexManager as
|
|
4342
|
+
import { IndexManager as IndexManager3 } from "@kitsy/coop-core";
|
|
3788
4343
|
function findPackageRoot(entryPath) {
|
|
3789
|
-
let current =
|
|
4344
|
+
let current = path17.dirname(entryPath);
|
|
3790
4345
|
while (true) {
|
|
3791
|
-
if (
|
|
4346
|
+
if (fs13.existsSync(path17.join(current, "package.json"))) {
|
|
3792
4347
|
return current;
|
|
3793
4348
|
}
|
|
3794
|
-
const parent =
|
|
4349
|
+
const parent = path17.dirname(current);
|
|
3795
4350
|
if (parent === current) {
|
|
3796
4351
|
throw new Error(`Unable to locate package root for ${entryPath}.`);
|
|
3797
4352
|
}
|
|
@@ -3822,7 +4377,7 @@ function openBrowser(url) {
|
|
|
3822
4377
|
}
|
|
3823
4378
|
function ensureIndex(repoRoot) {
|
|
3824
4379
|
const directory = ensureCoopInitialized(repoRoot);
|
|
3825
|
-
const manager = new
|
|
4380
|
+
const manager = new IndexManager3(directory);
|
|
3826
4381
|
if (manager.is_stale() || !manager.load_indexed_graph()) {
|
|
3827
4382
|
manager.build_full_index();
|
|
3828
4383
|
}
|
|
@@ -3837,11 +4392,12 @@ function attachSignalForwarding(child) {
|
|
|
3837
4392
|
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
3838
4393
|
}
|
|
3839
4394
|
async function startUiServer(repoRoot, host, port, shouldOpen) {
|
|
4395
|
+
const project = resolveProject(repoRoot);
|
|
3840
4396
|
ensureIndex(repoRoot);
|
|
3841
4397
|
const uiRoot = resolveUiPackageRoot();
|
|
3842
|
-
const requireFromUi = createRequire(
|
|
4398
|
+
const requireFromUi = createRequire(path17.join(uiRoot, "package.json"));
|
|
3843
4399
|
const vitePackageJson = requireFromUi.resolve("vite/package.json");
|
|
3844
|
-
const viteBin =
|
|
4400
|
+
const viteBin = path17.join(path17.dirname(vitePackageJson), "bin", "vite.js");
|
|
3845
4401
|
const url = `http://${host}:${port}`;
|
|
3846
4402
|
console.log(`COOP UI: ${url}`);
|
|
3847
4403
|
const child = spawn(process.execPath, [viteBin, "--host", host, "--port", String(port)], {
|
|
@@ -3850,6 +4406,7 @@ async function startUiServer(repoRoot, host, port, shouldOpen) {
|
|
|
3850
4406
|
env: {
|
|
3851
4407
|
...process.env,
|
|
3852
4408
|
COOP_REPO_ROOT: repoRoot,
|
|
4409
|
+
COOP_PROJECT_ID: project.id,
|
|
3853
4410
|
COOP_UI_HOST: host,
|
|
3854
4411
|
COOP_UI_PORT: String(port)
|
|
3855
4412
|
}
|
|
@@ -4197,9 +4754,9 @@ function registerWebhookCommand(program) {
|
|
|
4197
4754
|
}
|
|
4198
4755
|
|
|
4199
4756
|
// src/merge-driver/merge-driver.ts
|
|
4200
|
-
import
|
|
4757
|
+
import fs14 from "fs";
|
|
4201
4758
|
import os2 from "os";
|
|
4202
|
-
import
|
|
4759
|
+
import path18 from "path";
|
|
4203
4760
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
4204
4761
|
import { stringifyFrontmatter as stringifyFrontmatter3, parseFrontmatterContent, parseYamlContent, stringifyYamlContent } from "@kitsy/coop-core";
|
|
4205
4762
|
var STATUS_RANK = {
|
|
@@ -4258,16 +4815,16 @@ function chooseFieldValue(key, baseValue, oursValue, theirsValue, oursUpdated, t
|
|
|
4258
4815
|
}
|
|
4259
4816
|
function mergeTaskFrontmatter(base, ours, theirs) {
|
|
4260
4817
|
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)]);
|
|
4261
|
-
const
|
|
4818
|
+
const output3 = {};
|
|
4262
4819
|
const oursUpdated = asTimestamp(ours.updated);
|
|
4263
4820
|
const theirsUpdated = asTimestamp(theirs.updated);
|
|
4264
4821
|
for (const key of keys) {
|
|
4265
4822
|
const merged = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
|
|
4266
4823
|
if (merged !== void 0) {
|
|
4267
|
-
|
|
4824
|
+
output3[key] = merged;
|
|
4268
4825
|
}
|
|
4269
4826
|
}
|
|
4270
|
-
return
|
|
4827
|
+
return output3;
|
|
4271
4828
|
}
|
|
4272
4829
|
function mergeTextWithGit(ancestor, ours, theirs) {
|
|
4273
4830
|
const result = spawnSync5("git", ["merge-file", "-p", ours, ancestor, theirs], {
|
|
@@ -4282,33 +4839,33 @@ function mergeTextWithGit(ancestor, ours, theirs) {
|
|
|
4282
4839
|
return { ok: false, output: stdout };
|
|
4283
4840
|
}
|
|
4284
4841
|
function mergeTaskFile(ancestorPath, oursPath, theirsPath) {
|
|
4285
|
-
const ancestorRaw =
|
|
4286
|
-
const oursRaw =
|
|
4287
|
-
const theirsRaw =
|
|
4842
|
+
const ancestorRaw = fs14.readFileSync(ancestorPath, "utf8");
|
|
4843
|
+
const oursRaw = fs14.readFileSync(oursPath, "utf8");
|
|
4844
|
+
const theirsRaw = fs14.readFileSync(theirsPath, "utf8");
|
|
4288
4845
|
const ancestor = parseTaskDocument(ancestorRaw, ancestorPath);
|
|
4289
4846
|
const ours = parseTaskDocument(oursRaw, oursPath);
|
|
4290
4847
|
const theirs = parseTaskDocument(theirsRaw, theirsPath);
|
|
4291
4848
|
const mergedFrontmatter = mergeTaskFrontmatter(ancestor.frontmatter, ours.frontmatter, theirs.frontmatter);
|
|
4292
|
-
const tempDir =
|
|
4849
|
+
const tempDir = fs14.mkdtempSync(path18.join(os2.tmpdir(), "coop-merge-body-"));
|
|
4293
4850
|
try {
|
|
4294
|
-
const ancestorBody =
|
|
4295
|
-
const oursBody =
|
|
4296
|
-
const theirsBody =
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
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");
|
|
4300
4857
|
const mergedBody = mergeTextWithGit(ancestorBody, oursBody, theirsBody);
|
|
4301
|
-
const
|
|
4302
|
-
|
|
4858
|
+
const output3 = stringifyFrontmatter3(mergedFrontmatter, mergedBody.output);
|
|
4859
|
+
fs14.writeFileSync(oursPath, output3, "utf8");
|
|
4303
4860
|
return mergedBody.ok ? 0 : 1;
|
|
4304
4861
|
} finally {
|
|
4305
|
-
|
|
4862
|
+
fs14.rmSync(tempDir, { recursive: true, force: true });
|
|
4306
4863
|
}
|
|
4307
4864
|
}
|
|
4308
4865
|
function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
4309
|
-
const ancestor = parseYamlContent(
|
|
4310
|
-
const ours = parseYamlContent(
|
|
4311
|
-
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);
|
|
4312
4869
|
const oursUpdated = asTimestamp(ours.updated);
|
|
4313
4870
|
const theirsUpdated = asTimestamp(theirs.updated);
|
|
4314
4871
|
const base = ancestor;
|
|
@@ -4318,7 +4875,7 @@ function mergeDeliveryFile(ancestorPath, oursPath, theirsPath) {
|
|
|
4318
4875
|
const value = chooseFieldValue(key, base[key], ours[key], theirs[key], oursUpdated, theirsUpdated);
|
|
4319
4876
|
if (value !== void 0) merged[key] = value;
|
|
4320
4877
|
}
|
|
4321
|
-
|
|
4878
|
+
fs14.writeFileSync(oursPath, stringifyYamlContent(merged), "utf8");
|
|
4322
4879
|
return 0;
|
|
4323
4880
|
}
|
|
4324
4881
|
function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
@@ -4331,9 +4888,9 @@ function runMergeDriver(kind, ancestorPath, oursPath, theirsPath) {
|
|
|
4331
4888
|
// src/index.ts
|
|
4332
4889
|
function readVersion() {
|
|
4333
4890
|
const currentFile = fileURLToPath2(import.meta.url);
|
|
4334
|
-
const packageJsonPath =
|
|
4891
|
+
const packageJsonPath = path19.resolve(path19.dirname(currentFile), "..", "package.json");
|
|
4335
4892
|
try {
|
|
4336
|
-
const parsed = JSON.parse(
|
|
4893
|
+
const parsed = JSON.parse(fs15.readFileSync(packageJsonPath, "utf8"));
|
|
4337
4894
|
return parsed.version ?? "0.0.0";
|
|
4338
4895
|
} catch {
|
|
4339
4896
|
return "0.0.0";
|
|
@@ -4350,6 +4907,7 @@ function createProgram() {
|
|
|
4350
4907
|
program.version(readVersion());
|
|
4351
4908
|
program.description("COOP CLI");
|
|
4352
4909
|
program.option("--verbose", "Print stack traces for command errors");
|
|
4910
|
+
program.option("-p, --project <id>", "Select the active COOP project");
|
|
4353
4911
|
registerInitCommand(program);
|
|
4354
4912
|
registerCreateCommand(program);
|
|
4355
4913
|
registerAssignCommand(program);
|
|
@@ -4363,6 +4921,7 @@ function createProgram() {
|
|
|
4363
4921
|
registerLogCommand(program);
|
|
4364
4922
|
registerMigrateCommand(program);
|
|
4365
4923
|
registerPlanCommand(program);
|
|
4924
|
+
registerProjectCommand(program);
|
|
4366
4925
|
registerRunCommand(program);
|
|
4367
4926
|
registerServeCommand(program);
|
|
4368
4927
|
registerStatusCommand(program);
|
|
@@ -4426,7 +4985,7 @@ async function runCli(argv = process.argv) {
|
|
|
4426
4985
|
function isMainModule() {
|
|
4427
4986
|
const entry = process.argv[1];
|
|
4428
4987
|
if (!entry) return false;
|
|
4429
|
-
return
|
|
4988
|
+
return path19.resolve(entry) === fileURLToPath2(import.meta.url);
|
|
4430
4989
|
}
|
|
4431
4990
|
if (isMainModule()) {
|
|
4432
4991
|
await runCli(process.argv);
|