nikos 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +8 -0
- package/dist/commands/init.js +8 -1
- package/dist/commands/release.js +45 -12
- package/docs/format.md +2 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { runValidate } from "./commands/validate.js";
|
|
3
4
|
import { runPlan } from "./commands/plan.js";
|
|
4
5
|
import { runInit } from "./commands/init.js";
|
|
@@ -47,6 +48,7 @@ Commands:
|
|
|
47
48
|
release rename <n> Change a release's title or goal (--goal)
|
|
48
49
|
release move <id> <n> Move a story to another release, retagging its feature
|
|
49
50
|
release reorder <n> <pos> Move a release and renumber them all
|
|
51
|
+
release remove <n> Remove an empty release and renumber the rest
|
|
50
52
|
plan new <id> Start the next version of a story's plan
|
|
51
53
|
clear <id> Check a story is Lean and Executable before building
|
|
52
54
|
verify setup Set up a Cucumber runner for this repo's stack
|
|
@@ -63,6 +65,8 @@ Commands:
|
|
|
63
65
|
planning <cmd> Bets, key results and the planning dashboard (nikos-planning pack;
|
|
64
66
|
run \`nikos planning help\`)
|
|
65
67
|
|
|
68
|
+
--version Print the installed version
|
|
69
|
+
|
|
66
70
|
Docs: https://nikos.tools`;
|
|
67
71
|
async function main(argv) {
|
|
68
72
|
const [cmd, ...rest] = argv;
|
|
@@ -70,6 +74,10 @@ async function main(argv) {
|
|
|
70
74
|
console.log(help);
|
|
71
75
|
return 0;
|
|
72
76
|
}
|
|
77
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
78
|
+
console.log(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
73
81
|
const run = commands[cmd];
|
|
74
82
|
if (!run) {
|
|
75
83
|
console.error(`Unknown command '${cmd}'.\n\n${help}`);
|
package/dist/commands/init.js
CHANGED
|
@@ -44,10 +44,17 @@ export function init(root, opts = {}) {
|
|
|
44
44
|
}
|
|
45
45
|
return { intentDir, created, skipped, validation: validate(intentDir) };
|
|
46
46
|
}
|
|
47
|
+
const EXAMPLES = { name: '"Hello Todo"', key: "HT", dir: "." };
|
|
47
48
|
export function runInit(args) {
|
|
48
49
|
const opt = (n) => {
|
|
49
50
|
const i = args.indexOf(`--${n}`);
|
|
50
|
-
|
|
51
|
+
if (i < 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
const value = args[i + 1];
|
|
54
|
+
// Without this, a bare --name falls back to the folder name and the key changes silently.
|
|
55
|
+
if (value === undefined || value.startsWith("--") || !value.trim())
|
|
56
|
+
throw new Error(`--${n} needs a value, e.g. --${n} ${EXAMPLES[n] ?? "<value>"}`);
|
|
57
|
+
return value;
|
|
51
58
|
};
|
|
52
59
|
try {
|
|
53
60
|
const r = init(opt("dir") ?? process.cwd(), { name: opt("name"), key: opt("key") });
|
package/dist/commands/release.js
CHANGED
|
@@ -36,6 +36,18 @@ function retag(intentDir, moved) {
|
|
|
36
36
|
writeFileSync(file, setFeatureTag(text, "release", String(moved.get(id)), id));
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
/** Number the releases 1, 2, 3… in list order, retagging the stories of any that changed. */
|
|
40
|
+
function renumber(o) {
|
|
41
|
+
const moved = new Map();
|
|
42
|
+
o.releases.items.forEach((item, i) => {
|
|
43
|
+
if (numberOf(item) === i + 1)
|
|
44
|
+
return;
|
|
45
|
+
item.set("number", i + 1);
|
|
46
|
+
for (const id of storiesOf(item))
|
|
47
|
+
moved.set(id, i + 1);
|
|
48
|
+
});
|
|
49
|
+
retag(o.intentDir, moved);
|
|
50
|
+
}
|
|
39
51
|
function save(o) {
|
|
40
52
|
writeFileSync(o.mapPath, o.map.toString(yamlOpts));
|
|
41
53
|
return validate(o.intentDir);
|
|
@@ -46,12 +58,19 @@ const cleanTitle = (title) => {
|
|
|
46
58
|
throw new Error("A release needs a title");
|
|
47
59
|
return t;
|
|
48
60
|
};
|
|
61
|
+
/** Refuse a title another release already has (ignoring case), so two releases can't share a name. */
|
|
62
|
+
function uniqueTitle(o, title, except) {
|
|
63
|
+
const taken = o.releases.items.find((r) => r !== except && String(r.get("title")).trim().toLowerCase() === title.toLowerCase());
|
|
64
|
+
if (taken)
|
|
65
|
+
throw new Error(`Release ${numberOf(taken)} is already called '${String(taken.get("title"))}'`);
|
|
66
|
+
return title;
|
|
67
|
+
}
|
|
49
68
|
/** Add the next numbered release, with no stories yet. */
|
|
50
69
|
export function addRelease(input, dir) {
|
|
51
70
|
const o = open(dir);
|
|
52
71
|
const number = Math.max(0, ...o.releases.items.map(numberOf)) + 1;
|
|
53
72
|
const goal = input.goal?.trim();
|
|
54
|
-
o.releases.add(o.map.createNode({ number, title: cleanTitle(input.title), ...(goal ? { goal } : {}), stories: [] }));
|
|
73
|
+
o.releases.add(o.map.createNode({ number, title: uniqueTitle(o, cleanTitle(input.title)), ...(goal ? { goal } : {}), stories: [] }));
|
|
55
74
|
const added = o.releases.items[o.releases.items.length - 1];
|
|
56
75
|
added.get("stories", true).flow = true;
|
|
57
76
|
return { number, validation: save(o) };
|
|
@@ -63,7 +82,7 @@ export function renameRelease(input, dir) {
|
|
|
63
82
|
const o = open(dir);
|
|
64
83
|
const r = find(o, input.number);
|
|
65
84
|
if (input.title !== undefined)
|
|
66
|
-
r.set("title", cleanTitle(input.title));
|
|
85
|
+
r.set("title", uniqueTitle(o, cleanTitle(input.title), r));
|
|
67
86
|
if (input.goal !== undefined) {
|
|
68
87
|
const goal = input.goal.trim();
|
|
69
88
|
if (!goal)
|
|
@@ -98,6 +117,22 @@ export function moveStory(input, dir) {
|
|
|
98
117
|
retag(o.intentDir, new Map([[input.id, input.to]]));
|
|
99
118
|
return { id: input.id, from: fromNumber, to: input.to, validation: save(o) };
|
|
100
119
|
}
|
|
120
|
+
/** Remove an empty release and renumber the ones after it, retagging their stories. */
|
|
121
|
+
export function removeRelease(input, dir) {
|
|
122
|
+
const o = open(dir);
|
|
123
|
+
const r = find(o, input.number);
|
|
124
|
+
const stories = storiesOf(r);
|
|
125
|
+
if (stories.length)
|
|
126
|
+
throw new Error(`Release ${input.number} still has ${stories.join(", ")}. Move its stories out first with nikos release move`);
|
|
127
|
+
const bets = r.get("bets", true);
|
|
128
|
+
if (isSeq(bets) && bets.items.length)
|
|
129
|
+
throw new Error(`Release ${input.number} delivers bets (${bets.items.map((b) => String(b?.value ?? b)).join(", ")}). Move them to another release first`);
|
|
130
|
+
const title = String(r.get("title"));
|
|
131
|
+
const items = o.releases.items;
|
|
132
|
+
items.splice(items.indexOf(r), 1);
|
|
133
|
+
renumber(o);
|
|
134
|
+
return { number: input.number, title, validation: save(o) };
|
|
135
|
+
}
|
|
101
136
|
/** Move a release to a new position and renumber them all from 1, retagging every story that moved. */
|
|
102
137
|
export function reorderRelease(input, dir) {
|
|
103
138
|
const o = open(dir);
|
|
@@ -108,22 +143,15 @@ export function reorderRelease(input, dir) {
|
|
|
108
143
|
const items = o.releases.items;
|
|
109
144
|
items.splice(items.indexOf(r), 1);
|
|
110
145
|
items.splice(input.to - 1, 0, r);
|
|
111
|
-
|
|
112
|
-
items.forEach((item, i) => {
|
|
113
|
-
if (numberOf(item) === i + 1)
|
|
114
|
-
return;
|
|
115
|
-
item.set("number", i + 1);
|
|
116
|
-
for (const id of storiesOf(item))
|
|
117
|
-
moved.set(id, i + 1);
|
|
118
|
-
});
|
|
119
|
-
retag(o.intentDir, moved);
|
|
146
|
+
renumber(o);
|
|
120
147
|
return { order: items.map((i) => ({ number: numberOf(i), title: String(i.get("title")) })), validation: save(o) };
|
|
121
148
|
}
|
|
122
149
|
const USAGE = `Usage:
|
|
123
150
|
nikos release add "<title>" [--goal "<goal>"]
|
|
124
151
|
nikos release rename <n> ["<title>"] [--goal "<goal>"] (--goal "" removes it)
|
|
125
152
|
nikos release move <story-id> <n>
|
|
126
|
-
nikos release reorder <n> <position
|
|
153
|
+
nikos release reorder <n> <position>
|
|
154
|
+
nikos release remove <n> (only an empty release)`;
|
|
127
155
|
function report(v) {
|
|
128
156
|
if (v.errors.length)
|
|
129
157
|
console.log(`\n! intent/ now has ${v.errors.length} ${v.errors.length === 1 ? "problem" : "problems"}; run nikos validate to see them.`);
|
|
@@ -157,6 +185,11 @@ export function runRelease(args) {
|
|
|
157
185
|
console.log(`Releases are now:\n${r.order.map((x) => ` ${x.number}. ${x.title}`).join("\n")}`);
|
|
158
186
|
report(r.validation);
|
|
159
187
|
}
|
|
188
|
+
else if (sub === "remove" && positional[0]) {
|
|
189
|
+
const r = removeRelease({ number: Number(positional[0]) }, dir);
|
|
190
|
+
console.log(`Removed release ${r.number}: ${r.title}`);
|
|
191
|
+
report(r.validation);
|
|
192
|
+
}
|
|
160
193
|
else {
|
|
161
194
|
console.error(USAGE);
|
|
162
195
|
return 1;
|
package/docs/format.md
CHANGED
|
@@ -150,10 +150,11 @@ keeps comments and leaves the rest of the file as it was:
|
|
|
150
150
|
| Command | What it does |
|
|
151
151
|
| --- | --- |
|
|
152
152
|
| `nikos step add "<title>" --activity <id>` | Add a step under an activity (`--id` to choose its id) |
|
|
153
|
-
| `nikos release add "<title>" [--goal "<goal>"]` | Add the next numbered release, with no stories yet |
|
|
153
|
+
| `nikos release add "<title>" [--goal "<goal>"]` | Add the next numbered release, with no stories yet (titles must be unique) |
|
|
154
154
|
| `nikos release rename <n> ["<title>"] [--goal "<goal>"]` | Change a release's title or goal (`--goal ""` removes it) |
|
|
155
155
|
| `nikos release move <story-id> <n>` | Move a story to the end of another release and retag its feature |
|
|
156
156
|
| `nikos release reorder <n> <position>` | Move a release, renumber them all from 1 and retag every moved story |
|
|
157
|
+
| `nikos release remove <n>` | Remove a release with no stories or bets, renumber the rest and retag their stories |
|
|
157
158
|
|
|
158
159
|
---
|
|
159
160
|
|
package/package.json
CHANGED