@deftai/directive-core 0.68.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/doctor/constants.d.ts +9 -0
- package/dist/doctor/constants.js +11 -0
- package/dist/doctor/main.d.ts +47 -1
- package/dist/doctor/main.js +352 -2
- package/dist/doctor/payload-staleness.d.ts +8 -0
- package/dist/doctor/payload-staleness.js +11 -8
- package/dist/doctor/types.d.ts +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/gitignore.d.ts +18 -0
- package/dist/init-deposit/gitignore.js +94 -15
- package/dist/init-deposit/headless-manifest.d.ts +87 -0
- package/dist/init-deposit/headless-manifest.js +261 -0
- package/dist/init-deposit/hygiene.js +11 -0
- package/dist/init-deposit/index.d.ts +3 -0
- package/dist/init-deposit/index.js +3 -0
- package/dist/init-deposit/init-dispatch.d.ts +90 -0
- package/dist/init-deposit/init-dispatch.js +170 -0
- package/dist/init-deposit/refresh.d.ts +79 -2
- package/dist/init-deposit/refresh.js +177 -5
- package/dist/init-deposit/scaffold.d.ts +25 -0
- package/dist/init-deposit/scaffold.js +57 -0
- package/dist/init-deposit/untrack-core.d.ts +84 -0
- package/dist/init-deposit/untrack-core.js +150 -0
- package/dist/resolution/classify.d.ts +41 -0
- package/dist/resolution/classify.js +138 -0
- package/dist/resolution/engine-ladder.d.ts +98 -0
- package/dist/resolution/engine-ladder.js +185 -0
- package/dist/resolution/index.d.ts +19 -0
- package/dist/resolution/index.js +19 -0
- package/dist/resolution/integrity.d.ts +48 -0
- package/dist/resolution/integrity.js +80 -0
- package/dist/resolution/package-manager.d.ts +77 -0
- package/dist/resolution/package-manager.js +103 -0
- package/dist/resolution/pin.d.ts +73 -0
- package/dist/resolution/pin.js +169 -0
- package/dist/resolution/plan.d.ts +66 -0
- package/dist/resolution/plan.js +219 -0
- package/dist/resolution/skew-policy.d.ts +46 -0
- package/dist/resolution/skew-policy.js +120 -0
- package/dist/session/session-start.d.ts +2 -0
- package/dist/session/session-start.js +28 -1
- package/dist/user-config/index.d.ts +2 -0
- package/dist/user-config/index.js +2 -0
- package/dist/user-config/resolve-user-md.d.ts +76 -0
- package/dist/user-config/resolve-user-md.js +120 -0
- package/dist/xbrief-migrate/constants.d.ts +10 -0
- package/dist/xbrief-migrate/constants.js +19 -0
- package/dist/xbrief-migrate/detect.d.ts +34 -0
- package/dist/xbrief-migrate/detect.js +33 -0
- package/dist/xbrief-migrate/fs-helpers.d.ts +13 -0
- package/dist/xbrief-migrate/fs-helpers.js +46 -1
- package/dist/xbrief-migrate/index.d.ts +4 -2
- package/dist/xbrief-migrate/index.js +4 -2
- package/dist/xbrief-migrate/migrate-project.d.ts +23 -1
- package/dist/xbrief-migrate/migrate-project.js +97 -8
- package/dist/xbrief-migrate/signpost.d.ts +7 -1
- package/dist/xbrief-migrate/signpost.js +18 -3
- package/package.json +7 -3
|
@@ -43,6 +43,24 @@ export declare function resolveInitGitignoreLines(projectDir: string, gitLsFiles
|
|
|
43
43
|
export declare function ensureInitGitignoreLines(projectDir: string, io: InitDepositIo, options?: {
|
|
44
44
|
gitLsFiles?: GitLsFiles;
|
|
45
45
|
}): EnsureInitGitignoreResult;
|
|
46
|
+
/**
|
|
47
|
+
* The reconciled ignore set for the vendored→hybrid un-commit (#2269): the
|
|
48
|
+
* canonical baseline (which already covers `.deft/.cli/`, `.deft/ritual-state.json`,
|
|
49
|
+
* and the `.deft-cache/` path) plus the `.deft/core/` deposit entry that
|
|
50
|
+
* greenfield init born-ignores. `package.json` is deliberately absent — the
|
|
51
|
+
* committed pin MUST stay tracked so content can be reconstituted after the
|
|
52
|
+
* deposit is un-committed.
|
|
53
|
+
*/
|
|
54
|
+
export declare const UNTRACK_CORE_GITIGNORE_LINES: readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* Reconcile `.gitignore` for the `migrate --untrack-core` path: force the
|
|
57
|
+
* `.deft/core/` deposit entry (init leaves a tracked deposit alone, but
|
|
58
|
+
* un-track has just removed it from the index, so it MUST now be ignored) plus
|
|
59
|
+
* the canonical baseline, and never ignore the committed `package.json` pin.
|
|
60
|
+
* Idempotent: a second run over an already-reconciled `.gitignore` makes no
|
|
61
|
+
* change.
|
|
62
|
+
*/
|
|
63
|
+
export declare function ensureUntrackCoreGitignoreLines(projectDir: string, io: InitDepositIo): EnsureInitGitignoreResult;
|
|
46
64
|
/**
|
|
47
65
|
* Copy the content package into `.deft/core`, reporting whether the deposit was
|
|
48
66
|
* absent before copy (reconstitution). Always refreshes when present.
|
|
@@ -22,6 +22,7 @@ const DEFT_CORE_COVERING_LINES = new Set([".deft/core/", ".deft/core"]);
|
|
|
22
22
|
*/
|
|
23
23
|
export const CANONICAL_GITIGNORE_BASELINE = [
|
|
24
24
|
".deft-cache/",
|
|
25
|
+
".deft/.cli/",
|
|
25
26
|
".deft/ritual-state.json",
|
|
26
27
|
".deft/last-session.json",
|
|
27
28
|
".deft/routing.local.json",
|
|
@@ -95,14 +96,20 @@ export function resolveInitGitignoreLines(projectDir, gitLsFiles = defaultGitLsF
|
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* Ignore entries the deft framework MUST NEVER add to `.gitignore`. The
|
|
100
|
+
* committed `package.json` pin (#2264) is the reconstitution anchor for the
|
|
101
|
+
* un-committed deposit; ignoring it would silently drop the pin from version
|
|
102
|
+
* control and break `migrate --untrack-core`'s reconstitution guarantee (#2269).
|
|
101
103
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const NEVER_IGNORE_LINES = new Set(["package.json", "/package.json"]);
|
|
105
|
+
/**
|
|
106
|
+
* Read, heal, and reconcile `.gitignore` against `targetLines`. Shared by the
|
|
107
|
+
* greenfield-init and the vendored→hybrid un-commit reconcilers so the
|
|
108
|
+
* read/heal/append/write path lives in exactly one place. Pure of any caller
|
|
109
|
+
* messaging — the callers decide what to print from the returned record. The
|
|
110
|
+
* `package.json` pin is filtered out unconditionally (#2269 invariant).
|
|
111
|
+
*/
|
|
112
|
+
function reconcileGitignoreFile(projectDir, targetLines, includeDeftCoreRationale) {
|
|
106
113
|
const path = join(projectDir, ".gitignore");
|
|
107
114
|
let existing = "";
|
|
108
115
|
if (existsSync(path)) {
|
|
@@ -134,16 +141,20 @@ export function ensureInitGitignoreLines(projectDir, io, options = {}) {
|
|
|
134
141
|
}
|
|
135
142
|
const additions = [];
|
|
136
143
|
for (const line of targetLines) {
|
|
144
|
+
// Invariant (#2269): never ignore the committed package.json pin.
|
|
145
|
+
if (NEVER_IGNORE_LINES.has(line.trim()))
|
|
146
|
+
continue;
|
|
137
147
|
if (!gitignoreCoversLine(present, line)) {
|
|
138
148
|
additions.push(line);
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
if (!blanketRemoved && additions.length === 0) {
|
|
142
|
-
io.printf(".gitignore already covers the canonical deft entries — skipping.\n");
|
|
143
152
|
return {
|
|
144
153
|
changed: false,
|
|
154
|
+
additions,
|
|
155
|
+
blanketRemoved,
|
|
145
156
|
deftCoreIgnored: gitignoreCoversLine(present, GITIGNORE_DEFT_CORE_LINE),
|
|
146
|
-
|
|
157
|
+
alreadyCovered: true,
|
|
147
158
|
};
|
|
148
159
|
}
|
|
149
160
|
let healed = kept.join("\n");
|
|
@@ -159,7 +170,7 @@ export function ensureInitGitignoreLines(projectDir, io, options = {}) {
|
|
|
159
170
|
body += "\n";
|
|
160
171
|
}
|
|
161
172
|
body += DEFT_FRAMEWORK_GITIGNORE_HEADER;
|
|
162
|
-
if (
|
|
173
|
+
if (includeDeftCoreRationale && additions.includes(GITIGNORE_DEFT_CORE_LINE)) {
|
|
163
174
|
body += DEFT_CORE_GITIGNORE_RATIONALE;
|
|
164
175
|
}
|
|
165
176
|
for (const add of additions) {
|
|
@@ -172,22 +183,90 @@ export function ensureInitGitignoreLines(projectDir, io, options = {}) {
|
|
|
172
183
|
catch (cause) {
|
|
173
184
|
throw new Error(`could not write .gitignore: ${String(cause)}`);
|
|
174
185
|
}
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
const finalPresent = collectPresentGitignoreLines(body);
|
|
187
|
+
return {
|
|
188
|
+
changed: true,
|
|
189
|
+
additions,
|
|
190
|
+
blanketRemoved,
|
|
191
|
+
deftCoreIgnored: gitignoreCoversLine(finalPresent, GITIGNORE_DEFT_CORE_LINE),
|
|
192
|
+
alreadyCovered: false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Ensure the consumer `.gitignore` carries the canonical baseline plus, for
|
|
197
|
+
* greenfield installs, the `.deft/core/` ignore entry. Heals forbidden blanket
|
|
198
|
+
* `vbrief/.eval/` lines (#1464). Never un-commits a tracked deposit (#1941).
|
|
199
|
+
*/
|
|
200
|
+
export function ensureInitGitignoreLines(projectDir, io, options = {}) {
|
|
201
|
+
const gitLsFiles = options.gitLsFiles ?? defaultGitLsFiles;
|
|
202
|
+
const { lines: targetLines, includeDeftCore } = resolveInitGitignoreLines(projectDir, gitLsFiles);
|
|
203
|
+
const tracked = isDepositTrackedInGit(projectDir, gitLsFiles);
|
|
204
|
+
const res = reconcileGitignoreFile(projectDir, targetLines, includeDeftCore);
|
|
205
|
+
if (res.alreadyCovered) {
|
|
206
|
+
io.printf(".gitignore already covers the canonical deft entries — skipping.\n");
|
|
207
|
+
return {
|
|
208
|
+
changed: false,
|
|
209
|
+
deftCoreIgnored: res.deftCoreIgnored,
|
|
210
|
+
skippedDeftCoreBecauseTracked: tracked === true,
|
|
211
|
+
};
|
|
177
212
|
}
|
|
178
|
-
if (
|
|
213
|
+
if (res.additions.length > 0) {
|
|
214
|
+
io.printf(`.gitignore updated with canonical entries: ${res.additions.join(", ")}\n`);
|
|
215
|
+
}
|
|
216
|
+
if (res.blanketRemoved) {
|
|
179
217
|
io.printf(".gitignore healed: removed forbidden blanket vbrief/.eval/ line (#1464).\n");
|
|
180
218
|
}
|
|
181
219
|
if (tracked === true) {
|
|
182
220
|
io.printf(".deft/core is tracked in git — leaving it tracked; vendored→hybrid un-commit is #1941.\n");
|
|
183
221
|
}
|
|
184
|
-
const finalPresent = collectPresentGitignoreLines(body);
|
|
185
222
|
return {
|
|
186
223
|
changed: true,
|
|
187
|
-
deftCoreIgnored:
|
|
224
|
+
deftCoreIgnored: res.deftCoreIgnored,
|
|
188
225
|
skippedDeftCoreBecauseTracked: tracked === true,
|
|
189
226
|
};
|
|
190
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* The reconciled ignore set for the vendored→hybrid un-commit (#2269): the
|
|
230
|
+
* canonical baseline (which already covers `.deft/.cli/`, `.deft/ritual-state.json`,
|
|
231
|
+
* and the `.deft-cache/` path) plus the `.deft/core/` deposit entry that
|
|
232
|
+
* greenfield init born-ignores. `package.json` is deliberately absent — the
|
|
233
|
+
* committed pin MUST stay tracked so content can be reconstituted after the
|
|
234
|
+
* deposit is un-committed.
|
|
235
|
+
*/
|
|
236
|
+
export const UNTRACK_CORE_GITIGNORE_LINES = [
|
|
237
|
+
...CANONICAL_GITIGNORE_BASELINE,
|
|
238
|
+
GITIGNORE_DEFT_CORE_LINE,
|
|
239
|
+
];
|
|
240
|
+
/**
|
|
241
|
+
* Reconcile `.gitignore` for the `migrate --untrack-core` path: force the
|
|
242
|
+
* `.deft/core/` deposit entry (init leaves a tracked deposit alone, but
|
|
243
|
+
* un-track has just removed it from the index, so it MUST now be ignored) plus
|
|
244
|
+
* the canonical baseline, and never ignore the committed `package.json` pin.
|
|
245
|
+
* Idempotent: a second run over an already-reconciled `.gitignore` makes no
|
|
246
|
+
* change.
|
|
247
|
+
*/
|
|
248
|
+
export function ensureUntrackCoreGitignoreLines(projectDir, io) {
|
|
249
|
+
const res = reconcileGitignoreFile(projectDir, UNTRACK_CORE_GITIGNORE_LINES, false);
|
|
250
|
+
if (res.alreadyCovered) {
|
|
251
|
+
io.printf(".gitignore already ignores .deft/core/ and the canonical deft entries — skipping.\n");
|
|
252
|
+
return {
|
|
253
|
+
changed: false,
|
|
254
|
+
deftCoreIgnored: res.deftCoreIgnored,
|
|
255
|
+
skippedDeftCoreBecauseTracked: false,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (res.additions.length > 0) {
|
|
259
|
+
io.printf(`.gitignore updated with canonical entries: ${res.additions.join(", ")}\n`);
|
|
260
|
+
}
|
|
261
|
+
if (res.blanketRemoved) {
|
|
262
|
+
io.printf(".gitignore healed: removed forbidden blanket vbrief/.eval/ line (#1464).\n");
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
changed: true,
|
|
266
|
+
deftCoreIgnored: res.deftCoreIgnored,
|
|
267
|
+
skippedDeftCoreBecauseTracked: false,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
191
270
|
/**
|
|
192
271
|
* Copy the content package into `.deft/core`, reporting whether the deposit was
|
|
193
272
|
* absent before copy (reconstitution). Always refreshes when present.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `directive init --headless` — manifest emitter for no-shell consumer products
|
|
3
|
+
* (#2268 / epic #2203).
|
|
4
|
+
*
|
|
5
|
+
* Hosted consumer products (e.g. deftai/deftvisage#1036) have no shell or npm
|
|
6
|
+
* access to the end user's machine. This module serialises the merged keystone
|
|
7
|
+
* `plan()` schema into a `{ version, files: [{ path, content, encoding }] }`
|
|
8
|
+
* manifest with ALL execution side effects suppressed: no interactive prompts,
|
|
9
|
+
* no git operations, no git-hook installation, and no filesystem writes outside
|
|
10
|
+
* the explicit `--output` target. It makes no git-repo assumption and runs
|
|
11
|
+
* against an empty / non-existent directory.
|
|
12
|
+
*
|
|
13
|
+
* Single-sourcing (the #2268 acceptance): the manifest is DERIVED from `plan()`
|
|
14
|
+
* — the collected file set is threaded through `plan(..., { files })` and read
|
|
15
|
+
* back out of {@link ResolutionPlan.files}. There is no separate "what would
|
|
16
|
+
* init produce" reimplementation; the collectors here are the manifest-only,
|
|
17
|
+
* read-only counterparts of the executing deposit path in `scaffold.ts` /
|
|
18
|
+
* `init-deposit.ts` (they reuse the same render helpers WITHOUT filesystem or
|
|
19
|
+
* git writes).
|
|
20
|
+
*
|
|
21
|
+
* On registry / content-resolution failure the CLI wrapper exits non-zero and
|
|
22
|
+
* emits a JSON error object so the calling backend can treat it as non-fatal
|
|
23
|
+
* (never a crash or a partial write).
|
|
24
|
+
*/
|
|
25
|
+
import type { ResolutionFile } from "@deftai/directive-types";
|
|
26
|
+
/** The five xBRIEF lifecycle folders scaffolded into a fresh project. */
|
|
27
|
+
export declare const HEADLESS_XBRIEF_LIFECYCLE_DIRS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
|
|
28
|
+
/** A single file entry in the headless manifest. */
|
|
29
|
+
export type HeadlessManifestFile = ResolutionFile;
|
|
30
|
+
/** The serialised headless manifest shape. */
|
|
31
|
+
export interface HeadlessManifest {
|
|
32
|
+
/** Resolved framework version the manifest was built against. */
|
|
33
|
+
readonly version: string;
|
|
34
|
+
/** All files a consumer backend should materialise for a fresh install. */
|
|
35
|
+
readonly files: readonly HeadlessManifestFile[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* JSON error object emitted on a registry / content-resolution failure. The
|
|
39
|
+
* calling backend treats a non-zero exit + this object as non-fatal.
|
|
40
|
+
*/
|
|
41
|
+
export interface HeadlessManifestErrorObject {
|
|
42
|
+
readonly success: false;
|
|
43
|
+
readonly error: string;
|
|
44
|
+
readonly error_code: string;
|
|
45
|
+
}
|
|
46
|
+
/** Injectable seams; defaults are the real content-package resolution path. */
|
|
47
|
+
export interface HeadlessManifestSeams {
|
|
48
|
+
/** Resolve the installed content-package root. Default: real npm resolution. */
|
|
49
|
+
readonly resolveContentRoot?: () => Promise<string>;
|
|
50
|
+
/** Override the resolved version (default: read the content package.json). */
|
|
51
|
+
readonly readVersion?: (contentRoot: string) => string;
|
|
52
|
+
/** Deterministic UTC ISO timestamp for the VERSION marker (default: now). */
|
|
53
|
+
readonly nowIso?: () => string;
|
|
54
|
+
/** Deterministic managed-section session id (default: fixed headless token). */
|
|
55
|
+
readonly newSession?: () => string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build the headless manifest. Reads the content package read-only, collects
|
|
59
|
+
* the fresh-install file set (payload + version-consistent AGENTS.md + xBRIEF
|
|
60
|
+
* scaffold + pinned package.json), threads it through `plan()` so the manifest
|
|
61
|
+
* is single-sourced from the resolution schema, and returns the serialisable
|
|
62
|
+
* shape. Performs NO filesystem writes and NO git operations.
|
|
63
|
+
*
|
|
64
|
+
* Throws (never partially writes) on content-resolution failure; the CLI
|
|
65
|
+
* wrapper converts the throw into a JSON error object + non-zero exit.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildHeadlessManifest(seams?: HeadlessManifestSeams): Promise<HeadlessManifest>;
|
|
68
|
+
export interface RunInitHeadlessCliOptions {
|
|
69
|
+
/** Explicit `--output` target, or null to emit the manifest to stdout. */
|
|
70
|
+
readonly outputPath: string | null;
|
|
71
|
+
readonly writeOut: (text: string) => void;
|
|
72
|
+
readonly writeErr: (text: string) => void;
|
|
73
|
+
readonly seams?: HeadlessManifestSeams;
|
|
74
|
+
/**
|
|
75
|
+
* Filesystem write seam (test-only). The default creates the parent directory
|
|
76
|
+
* and writes the file; an injected seam OWNS all filesystem contact (including
|
|
77
|
+
* directory creation), so the manifest write stays fully isolatable in tests.
|
|
78
|
+
*/
|
|
79
|
+
readonly writeFile?: (path: string, data: string) => void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* CLI-facing entrypoint for `directive init --headless`. Emits the manifest to
|
|
83
|
+
* `--output` (the ONLY permitted filesystem write) or stdout, and on failure
|
|
84
|
+
* writes a JSON error object to stdout and returns exit code 1.
|
|
85
|
+
*/
|
|
86
|
+
export declare function runInitHeadlessCli(options: RunInitHeadlessCliOptions): Promise<number>;
|
|
87
|
+
//# sourceMappingURL=headless-manifest.d.ts.map
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `directive init --headless` — manifest emitter for no-shell consumer products
|
|
3
|
+
* (#2268 / epic #2203).
|
|
4
|
+
*
|
|
5
|
+
* Hosted consumer products (e.g. deftai/deftvisage#1036) have no shell or npm
|
|
6
|
+
* access to the end user's machine. This module serialises the merged keystone
|
|
7
|
+
* `plan()` schema into a `{ version, files: [{ path, content, encoding }] }`
|
|
8
|
+
* manifest with ALL execution side effects suppressed: no interactive prompts,
|
|
9
|
+
* no git operations, no git-hook installation, and no filesystem writes outside
|
|
10
|
+
* the explicit `--output` target. It makes no git-repo assumption and runs
|
|
11
|
+
* against an empty / non-existent directory.
|
|
12
|
+
*
|
|
13
|
+
* Single-sourcing (the #2268 acceptance): the manifest is DERIVED from `plan()`
|
|
14
|
+
* — the collected file set is threaded through `plan(..., { files })` and read
|
|
15
|
+
* back out of {@link ResolutionPlan.files}. There is no separate "what would
|
|
16
|
+
* init produce" reimplementation; the collectors here are the manifest-only,
|
|
17
|
+
* read-only counterparts of the executing deposit path in `scaffold.ts` /
|
|
18
|
+
* `init-deposit.ts` (they reuse the same render helpers WITHOUT filesystem or
|
|
19
|
+
* git writes).
|
|
20
|
+
*
|
|
21
|
+
* On registry / content-resolution failure the CLI wrapper exits non-zero and
|
|
22
|
+
* emits a JSON error object so the calling backend can treat it as non-fatal
|
|
23
|
+
* (never a crash or a partial write).
|
|
24
|
+
*/
|
|
25
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
27
|
+
import { ContentPackageNotFoundError, resolveInstalledContentRoot, } from "../deposit/resolve-content.js";
|
|
28
|
+
import { readCorePackageVersion } from "../engine-version.js";
|
|
29
|
+
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
30
|
+
import { plan } from "../resolution/plan.js";
|
|
31
|
+
import { CANONICAL_INSTALL_ROOT } from "./constants.js";
|
|
32
|
+
import { buildInstallManifestText, PIN_DEPENDENCY_NAME } from "./scaffold.js";
|
|
33
|
+
/** POSIX-relative path of the payload VERSION marker inside the manifest. */
|
|
34
|
+
const PAYLOAD_VERSION_PATH = `${CANONICAL_INSTALL_ROOT}/VERSION`;
|
|
35
|
+
/** Provenance stamp written into the emitted `.deft/core/VERSION` marker. */
|
|
36
|
+
const HEADLESS_FETCHED_BY = "directive-init-headless";
|
|
37
|
+
/** The five xBRIEF lifecycle folders scaffolded into a fresh project. */
|
|
38
|
+
export const HEADLESS_XBRIEF_LIFECYCLE_DIRS = [
|
|
39
|
+
"proposed",
|
|
40
|
+
"pending",
|
|
41
|
+
"active",
|
|
42
|
+
"completed",
|
|
43
|
+
"cancelled",
|
|
44
|
+
];
|
|
45
|
+
const XBRIEF_LIFECYCLE_GITKEEP = `# Keeps this xbrief/ lifecycle directory present in version control so the
|
|
46
|
+
# deft-directive-setup pre-cutover guard does not fire on a fresh headless
|
|
47
|
+
# install. Emitted by \`directive init --headless\` (#2268).
|
|
48
|
+
`;
|
|
49
|
+
function defaultNowIso() {
|
|
50
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
51
|
+
}
|
|
52
|
+
/** Read the resolved version from the content package.json (engine fallback). */
|
|
53
|
+
function resolveVersion(contentRoot, seams) {
|
|
54
|
+
if (seams.readVersion)
|
|
55
|
+
return seams.readVersion(contentRoot);
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(readFileSync(join(contentRoot, "package.json"), "utf8"));
|
|
58
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
59
|
+
const version = parsed.version;
|
|
60
|
+
if (version?.trim())
|
|
61
|
+
return version.trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Fall through to the engine version.
|
|
66
|
+
}
|
|
67
|
+
return readCorePackageVersion();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Decide the manifest encoding for a file's raw bytes. UTF-8 text round-trips
|
|
71
|
+
* losslessly; anything else (or bytes carrying a NUL) is base64 so binary
|
|
72
|
+
* payload assets survive JSON serialisation without corruption.
|
|
73
|
+
*/
|
|
74
|
+
function encodeBytes(buf) {
|
|
75
|
+
if (!buf.includes(0)) {
|
|
76
|
+
const text = buf.toString("utf8");
|
|
77
|
+
if (Buffer.from(text, "utf8").equals(buf)) {
|
|
78
|
+
return { content: text, encoding: "utf-8" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { content: buf.toString("base64"), encoding: "base64" };
|
|
82
|
+
}
|
|
83
|
+
/** POSIX-normalise a path (manifest paths are always forward-slash). */
|
|
84
|
+
function toPosix(p) {
|
|
85
|
+
return sep === "/" ? p : p.split(sep).join("/");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Walk the content-package tree read-only and collect one manifest entry per
|
|
89
|
+
* file, prefixed with the canonical install root. The manifest-only counterpart
|
|
90
|
+
* of `reconstituteDepositFromContent` — it materialises bytes into memory
|
|
91
|
+
* instead of copying them onto disk.
|
|
92
|
+
*/
|
|
93
|
+
function collectPayloadFiles(contentRoot) {
|
|
94
|
+
const out = [];
|
|
95
|
+
const stack = [contentRoot];
|
|
96
|
+
while (stack.length > 0) {
|
|
97
|
+
const dir = stack.pop();
|
|
98
|
+
if (dir === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
101
|
+
const full = join(dir, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
stack.push(full);
|
|
104
|
+
}
|
|
105
|
+
else if (entry.isFile()) {
|
|
106
|
+
const rel = toPosix(relative(contentRoot, full));
|
|
107
|
+
const { content, encoding } = encodeBytes(readFileSync(full));
|
|
108
|
+
out.push({ path: `${CANONICAL_INSTALL_ROOT}/${rel}`, content, encoding });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Render the version-consistent `.deft/core/VERSION` marker. The `sha=` slot is
|
|
116
|
+
* stamped with the resolved version (headless mode has no git checkout), which
|
|
117
|
+
* is what keeps the emitted AGENTS.md managed section and the payload provably
|
|
118
|
+
* consistent against a single resolved version.
|
|
119
|
+
*/
|
|
120
|
+
function collectVersionFile(version, nowIso) {
|
|
121
|
+
const tag = version.startsWith("v") ? version : `v${version}`;
|
|
122
|
+
const body = buildInstallManifestText({
|
|
123
|
+
ref: tag,
|
|
124
|
+
sha: version,
|
|
125
|
+
tag,
|
|
126
|
+
installRoot: CANONICAL_INSTALL_ROOT,
|
|
127
|
+
fetchedAt: nowIso,
|
|
128
|
+
fetchedBy: HEADLESS_FETCHED_BY,
|
|
129
|
+
});
|
|
130
|
+
return { path: PAYLOAD_VERSION_PATH, content: body, encoding: "utf-8" };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Render the greenfield AGENTS.md (unmanaged header + attributed managed
|
|
134
|
+
* section) from the SAME content root as the payload, so the managed section
|
|
135
|
+
* can never drift from the deposited framework version. The managed-section
|
|
136
|
+
* `sha=` is stamped with the resolved version for version-consistency.
|
|
137
|
+
*/
|
|
138
|
+
function collectAgentsMdFile(contentRoot, version, nowIso, sessionId) {
|
|
139
|
+
const agentsResult = agentsRefreshPlan("headless-project", {
|
|
140
|
+
frameworkRoot: contentRoot,
|
|
141
|
+
resolveSha: () => version,
|
|
142
|
+
nowIso: () => nowIso,
|
|
143
|
+
newSession: () => sessionId,
|
|
144
|
+
readAgents: () => null,
|
|
145
|
+
});
|
|
146
|
+
const newContent = agentsResult.new_content;
|
|
147
|
+
if (typeof newContent !== "string") {
|
|
148
|
+
throw new Error(`AGENTS.md render produced no content (state: ${String(agentsResult.state)})`);
|
|
149
|
+
}
|
|
150
|
+
return { path: "AGENTS.md", content: newContent, encoding: "utf-8" };
|
|
151
|
+
}
|
|
152
|
+
/** The five xBRIEF lifecycle `.gitkeep` files (empty dirs can't ride a manifest). */
|
|
153
|
+
function collectXbriefScaffoldFiles() {
|
|
154
|
+
return HEADLESS_XBRIEF_LIFECYCLE_DIRS.map((sub) => ({
|
|
155
|
+
path: `xbrief/${sub}/.gitkeep`,
|
|
156
|
+
content: XBRIEF_LIFECYCLE_GITKEEP,
|
|
157
|
+
encoding: "utf-8",
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/** The pinned `package.json` devDependency snippet (exact, private workspace). */
|
|
161
|
+
function collectPackageJsonFile(version) {
|
|
162
|
+
const pinVersion = version.trim().replace(/^v/i, "");
|
|
163
|
+
const pkg = {
|
|
164
|
+
private: true,
|
|
165
|
+
devDependencies: { [PIN_DEPENDENCY_NAME]: pinVersion },
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
path: "package.json",
|
|
169
|
+
content: `${JSON.stringify(pkg, null, 2)}\n`,
|
|
170
|
+
encoding: "utf-8",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Greenfield fact-set for the headless manifest. `hasDeftCore: false` routes
|
|
175
|
+
* `plan()` to `init` mode (Row 2), before any engine/pin reconciliation — no
|
|
176
|
+
* git-repo assumption, no reachable-engine requirement.
|
|
177
|
+
*/
|
|
178
|
+
function headlessGreenfieldFacts() {
|
|
179
|
+
return {
|
|
180
|
+
hasGit: false,
|
|
181
|
+
hasAppCode: false,
|
|
182
|
+
hasDeftCore: false,
|
|
183
|
+
deftCorePayloadVersion: null,
|
|
184
|
+
hasManagedSection: false,
|
|
185
|
+
managedSectionSha: null,
|
|
186
|
+
hasVbrief: false,
|
|
187
|
+
hasXbrief: false,
|
|
188
|
+
preCutoverArtifacts: false,
|
|
189
|
+
engineReachable: false,
|
|
190
|
+
engineVersion: null,
|
|
191
|
+
pinVersion: null,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Build the headless manifest. Reads the content package read-only, collects
|
|
196
|
+
* the fresh-install file set (payload + version-consistent AGENTS.md + xBRIEF
|
|
197
|
+
* scaffold + pinned package.json), threads it through `plan()` so the manifest
|
|
198
|
+
* is single-sourced from the resolution schema, and returns the serialisable
|
|
199
|
+
* shape. Performs NO filesystem writes and NO git operations.
|
|
200
|
+
*
|
|
201
|
+
* Throws (never partially writes) on content-resolution failure; the CLI
|
|
202
|
+
* wrapper converts the throw into a JSON error object + non-zero exit.
|
|
203
|
+
*/
|
|
204
|
+
export async function buildHeadlessManifest(seams = {}) {
|
|
205
|
+
const resolveContentRoot = seams.resolveContentRoot ?? resolveInstalledContentRoot;
|
|
206
|
+
const contentRoot = await resolveContentRoot();
|
|
207
|
+
const version = resolveVersion(contentRoot, seams);
|
|
208
|
+
const nowIso = (seams.nowIso ?? defaultNowIso)();
|
|
209
|
+
const sessionId = (seams.newSession ?? (() => "headlessmanif"))();
|
|
210
|
+
const payload = collectPayloadFiles(contentRoot).filter((file) => file.path !== PAYLOAD_VERSION_PATH);
|
|
211
|
+
const files = [
|
|
212
|
+
...payload,
|
|
213
|
+
collectVersionFile(version, nowIso),
|
|
214
|
+
collectAgentsMdFile(contentRoot, version, nowIso, sessionId),
|
|
215
|
+
...collectXbriefScaffoldFiles(),
|
|
216
|
+
collectPackageJsonFile(version),
|
|
217
|
+
].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
218
|
+
// Single-source the manifest from plan(): the collected set rides the
|
|
219
|
+
// ResolutionPlan.files channel, and we read it back out — no reimplementation.
|
|
220
|
+
const resolved = plan(headlessGreenfieldFacts(), {}, { files });
|
|
221
|
+
return { version, files: resolved.files };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* CLI-facing entrypoint for `directive init --headless`. Emits the manifest to
|
|
225
|
+
* `--output` (the ONLY permitted filesystem write) or stdout, and on failure
|
|
226
|
+
* writes a JSON error object to stdout and returns exit code 1.
|
|
227
|
+
*/
|
|
228
|
+
export async function runInitHeadlessCli(options) {
|
|
229
|
+
try {
|
|
230
|
+
const manifest = await buildHeadlessManifest(options.seams);
|
|
231
|
+
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
232
|
+
if (options.outputPath) {
|
|
233
|
+
const abs = resolve(options.outputPath);
|
|
234
|
+
const writeFile = options.writeFile ??
|
|
235
|
+
((path, data) => {
|
|
236
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
237
|
+
writeFileSync(path, data);
|
|
238
|
+
});
|
|
239
|
+
writeFile(abs, serialized);
|
|
240
|
+
options.writeErr(`directive init --headless: wrote ${manifest.files.length}-file manifest (v${manifest.version}) to ${abs}\n`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
options.writeOut(serialized);
|
|
244
|
+
}
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
catch (cause) {
|
|
248
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
249
|
+
const error_code = cause instanceof ContentPackageNotFoundError
|
|
250
|
+
? "content_resolution_failed"
|
|
251
|
+
: "headless_manifest_failed";
|
|
252
|
+
const errorObject = {
|
|
253
|
+
success: false,
|
|
254
|
+
error: message,
|
|
255
|
+
error_code,
|
|
256
|
+
};
|
|
257
|
+
options.writeOut(`${JSON.stringify(errorObject, null, 2)}\n`);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=headless-manifest.js.map
|
|
@@ -11,6 +11,8 @@ import { gitPorcelain } from "../story-ready/git.js";
|
|
|
11
11
|
import { CANONICAL_INSTALL_ROOT } from "./constants.js";
|
|
12
12
|
export const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
|
|
13
13
|
export const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
|
|
14
|
+
// The lifecycle dir names are identical across the legacy `vbrief/` tree and the
|
|
15
|
+
// post-#2034 / #2110 `xbrief/` tree, so both allowlist families reuse this list.
|
|
14
16
|
const VBRIEF_LIFECYCLE_DIRS = ["proposed", "pending", "active", "completed", "cancelled"];
|
|
15
17
|
/** Single source of truth for installer-managed paths (#1440 / #1576). */
|
|
16
18
|
export function installerManagedMatchers() {
|
|
@@ -24,11 +26,20 @@ export function installerManagedMatchers() {
|
|
|
24
26
|
{ exact: CODEQL_CONFIG_REL },
|
|
25
27
|
{ exact: CORE_GUARD_WORKFLOW_REL },
|
|
26
28
|
{ exact: "Taskfile.yml" },
|
|
29
|
+
// Legacy vbrief/ tree -- retained for not-yet-migrated consumers.
|
|
27
30
|
{ exact: "vbrief/.deft-version" },
|
|
28
31
|
{ exact: "vbrief/vbrief.md" },
|
|
29
32
|
{ prefix: "vbrief/schemas/" },
|
|
30
33
|
{ prefix: "vbrief/migration/" },
|
|
31
34
|
...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `vbrief/${sub}/.gitkeep` })),
|
|
35
|
+
// Migrated xbrief/ tree (#2034 / #2110). The framework-managed version marker
|
|
36
|
+
// now lives at xbrief/.deft-version, so it MUST be allowlisted or every routine
|
|
37
|
+
// `deft update` framework-deposit PR trips no-mixed-core-and-app (#2277).
|
|
38
|
+
{ exact: "xbrief/.deft-version" },
|
|
39
|
+
{ exact: "xbrief/xbrief.md" },
|
|
40
|
+
{ prefix: "xbrief/schemas/" },
|
|
41
|
+
{ prefix: "xbrief/migration/" },
|
|
42
|
+
...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `xbrief/${sub}/.gitkeep` })),
|
|
32
43
|
];
|
|
33
44
|
}
|
|
34
45
|
function escapeEre(value) {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
export * from "./headless-manifest.js";
|
|
1
2
|
export * from "./init-deposit.js";
|
|
3
|
+
export * from "./init-dispatch.js";
|
|
2
4
|
export * from "./legacy-detect.js";
|
|
3
5
|
export * from "./migrate.js";
|
|
4
6
|
export * from "./refresh.js";
|
|
5
7
|
export * from "./scaffold.js";
|
|
8
|
+
export * from "./untrack-core.js";
|
|
6
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
export * from "./headless-manifest.js";
|
|
1
2
|
export * from "./init-deposit.js";
|
|
3
|
+
export * from "./init-dispatch.js";
|
|
2
4
|
export * from "./legacy-detect.js";
|
|
3
5
|
export * from "./migrate.js";
|
|
4
6
|
export * from "./refresh.js";
|
|
5
7
|
export * from "./scaffold.js";
|
|
8
|
+
export * from "./untrack-core.js";
|
|
6
9
|
//# sourceMappingURL=index.js.map
|