@metaobjectsdev/cli 1.0.2 → 1.0.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.
- package/README.md +1 -1
- package/dist/src/commands/deps.d.ts +3 -0
- package/dist/src/commands/deps.d.ts.map +1 -0
- package/dist/src/commands/deps.js +200 -0
- package/dist/src/commands/deps.js.map +1 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +19 -3
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +54 -1
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +12 -18
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +24 -19
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +2 -1
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +154 -23
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +62 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
- package/dist/src/lib/agent-context-staleness.js +7 -1
- package/dist/src/lib/agent-context-staleness.js.map +1 -1
- package/dist/src/lib/args.d.ts +57 -1
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +39 -2
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/collection-load-options.d.ts +13 -0
- package/dist/src/lib/collection-load-options.d.ts.map +1 -0
- package/dist/src/lib/collection-load-options.js +19 -0
- package/dist/src/lib/collection-load-options.js.map +1 -0
- package/dist/src/lib/dependency-sync.d.ts +170 -0
- package/dist/src/lib/dependency-sync.d.ts.map +1 -0
- package/dist/src/lib/dependency-sync.js +457 -0
- package/dist/src/lib/dependency-sync.js.map +1 -0
- package/dist/src/lib/migrate-scope.d.ts +32 -1
- package/dist/src/lib/migrate-scope.d.ts.map +1 -1
- package/dist/src/lib/migrate-scope.js +51 -3
- package/dist/src/lib/migrate-scope.js.map +1 -1
- package/dist/src/lib/overlay-lint.d.ts +37 -0
- package/dist/src/lib/overlay-lint.d.ts.map +1 -0
- package/dist/src/lib/overlay-lint.js +102 -0
- package/dist/src/lib/overlay-lint.js.map +1 -0
- package/dist/src/lib/requirement-check.d.ts +9 -1
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +23 -7
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/deps.ts +231 -0
- package/src/commands/docs.ts +19 -3
- package/src/commands/gen.ts +61 -1
- package/src/commands/init.ts +13 -17
- package/src/commands/migrate.ts +26 -16
- package/src/commands/prompt-snapshot.ts +2 -1
- package/src/commands/verify.ts +174 -23
- package/src/index.ts +62 -2
- package/src/lib/agent-context-staleness.ts +7 -1
- package/src/lib/args.ts +89 -3
- package/src/lib/collection-load-options.ts +36 -0
- package/src/lib/dependency-sync.ts +656 -0
- package/src/lib/migrate-scope.ts +59 -4
- package/src/lib/overlay-lint.ts +129 -0
- package/src/lib/requirement-check.ts +33 -7
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// server/typescript/packages/cli/src/commands/deps.ts
|
|
2
|
+
//
|
|
3
|
+
// FR-023 Phase 1a — Task 14 shipped `meta deps sync` (`path` transport) and
|
|
4
|
+
// `meta deps list`; Task 15 shipped `meta deps check` (this file's `runCheck`).
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_METAOBJECTS_DIR,
|
|
8
|
+
discoverCollectionRoot,
|
|
9
|
+
loadConfig,
|
|
10
|
+
loadMemory,
|
|
11
|
+
resolveCollection,
|
|
12
|
+
type DependencySpec,
|
|
13
|
+
type Lock,
|
|
14
|
+
} from "@metaobjectsdev/sdk";
|
|
15
|
+
import { parseDepsArgs, type DepsFlags } from "../lib/args.js";
|
|
16
|
+
import { log } from "../lib/log.js";
|
|
17
|
+
import { emitStructured, type OutputFormat } from "../lib/format.js";
|
|
18
|
+
import { reportLoadError } from "../lib/load-error.js";
|
|
19
|
+
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
20
|
+
import {
|
|
21
|
+
applySync,
|
|
22
|
+
checkDependencies,
|
|
23
|
+
ERR_DEPENDENCY_UPSTREAM_DRIFT,
|
|
24
|
+
formatCheckLine,
|
|
25
|
+
hash8,
|
|
26
|
+
planSync,
|
|
27
|
+
readLockOrThrow,
|
|
28
|
+
} from "../lib/dependency-sync.js";
|
|
29
|
+
|
|
30
|
+
/** The declared `dependencies` for the config governing `cwd` — read
|
|
31
|
+
* DIRECTLY via `loadConfig`, never through `resolveCollection`/`Collection`:
|
|
32
|
+
* that resolver VERIFIES the snapshot against the lock (`verifySnapshot`)
|
|
33
|
+
* and throws `ERR_DEPENDENCY_SNAPSHOT_STALE` the moment they disagree — the
|
|
34
|
+
* exact condition `meta deps sync` exists to fix. A project with no config
|
|
35
|
+
* at all declares none (`resolveCollection`'s own default: no config ⇒ no
|
|
36
|
+
* dependencies), which `discoverCollectionRoot`'s `hasConfig` already knows
|
|
37
|
+
* without a doomed `loadConfig` call. */
|
|
38
|
+
async function declaredDependencies(cwd: string): Promise<{ configDir: string; specs: readonly DependencySpec[] }> {
|
|
39
|
+
const { dir: configDir, hasConfig } = await discoverCollectionRoot(cwd);
|
|
40
|
+
if (!hasConfig) return { configDir, specs: [] };
|
|
41
|
+
const cfg = await loadConfig(join(configDir, DEFAULT_METAOBJECTS_DIR));
|
|
42
|
+
return { configDir, specs: cfg.dependencies };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runSync(configDir: string, specs: readonly DependencySpec[], flags: DepsFlags, fmt: OutputFormat): Promise<number> {
|
|
46
|
+
let lock: Lock | undefined;
|
|
47
|
+
try {
|
|
48
|
+
lock = await readLockOrThrow(configDir);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// Same convention as `depsCommand`'s `declaredDependencies` catch below: a
|
|
51
|
+
// committed project file that fails to parse is a config-class problem,
|
|
52
|
+
// exit 2 — distinct from the exit-1 a `sync` verdict failure gets.
|
|
53
|
+
log.error((err as Error).message);
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let plan;
|
|
58
|
+
try {
|
|
59
|
+
plan = await planSync(configDir, specs, lock, flags.names);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
log.error((err as Error).message);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let applied;
|
|
66
|
+
try {
|
|
67
|
+
applied = await applySync(configDir, plan, { dryRun: flags.dryRun });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
log.error((err as Error).message);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (fmt === "text") {
|
|
74
|
+
for (const w of plan.warnings) log.info(w);
|
|
75
|
+
if (applied.report.length === 0) {
|
|
76
|
+
log.info("meta deps sync: nothing to do — no dependencies declared.");
|
|
77
|
+
} else {
|
|
78
|
+
for (const r of applied.report) log.info(r.line);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
emitStructured(
|
|
82
|
+
{
|
|
83
|
+
warnings: plan.warnings,
|
|
84
|
+
dependencies: applied.report.map((r) => ({ name: r.name, action: r.action, message: r.line })),
|
|
85
|
+
},
|
|
86
|
+
fmt,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A dry run wrote nothing to validate — the plan already stands for what
|
|
91
|
+
// WOULD load.
|
|
92
|
+
if (flags.dryRun) return 0;
|
|
93
|
+
|
|
94
|
+
// DESIGN §4.1's closing sentence: load the full collection once, so a sync
|
|
95
|
+
// that produces an unloadable model (a collision the plan missed because it
|
|
96
|
+
// only compares declared node LISTS, a scope that now excludes something
|
|
97
|
+
// load-bearing, …) fails in THIS command with the loader's own error,
|
|
98
|
+
// rather than surfacing on the next unrelated `meta gen`/`verify`.
|
|
99
|
+
let collection;
|
|
100
|
+
try {
|
|
101
|
+
collection = await resolveCollection(configDir);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
log.error(`meta deps sync: the updated dependency set does not resolve: ${(err as Error).message}`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await loadMemory(collection.configDir, collectionLoadOptions(collection));
|
|
108
|
+
} catch (err) {
|
|
109
|
+
reportLoadError(log, "meta deps sync: the updated dependency set does not load", err);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function runList(configDir: string, fmt: OutputFormat): Promise<number> {
|
|
117
|
+
let lock: Lock | undefined;
|
|
118
|
+
try {
|
|
119
|
+
lock = await readLockOrThrow(configDir);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
log.error((err as Error).message);
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
const entries = Object.entries(lock?.dependencies ?? {});
|
|
125
|
+
|
|
126
|
+
if (fmt === "text") {
|
|
127
|
+
if (entries.length === 0) {
|
|
128
|
+
log.info("meta deps list: no dependencies locked — run `meta deps sync`.");
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
for (const [name, entry] of entries) {
|
|
132
|
+
log.info(
|
|
133
|
+
`${name} ${entry.version} ${hash8(entry.integrity)} ${entry.nodes.length} node(s) ${entry.packages.join(", ")}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
emitStructured(
|
|
138
|
+
{ dependencies: entries.map(([name, entry]) => ({ name, ...entry })) },
|
|
139
|
+
fmt,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* `meta deps check` (Task 15) — re-resolve each declared dependency exactly
|
|
147
|
+
* as `sync` steps 1-2 do and compare its installed artifact's TRUE hash
|
|
148
|
+
* against what `.metaobjects/deps.lock.json` pinned. Read-only: never touches
|
|
149
|
+
* the lock or the snapshot (that is `sync`'s job). A `drifted` or
|
|
150
|
+
* `unresolved` dependency fails the command — a check that cannot check must
|
|
151
|
+
* not pass.
|
|
152
|
+
*/
|
|
153
|
+
async function runCheck(
|
|
154
|
+
configDir: string,
|
|
155
|
+
specs: readonly DependencySpec[],
|
|
156
|
+
fmt: OutputFormat,
|
|
157
|
+
): Promise<number> {
|
|
158
|
+
let lock: Lock | undefined;
|
|
159
|
+
try {
|
|
160
|
+
lock = await readLockOrThrow(configDir);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
log.error((err as Error).message);
|
|
163
|
+
return 2;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (specs.length === 0) {
|
|
167
|
+
if (fmt === "text") {
|
|
168
|
+
log.info("meta deps check: nothing to check — no dependencies declared.");
|
|
169
|
+
} else {
|
|
170
|
+
emitStructured({ dependencies: [] }, fmt);
|
|
171
|
+
}
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const results = await checkDependencies(configDir, specs, lock);
|
|
176
|
+
const failing = results.filter((r) => r.status !== "current");
|
|
177
|
+
|
|
178
|
+
if (fmt === "text") {
|
|
179
|
+
for (const r of results) log.info(formatCheckLine(r));
|
|
180
|
+
if (failing.length > 0) {
|
|
181
|
+
log.error(
|
|
182
|
+
`meta deps check: ${failing.length} of ${results.length} dependenc` +
|
|
183
|
+
`${results.length === 1 ? "y" : "ies"} drifted or unresolved (${ERR_DEPENDENCY_UPSTREAM_DRIFT}).`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
emitStructured(
|
|
188
|
+
{
|
|
189
|
+
dependencies: results.map((r) => ({ ...r, line: formatCheckLine(r) })),
|
|
190
|
+
...(failing.length > 0 ? { code: ERR_DEPENDENCY_UPSTREAM_DRIFT } : {}),
|
|
191
|
+
},
|
|
192
|
+
fmt,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return failing.length > 0 ? 1 : 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function depsCommand(args: string[], cwd: string, fmt: OutputFormat): Promise<number> {
|
|
200
|
+
let flags: DepsFlags;
|
|
201
|
+
try {
|
|
202
|
+
flags = parseDepsArgs(args);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
log.error((err as Error).message);
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let configDir: string;
|
|
209
|
+
let specs: readonly DependencySpec[];
|
|
210
|
+
try {
|
|
211
|
+
({ configDir, specs } = await declaredDependencies(cwd));
|
|
212
|
+
} catch (err) {
|
|
213
|
+
// Mirrors `resolveCollection`'s own convention for a config.json that
|
|
214
|
+
// EXISTS but fails to load (malformed JSON, a schema violation): exit 2,
|
|
215
|
+
// the same code every other routed command uses for that class of
|
|
216
|
+
// failure (gen.ts, migrate.ts, docs.ts) — distinct from the exit 1 a
|
|
217
|
+
// dependency-resolution failure gets below, because this is a config
|
|
218
|
+
// problem, not a `sync` verdict.
|
|
219
|
+
log.error((err as Error).message);
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
switch (flags.subverb) {
|
|
224
|
+
case "sync":
|
|
225
|
+
return runSync(configDir, specs, flags, fmt);
|
|
226
|
+
case "list":
|
|
227
|
+
return runList(configDir, fmt);
|
|
228
|
+
case "check":
|
|
229
|
+
return runCheck(configDir, specs, fmt);
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/commands/docs.ts
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
// output is therefore guaranteed — it is byte-for-byte the same generator the
|
|
12
12
|
// `meta gen` pipeline runs (gated by the docs conformance fixture).
|
|
13
13
|
|
|
14
|
-
import { resolve as resolvePath, basename } from "node:path";
|
|
14
|
+
import { resolve as resolvePath, basename, dirname } from "node:path";
|
|
15
15
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
16
16
|
import { log } from "../lib/log.js";
|
|
17
17
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
18
|
+
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
18
19
|
import { loadMemory, resolveCollection, resolveConfigDir, type Collection } from "@metaobjectsdev/sdk";
|
|
19
20
|
import { existsSync } from "node:fs";
|
|
20
21
|
import { join } from "node:path";
|
|
@@ -515,7 +516,7 @@ export async function docsCommand(
|
|
|
515
516
|
let root;
|
|
516
517
|
try {
|
|
517
518
|
root = await loadMemory(collection.configDir, {
|
|
518
|
-
|
|
519
|
+
...collectionLoadOptions(collection),
|
|
519
520
|
...configLoadOptions,
|
|
520
521
|
});
|
|
521
522
|
} catch (err) {
|
|
@@ -860,7 +861,22 @@ async function emitSite(
|
|
|
860
861
|
// files: a declared source directory holding no metadata yet would otherwise
|
|
861
862
|
// vanish from the site's group list entirely, and `sourceDirs` could come back
|
|
862
863
|
// empty where the pre-branch code always passed `<root>/metaobjects`.
|
|
863
|
-
|
|
864
|
+
// `collection.sourceRoots` deliberately EXCLUDES dependency artifacts (sdk's
|
|
865
|
+
// collection.ts: "a dependency's artifact is not a source root"). But
|
|
866
|
+
// `generateSite` below does NOT reuse the already-loaded `collection` root —
|
|
867
|
+
// it runs its own independent `loadModel` over exactly the dirs handed to
|
|
868
|
+
// it. Without the resolved snapshot dirs appended here, a consumer that
|
|
869
|
+
// `extends` an imported abstract or `overlay: true`s an imported node — the
|
|
870
|
+
// two constructs entities.md and abstracts-and-inheritance.md recommend for
|
|
871
|
+
// this exact boundary — hits `ERR_UNRESOLVED_SUPER` / `ERR_OVERLAY_NO_TARGET`
|
|
872
|
+
// in THIS loader even though the same metadata resolves fine through
|
|
873
|
+
// `loadMemory` above. `loadModel` handles a dependency's snapshot dir
|
|
874
|
+
// natively: each holds exactly the one artifact file, which its own
|
|
875
|
+
// `.(json|ya?ml)$` file walk picks up like any other source dir.
|
|
876
|
+
const sourceDirs = [
|
|
877
|
+
...collection.sourceRoots,
|
|
878
|
+
...new Set(collection.dependencies.map((d) => dirname(d.artifactPath))),
|
|
879
|
+
];
|
|
864
880
|
const seenDirs = new Set(sourceDirs);
|
|
865
881
|
if (promptsDir !== undefined && !existsSync(promptsDir)) {
|
|
866
882
|
log.warn(`docs: --prompts dir does not exist: ${promptsDir}`);
|
package/src/commands/gen.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { relative } from "node:path";
|
|
|
2
2
|
import { parseGenArgs } from "../lib/args.js";
|
|
3
3
|
import { resolveGenConfig } from "../lib/config.js";
|
|
4
4
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
5
|
+
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
5
6
|
import { formatGenResult, formatGenResultToon, type GenFileEntry, type GenFileStatus } from "../lib/output.js";
|
|
6
7
|
import { formatGenResultJson } from "../lib/output-json.js";
|
|
7
8
|
import type { OutputFormat } from "../lib/format.js";
|
|
@@ -16,6 +17,9 @@ import {
|
|
|
16
17
|
import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
|
|
17
18
|
import { runGen, listGenerators } from "@metaobjectsdev/codegen-ts";
|
|
18
19
|
import type { WriteStatus } from "@metaobjectsdev/codegen-ts";
|
|
20
|
+
import { packageOfResolutionKey } from "@metaobjectsdev/metadata";
|
|
21
|
+
import type { MetaRoot } from "@metaobjectsdev/metadata";
|
|
22
|
+
import type { Collection } from "@metaobjectsdev/sdk";
|
|
19
23
|
import { reportLoadError } from "../lib/load-error.js";
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -122,7 +126,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
122
126
|
let metadata;
|
|
123
127
|
try {
|
|
124
128
|
metadata = await loadMemory(genCollection.configDir, {
|
|
125
|
-
|
|
129
|
+
...collectionLoadOptions(genCollection),
|
|
126
130
|
...loadMemoryOptionsFrom(forgeConfig),
|
|
127
131
|
});
|
|
128
132
|
} catch (err) {
|
|
@@ -130,6 +134,19 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
130
134
|
return 2;
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
// FR-023 §11.1 item 2 — refuse a positional that names ONLY objects imported
|
|
138
|
+
// from a dependency and excluded from output by the default exclusion rule.
|
|
139
|
+
// Distinct from the runner's existing "no entities matched" WARNING
|
|
140
|
+
// (runner.ts): that path covers a name matching nothing loaded at all; this
|
|
141
|
+
// one covers a name that matches something real, just not something this
|
|
142
|
+
// project generates. A name matching nothing falls through untouched — the
|
|
143
|
+
// runner's own warning still covers it.
|
|
144
|
+
const importedRefusal = refuseImportedPositional(cliConfig.entities, metadata, genCollection);
|
|
145
|
+
if (importedRefusal !== undefined) {
|
|
146
|
+
log.error(importedRefusal);
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
let result;
|
|
134
151
|
try {
|
|
135
152
|
result = await runGen({
|
|
@@ -145,6 +162,10 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
145
162
|
// passed: an unconfigured project's predicate admits everything, so this
|
|
146
163
|
// is a no-op for the common case, not a behavior change.
|
|
147
164
|
scope: genCollection.inScope,
|
|
165
|
+
// FR-023 §4.3 — the collection's own source files (never a dependency's
|
|
166
|
+
// snapshot artifact), so sharedModelFile() defaults its `files` selection
|
|
167
|
+
// to exactly what this project itself declares.
|
|
168
|
+
sourceFiles: genCollection.ownFiles,
|
|
148
169
|
...(cliConfig.entities.length > 0 ? { entityFilter: cliConfig.entities } : {}),
|
|
149
170
|
});
|
|
150
171
|
} catch (err) {
|
|
@@ -220,6 +241,45 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
|
|
|
220
241
|
return hasFailure ? 1 : 0;
|
|
221
242
|
}
|
|
222
243
|
|
|
244
|
+
/**
|
|
245
|
+
* FR-023 §11.1 item 2 — the error message for `meta gen <Name>` when EVERY
|
|
246
|
+
* loaded object named `<Name>` is imported from a dependency and excluded from
|
|
247
|
+
* output (imported metadata is load-only by default; only the consumer's own
|
|
248
|
+
* `scope.include` naming the package literally opts it in). Returns undefined
|
|
249
|
+
* for every other case, INCLUDING a name matching nothing at all — that stays
|
|
250
|
+
* the runner's existing "no entities matched" warning, unchanged.
|
|
251
|
+
*
|
|
252
|
+
* A bare positional is a NAME, not a fully-qualified one (`cliConfig.entities`
|
|
253
|
+
* is matched against `entity.name` elsewhere in this file the same way), so two
|
|
254
|
+
* packages declaring the same short name are both candidates; the refusal
|
|
255
|
+
* fires only when NONE of them would generate.
|
|
256
|
+
*/
|
|
257
|
+
function refuseImportedPositional(
|
|
258
|
+
entityNames: readonly string[],
|
|
259
|
+
metadata: MetaRoot,
|
|
260
|
+
collection: Collection,
|
|
261
|
+
): string | undefined {
|
|
262
|
+
if (entityNames.length === 0) return undefined;
|
|
263
|
+
const allObjects = metadata.objects();
|
|
264
|
+
for (const name of entityNames) {
|
|
265
|
+
const matches = allObjects.filter((o) => o.name === name);
|
|
266
|
+
if (matches.length === 0) continue; // nothing loaded by this name — the runner's own warning covers it
|
|
267
|
+
const allExcludedImports = matches.every((o) => {
|
|
268
|
+
const fqn = o.resolutionKey();
|
|
269
|
+
return collection.imported(fqn) && !collection.inScope(fqn);
|
|
270
|
+
});
|
|
271
|
+
if (!allExcludedImports) continue;
|
|
272
|
+
const fqn = matches[0]!.resolutionKey();
|
|
273
|
+
const pkg = packageOfResolutionKey(fqn);
|
|
274
|
+
const dependencyName = collection.dependencies.find((d) => d.packages.includes(pkg))?.name ?? pkg;
|
|
275
|
+
return (
|
|
276
|
+
`meta gen: '${name}' (${fqn}) is imported from dependency '${dependencyName}' and is not ` +
|
|
277
|
+
`generated here — add its package to scope.include in .metaobjects/config.json to generate it`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
223
283
|
/**
|
|
224
284
|
* Run the advisory verify-as-teacher scan (same pass `meta verify` runs): surface
|
|
225
285
|
* authored source that hand-rolls what the metadata could model. `gen` is the
|
package/src/commands/init.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mkdir, writeFile, readFile, readdir, stat, rm } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
4
|
import { existsSync as existsSyncWrap, readFileSync as readFileSyncWrap } from "node:fs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import { DEFAULT_CONFIG, ConfigSchema, saveConfig, PACKAGE_MANIFEST_FILE, DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR } from "@metaobjectsdev/sdk";
|
|
6
|
+
import { DEFAULT_CONFIG, ConfigSchema, saveConfig, PACKAGE_MANIFEST_FILE, DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR, DEPS_DIR, LOCK_FILE } from "@metaobjectsdev/sdk";
|
|
7
7
|
import {
|
|
8
8
|
assemble, resolveAgentContextRoot, planScaffold,
|
|
9
9
|
AGENT_CONTEXT_MANIFEST_PATH, type Manifest, type Stack,
|
|
@@ -85,7 +85,8 @@ const METAOBJECTS_GITIGNORE_BODY = `# The codegen merge base. The snapshot BODIE
|
|
|
85
85
|
# These ARE meant to be tracked — keep them even if a broad pattern matches.
|
|
86
86
|
!migrations/
|
|
87
87
|
!config.json
|
|
88
|
-
|
|
88
|
+
!${DEPS_DIR}/
|
|
89
|
+
!${LOCK_FILE}
|
|
89
90
|
`;
|
|
90
91
|
|
|
91
92
|
// A minimal root .gitignore for a fresh project — only written when none exists,
|
|
@@ -673,7 +674,6 @@ export async function init(opts: InitOptions): Promise<InitResult> {
|
|
|
673
674
|
"metaobjects/meta.common.json",
|
|
674
675
|
".metaobjects/config.json",
|
|
675
676
|
".metaobjects/.gitignore",
|
|
676
|
-
`.metaobjects/${PACKAGE_MANIFEST_FILE}`,
|
|
677
677
|
);
|
|
678
678
|
result.created.push(".metaobjects/AGENTS.md", ".metaobjects/CLAUDE.md", ".claude/skills/metaobjects-*", AGENT_CONTEXT_MANIFEST_PATH);
|
|
679
679
|
for (const name of SCAFFOLDED_GENERATOR_NAMES) result.created.push(`${OWNED_GENERATORS_DIR}/${name}.ts`);
|
|
@@ -703,19 +703,15 @@ export async function init(opts: InitOptions): Promise<InitResult> {
|
|
|
703
703
|
await writeFile(join(agentDir, ".gitignore"), METAOBJECTS_GITIGNORE_BODY, "utf8");
|
|
704
704
|
result.created.push(".metaobjects/.gitignore");
|
|
705
705
|
|
|
706
|
-
// .metaobjects/package.meta.json —
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
await writeFile(manifestPath, JSON.stringify(manifestBody, null, 2) + "\n", "utf8");
|
|
716
|
-
result.created.push(`.metaobjects/${PACKAGE_MANIFEST_FILE}`);
|
|
717
|
-
} else {
|
|
718
|
-
result.preserved.push(`.metaobjects/${PACKAGE_MANIFEST_FILE}`);
|
|
706
|
+
// .metaobjects/package.meta.json — the v0.3 prototype manifest is deprecated
|
|
707
|
+
// (nothing reads it; FR-023 metadata dependencies is its replacement), so
|
|
708
|
+
// `meta init` no longer scaffolds it. A pre-existing one is left untouched;
|
|
709
|
+
// just note the deprecation so an existing project sees it.
|
|
710
|
+
const legacyManifestPath = join(agentDir, PACKAGE_MANIFEST_FILE);
|
|
711
|
+
if (await fileExists(legacyManifestPath)) {
|
|
712
|
+
result.warnings.push(
|
|
713
|
+
`note: .metaobjects/${PACKAGE_MANIFEST_FILE} is deprecated (nothing reads it; removed in 2.0) — see docs/features/metadata-dependencies.md`,
|
|
714
|
+
);
|
|
719
715
|
}
|
|
720
716
|
|
|
721
717
|
await writeAgentContext(opts, result);
|
package/src/commands/migrate.ts
CHANGED
|
@@ -13,7 +13,8 @@ import { buildKyselyFromUrl, redactUrl } from "../lib/kysely.js";
|
|
|
13
13
|
import { log } from "../lib/log.js";
|
|
14
14
|
import { loadMemory, resolveCollection, resolveConfigDir, type Collection } from "@metaobjectsdev/sdk";
|
|
15
15
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
16
|
-
import {
|
|
16
|
+
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
17
|
+
import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
|
|
17
18
|
import {
|
|
18
19
|
allowOptionFor,
|
|
19
20
|
buildExpectedSchemaWithProvenance,
|
|
@@ -201,7 +202,8 @@ function warnIfLedgerRelocated(cwd: string, resolvedOutDir: string): void {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
/**
|
|
204
|
-
* Report what a declared `migrate.scope` left out
|
|
205
|
+
* Report what this run did not govern — what a declared `migrate.scope` left out, and
|
|
206
|
+
* what a DEPENDENCY owns (wording: `exclusionNotes`).
|
|
205
207
|
*
|
|
206
208
|
* STDOUT in text format, STDERR otherwise. `--format json` / `--format toon` put a
|
|
207
209
|
* single machine-readable document on stdout, and a prose line ahead of it breaks
|
|
@@ -210,11 +212,15 @@ function warnIfLedgerRelocated(cwd: string, resolvedOutDir: string): void {
|
|
|
210
212
|
* (`resolveFormat`): suppressing it outright would silence the note for every
|
|
211
213
|
* piped and CI run, which is most of them.
|
|
212
214
|
*/
|
|
213
|
-
function logOutOfScope(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
function logOutOfScope(
|
|
216
|
+
names: readonly string[],
|
|
217
|
+
fromDependencies: readonly string[],
|
|
218
|
+
fmt: OutputFormat,
|
|
219
|
+
): void {
|
|
220
|
+
for (const msg of exclusionNotes("migrate", names, fromDependencies)) {
|
|
221
|
+
if (fmt === "text") log.info(msg);
|
|
222
|
+
else log.warn(msg);
|
|
223
|
+
}
|
|
218
224
|
}
|
|
219
225
|
|
|
220
226
|
function emitStructuredError(error: string, hint: string, fmt: OutputFormat): void {
|
|
@@ -629,7 +635,7 @@ export async function migrateCommand(
|
|
|
629
635
|
let metadata;
|
|
630
636
|
try {
|
|
631
637
|
metadata = await loadMemory(collection.configDir, {
|
|
632
|
-
|
|
638
|
+
...collectionLoadOptions(collection),
|
|
633
639
|
...postgresLoadOptions,
|
|
634
640
|
});
|
|
635
641
|
} catch (err) {
|
|
@@ -679,9 +685,9 @@ export async function migrateCommand(
|
|
|
679
685
|
// leave the expected schema here and are suppressed on the actual side below —
|
|
680
686
|
// dropping them from `expected` ALONE would propose DROP TABLE for every one of
|
|
681
687
|
// them that exists in the database.
|
|
682
|
-
const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
|
|
688
|
+
const scoped = scopeExpectedSchema(built, collection.inMigrateScope, importedOption(collection));
|
|
683
689
|
const expected = scoped.snapshot;
|
|
684
|
-
logOutOfScope(scoped.outOfScope, fmt);
|
|
690
|
+
logOutOfScope(scoped.outOfScope, scoped.importedOutOfScope ?? [], fmt);
|
|
685
691
|
let actual;
|
|
686
692
|
try {
|
|
687
693
|
actual = await introspect(kysely.db, kysely.dialect);
|
|
@@ -1057,7 +1063,7 @@ export async function runBaseline(
|
|
|
1057
1063
|
try {
|
|
1058
1064
|
const collection = await resolveCollection(metaRoot);
|
|
1059
1065
|
metadata = await loadMemory(collection.configDir, {
|
|
1060
|
-
|
|
1066
|
+
...collectionLoadOptions(collection),
|
|
1061
1067
|
...baselineLoadOptions,
|
|
1062
1068
|
});
|
|
1063
1069
|
} catch (err) {
|
|
@@ -1196,7 +1202,7 @@ export async function runOfflineGenerate(
|
|
|
1196
1202
|
try {
|
|
1197
1203
|
collection = await resolveCollection(metaRoot);
|
|
1198
1204
|
metadata = await loadMemory(collection.configDir, {
|
|
1199
|
-
|
|
1205
|
+
...collectionLoadOptions(collection),
|
|
1200
1206
|
...offlineLoadOptions,
|
|
1201
1207
|
});
|
|
1202
1208
|
} catch (err) {
|
|
@@ -1256,6 +1262,10 @@ export async function runOfflineGenerate(
|
|
|
1256
1262
|
views: offlineViews,
|
|
1257
1263
|
// Per-command scope — narrows BOTH sides of the offline diff (see planOffline).
|
|
1258
1264
|
...(offlineScope !== undefined ? { inScope: offlineScope } : {}),
|
|
1265
|
+
// ...and the import exclusion, which planOffline threads into the SAME call.
|
|
1266
|
+
// Passing it only at the direct call sites would leave the offline pipeline —
|
|
1267
|
+
// the default `meta migrate` — performing no import exclusion at all.
|
|
1268
|
+
...importedOption(collection),
|
|
1259
1269
|
allow: tokensToAllowOptions(config.allow),
|
|
1260
1270
|
onAmbiguous: async (a) => {
|
|
1261
1271
|
collectedAmbiguous.push(a);
|
|
@@ -1280,7 +1290,7 @@ export async function runOfflineGenerate(
|
|
|
1280
1290
|
// entries); `expected` is the governed side the emitter renders against. Equal
|
|
1281
1291
|
// for an unscoped run.
|
|
1282
1292
|
const { diff: diffResult, nextSnapshot, expected: governedExpected } = plan;
|
|
1283
|
-
logOutOfScope(plan.outOfScope, fmt);
|
|
1293
|
+
logOutOfScope(plan.outOfScope, plan.importedOutOfScope ?? [], fmt);
|
|
1284
1294
|
|
|
1285
1295
|
if (diffResult.blocked.length > 0) {
|
|
1286
1296
|
log.error(`migrate: ${diffResult.blocked.length} destructive change(s) blocked; re-run with --allow <tokens>`);
|
|
@@ -1461,7 +1471,7 @@ async function runD1Migrate(
|
|
|
1461
1471
|
let metadata;
|
|
1462
1472
|
try {
|
|
1463
1473
|
metadata = await loadMemory(collection.configDir, {
|
|
1464
|
-
|
|
1474
|
+
...collectionLoadOptions(collection),
|
|
1465
1475
|
...d1LoadOptions,
|
|
1466
1476
|
});
|
|
1467
1477
|
} catch (err) {
|
|
@@ -1482,9 +1492,9 @@ async function runD1Migrate(
|
|
|
1482
1492
|
const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
|
|
1483
1493
|
if (scopeRc !== undefined) return scopeRc;
|
|
1484
1494
|
// Per-command scope — both-sided, exactly as on the Kysely path above.
|
|
1485
|
-
const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
|
|
1495
|
+
const scoped = scopeExpectedSchema(built, collection.inMigrateScope, importedOption(collection));
|
|
1486
1496
|
const expected = scoped.snapshot;
|
|
1487
|
-
logOutOfScope(scoped.outOfScope, fmt);
|
|
1497
|
+
logOutOfScope(scoped.outOfScope, scoped.importedOutOfScope ?? [], fmt);
|
|
1488
1498
|
let actual;
|
|
1489
1499
|
try {
|
|
1490
1500
|
actual = await introspectD1({
|
|
@@ -16,6 +16,7 @@ import { FileProvider } from "../lib/file-provider.js";
|
|
|
16
16
|
import { snapshotPaths, unifiedDiff } from "../lib/snapshot.js";
|
|
17
17
|
import { reportLoadError } from "../lib/load-error.js";
|
|
18
18
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
19
|
+
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
19
20
|
import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
|
|
20
21
|
import { TYPE_TEMPLATE, TEMPLATE_ATTR_TEXT_REF, TEMPLATE_ATTR_FORMAT } from "@metaobjectsdev/metadata";
|
|
21
22
|
import { render, ESCAPERS, type RenderFormat } from "@metaobjectsdev/render";
|
|
@@ -75,7 +76,7 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
|
|
|
75
76
|
let root;
|
|
76
77
|
try {
|
|
77
78
|
root = await loadMemory(collection.configDir, {
|
|
78
|
-
|
|
79
|
+
...collectionLoadOptions(collection),
|
|
79
80
|
...configLoadOptions,
|
|
80
81
|
});
|
|
81
82
|
} catch (err) {
|