@softize/opus 11.1.1 → 12.0.1
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/CHANGELOG.md +63 -1
- package/README.md +31 -1
- package/bin/cli.mjs +97 -27
- package/bin/lib/check.mjs +55 -43
- package/bin/lib/copy.mjs +2202 -0
- package/bin/lib/create.mjs +227 -39
- package/bin/lib/db-migrate-runner.mjs +9 -6
- package/bin/lib/db-project-path.mjs +20 -0
- package/bin/lib/db-scaffold-runner.mjs +23 -8
- package/bin/lib/db.mjs +6 -4
- package/bin/lib/gen.mjs +60 -29
- package/bin/lib/init.mjs +212 -56
- package/bin/lib/introspect.mjs +3 -2
- package/bin/lib/materialize.mjs +623 -97
- package/bin/lib/postinstall.mjs +6 -5
- package/bin/lib/validate-skill.mjs +502 -30
- package/docs/code-style.md +142 -7
- package/docs/consumer-upgrade-propagation.md +4 -3
- package/docs/releasing.md +28 -17
- package/package.json +6 -1
- package/registry/git/pre-push.d/00-opus-copy +14 -0
- package/registry/git/pre-push.d/opus +7 -21
- package/registry/git/run-opus-pre-push.mjs +141 -0
- package/registry/hooks/opus-check-on-stop.mjs +13 -31
- package/registry/instructions/opus.md +11 -5
- package/registry/skills/build-opus-ui/SKILL.md +5 -4
- package/registry/skills/create-opus-action/SKILL.md +4 -4
- package/registry/skills/implement-opus-change/SKILL.md +7 -5
- package/registry/skills/upgrade-opus/SKILL.md +8 -4
- package/registry/skills/upgrade-opus/references/upgrade-checklist.md +4 -1
- package/registry/templates/app/package.json +4 -0
- package/registry/templates/app/pnpm-workspace.yaml +3 -2
- package/registry/templates/app/src/domains/tasks/actions/list.ts +1 -1
- package/registry/templates/monorepo/pnpm-workspace.yaml +3 -1
- package/src/ui/components/patterns/split.tsx +66 -24
- package/src/ui/docs/content/cli.md +8 -7
- package/src/ui/docs/content/communication.md +79 -126
- package/src/ui/docs/content/getting-started.md +29 -16
- package/src/ui/docs/content/split.md +43 -3
- package/src/ui/react.tsx +1 -1
- package/registry/skills/write-product-communication/SKILL.md +0 -28
- package/registry/skills/write-product-communication/agents/openai.yaml +0 -4
- package/registry/templates/app/_npmrc +0 -1
- package/registry/templates/monorepo/_npmrc +0 -1
package/bin/lib/gen.mjs
CHANGED
|
@@ -27,6 +27,15 @@ import path from 'node:path'
|
|
|
27
27
|
import { fileURLToPath } from 'node:url'
|
|
28
28
|
import { execFile } from 'node:child_process'
|
|
29
29
|
import { promisify } from 'node:util'
|
|
30
|
+
import {
|
|
31
|
+
canonicalProjectDirectory,
|
|
32
|
+
ensureProjectDirectory,
|
|
33
|
+
readProjectDirectory,
|
|
34
|
+
readProjectFile,
|
|
35
|
+
removeProjectFileIfUnchanged,
|
|
36
|
+
safeProjectPath,
|
|
37
|
+
writeProjectFileAtomically,
|
|
38
|
+
} from '@softize/base/project-path'
|
|
30
39
|
|
|
31
40
|
import { buildManifest } from './gen-manifest.mjs'
|
|
32
41
|
import { buildOpenAPI } from './gen-openapi.mjs'
|
|
@@ -59,13 +68,12 @@ function log(level, msg) {
|
|
|
59
68
|
// =============================================================================
|
|
60
69
|
|
|
61
70
|
export async function cmdGen(flags) {
|
|
62
|
-
const cwd = process.cwd()
|
|
71
|
+
const cwd = canonicalProjectDirectory(process.cwd())
|
|
63
72
|
const configRel = flags.config ?? 'opus.config.ts'
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
: path.resolve(cwd, configRel)
|
|
73
|
+
const config = safeProjectPath(cwd, configRel)
|
|
74
|
+
const configPath = config.path
|
|
67
75
|
|
|
68
|
-
if (!
|
|
76
|
+
if (!config.exists) {
|
|
69
77
|
log('error', `opus.config.ts não encontrado em ${configPath}`)
|
|
70
78
|
log(
|
|
71
79
|
'dim',
|
|
@@ -81,9 +89,9 @@ export async function cmdGen(flags) {
|
|
|
81
89
|
|
|
82
90
|
// Output: flag > config > default.
|
|
83
91
|
const outputDir = flags.output ?? payload.output ?? '.gen'
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
const output = safeProjectPath(cwd, outputDir)
|
|
93
|
+
const outputRel = path.relative(cwd, output.path) || '.'
|
|
94
|
+
const outputAbs = output.path
|
|
87
95
|
|
|
88
96
|
const manifest = buildManifest(payload)
|
|
89
97
|
const openapi = buildOpenAPI(manifest)
|
|
@@ -91,31 +99,32 @@ export async function cmdGen(flags) {
|
|
|
91
99
|
const stubs = buildClientStubs(manifest)
|
|
92
100
|
const dictStubs = buildDictStubs(manifest)
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
ensureProjectTree(cwd, outputRel)
|
|
103
|
+
const outputRoot = canonicalProjectDirectory(outputAbs)
|
|
104
|
+
ensureProjectDirectory(outputRoot, 'docs')
|
|
105
|
+
ensureProjectDirectory(outputRoot, 'client-stubs')
|
|
97
106
|
if (dictStubs.length > 0) {
|
|
98
|
-
|
|
99
|
-
recursive: true,
|
|
100
|
-
})
|
|
107
|
+
ensureProjectDirectory(outputRoot, 'client-stubs/dicts')
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
await writeJson(
|
|
104
|
-
await writeJson(
|
|
110
|
+
await writeJson(outputRoot, 'manifest.json', manifest, flags.force)
|
|
111
|
+
await writeJson(outputRoot, 'openapi.json', openapi, flags.force)
|
|
105
112
|
|
|
106
113
|
for (const doc of docs) {
|
|
107
|
-
await writeText(path.join(
|
|
114
|
+
await writeText(outputRoot, path.join('docs', doc.filename), doc.content, flags.force)
|
|
108
115
|
}
|
|
109
116
|
for (const stub of stubs) {
|
|
110
117
|
await writeText(
|
|
111
|
-
|
|
118
|
+
outputRoot,
|
|
119
|
+
path.join('client-stubs', stub.filename),
|
|
112
120
|
stub.content,
|
|
113
121
|
flags.force,
|
|
114
122
|
)
|
|
115
123
|
}
|
|
116
124
|
for (const dict of dictStubs) {
|
|
117
125
|
await writeText(
|
|
118
|
-
|
|
126
|
+
outputRoot,
|
|
127
|
+
path.join('client-stubs', 'dicts', dict.filename),
|
|
119
128
|
dict.content,
|
|
120
129
|
flags.force,
|
|
121
130
|
)
|
|
@@ -251,8 +260,16 @@ async function fileExists(p) {
|
|
|
251
260
|
}
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
|
|
255
|
-
|
|
263
|
+
function ensureProjectTree(root, requested) {
|
|
264
|
+
let cursor = '.'
|
|
265
|
+
for (const segment of requested.split(path.sep).filter((item) => item !== '' && item !== '.')) {
|
|
266
|
+
cursor = path.join(cursor, segment)
|
|
267
|
+
ensureProjectDirectory(root, cursor)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function writeJson(root, requested, value, _force) {
|
|
272
|
+
await writeText(root, requested, JSON.stringify(value, null, 2) + '\n', _force)
|
|
256
273
|
}
|
|
257
274
|
|
|
258
275
|
/**
|
|
@@ -262,25 +279,39 @@ async function writeJson(p, value, _force) {
|
|
|
262
279
|
* que o consumer tenha posto lá fica intacta. Exportada pra teste.
|
|
263
280
|
*/
|
|
264
281
|
export async function pruneOrphans(dir, keep, exts) {
|
|
265
|
-
let
|
|
282
|
+
let parent
|
|
266
283
|
try {
|
|
267
|
-
|
|
284
|
+
parent = canonicalProjectDirectory(path.dirname(path.resolve(dir)))
|
|
268
285
|
} catch {
|
|
269
286
|
return [] // dir nem existe → nada a podar
|
|
270
287
|
}
|
|
288
|
+
const requested = path.relative(parent, path.resolve(dir))
|
|
289
|
+
const inspected = safeProjectPath(parent, requested)
|
|
290
|
+
if (!inspected.exists) return []
|
|
291
|
+
const root = canonicalProjectDirectory(inspected.path)
|
|
292
|
+
const entries = readProjectDirectory(root, '.').entries
|
|
271
293
|
const removed = []
|
|
272
294
|
for (const e of entries) {
|
|
273
|
-
if (!e.isFile()) continue
|
|
274
295
|
if (!exts.some((x) => e.name.endsWith(x))) continue
|
|
296
|
+
if (e.isDirectory()) continue
|
|
297
|
+
// A identidade é verificada antes de `keep`: um artefato esperado também não pode
|
|
298
|
+
// ser link simbólico, hardlink ou outro tipo especial dentro do diretório gerenciado.
|
|
299
|
+
const file = readProjectFile(root, e.name)
|
|
275
300
|
if (keep.has(e.name)) continue
|
|
276
|
-
|
|
301
|
+
removeProjectFileIfUnchanged(root, e.name, file)
|
|
277
302
|
removed.push(e.name)
|
|
278
303
|
}
|
|
279
304
|
return removed.sort()
|
|
280
305
|
}
|
|
281
306
|
|
|
282
|
-
async function writeText(
|
|
283
|
-
|
|
307
|
+
async function writeText(root, requested, content, _force) {
|
|
308
|
+
const current = readProjectFile(root, requested, { allowMissing: true })
|
|
309
|
+
writeProjectFileAtomically(
|
|
310
|
+
root,
|
|
311
|
+
requested,
|
|
312
|
+
content,
|
|
313
|
+
{ exists: current.exists, content: current.content },
|
|
314
|
+
)
|
|
284
315
|
}
|
|
285
316
|
|
|
286
317
|
// =============================================================================
|
|
@@ -299,8 +330,8 @@ Lê opus.config.ts do consumer e gera:
|
|
|
299
330
|
- <output>/client-stubs/dicts/<domain>.ts Dicts (t.dict({...})) prontos pro frontend
|
|
300
331
|
|
|
301
332
|
Flags:
|
|
302
|
-
--config <path> Caminho
|
|
303
|
-
--output <path> Pasta
|
|
333
|
+
--config <path> Caminho interno ao projeto para opus.config.ts. Default: ./opus.config.ts
|
|
334
|
+
--output <path> Pasta interna ao projeto. Default: do config (ou ./.gen)
|
|
304
335
|
--force, -f Sobrescreve existente (atualmente sempre sobrescreve)
|
|
305
336
|
|
|
306
337
|
Exemplos:
|
package/bin/lib/init.mjs
CHANGED
|
@@ -4,40 +4,78 @@ import { promises as fs } from "node:fs";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
canonicalProjectDirectory,
|
|
9
|
+
projectPathErrorMessage,
|
|
10
|
+
readProjectDirectory,
|
|
11
|
+
readProjectFile,
|
|
12
|
+
removeProjectFileIfUnchanged,
|
|
13
|
+
safeProjectPath,
|
|
14
|
+
writeProjectFileAtomically,
|
|
15
|
+
} from "@softize/base/project-path";
|
|
16
|
+
import {
|
|
17
|
+
discoverProjectRoot,
|
|
18
|
+
validGitDirectory,
|
|
19
|
+
} from "@softize/base/project-root";
|
|
20
|
+
|
|
21
|
+
import { materializeOpus, PACKAGE_VERSION, validateOpusSetup } from "./materialize.mjs";
|
|
22
|
+
import { writeCopyInventory } from "./copy.mjs";
|
|
8
23
|
|
|
9
24
|
const MARKER = "opus.json";
|
|
10
25
|
|
|
11
|
-
async function
|
|
26
|
+
async function readJson(p) {
|
|
12
27
|
try {
|
|
13
|
-
await fs.
|
|
14
|
-
return true;
|
|
28
|
+
return JSON.parse(await fs.readFile(p, "utf-8"));
|
|
15
29
|
} catch {
|
|
16
|
-
return
|
|
30
|
+
return null;
|
|
17
31
|
}
|
|
18
32
|
}
|
|
19
33
|
|
|
20
|
-
|
|
34
|
+
function localFileExists(projectRoot, requested) {
|
|
35
|
+
return safeProjectPath(projectRoot, requested).exists;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readLocalText(projectRoot, requested) {
|
|
39
|
+
return readProjectFile(projectRoot, requested).content;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readLocalJson(projectRoot, requested) {
|
|
21
43
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
44
|
+
const file = readProjectFile(projectRoot, requested, { allowMissing: true });
|
|
45
|
+
return file.exists ? JSON.parse(file.content) : null;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof SyntaxError) return null;
|
|
48
|
+
throw error;
|
|
25
49
|
}
|
|
26
50
|
}
|
|
27
51
|
|
|
28
52
|
/** Raiz do repo (sobe até achar `.git`); sem repo → null (o chamador decide o fallback). */
|
|
29
53
|
export async function repoRootOf(dir) {
|
|
30
|
-
|
|
54
|
+
const origin = path.resolve(dir);
|
|
31
55
|
const temporaryRoot = path.resolve(tmpdir());
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
const discovered = discoverProjectRoot(origin);
|
|
57
|
+
// O diretório temporário global pode receber marcadores transitórios de ferramentas e
|
|
58
|
+
// sandboxes. Um projeto dentro dele não deve herdar `/tmp/.git` como raiz compartilhada.
|
|
59
|
+
if (discovered === temporaryRoot && origin !== temporaryRoot) return null;
|
|
60
|
+
|
|
61
|
+
let marker;
|
|
62
|
+
try {
|
|
63
|
+
marker = safeProjectPath(discovered, ".git");
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (!marker.exists) return null;
|
|
68
|
+
if (marker.kind === "directory") return validGitDirectory(marker.path) ? discovered : null;
|
|
69
|
+
if (marker.kind !== "file") return null;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const gitFile = readProjectFile(discovered, ".git");
|
|
73
|
+
const match = /^gitdir: ([^\0\r\n]+)\r?\n?$/u.exec(gitFile.content);
|
|
74
|
+
if (match === null || match[1].trim() !== match[1]) return null;
|
|
75
|
+
const gitDirectory = canonicalProjectDirectory(path.resolve(discovered, match[1]));
|
|
76
|
+
return validGitDirectory(gitDirectory) ? discovered : null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
41
79
|
}
|
|
42
80
|
}
|
|
43
81
|
|
|
@@ -148,26 +186,22 @@ const CSS_IGNORADAS = new Set([
|
|
|
148
186
|
*/
|
|
149
187
|
async function collectCss(projectDir, dir = "src", profundidade = 0) {
|
|
150
188
|
if (profundidade > 3) return [];
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
withFileTypes: true,
|
|
155
|
-
});
|
|
156
|
-
} catch {
|
|
157
|
-
return []; // sem `src/` → nada a varrer.
|
|
158
|
-
}
|
|
189
|
+
const directory = readProjectDirectory(projectDir, dir, { allowMissing: true });
|
|
190
|
+
if (!directory.exists) return []; // sem `src/` → nada a varrer.
|
|
191
|
+
const entradas = directory.entries;
|
|
159
192
|
const aqui = [];
|
|
160
193
|
const abaixo = [];
|
|
161
194
|
for (const e of entradas.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
162
195
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
163
196
|
if (e.isDirectory() && CSS_IGNORADAS.has(e.name)) continue;
|
|
164
197
|
const rel = `${dir}/${e.name}`;
|
|
198
|
+
if (e.isSymbolicLink()) safeProjectPath(projectDir, rel, { mustExist: true });
|
|
165
199
|
if (e.isDirectory()) {
|
|
166
200
|
abaixo.push(...(await collectCss(projectDir, rel, profundidade + 1)));
|
|
167
201
|
} else if (e.name.endsWith(".css")) {
|
|
168
202
|
aqui.push({
|
|
169
203
|
file: rel,
|
|
170
|
-
text:
|
|
204
|
+
text: readLocalText(projectDir, rel),
|
|
171
205
|
});
|
|
172
206
|
}
|
|
173
207
|
}
|
|
@@ -195,10 +229,11 @@ function pickCss(achados) {
|
|
|
195
229
|
* @returns {Promise<{applicable:boolean, created:string[], warnings:string[]}>}
|
|
196
230
|
*/
|
|
197
231
|
export async function setupUiFoundation(projectDir) {
|
|
198
|
-
|
|
232
|
+
projectDir = canonicalProjectDirectory(projectDir);
|
|
233
|
+
const hasIndexHtml = localFileExists(projectDir, "index.html");
|
|
199
234
|
let twConfig = null;
|
|
200
235
|
for (const c of TAILWIND_CONFIGS) {
|
|
201
|
-
if (
|
|
236
|
+
if (localFileExists(projectDir, c)) {
|
|
202
237
|
twConfig = c;
|
|
203
238
|
break;
|
|
204
239
|
}
|
|
@@ -209,7 +244,7 @@ export async function setupUiFoundation(projectDir) {
|
|
|
209
244
|
|
|
210
245
|
const created = [];
|
|
211
246
|
const warnings = [];
|
|
212
|
-
const pkg =
|
|
247
|
+
const pkg = readLocalJson(projectDir, "package.json");
|
|
213
248
|
const major = await tailwindMajor(projectDir, pkg);
|
|
214
249
|
const v4 = major !== null && major >= 4;
|
|
215
250
|
const todosCss = v4 ? await collectCss(projectDir) : [];
|
|
@@ -242,13 +277,14 @@ export async function setupUiFoundation(projectDir) {
|
|
|
242
277
|
);
|
|
243
278
|
}
|
|
244
279
|
} else if (!twConfig) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
280
|
+
const destination = readProjectFile(projectDir, "tailwind.config.js", { allowMissing: true });
|
|
281
|
+
writeProjectFileAtomically(projectDir, "tailwind.config.js", tailwindConfigTemplate(), {
|
|
282
|
+
exists: destination.exists,
|
|
283
|
+
content: destination.content,
|
|
284
|
+
});
|
|
249
285
|
created.push("tailwind.config.js");
|
|
250
286
|
} else {
|
|
251
|
-
const txt =
|
|
287
|
+
const txt = readLocalText(projectDir, twConfig);
|
|
252
288
|
if (!txt.includes("@softize/opus/ui/preset")) {
|
|
253
289
|
warnings.push(
|
|
254
290
|
`${twConfig}: estenda o preset — \`import preset from '@softize/opus/ui/preset'\` + \`presets: [preset]\` + glob \`./node_modules/@softize/opus/src/ui/**/*.{ts,tsx}\` no content.`,
|
|
@@ -267,14 +303,14 @@ export async function setupUiFoundation(projectDir) {
|
|
|
267
303
|
} else {
|
|
268
304
|
let entry = null;
|
|
269
305
|
for (const e of ENTRY_FILES) {
|
|
270
|
-
if (
|
|
306
|
+
if (localFileExists(projectDir, e)) {
|
|
271
307
|
entry = e;
|
|
272
308
|
break;
|
|
273
309
|
}
|
|
274
310
|
}
|
|
275
311
|
// App v4 sem CSS de entrada já foi avisado no passo 1 — não repete.
|
|
276
312
|
if (entry !== null && !v4) {
|
|
277
|
-
const txt =
|
|
313
|
+
const txt = readLocalText(projectDir, entry);
|
|
278
314
|
if (!txt.includes("@softize/opus/ui/theme.css")) {
|
|
279
315
|
warnings.push(
|
|
280
316
|
`${entry}: importe o tema — \`import '@softize/opus/ui/theme.css'\` (antes do seu CSS).`,
|
|
@@ -288,9 +324,8 @@ export async function setupUiFoundation(projectDir) {
|
|
|
288
324
|
}
|
|
289
325
|
|
|
290
326
|
// 3. allowImportingTsExtensions (o Opus exporta source .ts/.tsx; o consumidor compila).
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const txt = await fs.readFile(tsconfigPath, "utf-8");
|
|
327
|
+
if (localFileExists(projectDir, "tsconfig.json")) {
|
|
328
|
+
const txt = readLocalText(projectDir, "tsconfig.json");
|
|
294
329
|
if (!txt.includes("allowImportingTsExtensions")) {
|
|
295
330
|
warnings.push(
|
|
296
331
|
'tsconfig.json: adicione `"allowImportingTsExtensions": true`.',
|
|
@@ -316,34 +351,155 @@ export async function initProject(registryDir, projectDir) {
|
|
|
316
351
|
void registryDir;
|
|
317
352
|
const created = [];
|
|
318
353
|
const synced = [];
|
|
354
|
+
const warnings = [];
|
|
319
355
|
const version = PACKAGE_VERSION;
|
|
356
|
+
const repoRoot = (await repoRootOf(projectDir)) ?? path.resolve(projectDir);
|
|
357
|
+
const projectLocal = path.relative(repoRoot, path.resolve(projectDir)) || ".";
|
|
358
|
+
|
|
359
|
+
// Fase 1: fotografa todas as entradas específicas do init antes da primeira mutação.
|
|
360
|
+
// O materializador tem seu próprio preflight; aqui cobrimos marcador, legado e migração.
|
|
361
|
+
let appPkg = null;
|
|
362
|
+
const preflightErrors = [];
|
|
363
|
+
try {
|
|
364
|
+
const packageFile = readProjectFile(repoRoot, path.join(projectLocal, "package.json"), { allowMissing: true });
|
|
365
|
+
if (packageFile.exists) {
|
|
366
|
+
try {
|
|
367
|
+
appPkg = JSON.parse(packageFile.content);
|
|
368
|
+
} catch {
|
|
369
|
+
preflightErrors.push("package.json: JSON inválido; preservado.");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
preflightErrors.push(`package.json: ${projectPathErrorMessage(error)}`);
|
|
374
|
+
}
|
|
375
|
+
preflightErrors.push(...validateOpusSetup(repoRoot, { invocationDirectory: projectDir }));
|
|
376
|
+
|
|
377
|
+
const markerDestination = path.join(projectLocal, MARKER);
|
|
378
|
+
let markerFile = null;
|
|
379
|
+
let prev = null;
|
|
380
|
+
try {
|
|
381
|
+
markerFile = readProjectFile(repoRoot, markerDestination, { allowMissing: true });
|
|
382
|
+
if (markerFile.exists) {
|
|
383
|
+
try {
|
|
384
|
+
prev = JSON.parse(markerFile.content);
|
|
385
|
+
} catch {
|
|
386
|
+
prev = null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
preflightErrors.push(`${MARKER}: ${projectPathErrorMessage(error)}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const claudeDestination = path.join(projectLocal, "CLAUDE.md");
|
|
394
|
+
let claudeFile = null;
|
|
395
|
+
try {
|
|
396
|
+
claudeFile = readProjectFile(repoRoot, claudeDestination, { allowMissing: true });
|
|
397
|
+
} catch (error) {
|
|
398
|
+
preflightErrors.push(`CLAUDE.md: ${projectPathErrorMessage(error)}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const npmrcFiles = [];
|
|
402
|
+
for (const directory of new Set([path.resolve(projectDir), path.resolve(repoRoot)])) {
|
|
403
|
+
const destination = path.join(path.relative(repoRoot, directory) || ".", ".npmrc");
|
|
404
|
+
const label = path.relative(repoRoot, path.join(directory, ".npmrc")) || ".npmrc";
|
|
405
|
+
try {
|
|
406
|
+
npmrcFiles.push({ destination, label, file: readProjectFile(repoRoot, destination, { allowMissing: true }) });
|
|
407
|
+
} catch (error) {
|
|
408
|
+
preflightErrors.push(`${label}: ${projectPathErrorMessage(error)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
320
411
|
|
|
321
|
-
|
|
322
|
-
|
|
412
|
+
if (preflightErrors.length > 0) {
|
|
413
|
+
return {
|
|
414
|
+
version,
|
|
415
|
+
created,
|
|
416
|
+
synced,
|
|
417
|
+
warnings,
|
|
418
|
+
wasInitialized: prev !== null,
|
|
419
|
+
materialization: { ok: false, errors: preflightErrors, changes: [], expected: [] },
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Fase 2: aplica somente snapshots já validados. Conflitos concorrentes interrompem a
|
|
424
|
+
// sequência e nunca liberam a materialização sobre uma migração parcial conhecida.
|
|
425
|
+
const setupErrors = [];
|
|
323
426
|
const marker = `${JSON.stringify({ package: "@softize/opus", version }, null, 2)}\n`;
|
|
324
|
-
if (!
|
|
325
|
-
|
|
326
|
-
|
|
427
|
+
if (!markerFile.exists || markerFile.content !== marker) {
|
|
428
|
+
try {
|
|
429
|
+
writeProjectFileAtomically(
|
|
430
|
+
repoRoot,
|
|
431
|
+
markerDestination,
|
|
432
|
+
markerFile.exists ? marker.replace(/\n/gu, markerFile.content.includes("\r\n") ? "\r\n" : "\n") : marker,
|
|
433
|
+
{ exists: markerFile.exists, content: markerFile.content },
|
|
434
|
+
);
|
|
435
|
+
(prev === null ? created : synced).push(MARKER);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
setupErrors.push(`${MARKER}: ${projectPathErrorMessage(error)}`);
|
|
438
|
+
}
|
|
327
439
|
}
|
|
328
440
|
|
|
329
441
|
// Remove somente o bloco legado que o próprio setup antigo declarava como gerenciado.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const cur = await fs.readFile(claudePath, "utf-8");
|
|
442
|
+
if (setupErrors.length === 0 && claudeFile.exists) {
|
|
443
|
+
const cur = claudeFile.content;
|
|
333
444
|
const next = cur.replace(/<!-- opus:base -->[\s\S]*?<!-- \/opus:base -->\s*/g, "");
|
|
334
445
|
if (next !== cur) {
|
|
335
|
-
|
|
336
|
-
|
|
446
|
+
try {
|
|
447
|
+
writeProjectFileAtomically(repoRoot, claudeDestination, next, { exists: true, content: cur });
|
|
448
|
+
synced.push("CLAUDE.md (bloco legado removido)");
|
|
449
|
+
} catch (error) {
|
|
450
|
+
setupErrors.push(`CLAUDE.md: ${projectPathErrorMessage(error)}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// A registry privada foi descontinuada. Remove apenas a diretiva exata conhecida, em
|
|
456
|
+
// escopo de projeto/repo; configuração global da máquina é diagnosticada, nunca mutada.
|
|
457
|
+
for (const { destination, label, file: npmrc } of npmrcFiles) {
|
|
458
|
+
if (setupErrors.length > 0 || !npmrc.exists) continue;
|
|
459
|
+
const current = npmrc.content;
|
|
460
|
+
const lines = current.split(/\r?\n/u);
|
|
461
|
+
const kept = lines.filter((line) => !/^\s*@softize:registry\s*=\s*https?:\/\/registry\.softize\.com\.br\/?\s*$/iu.test(line));
|
|
462
|
+
if (kept.length === lines.length) continue;
|
|
463
|
+
const newline = current.includes("\r\n") ? "\r\n" : "\n";
|
|
464
|
+
const next = kept.join(newline).replace(/^(?:\r?\n)+|(?:\r?\n)+$/gu, "");
|
|
465
|
+
try {
|
|
466
|
+
if (next === "") removeProjectFileIfUnchanged(repoRoot, destination, npmrc);
|
|
467
|
+
else writeProjectFileAtomically(repoRoot, destination, `${next}${newline}`, { exists: true, content: current });
|
|
468
|
+
synced.push(`${label} (registry legada removida)`);
|
|
469
|
+
warnings.push("se a registry legada também estiver global, rode `pnpm config delete @softize:registry --global`.");
|
|
470
|
+
} catch (error) {
|
|
471
|
+
setupErrors.push(`${label}: ${projectPathErrorMessage(error)}`);
|
|
337
472
|
}
|
|
338
473
|
}
|
|
339
474
|
|
|
340
|
-
|
|
341
|
-
|
|
475
|
+
if (setupErrors.length > 0) {
|
|
476
|
+
return {
|
|
477
|
+
version,
|
|
478
|
+
created,
|
|
479
|
+
synced,
|
|
480
|
+
warnings,
|
|
481
|
+
wasInitialized: prev !== null,
|
|
482
|
+
materialization: { ok: false, errors: setupErrors, changes: [], expected: [] },
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const materialization = materializeOpus(repoRoot, "setup", { invocationDirectory: projectDir });
|
|
342
487
|
synced.push(...materialization.changes);
|
|
488
|
+
if (materialization.ok) {
|
|
489
|
+
const copy = writeCopyInventory(repoRoot);
|
|
490
|
+
if (!copy.ok) {
|
|
491
|
+
materialization.ok = false;
|
|
492
|
+
materialization.errors.push(
|
|
493
|
+
...copy.diagnostics.map(
|
|
494
|
+
(item) => `copy ${item.source}:${item.line}: ${item.message}`,
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
} else if (copy.changed) {
|
|
498
|
+
synced.push(path.relative(repoRoot, copy.path));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
343
501
|
|
|
344
|
-
// Avisos
|
|
345
|
-
const warnings = [];
|
|
346
|
-
const appPkg = await readJson(path.join(projectDir, "package.json"));
|
|
502
|
+
// Avisos do setup específico do Opus (sem sobrescrever, como na fundação de UI).
|
|
347
503
|
if (appPkg !== null && typeof appPkg.scripts?.test !== "string") {
|
|
348
504
|
warnings.push(
|
|
349
505
|
"package.json: sem script `test` — o gate de entrega não confere a suíte sem ele.",
|
package/bin/lib/introspect.mjs
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* opus introspect [dir] [--json]
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { promises as fs } from 'node:fs'
|
|
13
12
|
import path from 'node:path'
|
|
14
13
|
import ts from 'typescript'
|
|
14
|
+
import { canonicalProjectDirectory, readProjectFile } from '@softize/base/project-path'
|
|
15
15
|
import { walkTsFiles } from './check.mjs'
|
|
16
16
|
|
|
17
17
|
function prop(obj, key, sf) {
|
|
@@ -92,10 +92,11 @@ export function deriveWiring(model) {
|
|
|
92
92
|
|
|
93
93
|
/** Escaneia um diretório → modelo agregado + wiring. */
|
|
94
94
|
export async function introspect(rootDir) {
|
|
95
|
+
rootDir = canonicalProjectDirectory(rootDir)
|
|
95
96
|
const files = await walkTsFiles(rootDir)
|
|
96
97
|
const model = { actions: [], reactions: [], schedules: [] }
|
|
97
98
|
for (const file of files) {
|
|
98
|
-
const text =
|
|
99
|
+
const text = readProjectFile(rootDir, path.relative(rootDir, file)).content
|
|
99
100
|
if (!/define(Action|Reaction|Schedule)/.test(text)) continue
|
|
100
101
|
const s = parseStructure(file, text)
|
|
101
102
|
const rel = path.relative(rootDir, file)
|