storymaps 1.2.3 → 1.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.
@@ -21,6 +21,28 @@ export const isValidUrl = (url) => {
21
21
  }
22
22
  };
23
23
 
24
+ // Slice goal and target date are optional. The date is stored as YYYY-MM-DD; YAML can parse an
25
+ // unquoted date as a Date, so accept that too. Anything else is dropped.
26
+ export const SLICE_GOAL_MAX_LENGTH = 200;
27
+
28
+ export const normalizeSliceGoal = (value) =>
29
+ typeof value === 'string' ? value.trim().slice(0, SLICE_GOAL_MAX_LENGTH) : '';
30
+
31
+ export const normalizeSliceDate = (value) => {
32
+ if (value instanceof Date && !isNaN(value)) return value.toISOString().slice(0, 10);
33
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return '';
34
+ return isNaN(new Date(`${value}T00:00:00Z`)) ? '' : value;
35
+ };
36
+
37
+ // Copies a slice's goal and target date from one object to another, leaving out empty values
38
+ export const copySliceDetails = (from, to) => {
39
+ const goal = normalizeSliceGoal(from.goal);
40
+ const targetDate = normalizeSliceDate(from.targetDate);
41
+ if (goal) to.goal = goal;
42
+ if (targetDate) to.targetDate = targetDate;
43
+ return to;
44
+ };
45
+
24
46
  export const CARD_COLORS = {
25
47
  red: '#fca5a5',
26
48
  rose: '#fecdd3',
package/lib/core/yaml.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // YAML ↔ JSON transform + validation
3
3
 
4
4
  import jsyaml from 'js-yaml';
5
- import { generateId } from './constants.js';
5
+ import { generateId, copySliceDetails } from './constants.js';
6
6
 
7
7
  const DEFAULT_STEP_COLOR = '#86efac';
8
8
  const DEFAULT_STORY_COLOR = '#fef08a';
@@ -54,10 +54,12 @@ export function jsonToYamlObj(data) {
54
54
  partialSlugMap[pm.id] = slugify(pm.name) || pm.id;
55
55
  });
56
56
 
57
- // Classify each step
58
- const stepTypes = steps.map(step => {
57
+ // Classify each step. A hidden step with a name or cards is written as a normal step so its
58
+ // cards survive (only empty, unnamed hidden steps are spacers).
59
+ const hasCards = (i) => !!(usersArr[i]?.length || activitiesArr[i]?.length || slices.some(s => s.stories?.[i]?.length));
60
+ const stepTypes = steps.map((step, i) => {
59
61
  if (step.partialMapId) return 'partial';
60
- if (step.hidden) return 'spacer';
62
+ if (step.hidden && !step.name && !hasCards(i)) return 'spacer';
61
63
  return 'step';
62
64
  });
63
65
 
@@ -70,42 +72,18 @@ export function jsonToYamlObj(data) {
70
72
  });
71
73
  const duplicateNames = new Set(Object.keys(nameCounts).filter(n => nameCounts[n] > 1));
72
74
 
73
- // Resolve names — namespace duplicates with nearest activity/user
75
+ // Number duplicate names (Confirm[0], Confirm[1]) so cards point at the right step.
76
+ // Import strips the number, so the names come back unchanged.
77
+ const counters = {};
74
78
  const resolvedNames = steps.map((step, i) => {
75
79
  if (stepTypes[i] !== 'step') return null;
76
80
  const name = step.name || '';
77
81
  if (!duplicateNames.has(name)) return name;
78
- for (let j = i; j >= 0; j--) {
79
- if (activitiesArr[j]?.length) return `${activitiesArr[j][0].name}: ${name}`;
80
- }
81
- for (let j = i; j >= 0; j--) {
82
- if (usersArr[j]?.length) return `${usersArr[j][0].name}: ${name}`;
83
- }
84
- return name;
82
+ const idx = counters[name] || 0;
83
+ counters[name] = idx + 1;
84
+ return `${name}[${idx}]`;
85
85
  });
86
86
 
87
- // Second pass: add :[N] suffix to any still-duplicate resolved names
88
- const resolvedCounts = {};
89
- resolvedNames.forEach(n => {
90
- if (n != null) resolvedCounts[n] = (resolvedCounts[n] || 0) + 1;
91
- });
92
- const stillDuplicate = new Set(Object.keys(resolvedCounts).filter(n => resolvedCounts[n] > 1));
93
- if (stillDuplicate.size) {
94
- const counters = {};
95
- for (let i = 0; i < resolvedNames.length; i++) {
96
- const n = resolvedNames[i];
97
- if (n == null || !stillDuplicate.has(n)) continue;
98
- const idx = counters[n] || 0;
99
- counters[n] = idx + 1;
100
- const colonPos = n.indexOf(': ');
101
- if (colonPos >= 0) {
102
- resolvedNames[i] = n.slice(0, colonPos + 1) + `[${idx}]` + n.slice(colonPos + 1);
103
- } else {
104
- resolvedNames[i] = `${n}[${idx}]`;
105
- }
106
- }
107
- }
108
-
109
87
  // Build YAML object
110
88
  const result = {};
111
89
  result.name = data.name || '';
@@ -175,14 +153,16 @@ export function jsonToYamlObj(data) {
175
153
  if (stepTypes[i] === 'partial') return { partial: partialSlugMap[step.partialMapId] };
176
154
  const name = resolvedNames[i];
177
155
  const hasExtra = (step.color && step.color !== DEFAULT_STEP_COLOR)
178
- || step.url || step.body || step.points != null;
156
+ || step.body || step.url || step.status || step.points != null || step.tags?.length;
179
157
  if (!hasExtra) return name;
180
- const obj = { name };
181
- if (step.color && step.color !== DEFAULT_STEP_COLOR) obj.color = step.color;
182
- if (step.body) obj.body = step.body;
183
- if (step.url) obj.url = step.url;
184
- if (step.points != null) obj.points = step.points;
185
- return obj;
158
+ const s = { name };
159
+ if (step.color && step.color !== DEFAULT_STEP_COLOR) s.color = step.color;
160
+ if (step.body) s.body = step.body;
161
+ if (step.url) s.url = step.url;
162
+ if (step.status) s.status = step.status;
163
+ if (step.points != null) s.points = step.points;
164
+ if (step.tags?.length) s.tags = step.tags;
165
+ return s;
186
166
  });
187
167
 
188
168
  // Legend
@@ -194,6 +174,7 @@ export function jsonToYamlObj(data) {
194
174
  const obj = { name: slice.name || '' };
195
175
  if (slice.collapsed) obj.collapsed = true;
196
176
  if (slice.closedReason) obj.closedReason = slice.closedReason;
177
+ copySliceDetails(slice, obj);
197
178
  const stories = {};
198
179
  (slice.stories || []).forEach((cards, i) => {
199
180
  if (!cards?.length) return;
@@ -271,6 +252,7 @@ export function validateYaml(obj) {
271
252
  const label = slice.name || `#${si}`;
272
253
  if (slice.stories) {
273
254
  for (const [stepName, cards] of Object.entries(slice.stories)) {
255
+ if (!stepName) continue;
274
256
  if (!stepNameSet.has(stepName)) {
275
257
  errors.push(`Slice "${label}" references unknown step "${stepName}"`);
276
258
  }
@@ -330,9 +312,11 @@ export function yamlObjToJson(obj) {
330
312
  stepNameToIndex[rawName] = jsonSteps.length;
331
313
  const s = { name: displayName, color };
332
314
  if (typeof step === 'object' && step !== null) {
333
- if (step.url) s.url = step.url;
334
315
  if (step.body) s.body = step.body;
316
+ if (step.url) s.url = step.url;
317
+ if (step.status) s.status = step.status;
335
318
  if (step.points != null) s.points = step.points;
319
+ if (step.tags?.length) s.tags = step.tags;
336
320
  }
337
321
  jsonSteps.push(s);
338
322
  });
@@ -364,6 +348,7 @@ export function yamlObjToJson(obj) {
364
348
  const s = { name: slice.name || '', stories };
365
349
  if (slice.collapsed) s.collapsed = true;
366
350
  if (slice.closedReason) s.closedReason = slice.closedReason;
351
+ copySliceDetails(slice, s);
367
352
  return s;
368
353
  });
369
354
 
@@ -422,7 +407,8 @@ export function yamlObjToJson(obj) {
422
407
  };
423
408
 
424
409
  if (obj.legend?.length) result.legend = obj.legend;
425
- if (obj.notes) result.notes = obj.notes;
410
+ // Keep empty notes so a push with notes: "" clears them (no notes key leaves them alone)
411
+ if (typeof obj.notes === 'string') result.notes = obj.notes;
426
412
  if (jsonPartialMaps.length) result.partialMaps = jsonPartialMaps;
427
413
  if (obj.backups?.length) result.backups = obj.backups;
428
414
  return result;
@@ -442,6 +428,10 @@ function backboneToYaml(positionalArr, namesByIndex) {
442
428
  const obj = { name: card.name };
443
429
  if (card.body) obj.body = card.body;
444
430
  if (card.color) obj.color = card.color;
431
+ if (card.status) obj.status = card.status;
432
+ if (card.points != null) obj.points = card.points;
433
+ if (card.tags?.length) obj.tags = [...card.tags];
434
+ if (card.url) obj.url = card.url;
445
435
  obj.step = stepName;
446
436
  result.push(obj);
447
437
  });
package/lib/util/api.js CHANGED
@@ -10,9 +10,18 @@ class ApiError extends Error {
10
10
  }
11
11
  }
12
12
 
13
+ // The server sends errors as JSON ({"error": "..."}), so show just the message
14
+ function errorMessage(body) {
15
+ try {
16
+ return JSON.parse(body).error || body;
17
+ } catch {
18
+ return body;
19
+ }
20
+ }
21
+
13
22
  async function handleResponse(res) {
14
23
  if (res.ok) return;
15
- const body = await res.text().catch(() => '');
24
+ const body = errorMessage(await res.text().catch(() => ''));
16
25
  switch (res.status) {
17
26
  case 404: throw new ApiError('Map not found', 404);
18
27
  case 409: throw new ApiError(body || 'Conflict — the remote map has changed. Pull first or use --force.', 409, { conflict: true });
@@ -3,6 +3,115 @@
3
3
 
4
4
  import chalk from 'chalk';
5
5
  import jsyaml from 'js-yaml';
6
+ import { normalizeSliceGoal, normalizeSliceDate } from '../core/constants.js';
7
+
8
+ // Fields compared on cards and steps that exist on both sides
9
+ const FIELDS = ['body', 'color', 'url', 'status', 'points', 'tags'];
10
+
11
+ const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
12
+
13
+ // Slice names can span lines (e.g. "V1:\nBasic House"); keep each summary entry on one line
14
+ const oneLine = (s) => String(s ?? '').replace(/\s*\n\s*/g, ' ');
15
+
16
+ const changedFields = (a, b) =>
17
+ FIELDS.filter(f => JSON.stringify(a?.[f] ?? null) !== JSON.stringify(b?.[f] ?? null));
18
+
19
+ // True when the names both lists share appear in a different order
20
+ const reordered = (oldNames, newNames) => {
21
+ const common = oldNames.filter(n => newNames.includes(n));
22
+ const commonNew = newNames.filter(n => oldNames.includes(n));
23
+ return common.join('\n') !== commonNew.join('\n');
24
+ };
25
+
26
+ // Compare located cards ({ card, group, at }) across the whole map. Cards are
27
+ // paired by name in order, so a card that changes slice or step counts as moved
28
+ // rather than removed and added. Returns per-group summary lines.
29
+ function compareCards(oldItems, newItems) {
30
+ const stats = new Map();
31
+ const statsFor = (group) => {
32
+ if (!stats.has(group)) stats.set(group, { added: 0, removed: 0, moved: 0, updated: 0, fields: new Set() });
33
+ return stats.get(group);
34
+ };
35
+ // Report groups in their new order, then any that only exist on the old side
36
+ newItems.forEach(i => statsFor(i.group));
37
+ oldItems.forEach(i => statsFor(i.group));
38
+
39
+ const byName = (items) => {
40
+ const map = new Map();
41
+ for (const item of items) {
42
+ const name = item.card?.name ?? '';
43
+ if (!map.has(name)) map.set(name, []);
44
+ map.get(name).push(item);
45
+ }
46
+ return map;
47
+ };
48
+ const oldByName = byName(oldItems);
49
+ const newByName = byName(newItems);
50
+
51
+ for (const name of new Set([...oldByName.keys(), ...newByName.keys()])) {
52
+ const o = oldByName.get(name) || [];
53
+ const c = newByName.get(name) || [];
54
+ const paired = Math.min(o.length, c.length);
55
+ for (let i = 0; i < paired; i++) {
56
+ const s = statsFor(c[i].group);
57
+ if (o[i].group !== c[i].group || o[i].at !== c[i].at) s.moved++;
58
+ const fields = changedFields(o[i].card, c[i].card);
59
+ if (fields.length) {
60
+ s.updated++;
61
+ fields.forEach(f => s.fields.add(f));
62
+ }
63
+ }
64
+ c.slice(paired).forEach(item => statsFor(item.group).added++);
65
+ o.slice(paired).forEach(item => statsFor(item.group).removed++);
66
+ }
67
+
68
+ const lines = [];
69
+ for (const [group, s] of stats) {
70
+ // Equal numbers of unmatched adds and removes means the cards were renamed
71
+ if (s.added && s.added === s.removed) {
72
+ lines.push(chalk.yellow(` ~ ${oneLine(group)}: ${plural(s.added, 'card')} renamed`));
73
+ } else {
74
+ if (s.added) lines.push(chalk.green(` + ${oneLine(group)}: ${plural(s.added, 'card')}`));
75
+ if (s.removed) lines.push(chalk.red(` - ${oneLine(group)}: ${plural(s.removed, 'card')}`));
76
+ }
77
+ if (s.moved) lines.push(chalk.yellow(` ~ ${oneLine(group)}: ${plural(s.moved, 'card')} moved`));
78
+ if (s.updated) {
79
+ lines.push(chalk.yellow(` ~ ${oneLine(group)}: ${plural(s.updated, 'card')} updated (${[...s.fields].join(', ')})`));
80
+ }
81
+ }
82
+ return lines;
83
+ }
84
+
85
+ function compareLegend(oldLegend, newLegend) {
86
+ const lines = [];
87
+ const key = e => `${e?.color ?? ''}\n${e?.label ?? ''}`;
88
+ const removed = [...oldLegend];
89
+ const added = [];
90
+ for (const entry of newLegend) {
91
+ const i = removed.findIndex(e => key(e) === key(entry));
92
+ if (i >= 0) removed.splice(i, 1);
93
+ else added.push(entry);
94
+ }
95
+ // Same color with a new label is a rename; same label with a new color is a recolor
96
+ for (const entry of [...added]) {
97
+ let i = removed.findIndex(e => e.color === entry.color);
98
+ if (i >= 0) {
99
+ lines.push(chalk.yellow(` ~ legend: ${removed[i].label} → ${entry.label}`));
100
+ } else if ((i = removed.findIndex(e => e.label === entry.label)) >= 0) {
101
+ lines.push(chalk.yellow(` ~ legend: ${entry.label} color changed`));
102
+ } else {
103
+ continue;
104
+ }
105
+ removed.splice(i, 1);
106
+ added.splice(added.indexOf(entry), 1);
107
+ }
108
+ for (const e of added) lines.push(chalk.green(` + legend: ${e.label}`));
109
+ for (const e of removed) lines.push(chalk.red(` - legend: ${e.label}`));
110
+ if (!lines.length && reordered(oldLegend.map(key), newLegend.map(key))) {
111
+ lines.push(chalk.yellow(' ~ legend reordered'));
112
+ }
113
+ return lines;
114
+ }
6
115
 
7
116
  export function changeSummary(oldYaml, newYaml) {
8
117
  const old = jsyaml.load(oldYaml);
@@ -15,13 +124,15 @@ export function changeSummary(oldYaml, newYaml) {
15
124
  }
16
125
 
17
126
  // Steps (skip spacers and partial refs)
18
- const stepName = s => {
19
- if (typeof s === 'string' && s !== 'spacer') return s;
20
- if (typeof s === 'object' && s?.name) return s.name;
127
+ const toStep = s => {
128
+ if (typeof s === 'string' && s !== 'spacer') return { name: s };
129
+ if (typeof s === 'object' && s?.name) return s;
21
130
  return null;
22
131
  };
23
- const oldSteps = (old.steps || []).map(stepName).filter(Boolean);
24
- const newSteps = (cur.steps || []).map(stepName).filter(Boolean);
132
+ const oldStepObjs = (old.steps || []).map(toStep).filter(Boolean);
133
+ const newStepObjs = (cur.steps || []).map(toStep).filter(Boolean);
134
+ const oldSteps = oldStepObjs.map(s => s.name);
135
+ const newSteps = newStepObjs.map(s => s.name);
25
136
  const addedSteps = newSteps.filter(s => !oldSteps.includes(s));
26
137
  const removedSteps = oldSteps.filter(s => !newSteps.includes(s));
27
138
  if (addedSteps.length || removedSteps.length) {
@@ -31,55 +142,84 @@ export function changeSummary(oldYaml, newYaml) {
31
142
  for (const s of addedSteps) lines.push(chalk.green(` + ${s}`));
32
143
  for (const s of removedSteps) lines.push(chalk.red(` - ${s}`));
33
144
  }
145
+ if (reordered(oldSteps, newSteps)) lines.push(chalk.yellow(' ~ steps reordered'));
146
+ const stepFields = new Set();
147
+ let updatedSteps = 0;
148
+ for (const step of newStepObjs) {
149
+ const before = oldStepObjs.find(s => s.name === step.name);
150
+ if (!before) continue;
151
+ const fields = changedFields(before, step);
152
+ if (fields.length) {
153
+ updatedSteps++;
154
+ fields.forEach(f => stepFields.add(f));
155
+ }
156
+ }
157
+ if (updatedSteps) {
158
+ lines.push(chalk.yellow(` ~ steps: ${plural(updatedSteps, 'step')} updated (${[...stepFields].join(', ')})`));
159
+ }
34
160
 
35
161
  // Slices
36
162
  const oldSlices = (old.slices || []).map(s => s.name);
37
163
  const newSlices = (cur.slices || []).map(s => s.name);
38
- for (const s of newSlices.filter(s => !oldSlices.includes(s))) {
39
- lines.push(chalk.green(` + slice: ${s}`));
164
+ // A slice whose name changed in the same position is a rename, so its cards aren't reported as moved
165
+ const sliceRenames = new Map();
166
+ oldSlices.forEach((name, i) => {
167
+ const newName = newSlices[i];
168
+ if (newName !== undefined && !newSlices.includes(name) && !oldSlices.includes(newName)) {
169
+ sliceRenames.set(name, newName);
170
+ }
171
+ });
172
+ const currentName = name => sliceRenames.get(name) ?? name;
173
+ for (const [from, to] of sliceRenames) {
174
+ lines.push(chalk.yellow(` ~ slice: ${oneLine(from)} → ${oneLine(to)}`));
175
+ }
176
+ const renamedTo = new Set(sliceRenames.values());
177
+ for (const s of newSlices.filter(s => !oldSlices.includes(s) && !renamedTo.has(s))) {
178
+ lines.push(chalk.green(` + slice: ${oneLine(s)}`));
40
179
  }
41
- for (const s of oldSlices.filter(s => !newSlices.includes(s))) {
42
- lines.push(chalk.red(` - slice: ${s}`));
180
+ for (const s of oldSlices.filter(s => !newSlices.includes(s) && !sliceRenames.has(s))) {
181
+ lines.push(chalk.red(` - slice: ${oneLine(s)}`));
182
+ }
183
+ if (reordered(oldSlices.map(currentName), newSlices)) lines.push(chalk.yellow(' ~ slices reordered'));
184
+ for (const slice of cur.slices || []) {
185
+ const before = (old.slices || []).find(s => currentName(s.name) === slice.name);
186
+ if (!before) continue;
187
+ if (!!before.collapsed !== !!slice.collapsed || before.closedReason !== slice.closedReason) {
188
+ const state = !slice.collapsed ? 'reopened' : slice.closedReason === 'closed' ? 'closed' : 'completed';
189
+ lines.push(chalk.yellow(` ~ slice: ${oneLine(slice.name)} ${state}`));
190
+ }
191
+ const [oldGoal, newGoal] = [normalizeSliceGoal(before.goal), normalizeSliceGoal(slice.goal)];
192
+ if (oldGoal !== newGoal) {
193
+ const change = !oldGoal ? 'set' : !newGoal ? 'removed' : 'changed';
194
+ lines.push(chalk.yellow(` ~ slice: ${oneLine(slice.name)} goal ${change}`));
195
+ }
196
+ const [oldDate, newDate] = [normalizeSliceDate(before.targetDate), normalizeSliceDate(slice.targetDate)];
197
+ if (oldDate !== newDate) {
198
+ const change = !oldDate ? `set to ${newDate}` : !newDate ? 'removed' : `moved to ${newDate}`;
199
+ lines.push(chalk.yellow(` ~ slice: ${oneLine(slice.name)} target date ${change}`));
200
+ }
43
201
  }
44
202
 
45
- // Detect card-level changes per slice (name-aware, handles renames)
46
- const collectCardNames = (map) => {
47
- const result = {};
48
- for (const slice of (map.slices || [])) {
49
- const names = [];
50
- for (const cards of Object.values(slice.stories || {})) {
51
- if (Array.isArray(cards)) names.push(...cards.map(c => c.name).filter(Boolean));
203
+ // Cards: users, activities and stories in every slice
204
+ const collectCards = (map, sliceName) => {
205
+ const items = [];
206
+ for (const group of ['users', 'activities']) {
207
+ for (const card of map[group] || []) items.push({ card, group, at: card.step });
208
+ }
209
+ for (const slice of map.slices || []) {
210
+ const group = sliceName(slice.name);
211
+ for (const [step, cards] of Object.entries(slice.stories || {})) {
212
+ if (Array.isArray(cards)) cards.forEach(card => items.push({ card, group, at: step }));
52
213
  }
53
- result[slice.name] = names;
54
214
  }
55
- return result;
215
+ return items;
56
216
  };
57
- const oldCards = collectCardNames(old);
58
- const newCards = collectCardNames(cur);
59
- for (const sliceName of new Set([...Object.keys(oldCards), ...Object.keys(newCards)])) {
60
- const oldNames = oldCards[sliceName] || [];
61
- const newNames = newCards[sliceName] || [];
62
- const oldCounts = new Map();
63
- const newCounts = new Map();
64
- for (const n of oldNames) oldCounts.set(n, (oldCounts.get(n) || 0) + 1);
65
- for (const n of newNames) newCounts.set(n, (newCounts.get(n) || 0) + 1);
66
- const allNames = new Set([...oldCounts.keys(), ...newCounts.keys()]);
67
- let added = 0, removed = 0;
68
- for (const n of allNames) {
69
- const o = oldCounts.get(n) || 0;
70
- const c = newCounts.get(n) || 0;
71
- if (c > o) added += c - o;
72
- if (o > c) removed += o - c;
73
- }
74
- if (added === 0 && removed === 0) continue;
75
- const net = newNames.length - oldNames.length;
76
- if (net === 0) {
77
- const word = added === 1 ? 'card' : 'cards';
78
- lines.push(chalk.yellow(` ~ ${sliceName}: ${added} ${word} renamed`));
79
- } else {
80
- if (added) lines.push(chalk.green(` + ${sliceName}: ${added} ${added === 1 ? 'card' : 'cards'}`));
81
- if (removed) lines.push(chalk.red(` - ${sliceName}: ${removed} ${removed === 1 ? 'card' : 'cards'}`));
82
- }
217
+ lines.push(...compareCards(collectCards(old, currentName), collectCards(cur, name => name)));
218
+
219
+ lines.push(...compareLegend(old.legend || [], cur.legend || []));
220
+
221
+ if (JSON.stringify(old.partials ?? null) !== JSON.stringify(cur.partials ?? null)) {
222
+ lines.push(chalk.yellow(' ~ partials updated'));
83
223
  }
84
224
 
85
225
  // Notes
@@ -96,9 +236,12 @@ export function changeSummary(oldYaml, newYaml) {
96
236
 
97
237
  export function printChangeSummary(oldYaml, newYaml) {
98
238
  const lines = changeSummary(oldYaml, newYaml);
99
- if (lines.length === 0) {
239
+ if (lines.length) {
240
+ lines.forEach(l => console.log(l));
241
+ } else if (oldYaml === newYaml) {
100
242
  console.log(chalk.dim('No changes.'));
101
243
  } else {
102
- lines.forEach(l => console.log(l));
244
+ // Something differs that the summary doesn't describe (e.g. card order within a step)
245
+ console.log(chalk.yellow(` ~ changes not shown in summary, run ${chalk.cyan('storymaps diff --full')} to see them`));
103
246
  }
104
247
  }
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "storymaps",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "CLI tool for storymaps.io - import, export, validate, and manage user story maps from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "storymaps": "./bin/storymaps.js"
8
8
  },
9
+ "scripts": {
10
+ "test": "node --test 'test/unit/**/*.test.js'",
11
+ "test:integration": "node --test 'test/integration/**/*.test.js'"
12
+ },
9
13
  "keywords": [
10
14
  "story-mapping",
11
15
  "user-story",