@memoring/cli 0.13.0 → 0.15.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.
@@ -0,0 +1,133 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { dim, fail, green, printTable, stringFlag } from './shared.js';
4
+ export const STATUS_LABELS = {
5
+ ACTIVE: 'Ativo',
6
+ ARCHIVED: 'Arquivado',
7
+ COMPLETED: 'Concluída',
8
+ PENDING: 'Pendente',
9
+ IN_PROGRESS: 'Em andamento',
10
+ POSTPONED: 'Adiada',
11
+ DISMISSED: 'Dispensada',
12
+ CANCELLED: 'Cancelada',
13
+ };
14
+ // PlanStatus da web usa COMPLETED no masculino ("planejamento concluído");
15
+ // STATUS_LABELS acima atende atividades/Metas ("concluída").
16
+ export const PLAN_STATUS_LABELS = {
17
+ ...STATUS_LABELS,
18
+ COMPLETED: 'Concluído',
19
+ };
20
+ export const TYPE_LABELS = {
21
+ THEORY: 'Teoria',
22
+ MAPPING: 'Mapeamento',
23
+ REVIEW: 'Revisão',
24
+ QUESTIONS: 'Questões',
25
+ TEST: 'Teste',
26
+ REINFORCEMENT: 'Reforço',
27
+ };
28
+ export const MATURITY_LABELS = {
29
+ BEGINNER: 'Iniciante',
30
+ INTERMEDIATE: 'Intermediário',
31
+ ADVANCED: 'Avançado',
32
+ };
33
+ export const ERROR_LABELS = {
34
+ UNKNOWN: 'Não identificado',
35
+ FORGETTING: 'Esquecimento',
36
+ CONCEPTUAL_CONFUSION: 'Confusão conceitual',
37
+ INATTENTION: 'Desatenção',
38
+ INTERPRETATION: 'Interpretação',
39
+ TIME_SHORTAGE: 'Falta de tempo',
40
+ };
41
+ export function object(value, message) {
42
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
43
+ fail(message);
44
+ }
45
+ return value;
46
+ }
47
+ export function readJson(file) {
48
+ try {
49
+ const raw = file === '-' ? readFileSync(0, 'utf8') : readFileSync(resolve(file), 'utf8');
50
+ return JSON.parse(raw);
51
+ }
52
+ catch (error) {
53
+ fail(`não foi possível ler JSON de ${file === '-' ? 'stdin' : file}: ${error instanceof Error ? error.message : String(error)}`);
54
+ }
55
+ }
56
+ export function positiveId(raw, label) {
57
+ if (raw === undefined || !/^\d+$/.test(raw) || Number(raw) < 1 || !Number.isSafeInteger(Number(raw))) {
58
+ fail(`${label} deve ser um inteiro positivo.`);
59
+ }
60
+ return Number(raw);
61
+ }
62
+ export function integerFlag(flags, name, options) {
63
+ const raw = stringFlag(flags, name);
64
+ if (raw === undefined) {
65
+ if (options.required) {
66
+ fail(`--${name} é obrigatório.`);
67
+ }
68
+ return undefined;
69
+ }
70
+ if (!/^-?\d+$/.test(raw)) {
71
+ fail(`--${name} deve ser um inteiro.`);
72
+ }
73
+ const value = Number(raw);
74
+ if (!Number.isSafeInteger(value) || value < options.min || (options.max !== undefined && value > options.max)) {
75
+ fail(`--${name} deve estar entre ${options.min} e ${options.max ?? '∞'}.`);
76
+ }
77
+ return value;
78
+ }
79
+ export function normalized(value) {
80
+ return value
81
+ .normalize('NFD')
82
+ .replace(/[\u0300-\u036f]/g, '')
83
+ .trim()
84
+ .replace(/[\s-]+/g, '_')
85
+ .toUpperCase();
86
+ }
87
+ export function formatPercent(value) {
88
+ return typeof value === 'number' ? `${value.toFixed(2).replace('.', ',')}%` : '—';
89
+ }
90
+ export function formatSeconds(value) {
91
+ if (typeof value !== 'number') {
92
+ return '—';
93
+ }
94
+ if (value < 60) {
95
+ return `${Math.round(value)}s`;
96
+ }
97
+ const minutes = Math.floor(value / 60);
98
+ const seconds = Math.round(value % 60);
99
+ return minutes < 60 ? `${minutes}min${seconds ? ` ${seconds}s` : ''}` : `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}`;
100
+ }
101
+ export function printActivity(activity, heading = 'Atividade atualizada') {
102
+ process.stdout.write(green(`${heading}: #${activity.id} — ${activity.title}\n`));
103
+ process.stdout.write(` tipo: ${TYPE_LABELS[activity.type] ?? activity.type} status: ${STATUS_LABELS[activity.status] ?? activity.status} estimativa: ${activity.estimatedMinutes}min\n`);
104
+ if (activity.command) {
105
+ process.stdout.write(` ${activity.command}\n`);
106
+ }
107
+ }
108
+ export function printGoal(details, heading) {
109
+ process.stdout.write(green(`${heading}: Meta #${details.goal.number} (id ${details.goal.id}) — alvo de ${details.goal.activityTarget} atividades.\n`));
110
+ if (details.activities.length === 0) {
111
+ process.stdout.write('Nenhuma atividade nesta Meta.\n');
112
+ return;
113
+ }
114
+ printTable(details.activities.map((activity) => ({
115
+ id: `#${activity.id}`,
116
+ type: TYPE_LABELS[activity.type] ?? activity.type,
117
+ title: activity.title,
118
+ status: STATUS_LABELS[activity.status] ?? activity.status,
119
+ minutes: `${activity.estimatedMinutes}min`,
120
+ questions: activity.questionTarget ?? activity.questionCount ?? '—',
121
+ })), [
122
+ { key: 'id', label: 'ID' },
123
+ { key: 'type', label: 'TIPO' },
124
+ { key: 'title', label: 'ATIVIDADE' },
125
+ { key: 'status', label: 'STATUS' },
126
+ { key: 'minutes', label: 'ESTIMATIVA' },
127
+ { key: 'questions', label: 'QUESTÕES' },
128
+ ]);
129
+ const first = details.activities.find((activity) => activity.status === 'PENDING' || activity.status === 'IN_PROGRESS');
130
+ if (first) {
131
+ process.stdout.write(dim(`\nPróxima: memoring plan start ${first.id}\n`));
132
+ }
133
+ }
@@ -0,0 +1,219 @@
1
+ import { apiFetch } from '../api.js';
2
+ import { deleteDraftAfterCreation, readPlanningDraft, requireReviewed, reviewDraft, } from './planDrafts.js';
3
+ import { PLAN_SCHEMA, analyze, planBody, planPayload } from './plan-analyze.js';
4
+ import { diagnostics } from './plan-diagnostics.js';
5
+ import { examsCommand } from './plan-exams.js';
6
+ import { goalCommand, recordResult } from './plan-goal.js';
7
+ import { MATURITY_LABELS, PLAN_STATUS_LABELS, STATUS_LABELS, formatPercent, object, positiveId, printActivity, } from './plan-shared.js';
8
+ import { bold, fail, green, printJson, printTable, stringFlag } from './shared.js';
9
+ /**
10
+ * `memoring plan` — dispatcher dos subcomandos do Planejamento. O módulo
11
+ * ficou grande demais como arquivo único; cada família coesa vive no seu
12
+ * próprio módulo: `plan-analyze` (schema/analyze/create payload), `plan-goal`
13
+ * (current/next/start/complete/postpone/dismiss/cancel/result), `plan-exams`,
14
+ * `plan-diagnostics` e `plan-shared` (rótulos, parsers e impressão).
15
+ */
16
+ function printHelp() {
17
+ process.stdout.write(`Uso: memoring plan <subcomando> [argumentos] [flags]
18
+
19
+ schema contrato JSON editável do planejamento e das provas
20
+ analyze <pdfs...> analisa PDFs no backend e aguarda a proposta
21
+ [--out proposta.json] salva o rascunho (com --json, imprime só o rascunho)
22
+ [--web] [--no-open] envia a proposta para revisão web, sem criar o plano
23
+ review <draftId> [--wait] lê o rascunho; --wait aguarda REVIEWED
24
+ [--timeout N] [--out f] timeout em segundos; exporta o payload já REVIEWED
25
+ create --file proposta.json cria o planejamento após sua revisão no arquivo
26
+ create --draft <uuid> cria somente de um rascunho PLAN REVIEWED e o exclui
27
+ list lista planejamentos
28
+ show <planId> detalha disciplinas e assuntos
29
+ current <planId> mostra a Meta ativa
30
+ next <planId> --activities N gera a próxima Meta (activityTarget obrigatório)
31
+ next <planId> --web cria handoff de Meta para revisão web, sem gerar a Meta
32
+ next <planId> --draft <uuid> gera de um rascunho GOAL REVIEWED e o exclui
33
+ start|complete|postpone|dismiss|cancel <activityId>
34
+ transições de uma atividade
35
+ (cancel = redução definitiva de escopo:
36
+ encerra na Meta e nunca é recomposta)
37
+ result <activityId> --total N --correct N [--seconds N]
38
+ [--errors motivo:N,...] registra desempenho e motivos dos erros
39
+ tec-result <activityId> importa resultado do caderno TEC já disponível
40
+ performance|error-patterns|comparison|journey <planId>
41
+ diagnósticos do planejamento
42
+ coordinates coordenadas do método de planejamento
43
+ exams list lista provas
44
+ exams create --file prova.json registra prova (ou use --name/--date/--total/--correct)
45
+ exams delete <examId> exclui prova
46
+
47
+ Maturidades: BEGINNER (Iniciante), INTERMEDIATE (Intermediário), ADVANCED (Avançado).
48
+ Motivos de erro: UNKNOWN, FORGETTING, CONCEPTUAL_CONFUSION, INATTENTION, INTERPRETATION, TIME_SHORTAGE (aliases em PT-BR também são aceitos).
49
+ `);
50
+ }
51
+ export async function planCommand(parsed) {
52
+ const [sub, ...rest] = parsed._;
53
+ switch (sub) {
54
+ case 'schema':
55
+ printJson(PLAN_SCHEMA);
56
+ return;
57
+ case 'analyze':
58
+ await analyze(parsed, rest);
59
+ return;
60
+ case 'review':
61
+ case 'draft':
62
+ await reviewDraft(parsed, rest[0]);
63
+ return;
64
+ case 'create': {
65
+ const file = stringFlag(parsed.flags, 'file');
66
+ if (file && parsed.flags.draft !== undefined) {
67
+ fail('use --file ou --draft, não os dois.');
68
+ }
69
+ let body;
70
+ let reviewedDraft = null;
71
+ if (parsed.flags.draft !== undefined) {
72
+ reviewedDraft = await readPlanningDraft(stringFlag(parsed.flags, 'draft'));
73
+ requireReviewed(reviewedDraft, 'PLAN');
74
+ body = planPayload(reviewedDraft.payload);
75
+ }
76
+ else if (file) {
77
+ body = planBody(file);
78
+ }
79
+ else {
80
+ fail('informe a proposta revisada: memoring plan create --file proposta.json ou --draft <uuid>.');
81
+ }
82
+ const details = (await apiFetch('/planning/plans', { method: 'POST', body }));
83
+ if (reviewedDraft) {
84
+ await deleteDraftAfterCreation(reviewedDraft);
85
+ }
86
+ if (parsed.flags.json) {
87
+ printJson(details);
88
+ return;
89
+ }
90
+ const plan = object(details.plan, 'resposta inválida ao criar planejamento.');
91
+ process.stdout.write(green(`Planejamento "${String(plan.name)}" criado (#${String(plan.id)}).\n`));
92
+ process.stdout.write(`Gere a primeira Meta: memoring plan next ${String(plan.id)} --activities N\n`);
93
+ return;
94
+ }
95
+ case 'list': {
96
+ const plans = (await apiFetch('/planning/plans'));
97
+ if (parsed.flags.json) {
98
+ printJson(plans);
99
+ return;
100
+ }
101
+ if (plans.length === 0) {
102
+ process.stdout.write('Nenhum planejamento. Analise PDFs com `memoring plan analyze <pdfs...>`.\n');
103
+ return;
104
+ }
105
+ printTable(plans.map((plan) => ({
106
+ id: `#${String(plan.id)}`,
107
+ name: plan.name,
108
+ status: PLAN_STATUS_LABELS[String(plan.status)] ?? plan.status,
109
+ subjects: plan.activeSubjectLimit,
110
+ weekly: plan.weeklyMinutes ? `${String(plan.weeklyMinutes)}min` : '—',
111
+ })), [
112
+ { key: 'id', label: 'ID' },
113
+ { key: 'name', label: 'PLANEJAMENTO' },
114
+ { key: 'status', label: 'STATUS' },
115
+ { key: 'subjects', label: 'DISCIPLINAS ATIVAS' },
116
+ { key: 'weekly', label: 'TEMPO SEMANAL' },
117
+ ]);
118
+ return;
119
+ }
120
+ case 'show': {
121
+ const planId = positiveId(rest[0], 'planId');
122
+ const details = (await apiFetch(`/planning/plans/${planId}`));
123
+ if (parsed.flags.json) {
124
+ printJson(details);
125
+ return;
126
+ }
127
+ const plan = object(details.plan, 'resposta inválida ao consultar planejamento.');
128
+ process.stdout.write(bold(`${String(plan.name)} (#${planId}) — ${PLAN_STATUS_LABELS[String(plan.status)] ?? String(plan.status)}\n`));
129
+ if (plan.objective) {
130
+ process.stdout.write(`${String(plan.objective)}\n`);
131
+ }
132
+ const programs = Array.isArray(details.programs) ? details.programs : [];
133
+ const topics = Array.isArray(details.topics) ? details.topics : [];
134
+ for (const program of programs) {
135
+ process.stdout.write(`\n${String(program.subjectName)} — ${MATURITY_LABELS[String(program.maturity)] ?? String(program.maturity)}\n`);
136
+ const programTopics = topics.filter((topic) => topic.programId === program.id);
137
+ printTable(programTopics.map((topic) => ({
138
+ id: `#${String(topic.id)}`,
139
+ title: topic.title,
140
+ pages: topic.pageFrom ? `${String(topic.pageFrom)}${topic.pageTo !== topic.pageFrom ? `–${String(topic.pageTo)}` : ''}` : '—',
141
+ minutes: `${String(topic.estimatedMinutes)}min`,
142
+ status: STATUS_LABELS[String(topic.status)] ?? topic.status,
143
+ })), [
144
+ { key: 'id', label: 'ID' },
145
+ { key: 'title', label: 'ASSUNTO' },
146
+ { key: 'pages', label: 'PÁGINAS' },
147
+ { key: 'minutes', label: 'ESTIMATIVA' },
148
+ { key: 'status', label: 'STATUS' },
149
+ ]);
150
+ }
151
+ return;
152
+ }
153
+ case 'current':
154
+ case 'next':
155
+ await goalCommand(sub, positiveId(rest[0], 'planId'), parsed);
156
+ return;
157
+ case 'start':
158
+ case 'complete':
159
+ case 'postpone':
160
+ case 'dismiss':
161
+ case 'cancel': {
162
+ const activityId = positiveId(rest[0], 'activityId');
163
+ const activity = (await apiFetch(`/planning/activities/${activityId}/${sub}`, { method: 'POST' }));
164
+ if (parsed.flags.json) {
165
+ printJson(activity);
166
+ }
167
+ else {
168
+ printActivity(activity);
169
+ }
170
+ return;
171
+ }
172
+ case 'result':
173
+ await recordResult(positiveId(rest[0], 'activityId'), parsed);
174
+ return;
175
+ case 'tec-result': {
176
+ const activityId = positiveId(rest[0], 'activityId');
177
+ const result = (await apiFetch(`/planning/activities/${activityId}/tec-result`, { method: 'POST' }));
178
+ if (parsed.flags.json) {
179
+ printJson(result);
180
+ return;
181
+ }
182
+ process.stdout.write(green(`Resultado TEC importado para a atividade #${activityId}.\n`));
183
+ process.stdout.write(` ${String(result.correctCount)}/${String(result.questionCount)} corretas (${formatPercent(result.accuracyPercent)}); ` +
184
+ `${String(result.errorCount)} erros.\n`);
185
+ return;
186
+ }
187
+ case 'performance':
188
+ case 'error-patterns':
189
+ case 'comparison':
190
+ case 'journey':
191
+ await diagnostics(sub, positiveId(rest[0], 'planId'), parsed.flags.json === true);
192
+ return;
193
+ case 'coordinates': {
194
+ const guides = (await apiFetch('/planning/coordinates'));
195
+ if (parsed.flags.json) {
196
+ printJson(guides);
197
+ return;
198
+ }
199
+ for (const guide of guides) {
200
+ process.stdout.write(bold(`${String(guide.title)}\n`));
201
+ process.stdout.write(`${String(guide.description)}\n`);
202
+ const actions = Array.isArray(guide.actions) ? guide.actions : [];
203
+ for (const action of actions) {
204
+ process.stdout.write(` • ${String(action)}\n`);
205
+ }
206
+ process.stdout.write('\n');
207
+ }
208
+ return;
209
+ }
210
+ case 'exams':
211
+ await examsCommand(parsed, rest);
212
+ return;
213
+ default:
214
+ printHelp();
215
+ if (sub) {
216
+ process.exitCode = 1;
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,162 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { apiFetch } from '../api.js';
4
+ import { openBrowser } from '../auth.js';
5
+ import { loadConfig } from '../config.js';
6
+ import { fail, green, printJson, stringFlag, yellow } from './shared.js';
7
+ const DRAFT_POLL_INTERVAL_MS = 2_000;
8
+ const DRAFT_REVIEW_TIMEOUT_SECONDS = 10 * 60;
9
+ function object(value, message) {
10
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
11
+ fail(message);
12
+ }
13
+ return value;
14
+ }
15
+ function draftId(raw) {
16
+ if (!raw || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
17
+ fail('draftId deve ser um UUID.');
18
+ }
19
+ return raw;
20
+ }
21
+ function planningDraft(value) {
22
+ const draft = object(value, 'resposta inválida ao consultar rascunho de planejamento.');
23
+ const id = draftId(typeof draft.id === 'string' ? draft.id : undefined);
24
+ if (draft.kind !== 'PLAN' && draft.kind !== 'GOAL') {
25
+ fail('resposta inválida: kind do rascunho deve ser PLAN ou GOAL.');
26
+ }
27
+ if (draft.status !== 'OPEN' && draft.status !== 'REVIEWED') {
28
+ fail('resposta inválida: status do rascunho deve ser OPEN ou REVIEWED.');
29
+ }
30
+ if (draft.planId !== null && (typeof draft.planId !== 'number' || !Number.isSafeInteger(draft.planId) || draft.planId < 1)) {
31
+ fail('resposta inválida: planId do rascunho deve ser um inteiro positivo ou null.');
32
+ }
33
+ for (const field of ['createdAt', 'updatedAt', 'expiresAt']) {
34
+ if (typeof draft[field] !== 'string') {
35
+ fail(`resposta inválida: ${field} do rascunho é obrigatório.`);
36
+ }
37
+ }
38
+ return {
39
+ id,
40
+ kind: draft.kind,
41
+ planId: draft.planId,
42
+ payload: object(draft.payload, 'resposta inválida: payload do rascunho deve ser um objeto.'),
43
+ status: draft.status,
44
+ createdAt: draft.createdAt,
45
+ updatedAt: draft.updatedAt,
46
+ expiresAt: draft.expiresAt,
47
+ };
48
+ }
49
+ function draftReviewUrl(draft) {
50
+ const webBase = loadConfig().webBase.replace(/\/+$/, '');
51
+ if (draft.kind === 'PLAN') {
52
+ return `${webBase}/planning/new?draft=${encodeURIComponent(draft.id)}`;
53
+ }
54
+ if (draft.planId === null) {
55
+ fail('resposta inválida: rascunho de Meta sem planId.');
56
+ }
57
+ return `${webBase}/planning/${draft.planId}?goalDraft=${encodeURIComponent(draft.id)}`;
58
+ }
59
+ function nextDraftCommand(draft) {
60
+ return draft.kind === 'PLAN'
61
+ ? `memoring plan create --draft ${draft.id} --json`
62
+ : `memoring plan next ${draft.planId} --draft ${draft.id} --json`;
63
+ }
64
+ function reviewTimeoutSeconds(parsed) {
65
+ const raw = stringFlag(parsed.flags, 'timeout');
66
+ if (raw === undefined) {
67
+ return DRAFT_REVIEW_TIMEOUT_SECONDS;
68
+ }
69
+ if (!/^-?\d+$/.test(raw)) {
70
+ fail('--timeout deve ser um inteiro.');
71
+ }
72
+ const value = Number(raw);
73
+ if (!Number.isSafeInteger(value) || value < 1 || value > 86_400) {
74
+ fail('--timeout deve estar entre 1 e 86400.');
75
+ }
76
+ return value;
77
+ }
78
+ export async function readPlanningDraft(rawId) {
79
+ const id = draftId(rawId);
80
+ return planningDraft(await apiFetch(`/planning/drafts/${id}`));
81
+ }
82
+ export function requireReviewed(draft, kind, planId) {
83
+ if (draft.kind !== kind) {
84
+ fail(`o rascunho ${draft.id} é do tipo ${draft.kind}; era esperado ${kind}.`);
85
+ }
86
+ if (draft.status !== 'REVIEWED') {
87
+ fail(`o rascunho ${draft.id} ainda está ${draft.status}; conclua a revisão web antes de criar.`);
88
+ }
89
+ if (kind === 'PLAN' && draft.planId !== null) {
90
+ fail('o rascunho de planejamento não pode estar associado a um planId.');
91
+ }
92
+ if (kind === 'GOAL' && draft.planId !== planId) {
93
+ fail(`o rascunho ${draft.id} pertence ao planejamento #${draft.planId ?? 'desconhecido'}, não ao #${planId}.`);
94
+ }
95
+ }
96
+ export async function createDraftHandoff(kind, planId, payload, parsed) {
97
+ const draft = planningDraft(await apiFetch('/planning/drafts', {
98
+ method: 'POST',
99
+ body: { kind, planId, payload },
100
+ }));
101
+ const reviewUrl = draftReviewUrl(draft);
102
+ const handoff = { draft, reviewUrl, nextCommand: nextDraftCommand(draft) };
103
+ if (!parsed.flags['no-open']) {
104
+ openBrowser(reviewUrl);
105
+ }
106
+ if (parsed.flags.json) {
107
+ printJson(handoff);
108
+ return;
109
+ }
110
+ process.stdout.write(green('Rascunho criado. Nenhum planejamento ou Meta foi criado.\n'));
111
+ process.stdout.write(`Revise no navegador: ${reviewUrl}\n`);
112
+ process.stdout.write('O botão da página apenas conclui a revisão; depois volte ao terminal.\n');
113
+ process.stdout.write(`Consulte ou aguarde: memoring plan review ${draft.id} --wait\n`);
114
+ process.stdout.write(`Quando estiver REVIEWED, rode: ${handoff.nextCommand}\n`);
115
+ }
116
+ export async function deleteDraftAfterCreation(draft) {
117
+ try {
118
+ await apiFetch(`/planning/drafts/${draft.id}`, { method: 'DELETE' });
119
+ }
120
+ catch (error) {
121
+ process.stderr.write(yellow(`Aviso: a criação foi concluída, mas não foi possível excluir o rascunho ${draft.id} ` +
122
+ `(${error instanceof Error ? error.message : String(error)}). Ele expira em ${draft.expiresAt}.\n`));
123
+ }
124
+ }
125
+ export async function reviewDraft(parsed, rawId) {
126
+ let draft = await readPlanningDraft(rawId);
127
+ if (parsed.flags.wait && draft.status === 'OPEN') {
128
+ const timeoutSeconds = reviewTimeoutSeconds(parsed);
129
+ const deadline = Date.now() + timeoutSeconds * 1_000;
130
+ if (!parsed.flags.json) {
131
+ process.stderr.write(`Aguardando a revisão do rascunho ${draft.id} por até ${timeoutSeconds}s…\n`);
132
+ }
133
+ while (draft.status === 'OPEN') {
134
+ if (Date.now() >= deadline) {
135
+ fail(`a revisão não foi concluída em ${timeoutSeconds}s; o rascunho continua OPEN. Consulte depois com memoring plan review ${draft.id}.`);
136
+ }
137
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, DRAFT_POLL_INTERVAL_MS));
138
+ draft = await readPlanningDraft(draft.id);
139
+ }
140
+ }
141
+ const output = stringFlag(parsed.flags, 'out');
142
+ if (output && draft.status !== 'REVIEWED') {
143
+ fail(`o rascunho ${draft.id} ainda está OPEN; use --wait ou conclua a revisão antes de exportar o payload.`);
144
+ }
145
+ if (output) {
146
+ writeFileSync(resolve(output), `${JSON.stringify(draft.payload, null, 2)}\n`, 'utf8');
147
+ }
148
+ if (parsed.flags.json) {
149
+ printJson(draft);
150
+ return;
151
+ }
152
+ process.stdout.write(`${draft.kind} ${draft.id}: ${draft.status} (expira em ${draft.expiresAt}).\n`);
153
+ if (draft.status === 'OPEN') {
154
+ process.stdout.write(`Revise em ${draftReviewUrl(draft)}\n`);
155
+ process.stdout.write(`Para aguardar: memoring plan review ${draft.id} --wait\n`);
156
+ return;
157
+ }
158
+ if (output) {
159
+ process.stdout.write(`Payload revisado salvo em ${output}.\n`);
160
+ }
161
+ process.stdout.write(`Próximo passo: ${nextDraftCommand(draft)}\n`);
162
+ }