@deftai/directive-core 0.55.2 → 0.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/errors.d.ts +4 -1
- package/dist/cache/errors.js +8 -2
- package/dist/cache/fetch.d.ts +50 -0
- package/dist/cache/fetch.js +224 -3
- package/dist/cache/main.js +10 -5
- package/dist/cache/operations.js +6 -4
- package/dist/deposit/copy-tree.d.ts +16 -0
- package/dist/deposit/copy-tree.js +58 -0
- package/dist/deposit/resolve-content.d.ts +28 -0
- package/dist/deposit/resolve-content.js +70 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +37 -0
- package/dist/doctor/constants.d.ts +2 -0
- package/dist/doctor/constants.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/gitignore.d.ts +51 -0
- package/dist/init-deposit/gitignore.js +200 -0
- package/dist/init-deposit/index.d.ts +6 -0
- package/dist/init-deposit/index.js +6 -0
- package/dist/init-deposit/init-deposit.d.ts +44 -0
- package/dist/init-deposit/init-deposit.js +197 -0
- package/dist/init-deposit/legacy-detect.d.ts +61 -0
- package/dist/init-deposit/legacy-detect.js +223 -0
- package/dist/init-deposit/migrate.d.ts +88 -0
- package/dist/init-deposit/migrate.js +196 -0
- package/dist/init-deposit/refresh.d.ts +50 -0
- package/dist/init-deposit/refresh.js +292 -0
- package/dist/init-deposit/scaffold.d.ts +41 -0
- package/dist/init-deposit/scaffold.js +753 -0
- package/dist/legacy-bridge/bridge-drift.d.ts +70 -0
- package/dist/legacy-bridge/bridge-drift.js +171 -0
- package/dist/legacy-bridge/freeze-gate.d.ts +79 -0
- package/dist/legacy-bridge/freeze-gate.js +168 -0
- package/dist/legacy-bridge/index.d.ts +4 -0
- package/dist/legacy-bridge/index.js +4 -0
- package/dist/legacy-bridge/sot.d.ts +45 -0
- package/dist/legacy-bridge/sot.js +49 -0
- package/dist/preflight-cache/evaluate.d.ts +11 -0
- package/dist/preflight-cache/evaluate.js +63 -13
- package/dist/release-e2e/flags.js +5 -1
- package/dist/release-e2e/legacy-bridge-leg.d.ts +134 -0
- package/dist/release-e2e/legacy-bridge-leg.js +478 -0
- package/dist/release-e2e/main.d.ts +2 -1
- package/dist/release-e2e/main.js +17 -1
- package/dist/release-e2e/types.d.ts +9 -0
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/project-render.js +4 -3
- package/dist/render/roadmap-render.d.ts +4 -2
- package/dist/render/roadmap-render.js +14 -7
- package/dist/triage/actions/candidates-log.d.ts +12 -1
- package/dist/triage/actions/candidates-log.js +44 -7
- package/dist/triage/actions/index.d.ts +25 -1
- package/dist/triage/actions/index.js +94 -3
- package/dist/triage/summary/index.d.ts +1 -3
- package/dist/triage/summary/index.js +6 -57
- package/dist/triage/summary/reconcilable.js +4 -33
- package/dist/triage/welcome/default-mode.d.ts +1 -0
- package/dist/triage/welcome/default-mode.js +6 -0
- package/dist/vbrief-validate/project-definition.js +4 -69
- package/dist/vbrief-validate/registry-status.d.ts +20 -0
- package/dist/vbrief-validate/registry-status.js +85 -0
- package/package.json +11 -2
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy on-disk layout detection for the npm CLI (#1912 Tier-2).
|
|
3
|
+
*
|
|
4
|
+
* The `npx @deftai/directive` CLI is fetched fresh at use-time. Before it
|
|
5
|
+
* deposits (init) or refreshes (update) the canonical `.deft/core/` vendored
|
|
6
|
+
* layout, it must detect a LEGACY on-disk layout and REFUSE -- the npm path
|
|
7
|
+
* never migrates. Stage 1 (the frozen final Go bridge installer) migrates the
|
|
8
|
+
* old layout; stage 2 (this npm CLI) takes over once the layout is
|
|
9
|
+
* canonical-vendored.
|
|
10
|
+
*
|
|
11
|
+
* Legacy shapes recognized:
|
|
12
|
+
* - orphan-deft-version : `.deft/VERSION` present but `.deft/core/` absent
|
|
13
|
+
* - legacy-deft-prefixed : old `deft/`-prefixed install root
|
|
14
|
+
* - git-clone-or-submodule : framework checked out as a clone / git submodule
|
|
15
|
+
* - pre-v0.27-sentinel-agents: pre-v0.27 AGENTS.md without a v2/v3 managed-section
|
|
16
|
+
*
|
|
17
|
+
* Reuses the manifest/AGENTS heuristics in ../doctor/manifest.ts -- it does NOT
|
|
18
|
+
* reinvent them.
|
|
19
|
+
*
|
|
20
|
+
* Core principle (locked): never bake the upgrade command/version into the
|
|
21
|
+
* artifact being upgraded -- bake in a stable pointer resolved fresh. The
|
|
22
|
+
* refusal therefore signposts a STABLE UPGRADING.md URL, never a baked version.
|
|
23
|
+
*
|
|
24
|
+
* Refs #1912, #1669.
|
|
25
|
+
*/
|
|
26
|
+
import { statSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL } from "../doctor/constants.js";
|
|
29
|
+
import { extractManagedSection, parseInstallRootFromAgentsMd } from "../doctor/manifest.js";
|
|
30
|
+
import { readTextSafe } from "../doctor/paths.js";
|
|
31
|
+
/** Non-zero exit code for a use-time legacy-layout refusal (needs-action). */
|
|
32
|
+
export const LEGACY_LAYOUT_REFUSED_EXIT_CODE = 2;
|
|
33
|
+
const NOT_LEGACY = {
|
|
34
|
+
legacy: false,
|
|
35
|
+
kind: null,
|
|
36
|
+
detail: "Canonical or greenfield layout -- no legacy Deft layout detected.",
|
|
37
|
+
evidence: [],
|
|
38
|
+
};
|
|
39
|
+
function defaultIsFile(p) {
|
|
40
|
+
try {
|
|
41
|
+
return statSync(p).isFile();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function defaultIsDir(p) {
|
|
48
|
+
try {
|
|
49
|
+
return statSync(p).isDirectory();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Does `.gitmodules` reference the deft framework at a deft install path? */
|
|
56
|
+
function gitmodulesReferencesFramework(text) {
|
|
57
|
+
const normalized = text.replace(/\r\n/g, "\n").toLowerCase();
|
|
58
|
+
if (normalized.includes("deftai/directive")) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// A submodule whose path is the framework install dir is a strong signal even
|
|
62
|
+
// when the url host differs (mirror, fork). Match `path = deft` / `path = .deft`.
|
|
63
|
+
return /(^|\n)\s*path\s*=\s*\.?deft(\/|\s|$)/.test(normalized);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Classify the on-disk layout at `projectDir`. Returns `legacy: false` for the
|
|
67
|
+
* canonical `.deft/core/` vendored layout and for a greenfield project (so init
|
|
68
|
+
* proceeds); returns `legacy: true` with the matched `kind` for any legacy shape.
|
|
69
|
+
*/
|
|
70
|
+
export function detectLegacyLayout(projectDir, seams = {}) {
|
|
71
|
+
const isFile = seams.isFile ?? defaultIsFile;
|
|
72
|
+
const isDir = seams.isDir ?? defaultIsDir;
|
|
73
|
+
const readText = seams.readText ?? readTextSafe;
|
|
74
|
+
// Canonical vendored layout present -> the npm path owns it; not legacy. Any
|
|
75
|
+
// AGENTS.md / manifest drift here is repaired by the healthy refresh path,
|
|
76
|
+
// not by refusing.
|
|
77
|
+
if (isDir(join(projectDir, ".deft", "core"))) {
|
|
78
|
+
return NOT_LEGACY;
|
|
79
|
+
}
|
|
80
|
+
// Orphan `.deft/VERSION` (the old pre-`.deft/core/` manifest location) with no
|
|
81
|
+
// `.deft/core/` directory.
|
|
82
|
+
if (isFile(join(projectDir, ".deft", "VERSION"))) {
|
|
83
|
+
return {
|
|
84
|
+
legacy: true,
|
|
85
|
+
kind: "orphan-deft-version",
|
|
86
|
+
detail: "Found an orphan .deft/VERSION manifest with no .deft/core/ directory -- " +
|
|
87
|
+
"this is a pre-.deft/core/ layout the npm CLI does not migrate.",
|
|
88
|
+
evidence: [".deft/VERSION"],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Legacy `deft/`-prefixed install root: a top-level `deft/` directory carrying
|
|
92
|
+
// framework markers. Distinguish a git-clone / submodule deposit (a `.git`
|
|
93
|
+
// inside `deft/`, or a `.gitmodules` pointing at the framework) from a plain
|
|
94
|
+
// legacy-prefixed vendored copy.
|
|
95
|
+
const deftDir = join(projectDir, "deft");
|
|
96
|
+
const deftMarkers = [
|
|
97
|
+
join(deftDir, "VERSION"),
|
|
98
|
+
join(deftDir, "main.md"),
|
|
99
|
+
join(deftDir, "Taskfile.yml"),
|
|
100
|
+
];
|
|
101
|
+
const deftDirIsFramework = isDir(deftDir) && (deftMarkers.some((m) => isFile(m)) || isDir(join(deftDir, "skills")));
|
|
102
|
+
if (deftDirIsFramework) {
|
|
103
|
+
const submoduleGit = isFile(join(deftDir, ".git")) || isDir(join(deftDir, ".git"));
|
|
104
|
+
if (submoduleGit) {
|
|
105
|
+
return {
|
|
106
|
+
legacy: true,
|
|
107
|
+
kind: "git-clone-or-submodule",
|
|
108
|
+
detail: "Found a deft/ framework directory backed by its own .git (clone or " +
|
|
109
|
+
"git submodule) -- the npm CLI does not migrate a clone/submodule deposit.",
|
|
110
|
+
evidence: ["deft/", "deft/.git"],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
legacy: true,
|
|
115
|
+
kind: "legacy-deft-prefixed",
|
|
116
|
+
detail: "Found a legacy deft/-prefixed framework install -- the canonical layout " +
|
|
117
|
+
"is .deft/core/. The npm CLI does not migrate the deft/ -> .deft/core/ move.",
|
|
118
|
+
evidence: ["deft/"],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Git submodule recorded in `.gitmodules` pointing at the framework, even when
|
|
122
|
+
// the submodule directory has not been checked out.
|
|
123
|
+
const gitmodulesPath = join(projectDir, ".gitmodules");
|
|
124
|
+
if (isFile(gitmodulesPath)) {
|
|
125
|
+
const text = readText(gitmodulesPath);
|
|
126
|
+
if (text !== null && gitmodulesReferencesFramework(text)) {
|
|
127
|
+
return {
|
|
128
|
+
legacy: true,
|
|
129
|
+
kind: "git-clone-or-submodule",
|
|
130
|
+
detail: "Found a .gitmodules entry referencing the Deft framework -- a submodule " +
|
|
131
|
+
"deposit the npm CLI does not migrate.",
|
|
132
|
+
evidence: [".gitmodules"],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Pre-v0.27 AGENTS.md: deft-managed but missing a v2/v3 managed-section, or
|
|
137
|
+
// declaring the legacy `deft/` install root.
|
|
138
|
+
const agentsText = readText(join(projectDir, "AGENTS.md"));
|
|
139
|
+
if (agentsText !== null) {
|
|
140
|
+
const installRoot = parseInstallRootFromAgentsMd(agentsText);
|
|
141
|
+
if (installRoot === "deft") {
|
|
142
|
+
return {
|
|
143
|
+
legacy: true,
|
|
144
|
+
kind: "legacy-deft-prefixed",
|
|
145
|
+
detail: "AGENTS.md declares the legacy deft/ install root -- the canonical layout " +
|
|
146
|
+
"is .deft/core/. The npm CLI does not migrate the deft/ -> .deft/core/ move.",
|
|
147
|
+
evidence: ["AGENTS.md (install root: deft)"],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Match the OPEN tag only (`<!-- deft:managed-section ...`). A bare
|
|
151
|
+
// `deft:managed-section` substring also appears in the close tag
|
|
152
|
+
// (`<!-- /deft:managed-section -->`), so a truncated / mid-write AGENTS.md
|
|
153
|
+
// carrying only the close tag (or an open tag with no extractable body)
|
|
154
|
+
// would otherwise be misclassified as a pre-v0.27 legacy layout.
|
|
155
|
+
const hasManagedMarker = agentsText.includes("<!-- deft:managed-section");
|
|
156
|
+
if (hasManagedMarker && extractManagedSection(agentsText) === null) {
|
|
157
|
+
return {
|
|
158
|
+
legacy: true,
|
|
159
|
+
kind: "pre-v0.27-sentinel-agents-md",
|
|
160
|
+
detail: "AGENTS.md carries a pre-v0.27 sentinel-only managed-section (no v2/v3 " +
|
|
161
|
+
"managed-section markers) -- run the Go bridge to migrate before the npm CLI.",
|
|
162
|
+
evidence: ["AGENTS.md (sentinel-only managed-section)"],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return NOT_LEGACY;
|
|
167
|
+
}
|
|
168
|
+
/** Thrown by the deposit/refresh core path when a legacy layout is detected. */
|
|
169
|
+
export class LegacyLayoutRefusedError extends Error {
|
|
170
|
+
detection;
|
|
171
|
+
constructor(detection) {
|
|
172
|
+
super(detection.detail);
|
|
173
|
+
this.name = "LegacyLayoutRefusedError";
|
|
174
|
+
this.detection = detection;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Build the human-facing two-step recovery message. Version-neutral: it
|
|
179
|
+
* signposts the stable UPGRADING.md doc + the frozen-bridge releases page and
|
|
180
|
+
* NEVER bakes a Go-installer version or a literal upgrade command.
|
|
181
|
+
*/
|
|
182
|
+
export function buildLegacyRefusalMessage(command, detection) {
|
|
183
|
+
const rerun = `npx @deftai/directive ${command}`;
|
|
184
|
+
return (`directive ${command}: refusing to ${command === "init" ? "deposit" : "refresh"} -- ` +
|
|
185
|
+
"a LEGACY Deft layout was detected.\n\n" +
|
|
186
|
+
` Detected: ${detection.detail}` +
|
|
187
|
+
(detection.kind ? ` (${detection.kind})` : "") +
|
|
188
|
+
"\n\n" +
|
|
189
|
+
"The npm CLI does not migrate legacy layouts. Recover in two steps:\n\n" +
|
|
190
|
+
" 1. Run the frozen final Go bridge installer to migrate this layout to the\n" +
|
|
191
|
+
" canonical .deft/core/ vendored layout. The binaries + exact command are\n" +
|
|
192
|
+
" documented (version-neutral) at:\n" +
|
|
193
|
+
` ${UPGRADING_DOC_URL}\n` +
|
|
194
|
+
` Frozen Go bridge releases: ${GO_BRIDGE_RELEASES_URL}\n` +
|
|
195
|
+
` 2. Re-run \`${rerun}\` once the layout is canonical-vendored.\n\n` +
|
|
196
|
+
"Why a pointer, not a command: never bake the upgrade command/version into the\n" +
|
|
197
|
+
"artifact being upgraded -- the links above resolve the current bridge fresh.\n");
|
|
198
|
+
}
|
|
199
|
+
/** Build the machine-readable refusal payload for `--json` callers. */
|
|
200
|
+
export function buildLegacyRefusalJson(command, projectDir, detection) {
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
action: "refuse",
|
|
204
|
+
refused: true,
|
|
205
|
+
error_code: "legacy_layout_refused",
|
|
206
|
+
command,
|
|
207
|
+
project_dir: projectDir,
|
|
208
|
+
legacy_layout: true,
|
|
209
|
+
legacy_layout_kind: detection.kind,
|
|
210
|
+
detail: detection.detail,
|
|
211
|
+
evidence: [...detection.evidence],
|
|
212
|
+
upgrading_doc_url: UPGRADING_DOC_URL,
|
|
213
|
+
go_bridge_releases_url: GO_BRIDGE_RELEASES_URL,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/** One-line doctor signpost carrying the stable URL (no baked version/command). */
|
|
217
|
+
export function legacyLayoutSignpostLine(detection) {
|
|
218
|
+
return (`Legacy Deft layout detected (${detection.kind ?? "unknown"}): ${detection.detail} ` +
|
|
219
|
+
"Run the frozen Go bridge installer to migrate to .deft/core/, then use the npm " +
|
|
220
|
+
`CLI (\`npx @deftai/directive update\`). See ${UPGRADING_DOC_URL} ` +
|
|
221
|
+
`(frozen bridge: ${GO_BRIDGE_RELEASES_URL}).`);
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=legacy-detect.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage-2 provenance migration (#1941): stamp a canonical-vendored `.deft/core`
|
|
3
|
+
* deposit as npm-managed so the npm channel + `directive update` own the payload.
|
|
4
|
+
*
|
|
5
|
+
* This is a THIN provenance verb (locked decision F): the on-disk shape is
|
|
6
|
+
* UNCHANGED. It never normalizes legacy layouts (stage-1 frozen Go guarantees
|
|
7
|
+
* canonical-vendored input), never moves/renames/trims `.deft/core` content, and
|
|
8
|
+
* NEVER downloads a payload (#1933 never-first-start decree).
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* 1. Detect a canonical-vendored `.deft/core/VERSION` deposit (reuse the
|
|
12
|
+
* manifest locator in doctor/manifest.ts).
|
|
13
|
+
* 2. Idempotency: if the manifest already carries the npm-managed handshake
|
|
14
|
+
* sentinel (`managed_by: 'npm'`), make zero changes (already-hybrid).
|
|
15
|
+
* 3. Engine-resolve check: verify the global engine resolves via
|
|
16
|
+
* content-root.ts `resolveContentPackageRoot`. If absent, signpost the
|
|
17
|
+
* README and return needs-action -- NEVER install/download.
|
|
18
|
+
* 4. Stamp the sentinel into the manifest, writing a timestamped backup first.
|
|
19
|
+
*
|
|
20
|
+
* Three-state result:
|
|
21
|
+
* - exitCode 0: migrated OR already-hybrid
|
|
22
|
+
* - exitCode 1: needs action (engine missing -> README signpost)
|
|
23
|
+
* - exitCode 2: config error (no canonical-vendored deposit / unreadable manifest)
|
|
24
|
+
*
|
|
25
|
+
* Refs #1941, #1912 (freeze prerequisite b), #1933 (never-first-start), #1670.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The npm-managed handshake sentinel. `directive migrate` adds this key/value to
|
|
29
|
+
* the install manifest (`.deft/core/VERSION`) to mark the deposit as owned by the
|
|
30
|
+
* npm content channel. `doctor` (parseInstallManifest reads every colon
|
|
31
|
+
* key/value line) and future `directive update` recognize it; its presence is
|
|
32
|
+
* also the idempotency signal that the deposit is already hybrid.
|
|
33
|
+
*/
|
|
34
|
+
export declare const NPM_MANAGED_SENTINEL_KEY = "managed_by";
|
|
35
|
+
export declare const NPM_MANAGED_SENTINEL_VALUE = "npm";
|
|
36
|
+
/** The npm package the README tells operators to install for the engine. */
|
|
37
|
+
export declare const ENGINE_PACKAGE_NAME = "@deftai/directive";
|
|
38
|
+
export type MigrateOutcome = "migrated" | "already-hybrid" | "engine-missing" | "no-deposit" | "manifest-unreadable";
|
|
39
|
+
export interface MigrateResult {
|
|
40
|
+
readonly outcome: MigrateOutcome;
|
|
41
|
+
readonly exitCode: 0 | 1 | 2;
|
|
42
|
+
readonly manifestPath: string | null;
|
|
43
|
+
readonly backupPath: string | null;
|
|
44
|
+
readonly sentinelKey: string;
|
|
45
|
+
readonly message: string;
|
|
46
|
+
}
|
|
47
|
+
export interface MigrateSeams {
|
|
48
|
+
/** Existence probe (default: node:fs existsSync). */
|
|
49
|
+
isFile?: (path: string) => boolean;
|
|
50
|
+
/** Text read returning null on failure (default: node:fs readFileSync, utf8). */
|
|
51
|
+
readText?: (path: string) => string | null;
|
|
52
|
+
/** Text write (default: node:fs writeFileSync, utf8). */
|
|
53
|
+
writeText?: (path: string, text: string) => void;
|
|
54
|
+
/** ISO-8601 UTC timestamp source for the backup filename (default: Date.now). */
|
|
55
|
+
nowIso?: () => string;
|
|
56
|
+
/** Engine-resolve check; returns the resolved content package root or null. */
|
|
57
|
+
resolveEngine?: () => string | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Locate the canonical-vendored install manifest (`.deft/core/VERSION`).
|
|
61
|
+
* Returns the path only when the canonical install-root manifest is present;
|
|
62
|
+
* legacy `.deft/VERSION` / `deft/VERSION` layouts are intentionally NOT
|
|
63
|
+
* normalized here (stage-1 guarantees canonical-vendored input).
|
|
64
|
+
*/
|
|
65
|
+
export declare function detectCanonicalVendoredManifest(projectRoot: string, isFile?: (path: string) => boolean): string | null;
|
|
66
|
+
/** True when the manifest already carries the npm-managed handshake sentinel. */
|
|
67
|
+
export declare function isNpmManaged(manifest: Record<string, string>): boolean;
|
|
68
|
+
/** Append the npm-managed sentinel line to existing manifest text. */
|
|
69
|
+
export declare function stampManifestText(text: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Orchestrate the stage-2 provenance migration for `projectRoot`. Pure of
|
|
72
|
+
* process exit; the caller maps {@link MigrateResult.exitCode} to a process code.
|
|
73
|
+
*/
|
|
74
|
+
export declare function runMigrate(projectRoot: string, seams?: MigrateSeams): MigrateResult;
|
|
75
|
+
export interface RunMigrateCliOptions {
|
|
76
|
+
readonly projectDir: string;
|
|
77
|
+
readonly jsonOut: boolean;
|
|
78
|
+
readonly writeOut: (text: string) => void;
|
|
79
|
+
readonly writeErr: (text: string) => void;
|
|
80
|
+
readonly seams?: MigrateSeams;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* CLI-facing wrapper: runs the migration, emits JSON or human output, maps the
|
|
84
|
+
* outcome to a 0/1/2 exit code. Engine-missing prints the README signpost to
|
|
85
|
+
* stderr; config errors print to stderr; success prints to stdout.
|
|
86
|
+
*/
|
|
87
|
+
export declare function runMigrateCli(options: RunMigrateCliOptions): number;
|
|
88
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage-2 provenance migration (#1941): stamp a canonical-vendored `.deft/core`
|
|
3
|
+
* deposit as npm-managed so the npm channel + `directive update` own the payload.
|
|
4
|
+
*
|
|
5
|
+
* This is a THIN provenance verb (locked decision F): the on-disk shape is
|
|
6
|
+
* UNCHANGED. It never normalizes legacy layouts (stage-1 frozen Go guarantees
|
|
7
|
+
* canonical-vendored input), never moves/renames/trims `.deft/core` content, and
|
|
8
|
+
* NEVER downloads a payload (#1933 never-first-start decree).
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* 1. Detect a canonical-vendored `.deft/core/VERSION` deposit (reuse the
|
|
12
|
+
* manifest locator in doctor/manifest.ts).
|
|
13
|
+
* 2. Idempotency: if the manifest already carries the npm-managed handshake
|
|
14
|
+
* sentinel (`managed_by: 'npm'`), make zero changes (already-hybrid).
|
|
15
|
+
* 3. Engine-resolve check: verify the global engine resolves via
|
|
16
|
+
* content-root.ts `resolveContentPackageRoot`. If absent, signpost the
|
|
17
|
+
* README and return needs-action -- NEVER install/download.
|
|
18
|
+
* 4. Stamp the sentinel into the manifest, writing a timestamped backup first.
|
|
19
|
+
*
|
|
20
|
+
* Three-state result:
|
|
21
|
+
* - exitCode 0: migrated OR already-hybrid
|
|
22
|
+
* - exitCode 1: needs action (engine missing -> README signpost)
|
|
23
|
+
* - exitCode 2: config error (no canonical-vendored deposit / unreadable manifest)
|
|
24
|
+
*
|
|
25
|
+
* Refs #1941, #1912 (freeze prerequisite b), #1933 (never-first-start), #1670.
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { resolveContentPackageRoot } from "../content-root.js";
|
|
31
|
+
import { locateManifest, parseInstallManifest } from "../doctor/manifest.js";
|
|
32
|
+
import { CANONICAL_INSTALL_ROOT } from "./scaffold.js";
|
|
33
|
+
/**
|
|
34
|
+
* The npm-managed handshake sentinel. `directive migrate` adds this key/value to
|
|
35
|
+
* the install manifest (`.deft/core/VERSION`) to mark the deposit as owned by the
|
|
36
|
+
* npm content channel. `doctor` (parseInstallManifest reads every colon
|
|
37
|
+
* key/value line) and future `directive update` recognize it; its presence is
|
|
38
|
+
* also the idempotency signal that the deposit is already hybrid.
|
|
39
|
+
*/
|
|
40
|
+
export const NPM_MANAGED_SENTINEL_KEY = "managed_by";
|
|
41
|
+
export const NPM_MANAGED_SENTINEL_VALUE = "npm";
|
|
42
|
+
/** The npm package the README tells operators to install for the engine. */
|
|
43
|
+
export const ENGINE_PACKAGE_NAME = "@deftai/directive";
|
|
44
|
+
function defaultReadText(path) {
|
|
45
|
+
try {
|
|
46
|
+
return readFileSync(path, "utf8");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function migrateModuleDir() {
|
|
53
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Default engine-resolve check: walk up from this module's own install location
|
|
57
|
+
* (a globally-installed engine has the `@deftai/directive-content` package as a
|
|
58
|
+
* sibling in the same node_modules) and return the resolved content package
|
|
59
|
+
* root, or null when the engine does not resolve.
|
|
60
|
+
*/
|
|
61
|
+
function defaultResolveEngine() {
|
|
62
|
+
return resolveContentPackageRoot(migrateModuleDir());
|
|
63
|
+
}
|
|
64
|
+
/** Filesystem-safe rendering of an ISO-8601 timestamp for a backup suffix. */
|
|
65
|
+
function backupSuffix(nowIso) {
|
|
66
|
+
return nowIso.replace(/:/g, "-");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Locate the canonical-vendored install manifest (`.deft/core/VERSION`).
|
|
70
|
+
* Returns the path only when the canonical install-root manifest is present;
|
|
71
|
+
* legacy `.deft/VERSION` / `deft/VERSION` layouts are intentionally NOT
|
|
72
|
+
* normalized here (stage-1 guarantees canonical-vendored input).
|
|
73
|
+
*/
|
|
74
|
+
export function detectCanonicalVendoredManifest(projectRoot, isFile = existsSync) {
|
|
75
|
+
const canonical = join(projectRoot, CANONICAL_INSTALL_ROOT, "VERSION");
|
|
76
|
+
const located = locateManifest(projectRoot, CANONICAL_INSTALL_ROOT, isFile);
|
|
77
|
+
return located === canonical ? located : null;
|
|
78
|
+
}
|
|
79
|
+
/** True when the manifest already carries the npm-managed handshake sentinel. */
|
|
80
|
+
export function isNpmManaged(manifest) {
|
|
81
|
+
return manifest[NPM_MANAGED_SENTINEL_KEY] === NPM_MANAGED_SENTINEL_VALUE;
|
|
82
|
+
}
|
|
83
|
+
/** Append the npm-managed sentinel line to existing manifest text. */
|
|
84
|
+
export function stampManifestText(text) {
|
|
85
|
+
const base = text.endsWith("\n") || text.length === 0 ? text : `${text}\n`;
|
|
86
|
+
return `${base}${NPM_MANAGED_SENTINEL_KEY}: '${NPM_MANAGED_SENTINEL_VALUE}'\n`;
|
|
87
|
+
}
|
|
88
|
+
function engineMissingMessage() {
|
|
89
|
+
return (`directive migrate: the global Deft engine (${ENGINE_PACKAGE_NAME}) does not resolve.\n` +
|
|
90
|
+
"Install it first, then re-run migrate:\n\n" +
|
|
91
|
+
` npm i -g ${ENGINE_PACKAGE_NAME}\n\n` +
|
|
92
|
+
"See the README (Install / Upgrade) for details. migrate never installs or " +
|
|
93
|
+
"downloads payload on your behalf (#1933).");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Orchestrate the stage-2 provenance migration for `projectRoot`. Pure of
|
|
97
|
+
* process exit; the caller maps {@link MigrateResult.exitCode} to a process code.
|
|
98
|
+
*/
|
|
99
|
+
export function runMigrate(projectRoot, seams = {}) {
|
|
100
|
+
const isFile = seams.isFile ?? existsSync;
|
|
101
|
+
const readText = seams.readText ?? defaultReadText;
|
|
102
|
+
const writeText = seams.writeText ?? ((path, text) => writeFileSync(path, text, "utf8"));
|
|
103
|
+
const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
|
|
104
|
+
const resolveEngine = seams.resolveEngine ?? defaultResolveEngine;
|
|
105
|
+
const manifestPath = detectCanonicalVendoredManifest(projectRoot, isFile);
|
|
106
|
+
if (manifestPath === null) {
|
|
107
|
+
return {
|
|
108
|
+
outcome: "no-deposit",
|
|
109
|
+
exitCode: 2,
|
|
110
|
+
manifestPath: null,
|
|
111
|
+
backupPath: null,
|
|
112
|
+
sentinelKey: NPM_MANAGED_SENTINEL_KEY,
|
|
113
|
+
message: `directive migrate: no canonical-vendored ${CANONICAL_INSTALL_ROOT}/VERSION deposit found under ${projectRoot}. Nothing to migrate.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const text = readText(manifestPath);
|
|
117
|
+
// `parseInstallManifest` always returns a record (never null), so the only
|
|
118
|
+
// unreadable/empty cases are a failed read or a manifest with no key/value
|
|
119
|
+
// lines; both map to the same config error.
|
|
120
|
+
const manifest = text === null ? {} : parseInstallManifest(text);
|
|
121
|
+
if (text === null || Object.keys(manifest).length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
outcome: "manifest-unreadable",
|
|
124
|
+
exitCode: 2,
|
|
125
|
+
manifestPath,
|
|
126
|
+
backupPath: null,
|
|
127
|
+
sentinelKey: NPM_MANAGED_SENTINEL_KEY,
|
|
128
|
+
message: `directive migrate: install manifest at ${manifestPath} is empty or unreadable.`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (isNpmManaged(manifest)) {
|
|
132
|
+
return {
|
|
133
|
+
outcome: "already-hybrid",
|
|
134
|
+
exitCode: 0,
|
|
135
|
+
manifestPath,
|
|
136
|
+
backupPath: null,
|
|
137
|
+
sentinelKey: NPM_MANAGED_SENTINEL_KEY,
|
|
138
|
+
message: `directive migrate: ${manifestPath} is already npm-managed (already hybrid) -- no change.`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (resolveEngine() === null) {
|
|
142
|
+
return {
|
|
143
|
+
outcome: "engine-missing",
|
|
144
|
+
exitCode: 1,
|
|
145
|
+
manifestPath,
|
|
146
|
+
backupPath: null,
|
|
147
|
+
sentinelKey: NPM_MANAGED_SENTINEL_KEY,
|
|
148
|
+
message: engineMissingMessage(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const backupPath = `${manifestPath}.bak.${backupSuffix(nowIso())}`;
|
|
152
|
+
writeText(backupPath, text);
|
|
153
|
+
writeText(manifestPath, stampManifestText(text));
|
|
154
|
+
return {
|
|
155
|
+
outcome: "migrated",
|
|
156
|
+
exitCode: 0,
|
|
157
|
+
manifestPath,
|
|
158
|
+
backupPath,
|
|
159
|
+
sentinelKey: NPM_MANAGED_SENTINEL_KEY,
|
|
160
|
+
message: `directive migrate: stamped ${manifestPath} npm-managed (${NPM_MANAGED_SENTINEL_KEY}: '${NPM_MANAGED_SENTINEL_VALUE}'); backup written to ${backupPath}.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function buildMigrateSummaryJson(result, projectDir) {
|
|
164
|
+
return {
|
|
165
|
+
success: result.exitCode === 0,
|
|
166
|
+
action: "migrate",
|
|
167
|
+
outcome: result.outcome,
|
|
168
|
+
exit_code: result.exitCode,
|
|
169
|
+
project_dir: projectDir,
|
|
170
|
+
manifest_path: result.manifestPath,
|
|
171
|
+
backup_path: result.backupPath,
|
|
172
|
+
sentinel_key: result.sentinelKey,
|
|
173
|
+
sentinel_value: NPM_MANAGED_SENTINEL_VALUE,
|
|
174
|
+
message: result.message,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* CLI-facing wrapper: runs the migration, emits JSON or human output, maps the
|
|
179
|
+
* outcome to a 0/1/2 exit code. Engine-missing prints the README signpost to
|
|
180
|
+
* stderr; config errors print to stderr; success prints to stdout.
|
|
181
|
+
*/
|
|
182
|
+
export function runMigrateCli(options) {
|
|
183
|
+
const result = runMigrate(options.projectDir, options.seams);
|
|
184
|
+
if (options.jsonOut) {
|
|
185
|
+
options.writeOut(`${JSON.stringify(buildMigrateSummaryJson(result, options.projectDir), null, 2)}\n`);
|
|
186
|
+
return result.exitCode;
|
|
187
|
+
}
|
|
188
|
+
if (result.exitCode === 0) {
|
|
189
|
+
options.writeOut(`${result.message}\n`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
options.writeErr(`${result.message}\n`);
|
|
193
|
+
}
|
|
194
|
+
return result.exitCode;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TS-native healthy-path refresh for `directive update` (#1942 S3).
|
|
3
|
+
*
|
|
4
|
+
* Re-copies the pinned @deftai/directive-content into `.deft/core`, surgically
|
|
5
|
+
* re-renders the AGENTS.md managed-section, runs #1430 neutralization, and
|
|
6
|
+
* discloses refresh side-effects + engine/content version skew.
|
|
7
|
+
*
|
|
8
|
+
* Refs #1942, #1430, #1671.
|
|
9
|
+
*/
|
|
10
|
+
import { type InitDepositArgs } from "./init-deposit.js";
|
|
11
|
+
import { type LegacyLayoutDetection } from "./legacy-detect.js";
|
|
12
|
+
import { type InitDepositIo } from "./scaffold.js";
|
|
13
|
+
export interface RefreshDepositArgs extends InitDepositArgs {
|
|
14
|
+
readonly upgrade: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface RefreshDepositResult {
|
|
17
|
+
readonly projectDir: string;
|
|
18
|
+
readonly deftDir: string;
|
|
19
|
+
readonly contentVersion: string;
|
|
20
|
+
readonly engineVersion: string;
|
|
21
|
+
readonly previousDepositVersion: string | null;
|
|
22
|
+
readonly agentsMdUpdated: boolean;
|
|
23
|
+
readonly versionSkewNotice: string | null;
|
|
24
|
+
readonly legacyLayout: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface RefreshDepositSeams {
|
|
27
|
+
resolveContentRoot?: () => Promise<string>;
|
|
28
|
+
copyContent?: (src: string, dst: string) => Promise<void>;
|
|
29
|
+
readPackageVersion?: () => string;
|
|
30
|
+
readEngineVersion?: () => string;
|
|
31
|
+
nowIso?: () => string;
|
|
32
|
+
gitPorcelain?: (projectRoot: string) => string | null;
|
|
33
|
+
detectLegacy?: (projectDir: string) => LegacyLayoutDetection;
|
|
34
|
+
}
|
|
35
|
+
/** Framework-managed uncommitted paths after refresh (#1671). */
|
|
36
|
+
export declare function frameworkRefreshSideEffects(projectDir: string, readPorcelain?: (root: string) => string | null): string[];
|
|
37
|
+
export declare function printRefreshSideEffects(io: InitDepositIo, files: readonly string[]): void;
|
|
38
|
+
export declare function buildVersionSkewNotice(engineVersion: string, contentVersion: string, previousDepositVersion: string | null): string | null;
|
|
39
|
+
export declare function buildUpdateSummaryJson(result: RefreshDepositResult, options: RefreshDepositArgs): Record<string, unknown>;
|
|
40
|
+
export declare function printUpdateComplete(result: RefreshDepositResult, io: InitDepositIo): void;
|
|
41
|
+
export declare function runRefreshDeposit(args: RefreshDepositArgs, io: InitDepositIo, seams?: RefreshDepositSeams): Promise<RefreshDepositResult>;
|
|
42
|
+
export interface RunRefreshDepositCliOptions extends RefreshDepositArgs {
|
|
43
|
+
readonly writeOut: (text: string) => void;
|
|
44
|
+
readonly writeErr: (text: string) => void;
|
|
45
|
+
readonly seams?: RefreshDepositSeams;
|
|
46
|
+
}
|
|
47
|
+
/** CLI-facing wrapper: runs refresh, emits JSON or wizard UX, returns exit code. */
|
|
48
|
+
export declare function runRefreshDepositCli(options: RunRefreshDepositCliOptions): Promise<number>;
|
|
49
|
+
export declare function parseUpdateArgv(canonicalArgv: readonly string[], userArgv?: readonly string[]): RefreshDepositArgs;
|
|
50
|
+
//# sourceMappingURL=refresh.d.ts.map
|