@metaobjectsdev/cli 1.0.1 → 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 +34 -33
- 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/allow.d.ts.map +1 -1
- package/dist/src/lib/allow.js +7 -1
- package/dist/src/lib/allow.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 +36 -29
- 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/allow.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,8 +13,10 @@ 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 {
|
|
19
|
+
allowOptionFor,
|
|
18
20
|
buildExpectedSchemaWithProvenance,
|
|
19
21
|
scopeExpectedSchema,
|
|
20
22
|
scopedDiffInputs,
|
|
@@ -57,7 +59,7 @@ import {
|
|
|
57
59
|
type WranglerRunner,
|
|
58
60
|
} from "../lib/wrangler.js";
|
|
59
61
|
import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
|
|
60
|
-
import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
|
|
62
|
+
import { ALLOW_TOKEN_MAP, tokensToAllowOptions, describeChange } from "../lib/allow.js";
|
|
61
63
|
import { reportLoadError } from "../lib/load-error.js";
|
|
62
64
|
|
|
63
65
|
/**
|
|
@@ -200,7 +202,8 @@ function warnIfLedgerRelocated(cwd: string, resolvedOutDir: string): void {
|
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
/**
|
|
203
|
-
* 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`).
|
|
204
207
|
*
|
|
205
208
|
* STDOUT in text format, STDERR otherwise. `--format json` / `--format toon` put a
|
|
206
209
|
* single machine-readable document on stdout, and a prose line ahead of it breaks
|
|
@@ -209,11 +212,15 @@ function warnIfLedgerRelocated(cwd: string, resolvedOutDir: string): void {
|
|
|
209
212
|
* (`resolveFormat`): suppressing it outright would silence the note for every
|
|
210
213
|
* piped and CI run, which is most of them.
|
|
211
214
|
*/
|
|
212
|
-
function logOutOfScope(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
}
|
|
217
224
|
}
|
|
218
225
|
|
|
219
226
|
function emitStructuredError(error: string, hint: string, fmt: OutputFormat): void {
|
|
@@ -376,24 +383,20 @@ async function snapshotAbsentDrops(changes: Change[], snapPath: string): Promise
|
|
|
376
383
|
return absent;
|
|
377
384
|
}
|
|
378
385
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
case "change-column-nullable": return "nullable-to-not-null";
|
|
387
|
-
case "change-column-default": return "drop-identity-default";
|
|
388
|
-
default: return kind;
|
|
389
|
-
}
|
|
386
|
+
/** The `--allow` token that unblocks `c`. migrate-ts picks the permission by what blocked the
|
|
387
|
+
* change (a type change can be blocked by the auto-sequence default it carries), so this only
|
|
388
|
+
* maps that permission back to its CLI token. */
|
|
389
|
+
function allowFlagFor(c: Change): string {
|
|
390
|
+
const option = allowOptionFor(c);
|
|
391
|
+
const token = Object.keys(ALLOW_TOKEN_MAP).find((t) => ALLOW_TOKEN_MAP[t] === option);
|
|
392
|
+
return token ?? c.kind;
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
function blockedToEntries(err: BlockedChangesError): BlockedEntry[] {
|
|
393
396
|
return err.blocked.map((c) => ({
|
|
394
397
|
kind: c.kind,
|
|
395
398
|
description: describeChange(c),
|
|
396
|
-
allowFlag: allowFlagFor(c
|
|
399
|
+
allowFlag: allowFlagFor(c),
|
|
397
400
|
}));
|
|
398
401
|
}
|
|
399
402
|
|
|
@@ -632,7 +635,7 @@ export async function migrateCommand(
|
|
|
632
635
|
let metadata;
|
|
633
636
|
try {
|
|
634
637
|
metadata = await loadMemory(collection.configDir, {
|
|
635
|
-
|
|
638
|
+
...collectionLoadOptions(collection),
|
|
636
639
|
...postgresLoadOptions,
|
|
637
640
|
});
|
|
638
641
|
} catch (err) {
|
|
@@ -682,9 +685,9 @@ export async function migrateCommand(
|
|
|
682
685
|
// leave the expected schema here and are suppressed on the actual side below —
|
|
683
686
|
// dropping them from `expected` ALONE would propose DROP TABLE for every one of
|
|
684
687
|
// them that exists in the database.
|
|
685
|
-
const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
|
|
688
|
+
const scoped = scopeExpectedSchema(built, collection.inMigrateScope, importedOption(collection));
|
|
686
689
|
const expected = scoped.snapshot;
|
|
687
|
-
logOutOfScope(scoped.outOfScope, fmt);
|
|
690
|
+
logOutOfScope(scoped.outOfScope, scoped.importedOutOfScope ?? [], fmt);
|
|
688
691
|
let actual;
|
|
689
692
|
try {
|
|
690
693
|
actual = await introspect(kysely.db, kysely.dialect);
|
|
@@ -1060,7 +1063,7 @@ export async function runBaseline(
|
|
|
1060
1063
|
try {
|
|
1061
1064
|
const collection = await resolveCollection(metaRoot);
|
|
1062
1065
|
metadata = await loadMemory(collection.configDir, {
|
|
1063
|
-
|
|
1066
|
+
...collectionLoadOptions(collection),
|
|
1064
1067
|
...baselineLoadOptions,
|
|
1065
1068
|
});
|
|
1066
1069
|
} catch (err) {
|
|
@@ -1199,7 +1202,7 @@ export async function runOfflineGenerate(
|
|
|
1199
1202
|
try {
|
|
1200
1203
|
collection = await resolveCollection(metaRoot);
|
|
1201
1204
|
metadata = await loadMemory(collection.configDir, {
|
|
1202
|
-
|
|
1205
|
+
...collectionLoadOptions(collection),
|
|
1203
1206
|
...offlineLoadOptions,
|
|
1204
1207
|
});
|
|
1205
1208
|
} catch (err) {
|
|
@@ -1259,6 +1262,10 @@ export async function runOfflineGenerate(
|
|
|
1259
1262
|
views: offlineViews,
|
|
1260
1263
|
// Per-command scope — narrows BOTH sides of the offline diff (see planOffline).
|
|
1261
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),
|
|
1262
1269
|
allow: tokensToAllowOptions(config.allow),
|
|
1263
1270
|
onAmbiguous: async (a) => {
|
|
1264
1271
|
collectedAmbiguous.push(a);
|
|
@@ -1283,7 +1290,7 @@ export async function runOfflineGenerate(
|
|
|
1283
1290
|
// entries); `expected` is the governed side the emitter renders against. Equal
|
|
1284
1291
|
// for an unscoped run.
|
|
1285
1292
|
const { diff: diffResult, nextSnapshot, expected: governedExpected } = plan;
|
|
1286
|
-
logOutOfScope(plan.outOfScope, fmt);
|
|
1293
|
+
logOutOfScope(plan.outOfScope, plan.importedOutOfScope ?? [], fmt);
|
|
1287
1294
|
|
|
1288
1295
|
if (diffResult.blocked.length > 0) {
|
|
1289
1296
|
log.error(`migrate: ${diffResult.blocked.length} destructive change(s) blocked; re-run with --allow <tokens>`);
|
|
@@ -1464,7 +1471,7 @@ async function runD1Migrate(
|
|
|
1464
1471
|
let metadata;
|
|
1465
1472
|
try {
|
|
1466
1473
|
metadata = await loadMemory(collection.configDir, {
|
|
1467
|
-
|
|
1474
|
+
...collectionLoadOptions(collection),
|
|
1468
1475
|
...d1LoadOptions,
|
|
1469
1476
|
});
|
|
1470
1477
|
} catch (err) {
|
|
@@ -1485,9 +1492,9 @@ async function runD1Migrate(
|
|
|
1485
1492
|
const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
|
|
1486
1493
|
if (scopeRc !== undefined) return scopeRc;
|
|
1487
1494
|
// Per-command scope — both-sided, exactly as on the Kysely path above.
|
|
1488
|
-
const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
|
|
1495
|
+
const scoped = scopeExpectedSchema(built, collection.inMigrateScope, importedOption(collection));
|
|
1489
1496
|
const expected = scoped.snapshot;
|
|
1490
|
-
logOutOfScope(scoped.outOfScope, fmt);
|
|
1497
|
+
logOutOfScope(scoped.outOfScope, scoped.importedOutOfScope ?? [], fmt);
|
|
1491
1498
|
let actual;
|
|
1492
1499
|
try {
|
|
1493
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) {
|