@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.
- package/README.md +162 -9
- package/dist/api.js +90 -4
- package/dist/args.js +14 -1
- package/dist/auth.js +1 -1
- package/dist/commands/batch.js +327 -0
- package/dist/commands/materialReferences.js +359 -0
- package/dist/commands/plan-analyze.js +256 -0
- package/dist/commands/plan-diagnostics.js +65 -0
- package/dist/commands/plan-exams.js +122 -0
- package/dist/commands/plan-goal.js +131 -0
- package/dist/commands/plan-shared.js +133 -0
- package/dist/commands/plan.js +219 -0
- package/dist/commands/planDrafts.js +162 -0
- package/dist/commands/scratch.js +632 -0
- package/dist/commands/stage.js +215 -77
- package/dist/config.js +2 -0
- package/dist/main.js +43 -2
- package/dist/pdf/batchValidation.js +72 -0
- package/dist/pdf/documentCache.js +68 -0
- package/dist/pdf/quoteLocate.js +181 -0
- package/package.json +8 -2
- package/skills/memoring-cli/SKILL.md +57 -7
|
@@ -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
|
+
}
|