@lumoai/cli 1.55.0 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skill/SKILL.md +71 -0
- package/assets/skill/references/priority.md +6 -5
- package/assets/skill/references/tasks.md +15 -3
- package/dist/cli/src/commands/idea-comment.js +127 -0
- package/dist/cli/src/commands/idea-figma-add.js +30 -0
- package/dist/cli/src/commands/idea-figma-context.js +65 -0
- package/dist/cli/src/commands/idea-figma-list.js +32 -0
- package/dist/cli/src/commands/idea-figma-refresh.js +40 -0
- package/dist/cli/src/commands/idea-figma-rm.js +19 -0
- package/dist/cli/src/commands/idea-slack-add.js +66 -0
- package/dist/cli/src/commands/idea-slack-rm.js +63 -0
- package/dist/cli/src/commands/idea-slack-show.js +65 -0
- package/dist/cli/src/commands/idea-web-add.js +66 -0
- package/dist/cli/src/commands/idea-web-rm.js +63 -0
- package/dist/cli/src/commands/idea-web-show.js +71 -0
- package/dist/cli/src/commands/idea.js +140 -0
- package/dist/cli/src/commands/initiative.js +132 -0
- package/dist/cli/src/commands/next.js +9 -1
- package/dist/cli/src/commands/session-attach.js +4 -0
- package/dist/cli/src/commands/setup.js +17 -8
- package/dist/cli/src/commands/task-comment-list.js +2 -1
- package/dist/cli/src/commands/task-context.js +5 -1
- package/dist/cli/src/commands/task-create.js +18 -1
- package/dist/cli/src/commands/task-update.js +16 -0
- package/dist/cli/src/commands/worktree-add.js +6 -2
- package/dist/cli/src/index.js +118 -3
- package/dist/cli/src/lib/idea-figma-api.js +65 -0
- package/dist/cli/src/lib/next-steps.js +55 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.assertSameTeam = assertSameTeam;
|
|
4
4
|
exports.normalizePriority = normalizePriority;
|
|
5
|
+
exports.formatCreateOutput = formatCreateOutput;
|
|
5
6
|
exports.formatCreatedTaskLine = formatCreatedTaskLine;
|
|
6
7
|
exports.taskCreate = taskCreate;
|
|
7
8
|
const config_1 = require("../lib/config");
|
|
@@ -9,6 +10,7 @@ const api_1 = require("../lib/api");
|
|
|
9
10
|
const tag_resolver_1 = require("../lib/tag-resolver");
|
|
10
11
|
const resolve_1 = require("../lib/resolve");
|
|
11
12
|
const sanitize_1 = require("../lib/sanitize");
|
|
13
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
12
14
|
const ALLOWED_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'URGENT'];
|
|
13
15
|
/**
|
|
14
16
|
* Assert that the sprint belongs to the same team as the newly created task.
|
|
@@ -80,6 +82,21 @@ function normalizePriority(value) {
|
|
|
80
82
|
? upper
|
|
81
83
|
: null;
|
|
82
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Compose the full `task create` output: the result line first, then the
|
|
87
|
+
* server-computed next-step block (LUM-686). Exported so the render test
|
|
88
|
+
* exercises the real composition rather than re-assembling the parts itself.
|
|
89
|
+
*
|
|
90
|
+
* The result line stays byte-identical and first — a reader taking line 1 is
|
|
91
|
+
* unaffected by the block below it.
|
|
92
|
+
*/
|
|
93
|
+
function formatCreateOutput(task, steps = [], opts = {}, env = process.env) {
|
|
94
|
+
const head = formatCreatedTaskLine(task);
|
|
95
|
+
if ((0, next_steps_1.nextStepsSilenced)(env, opts))
|
|
96
|
+
return head;
|
|
97
|
+
const block = (0, next_steps_1.formatNextSteps)(steps);
|
|
98
|
+
return block === '' ? head : `${head}\n\n${block}`;
|
|
99
|
+
}
|
|
83
100
|
/**
|
|
84
101
|
* Format the single-line success output. Title is double-quoted; embedded
|
|
85
102
|
* double-quotes are backslash-escaped so the output stays parseable as one
|
|
@@ -173,7 +190,7 @@ async function taskCreate(title, opts) {
|
|
|
173
190
|
if (res.status === 201) {
|
|
174
191
|
const data = (await res.json());
|
|
175
192
|
const responseTagNames = (data.task.taskTags ?? []).map(t => t.name);
|
|
176
|
-
process.stdout.write(
|
|
193
|
+
process.stdout.write(formatCreateOutput({ ...data.task, tags: responseTagNames }, data.nextSteps ?? [], opts) + '\n');
|
|
177
194
|
// Sprint binding (optional): if --sprint was passed, bind the new task.
|
|
178
195
|
if (opts.sprint) {
|
|
179
196
|
const workspaceSlug = creds.workspaceSlug ?? '';
|
|
@@ -11,6 +11,7 @@ const task_create_1 = require("./task-create");
|
|
|
11
11
|
const tag_resolver_1 = require("../lib/tag-resolver");
|
|
12
12
|
const resolve_1 = require("../lib/resolve");
|
|
13
13
|
const sanitize_1 = require("../lib/sanitize");
|
|
14
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
14
15
|
const ALLOWED_STATUSES = ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE'];
|
|
15
16
|
/**
|
|
16
17
|
* Pure function: given the task's current sprint binding and the resolved
|
|
@@ -101,7 +102,21 @@ function formatUpdatedTaskLine(task) {
|
|
|
101
102
|
}
|
|
102
103
|
return head;
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Thin wrapper so the next-step block always lands last (LUM-686). The update
|
|
107
|
+
* flow has several success exits — a plain PATCH, plus the sprint bind/unbind
|
|
108
|
+
* branches — and hints belong after whatever the command printed, not wedged
|
|
109
|
+
* before the Sprint line. Only the wrapper emits; the inner run just collects.
|
|
110
|
+
*/
|
|
104
111
|
async function taskUpdate(identifier, opts) {
|
|
112
|
+
const collected = { steps: [] };
|
|
113
|
+
const code = await runTaskUpdate(identifier, opts, collected);
|
|
114
|
+
// Success only — a failed update's advice would be noise on top of an error.
|
|
115
|
+
if (code === undefined)
|
|
116
|
+
(0, next_steps_1.emitNextSteps)(collected.steps, opts);
|
|
117
|
+
return code;
|
|
118
|
+
}
|
|
119
|
+
async function runTaskUpdate(identifier, opts, collected) {
|
|
105
120
|
if (!identifier || identifier.trim().length === 0) {
|
|
106
121
|
console.error('Error: missing <identifier>. Usage: lumo task update <LUM-42> [options]');
|
|
107
122
|
return 1;
|
|
@@ -249,6 +264,7 @@ async function taskUpdate(identifier, opts) {
|
|
|
249
264
|
if (responseTagNames !== undefined)
|
|
250
265
|
taskLine.tags = responseTagNames;
|
|
251
266
|
process.stdout.write(formatUpdatedTaskLine(taskLine) + '\n');
|
|
267
|
+
collected.steps = data.nextSteps ?? [];
|
|
252
268
|
}
|
|
253
269
|
else {
|
|
254
270
|
let serverMsg = null;
|
|
@@ -37,6 +37,7 @@ exports.worktreeAdd = worktreeAdd;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const child_process_1 = require("child_process");
|
|
40
|
+
const next_steps_1 = require("../lib/next-steps");
|
|
40
41
|
const worktree_1 = require("../lib/worktree");
|
|
41
42
|
function printGuidance(dir, branch) {
|
|
42
43
|
console.log('');
|
|
@@ -44,8 +45,11 @@ function printGuidance(dir, branch) {
|
|
|
44
45
|
console.log(` branch: ${branch}`);
|
|
45
46
|
console.log(` node_modules: symlinked to main checkout`);
|
|
46
47
|
console.log('');
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Locally-static source: no server round-trip to carry steps, but the same
|
|
49
|
+
// renderer so the block looks identical everywhere (LUM-686).
|
|
50
|
+
console.log((0, next_steps_1.formatNextSteps)([
|
|
51
|
+
{ command: `cd ${dir}`, why: 'work from the isolated checkout' },
|
|
52
|
+
]));
|
|
49
53
|
console.log('');
|
|
50
54
|
console.log('Gotchas baked into this worktree:');
|
|
51
55
|
console.log(' • prisma: the generated client is SHARED via the node_modules symlink.');
|
package/dist/cli/src/index.js
CHANGED
|
@@ -47,6 +47,19 @@ const session_attach_1 = require("./commands/session-attach");
|
|
|
47
47
|
const session_status_1 = require("./commands/session-status");
|
|
48
48
|
const next_1 = require("./commands/next");
|
|
49
49
|
const idea_1 = require("./commands/idea");
|
|
50
|
+
const idea_slack_add_1 = require("./commands/idea-slack-add");
|
|
51
|
+
const idea_slack_show_1 = require("./commands/idea-slack-show");
|
|
52
|
+
const idea_slack_rm_1 = require("./commands/idea-slack-rm");
|
|
53
|
+
const idea_web_add_1 = require("./commands/idea-web-add");
|
|
54
|
+
const idea_web_show_1 = require("./commands/idea-web-show");
|
|
55
|
+
const idea_web_rm_1 = require("./commands/idea-web-rm");
|
|
56
|
+
const idea_figma_add_1 = require("./commands/idea-figma-add");
|
|
57
|
+
const idea_figma_list_1 = require("./commands/idea-figma-list");
|
|
58
|
+
const idea_figma_rm_1 = require("./commands/idea-figma-rm");
|
|
59
|
+
const idea_figma_refresh_1 = require("./commands/idea-figma-refresh");
|
|
60
|
+
const idea_figma_context_1 = require("./commands/idea-figma-context");
|
|
61
|
+
const idea_comment_1 = require("./commands/idea-comment");
|
|
62
|
+
const initiative_1 = require("./commands/initiative");
|
|
50
63
|
const plan_1 = require("./commands/plan");
|
|
51
64
|
const cost_1 = require("./commands/cost");
|
|
52
65
|
const priority_1 = require("./commands/priority");
|
|
@@ -203,8 +216,18 @@ const program = new commander_1.Command()
|
|
|
203
216
|
// point at --help instead of dead-ending on "unknown option". Subcommands
|
|
204
217
|
// created via .command() inherit these settings.
|
|
205
218
|
.showSuggestionAfterError(true)
|
|
206
|
-
.showHelpAfterError('(run the command with --help to list its valid flags and arguments)')
|
|
219
|
+
.showHelpAfterError('(run the command with --help to list its valid flags and arguments)')
|
|
220
|
+
// LUM-686: global mute for the trailing next-step block. `LUMO_NO_HINTS=1`
|
|
221
|
+
// does the same for a whole shell.
|
|
222
|
+
.option('--no-hints', 'suppress the trailing next-step suggestions');
|
|
207
223
|
exports.program = program;
|
|
224
|
+
// Commander does not merge program-level options into a subcommand's own
|
|
225
|
+
// opts(), so the global `--no-hints` is folded into the env channel before any
|
|
226
|
+
// action runs. One silencing path (`nextStepsSilenced`) instead of two.
|
|
227
|
+
program.hook('preAction', thisCommand => {
|
|
228
|
+
if (thisCommand.opts().hints === false)
|
|
229
|
+
process.env.LUMO_NO_HINTS = '1';
|
|
230
|
+
});
|
|
208
231
|
const auth = program.command('auth').description('Manage Lumo authentication');
|
|
209
232
|
auth
|
|
210
233
|
.command('login')
|
|
@@ -279,11 +302,103 @@ program
|
|
|
279
302
|
.option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
|
|
280
303
|
.option('--claimable', 'Only recommend agent-claimable tasks that are unblocked (F3) and within milestone budget (F2)')
|
|
281
304
|
.action(wrap(options => (0, next_1.nextCommand)(options)));
|
|
282
|
-
program
|
|
283
|
-
.command('idea
|
|
305
|
+
const ideaCmd = program
|
|
306
|
+
.command('idea [statement]')
|
|
284
307
|
.description('Capture a team-level idea into the pool (<10s, no friction). Grabs the current Claude Code session id and its bound task as provenance; prints an I-prefixed id (e.g. LUM-I42). The transformer (Spec 2) consumes the pool.')
|
|
285
308
|
.option('-c, --context <text>', 'Free-text origin context for the idea')
|
|
286
309
|
.action(wrap((statement, options) => (0, idea_1.ideaCapture)(statement, options)));
|
|
310
|
+
ideaCmd
|
|
311
|
+
.command('list')
|
|
312
|
+
.description('List the team idea pool newest-first — each line shows the LUM-I<n> id, status (CAPTURED|DEVELOPING|PLANNED|DROPPED) and statement.')
|
|
313
|
+
.action(wrap(() => (0, idea_1.ideaList)()));
|
|
314
|
+
ideaCmd
|
|
315
|
+
.command('update <id>')
|
|
316
|
+
.description('Move an idea through its lifecycle (LUM-679). Allowed: CAPTURED↔DEVELOPING→PLANNED and any state→DROPPED (DROPPED is terminal). <id> is the LUM-I<n> id (a bare number also resolves).')
|
|
317
|
+
.option('-s, --status <value>', 'New status: captured | developing | planned | dropped (case-insensitive)')
|
|
318
|
+
.action(wrap((id, options) => (0, idea_1.ideaUpdate)(id, options)));
|
|
319
|
+
// Idea evidence commands (LUM-681): mirrors `task slack/web/figma`, hitting
|
|
320
|
+
// `/api/ideas/:id/...` instead of `/api/tasks/:id/...`. Lets an agent attach
|
|
321
|
+
// Slack threads, web links, and Figma design links to an idea before it goes
|
|
322
|
+
// through the plan-run converter.
|
|
323
|
+
const ideaSlack = ideaCmd
|
|
324
|
+
.command('slack')
|
|
325
|
+
.description('Attach & inspect Slack context on an idea');
|
|
326
|
+
ideaSlack
|
|
327
|
+
.command('add <idea> <permalink>')
|
|
328
|
+
.description('Attach a Slack thread to an idea')
|
|
329
|
+
.action(wrap((id, p) => (0, idea_slack_add_1.ideaSlackAdd)(id, p)));
|
|
330
|
+
ideaSlack
|
|
331
|
+
.command('show <idea> <contextId>')
|
|
332
|
+
.description('Show the full stored Slack thread snapshot')
|
|
333
|
+
.action(wrap((id, ctx) => (0, idea_slack_show_1.ideaSlackShow)(id, ctx)));
|
|
334
|
+
ideaSlack
|
|
335
|
+
.command('rm <idea> <contextId>')
|
|
336
|
+
.description('Remove a Slack context from an idea')
|
|
337
|
+
.action(wrap((id, ctx) => (0, idea_slack_rm_1.ideaSlackRm)(id, ctx)));
|
|
338
|
+
const ideaWeb = ideaCmd
|
|
339
|
+
.command('web')
|
|
340
|
+
.description('Attach & inspect web links on an idea');
|
|
341
|
+
ideaWeb
|
|
342
|
+
.command('add <idea> <url>')
|
|
343
|
+
.description('Attach a web link to an idea')
|
|
344
|
+
.action(wrap((id, u) => (0, idea_web_add_1.ideaWebAdd)(id, u)));
|
|
345
|
+
ideaWeb
|
|
346
|
+
.command('show <idea> <linkId>')
|
|
347
|
+
.description('Show the fetched web link body as plain text')
|
|
348
|
+
.action(wrap((id, l) => (0, idea_web_show_1.ideaWebShow)(id, l)));
|
|
349
|
+
ideaWeb
|
|
350
|
+
.command('rm <idea> <linkId>')
|
|
351
|
+
.description('Remove a web link from an idea')
|
|
352
|
+
.action(wrap((id, l) => (0, idea_web_rm_1.ideaWebRm)(id, l)));
|
|
353
|
+
const ideaFigma = ideaCmd
|
|
354
|
+
.command('figma')
|
|
355
|
+
.description('Attach Figma links to an idea');
|
|
356
|
+
ideaFigma
|
|
357
|
+
.command('add <idea> <url>')
|
|
358
|
+
.description('Attach a Figma file/frame URL to an idea')
|
|
359
|
+
.action(wrap((id, u) => (0, idea_figma_add_1.ideaFigmaAdd)({ identifier: id, url: u })));
|
|
360
|
+
ideaFigma
|
|
361
|
+
.command('list <idea>')
|
|
362
|
+
.description('List Figma links on an idea')
|
|
363
|
+
.action(wrap(id => (0, idea_figma_list_1.ideaFigmaList)({ identifier: id })));
|
|
364
|
+
ideaFigma
|
|
365
|
+
.command('rm <idea> <link-id-or-url>')
|
|
366
|
+
.description('Remove a Figma link from an idea (idempotent)')
|
|
367
|
+
.action(wrap((id, l) => (0, idea_figma_rm_1.ideaFigmaRm)({ identifier: id, linkIdOrUrl: l })));
|
|
368
|
+
ideaFigma
|
|
369
|
+
.command('refresh <idea>')
|
|
370
|
+
.description('Re-fetch Figma metadata for every link on an idea')
|
|
371
|
+
.action(wrap(id => (0, idea_figma_refresh_1.ideaFigmaRefresh)({ identifier: id })));
|
|
372
|
+
ideaFigma
|
|
373
|
+
.command('context <idea> <linkId>')
|
|
374
|
+
.description('Show cached Figma design context')
|
|
375
|
+
.action(wrap((id, l) => (0, idea_figma_context_1.ideaFigmaContext)(id, l)));
|
|
376
|
+
ideaCmd
|
|
377
|
+
.command('comment <id> <body>')
|
|
378
|
+
.description('Comment on an idea (LUM-680). <id> is the LUM-I<n> id (a bare number also resolves). Body is plain text — quote it to pass spaces or newlines.')
|
|
379
|
+
.action(wrap((id, body) => (0, idea_comment_1.ideaComment)(id, body)));
|
|
380
|
+
// Plural `comments` parent (mirrors `task comments`): `idea comment <id> <body>`
|
|
381
|
+
// already exists for posting, and commander disallows a duplicate name.
|
|
382
|
+
const ideaComments = ideaCmd
|
|
383
|
+
.command('comments')
|
|
384
|
+
.description('Inspect an idea comment thread');
|
|
385
|
+
ideaComments
|
|
386
|
+
.command('list <id>')
|
|
387
|
+
.description('List an idea comment thread (capped to the output budget; --full prints every comment).')
|
|
388
|
+
.option('--full', 'Print every comment, bypassing the output-token cap')
|
|
389
|
+
.action(wrap((id, opts) => (0, idea_comment_1.ideaCommentList)(id, opts)));
|
|
390
|
+
const initiativeCmd = program
|
|
391
|
+
.command('initiative')
|
|
392
|
+
.description('Team-level Initiatives (LUM-INIT-<n>): create one directly (no plan run required) or list them. Converter-materialised initiatives show here too.');
|
|
393
|
+
initiativeCmd
|
|
394
|
+
.command('create <goal>')
|
|
395
|
+
.description('Create a team-level initiative directly from a goal statement — no plan run required. Prints the LUM-INIT-<n> id; --assumption records the bet behind it.')
|
|
396
|
+
.option('--assumption <text>', 'The bet/assumption behind the initiative (赌注旁注)')
|
|
397
|
+
.action(wrap((goal, options) => (0, initiative_1.initiativeCreate)(goal, options)));
|
|
398
|
+
initiativeCmd
|
|
399
|
+
.command('list')
|
|
400
|
+
.description('List the team initiatives newest-first — each line shows the LUM-INIT-<n> id, status (ACTIVE|DONE|DROPPED) and goal.')
|
|
401
|
+
.action(wrap(() => (0, initiative_1.initiativeList)()));
|
|
287
402
|
const planCmd = program
|
|
288
403
|
.command('plan')
|
|
289
404
|
.description('Start a design-thinking 转换器 run (聚类→对齐→成计划) and print the gate-A web deep-link. Refuses when a run is already active (see `lumo plan status`); --abandon-active abandons the prior run first. Editing/confirmation happen in web.')
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addIdeaFigmaLink = addIdeaFigmaLink;
|
|
4
|
+
exports.listIdeaFigmaLinks = listIdeaFigmaLinks;
|
|
5
|
+
exports.removeIdeaFigmaLink = removeIdeaFigmaLink;
|
|
6
|
+
exports.refreshIdeaFigmaLinks = refreshIdeaFigmaLinks;
|
|
7
|
+
const api_1 = require("./api");
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
function buildErr(status, body) {
|
|
10
|
+
const err = Object.assign(new Error(body.error ?? `HTTP ${status}`), {
|
|
11
|
+
status,
|
|
12
|
+
});
|
|
13
|
+
if (body.code !== undefined)
|
|
14
|
+
err.code = body.code;
|
|
15
|
+
return err;
|
|
16
|
+
}
|
|
17
|
+
async function call(path, init) {
|
|
18
|
+
const creds = (0, config_1.readCredentials)();
|
|
19
|
+
if (!creds)
|
|
20
|
+
throw new Error('Not logged in. Run: lumo auth login');
|
|
21
|
+
const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
|
|
22
|
+
const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}${path}`, {
|
|
23
|
+
...init,
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${creds.token}`,
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...(init.headers ?? {}),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
let parsed = {};
|
|
32
|
+
try {
|
|
33
|
+
parsed = (await res.json());
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* non-JSON body */
|
|
37
|
+
}
|
|
38
|
+
throw buildErr(res.status, parsed);
|
|
39
|
+
}
|
|
40
|
+
return (await res.json());
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Idea-side counterpart to `cli/src/lib/figma-api.ts` (LUM-681). Same shape,
|
|
44
|
+
* same error codes (`figma_not_connected` / `figma_needs_reauth`), only the
|
|
45
|
+
* URL prefix differs: `/api/ideas/:id/...` instead of `/api/tasks/:id/...`.
|
|
46
|
+
*/
|
|
47
|
+
async function addIdeaFigmaLink(identifier, url) {
|
|
48
|
+
return call(`/api/ideas/${encodeURIComponent(identifier)}/figma`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: JSON.stringify({ url }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function listIdeaFigmaLinks(identifier) {
|
|
54
|
+
return call(`/api/ideas/${encodeURIComponent(identifier)}/figma`, {
|
|
55
|
+
method: 'GET',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function removeIdeaFigmaLink(identifier, linkId) {
|
|
59
|
+
return call(`/api/ideas/${encodeURIComponent(identifier)}/figma/${encodeURIComponent(linkId)}`, { method: 'DELETE' });
|
|
60
|
+
}
|
|
61
|
+
async function refreshIdeaFigmaLinks(identifier) {
|
|
62
|
+
return call(`/api/ideas/${encodeURIComponent(identifier)}/figma/refresh`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NEXT_STEPS_MAX = void 0;
|
|
4
|
+
exports.nextStepsSilenced = nextStepsSilenced;
|
|
5
|
+
exports.formatNextSteps = formatNextSteps;
|
|
6
|
+
exports.emitNextSteps = emitNextSteps;
|
|
7
|
+
const sanitize_1 = require("./sanitize");
|
|
8
|
+
/** Mirrors NEXT_STEPS_MAX server-side; a defence in depth, not the only cap. */
|
|
9
|
+
exports.NEXT_STEPS_MAX = 3;
|
|
10
|
+
const HEADING = 'Next:';
|
|
11
|
+
/**
|
|
12
|
+
* True when the user has muted suggestions. Hints cost tokens on every command;
|
|
13
|
+
* anyone who finds them noise must be able to turn them off for good.
|
|
14
|
+
*/
|
|
15
|
+
function nextStepsSilenced(env = process.env, opts = {}) {
|
|
16
|
+
// Commander maps `--no-hints` to `opts.hints === false`.
|
|
17
|
+
if (opts.hints === false)
|
|
18
|
+
return true;
|
|
19
|
+
const flag = env.LUMO_NO_HINTS;
|
|
20
|
+
return flag !== undefined && flag !== '' && flag !== '0';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Render the trailing suggestion block. Returns "" when there is nothing to
|
|
24
|
+
* say, so callers can append unconditionally without emitting a bare heading.
|
|
25
|
+
*/
|
|
26
|
+
function formatNextSteps(steps) {
|
|
27
|
+
const capped = steps.slice(0, exports.NEXT_STEPS_MAX);
|
|
28
|
+
if (capped.length === 0)
|
|
29
|
+
return '';
|
|
30
|
+
const lines = [HEADING];
|
|
31
|
+
for (const step of capped) {
|
|
32
|
+
const why = (0, sanitize_1.sanitizeField)(step.why);
|
|
33
|
+
lines.push(step.command === null
|
|
34
|
+
? ` ${why}`
|
|
35
|
+
: ` ${(0, sanitize_1.sanitizeField)(step.command)}\n ${why}`);
|
|
36
|
+
}
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Write the block to **stdout**, after the command's own result line.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately not stderr: suggestions are not errors, `2>/dev/null` would
|
|
43
|
+
* swallow them, some harnesses read any stderr output as failure, and with
|
|
44
|
+
* both streams redirected to one file the block can surface *above* the result
|
|
45
|
+
* it follows. Machine consumers are served by `--json` instead, where the
|
|
46
|
+
* steps ride as a field rather than as text.
|
|
47
|
+
*/
|
|
48
|
+
function emitNextSteps(steps, opts = {}, out = process.stdout, env = process.env) {
|
|
49
|
+
if (nextStepsSilenced(env, opts))
|
|
50
|
+
return;
|
|
51
|
+
const block = formatNextSteps(steps);
|
|
52
|
+
if (block === '')
|
|
53
|
+
return;
|
|
54
|
+
out.write(`\n${block}\n`);
|
|
55
|
+
}
|