@bvdm/delano 0.2.3 → 0.2.5
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/.delano/viewer/README.md +3 -2
- package/.delano/viewer/public/app.js +13 -1
- package/.delano/viewer/public/app.jsx +2312 -0
- package/.delano/viewer/public/delano-mark.svg +4 -0
- package/.delano/viewer/public/index.html +12 -14
- package/.delano/viewer/public/styles.css +1005 -833
- package/.delano/viewer/server.js +46 -5
- package/README.md +63 -3
- package/assets/install-manifest.json +7 -0
- package/assets/payload/.agents/adapters/manifest.schema.json +103 -0
- package/assets/payload/.agents/adapters/spec-kit/adapter.json +71 -0
- package/assets/payload/.agents/hooks/README.md +6 -1
- package/assets/payload/.agents/hooks/codex-session-status.js +123 -0
- package/assets/payload/.agents/schemas/status-transitions.json +35 -0
- package/assets/payload/.agents/scripts/README.md +1 -1
- package/assets/payload/.agents/scripts/check-status-transitions.mjs +171 -2
- package/assets/payload/.agents/scripts/pm/import-spec-kit.sh +605 -0
- package/assets/payload/.agents/scripts/pm/init.sh +31 -2
- package/assets/payload/.agents/scripts/pm/research.sh +296 -0
- package/assets/payload/.agents/scripts/pm/status.sh +135 -28
- package/assets/payload/.agents/scripts/pm/validate.sh +16 -0
- package/assets/payload/.codex/hooks.json +17 -0
- package/assets/payload/.delano/viewer/README.md +3 -2
- package/assets/payload/.delano/viewer/public/app.js +13 -1
- package/assets/payload/.delano/viewer/public/index.html +12 -14
- package/assets/payload/.delano/viewer/public/styles.css +1005 -833
- package/assets/payload/.delano/viewer/server.js +46 -5
- package/assets/payload/.project/templates/decisions.md +18 -0
- package/assets/payload/.project/templates/plan.md +17 -0
- package/assets/payload/.project/templates/spec.md +12 -0
- package/assets/payload/.project/templates/task.md +6 -0
- package/assets/payload/.project/templates/workstream.md +1 -0
- package/package.json +4 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/state.js +689 -0
- package/src/cli/commands/viewer.js +2 -1
- package/src/cli/commands/wrapper.js +29 -5
- package/src/cli/index.js +120 -7
- package/src/cli/lib/install.js +179 -2
- package/src/cli/lib/project-state.js +918 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
|
|
3
|
+
const { CliError } = require("../lib/errors");
|
|
4
|
+
const {
|
|
5
|
+
addTaskFromTemplate,
|
|
6
|
+
addUpdateFromTemplate,
|
|
7
|
+
addWorkstreamFromTemplate,
|
|
8
|
+
applyProjectAction,
|
|
9
|
+
applyTaskAction,
|
|
10
|
+
applyWorkstreamAction,
|
|
11
|
+
createProjectFromTemplates,
|
|
12
|
+
loadProject,
|
|
13
|
+
nowIso,
|
|
14
|
+
parseCsvList,
|
|
15
|
+
requireDelanoRoot,
|
|
16
|
+
resolveTask,
|
|
17
|
+
resolveWorkstream,
|
|
18
|
+
writeChangedProjectFiles
|
|
19
|
+
} = require("../lib/project-state");
|
|
20
|
+
|
|
21
|
+
const PROJECT_ACTIONS = new Set(["start", "close", "block", "defer", "update"]);
|
|
22
|
+
const WORKSTREAM_ACTIONS = new Set(["start", "close", "block", "defer", "update"]);
|
|
23
|
+
const TASK_ACTIONS = new Set(["open", "start", "close", "block", "defer", "update"]);
|
|
24
|
+
|
|
25
|
+
function getProjectHelp() {
|
|
26
|
+
return [
|
|
27
|
+
"Usage:",
|
|
28
|
+
" delano project create <project-slug> [options]",
|
|
29
|
+
" delano project show <project-slug> [--json]",
|
|
30
|
+
" delano project <start|close|block|defer|update> <project-slug> [options]",
|
|
31
|
+
"",
|
|
32
|
+
"Creation renders from .project/templates/spec.md, plan.md, and decisions.md.",
|
|
33
|
+
"Lifecycle actions patch existing artifacts without regenerating templates.",
|
|
34
|
+
"",
|
|
35
|
+
"Create options:",
|
|
36
|
+
" --name <name> Project display name",
|
|
37
|
+
" --owner <owner> Project owner, defaults to team",
|
|
38
|
+
" --lead <lead> Plan lead, defaults to owner",
|
|
39
|
+
" --outcome <text> Outcome frontmatter, defaults to a scaffolded outcome",
|
|
40
|
+
" --uncertainty <low|medium|high> Defaults to medium",
|
|
41
|
+
" --probe-required <true|false> Defaults to false",
|
|
42
|
+
" --probe-status <status> Defaults to skipped or pending",
|
|
43
|
+
" --risk-level <low|medium|high> Defaults to uncertainty",
|
|
44
|
+
"",
|
|
45
|
+
"Lifecycle options:",
|
|
46
|
+
" --message <text> Update text for project update",
|
|
47
|
+
" --evidence <text> Evidence text for project close",
|
|
48
|
+
" --reason <text> Reason for start, block, or defer",
|
|
49
|
+
" --owner <owner> Blocking owner for block",
|
|
50
|
+
" --check-back <date> Blocking check-back date for block",
|
|
51
|
+
" --json Print a machine-readable summary",
|
|
52
|
+
" -h, --help Show help"
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getWorkstreamHelp() {
|
|
57
|
+
return [
|
|
58
|
+
"Usage:",
|
|
59
|
+
" delano workstream add <project-slug> <workstream-id> [options]",
|
|
60
|
+
" delano workstream show <project-slug> <workstream-id-or-file> [--json]",
|
|
61
|
+
" delano workstream <start|close|block|defer|update> <project-slug> <workstream-id-or-file> [options]",
|
|
62
|
+
"",
|
|
63
|
+
"Add renders from .project/templates/workstream.md.",
|
|
64
|
+
"Lifecycle actions patch existing artifacts without regenerating templates.",
|
|
65
|
+
"",
|
|
66
|
+
"Add options:",
|
|
67
|
+
" --name <name> Workstream name, for example API Foundation",
|
|
68
|
+
" --owner <owner> Workstream owner, defaults to team",
|
|
69
|
+
"",
|
|
70
|
+
"Lifecycle options:",
|
|
71
|
+
" --message <text> Update text for workstream update",
|
|
72
|
+
" --evidence <text> Evidence text for workstream close",
|
|
73
|
+
" --reason <text> Reason for start, block, or defer",
|
|
74
|
+
" --owner <owner> Blocking owner for block",
|
|
75
|
+
" --check-back <date> Blocking check-back date for block",
|
|
76
|
+
" --json Print a machine-readable summary",
|
|
77
|
+
" -h, --help Show help"
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getTaskHelp() {
|
|
82
|
+
return [
|
|
83
|
+
"Usage:",
|
|
84
|
+
" delano task add <project-slug> <task-id> --name <name> --workstream <workstream-id> [options]",
|
|
85
|
+
" delano task <open|start|close|block|defer|update> <project-slug> <task-id-or-file> [options]",
|
|
86
|
+
" delano task <open|start|close|block|defer|update> <task-id-or-file> --project <project-slug> [options]",
|
|
87
|
+
"",
|
|
88
|
+
"Add renders from .project/templates/task.md.",
|
|
89
|
+
"Lifecycle actions patch one task and apply scoped rollups without full validation.",
|
|
90
|
+
"",
|
|
91
|
+
"Add options:",
|
|
92
|
+
" --name <name> Task title",
|
|
93
|
+
" --workstream <id> Parent workstream, for example WS-A",
|
|
94
|
+
" --description <text> Optional description body",
|
|
95
|
+
" --acceptance <text> Acceptance criterion; repeatable",
|
|
96
|
+
" --depends-on <ids> Comma-separated task dependencies",
|
|
97
|
+
" --conflicts-with <values> Comma-separated conflict zones",
|
|
98
|
+
" --parallel <true|false> Defaults to true",
|
|
99
|
+
" --priority <low|medium|high> Defaults to medium",
|
|
100
|
+
" --estimate <S|M|L|XL> Defaults to M",
|
|
101
|
+
" --story <id> Story id",
|
|
102
|
+
" --acceptance-criteria <ids> Comma-separated acceptance criteria ids",
|
|
103
|
+
"",
|
|
104
|
+
"Lifecycle options:",
|
|
105
|
+
" --project <slug> Project slug when not passed positionally",
|
|
106
|
+
" --message <text> Evidence text for update",
|
|
107
|
+
" --evidence <text> Evidence log entry for close",
|
|
108
|
+
" --owner <owner> Blocking owner for block",
|
|
109
|
+
" --check-back <date> Blocking check-back date for block",
|
|
110
|
+
" --reason <text> Reason for open, block, defer, start, or close",
|
|
111
|
+
" --json Print a machine-readable summary",
|
|
112
|
+
" -h, --help Show help",
|
|
113
|
+
"",
|
|
114
|
+
"Rollups:",
|
|
115
|
+
" - start/close promotes planned project and workstream lifecycle to active.",
|
|
116
|
+
" - open reopens closed parent lifecycle when reopening a closed task.",
|
|
117
|
+
" - close/defer marks an affected workstream done when it has no open tasks.",
|
|
118
|
+
" - close/defer marks spec complete and plan done when the project has no open tasks."
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getUpdateHelp() {
|
|
123
|
+
return [
|
|
124
|
+
"Usage:",
|
|
125
|
+
" delano update add <project-slug> --message <text> [options]",
|
|
126
|
+
"",
|
|
127
|
+
"Add renders from .project/templates/progress-update.md.",
|
|
128
|
+
"",
|
|
129
|
+
"Options:",
|
|
130
|
+
" --message <text> Progress update text",
|
|
131
|
+
" --status <status> Defaults to in-progress",
|
|
132
|
+
" --task <task-id> Related task id",
|
|
133
|
+
" --stream <workstream-id> Related workstream id",
|
|
134
|
+
" --section <section> completed, in-progress, blockers, or next",
|
|
135
|
+
" --title <title> Filename title override",
|
|
136
|
+
" --json Print a machine-readable summary",
|
|
137
|
+
" -h, --help Show help"
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function runProjectCommand(args) {
|
|
142
|
+
const [action, ...rest] = args;
|
|
143
|
+
if (action === "create") {
|
|
144
|
+
return runProjectCreate(rest);
|
|
145
|
+
}
|
|
146
|
+
if (action === "show") {
|
|
147
|
+
return runProjectShow(rest);
|
|
148
|
+
}
|
|
149
|
+
if (PROJECT_ACTIONS.has(action)) {
|
|
150
|
+
return runProjectLifecycle(action, rest);
|
|
151
|
+
}
|
|
152
|
+
throw new CliError(`${getProjectHelp()}\n\nError: project action must be create, show, start, close, block, defer, or update.`, 1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function runWorkstreamCommand(args) {
|
|
156
|
+
const [action, ...rest] = args;
|
|
157
|
+
if (action === "add") {
|
|
158
|
+
return runWorkstreamAdd(rest);
|
|
159
|
+
}
|
|
160
|
+
if (action === "show") {
|
|
161
|
+
return runWorkstreamShow(rest);
|
|
162
|
+
}
|
|
163
|
+
if (WORKSTREAM_ACTIONS.has(action)) {
|
|
164
|
+
return runWorkstreamLifecycle(action, rest);
|
|
165
|
+
}
|
|
166
|
+
throw new CliError(`${getWorkstreamHelp()}\n\nError: workstream action must be add, show, start, close, block, defer, or update.`, 1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function runTaskCommand(args) {
|
|
170
|
+
const [action, ...rest] = args;
|
|
171
|
+
if (action === "add") {
|
|
172
|
+
return runTaskAdd(rest);
|
|
173
|
+
}
|
|
174
|
+
if (TASK_ACTIONS.has(action)) {
|
|
175
|
+
return runTaskLifecycle(action, rest);
|
|
176
|
+
}
|
|
177
|
+
throw new CliError(`${getTaskHelp()}\n\nError: task action must be add, open, start, close, block, defer, or update.`, 1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function runUpdateCommand(args) {
|
|
181
|
+
const [action, ...rest] = args;
|
|
182
|
+
if (action !== "add") {
|
|
183
|
+
throw new CliError(`${getUpdateHelp()}\n\nError: update action must be add.`, 1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const options = parseUpdateAddArgs(rest);
|
|
187
|
+
const root = requireDelanoRoot();
|
|
188
|
+
const result = addUpdateFromTemplate(root, options);
|
|
189
|
+
const summary = {
|
|
190
|
+
ok: true,
|
|
191
|
+
command: "update",
|
|
192
|
+
action: "add",
|
|
193
|
+
project: options.project,
|
|
194
|
+
files: result.files
|
|
195
|
+
};
|
|
196
|
+
printResult(summary, options.json, `Created update ${result.files[0]}.`);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function runProjectCreate(args) {
|
|
201
|
+
const options = parseProjectCreateArgs(args);
|
|
202
|
+
const root = requireDelanoRoot();
|
|
203
|
+
const result = createProjectFromTemplates(root, options);
|
|
204
|
+
const summary = {
|
|
205
|
+
ok: true,
|
|
206
|
+
command: "project",
|
|
207
|
+
action: "create",
|
|
208
|
+
project: result.slug,
|
|
209
|
+
files: result.files
|
|
210
|
+
};
|
|
211
|
+
printResult(summary, options.json, `Created project ${result.slug}.`);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function runProjectShow(args) {
|
|
216
|
+
const options = parseProjectRefArgs(args);
|
|
217
|
+
const root = requireDelanoRoot();
|
|
218
|
+
const project = loadProject(root, options.project);
|
|
219
|
+
const summary = summarizeProject(project, options.project);
|
|
220
|
+
printResult(summary, options.json, `Project ${options.project}: spec=${summary.spec_status || "missing"} plan=${summary.plan_status || "missing"} tasks=${summary.tasks}.`);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function runProjectLifecycle(action, args) {
|
|
225
|
+
const options = parseProjectLifecycleArgs(action, args);
|
|
226
|
+
const root = requireDelanoRoot();
|
|
227
|
+
const project = loadProject(root, options.project);
|
|
228
|
+
const changes = applyProjectAction(project, options);
|
|
229
|
+
const summary = {
|
|
230
|
+
ok: true,
|
|
231
|
+
command: "project",
|
|
232
|
+
action,
|
|
233
|
+
project: options.project,
|
|
234
|
+
changes
|
|
235
|
+
};
|
|
236
|
+
printResult(summary, options.json, `Project ${options.project} patched.`);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function runWorkstreamAdd(args) {
|
|
241
|
+
const options = parseWorkstreamAddArgs(args);
|
|
242
|
+
const root = requireDelanoRoot();
|
|
243
|
+
const result = addWorkstreamFromTemplate(root, options);
|
|
244
|
+
const summary = {
|
|
245
|
+
ok: true,
|
|
246
|
+
command: "workstream",
|
|
247
|
+
action: "add",
|
|
248
|
+
project: options.project,
|
|
249
|
+
workstream: result.id,
|
|
250
|
+
files: result.files
|
|
251
|
+
};
|
|
252
|
+
printResult(summary, options.json, `Created workstream ${result.id}.`);
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function runWorkstreamShow(args) {
|
|
257
|
+
const options = parseWorkstreamRefArgs(args);
|
|
258
|
+
const root = requireDelanoRoot();
|
|
259
|
+
const project = loadProject(root, options.project);
|
|
260
|
+
const workstream = resolveWorkstream(project, options.workstream);
|
|
261
|
+
const summary = {
|
|
262
|
+
ok: true,
|
|
263
|
+
command: "workstream",
|
|
264
|
+
action: "show",
|
|
265
|
+
project: options.project,
|
|
266
|
+
workstream: workstream.frontmatter.id || path.basename(workstream.path, ".md"),
|
|
267
|
+
name: workstream.frontmatter.name || "",
|
|
268
|
+
status: workstream.frontmatter.status || "",
|
|
269
|
+
file: path.relative(project.projectDir, workstream.path).replace(/\\/g, "/")
|
|
270
|
+
};
|
|
271
|
+
printResult(summary, options.json, `Workstream ${summary.workstream}: ${summary.status || "missing status"}.`);
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function runWorkstreamLifecycle(action, args) {
|
|
276
|
+
const options = parseWorkstreamLifecycleArgs(action, args);
|
|
277
|
+
const root = requireDelanoRoot();
|
|
278
|
+
const project = loadProject(root, options.project);
|
|
279
|
+
const workstream = resolveWorkstream(project, options.workstream);
|
|
280
|
+
const changes = applyWorkstreamAction(project, workstream, options);
|
|
281
|
+
const summary = {
|
|
282
|
+
ok: true,
|
|
283
|
+
command: "workstream",
|
|
284
|
+
action,
|
|
285
|
+
project: options.project,
|
|
286
|
+
workstream: workstream.frontmatter.id || path.basename(workstream.path, ".md"),
|
|
287
|
+
status: workstream.frontmatter.status,
|
|
288
|
+
changes
|
|
289
|
+
};
|
|
290
|
+
printResult(summary, options.json, `Workstream ${summary.workstream} is now ${summary.status}.`);
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function runTaskAdd(args) {
|
|
295
|
+
const options = parseTaskAddArgs(args);
|
|
296
|
+
const root = requireDelanoRoot();
|
|
297
|
+
const result = addTaskFromTemplate(root, options);
|
|
298
|
+
const summary = {
|
|
299
|
+
ok: true,
|
|
300
|
+
command: "task",
|
|
301
|
+
action: "add",
|
|
302
|
+
project: options.project,
|
|
303
|
+
task: result.id,
|
|
304
|
+
files: result.files,
|
|
305
|
+
changes: result.changes
|
|
306
|
+
};
|
|
307
|
+
printResult(summary, options.json, `Created task ${result.id}.`);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function runTaskLifecycle(action, args) {
|
|
312
|
+
const options = parseTaskArgs([action, ...args]);
|
|
313
|
+
const root = requireDelanoRoot();
|
|
314
|
+
const project = loadProject(root, options.project);
|
|
315
|
+
const task = resolveTask(project, options.task);
|
|
316
|
+
const changes = [];
|
|
317
|
+
applyTaskAction({
|
|
318
|
+
project,
|
|
319
|
+
task,
|
|
320
|
+
action: options.action,
|
|
321
|
+
timestamp: options.now || nowIso(),
|
|
322
|
+
evidence: options.evidence,
|
|
323
|
+
owner: options.owner,
|
|
324
|
+
checkBack: options.checkBack,
|
|
325
|
+
reason: options.reason,
|
|
326
|
+
message: options.message,
|
|
327
|
+
changes
|
|
328
|
+
});
|
|
329
|
+
writeChangedProjectFiles(project);
|
|
330
|
+
const summary = {
|
|
331
|
+
ok: true,
|
|
332
|
+
command: "task",
|
|
333
|
+
action,
|
|
334
|
+
project: options.project,
|
|
335
|
+
task: task.frontmatter.id || path.basename(task.path, ".md"),
|
|
336
|
+
status: task.frontmatter.status,
|
|
337
|
+
changes
|
|
338
|
+
};
|
|
339
|
+
printResult(summary, options.json, `Task ${summary.task} is now ${summary.status}.`);
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseProjectCreateArgs(args) {
|
|
344
|
+
const options = {
|
|
345
|
+
slug: "",
|
|
346
|
+
name: "",
|
|
347
|
+
owner: "team",
|
|
348
|
+
lead: "",
|
|
349
|
+
outcome: "",
|
|
350
|
+
uncertainty: "medium",
|
|
351
|
+
probeRequired: "false",
|
|
352
|
+
probeStatus: "",
|
|
353
|
+
riskLevel: "",
|
|
354
|
+
json: false
|
|
355
|
+
};
|
|
356
|
+
const positional = [];
|
|
357
|
+
|
|
358
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
359
|
+
const value = args[index];
|
|
360
|
+
if (value === "--json") options.json = true;
|
|
361
|
+
else if (value === "--name") options.name = requireValue(args, index, value), index += 1;
|
|
362
|
+
else if (value === "--owner") options.owner = requireValue(args, index, value), index += 1;
|
|
363
|
+
else if (value === "--lead") options.lead = requireValue(args, index, value), index += 1;
|
|
364
|
+
else if (value === "--outcome") options.outcome = requireValue(args, index, value), index += 1;
|
|
365
|
+
else if (value === "--uncertainty") options.uncertainty = requireValue(args, index, value), index += 1;
|
|
366
|
+
else if (value === "--probe-required") options.probeRequired = requireValue(args, index, value), index += 1;
|
|
367
|
+
else if (value === "--probe-status") options.probeStatus = requireValue(args, index, value), index += 1;
|
|
368
|
+
else if (value === "--risk-level") options.riskLevel = requireValue(args, index, value), index += 1;
|
|
369
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown project create option: ${value}`, 1);
|
|
370
|
+
else positional.push(value);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
options.slug = positional[0] || "";
|
|
374
|
+
if (!options.slug) {
|
|
375
|
+
throw new CliError(`${getProjectHelp()}\n\nError: project slug is required.`, 1);
|
|
376
|
+
}
|
|
377
|
+
if (positional.length > 1) {
|
|
378
|
+
throw new CliError("Too many project create arguments. Use delano project create <project-slug>.", 1);
|
|
379
|
+
}
|
|
380
|
+
return options;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function parseProjectRefArgs(args) {
|
|
384
|
+
const options = { project: "", json: false };
|
|
385
|
+
const positional = [];
|
|
386
|
+
for (const value of args) {
|
|
387
|
+
if (value === "--json") options.json = true;
|
|
388
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown project option: ${value}`, 1);
|
|
389
|
+
else positional.push(value);
|
|
390
|
+
}
|
|
391
|
+
options.project = positional[0] || "";
|
|
392
|
+
if (!options.project) throw new CliError("project slug is required.", 1);
|
|
393
|
+
if (positional.length > 1) throw new CliError("Too many project arguments.", 1);
|
|
394
|
+
return options;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function parseProjectLifecycleArgs(action, args) {
|
|
398
|
+
const options = {
|
|
399
|
+
action,
|
|
400
|
+
project: "",
|
|
401
|
+
message: "",
|
|
402
|
+
evidence: "",
|
|
403
|
+
reason: "",
|
|
404
|
+
owner: "",
|
|
405
|
+
checkBack: "",
|
|
406
|
+
json: false
|
|
407
|
+
};
|
|
408
|
+
const positional = [];
|
|
409
|
+
|
|
410
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
411
|
+
const value = args[index];
|
|
412
|
+
if (value === "--json") options.json = true;
|
|
413
|
+
else if (value === "--message") options.message = requireValue(args, index, value), index += 1;
|
|
414
|
+
else if (value === "--evidence") options.evidence = requireValue(args, index, value), index += 1;
|
|
415
|
+
else if (value === "--reason") options.reason = requireValue(args, index, value), index += 1;
|
|
416
|
+
else if (value === "--owner") options.owner = requireValue(args, index, value), index += 1;
|
|
417
|
+
else if (value === "--check-back") options.checkBack = requireValue(args, index, value), index += 1;
|
|
418
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown project ${action} option: ${value}`, 1);
|
|
419
|
+
else positional.push(value);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
options.project = positional[0] || "";
|
|
423
|
+
if (!options.project) throw new CliError(`delano project ${action} requires a project slug.`, 1);
|
|
424
|
+
if (positional.length > 1) throw new CliError(`Too many project ${action} arguments.`, 1);
|
|
425
|
+
if (action === "block" && (!options.owner || !options.checkBack)) {
|
|
426
|
+
throw new CliError("delano project block requires --owner and --check-back.", 1);
|
|
427
|
+
}
|
|
428
|
+
if (action === "update" && !options.message) {
|
|
429
|
+
throw new CliError("delano project update requires --message.", 1);
|
|
430
|
+
}
|
|
431
|
+
return options;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function parseWorkstreamAddArgs(args) {
|
|
435
|
+
const options = { project: "", id: "", name: "", owner: "team", json: false };
|
|
436
|
+
const positional = [];
|
|
437
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
438
|
+
const value = args[index];
|
|
439
|
+
if (value === "--json") options.json = true;
|
|
440
|
+
else if (value === "--name") options.name = requireValue(args, index, value), index += 1;
|
|
441
|
+
else if (value === "--owner") options.owner = requireValue(args, index, value), index += 1;
|
|
442
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown workstream add option: ${value}`, 1);
|
|
443
|
+
else positional.push(value);
|
|
444
|
+
}
|
|
445
|
+
options.project = positional[0] || "";
|
|
446
|
+
options.id = positional[1] || "";
|
|
447
|
+
if (!options.project || !options.id) {
|
|
448
|
+
throw new CliError(`${getWorkstreamHelp()}\n\nError: project slug and workstream id are required.`, 1);
|
|
449
|
+
}
|
|
450
|
+
if (positional.length > 2) throw new CliError("Too many workstream add arguments.", 1);
|
|
451
|
+
return options;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function parseWorkstreamRefArgs(args) {
|
|
455
|
+
const options = { project: "", workstream: "", json: false };
|
|
456
|
+
const positional = [];
|
|
457
|
+
for (const value of args) {
|
|
458
|
+
if (value === "--json") options.json = true;
|
|
459
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown workstream option: ${value}`, 1);
|
|
460
|
+
else positional.push(value);
|
|
461
|
+
}
|
|
462
|
+
options.project = positional[0] || "";
|
|
463
|
+
options.workstream = positional[1] || "";
|
|
464
|
+
if (!options.project || !options.workstream) throw new CliError("project slug and workstream reference are required.", 1);
|
|
465
|
+
if (positional.length > 2) throw new CliError("Too many workstream arguments.", 1);
|
|
466
|
+
return options;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function parseWorkstreamLifecycleArgs(action, args) {
|
|
470
|
+
const options = {
|
|
471
|
+
action,
|
|
472
|
+
project: "",
|
|
473
|
+
workstream: "",
|
|
474
|
+
message: "",
|
|
475
|
+
evidence: "",
|
|
476
|
+
reason: "",
|
|
477
|
+
owner: "",
|
|
478
|
+
checkBack: "",
|
|
479
|
+
json: false
|
|
480
|
+
};
|
|
481
|
+
const positional = [];
|
|
482
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
483
|
+
const value = args[index];
|
|
484
|
+
if (value === "--json") options.json = true;
|
|
485
|
+
else if (value === "--message") options.message = requireValue(args, index, value), index += 1;
|
|
486
|
+
else if (value === "--evidence") options.evidence = requireValue(args, index, value), index += 1;
|
|
487
|
+
else if (value === "--reason") options.reason = requireValue(args, index, value), index += 1;
|
|
488
|
+
else if (value === "--owner") options.owner = requireValue(args, index, value), index += 1;
|
|
489
|
+
else if (value === "--check-back") options.checkBack = requireValue(args, index, value), index += 1;
|
|
490
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown workstream ${action} option: ${value}`, 1);
|
|
491
|
+
else positional.push(value);
|
|
492
|
+
}
|
|
493
|
+
options.project = positional[0] || "";
|
|
494
|
+
options.workstream = positional[1] || "";
|
|
495
|
+
if (!options.project || !options.workstream) throw new CliError(`delano workstream ${action} requires project slug and workstream reference.`, 1);
|
|
496
|
+
if (positional.length > 2) throw new CliError(`Too many workstream ${action} arguments.`, 1);
|
|
497
|
+
if (action === "block" && (!options.owner || !options.checkBack)) {
|
|
498
|
+
throw new CliError("delano workstream block requires --owner and --check-back.", 1);
|
|
499
|
+
}
|
|
500
|
+
if (action === "update" && !options.message) {
|
|
501
|
+
throw new CliError("delano workstream update requires --message.", 1);
|
|
502
|
+
}
|
|
503
|
+
return options;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function parseTaskAddArgs(args) {
|
|
507
|
+
const options = {
|
|
508
|
+
project: "",
|
|
509
|
+
id: "",
|
|
510
|
+
name: "",
|
|
511
|
+
workstream: "",
|
|
512
|
+
description: "",
|
|
513
|
+
acceptanceCriteria: [],
|
|
514
|
+
dependsOn: [],
|
|
515
|
+
conflictsWith: [],
|
|
516
|
+
parallel: "true",
|
|
517
|
+
priority: "medium",
|
|
518
|
+
estimate: "M",
|
|
519
|
+
storyId: "",
|
|
520
|
+
acceptanceCriteriaIds: [],
|
|
521
|
+
json: false
|
|
522
|
+
};
|
|
523
|
+
const positional = [];
|
|
524
|
+
|
|
525
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
526
|
+
const value = args[index];
|
|
527
|
+
if (value === "--json") options.json = true;
|
|
528
|
+
else if (value === "--name") options.name = requireValue(args, index, value), index += 1;
|
|
529
|
+
else if (value === "--workstream") options.workstream = requireValue(args, index, value), index += 1;
|
|
530
|
+
else if (value === "--description") options.description = requireValue(args, index, value), index += 1;
|
|
531
|
+
else if (value === "--acceptance") options.acceptanceCriteria.push(requireValue(args, index, value)), index += 1;
|
|
532
|
+
else if (value === "--depends-on") options.dependsOn = parseCsvList(requireValue(args, index, value)), index += 1;
|
|
533
|
+
else if (value === "--conflicts-with") options.conflictsWith = parseCsvList(requireValue(args, index, value)), index += 1;
|
|
534
|
+
else if (value === "--parallel") options.parallel = requireValue(args, index, value), index += 1;
|
|
535
|
+
else if (value === "--priority") options.priority = requireValue(args, index, value), index += 1;
|
|
536
|
+
else if (value === "--estimate") options.estimate = requireValue(args, index, value), index += 1;
|
|
537
|
+
else if (value === "--story") options.storyId = requireValue(args, index, value), index += 1;
|
|
538
|
+
else if (value === "--acceptance-criteria") options.acceptanceCriteriaIds = parseCsvList(requireValue(args, index, value)), index += 1;
|
|
539
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown task add option: ${value}`, 1);
|
|
540
|
+
else positional.push(value);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
options.project = positional[0] || "";
|
|
544
|
+
options.id = positional[1] || "";
|
|
545
|
+
if (!options.project || !options.id || !options.name || !options.workstream) {
|
|
546
|
+
throw new CliError(`${getTaskHelp()}\n\nError: project slug, task id, --name, and --workstream are required for task add.`, 1);
|
|
547
|
+
}
|
|
548
|
+
if (positional.length > 2) throw new CliError("Too many task add arguments.", 1);
|
|
549
|
+
return options;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function parseTaskArgs(args) {
|
|
553
|
+
const [action, ...rest] = args;
|
|
554
|
+
if (!TASK_ACTIONS.has(action)) {
|
|
555
|
+
throw new CliError(`${getTaskHelp()}\n\nError: task action must be one of open, start, close, block, defer, or update.`, 1);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const options = {
|
|
559
|
+
action,
|
|
560
|
+
project: "",
|
|
561
|
+
task: "",
|
|
562
|
+
evidence: "",
|
|
563
|
+
owner: "",
|
|
564
|
+
checkBack: "",
|
|
565
|
+
reason: "",
|
|
566
|
+
message: "",
|
|
567
|
+
json: false
|
|
568
|
+
};
|
|
569
|
+
const positional = [];
|
|
570
|
+
|
|
571
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
572
|
+
const value = rest[index];
|
|
573
|
+
if (value === "--json") options.json = true;
|
|
574
|
+
else if (value === "--project") options.project = requireValue(rest, index, value), index += 1;
|
|
575
|
+
else if (value === "--evidence") options.evidence = requireValue(rest, index, value), index += 1;
|
|
576
|
+
else if (value === "--owner") options.owner = requireValue(rest, index, value), index += 1;
|
|
577
|
+
else if (value === "--check-back") options.checkBack = requireValue(rest, index, value), index += 1;
|
|
578
|
+
else if (value === "--reason") options.reason = requireValue(rest, index, value), index += 1;
|
|
579
|
+
else if (value === "--message") options.message = requireValue(rest, index, value), index += 1;
|
|
580
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown task option: ${value}`, 1);
|
|
581
|
+
else positional.push(value);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (options.project) {
|
|
585
|
+
options.task = positional[0] || "";
|
|
586
|
+
if (positional.length > 1) {
|
|
587
|
+
throw new CliError("Too many task arguments. Use either <project> <task> or <task> --project <project>.", 1);
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
options.project = positional[0] || "";
|
|
591
|
+
options.task = positional[1] || "";
|
|
592
|
+
if (positional.length > 2) {
|
|
593
|
+
throw new CliError("Too many task arguments. Use delano task <action> <project> <task>.", 1);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (!options.project || !options.task) {
|
|
598
|
+
throw new CliError(`${getTaskHelp()}\n\nError: project and task are required.`, 1);
|
|
599
|
+
}
|
|
600
|
+
if (action === "block" && (!options.owner || !options.checkBack)) {
|
|
601
|
+
throw new CliError("delano task block requires --owner and --check-back.", 1);
|
|
602
|
+
}
|
|
603
|
+
if (action === "close" && !options.evidence) {
|
|
604
|
+
throw new CliError("delano task close requires --evidence so the status change has a local proof trail.", 1);
|
|
605
|
+
}
|
|
606
|
+
if (action === "update" && !options.message) {
|
|
607
|
+
throw new CliError("delano task update requires --message.", 1);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return options;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function parseUpdateAddArgs(args) {
|
|
614
|
+
const options = {
|
|
615
|
+
project: "",
|
|
616
|
+
message: "",
|
|
617
|
+
status: "in-progress",
|
|
618
|
+
task: "",
|
|
619
|
+
stream: "",
|
|
620
|
+
section: "",
|
|
621
|
+
title: "",
|
|
622
|
+
json: false
|
|
623
|
+
};
|
|
624
|
+
const positional = [];
|
|
625
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
626
|
+
const value = args[index];
|
|
627
|
+
if (value === "--json") options.json = true;
|
|
628
|
+
else if (value === "--message") options.message = requireValue(args, index, value), index += 1;
|
|
629
|
+
else if (value === "--status") options.status = requireValue(args, index, value), index += 1;
|
|
630
|
+
else if (value === "--task") options.task = requireValue(args, index, value), index += 1;
|
|
631
|
+
else if (value === "--stream") options.stream = requireValue(args, index, value), index += 1;
|
|
632
|
+
else if (value === "--section") options.section = requireValue(args, index, value), index += 1;
|
|
633
|
+
else if (value === "--title") options.title = requireValue(args, index, value), index += 1;
|
|
634
|
+
else if (value.startsWith("-")) throw new CliError(`Unknown update add option: ${value}`, 1);
|
|
635
|
+
else positional.push(value);
|
|
636
|
+
}
|
|
637
|
+
options.project = positional[0] || "";
|
|
638
|
+
if (!options.project) throw new CliError(`${getUpdateHelp()}\n\nError: project slug is required.`, 1);
|
|
639
|
+
if (positional.length > 1) throw new CliError("Too many update add arguments.", 1);
|
|
640
|
+
return options;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function summarizeProject(project, slug) {
|
|
644
|
+
return {
|
|
645
|
+
ok: true,
|
|
646
|
+
command: "project",
|
|
647
|
+
action: "show",
|
|
648
|
+
project: slug,
|
|
649
|
+
spec_status: project.spec?.frontmatter.status || "",
|
|
650
|
+
plan_status: project.plan?.frontmatter.status || "",
|
|
651
|
+
workstreams: project.workstreams.length,
|
|
652
|
+
tasks: project.tasks.length
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function requireValue(values, index, flag) {
|
|
657
|
+
const value = values[index + 1];
|
|
658
|
+
if (value === undefined || value === "") {
|
|
659
|
+
throw new CliError(`${flag} requires a value.`, 1);
|
|
660
|
+
}
|
|
661
|
+
return value;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function printResult(result, json, message) {
|
|
665
|
+
if (json) {
|
|
666
|
+
console.log(JSON.stringify(result));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
console.log(message);
|
|
671
|
+
for (const change of result.changes || []) {
|
|
672
|
+
console.log(`- ${change}`);
|
|
673
|
+
}
|
|
674
|
+
for (const file of result.files || []) {
|
|
675
|
+
console.log(`- ${file}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
module.exports = {
|
|
680
|
+
getProjectHelp,
|
|
681
|
+
getTaskHelp,
|
|
682
|
+
getUpdateHelp,
|
|
683
|
+
getWorkstreamHelp,
|
|
684
|
+
parseTaskArgs,
|
|
685
|
+
runProjectCommand,
|
|
686
|
+
runTaskCommand,
|
|
687
|
+
runUpdateCommand,
|
|
688
|
+
runWorkstreamCommand
|
|
689
|
+
};
|
|
@@ -43,7 +43,8 @@ function getViewerHelp() {
|
|
|
43
43
|
" -h, --help Show help",
|
|
44
44
|
"",
|
|
45
45
|
"Environment:",
|
|
46
|
-
" DELANO_VIEWER_PORT or PORT
|
|
46
|
+
" DELANO_VIEWER_PORT or PORT sets the starting port, defaulting to 3977.",
|
|
47
|
+
" If that port is busy, the viewer starts on the next available port."
|
|
47
48
|
].join("\n");
|
|
48
49
|
}
|
|
49
50
|
|