@neetru/cli 2.29.0 → 2.32.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.
Files changed (259) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/README.md +63 -17
  3. package/dist/cli-kit/choices.d.ts +33 -0
  4. package/dist/cli-kit/choices.js +32 -0
  5. package/dist/cli-kit/command-context.d.ts +98 -0
  6. package/dist/cli-kit/command-context.js +108 -0
  7. package/dist/cli-kit/errors.d.ts +79 -0
  8. package/dist/cli-kit/errors.js +94 -0
  9. package/dist/cli-kit/index.d.ts +8 -0
  10. package/dist/cli-kit/index.js +8 -0
  11. package/dist/cli-kit/suggest.d.ts +74 -0
  12. package/dist/cli-kit/suggest.js +207 -0
  13. package/dist/commands/admin/oidc-login.js +1 -1
  14. package/dist/commands/admin/sdk-credential.js +1 -1
  15. package/dist/commands/autocomplete.d.ts +1 -1
  16. package/dist/commands/autocomplete.js +1 -0
  17. package/dist/commands/bug.d.ts +59 -0
  18. package/dist/commands/bug.js +114 -2
  19. package/dist/commands/build.d.ts +24 -1
  20. package/dist/commands/build.js +195 -73
  21. package/dist/commands/chat.d.ts +94 -1
  22. package/dist/commands/chat.js +258 -26
  23. package/dist/commands/customers.js +1 -1
  24. package/dist/commands/db-move.js +3 -3
  25. package/dist/commands/db.js +17 -24
  26. package/dist/commands/deploy.d.ts +136 -2
  27. package/dist/commands/deploy.js +547 -69
  28. package/dist/commands/deployments.js +11 -6
  29. package/dist/commands/dev-context.js +15 -5
  30. package/dist/commands/dev-up.d.ts +7 -5
  31. package/dist/commands/dev-up.js +24 -23
  32. package/dist/commands/docs-produto.d.ts +90 -0
  33. package/dist/commands/docs-produto.js +384 -0
  34. package/dist/commands/doctor.js +36 -1
  35. package/dist/commands/domains.js +85 -4
  36. package/dist/commands/env.d.ts +9 -3
  37. package/dist/commands/env.js +23 -3
  38. package/dist/commands/infra-read.js +2 -2
  39. package/dist/commands/init.d.ts +2 -0
  40. package/dist/commands/init.js +14 -1
  41. package/dist/commands/login.js +35 -8
  42. package/dist/commands/logs.js +12 -4
  43. package/dist/commands/mobile.js +33 -9
  44. package/dist/commands/new.d.ts +31 -1
  45. package/dist/commands/new.js +94 -4
  46. package/dist/commands/open.js +16 -6
  47. package/dist/commands/ops.js +1 -1
  48. package/dist/commands/products-db.d.ts +1 -0
  49. package/dist/commands/products-db.js +41 -8
  50. package/dist/commands/products.d.ts +17 -3
  51. package/dist/commands/products.js +84 -16
  52. package/dist/commands/promote.js +30 -5
  53. package/dist/commands/secrets.d.ts +91 -0
  54. package/dist/commands/secrets.js +471 -0
  55. package/dist/commands/servers.js +6 -4
  56. package/dist/commands/status.js +2 -2
  57. package/dist/commands/support.js +5 -3
  58. package/dist/commands/tenants.js +28 -16
  59. package/dist/commands/upgrade.d.ts +9 -0
  60. package/dist/commands/upgrade.js +50 -1
  61. package/dist/commands/vm-memory.js +1 -0
  62. package/dist/commands/webhooks.d.ts +2 -0
  63. package/dist/commands/webhooks.js +9 -3
  64. package/dist/commands/workspaces.js +42 -11
  65. package/dist/index.js +142 -20
  66. package/dist/lib/api-client.d.ts +22 -1
  67. package/dist/lib/api-client.js +79 -8
  68. package/dist/lib/api-error.d.ts +14 -0
  69. package/dist/lib/api-error.js +107 -46
  70. package/dist/lib/build-targets.d.ts +127 -0
  71. package/dist/lib/build-targets.js +348 -0
  72. package/dist/lib/cli-error.d.ts +13 -0
  73. package/dist/lib/cli-error.js +24 -0
  74. package/dist/lib/cli-write.d.ts +43 -9
  75. package/dist/lib/cli-write.js +154 -45
  76. package/dist/lib/compose-generator.d.ts +13 -9
  77. package/dist/lib/compose-generator.js +28 -60
  78. package/dist/lib/config-schema.d.ts +36 -2
  79. package/dist/lib/config-schema.js +11 -0
  80. package/dist/lib/db-local/db-json.d.ts +1 -1
  81. package/dist/lib/db-local/db-json.js +13 -5
  82. package/dist/lib/db-local/env.d.ts +15 -3
  83. package/dist/lib/db-local/env.js +37 -10
  84. package/dist/lib/db-pipeline/build-deps.d.ts +5 -1
  85. package/dist/lib/db-pipeline/build-deps.js +23 -3
  86. package/dist/lib/db-pipeline/types.d.ts +6 -2
  87. package/dist/lib/db-pipeline/types.js +0 -18
  88. package/dist/lib/deploy-target-contract.d.ts +152 -0
  89. package/dist/lib/deploy-target-contract.js +411 -0
  90. package/dist/lib/env-file-do-build.d.ts +19 -0
  91. package/dist/lib/env-file-do-build.js +44 -0
  92. package/dist/lib/envs.d.ts +108 -0
  93. package/dist/lib/envs.js +127 -0
  94. package/dist/lib/fs-seguro.d.ts +63 -0
  95. package/dist/lib/fs-seguro.js +108 -0
  96. package/dist/lib/mobile-local/mobile-json.js +10 -4
  97. package/dist/lib/pickers.d.ts +44 -7
  98. package/dist/lib/pickers.js +60 -4
  99. package/dist/lib/product-docs/check.d.ts +62 -0
  100. package/dist/lib/product-docs/check.js +489 -0
  101. package/dist/lib/product-docs/config.d.ts +66 -0
  102. package/dist/lib/product-docs/config.js +234 -0
  103. package/dist/lib/product-docs/dates.d.ts +17 -0
  104. package/dist/lib/product-docs/dates.js +41 -0
  105. package/dist/lib/product-docs/frontmatter.d.ts +43 -0
  106. package/dist/lib/product-docs/frontmatter.js +302 -0
  107. package/dist/lib/product-docs/git.d.ts +16 -0
  108. package/dist/lib/product-docs/git.js +92 -0
  109. package/dist/lib/product-docs/markdown.d.ts +27 -0
  110. package/dist/lib/product-docs/markdown.js +98 -0
  111. package/dist/lib/product-docs/ops.d.ts +133 -0
  112. package/dist/lib/product-docs/ops.js +975 -0
  113. package/dist/lib/product-docs/structure.d.ts +61 -0
  114. package/dist/lib/product-docs/structure.js +239 -0
  115. package/dist/lib/product-docs/sync.d.ts +107 -0
  116. package/dist/lib/product-docs/sync.js +322 -0
  117. package/dist/lib/product-docs/taxonomy.d.ts +46 -0
  118. package/dist/lib/product-docs/taxonomy.js +163 -0
  119. package/dist/lib/product-docs/templates.d.ts +53 -0
  120. package/dist/lib/product-docs/templates.js +245 -0
  121. package/dist/lib/public-secret-scan.d.ts +117 -0
  122. package/dist/lib/public-secret-scan.js +771 -0
  123. package/dist/lib/render.d.ts +16 -0
  124. package/dist/lib/render.js +34 -4
  125. package/dist/lib/segredo-produto.d.ts +124 -0
  126. package/dist/lib/segredo-produto.js +336 -0
  127. package/dist/lib/texto-utf8.d.ts +96 -0
  128. package/dist/lib/texto-utf8.js +196 -0
  129. package/package.json +2 -1
  130. package/dist/cli-kit/format.js.map +0 -1
  131. package/dist/cli-kit/glyphs.js.map +0 -1
  132. package/dist/cli-kit/index.js.map +0 -1
  133. package/dist/cli-kit/palette.js.map +0 -1
  134. package/dist/commands/add.js.map +0 -1
  135. package/dist/commands/admin/iam-check.js.map +0 -1
  136. package/dist/commands/admin/oidc-login.js.map +0 -1
  137. package/dist/commands/admin/sdk-credential.js.map +0 -1
  138. package/dist/commands/agent-release.js.map +0 -1
  139. package/dist/commands/agent-write.js.map +0 -1
  140. package/dist/commands/ai-models.js.map +0 -1
  141. package/dist/commands/ai.js.map +0 -1
  142. package/dist/commands/api-catalog.js.map +0 -1
  143. package/dist/commands/archive.js.map +0 -1
  144. package/dist/commands/artifact-registry.js.map +0 -1
  145. package/dist/commands/audit.js.map +0 -1
  146. package/dist/commands/auth.js.map +0 -1
  147. package/dist/commands/autocomplete.js.map +0 -1
  148. package/dist/commands/billing.js.map +0 -1
  149. package/dist/commands/bootstrap.js.map +0 -1
  150. package/dist/commands/bug.js.map +0 -1
  151. package/dist/commands/build.js.map +0 -1
  152. package/dist/commands/chat-menu.js.map +0 -1
  153. package/dist/commands/chat.js.map +0 -1
  154. package/dist/commands/cloud-run.js.map +0 -1
  155. package/dist/commands/code.js.map +0 -1
  156. package/dist/commands/config.js.map +0 -1
  157. package/dist/commands/customers.js.map +0 -1
  158. package/dist/commands/db-move.js.map +0 -1
  159. package/dist/commands/db.js.map +0 -1
  160. package/dist/commands/deploy.js.map +0 -1
  161. package/dist/commands/deployments.js.map +0 -1
  162. package/dist/commands/dev-context.js.map +0 -1
  163. package/dist/commands/dev-up.js.map +0 -1
  164. package/dist/commands/dev.js.map +0 -1
  165. package/dist/commands/docs.js.map +0 -1
  166. package/dist/commands/doctor.js.map +0 -1
  167. package/dist/commands/domains.js.map +0 -1
  168. package/dist/commands/dr.js.map +0 -1
  169. package/dist/commands/env.js.map +0 -1
  170. package/dist/commands/hosting-write.js.map +0 -1
  171. package/dist/commands/infra-read.js.map +0 -1
  172. package/dist/commands/init.js.map +0 -1
  173. package/dist/commands/login.js.map +0 -1
  174. package/dist/commands/logout.js.map +0 -1
  175. package/dist/commands/logs.js.map +0 -1
  176. package/dist/commands/mobile.js.map +0 -1
  177. package/dist/commands/mocks.js.map +0 -1
  178. package/dist/commands/new.js.map +0 -1
  179. package/dist/commands/open.js.map +0 -1
  180. package/dist/commands/ops.js.map +0 -1
  181. package/dist/commands/products-db.js.map +0 -1
  182. package/dist/commands/products.js.map +0 -1
  183. package/dist/commands/promote.js.map +0 -1
  184. package/dist/commands/publish.js.map +0 -1
  185. package/dist/commands/schema.js.map +0 -1
  186. package/dist/commands/servers.js.map +0 -1
  187. package/dist/commands/smoke.js.map +0 -1
  188. package/dist/commands/status.js.map +0 -1
  189. package/dist/commands/support.js.map +0 -1
  190. package/dist/commands/surface-status.js.map +0 -1
  191. package/dist/commands/tenants.js.map +0 -1
  192. package/dist/commands/ui.js.map +0 -1
  193. package/dist/commands/upgrade.js.map +0 -1
  194. package/dist/commands/validate.js.map +0 -1
  195. package/dist/commands/vm-memory.js.map +0 -1
  196. package/dist/commands/vm.js.map +0 -1
  197. package/dist/commands/webhooks.js.map +0 -1
  198. package/dist/commands/whoami.js.map +0 -1
  199. package/dist/commands/workspaces.js.map +0 -1
  200. package/dist/index.js.map +0 -1
  201. package/dist/lib/agent-artifact-url.js.map +0 -1
  202. package/dist/lib/agent-commands.js.map +0 -1
  203. package/dist/lib/ai/auth-discovery.js.map +0 -1
  204. package/dist/lib/ai/context.js.map +0 -1
  205. package/dist/lib/ai/orchestrator.js.map +0 -1
  206. package/dist/lib/api-client.js.map +0 -1
  207. package/dist/lib/api-error.js.map +0 -1
  208. package/dist/lib/auth.js.map +0 -1
  209. package/dist/lib/cli-read.js.map +0 -1
  210. package/dist/lib/cli-write.js.map +0 -1
  211. package/dist/lib/code-agent-budget.js.map +0 -1
  212. package/dist/lib/code-agent-files.js.map +0 -1
  213. package/dist/lib/code-agent-gateway.js.map +0 -1
  214. package/dist/lib/code-agent-git-tools.js.map +0 -1
  215. package/dist/lib/code-agent-protocol.js.map +0 -1
  216. package/dist/lib/code-agent-session.js.map +0 -1
  217. package/dist/lib/code-agent-settings.js.map +0 -1
  218. package/dist/lib/code-agent-skills.js.map +0 -1
  219. package/dist/lib/code-agent-subagents.js.map +0 -1
  220. package/dist/lib/code-agent-tools.js.map +0 -1
  221. package/dist/lib/code-agent-tui.js.map +0 -1
  222. package/dist/lib/code-agent-types.js.map +0 -1
  223. package/dist/lib/code-agent-ui.js.map +0 -1
  224. package/dist/lib/code-agent-web-tools.js.map +0 -1
  225. package/dist/lib/code-agent-write-tools.js.map +0 -1
  226. package/dist/lib/compose-generator.js.map +0 -1
  227. package/dist/lib/config-schema.js.map +0 -1
  228. package/dist/lib/config.js.map +0 -1
  229. package/dist/lib/db-local/db-json.js.map +0 -1
  230. package/dist/lib/db-local/env.js.map +0 -1
  231. package/dist/lib/db-local/fingerprint.js.map +0 -1
  232. package/dist/lib/db-local/index.js.map +0 -1
  233. package/dist/lib/db-pipeline/build-deps.js.map +0 -1
  234. package/dist/lib/db-pipeline/errors.js.map +0 -1
  235. package/dist/lib/db-pipeline/index.js.map +0 -1
  236. package/dist/lib/db-pipeline/pipeline.js.map +0 -1
  237. package/dist/lib/db-pipeline/rehearse.js.map +0 -1
  238. package/dist/lib/db-pipeline/types.js.map +0 -1
  239. package/dist/lib/dev-local-context.js.map +0 -1
  240. package/dist/lib/iam-preflight.js.map +0 -1
  241. package/dist/lib/mobile-local/index.js.map +0 -1
  242. package/dist/lib/mobile-local/mobile-json.js.map +0 -1
  243. package/dist/lib/pickers.js.map +0 -1
  244. package/dist/lib/product-config.js.map +0 -1
  245. package/dist/lib/render.js.map +0 -1
  246. package/dist/lib/schema-cache.js.map +0 -1
  247. package/dist/lib/sse.js.map +0 -1
  248. package/dist/lib/telemetry.js.map +0 -1
  249. package/dist/lib/token-resolver.js.map +0 -1
  250. package/dist/lib/url-allowlist.js.map +0 -1
  251. package/dist/lib/version-check.js.map +0 -1
  252. package/dist/utils/logger.js.map +0 -1
  253. package/dist/utils/logo.js.map +0 -1
  254. package/dist/utils/safe-exit.js.map +0 -1
  255. package/dist/utils/spinner-registry.js.map +0 -1
  256. package/dist/version.js.map +0 -1
  257. package/templates/dev/Dockerfile.agent +0 -39
  258. package/templates/dev/agent-entrypoint.sh +0 -41
  259. package/templates/dev/docker-compose.dev.yml +0 -63
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Chave `docs` do `neetru.config.json` (PROTOCOLO_DOCS_PRODUTOS_SAAS §7.1 +
3
+ * decisão do dono 2026-10-03):
4
+ *
5
+ * ```json
6
+ * "docs": {
7
+ * "protocolVersion": 1,
8
+ * "root": "docs",
9
+ * "publish": true, // false = os docs ficam só no repositório (sync recusa)
10
+ * "publishOnDeploy": false, // true = deploy de PRODUÇÃO publica os docs no fim (opt-in)
11
+ * "api": false // true = o produto expõe /api/v1 → API_V1.md vira obrigatório
12
+ * }
13
+ * ```
14
+ */
15
+ import * as fs from 'node:fs/promises';
16
+ import * as fsSync from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import { git } from './git.js';
19
+ export const CONFIG_FILES = ['neetru.config.json', '.neetru.json'];
20
+ export const DEFAULT_DOCS_CONFIG = {
21
+ protocolVersion: 1,
22
+ root: 'docs',
23
+ publish: true,
24
+ publishOnDeploy: false,
25
+ api: false,
26
+ };
27
+ export class ProductConfigMissingError extends Error {
28
+ constructor(cwd) {
29
+ super(`neetru.config.json não encontrado em ${cwd} nem nas pastas acima até a raiz do repositório git. ` +
30
+ 'Rode `neetru init` primeiro (ou entre na pasta do produto).');
31
+ this.name = 'ProductConfigMissingError';
32
+ }
33
+ }
34
+ export class ProductConfigInvalidError extends Error {
35
+ constructor(file, why) {
36
+ super(`${file} inválido: ${why}`);
37
+ this.name = 'ProductConfigInvalidError';
38
+ }
39
+ }
40
+ function rawRoot(root) {
41
+ if (typeof root !== 'string' || !root.trim())
42
+ return undefined;
43
+ return root.trim().replace(/\\/g, '/');
44
+ }
45
+ export function normalizeDocsConfig(x) {
46
+ const d = (x && typeof x === 'object' ? x : {});
47
+ return {
48
+ protocolVersion: typeof d.protocolVersion === 'number' ? d.protocolVersion : DEFAULT_DOCS_CONFIG.protocolVersion,
49
+ root: rawRoot(d.root) ?? DEFAULT_DOCS_CONFIG.root,
50
+ publish: d.publish !== false,
51
+ publishOnDeploy: d.publishOnDeploy === true,
52
+ api: d.api === true,
53
+ };
54
+ }
55
+ function realDir(p) {
56
+ try {
57
+ return fsSync.realpathSync.native(p);
58
+ }
59
+ catch {
60
+ return path.resolve(p);
61
+ }
62
+ }
63
+ /** `child` está DENTRO de `parent` (estritamente)? */
64
+ function isInside(child, parent) {
65
+ const rel = path.relative(parent, child);
66
+ return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
67
+ }
68
+ const toPosix = (p) => p.split(path.sep).join('/');
69
+ /**
70
+ * Lê o config do produto procurando `neetru.config.json` (ou `.neetru.json`)
71
+ * em `start` e nas pastas ACIMA até a raiz do repositório git (fora de git,
72
+ * só em `start`). `docs.root` é relativo ao config e pode apontar acima dele
73
+ * (`../../docs`), desde que fique DENTRO da raiz do repositório git — fora
74
+ * disso (ou caminho absoluto) é recusado.
75
+ * Lança `ProductConfigMissingError`/`ProductConfigInvalidError`.
76
+ */
77
+ export async function loadProductRepo(start) {
78
+ const startDir = realDir(start);
79
+ const top = git(startDir, ['rev-parse', '--show-toplevel']);
80
+ const gitRoot = top ? realDir(top) : null;
81
+ const candidates = [startDir];
82
+ if (gitRoot && (isInside(startDir, gitRoot) || startDir === gitRoot)) {
83
+ let d = startDir;
84
+ while (d !== gitRoot) {
85
+ const up = path.dirname(d);
86
+ if (up === d)
87
+ break;
88
+ d = up;
89
+ candidates.push(d);
90
+ }
91
+ }
92
+ for (const configDir of candidates) {
93
+ for (const name of CONFIG_FILES) {
94
+ const configPath = path.join(configDir, name);
95
+ if (!fsSync.existsSync(configPath))
96
+ continue;
97
+ const label = toPosix(path.relative(startDir, configPath)) || name;
98
+ let raw;
99
+ try {
100
+ raw = JSON.parse((await fs.readFile(configPath, 'utf8')).replace(/^\uFEFF/, ''));
101
+ }
102
+ catch (e) {
103
+ throw new ProductConfigInvalidError(label, `JSON ilegível (${e.message})`);
104
+ }
105
+ if (!raw || typeof raw !== 'object')
106
+ throw new ProductConfigInvalidError(label, 'não é um objeto JSON');
107
+ const slug = typeof raw.slug === 'string' ? raw.slug.trim() : '';
108
+ if (!slug)
109
+ throw new ProductConfigInvalidError(label, 'falta o campo "slug"');
110
+ const nameField = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : slug;
111
+ const docs = normalizeDocsConfig(raw.docs);
112
+ // ── onde fica a pasta de docs ─────────────────────────────────────
113
+ const configured = rawRoot(raw.docs?.root);
114
+ let docsRootInferred;
115
+ let docsAbs;
116
+ if (configured) {
117
+ if (path.isAbsolute(configured) || /^[A-Za-z]:/.test(configured) || configured.startsWith('/')) {
118
+ throw new ProductConfigInvalidError(label, `docs.root "${configured}" é caminho absoluto — use um caminho relativo ao neetru.config.json.`);
119
+ }
120
+ docsAbs = path.resolve(configDir, configured);
121
+ }
122
+ else {
123
+ docsAbs = path.join(configDir, 'docs');
124
+ // Monorepo: config numa subpasta, docs/ só na raiz do git.
125
+ if (!fsSync.existsSync(docsAbs) && gitRoot && gitRoot !== configDir && fsSync.existsSync(path.join(gitRoot, 'docs'))) {
126
+ docsAbs = path.join(gitRoot, 'docs');
127
+ docsRootInferred = toPosix(path.relative(configDir, docsAbs));
128
+ }
129
+ }
130
+ const boundary = gitRoot ?? configDir;
131
+ if (!isInside(docsAbs, boundary)) {
132
+ throw new ProductConfigInvalidError(label, `docs.root "${configured ?? 'docs'}" aponta para fora do ${gitRoot ? 'repositório git' : 'diretório do produto'} (${boundary}).`);
133
+ }
134
+ const cwd = isInside(docsAbs, configDir) ? configDir : boundary;
135
+ return {
136
+ cwd,
137
+ configDir,
138
+ configPath,
139
+ gitRoot,
140
+ raw,
141
+ slug,
142
+ name: nameField,
143
+ docs: { ...docs, root: toPosix(path.relative(cwd, docsAbs)) },
144
+ hasDocsKey: !!raw.docs && typeof raw.docs === 'object',
145
+ ...(docsRootInferred ? { docsRootInferred } : {}),
146
+ };
147
+ }
148
+ }
149
+ throw new ProductConfigMissingError(startDir);
150
+ }
151
+ /** Versão do `package.json` do produto (pasta do config; senão a base). */
152
+ export function readProductVersion(repo) {
153
+ for (const dir of [repo.configDir, repo.cwd]) {
154
+ try {
155
+ const v = JSON.parse(fsSync.readFileSync(path.join(dir, 'package.json'), 'utf8').replace(/^\uFEFF/, '')).version;
156
+ if (typeof v === 'string' && v.trim())
157
+ return v.trim();
158
+ }
159
+ catch {
160
+ /* próximo */
161
+ }
162
+ }
163
+ return undefined;
164
+ }
165
+ /**
166
+ * Garante a chave `docs` no config, acrescentando SÓ as subchaves que faltam
167
+ * (nunca muda valor existente, salvo as listadas em `override`). Devolve as
168
+ * subchaves acrescentadas/alteradas.
169
+ */
170
+ export async function ensureDocsKey(repo, extra = {}, dryRun = false,
171
+ /** Subchaves de `extra` que valem mesmo se já existirem (pedido explícito do usuário). */
172
+ override = []) {
173
+ const current = (repo.raw.docs && typeof repo.raw.docs === 'object' ? repo.raw.docs : {});
174
+ const wanted = {
175
+ protocolVersion: DEFAULT_DOCS_CONFIG.protocolVersion,
176
+ root: repo.docsRootInferred ?? DEFAULT_DOCS_CONFIG.root,
177
+ publish: DEFAULT_DOCS_CONFIG.publish,
178
+ publishOnDeploy: DEFAULT_DOCS_CONFIG.publishOnDeploy,
179
+ ...extra,
180
+ };
181
+ const added = [];
182
+ const next = { ...current };
183
+ for (const [k, v] of Object.entries(wanted)) {
184
+ if (!(k in current) || (override.includes(k) && current[k] !== v)) {
185
+ next[k] = v;
186
+ added.push(k);
187
+ }
188
+ }
189
+ if (added.length === 0)
190
+ return [];
191
+ if (!dryRun) {
192
+ const updated = { ...repo.raw, docs: next };
193
+ const original = await fs.readFile(repo.configPath, 'utf8');
194
+ const eol = original.includes('\r\n') ? '\r\n' : '\n';
195
+ const indentMatch = original.match(/^[ \t]+(?=")/m);
196
+ const indent = indentMatch ? indentMatch[0] : ' ';
197
+ // Preserva o estilo do arquivo: fim de linha, indentação e (ausência de) quebra final.
198
+ const finalEol = /\r?\n$/.test(original) ? eol : '';
199
+ const text = JSON.stringify(updated, null, indent).replace(/\n/g, eol) + finalEol;
200
+ const tmp = `${repo.configPath}.${process.pid}.tmp`;
201
+ await fs.writeFile(tmp, text, 'utf8');
202
+ await fs.rename(tmp, repo.configPath);
203
+ repo.raw = updated;
204
+ // `root` em memória continua relativo à base (já resolvido no load).
205
+ repo.docs = { ...normalizeDocsConfig(next), root: repo.docs.root };
206
+ repo.hasDocsKey = true;
207
+ }
208
+ return added;
209
+ }
210
+ /**
211
+ * O produto expõe `/api/v1`? `docs.api: true`, ou app móvel registrado
212
+ * (`.neetru/mobile.json` com apps), ou o config declara `mobile`/`integrations`.
213
+ */
214
+ export function productExposesApi(repo) {
215
+ if (repo.docs.api)
216
+ return true;
217
+ const raw = repo.raw;
218
+ const truthy = (v) => v === true || (Array.isArray(v) && v.length > 0) || (!!v && typeof v === 'object' && Object.keys(v).length > 0);
219
+ if (truthy(raw.mobile) || truthy(raw.mobileApps) || truthy(raw.integrations) || truthy(raw.integracoes))
220
+ return true;
221
+ try {
222
+ const p = path.join(repo.configDir, '.neetru', 'mobile.json');
223
+ if (fsSync.existsSync(p)) {
224
+ const m = JSON.parse(fsSync.readFileSync(p, 'utf8'));
225
+ if (Array.isArray(m.apps) && m.apps.length > 0)
226
+ return true;
227
+ }
228
+ }
229
+ catch {
230
+ /* ignora */
231
+ }
232
+ return false;
233
+ }
234
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Datas dos docs (AAAA-MM-DD) no FUSO DO NEGÓCIO, nunca em UTC.
3
+ *
4
+ * Bug da migração real (2026-10-03): `toISOString()` é UTC — depois das 21h
5
+ * BRT os docs criados, o nome do relatório `MIGRACAO_DOCS_<data>`, o
6
+ * `gerado_em` e a data do CHANGELOG saíam com o dia seguinte.
7
+ *
8
+ * Fuso: `NEETRU_TZ` (ex.: `America/Manaus`) quando definido; senão
9
+ * `America/Sao_Paulo` — explícito de propósito: o fuso "local" de um container
10
+ * de CI costuma ser UTC e reintroduziria o bug.
11
+ */
12
+ export declare const DEFAULT_DOCS_TZ = "America/Sao_Paulo";
13
+ export declare function docsTimeZone(): string;
14
+ /** AAAA-MM-DD de `d` no fuso dos docs. */
15
+ export declare function localIsoDate(d?: Date, timeZone?: string): string;
16
+ /** Hoje (AAAA-MM-DD) no fuso dos docs. Usa `Date.now()` — o relógio é injetável em teste. */
17
+ export declare function todayIso(): string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Datas dos docs (AAAA-MM-DD) no FUSO DO NEGÓCIO, nunca em UTC.
3
+ *
4
+ * Bug da migração real (2026-10-03): `toISOString()` é UTC — depois das 21h
5
+ * BRT os docs criados, o nome do relatório `MIGRACAO_DOCS_<data>`, o
6
+ * `gerado_em` e a data do CHANGELOG saíam com o dia seguinte.
7
+ *
8
+ * Fuso: `NEETRU_TZ` (ex.: `America/Manaus`) quando definido; senão
9
+ * `America/Sao_Paulo` — explícito de propósito: o fuso "local" de um container
10
+ * de CI costuma ser UTC e reintroduziria o bug.
11
+ */
12
+ export const DEFAULT_DOCS_TZ = 'America/Sao_Paulo';
13
+ export function docsTimeZone() {
14
+ const tz = process.env.NEETRU_TZ?.trim();
15
+ if (tz) {
16
+ try {
17
+ new Intl.DateTimeFormat('en-CA', { timeZone: tz });
18
+ return tz;
19
+ }
20
+ catch {
21
+ /* fuso inválido → padrão */
22
+ }
23
+ }
24
+ return DEFAULT_DOCS_TZ;
25
+ }
26
+ /** AAAA-MM-DD de `d` no fuso dos docs. */
27
+ export function localIsoDate(d = new Date(), timeZone = docsTimeZone()) {
28
+ const parts = new Intl.DateTimeFormat('en-CA', {
29
+ timeZone,
30
+ year: 'numeric',
31
+ month: '2-digit',
32
+ day: '2-digit',
33
+ }).formatToParts(d);
34
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? '';
35
+ return `${get('year')}-${get('month')}-${get('day')}`;
36
+ }
37
+ /** Hoje (AAAA-MM-DD) no fuso dos docs. Usa `Date.now()` — o relógio é injetável em teste. */
38
+ export function todayIso() {
39
+ return localIsoDate(new Date());
40
+ }
41
+ //# sourceMappingURL=dates.js.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Frontmatter YAML dos docs — parser e serializador SEM dependência.
3
+ *
4
+ * O CLI publicado não carrega `js-yaml`/`gray-matter`. Este parser cobre o
5
+ * subconjunto que o protocolo usa (e o que aparece nos repos de produto):
6
+ * - `chave: valor` (string sem aspas, "aspas duplas" com escapes, 'aspas simples')
7
+ * - inteiros, `true`/`false`, `null`/`~`
8
+ * - lista em linha `[a, "b c"]` e lista em bloco (`chave:` + linhas ` - item`)
9
+ * - bloco literal/dobrado (`chave: |` / `chave: >`)
10
+ * - mapa aninhado de 1 nível (`chave:` + linhas ` sub: valor`) — fica como objeto
11
+ * - comentários `#` em linha própria
12
+ *
13
+ * Erros que um parser YAML de verdade recusaria viram `error` (o validador
14
+ * reporta FM001): bloco sem fechamento, linha que não é `chave: valor`,
15
+ * indentação com TAB, chave duplicada, aspas sem fechamento.
16
+ *
17
+ * Datas (`2026-10-03`) ficam como STRING — o validador confere o formato.
18
+ */
19
+ export type FmValue = string | number | boolean | null | FmValue[] | {
20
+ [k: string]: FmValue;
21
+ };
22
+ export type FmData = Record<string, FmValue>;
23
+ export interface ParsedDoc {
24
+ /** `true` quando o arquivo começa com `---` (com ou sem BOM). */
25
+ hasFrontmatter: boolean;
26
+ data: FmData;
27
+ /** Corpo depois do bloco de frontmatter (ou o arquivo inteiro, sem frontmatter). */
28
+ body: string;
29
+ /** Linha (1-based) onde o corpo começa no arquivo original. */
30
+ bodyStartLine: number;
31
+ /** Mensagem do erro de YAML (FM001), se houver. */
32
+ error?: string;
33
+ /** O arquivo começava com BOM UTF-8. */
34
+ bom: boolean;
35
+ /** Fim de linha dominante do arquivo. */
36
+ eol: '\n' | '\r\n';
37
+ }
38
+ /** Separa e interpreta o frontmatter de um markdown. Nunca lança. */
39
+ export declare function parseDoc(raw: string): ParsedDoc;
40
+ /** Ordem canônica das chaves (§6.1 do protocolo da wiki + `product`). */
41
+ export declare const FRONTMATTER_KEY_ORDER: string[];
42
+ /** Gera o bloco `---\n...\n---` (sem quebra final). Chaves conhecidas em ordem canônica, o resto depois. */
43
+ export declare function serializeFrontmatter(data: FmData, eol?: '\n' | '\r\n'): string;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Frontmatter YAML dos docs — parser e serializador SEM dependência.
3
+ *
4
+ * O CLI publicado não carrega `js-yaml`/`gray-matter`. Este parser cobre o
5
+ * subconjunto que o protocolo usa (e o que aparece nos repos de produto):
6
+ * - `chave: valor` (string sem aspas, "aspas duplas" com escapes, 'aspas simples')
7
+ * - inteiros, `true`/`false`, `null`/`~`
8
+ * - lista em linha `[a, "b c"]` e lista em bloco (`chave:` + linhas ` - item`)
9
+ * - bloco literal/dobrado (`chave: |` / `chave: >`)
10
+ * - mapa aninhado de 1 nível (`chave:` + linhas ` sub: valor`) — fica como objeto
11
+ * - comentários `#` em linha própria
12
+ *
13
+ * Erros que um parser YAML de verdade recusaria viram `error` (o validador
14
+ * reporta FM001): bloco sem fechamento, linha que não é `chave: valor`,
15
+ * indentação com TAB, chave duplicada, aspas sem fechamento.
16
+ *
17
+ * Datas (`2026-10-03`) ficam como STRING — o validador confere o formato.
18
+ */
19
+ const BOM = '';
20
+ function detectEol(raw) {
21
+ const crlf = (raw.match(/\r\n/g) ?? []).length;
22
+ const lf = (raw.match(/\n/g) ?? []).length - crlf;
23
+ return crlf > lf ? '\r\n' : '\n';
24
+ }
25
+ function parseScalar(rawVal) {
26
+ const v = rawVal.trim();
27
+ if (v === '')
28
+ return { value: null };
29
+ if (v.startsWith('"')) {
30
+ // acha o fechamento respeitando escapes
31
+ let out = '';
32
+ let i = 1;
33
+ let closed = false;
34
+ while (i < v.length) {
35
+ const c = v[i];
36
+ if (c === '\\' && i + 1 < v.length) {
37
+ const n = v[i + 1];
38
+ out += n === 'n' ? '\n' : n === 't' ? '\t' : n;
39
+ i += 2;
40
+ continue;
41
+ }
42
+ if (c === '"') {
43
+ closed = true;
44
+ i++;
45
+ break;
46
+ }
47
+ out += c;
48
+ i++;
49
+ }
50
+ if (!closed)
51
+ return { value: v, error: 'aspas duplas sem fechamento' };
52
+ const rest = v.slice(i).trim();
53
+ if (rest && !rest.startsWith('#'))
54
+ return { value: out, error: `conteúdo depois das aspas: "${rest}"` };
55
+ return { value: out };
56
+ }
57
+ if (v.startsWith("'")) {
58
+ let out = '';
59
+ let i = 1;
60
+ let closed = false;
61
+ while (i < v.length) {
62
+ if (v[i] === "'") {
63
+ if (v[i + 1] === "'") {
64
+ out += "'";
65
+ i += 2;
66
+ continue;
67
+ }
68
+ closed = true;
69
+ i++;
70
+ break;
71
+ }
72
+ out += v[i];
73
+ i++;
74
+ }
75
+ if (!closed)
76
+ return { value: v, error: 'aspas simples sem fechamento' };
77
+ const rest = v.slice(i).trim();
78
+ if (rest && !rest.startsWith('#'))
79
+ return { value: out, error: `conteúdo depois das aspas: "${rest}"` };
80
+ return { value: out };
81
+ }
82
+ if (v.startsWith('[')) {
83
+ if (!v.endsWith(']'))
84
+ return { value: v, error: 'lista em linha sem "]"' };
85
+ const inner = v.slice(1, -1).trim();
86
+ if (inner === '')
87
+ return { value: [] };
88
+ const items = [];
89
+ let cur = '';
90
+ let quote = null;
91
+ for (const ch of inner) {
92
+ if (quote) {
93
+ cur += ch;
94
+ if (ch === quote)
95
+ quote = null;
96
+ continue;
97
+ }
98
+ if (ch === '"' || ch === "'") {
99
+ quote = ch;
100
+ cur += ch;
101
+ continue;
102
+ }
103
+ if (ch === ',') {
104
+ const r = parseScalar(cur);
105
+ if (r.error)
106
+ return { value: v, error: r.error };
107
+ items.push(r.value);
108
+ cur = '';
109
+ continue;
110
+ }
111
+ cur += ch;
112
+ }
113
+ if (quote)
114
+ return { value: v, error: 'aspas sem fechamento na lista' };
115
+ const r = parseScalar(cur);
116
+ if (r.error)
117
+ return { value: v, error: r.error };
118
+ items.push(r.value);
119
+ return { value: items };
120
+ }
121
+ // comentário no fim da linha (` #`) — corta
122
+ const hash = v.search(/\s#/);
123
+ const plain = hash >= 0 ? v.slice(0, hash).trim() : v;
124
+ if (plain === 'true')
125
+ return { value: true };
126
+ if (plain === 'false')
127
+ return { value: false };
128
+ if (plain === 'null' || plain === '~')
129
+ return { value: null };
130
+ if (/^-?\d+$/.test(plain))
131
+ return { value: Number.parseInt(plain, 10) };
132
+ return { value: plain };
133
+ }
134
+ /** Separa e interpreta o frontmatter de um markdown. Nunca lança. */
135
+ export function parseDoc(raw) {
136
+ const eol = detectEol(raw);
137
+ const bom = raw.startsWith(BOM);
138
+ const text = bom ? raw.slice(1) : raw;
139
+ const lines = text.split(/\r?\n/);
140
+ const base = { hasFrontmatter: false, data: {}, body: text, bodyStartLine: 1, bom, eol };
141
+ if (lines[0]?.trim() !== '---')
142
+ return base;
143
+ let end = -1;
144
+ for (let i = 1; i < lines.length; i++) {
145
+ if (lines[i].trim() === '---' || lines[i].trim() === '...') {
146
+ end = i;
147
+ break;
148
+ }
149
+ }
150
+ if (end < 0) {
151
+ return { ...base, hasFrontmatter: true, error: 'bloco de frontmatter sem a linha "---" de fechamento' };
152
+ }
153
+ const fmLines = lines.slice(1, end);
154
+ const body = lines.slice(end + 1).join(eol);
155
+ const out = { ...base, hasFrontmatter: true, body, bodyStartLine: end + 2 };
156
+ const data = {};
157
+ let i = 0;
158
+ const fail = (lineIdx, msg) => ({
159
+ ...out,
160
+ data,
161
+ error: `linha ${lineIdx + 2} do frontmatter: ${msg}`,
162
+ });
163
+ while (i < fmLines.length) {
164
+ const line = fmLines[i];
165
+ if (line.trim() === '' || line.trim().startsWith('#')) {
166
+ i++;
167
+ continue;
168
+ }
169
+ if (/^\t/.test(line))
170
+ return fail(i, 'indentação com TAB não é YAML válido');
171
+ if (/^\s/.test(line))
172
+ return fail(i, `linha indentada fora de uma lista ou bloco: "${line.trim()}"`);
173
+ const kv = line.match(/^([A-Za-z_$][\w$.-]*)\s*:(?:\s+(.*)|\s*)$/);
174
+ if (!kv)
175
+ return fail(i, `esperado "chave: valor", veio "${line.trim()}"`);
176
+ const key = kv[1];
177
+ const rest = (kv[2] ?? '').trim();
178
+ if (Object.prototype.hasOwnProperty.call(data, key))
179
+ return fail(i, `chave "${key}" repetida`);
180
+ if (rest === '|' || rest === '>' || rest === '|-' || rest === '>-') {
181
+ const parts = [];
182
+ i++;
183
+ while (i < fmLines.length && (/^\s+/.test(fmLines[i]) || fmLines[i].trim() === '')) {
184
+ if (/^\t/.test(fmLines[i]))
185
+ return fail(i, 'indentação com TAB não é YAML válido');
186
+ parts.push(fmLines[i].trim());
187
+ i++;
188
+ }
189
+ data[key] = rest.startsWith('|') ? parts.join('\n').trim() : parts.filter(Boolean).join(' ').trim();
190
+ continue;
191
+ }
192
+ if (rest === '' || rest.startsWith('#')) {
193
+ // lista em bloco, mapa aninhado ou vazio
194
+ const items = [];
195
+ const map = {};
196
+ let kind = null;
197
+ i++;
198
+ while (i < fmLines.length && (/^\s+/.test(fmLines[i]) || fmLines[i].trim() === '' || /^-\s/.test(fmLines[i]))) {
199
+ const l = fmLines[i];
200
+ if (l.trim() === '' || l.trim().startsWith('#')) {
201
+ i++;
202
+ continue;
203
+ }
204
+ if (/^\t/.test(l))
205
+ return fail(i, 'indentação com TAB não é YAML válido');
206
+ const t = l.trim();
207
+ if (t.startsWith('- ') || t === '-') {
208
+ if (kind === 'mapa')
209
+ return fail(i, 'mistura de lista e mapa');
210
+ kind = 'lista';
211
+ const r = parseScalar(t.slice(1));
212
+ if (r.error)
213
+ return fail(i, r.error);
214
+ items.push(r.value);
215
+ }
216
+ else {
217
+ if (kind === 'lista')
218
+ return fail(i, 'mistura de lista e mapa');
219
+ kind = 'mapa';
220
+ const sub = t.match(/^([A-Za-z_$][\w$.-]*)\s*:(?:\s+(.*)|\s*)$/);
221
+ if (!sub)
222
+ return fail(i, `esperado "chave: valor" dentro de "${key}"`);
223
+ const r = parseScalar(sub[2] ?? '');
224
+ if (r.error)
225
+ return fail(i, r.error);
226
+ map[sub[1]] = r.value;
227
+ }
228
+ i++;
229
+ }
230
+ data[key] = kind === 'lista' ? items : kind === 'mapa' ? map : null;
231
+ continue;
232
+ }
233
+ const r = parseScalar(rest);
234
+ if (r.error)
235
+ return fail(i, r.error);
236
+ data[key] = r.value;
237
+ i++;
238
+ }
239
+ return { ...out, data };
240
+ }
241
+ // ─── serialização ─────────────────────────────────────────────────────────
242
+ /** Ordem canônica das chaves (§6.1 do protocolo da wiki + `product`). */
243
+ export const FRONTMATTER_KEY_ORDER = [
244
+ 'title', 'description', 'product', 'category', 'subcategory', 'type', 'status',
245
+ 'date', 'updated', 'owner', 'audience', 'adr', 'order', 'superseded_by', 'supersedes',
246
+ 'archived_at', 'archived_from', 'archive_reason', 'status_evidence', 'status_checked_at',
247
+ 'status_note', 'related', 'tags', 'icon', 'command',
248
+ ];
249
+ /** Chaves cujo valor sempre vai entre aspas duplas (texto livre). */
250
+ const ALWAYS_QUOTE = new Set(['title', 'description', 'archive_reason', 'status_note', 'status_evidence']);
251
+ function needsQuote(s) {
252
+ if (s === '')
253
+ return true;
254
+ // inclui os booleanos do YAML 1.1 (yes/no/on/off) — parsers antigos os leem como bool
255
+ if (/^(true|false|yes|no|on|off|null|~|-?\d+(\.\d+)?)$/i.test(s))
256
+ return true;
257
+ if (/^[\s\-?:,[\]{}#&*!|>'"%@`]/.test(s))
258
+ return true;
259
+ if (/:\s|\s#|[\r\n\t]/.test(s) || s.endsWith(':') || s.endsWith(' '))
260
+ return true;
261
+ return false;
262
+ }
263
+ function quote(s) {
264
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
265
+ }
266
+ function serializeScalar(key, v) {
267
+ if (v === null)
268
+ return '';
269
+ if (typeof v === 'number' || typeof v === 'boolean')
270
+ return String(v);
271
+ if (Array.isArray(v))
272
+ return `[${v.map((x) => serializeScalar('', x)).join(', ')}]`;
273
+ if (typeof v === 'object')
274
+ return JSON.stringify(v);
275
+ if (ALWAYS_QUOTE.has(key) || needsQuote(v))
276
+ return quote(v);
277
+ return v;
278
+ }
279
+ /** Gera o bloco `---\n...\n---` (sem quebra final). Chaves conhecidas em ordem canônica, o resto depois. */
280
+ export function serializeFrontmatter(data, eol = '\n') {
281
+ const keys = Object.keys(data);
282
+ const ordered = [
283
+ ...FRONTMATTER_KEY_ORDER.filter((k) => keys.includes(k)),
284
+ ...keys.filter((k) => !FRONTMATTER_KEY_ORDER.includes(k)),
285
+ ];
286
+ const lines = ['---'];
287
+ for (const k of ordered) {
288
+ const v = data[k];
289
+ if (v === undefined)
290
+ continue;
291
+ if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
292
+ lines.push(`${k}:`);
293
+ for (const [sk, sv] of Object.entries(v))
294
+ lines.push(` ${sk}: ${serializeScalar(sk, sv)}`.trimEnd());
295
+ continue;
296
+ }
297
+ lines.push(`${k}: ${serializeScalar(k, v)}`.trimEnd());
298
+ }
299
+ lines.push('---');
300
+ return lines.join(eol);
301
+ }
302
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1,16 @@
1
+ export declare function git(cwd: string, args: string[]): string | null;
2
+ /** Ref base para comparar ("branch principal"): origin/main, main, origin/master, master. */
3
+ export declare function resolveBaseRef(cwd: string): string | null;
4
+ /**
5
+ * Arquivos alterados contra a branch principal (commits da branch + mudanças
6
+ * não commitadas + arquivos novos não rastreados). Caminhos relativos à raiz
7
+ * do repo, com `/`. `null` = não deu para descobrir (sem git, sem base).
8
+ */
9
+ export declare function changedFiles(cwd: string): {
10
+ files: string[];
11
+ base: string;
12
+ } | null;
13
+ /** Data (AAAA-MM-DD) do commit que CRIOU o arquivo, seguindo renomeações. */
14
+ export declare function firstCommitDate(cwd: string, relPath: string): string | null;
15
+ /** Data (AAAA-MM-DD) do último commit que tocou o arquivo. */
16
+ export declare function lastCommitDate(cwd: string, relPath: string): string | null;