@grainulation/orchard 1.0.1 → 1.0.2
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/CONTRIBUTING.md +6 -0
- package/README.md +21 -20
- package/bin/orchard.js +227 -80
- package/lib/assignments.js +19 -17
- package/lib/conflicts.js +177 -29
- package/lib/dashboard.js +100 -47
- package/lib/decompose.js +268 -0
- package/lib/doctor.js +48 -32
- package/lib/export.js +72 -44
- package/lib/farmer.js +54 -38
- package/lib/hackathon.js +349 -0
- package/lib/planner.js +150 -21
- package/lib/server.js +395 -165
- package/lib/sync.js +31 -25
- package/lib/tracker.js +52 -40
- package/package.json +7 -3
package/CONTRIBUTING.md
CHANGED
|
@@ -15,16 +15,20 @@ No `npm install` needed -- orchard has zero dependencies.
|
|
|
15
15
|
## How to contribute
|
|
16
16
|
|
|
17
17
|
### Report a bug
|
|
18
|
+
|
|
18
19
|
Open an issue with:
|
|
20
|
+
|
|
19
21
|
- What you expected
|
|
20
22
|
- What happened instead
|
|
21
23
|
- Your Node version (`node --version`)
|
|
22
24
|
- Steps to reproduce
|
|
23
25
|
|
|
24
26
|
### Suggest a feature
|
|
27
|
+
|
|
25
28
|
Open an issue describing the use case, not just the solution. "I need X because Y" is more useful than "add X."
|
|
26
29
|
|
|
27
30
|
### Submit a PR
|
|
31
|
+
|
|
28
32
|
1. Fork the repo
|
|
29
33
|
2. Create a branch (`git checkout -b fix/description`)
|
|
30
34
|
3. Make your changes
|
|
@@ -74,11 +78,13 @@ Tests use Node's built-in test runner. No test framework dependencies.
|
|
|
74
78
|
## Commit messages
|
|
75
79
|
|
|
76
80
|
Follow the existing pattern:
|
|
81
|
+
|
|
77
82
|
```
|
|
78
83
|
orchard: <what changed>
|
|
79
84
|
```
|
|
80
85
|
|
|
81
86
|
Examples:
|
|
87
|
+
|
|
82
88
|
```
|
|
83
89
|
orchard: add cross-sprint dependency graph
|
|
84
90
|
orchard: fix timeline calculation for overlapping sprints
|
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://www.npmjs.com/package/@grainulation/orchard"><img src="https://img.shields.io/npm/v/@grainulation/orchard" alt="npm version"></a> <a href="https://www.npmjs.com/package/@grainulation/orchard"><img src="https://img.shields.io/npm/dm/@grainulation/orchard" alt="npm downloads"></a> <a href="https://github.com/grainulation/orchard/blob/main/LICENSE"><img src="https://img.shields.io/
|
|
6
|
+
<a href="https://www.npmjs.com/package/@grainulation/orchard"><img src="https://img.shields.io/npm/v/@grainulation/orchard" alt="npm version"></a> <a href="https://www.npmjs.com/package/@grainulation/orchard"><img src="https://img.shields.io/npm/dm/@grainulation/orchard" alt="npm downloads"></a> <a href="https://github.com/grainulation/orchard/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a> <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@grainulation/orchard" alt="node"></a> <a href="https://github.com/grainulation/orchard/actions"><img src="https://github.com/grainulation/orchard/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
7
|
+
<a href="https://deepwiki.com/grainulation/orchard"><img src="https://deepwiki.com/badge.svg" alt="Explore on DeepWiki"></a>
|
|
7
8
|
</p>
|
|
8
9
|
|
|
9
10
|
<p align="center"><strong>Multi-sprint orchestration and dependency tracking.</strong></p>
|
|
@@ -69,15 +70,15 @@ orchard dashboard # Generate unified HTML dashboard
|
|
|
69
70
|
|
|
70
71
|
## CLI
|
|
71
72
|
|
|
72
|
-
| Command
|
|
73
|
-
|
|
74
|
-
| `orchard plan`
|
|
75
|
-
| `orchard status`
|
|
76
|
-
| `orchard assign <path> <person>` | Assign a person to a sprint
|
|
77
|
-
| `orchard sync`
|
|
78
|
-
| `orchard dashboard [outfile]`
|
|
79
|
-
| `orchard init`
|
|
80
|
-
| `orchard serve`
|
|
73
|
+
| Command | Description |
|
|
74
|
+
| -------------------------------- | ---------------------------------------- |
|
|
75
|
+
| `orchard plan` | Show sprint dependency graph |
|
|
76
|
+
| `orchard status` | Show status of all tracked sprints |
|
|
77
|
+
| `orchard assign <path> <person>` | Assign a person to a sprint |
|
|
78
|
+
| `orchard sync` | Sync sprint states from directories |
|
|
79
|
+
| `orchard dashboard [outfile]` | Generate unified HTML dashboard |
|
|
80
|
+
| `orchard init` | Initialize orchard.json |
|
|
81
|
+
| `orchard serve` | Start the portfolio dashboard web server |
|
|
81
82
|
|
|
82
83
|
## Conflict detection
|
|
83
84
|
|
|
@@ -92,16 +93,16 @@ Node built-in modules only.
|
|
|
92
93
|
|
|
93
94
|
## Part of the grainulation ecosystem
|
|
94
95
|
|
|
95
|
-
| Tool
|
|
96
|
-
|
|
97
|
-
| [wheat](https://github.com/grainulation/wheat)
|
|
98
|
-
| [farmer](https://github.com/grainulation/farmer)
|
|
99
|
-
| [barn](https://github.com/grainulation/barn)
|
|
100
|
-
| [mill](https://github.com/grainulation/mill)
|
|
101
|
-
| [silo](https://github.com/grainulation/silo)
|
|
102
|
-
| [harvest](https://github.com/grainulation/harvest)
|
|
103
|
-
| **orchard**
|
|
104
|
-
| [grainulation](https://github.com/grainulation/grainulation) | Unified CLI -- single entry point to the ecosystem
|
|
96
|
+
| Tool | Role |
|
|
97
|
+
| ------------------------------------------------------------ | ----------------------------------------------------------- |
|
|
98
|
+
| [wheat](https://github.com/grainulation/wheat) | Research engine -- grow structured evidence |
|
|
99
|
+
| [farmer](https://github.com/grainulation/farmer) | Permission dashboard -- approve AI actions in real time |
|
|
100
|
+
| [barn](https://github.com/grainulation/barn) | Shared tools -- templates, validators, sprint detection |
|
|
101
|
+
| [mill](https://github.com/grainulation/mill) | Format conversion -- export to PDF, CSV, slides, 24 formats |
|
|
102
|
+
| [silo](https://github.com/grainulation/silo) | Knowledge storage -- reusable claim libraries and packs |
|
|
103
|
+
| [harvest](https://github.com/grainulation/harvest) | Analytics -- cross-sprint patterns and prediction scoring |
|
|
104
|
+
| **orchard** | Orchestration -- multi-sprint coordination and dependencies |
|
|
105
|
+
| [grainulation](https://github.com/grainulation/grainulation) | Unified CLI -- single entry point to the ecosystem |
|
|
105
106
|
|
|
106
107
|
## License
|
|
107
108
|
|
package/bin/orchard.js
CHANGED
|
@@ -1,74 +1,78 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
"use strict";
|
|
3
3
|
|
|
4
|
-
const path = require(
|
|
5
|
-
const fs = require(
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
6
|
|
|
7
|
-
const verbose =
|
|
7
|
+
const verbose =
|
|
8
|
+
process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
8
9
|
function vlog(...a) {
|
|
9
10
|
if (!verbose) return;
|
|
10
11
|
const ts = new Date().toISOString();
|
|
11
|
-
process.stderr.write(`[${ts}] orchard: ${a.join(
|
|
12
|
+
process.stderr.write(`[${ts}] orchard: ${a.join(" ")}\n`);
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const COMMANDS = {
|
|
15
|
-
init:
|
|
16
|
-
plan:
|
|
17
|
-
status:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
init: "Initialize orchard.json in the current directory",
|
|
17
|
+
plan: "Show sprint dependency graph (--format mermaid|ascii, --mermaid)",
|
|
18
|
+
status: "Show status of all tracked sprints",
|
|
19
|
+
conflicts: "Show cross-sprint conflicts (--severity critical|warning|info)",
|
|
20
|
+
assign: "Assign a person to a sprint",
|
|
21
|
+
sync: "Sync sprint states from their directories",
|
|
22
|
+
decompose: "Auto-decompose a question into sub-sprints",
|
|
23
|
+
hackathon: "Hackathon coordinator (init|team|status|end)",
|
|
24
|
+
dashboard: "Generate unified HTML dashboard",
|
|
25
|
+
serve: "Start the portfolio dashboard web server",
|
|
26
|
+
connect: "Connect to a farmer instance",
|
|
27
|
+
doctor: "Check health of orchard setup",
|
|
28
|
+
help: "Show this help message",
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
function loadConfig(dir) {
|
|
28
|
-
const configPath = path.join(dir,
|
|
32
|
+
const configPath = path.join(dir, "orchard.json");
|
|
29
33
|
if (!fs.existsSync(configPath)) {
|
|
30
34
|
return null;
|
|
31
35
|
}
|
|
32
|
-
return JSON.parse(fs.readFileSync(configPath,
|
|
36
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
function findOrchardRoot() {
|
|
36
40
|
let dir = process.cwd();
|
|
37
41
|
while (dir !== path.dirname(dir)) {
|
|
38
|
-
if (fs.existsSync(path.join(dir,
|
|
42
|
+
if (fs.existsSync(path.join(dir, "orchard.json"))) return dir;
|
|
39
43
|
dir = path.dirname(dir);
|
|
40
44
|
}
|
|
41
45
|
return null;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
function printHelp() {
|
|
45
|
-
console.log(
|
|
46
|
-
console.log(
|
|
47
|
-
console.log(
|
|
48
|
-
console.log(
|
|
49
|
-
console.log(
|
|
50
|
-
console.log(
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log(" orchard - Multi-sprint research orchestrator");
|
|
51
|
+
console.log("");
|
|
52
|
+
console.log(" Usage: orchard <command> [options]");
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(" Commands:");
|
|
51
55
|
for (const [cmd, desc] of Object.entries(COMMANDS)) {
|
|
52
56
|
console.log(` ${cmd.padEnd(12)} ${desc}`);
|
|
53
57
|
}
|
|
54
|
-
console.log(
|
|
55
|
-
console.log(
|
|
56
|
-
console.log(
|
|
57
|
-
console.log(
|
|
58
|
-
console.log(
|
|
59
|
-
console.log(
|
|
60
|
-
console.log(
|
|
61
|
-
console.log(
|
|
62
|
-
console.log(
|
|
63
|
-
console.log(
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log(" Serve options:");
|
|
60
|
+
console.log(" --port 9097 Port for the web server (default: 9097)");
|
|
61
|
+
console.log(" --root <dir> Root directory to scan for sprints");
|
|
62
|
+
console.log("");
|
|
63
|
+
console.log(" Connect:");
|
|
64
|
+
console.log(" orchard connect farmer --url http://localhost:9090");
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log(" Config: orchard.json in project root");
|
|
67
|
+
console.log("");
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
async function main() {
|
|
67
71
|
const args = process.argv.slice(2);
|
|
68
|
-
const command = args[0] ||
|
|
69
|
-
vlog(
|
|
72
|
+
const command = args[0] || "help";
|
|
73
|
+
vlog("startup", `command=${command}`, `cwd=${process.cwd()}`);
|
|
70
74
|
|
|
71
|
-
if (command ===
|
|
75
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
72
76
|
printHelp();
|
|
73
77
|
process.exit(0);
|
|
74
78
|
}
|
|
@@ -80,30 +84,30 @@ async function main() {
|
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
// Serve command — start the HTTP server (ESM module)
|
|
83
|
-
if (command ===
|
|
84
|
-
const serverPath = path.join(__dirname,
|
|
85
|
-
const { spawn } = require(
|
|
87
|
+
if (command === "serve") {
|
|
88
|
+
const serverPath = path.join(__dirname, "..", "lib", "server.js");
|
|
89
|
+
const { spawn } = require("node:child_process");
|
|
86
90
|
|
|
87
91
|
// Forward remaining args to the server
|
|
88
92
|
const serverArgs = args.slice(1);
|
|
89
93
|
const child = spawn(process.execPath, [serverPath, ...serverArgs], {
|
|
90
|
-
stdio:
|
|
94
|
+
stdio: "inherit",
|
|
91
95
|
});
|
|
92
96
|
|
|
93
|
-
child.on(
|
|
94
|
-
child.on(
|
|
97
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
98
|
+
child.on("error", (err) => {
|
|
95
99
|
console.error(`orchard: failed to start server: ${err.message}`);
|
|
96
100
|
process.exit(1);
|
|
97
101
|
});
|
|
98
|
-
process.on(
|
|
99
|
-
process.on(
|
|
102
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
103
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
100
104
|
return;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
if (command ===
|
|
104
|
-
const { connect: farmerConnect } = require(
|
|
105
|
-
const connectArgs = process.argv.slice(process.argv.indexOf(
|
|
106
|
-
const rootIdx = connectArgs.indexOf(
|
|
107
|
+
if (command === "connect") {
|
|
108
|
+
const { connect: farmerConnect } = require("../lib/farmer.js");
|
|
109
|
+
const connectArgs = process.argv.slice(process.argv.indexOf("connect") + 1);
|
|
110
|
+
const rootIdx = connectArgs.indexOf("--root");
|
|
107
111
|
let targetDir = process.cwd();
|
|
108
112
|
if (rootIdx !== -1 && connectArgs[rootIdx + 1]) {
|
|
109
113
|
targetDir = path.resolve(connectArgs[rootIdx + 1]);
|
|
@@ -112,85 +116,228 @@ async function main() {
|
|
|
112
116
|
return;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
if (command ===
|
|
116
|
-
const { parseArgs } = require(
|
|
119
|
+
if (command === "init") {
|
|
120
|
+
const { parseArgs } = require("node:util");
|
|
117
121
|
let rootDir = process.cwd();
|
|
118
122
|
try {
|
|
119
|
-
const { values } = parseArgs({
|
|
123
|
+
const { values } = parseArgs({
|
|
124
|
+
args: process.argv.slice(3),
|
|
125
|
+
options: { root: { type: "string" } },
|
|
126
|
+
allowPositionals: true,
|
|
127
|
+
});
|
|
120
128
|
if (values.root) rootDir = path.resolve(values.root);
|
|
121
|
-
} catch (_) {
|
|
122
|
-
|
|
129
|
+
} catch (_) {
|
|
130
|
+
/* ignore parse errors for init */
|
|
131
|
+
}
|
|
132
|
+
const configPath = path.join(rootDir, "orchard.json");
|
|
123
133
|
if (fs.existsSync(configPath)) {
|
|
124
|
-
console.error(
|
|
134
|
+
console.error("orchard: orchard.json already exists");
|
|
125
135
|
process.exit(1);
|
|
126
136
|
}
|
|
127
|
-
const defaultConfig = {
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
const defaultConfig = {
|
|
138
|
+
sprints: [],
|
|
139
|
+
settings: { sync_interval: "manual" },
|
|
140
|
+
};
|
|
141
|
+
fs.writeFileSync(
|
|
142
|
+
configPath,
|
|
143
|
+
JSON.stringify(defaultConfig, null, 2) + "\n",
|
|
144
|
+
"utf8",
|
|
145
|
+
);
|
|
146
|
+
console.log("Initialized orchard.json — add sprints with `orchard plan`");
|
|
130
147
|
process.exit(0);
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
const root = findOrchardRoot();
|
|
134
|
-
if (!root && command !==
|
|
135
|
-
console.error(
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
if (!root && command !== "help" && command !== "doctor") {
|
|
152
|
+
console.error(
|
|
153
|
+
"orchard: no orchard.json found. Run from a directory with orchard.json or a subdirectory.",
|
|
154
|
+
);
|
|
155
|
+
console.error("");
|
|
156
|
+
console.error("Create one:");
|
|
138
157
|
console.error(' { "sprints": [] }');
|
|
139
158
|
process.exit(1);
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
const config = root ? loadConfig(root) : { sprints: [] };
|
|
143
|
-
const jsonMode = args.includes(
|
|
162
|
+
const jsonMode = args.includes("--json");
|
|
144
163
|
|
|
145
164
|
switch (command) {
|
|
146
|
-
case
|
|
165
|
+
case "plan": {
|
|
147
166
|
if (jsonMode) {
|
|
148
|
-
const {
|
|
167
|
+
const {
|
|
168
|
+
buildGraph,
|
|
169
|
+
topoSort,
|
|
170
|
+
detectCycles,
|
|
171
|
+
} = require("../lib/planner.js");
|
|
149
172
|
const graph = buildGraph(config);
|
|
150
173
|
const order = topoSort(config);
|
|
151
174
|
const cycles = detectCycles(config);
|
|
152
175
|
console.log(JSON.stringify({ graph, order, cycles }, null, 2));
|
|
153
176
|
break;
|
|
154
177
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
178
|
+
const fmtIdx = args.indexOf("--format");
|
|
179
|
+
const planFormat =
|
|
180
|
+
fmtIdx !== -1 && args[fmtIdx + 1] ? args[fmtIdx + 1] : null;
|
|
181
|
+
if (args.includes("--mermaid") || planFormat === "mermaid") {
|
|
182
|
+
const { generateMermaid } = require("../lib/planner.js");
|
|
183
|
+
console.log(generateMermaid(config));
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (planFormat === "ascii" || !planFormat) {
|
|
187
|
+
const { printDependencyGraph } = require("../lib/planner.js");
|
|
188
|
+
printDependencyGraph(config, root);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
console.error(
|
|
192
|
+
`orchard: unknown plan format: ${planFormat}. Supported: ascii, mermaid`,
|
|
193
|
+
);
|
|
194
|
+
process.exit(1);
|
|
157
195
|
break;
|
|
158
196
|
}
|
|
159
|
-
case
|
|
197
|
+
case "status": {
|
|
160
198
|
if (jsonMode) {
|
|
161
|
-
const { getStatusData } = require(
|
|
199
|
+
const { getStatusData } = require("../lib/tracker.js");
|
|
162
200
|
const data = getStatusData(config, root);
|
|
163
201
|
console.log(JSON.stringify(data, null, 2));
|
|
164
202
|
break;
|
|
165
203
|
}
|
|
166
|
-
const { printStatus } = require(
|
|
204
|
+
const { printStatus } = require("../lib/tracker.js");
|
|
167
205
|
printStatus(config, root);
|
|
168
206
|
break;
|
|
169
207
|
}
|
|
170
|
-
case
|
|
208
|
+
case "assign": {
|
|
171
209
|
const sprintPath = args[1];
|
|
172
210
|
const person = args[2];
|
|
173
211
|
if (!sprintPath || !person) {
|
|
174
|
-
console.error(
|
|
212
|
+
console.error("orchard: usage: orchard assign <sprint-path> <person>");
|
|
175
213
|
process.exit(1);
|
|
176
214
|
}
|
|
177
|
-
const { assignSprint } = require(
|
|
215
|
+
const { assignSprint } = require("../lib/assignments.js");
|
|
178
216
|
assignSprint(config, root, sprintPath, person);
|
|
179
217
|
break;
|
|
180
218
|
}
|
|
181
|
-
case
|
|
182
|
-
const { syncAll } = require(
|
|
219
|
+
case "sync": {
|
|
220
|
+
const { syncAll } = require("../lib/sync.js");
|
|
183
221
|
syncAll(config, root);
|
|
184
222
|
break;
|
|
185
223
|
}
|
|
186
|
-
case
|
|
187
|
-
const { generateDashboard } = require(
|
|
188
|
-
const outPath = args[1] || path.join(root,
|
|
224
|
+
case "dashboard": {
|
|
225
|
+
const { generateDashboard } = require("../lib/dashboard.js");
|
|
226
|
+
const outPath = args[1] || path.join(root, "orchard-dashboard.html");
|
|
189
227
|
generateDashboard(config, root, outPath);
|
|
190
228
|
break;
|
|
191
229
|
}
|
|
192
|
-
case
|
|
193
|
-
const {
|
|
230
|
+
case "conflicts": {
|
|
231
|
+
const {
|
|
232
|
+
detectConflicts,
|
|
233
|
+
filterBySeverity,
|
|
234
|
+
printConflicts,
|
|
235
|
+
} = require("../lib/conflicts.js");
|
|
236
|
+
const sevIdx = args.indexOf("--severity");
|
|
237
|
+
const severity =
|
|
238
|
+
sevIdx !== -1 && args[sevIdx + 1] ? args[sevIdx + 1] : "info";
|
|
239
|
+
if (jsonMode) {
|
|
240
|
+
const all = detectConflicts(config, root);
|
|
241
|
+
const filtered = filterBySeverity(all, severity);
|
|
242
|
+
console.log(
|
|
243
|
+
JSON.stringify(
|
|
244
|
+
{ conflicts: filtered, count: filtered.length },
|
|
245
|
+
null,
|
|
246
|
+
2,
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
printConflicts(config, root, { severity });
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case "decompose": {
|
|
255
|
+
const question = args
|
|
256
|
+
.filter((a) => !a.startsWith("--"))
|
|
257
|
+
.slice(1)
|
|
258
|
+
.join(" ");
|
|
259
|
+
if (!question) {
|
|
260
|
+
console.error(
|
|
261
|
+
'orchard: usage: orchard decompose "<question>" [--apply] [--max <n>]',
|
|
262
|
+
);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
const maxIdx = args.indexOf("--max");
|
|
266
|
+
const maxSprints =
|
|
267
|
+
maxIdx !== -1 && args[maxIdx + 1] ? parseInt(args[maxIdx + 1], 10) : 5;
|
|
268
|
+
if (args.includes("--apply")) {
|
|
269
|
+
const { applyDecomposition } = require("../lib/decompose.js");
|
|
270
|
+
const sprints = applyDecomposition(root, question, { maxSprints });
|
|
271
|
+
console.log(`Created ${sprints.length} sub-sprints for: "${question}"`);
|
|
272
|
+
for (const s of sprints) {
|
|
273
|
+
console.log(` ${s.path}`);
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
const { printDecomposition } = require("../lib/decompose.js");
|
|
277
|
+
printDecomposition(question, { maxSprints });
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "hackathon": {
|
|
282
|
+
const sub = args[1];
|
|
283
|
+
const hack = require("../lib/hackathon.js");
|
|
284
|
+
switch (sub) {
|
|
285
|
+
case "init": {
|
|
286
|
+
const nameIdx = args.indexOf("--name");
|
|
287
|
+
const durIdx = args.indexOf("--duration");
|
|
288
|
+
const name =
|
|
289
|
+
nameIdx !== -1 && args[nameIdx + 1] ? args[nameIdx + 1] : undefined;
|
|
290
|
+
const duration =
|
|
291
|
+
durIdx !== -1 && args[durIdx + 1]
|
|
292
|
+
? parseInt(args[durIdx + 1], 10)
|
|
293
|
+
: undefined;
|
|
294
|
+
const h = hack.initHackathon(root, { name, duration });
|
|
295
|
+
console.log(`Hackathon "${h.name}" started — ends at ${h.endTime}`);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "team": {
|
|
299
|
+
const teamName = args[2];
|
|
300
|
+
const qIdx = args.indexOf("--question");
|
|
301
|
+
const question =
|
|
302
|
+
qIdx !== -1 ? args.slice(qIdx + 1).join(" ") : undefined;
|
|
303
|
+
if (!teamName) {
|
|
304
|
+
console.error(
|
|
305
|
+
'orchard: usage: orchard hackathon team <name> [--question "..."]',
|
|
306
|
+
);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
const result = hack.addTeam(root, teamName, question);
|
|
310
|
+
console.log(
|
|
311
|
+
`Team "${result.teamName}" added — sprint: ${result.sprintPath}`,
|
|
312
|
+
);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case "end": {
|
|
316
|
+
const board = hack.endHackathon(root);
|
|
317
|
+
console.log("Hackathon ended! Final leaderboard:");
|
|
318
|
+
for (let i = 0; i < board.length; i++) {
|
|
319
|
+
const t = board[i];
|
|
320
|
+
console.log(
|
|
321
|
+
` ${i + 1}. ${t.team} — ${t.score}pts (${t.claimCount} claims)`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
default: {
|
|
327
|
+
if (jsonMode) {
|
|
328
|
+
const timer = hack.timerStatus(root);
|
|
329
|
+
const board = hack.leaderboard(root);
|
|
330
|
+
console.log(JSON.stringify({ timer, leaderboard: board }, null, 2));
|
|
331
|
+
} else {
|
|
332
|
+
hack.printHackathon(root);
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case "doctor": {
|
|
340
|
+
const { runChecks, printReport } = require("../lib/doctor.js");
|
|
194
341
|
const result = runChecks(root || process.cwd());
|
|
195
342
|
if (jsonMode) {
|
|
196
343
|
console.log(JSON.stringify(result, null, 2));
|
package/lib/assignments.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Assign a person to a sprint. Updates orchard.json.
|
|
@@ -11,7 +11,7 @@ function assignSprint(config, root, sprintPath, person) {
|
|
|
11
11
|
|
|
12
12
|
if (!sprint) {
|
|
13
13
|
console.error(`Sprint not found: ${sprintPath}`);
|
|
14
|
-
console.error(
|
|
14
|
+
console.error("Available sprints:");
|
|
15
15
|
for (const s of config.sprints || []) {
|
|
16
16
|
console.error(` ${s.path}`);
|
|
17
17
|
}
|
|
@@ -21,13 +21,15 @@ function assignSprint(config, root, sprintPath, person) {
|
|
|
21
21
|
const prev = sprint.assigned_to;
|
|
22
22
|
sprint.assigned_to = person;
|
|
23
23
|
|
|
24
|
-
const configPath = path.join(root,
|
|
25
|
-
const tmp = configPath +
|
|
26
|
-
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) +
|
|
24
|
+
const configPath = path.join(root, "orchard.json");
|
|
25
|
+
const tmp = configPath + ".tmp." + process.pid;
|
|
26
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
|
|
27
27
|
fs.renameSync(tmp, configPath);
|
|
28
28
|
|
|
29
29
|
if (prev) {
|
|
30
|
-
console.log(
|
|
30
|
+
console.log(
|
|
31
|
+
`Reassigned ${path.basename(sprintPath)}: ${prev} -> ${person}`,
|
|
32
|
+
);
|
|
31
33
|
} else {
|
|
32
34
|
console.log(`Assigned ${path.basename(sprintPath)} to ${person}`);
|
|
33
35
|
}
|
|
@@ -40,7 +42,7 @@ function getWorkload(config) {
|
|
|
40
42
|
const workload = new Map();
|
|
41
43
|
|
|
42
44
|
for (const sprint of config.sprints || []) {
|
|
43
|
-
const person = sprint.assigned_to ||
|
|
45
|
+
const person = sprint.assigned_to || "unassigned";
|
|
44
46
|
if (!workload.has(person)) {
|
|
45
47
|
workload.set(person, []);
|
|
46
48
|
}
|
|
@@ -56,20 +58,20 @@ function getWorkload(config) {
|
|
|
56
58
|
function printWorkload(config) {
|
|
57
59
|
const workload = getWorkload(config);
|
|
58
60
|
|
|
59
|
-
console.log(
|
|
60
|
-
console.log(
|
|
61
|
-
console.log(
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log(" Workload Distribution");
|
|
63
|
+
console.log(" " + "-".repeat(40));
|
|
62
64
|
|
|
63
65
|
for (const [person, sprints] of workload) {
|
|
64
|
-
const active = sprints.filter((s) => s.status !==
|
|
66
|
+
const active = sprints.filter((s) => s.status !== "done").length;
|
|
65
67
|
console.log(` ${person}: ${sprints.length} sprints (${active} active)`);
|
|
66
68
|
for (const s of sprints) {
|
|
67
|
-
const status = s.status ||
|
|
69
|
+
const status = s.status || "unknown";
|
|
68
70
|
console.log(` - ${path.basename(s.path)} [${status}]`);
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
console.log(
|
|
74
|
+
console.log("");
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/**
|
|
@@ -80,8 +82,8 @@ function findOverloaded(config, maxLoad = 3) {
|
|
|
80
82
|
const overloaded = [];
|
|
81
83
|
|
|
82
84
|
for (const [person, sprints] of workload) {
|
|
83
|
-
if (person ===
|
|
84
|
-
const active = sprints.filter((s) => s.status !==
|
|
85
|
+
if (person === "unassigned") continue;
|
|
86
|
+
const active = sprints.filter((s) => s.status !== "done").length;
|
|
85
87
|
if (active > maxLoad) {
|
|
86
88
|
overloaded.push({ person, active, sprints });
|
|
87
89
|
}
|