@deftai/directive-core 0.68.1 → 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/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
|
|
@@ -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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal adoption dispatcher for `directive init` (#2265 / epic #2203).
|
|
3
|
+
*
|
|
4
|
+
* `directive init` is the ONE entrypoint a first-time adopter runs regardless of
|
|
5
|
+
* their directory state. This module classifies the directory via the shared
|
|
6
|
+
* keystone `classify()`/`plan()` fact-set (#2264, consumed read-only) and
|
|
7
|
+
* dispatches to exactly one of four paths, always printing a concise state
|
|
8
|
+
* summary plus exactly one recommended next action:
|
|
9
|
+
*
|
|
10
|
+
* - `scaffold` — empty greenfield dir; deposit Directive.
|
|
11
|
+
* - `brownfield-install` — app code but no Directive deposit; install support
|
|
12
|
+
* beside the app code without disturbing it.
|
|
13
|
+
* - `delegate-update` — already initialized; DELEGATE to `update` with an
|
|
14
|
+
* explicit disclosure line. init never silently
|
|
15
|
+
* refreshes under its own name (the #2199 anti-pattern)
|
|
16
|
+
* and never re-scaffolds an existing install.
|
|
17
|
+
* - `route-migrate` — legacy / pre-cutover layout; route to `migrate`.
|
|
18
|
+
*
|
|
19
|
+
* Logic is single-sourced: each delegate branch CALLS the existing narrow verb
|
|
20
|
+
* (`runInitDepositCli` / `runRefreshDepositCli` / `runMigrateCli`) — the
|
|
21
|
+
* dispatcher never re-implements deposit, refresh, or migrate behavior.
|
|
22
|
+
*/
|
|
23
|
+
import type { ResolutionFacts, ResolutionPlan } from "@deftai/directive-types";
|
|
24
|
+
import { type ClassifySeams } from "../resolution/index.js";
|
|
25
|
+
import { type RunInitDepositCliOptions } from "./init-deposit.js";
|
|
26
|
+
import { type RunMigrateCliOptions } from "./migrate.js";
|
|
27
|
+
import { type RunRefreshDepositCliOptions } from "./refresh.js";
|
|
28
|
+
import type { InitDepositIo } from "./scaffold.js";
|
|
29
|
+
/** The four adoption paths `directive init` dispatches into. */
|
|
30
|
+
export type InitDispatchDecision = "scaffold" | "brownfield-install" | "delegate-update" | "route-migrate";
|
|
31
|
+
/** Human-readable state label per decision, used in the printed summary. */
|
|
32
|
+
export declare const INIT_DISPATCH_STATE_LABEL: Record<InitDispatchDecision, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Explicit disclosure printed BEFORE delegating an already-initialized project
|
|
35
|
+
* to `update`. Its presence is what keeps `init` from silently masquerading as
|
|
36
|
+
* `update` (the #2199 anti-pattern): the user always sees that init delegated.
|
|
37
|
+
*/
|
|
38
|
+
export declare const UPDATE_DELEGATION_DISCLOSURE = "Existing install detected \u2014 refreshing via `update` (init delegates to update; it does not re-scaffold).";
|
|
39
|
+
/**
|
|
40
|
+
* Collapse the shared resolution {@link ResolutionPlan} + fact-set onto one of
|
|
41
|
+
* the four init dispatch decisions. There is no second classifier: `plan.mode`
|
|
42
|
+
* is the single source of truth, and the empty-vs-brownfield split reads the
|
|
43
|
+
* `hasAppCode`/`hasGit` facts already produced by `classify()`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function decideInitDispatch(facts: ResolutionFacts, resolutionPlan: ResolutionPlan): InitDispatchDecision;
|
|
46
|
+
export interface InitDispatchClassification {
|
|
47
|
+
readonly decision: InitDispatchDecision;
|
|
48
|
+
readonly plan: ResolutionPlan;
|
|
49
|
+
readonly facts: ResolutionFacts;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run the up-front dispatch classifier. Reuses the keystone `classify()`
|
|
53
|
+
* fact-set + `plan()` precedence table — no second classifier lives here.
|
|
54
|
+
*/
|
|
55
|
+
export declare function classifyInitDispatch(projectDir: string, classifySeams?: ClassifySeams): InitDispatchClassification;
|
|
56
|
+
/** The single recommended next-action line for a decision. */
|
|
57
|
+
export declare function initDispatchNextAction(decision: InitDispatchDecision, resolutionPlan: ResolutionPlan): string;
|
|
58
|
+
/** Print the concise state summary + exactly one recommended next action. */
|
|
59
|
+
export declare function printInitDispatchSummary(io: InitDepositIo, decision: InitDispatchDecision, resolutionPlan: ResolutionPlan): void;
|
|
60
|
+
/** Machine-readable payload emitted for `--dry-run`/`--plan` (no execution). */
|
|
61
|
+
export declare function buildInitDispatchDryRunJson(projectDir: string, classification: InitDispatchClassification): Record<string, unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* Injectable delegate seams. Defaults are the real narrow-verb CLIs; tests
|
|
64
|
+
* inject fakes to assert single-sourced delegation without touching the fs.
|
|
65
|
+
*/
|
|
66
|
+
export interface InitDispatchSeams {
|
|
67
|
+
readonly classifySeams?: ClassifySeams;
|
|
68
|
+
readonly runScaffold?: (options: RunInitDepositCliOptions) => Promise<number>;
|
|
69
|
+
readonly runRefresh?: (options: RunRefreshDepositCliOptions) => Promise<number>;
|
|
70
|
+
readonly runMigrate?: (options: RunMigrateCliOptions) => number;
|
|
71
|
+
}
|
|
72
|
+
export interface RunInitDispatchCliOptions {
|
|
73
|
+
readonly projectDir: string;
|
|
74
|
+
readonly jsonOut: boolean;
|
|
75
|
+
readonly nonInteractive: boolean;
|
|
76
|
+
/** `--dry-run`/`--plan`: print the classified plan without executing. */
|
|
77
|
+
readonly dryRun: boolean;
|
|
78
|
+
readonly writeOut: (text: string) => void;
|
|
79
|
+
readonly writeErr: (text: string) => void;
|
|
80
|
+
readonly seams?: InitDispatchSeams;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* CLI-facing entrypoint for `directive init`. Classifies, prints a state summary
|
|
84
|
+
* + one next action, then either stops (`--dry-run`) or delegates to the narrow
|
|
85
|
+
* verb for the resolved decision. The summary/disclosure use the `printf`
|
|
86
|
+
* channel (stderr under `--json`) so that in JSON mode stdout stays a SINGLE
|
|
87
|
+
* JSON object — the delegate's result, or the dry-run plan.
|
|
88
|
+
*/
|
|
89
|
+
export declare function runInitDispatchCli(options: RunInitDispatchCliOptions): Promise<number>;
|
|
90
|
+
//# sourceMappingURL=init-dispatch.d.ts.map
|