@aksp/opencrew 1.6.1 → 1.6.3

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +20 -8
  3. package/package.json +2 -2
  4. package/src/cli.js +15 -38
  5. package/src/commands/init.js +41 -44
  6. package/src/commands/update.js +78 -75
  7. package/src/lib/blocos.js +148 -0
  8. package/src/lib/deteccao.js +69 -0
  9. package/src/lib/fsx.js +1 -55
  10. package/src/lib/ides.js +4 -0
  11. package/src/lib/legado.js +142 -0
  12. package/src/lib/manifest.js +67 -26
  13. package/src/lib/mcp.js +131 -0
  14. package/src/lib/migrations.js +77 -74
  15. package/src/lib/node-version.js +43 -0
  16. package/src/lib/prompts.js +25 -2
  17. package/src/lib/resumo.js +125 -0
  18. package/templates/.mcp.json +1 -1
  19. package/templates/_opencrew/.opencrew-version +1 -1
  20. package/templates/_opencrew/core/best-practices/social-networks-publishing.md +14 -14
  21. package/templates/_opencrew/core/prompts/export.prompt.md +1 -1
  22. package/templates/_opencrew/core/prompts/sherlock-shared.md +5 -5
  23. package/templates/_opencrew/core/runner.pipeline.md +32 -12
  24. package/templates/_opencrew/core/scripts/comum.mjs +49 -4
  25. package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +42 -3
  26. package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +18 -6
  27. package/templates/_opencrew/core/scripts/conferir-fontes.mjs +97 -39
  28. package/templates/_opencrew/core/scripts/verificar/regras.mjs +32 -1
  29. package/templates/_opencrew/core/scripts/verificar.mjs +7 -4
  30. package/templates/_opencrew/core/skills.engine.md +7 -3
  31. package/templates/gitignore +1 -0
  32. package/templates/skills/blotato/SKILL.md +39 -10
  33. package/templates/skills/image-ai-generator/SKILL.md +18 -5
  34. package/templates/skills/image-ai-generator/scripts/generate.py +52 -10
  35. package/templates/skills/instagram-publisher/SKILL.md +4 -0
  36. package/templates/skills/opencrew-skill-creator/references/skill-format.md +1 -0
  37. package/templates/skills/resend/SKILL.md +52 -13
package/src/lib/mcp.js ADDED
@@ -0,0 +1,131 @@
1
+ // The project's .mcp.json (R2 rules 8 to 11). The Playwright server OpenCrew ships is delivered
2
+ // ONCE, and the manifest records it (`.mcp.json` → sha256 of the text, like any delivered file):
3
+ // `init` records when it creates the file; `update` without the record creates the file or adds
4
+ // the server — whichever is missing — and records. With the record, `update` never creates the
5
+ // file nor puts the server back: removing it is the user's choice. Every rewrite copies the
6
+ // whole file to .opencrew-backup/ first and keeps its indentation and line ending. The pinned
7
+ // version is never changed, and a file out of the expected format is left as it is.
8
+ import { promises as fs } from 'node:fs';
9
+ import path from 'node:path';
10
+ import { exists } from './fsx.js';
11
+ import { backupFile, hashOf, isRecord } from './manifest.js';
12
+
13
+ /** Path of the file, relative to the project root: also its key in the manifest. */
14
+ export const MCP_FILE = '.mcp.json';
15
+
16
+ // The entry that is OpenCrew's own is the one that uses its Playwright config.
17
+ const CONFIG = '_opencrew/config/playwright.config.json';
18
+ const OUTPUT_DIR = ['--output-dir', '_opencrew/logs/playwright'];
19
+ const SKILLS = 'as skills image-creator e image-fetcher';
20
+
21
+ const hasOutputDir = (args) => args.some((a) => a === '--output-dir' || String(a).startsWith('--output-dir='));
22
+ const lacksOutputDir = (args) => Array.isArray(args) && args.some((a) => String(a).includes(CONFIG)) && !hasOutputDir(args);
23
+
24
+ /**
25
+ * `init`: write the template when the project has no .mcp.json, and record the delivery. An
26
+ * existing file is the user's: it stays as it is and nothing is recorded.
27
+ * @returns {Promise<boolean>} true when the file was created
28
+ */
29
+ export async function createMcp(ctx, template) {
30
+ const dest = path.join(ctx.target, MCP_FILE);
31
+ if (await exists(dest)) return false;
32
+ await fs.writeFile(dest, template);
33
+ ctx.files[MCP_FILE] = hashOf(template);
34
+ return true;
35
+ }
36
+
37
+ /** Indentation of the first indented line of `text`; 2 spaces when no line is indented. */
38
+ function indentOf(text) {
39
+ for (let nl = text.indexOf('\n'); nl !== -1; nl = text.indexOf('\n', nl + 1)) {
40
+ let end = nl + 1;
41
+ while (text[end] === ' ' || text[end] === '\t') end += 1;
42
+ if (end > nl + 1 && end < text.length && text[end] !== '\r' && text[end] !== '\n') return text.slice(nl + 1, end);
43
+ }
44
+ return ' ';
45
+ }
46
+
47
+ const BOM = String.fromCharCode(0xfeff); // some editors on Windows save JSON with it
48
+
49
+ /** `data` as JSON in the indentation, line ending and final line break of `text`. */
50
+ function formatLike(text, data) {
51
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
52
+ const json = (text.startsWith(BOM) ? BOM : '') + JSON.stringify(data, null, indentOf(text)).replace(/\n/g, eol);
53
+ return text.endsWith('\n') ? json + eol : json;
54
+ }
55
+
56
+ /**
57
+ * What `update` does to a .mcp.json whose content is `text`. `delivered` = the manifest has the
58
+ * record. `data` (what to write) comes only with the two actions that change the file.
59
+ */
60
+ function planMcp(text, template, delivered) {
61
+ let data;
62
+ try { data = JSON.parse(text.startsWith(BOM) ? text.slice(1) : text); } catch { return { action: 'not-json' }; }
63
+ // An object with no `mcpServers` counts as one with no server; any other shape is left alone.
64
+ const servers = isRecord(data) && data.mcpServers === undefined ? {} : data?.mcpServers;
65
+ if (!isRecord(servers)) return { action: 'bad-format' };
66
+ if (!Object.hasOwn(servers, 'playwright')) {
67
+ if (delivered) return { action: 'no-server' };
68
+ data.mcpServers = { ...servers, playwright: JSON.parse(template).mcpServers.playwright };
69
+ return { action: 'added', data };
70
+ }
71
+ const args = servers.playwright?.args;
72
+ if (!lacksOutputDir(args)) return { action: 'kept' };
73
+ args.push(...OUTPUT_DIR);
74
+ return { action: 'output-dir', data };
75
+ }
76
+
77
+ /**
78
+ * `update`: deliver the Playwright server once and send the output of the OpenCrew entry to
79
+ * _opencrew/logs/playwright/. The record always goes on to the next manifest (`ctx.files`).
80
+ * - `created` / `added`: no record — the missing file was written, or the missing server added;
81
+ * - `missing` / `no-server`: recorded before — not created, not put back;
82
+ * - `output-dir`: the OpenCrew entry had neither `--output-dir` nor `--output-dir=…`;
83
+ * - `not-json` / `bad-format`: the file is left alone, and no record is made (nothing was
84
+ * delivered: once the file is fixed, the server can still arrive);
85
+ * - `kept`: nothing to do — without a record, this is what gets recorded.
86
+ * @param {string} template text of templates/.mcp.json
87
+ * @returns {Promise<{action:string, copied:boolean}>} `copied` = the file was copied before the rewrite
88
+ */
89
+ export async function updateMcp(ctx, template) {
90
+ const dest = path.join(ctx.target, MCP_FILE);
91
+ const record = ctx.manifest?.files?.[MCP_FILE];
92
+ if (record) ctx.files[MCP_FILE] = record;
93
+ if (!(await exists(dest))) {
94
+ if (!record) await createMcp(ctx, template);
95
+ return { action: record ? 'missing' : 'created', copied: false };
96
+ }
97
+ const text = await fs.readFile(dest, 'utf8');
98
+ const plan = planMcp(text, template, Boolean(record));
99
+ if (!plan.data) {
100
+ if (plan.action === 'kept' && !record) ctx.files[MCP_FILE] = hashOf(text); // nothing was missing
101
+ return { action: plan.action, copied: false };
102
+ }
103
+ const copied = await backupFile(ctx, MCP_FILE);
104
+ const rewritten = formatLike(text, plan.data);
105
+ await fs.writeFile(dest, rewritten);
106
+ ctx.files[MCP_FILE] = hashOf(rewritten);
107
+ return { action: plan.action, copied };
108
+ }
109
+
110
+ // Spec R2 §6. `copy` = where the copy of the file is, for the two actions that rewrite it.
111
+ const MESSAGES = {
112
+ created: () => `\`.mcp.json\` criado com o servidor Playwright do OpenCrew (${SKILLS} precisam dele). Se você apagou esse arquivo de propósito, pode apagar de novo: o \`update\` não recria mais.`,
113
+ added: (copy) => `Servidor Playwright acrescentado ao \`.mcp.json\` ${copy}. Se você o removeu de propósito, pode remover de novo: o \`update\` não repõe mais.`,
114
+ 'output-dir': (copy) => `\`.mcp.json\`: a saída do Playwright agora vai para \`_opencrew/logs/playwright/\` ${copy}.`,
115
+ missing: () => `O \`.mcp.json\` não existe e não foi recriado. Sem o servidor Playwright, ${SKILLS} não funcionam.`,
116
+ 'no-server': () => `O \`.mcp.json\` está sem o servidor Playwright do OpenCrew e ficou como está. Sem ele, ${SKILLS} não funcionam.`,
117
+ 'bad-format': () => 'O `.mcp.json` não tem o formato esperado (um objeto com `mcpServers`) e não foi alterado. Confira o arquivo.',
118
+ 'not-json': () => '.mcp.json não é um JSON válido — não alterado. Confira o arquivo.',
119
+ };
120
+ const DONE = new Set(['created', 'added', 'output-dir']);
121
+
122
+ /**
123
+ * The line `update` prints for an action of `updateMcp`; null when there is nothing to say.
124
+ * @returns {{text:string, warn:boolean}|null} `warn` = a warning, not something that was done
125
+ */
126
+ export function mcpMessage(ctx, action) {
127
+ if (!MESSAGES[action]) return null;
128
+ const dir = path.relative(ctx.target, ctx.backupDir).split(path.sep).join('/');
129
+ const text = MESSAGES[action](`(cópia do arquivo anterior em \`${dir}/${MCP_FILE}\`)`);
130
+ return { text, warn: !DONE.has(action) };
131
+ }
@@ -1,11 +1,14 @@
1
1
  // What `update` does beyond refreshing _opencrew/core and the catalog skills, so that every
2
- // improvement reaches people who already use OpenCrew (AGENTS.md rule 14). The IDE detection
3
- // is shared with `init --repair-bridges`, whose helpers live here too.
2
+ // improvement reaches people who already use OpenCrew (AGENTS.md rule 14). The helpers of
3
+ // `init --repair-bridges` live here too, with the version guard the two commands share.
4
+ // The IDE detection itself is in ./deteccao.js.
4
5
  import { promises as fs } from 'node:fs';
5
6
  import path from 'node:path';
6
- import { exists, writeBridgeFile } from './fsx.js';
7
- import { IDES, allIdeIds } from './ides.js';
8
- import { deliverFile, writeManifest } from './manifest.js';
7
+ import { exists } from './fsx.js';
8
+ import { allIdeIds } from './ides.js';
9
+ import { writeManifest } from './manifest.js';
10
+ import { copyLine } from './blocos.js';
11
+ import { detectInstalledIdes } from './deteccao.js';
9
12
 
10
13
  /** Semver compare (no pre-release tags): >0 if a > b, <0 if a < b, 0 if equal. */
11
14
  export function compareVersions(a, b) {
@@ -18,33 +21,27 @@ export function compareVersions(a, b) {
18
21
  return 0;
19
22
  }
20
23
 
21
- const sharedPaths = (() => {
22
- const count = new Map();
23
- for (const ide of IDES) for (const f of ide.files) count.set(f.path, (count.get(f.path) ?? 0) + 1);
24
- return new Set([...count].filter(([, n]) => n > 1).map(([p]) => p));
25
- })();
26
-
27
- async function hasOpencrew(file) {
28
- return (await exists(file)) && /opencrew/i.test(await fs.readFile(file, 'utf8'));
24
+ /** Version stamped in the workspace by the last init or update that finished; null = no stamp. */
25
+ export async function installedVersion(target) {
26
+ const stamp = path.join(target, '_opencrew', '.opencrew-version');
27
+ return (await exists(stamp)) ? (await fs.readFile(stamp, 'utf8')).trim() : null;
29
28
  }
30
29
 
30
+ const RUN_UPDATE = 'Nada foi alterado. Rode `npx @aksp/opencrew@latest update`: ele já atualiza as pontes. Para a ponte de uma IDE nova, repita este comando depois.';
31
+
31
32
  /**
32
- * IDEs installed in `target`, detected by their own bridge files (a path shared by several
33
- * IDEs only counts for an IDE that has no file of its own — e.g. Codex).
33
+ * Spec R2, rule 6: `init --repair-bridges` only rewrites with the package at the version
34
+ * stamped in the workspace. An older package would bring old bridges back; a newer one would
35
+ * point them to files the project does not have yet. No stamp (interrupted install): it runs.
36
+ * @returns {Promise<string|null>} the message that stops the repair, or null to go on
34
37
  */
35
- export async function detectInstalledIdes(target) {
36
- const found = [];
37
- for (const ide of IDES) {
38
- const own = ide.files.filter((f) => !sharedPaths.has(f.path));
39
- const probes = own.length ? own : ide.files;
40
- for (const f of probes) {
41
- if (await hasOpencrew(path.join(target, f.path))) {
42
- found.push(ide);
43
- break;
44
- }
45
- }
46
- }
47
- return found;
38
+ export async function repairVersionGuard(target, version) {
39
+ const installed = await installedVersion(target);
40
+ const diff = installed ? compareVersions(installed, version) : 0;
41
+ if (!diff) return null;
42
+ return diff > 0
43
+ ? `Você tem a v${installed} instalada e este pacote é a v${version} (mais antigo). ${RUN_UPDATE}`
44
+ : `Este projeto está na v${installed} e este pacote é a v${version} (mais novo). O reparo não atualiza o projeto. ${RUN_UPDATE}`;
48
45
  }
49
46
 
50
47
  /**
@@ -79,57 +76,35 @@ export function backupSummary(ctx) {
79
76
  const dir = path.relative(ctx.target, ctx.backupDir).split(path.sep).join('/');
80
77
  return [
81
78
  `${ctx.copied.length} cópia(s) de segurança feita(s) antes de regravar:`,
82
- ...ctx.copied.map((file) => ` ${dir}/${file}`),
79
+ ...ctx.copied.map((file) => ` ${copyLine(ctx, file, `${dir}/${file}`)}`),
83
80
  ];
84
81
  }
85
82
 
86
- /** Rewrite the bridges of the installed IDEs only (frontmatter files whole, others by block). */
87
- export async function refreshBridges(ctx, ides) {
88
- const done = new Set();
89
- for (const ide of ides) {
90
- for (const f of ide.files) {
91
- if (done.has(f.path)) continue;
92
- done.add(f.path);
93
- const file = path.join(ctx.target, f.path);
94
- if (f.content.startsWith('---')) await deliverFile(ctx, file, f.content, { overwrite: true });
95
- else await writeBridgeFile(file, f.content);
96
- }
97
- }
98
- }
83
+ // The bridges themselves are written by `deliverBridges` (./blocos.js), for init, update and
84
+ // repair; the project's .mcp.json is handled in ./mcp.js.
99
85
 
100
- const OUTPUT_DIR = ['--output-dir', '_opencrew/logs/playwright'];
86
+ // ── What an old OpenSquad install left behind (R2 rule 16): only reported, never touched ──
101
87
 
102
- /**
103
- * Merge the Playwright server of the template into the project's .mcp.json without touching
104
- * other servers. @returns 'created' | 'updated' | 'unchanged' | 'invalid'
105
- */
106
- export async function mergeMcp(target, templateFile) {
107
- const file = path.join(target, '.mcp.json');
108
- const template = JSON.parse(await fs.readFile(templateFile, 'utf8'));
109
- if (!(await exists(file))) {
110
- await fs.writeFile(file, JSON.stringify(template, null, 2) + '\n');
111
- return 'created';
112
- }
113
- let current;
114
- try {
115
- current = JSON.parse(await fs.readFile(file, 'utf8'));
116
- } catch {
117
- return 'invalid';
118
- }
119
- current.mcpServers ??= {};
120
- const pw = current.mcpServers.playwright;
121
- if (!pw) current.mcpServers.playwright = template.mcpServers.playwright;
122
- else if (Array.isArray(pw.args) && !pw.args.includes('--output-dir')) pw.args.push(...OUTPUT_DIR);
123
- else return 'unchanged';
124
- await fs.writeFile(file, JSON.stringify(current, null, 2) + '\n');
125
- return 'updated';
126
- }
88
+ const OLD_DIR = '_opensquad/';
89
+ // Folders where a `.md` that cites `_opensquad/` is reported, down to two subfolders.
90
+ const LEFTOVER_ROOTS = ['.gemini/skills', '.claude/skills', '.agents/skills', '.agent/workflows', '.agent/rules'];
91
+ // Bridges of the OpenSquad reported by their exact path (checked with `opensquad` 0.1.15).
92
+ const LEFTOVER_BRIDGES = [
93
+ '.cursor/rules/opensquad.mdc',
94
+ '.cursor/commands/opensquad.md',
95
+ '.opencode/commands/opensquad.md',
96
+ '.qwen/skills/opensquad/SKILL.md',
97
+ '.trae/rules/opensquad.md',
98
+ '.github/prompts/opensquad.prompt.md',
99
+ '.agents/skills/opensquad/SKILL.md',
100
+ ];
127
101
 
128
- const LEGACY_ROOTS = ['.gemini/skills', '.claude/skills', '.agents/skills', '.agent/workflows', '.agent/rules'];
102
+ /** A folder `opensquad/` or a file `opensquad.*` in the path: a bridge of the OpenSquad. */
103
+ const namedOpensquad = (file) => file.split('/').some((part, i, parts) => (
104
+ i < parts.length - 1 ? part === 'opensquad' : part.startsWith('opensquad.')));
129
105
 
130
- /** Old bridges that point to a system no longer installed (e.g. `_opensquad/`). Never deleted. */
131
- export async function findLegacyBridges(target) {
132
- if (await exists(path.join(target, '_opensquad'))) return [];
106
+ /** The `.md` files under `root` (two subfolders deep) that cite `_opensquad/`. */
107
+ async function citingOldDir(target, root) {
133
108
  const found = [];
134
109
  async function walk(dir, depth) {
135
110
  let entries;
@@ -137,11 +112,39 @@ export async function findLegacyBridges(target) {
137
112
  for (const e of entries) {
138
113
  const p = path.join(dir, e.name);
139
114
  if (e.isDirectory() && depth < 2) await walk(p, depth + 1);
140
- else if (e.isFile() && e.name.endsWith('.md') && /_opensquad\//.test(await fs.readFile(p, 'utf8'))) {
115
+ else if (e.isFile() && e.name.endsWith('.md') && (await fs.readFile(p, 'utf8')).includes(OLD_DIR)) {
141
116
  found.push(path.relative(target, p).split(path.sep).join('/'));
142
117
  }
143
118
  }
144
119
  }
145
- for (const root of LEGACY_ROOTS) await walk(path.join(target, root), 0);
120
+ await walk(path.join(target, root), 0);
146
121
  return found;
147
122
  }
123
+
124
+ /** The file of `_opensquad/` the `playwright` server of .mcp.json points to; null when none. */
125
+ async function oldPlaywrightConfig(target) {
126
+ let args;
127
+ try {
128
+ args = JSON.parse(await fs.readFile(path.join(target, '.mcp.json'), 'utf8')).mcpServers.playwright.args;
129
+ } catch { return null; }
130
+ if (!Array.isArray(args)) return null;
131
+ const values = args.map((a) => String(a)).map((a) => (a.startsWith('--') && a.includes('=') ? a.slice(a.indexOf('=') + 1) : a));
132
+ return values.find((v) => v.replace(/\\/g, '/').replace(/^\.\//, '').startsWith(OLD_DIR)) ?? null;
133
+ }
134
+
135
+ /**
136
+ * Leftovers of an OpenSquad install that is no longer there. With `_opensquad` at the project
137
+ * root nothing is reported (the system is still installed). Nothing is deleted nor changed.
138
+ * @returns {Promise<{files: Array<{file:string, bridge:boolean}>, config: string|null}>}
139
+ * `files`, sorted by path: `bridge` = recognised by its name; false = it only cites
140
+ * `_opensquad/` and may be the user's. `config` = the path of `_opensquad/` (so, missing)
141
+ * that the `playwright` server of .mcp.json uses.
142
+ */
143
+ export async function findLeftovers(target) {
144
+ if (await exists(path.join(target, '_opensquad'))) return { files: [], config: null };
145
+ const found = new Set();
146
+ for (const root of LEFTOVER_ROOTS) for (const file of await citingOldDir(target, root)) found.add(file);
147
+ for (const file of LEFTOVER_BRIDGES) if (await exists(path.join(target, file))) found.add(file);
148
+ const files = [...found].sort().map((file) => ({ file, bridge: namedOpensquad(file) }));
149
+ return { files, config: await oldPlaywrightConfig(target) };
150
+ }
@@ -0,0 +1,43 @@
1
+ // Node floor (spec R2, rule 27): the lowest Node the package accepts, read from the
2
+ // `engines.node` of package.json, and the decision to stop when the machine is below it.
3
+ // Lives outside cli.js so the rule can be tested without touching `process.versions`.
4
+
5
+ /** a < b, comparing major.minor.patch as numbers (no prerelease tags). */
6
+ function lt(a, b) {
7
+ const pa = a.split('.').map(Number);
8
+ const pb = b.split('.').map(Number);
9
+ for (let i = 0; i < 3; i++) {
10
+ const na = pa[i] || 0;
11
+ const nb = pb[i] || 0;
12
+ if (na !== nb) return na < nb;
13
+ }
14
+ return false; // equal
15
+ }
16
+
17
+ /**
18
+ * Lowest version an engines.node range accepts: the first version of each `||` alternative,
19
+ * then the smallest of them. Handles ">=20.17.0", "^20.5", ">=18.0.0 || >=20.0.0", "20.0.0".
20
+ * @returns {string | null} null when the range names no version
21
+ */
22
+ export function minNodeVersion(range) {
23
+ let lowest = null;
24
+ for (const part of String(range ?? '').split('||')) {
25
+ const v = part.match(/\d+(?:\.\d+){0,2}/)?.[0];
26
+ if (v && (!lowest || lt(v, lowest))) lowest = v;
27
+ }
28
+ return lowest;
29
+ }
30
+
31
+ /**
32
+ * The two lines to show when `current` is below the floor of `range`; null when this Node
33
+ * may run the CLI (also when the package declares no floor).
34
+ * @returns {[string, string] | null}
35
+ */
36
+ export function nodeBelowFloor(range, current = process.versions.node) {
37
+ const floor = minNodeVersion(range);
38
+ if (!floor || !lt(current, floor)) return null;
39
+ return [
40
+ `O OpenCrew precisa do Node.js ${floor} ou mais novo. Esta máquina está com o v${current}.`,
41
+ 'Instale a versão LTS em https://nodejs.org/ e rode o comando de novo. Nada foi alterado nesta pasta.',
42
+ ];
43
+ }
@@ -3,7 +3,30 @@
3
3
  import { IDES, allIdeIds } from './ides.js';
4
4
  import { warn } from './ui.js';
5
5
 
6
- export async function pickIdes({ preselected } = {}) {
6
+ const loadCheckbox = () => import('@inquirer/checkbox');
7
+
8
+ /**
9
+ * Load the list's library. Only the import is guarded (spec R2, rule 28): on a Node where it
10
+ * does not load, the user gets the way out; Ctrl+C on the open list still passes through.
11
+ */
12
+ async function openIdeList(load) {
13
+ try {
14
+ return await load();
15
+ } catch (e) {
16
+ throw new Error(
17
+ `Não consegui abrir a lista de IDEs neste Node (v${process.versions.node}).\n`
18
+ + 'Atualize o Node em https://nodejs.org/ ou escolha as IDEs no próprio comando: '
19
+ + 'npx @aksp/opencrew init --ide=claude-code (ou --all para todas). Nada foi alterado nesta pasta.',
20
+ { cause: e },
21
+ );
22
+ }
23
+ }
24
+
25
+ /**
26
+ * @param {{ preselected?: string[] }} [opts]
27
+ * @param {{ load?: () => Promise<object> }} [deps] injectable for tests
28
+ */
29
+ export async function pickIdes({ preselected } = {}, { load = loadCheckbox } = {}) {
7
30
  const validIds = new Set(allIdeIds());
8
31
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
9
32
 
@@ -23,7 +46,7 @@ export async function pickIdes({ preselected } = {}) {
23
46
  warn(`Non-interactive terminal — configuring IDEs: ${chosen.join(', ')}`);
24
47
  return chosen;
25
48
  }
26
- const { default: checkbox, Separator } = await import('@inquirer/checkbox');
49
+ const { default: checkbox, Separator } = await openIdeList(load);
27
50
  const answer = await checkbox({
28
51
  message: 'Which AI IDEs do you use? (space to toggle, enter to confirm)',
29
52
  instructions: false,
@@ -0,0 +1,125 @@
1
+ // What `update` says (spec R2, rules 13 to 16; texts in §6). Each step of the command returns
2
+ // what it created, rewrote or kept, and the lines here are built ONLY from that: no line states
3
+ // something the step did not do, and a step that changed nothing gets no line. These sentences
4
+ // are in PT-BR; the rest of the CLI is not yet (→ U5).
5
+ import path from 'node:path';
6
+ import { copyLine } from './blocos.js';
7
+ import { legacyLines } from './legado.js';
8
+ import { mcpMessage } from './mcp.js';
9
+ import { UNREADABLE } from './manifest.js';
10
+ import { log, info, ok, warn } from './ui.js';
11
+
12
+ /** Last line of `update`: only what the code guarantees (a deleted crew template comes back). */
13
+ export const UNTOUCHED = 'Não foram alterados: as crews que você criou, `_opencrew/_memory/`, `_opencrew/best-practices.local/` e `.env`.';
14
+
15
+ /** `init` in a workspace that is already installed (rule 15). */
16
+ export const ALREADY_INSTALLED = 'Para atualizar, rode `npx @aksp/opencrew@latest update`. Não apague `_opencrew/` para reinstalar: a pasta guarda a sua memória (`_opencrew/_memory/`) e as suas best-practices (`_opencrew/best-practices.local/`).';
17
+
18
+ const NO_BRIDGES = 'Nenhuma ponte de IDE encontrada: nada a atualizar. Para criar a ponte de uma IDE: `npx @aksp/opencrew@latest init --repair-bridges --ide=<id>`.';
19
+ const LEAK_REMOVED = 'CLAUDE.md: removi a seção de STATUS.md que as versões 1.4.0 e 1.4.1 gravaram por engano.';
20
+ const FIRST_PROTECTED = 'Primeira atualização com proteção: sem registro anterior, guardamos tudo o que diferia. Daqui em diante, só o que você editar.';
21
+ // By action of `deliverBlock`; `kept` has no line.
22
+ const BLOCK = {
23
+ 'AGENTS.md': {
24
+ created: 'AGENTS.md criado com o bloco do OpenCrew.',
25
+ added: 'AGENTS.md: bloco do OpenCrew acrescentado no topo; o seu texto foi mantido.',
26
+ updated: 'AGENTS.md: bloco do OpenCrew atualizado.',
27
+ },
28
+ '.gitignore': {
29
+ created: '`.gitignore` criado com o bloco do OpenCrew.',
30
+ added: '`.gitignore`: bloco do OpenCrew acrescentado no fim; as suas linhas foram mantidas.',
31
+ updated: '`.gitignore`: bloco do OpenCrew atualizado.',
32
+ },
33
+ };
34
+ const LEFTOVER = {
35
+ header: (n) => `${n} resto(s) de uma instalação antiga do OpenSquad (a pasta \`_opensquad/\` não existe neste projeto). Nada foi apagado:`,
36
+ bridge: (file) => `${file} — ponte do OpenSquad; o OpenCrew não usa este arquivo.`,
37
+ cites: (file) => `${file} — cita \`_opensquad/\`; confira antes de apagar, pode ser um arquivo seu.`,
38
+ end: 'Se você não usa mais o OpenSquad, pode apagar as pontes listadas. Este aviso volta a cada `update` enquanto os arquivos existirem.',
39
+ config: (file) => `O servidor \`playwright\` do \`.mcp.json\` aponta para \`${file}\`, que não existe neste projeto (resto do OpenSquad). Não alterei o arquivo.`,
40
+ };
41
+ const MCP_REWRITTEN = new Set(['added', 'output-dir']); // actions of `updateMcp` that change the file
42
+ const MAX_COPIES = 15;
43
+
44
+ /** Print the lines a function of this module returned: `[printer, text]` each. */
45
+ export function say(lines) {
46
+ for (const [print, text] of lines) print(text);
47
+ }
48
+
49
+ /** Rule 14: the files `update` wrote in `crews/` because they were missing (`results` of deliverTree). */
50
+ export function recreatedLines(results) {
51
+ const files = results.filter((r) => r.action === 'created').map((r) => r.file);
52
+ if (!files.length) return [];
53
+ return [[info, `${files.length} arquivo(s) que faltava(m) em \`crews/\` foram entregues de novo: ${files.join(', ')}.`]];
54
+ }
55
+
56
+ const blockLines = (results) => results
57
+ .map((r) => BLOCK[r?.file]?.[r.action])
58
+ .filter(Boolean)
59
+ .map((text) => [ok, text]);
60
+
61
+ /** One line per case that happened: IDEs with a bridge rewritten, files created, or neither. */
62
+ function bridgeLines(ides, bridges, legacy = []) {
63
+ if (!ides.length) return [[info, NO_BRIDGES]];
64
+ const cleaned = new Set(legacy.filter((l) => l.action === 'removed').map((l) => l.file));
65
+ const rewritten = bridges.filter((b) => b.action === 'updated' || b.action === 'added' || cleaned.has(b.file));
66
+ const labels = [...new Set(rewritten.map((b) => b.ide.label))];
67
+ const created = bridges.filter((b) => b.action === 'created' && !b.shared).map((b) => b.file);
68
+ const lines = [];
69
+ if (labels.length) lines.push([ok, `Pontes atualizadas: ${labels.join(', ')}.`]);
70
+ if (created.length) lines.push([ok, `Pontes criadas: ${created.join(', ')}.`]);
71
+ return lines.length ? lines : [[ok, 'Pontes das IDEs já estavam em dia.']];
72
+ }
73
+
74
+ function mcpLines(ctx, mcp) {
75
+ const said = mcpMessage(ctx, mcp.action);
76
+ return said ? [[said.warn ? warn : ok, said.text]] : [];
77
+ }
78
+
79
+ /** Rule 16. `mcp` = what this run did to .mcp.json: the line only says "não alterei" if so. */
80
+ function leftoverLines({ files, config }, mcp) {
81
+ const lines = [];
82
+ if (files.length) {
83
+ lines.push([warn, LEFTOVER.header(files.length)]);
84
+ for (const { file, bridge } of files) lines.push([log, ` ${(bridge ? LEFTOVER.bridge : LEFTOVER.cites)(file)}`]);
85
+ lines.push([info, LEFTOVER.end]);
86
+ }
87
+ if (config && !MCP_REWRITTEN.has(mcp.action)) lines.push([warn, LEFTOVER.config(config)]);
88
+ return lines;
89
+ }
90
+
91
+ /** The copies of this run. `unreadable` = the manifest existed and could not be used. */
92
+ function copyLines(ctx, unreadable) {
93
+ const n = ctx.copied.length;
94
+ if (!n) return [];
95
+ const dir = path.relative(ctx.target, ctx.backupDir).split(path.sep).join('/');
96
+ const header = ctx.manifest
97
+ ? `${n} arquivo(s) foram copiados para ${dir}/ antes de serem substituídos (editados por você, ou sem registro de entrega):`
98
+ : `${n} arquivo(s) diferentes do pacote novo foram copiados para ${dir}/ antes de serem substituídos:`;
99
+ const lines = [[warn, header], ...ctx.copied.slice(0, MAX_COPIES).map((f) => [log, ` ${copyLine(ctx, f)}`])];
100
+ if (n > MAX_COPIES) lines.push([log, ` … e mais ${n - MAX_COPIES}`]);
101
+ if (unreadable) lines.push([info, UNREADABLE.summary]);
102
+ else if (!ctx.manifest) lines.push([info, FIRST_PROTECTED]);
103
+ return lines;
104
+ }
105
+
106
+ /**
107
+ * The summary of an `update`, in the order the steps ran.
108
+ * @param {object} ctx delivery context (src/lib/manifest.js)
109
+ * @param {object} done what each step returned: `agents` and `gitignore` (deliverBlock; `agents`
110
+ * is null when a pre-1.3 AGENTS.md was migrated), `ides` (detected), `bridges` (deliverBridges),
111
+ * `leak` (the STATUS.md section left CLAUDE.md's block), `mcp` (updateMcp), `leftovers`
112
+ * (findLeftovers) and `unreadable` (manifest)
113
+ * @returns {Array<[Function, string]>} lines for `say`
114
+ */
115
+ export function updateSummary(ctx, done) {
116
+ return [
117
+ ...blockLines([done.agents, done.gitignore]),
118
+ ...bridgeLines(done.ides, done.bridges, ctx.legacy),
119
+ ...(done.leak ? [[ok, LEAK_REMOVED]] : []),
120
+ ...legacyLines(ctx).map((line) => [warn, line]),
121
+ ...mcpLines(ctx, done.mcp),
122
+ ...leftoverLines(done.leftovers, done.mcp),
123
+ ...copyLines(ctx, done.unreadable),
124
+ ];
125
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "The Playwright MCP version is pinned for stability. To upgrade: npx @playwright/mcp@latest --version, then update the version below and run 'npx @aksp/opencrew update' to refresh.",
2
+ "_comment": "Versão do Playwright MCP fixada por estabilidade. Para trocar, edite a versão abaixo e reinicie a IDE. O `update` do OpenCrew não altera a versão que está neste arquivo.",
3
3
  "mcpServers": {
4
4
  "playwright": {
5
5
  "command": "npx",
@@ -1 +1 @@
1
- 1.6.1
1
+ 1.6.3