dxcomplete 0.2.1 → 0.3.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/.env.example +0 -7
- package/README.md +68 -103
- package/dist/cli.js +2 -24
- package/dist/validate.js +10 -26
- package/docs/cost-model.md +2 -2
- package/docs/decision-basis.md +5 -11
- package/docs/diagrams.md +3 -3
- package/docs/index.md +25 -39
- package/docs/model.md +15 -23
- package/docs/open-questions.md +1 -1
- package/docs/taxonomy.md +7 -8
- package/docs/workflows.md +3 -3
- package/package.json +24 -24
- package/templates/process/README.md +11 -11
- package/templates/process/controls.yml +19 -19
- package/templates/process/cost-model.yml +3 -3
- package/templates/process/decision-basis.yml +4 -4
- package/templates/process/diagrams/00-decision-basis.mmd +1 -1
- package/templates/process/diagrams/00-overview.mmd +1 -1
- package/templates/process/diagrams/01-intake-triage.mmd +4 -4
- package/templates/process/diagrams/02-product-definition.mmd +3 -3
- package/templates/process/diagrams/03-engineering-execution.mmd +1 -1
- package/templates/process/diagrams/04-qa-verification.mmd +1 -1
- package/templates/process/diagrams/05-product-validation.mmd +1 -1
- package/templates/process/diagrams/06-change-release-control.mmd +1 -1
- package/templates/process/diagrams/07-deployment-operations.mmd +1 -1
- package/templates/process/diagrams/08-support-incident-management.mmd +1 -1
- package/templates/process/diagrams/09-problem-improvement.mmd +1 -1
- package/templates/process/diagrams/10-risk-control-management.mmd +1 -1
- package/templates/process/diagrams/11-audit-evidence-capture.mmd +1 -1
- package/templates/process/roles.yml +6 -6
- package/templates/process/taxonomy.yml +46 -46
- package/templates/process/workflows.yml +29 -29
- package/website/account.html +57 -0
- package/website/app.js +177 -0
- package/website/flow.html +4 -0
- package/website/glossary.html +4 -0
- package/website/index.html +4 -0
- package/website/objects.html +4 -0
- package/website/operating-guide.html +4 -0
- package/website/outcomes.html +4 -0
- package/website/phase-build.html +4 -0
- package/website/phase-elicit.html +4 -0
- package/website/phase-go-live.html +4 -0
- package/website/phase-measure.html +4 -0
- package/website/phase-operate.html +4 -0
- package/website/phase-orient.html +4 -0
- package/website/phase-weigh.html +4 -0
- package/website/roles.html +4 -0
- package/website/styles.css +217 -1
- package/dist/http/service.d.ts +0 -7
- package/dist/http/service.js +0 -725
- package/dist/mcp/docs.d.ts +0 -114
- package/dist/mcp/docs.js +0 -626
- package/dist/mcp/server.d.ts +0 -20
- package/dist/mcp/server.js +0 -3059
- package/dist/runtime/auth.d.ts +0 -162
- package/dist/runtime/auth.js +0 -394
- package/dist/runtime/check.d.ts +0 -7
- package/dist/runtime/check.js +0 -16
- package/dist/runtime/config.d.ts +0 -17
- package/dist/runtime/config.js +0 -93
- package/dist/runtime/mongo.d.ts +0 -9
- package/dist/runtime/mongo.js +0 -56
- package/dist/runtime/records.d.ts +0 -427
- package/dist/runtime/records.js +0 -2092
- package/scripts/check-env-surface.mjs +0 -136
- package/scripts/check-public-copy.mjs +0 -263
- package/scripts/check-service-boundary.mjs +0 -63
- package/scripts/runtime-work-order.mjs +0 -506
- package/scripts/smoke-mcp-http.mjs +0 -4026
- package/src/cli.ts +0 -268
- package/src/http/server.ts +0 -314
- package/src/http/service.ts +0 -934
- package/src/init.ts +0 -262
- package/src/install-manifest.ts +0 -144
- package/src/mcp/docs.ts +0 -777
- package/src/mcp/server.ts +0 -4580
- package/src/package-root.ts +0 -31
- package/src/runtime/actor.ts +0 -61
- package/src/runtime/auth.ts +0 -673
- package/src/runtime/check.ts +0 -18
- package/src/runtime/config.ts +0 -128
- package/src/runtime/mongo.ts +0 -89
- package/src/runtime/records.ts +0 -3205
- package/src/runtime/workspace.ts +0 -155
- package/src/upgrade.ts +0 -356
- package/src/validate.ts +0 -141
- package/src/version.ts +0 -16
package/src/runtime/workspace.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { constants as fsConstants } from "node:fs";
|
|
2
|
-
import { access, readFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { normalizeEmail } from "./actor.js";
|
|
5
|
-
|
|
6
|
-
export type WorkspaceBootstrapMember = {
|
|
7
|
-
email: string;
|
|
8
|
-
roles: WorkspaceBootstrapRole[];
|
|
9
|
-
role?: "owner" | "member";
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type WorkspaceConfig = {
|
|
13
|
-
workspaceId: string;
|
|
14
|
-
name: string;
|
|
15
|
-
mode?: WorkspaceMode;
|
|
16
|
-
bootstrapMembers: WorkspaceBootstrapMember[];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type WorkspaceMode = "transformation" | "greenfield" | "limited-disclosure";
|
|
20
|
-
export type WorkspaceBootstrapRole = "owner" | "engineer" | "tester" | "operator" | "support_agent" | "end_user";
|
|
21
|
-
|
|
22
|
-
export type WorkspaceConfigOptions = {
|
|
23
|
-
cwd?: string;
|
|
24
|
-
configPath?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const DEFAULT_WORKSPACE_CONFIG = path.join("dxcomplete", "workspace.json");
|
|
28
|
-
|
|
29
|
-
export async function loadWorkspaceConfig(options: WorkspaceConfigOptions = {}): Promise<WorkspaceConfig> {
|
|
30
|
-
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
31
|
-
const configPath = path.resolve(cwd, options.configPath ?? DEFAULT_WORKSPACE_CONFIG);
|
|
32
|
-
|
|
33
|
-
if (!(await fileExists(configPath))) {
|
|
34
|
-
throw new Error(`DX Complete workspace config not found: ${configPath}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const parsed = JSON.parse(await readFile(configPath, "utf8")) as unknown;
|
|
38
|
-
return parseWorkspaceConfig(parsed, configPath);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function parseWorkspaceConfig(value: unknown, source = "workspace config"): WorkspaceConfig {
|
|
42
|
-
if (!value || typeof value !== "object") {
|
|
43
|
-
throw new Error(`${source} must be a JSON object.`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const input = value as Record<string, unknown>;
|
|
47
|
-
const workspaceId = readRequiredString(input.workspaceId, "workspaceId", source);
|
|
48
|
-
const name = readRequiredString(input.name, "name", source);
|
|
49
|
-
const mode = parseWorkspaceMode(input.mode, source);
|
|
50
|
-
const bootstrapMembers = parseBootstrapMembers(input.bootstrapMembers, source);
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
workspaceId,
|
|
54
|
-
name,
|
|
55
|
-
...(mode ? { mode } : {}),
|
|
56
|
-
bootstrapMembers
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function parseWorkspaceMode(value: unknown, source: string): WorkspaceMode | undefined {
|
|
61
|
-
if (value === undefined) {
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (value === "transformation" || value === "greenfield" || value === "limited-disclosure") {
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
throw new Error(`${source} mode must be transformation, greenfield, or limited-disclosure when provided.`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function parseBootstrapMembers(value: unknown, source: string): WorkspaceBootstrapMember[] {
|
|
73
|
-
if (value === undefined) {
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!Array.isArray(value)) {
|
|
78
|
-
throw new Error(`${source} bootstrapMembers must be an array when provided.`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return value.map((entry, index) => {
|
|
82
|
-
if (!entry || typeof entry !== "object") {
|
|
83
|
-
throw new Error(`${source} bootstrapMembers[${index}] must be an object.`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const record = entry as Record<string, unknown>;
|
|
87
|
-
const email = normalizeEmail(readRequiredString(record.email, `bootstrapMembers[${index}].email`, source));
|
|
88
|
-
const roles = parseBootstrapMemberRoles(record, `bootstrapMembers[${index}]`, source);
|
|
89
|
-
|
|
90
|
-
return { email, roles };
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function parseBootstrapMemberRoles(
|
|
95
|
-
record: Record<string, unknown>,
|
|
96
|
-
keyPrefix: string,
|
|
97
|
-
source: string
|
|
98
|
-
): WorkspaceBootstrapRole[] {
|
|
99
|
-
if (Array.isArray(record.roles)) {
|
|
100
|
-
const roles = record.roles.map((role, roleIndex) =>
|
|
101
|
-
parseBootstrapRole(role, `${keyPrefix}.roles[${roleIndex}]`, source)
|
|
102
|
-
);
|
|
103
|
-
return uniqueRoles(roles);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (record.role === "owner") {
|
|
107
|
-
return ["owner"];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (record.role === "member") {
|
|
111
|
-
return ["end_user"];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
throw new Error(`${source} ${keyPrefix}.roles must contain at least one valid workspace role.`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function parseBootstrapRole(value: unknown, key: string, source: string): WorkspaceBootstrapRole {
|
|
118
|
-
if (
|
|
119
|
-
value === "owner" ||
|
|
120
|
-
value === "engineer" ||
|
|
121
|
-
value === "tester" ||
|
|
122
|
-
value === "operator" ||
|
|
123
|
-
value === "support_agent" ||
|
|
124
|
-
value === "end_user"
|
|
125
|
-
) {
|
|
126
|
-
return value;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
throw new Error(`${source} ${key} must be owner, engineer, tester, operator, support_agent, or end_user.`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function uniqueRoles(roles: WorkspaceBootstrapRole[]): WorkspaceBootstrapRole[] {
|
|
133
|
-
const unique = [...new Set(roles)];
|
|
134
|
-
if (unique.length === 0) {
|
|
135
|
-
throw new Error("workspace config bootstrap member roles must contain at least one role.");
|
|
136
|
-
}
|
|
137
|
-
return unique.sort();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function readRequiredString(value: unknown, key: string, source: string): string {
|
|
141
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
142
|
-
throw new Error(`${source} ${key} must be a non-empty string.`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return value.trim();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function fileExists(filePath: string): Promise<boolean> {
|
|
149
|
-
try {
|
|
150
|
-
await access(filePath, fsConstants.F_OK);
|
|
151
|
-
return true;
|
|
152
|
-
} catch {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
}
|
package/src/upgrade.ts
DELETED
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
INSTALL_MANIFEST_SCHEMA_VERSION,
|
|
5
|
-
INSTALL_MANIFEST_PATH,
|
|
6
|
-
type InstallManifest,
|
|
7
|
-
type UpgradeManagedFile,
|
|
8
|
-
hashContent,
|
|
9
|
-
hashFile,
|
|
10
|
-
readInstallManifest,
|
|
11
|
-
writeInstallManifest
|
|
12
|
-
} from "./install-manifest.js";
|
|
13
|
-
import { fileExists, resolvePackageRoot } from "./package-root.js";
|
|
14
|
-
import {
|
|
15
|
-
DXCOMPLETE_PACKAGE_VERSION,
|
|
16
|
-
WORKSPACE_COMPATIBILITY_VERSION
|
|
17
|
-
} from "./version.js";
|
|
18
|
-
|
|
19
|
-
export type UpgradeOptions = {
|
|
20
|
-
targetDir: string;
|
|
21
|
-
apply?: boolean;
|
|
22
|
-
force?: boolean;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type UpgradeResult = {
|
|
26
|
-
targetDir: string;
|
|
27
|
-
applied: boolean;
|
|
28
|
-
packageVersion: string;
|
|
29
|
-
workspaceCompatibility: number;
|
|
30
|
-
written: string[];
|
|
31
|
-
planned: string[];
|
|
32
|
-
unchanged: string[];
|
|
33
|
-
manualReview: string[];
|
|
34
|
-
userOwnedDrift: string[];
|
|
35
|
-
manifestWritten: boolean;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const WELL_KNOWN_REWRITES = [
|
|
39
|
-
{
|
|
40
|
-
source: "/.well-known/oauth-authorization-server",
|
|
41
|
-
destination: "/api/dxcomplete/.well-known/oauth-authorization-server"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
source: "/.well-known/oauth-protected-resource/api/mcp",
|
|
45
|
-
destination: "/api/dxcomplete/.well-known/oauth-protected-resource/api/mcp"
|
|
46
|
-
}
|
|
47
|
-
];
|
|
48
|
-
const WORKSPACE_CONFIG_PATH = path.join("dxcomplete", "workspace.json");
|
|
49
|
-
|
|
50
|
-
export async function upgradeProject(options: UpgradeOptions): Promise<UpgradeResult> {
|
|
51
|
-
const targetDir = path.resolve(options.targetDir);
|
|
52
|
-
const apply = options.apply ?? false;
|
|
53
|
-
const force = options.force ?? false;
|
|
54
|
-
await assertExistingDxcompleteInstall(targetDir);
|
|
55
|
-
const packageRoot = await resolvePackageRoot();
|
|
56
|
-
const managedFiles = getUpgradeManagedFiles(packageRoot, targetDir);
|
|
57
|
-
const manifest = await readInstallManifest(targetDir);
|
|
58
|
-
const result: UpgradeResult = {
|
|
59
|
-
targetDir,
|
|
60
|
-
applied: apply,
|
|
61
|
-
packageVersion: DXCOMPLETE_PACKAGE_VERSION,
|
|
62
|
-
workspaceCompatibility: WORKSPACE_COMPATIBILITY_VERSION,
|
|
63
|
-
written: [],
|
|
64
|
-
planned: [],
|
|
65
|
-
unchanged: [],
|
|
66
|
-
manualReview: [],
|
|
67
|
-
userOwnedDrift: [],
|
|
68
|
-
manifestWritten: false
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (apply) {
|
|
72
|
-
const preview = await upgradeProject({ targetDir, apply: false, force });
|
|
73
|
-
if (preview.manualReview.length > 0) {
|
|
74
|
-
return {
|
|
75
|
-
...preview,
|
|
76
|
-
applied: true,
|
|
77
|
-
written: [],
|
|
78
|
-
planned: [],
|
|
79
|
-
manifestWritten: false
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for (const file of managedFiles) {
|
|
85
|
-
if (file.strategy === "merge-vercel") {
|
|
86
|
-
await upgradeVercelConfig(file, { apply, force, manifest }, result);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
await upgradeCopiedFile(file, { apply, force, manifest }, result);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
result.userOwnedDrift.push(...(await collectUserOwnedDrift(packageRoot, targetDir)));
|
|
94
|
-
|
|
95
|
-
const manifestNeedsWrite =
|
|
96
|
-
result.planned.length > 0 || result.written.length > 0 || shouldWriteManifest(manifest, managedFiles);
|
|
97
|
-
|
|
98
|
-
if (apply && result.manualReview.length === 0 && manifestNeedsWrite) {
|
|
99
|
-
await writeInstallManifest(targetDir, managedFiles);
|
|
100
|
-
result.written.push(INSTALL_MANIFEST_PATH);
|
|
101
|
-
result.manifestWritten = true;
|
|
102
|
-
} else if (!apply && manifestNeedsWrite) {
|
|
103
|
-
result.planned.push(INSTALL_MANIFEST_PATH);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function assertExistingDxcompleteInstall(targetDir: string): Promise<void> {
|
|
110
|
-
if (await fileExists(path.join(targetDir, WORKSPACE_CONFIG_PATH))) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
throw new Error(
|
|
115
|
-
`DX Complete install not found in ${targetDir}. Run dxcomplete init before dxcomplete upgrade.`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function shouldWriteManifest(manifest: InstallManifest | undefined, files: UpgradeManagedFile[]): boolean {
|
|
120
|
-
if (!manifest) {
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
manifest.schemaVersion !== INSTALL_MANIFEST_SCHEMA_VERSION ||
|
|
126
|
-
manifest.packageVersion !== DXCOMPLETE_PACKAGE_VERSION ||
|
|
127
|
-
manifest.workspaceCompatibility !== WORKSPACE_COMPATIBILITY_VERSION
|
|
128
|
-
) {
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return files.some((file) => !manifest.managedFiles[file.destinationRelative]);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function getUpgradeManagedFiles(packageRoot: string, targetDir: string): UpgradeManagedFile[] {
|
|
136
|
-
const files = [
|
|
137
|
-
{
|
|
138
|
-
sourceRelative: path.join("templates", "next", "pages", "api", "mcp.js"),
|
|
139
|
-
destinationRelative: path.join("pages", "api", "mcp.js"),
|
|
140
|
-
strategy: "copy" as const
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
sourceRelative: path.join("templates", "next", "pages", "api", "dxcomplete.js"),
|
|
144
|
-
destinationRelative: path.join("pages", "api", "dxcomplete.js"),
|
|
145
|
-
strategy: "copy" as const
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
sourceRelative: path.join("templates", "next", "pages", "api", "dxcomplete", "[...path].js"),
|
|
149
|
-
destinationRelative: path.join("pages", "api", "dxcomplete", "[...path].js"),
|
|
150
|
-
strategy: "copy" as const
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
sourceRelative: path.join("templates", "next", "pages", "api", "auth", "callback", "google.js"),
|
|
154
|
-
destinationRelative: path.join("pages", "api", "auth", "callback", "google.js"),
|
|
155
|
-
strategy: "copy" as const
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
sourceRelative: path.join("templates", "next", "vercel.json"),
|
|
159
|
-
destinationRelative: "vercel.json",
|
|
160
|
-
strategy: "merge-vercel" as const
|
|
161
|
-
}
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
return files.map((file) => ({
|
|
165
|
-
...file,
|
|
166
|
-
sourcePath: path.join(packageRoot, file.sourceRelative),
|
|
167
|
-
destinationPath: path.join(targetDir, file.destinationRelative)
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function upgradeCopiedFile(
|
|
172
|
-
file: UpgradeManagedFile,
|
|
173
|
-
options: {
|
|
174
|
-
apply: boolean;
|
|
175
|
-
force: boolean;
|
|
176
|
-
manifest: InstallManifest | undefined;
|
|
177
|
-
},
|
|
178
|
-
result: UpgradeResult
|
|
179
|
-
): Promise<void> {
|
|
180
|
-
const desiredHash = await hashFile(file.sourcePath);
|
|
181
|
-
const exists = await fileExists(file.destinationPath);
|
|
182
|
-
const currentHash = exists ? await hashFile(file.destinationPath) : undefined;
|
|
183
|
-
|
|
184
|
-
if (currentHash === desiredHash) {
|
|
185
|
-
result.unchanged.push(file.destinationRelative);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const previousHash = options.manifest?.managedFiles[file.destinationRelative]?.sha256;
|
|
190
|
-
if (exists && !options.force && currentHash !== previousHash) {
|
|
191
|
-
result.manualReview.push(
|
|
192
|
-
previousHash
|
|
193
|
-
? `${file.destinationRelative} (modified since last DX Complete install)`
|
|
194
|
-
: `${file.destinationRelative} (differs from the current scaffold and no install manifest is available)`
|
|
195
|
-
);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!options.apply) {
|
|
200
|
-
result.planned.push(file.destinationRelative);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
await mkdir(path.dirname(file.destinationPath), { recursive: true });
|
|
205
|
-
await copyFile(file.sourcePath, file.destinationPath);
|
|
206
|
-
result.written.push(file.destinationRelative);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function upgradeVercelConfig(
|
|
210
|
-
file: UpgradeManagedFile,
|
|
211
|
-
options: {
|
|
212
|
-
apply: boolean;
|
|
213
|
-
force: boolean;
|
|
214
|
-
manifest: InstallManifest | undefined;
|
|
215
|
-
},
|
|
216
|
-
result: UpgradeResult
|
|
217
|
-
): Promise<void> {
|
|
218
|
-
const exists = await fileExists(file.destinationPath);
|
|
219
|
-
const templateContent = await readFile(file.sourcePath, "utf8");
|
|
220
|
-
let desiredContent = templateContent;
|
|
221
|
-
|
|
222
|
-
if (exists) {
|
|
223
|
-
const currentContent = await readFile(file.destinationPath, "utf8");
|
|
224
|
-
try {
|
|
225
|
-
desiredContent = `${JSON.stringify(mergeVercelConfig(JSON.parse(currentContent)), null, 2)}\n`;
|
|
226
|
-
} catch {
|
|
227
|
-
if (!options.force) {
|
|
228
|
-
result.manualReview.push(`${file.destinationRelative} (invalid JSON; cannot merge safely)`);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (hashContent(currentContent) === hashContent(desiredContent)) {
|
|
234
|
-
result.unchanged.push(file.destinationRelative);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!options.apply) {
|
|
240
|
-
result.planned.push(file.destinationRelative);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
await writeFile(file.destinationPath, desiredContent, "utf8");
|
|
245
|
-
result.written.push(file.destinationRelative);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function mergeVercelConfig(value: unknown): Record<string, unknown> {
|
|
249
|
-
const config = isPlainObject(value) ? { ...value } : {};
|
|
250
|
-
const functions = isPlainObject(config.functions) ? { ...config.functions } : {};
|
|
251
|
-
const pagesFunctionKey = "pages/api/**/*.js";
|
|
252
|
-
const pagesFunction = isPlainObject(functions[pagesFunctionKey])
|
|
253
|
-
? { ...functions[pagesFunctionKey] }
|
|
254
|
-
: {};
|
|
255
|
-
|
|
256
|
-
pagesFunction.includeFiles = mergeIncludeFiles(pagesFunction.includeFiles, "dxcomplete/workspace.json");
|
|
257
|
-
functions[pagesFunctionKey] = pagesFunction;
|
|
258
|
-
config.functions = functions;
|
|
259
|
-
config.rewrites = mergeRewrites(config.rewrites);
|
|
260
|
-
|
|
261
|
-
return config;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function mergeIncludeFiles(value: unknown, requiredFile: string): string | string[] {
|
|
265
|
-
if (typeof value === "string") {
|
|
266
|
-
return value === requiredFile ? value : uniqueStrings([value, requiredFile]);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (Array.isArray(value)) {
|
|
270
|
-
return uniqueStrings([...value.filter((entry): entry is string => typeof entry === "string"), requiredFile]);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return requiredFile;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function mergeRewrites(value: unknown): Record<string, unknown>[] {
|
|
277
|
-
const rewrites = Array.isArray(value)
|
|
278
|
-
? value.filter((entry): entry is Record<string, unknown> => isPlainObject(entry)).map((entry) => ({ ...entry }))
|
|
279
|
-
: [];
|
|
280
|
-
|
|
281
|
-
for (const requiredRewrite of WELL_KNOWN_REWRITES) {
|
|
282
|
-
const existing = rewrites.find((rewrite) => rewrite.source === requiredRewrite.source);
|
|
283
|
-
if (existing) {
|
|
284
|
-
existing.destination = requiredRewrite.destination;
|
|
285
|
-
} else {
|
|
286
|
-
rewrites.push({ ...requiredRewrite });
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return rewrites;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async function collectUserOwnedDrift(packageRoot: string, targetDir: string): Promise<string[]> {
|
|
294
|
-
const files = [
|
|
295
|
-
...(await collectTemplateFiles(
|
|
296
|
-
path.join(packageRoot, "docs"),
|
|
297
|
-
path.join(targetDir, "dxcomplete", "docs"),
|
|
298
|
-
path.join("dxcomplete", "docs")
|
|
299
|
-
)),
|
|
300
|
-
...(await collectTemplateFiles(
|
|
301
|
-
path.join(packageRoot, "templates", "process"),
|
|
302
|
-
path.join(targetDir, "dxcomplete", "process"),
|
|
303
|
-
path.join("dxcomplete", "process")
|
|
304
|
-
))
|
|
305
|
-
];
|
|
306
|
-
const drift: string[] = [];
|
|
307
|
-
|
|
308
|
-
for (const file of files) {
|
|
309
|
-
if (!(await fileExists(file.destinationPath))) {
|
|
310
|
-
drift.push(`${file.destinationRelative} (missing user-owned scaffold file)`);
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if ((await hashFile(file.sourcePath)) !== (await hashFile(file.destinationPath))) {
|
|
315
|
-
drift.push(`${file.destinationRelative} (differs from current package template)`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return drift;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function collectTemplateFiles(
|
|
323
|
-
sourceDir: string,
|
|
324
|
-
destinationDir: string,
|
|
325
|
-
destinationRootRelative: string
|
|
326
|
-
): Promise<Array<{ sourcePath: string; destinationPath: string; destinationRelative: string }>> {
|
|
327
|
-
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
328
|
-
const files: Array<{ sourcePath: string; destinationPath: string; destinationRelative: string }> = [];
|
|
329
|
-
|
|
330
|
-
for (const entry of entries) {
|
|
331
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
332
|
-
const destinationPath = path.join(destinationDir, entry.name);
|
|
333
|
-
const destinationRelative = path.join(destinationRootRelative, entry.name);
|
|
334
|
-
|
|
335
|
-
if (entry.isDirectory()) {
|
|
336
|
-
files.push(...(await collectTemplateFiles(sourcePath, destinationPath, destinationRelative)));
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (!entry.isFile()) {
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
files.push({ sourcePath, destinationPath, destinationRelative });
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return files;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
351
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function uniqueStrings(values: string[]): string[] {
|
|
355
|
-
return [...new Set(values)];
|
|
356
|
-
}
|
package/src/validate.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { constants as fsConstants } from "node:fs";
|
|
2
|
-
import { access } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
export type ValidateOptions = {
|
|
6
|
-
targetDir: string;
|
|
7
|
-
packageLayout?: boolean;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export type ValidateResult = {
|
|
11
|
-
targetDir: string;
|
|
12
|
-
ok: boolean;
|
|
13
|
-
missing: string[];
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const docs = [
|
|
17
|
-
"index.md",
|
|
18
|
-
"decision-basis.md",
|
|
19
|
-
"cost-model.md",
|
|
20
|
-
"glossary.md",
|
|
21
|
-
"model.md",
|
|
22
|
-
"operating-guide.md",
|
|
23
|
-
"roles.md",
|
|
24
|
-
"taxonomy.md",
|
|
25
|
-
"workflows.md",
|
|
26
|
-
"diagrams.md",
|
|
27
|
-
"codex-integration.md",
|
|
28
|
-
"open-questions.md"
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const processFiles = [
|
|
32
|
-
"README.md",
|
|
33
|
-
"decision-basis.yml",
|
|
34
|
-
"cost-model.yml",
|
|
35
|
-
"roles.yml",
|
|
36
|
-
"taxonomy.yml",
|
|
37
|
-
"workflows.yml",
|
|
38
|
-
"controls.yml"
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const diagrams = [
|
|
42
|
-
"00-decision-basis.mmd",
|
|
43
|
-
"00-overview.mmd",
|
|
44
|
-
"01-intake-triage.mmd",
|
|
45
|
-
"02-product-definition.mmd",
|
|
46
|
-
"03-engineering-execution.mmd",
|
|
47
|
-
"04-qa-verification.mmd",
|
|
48
|
-
"05-product-validation.mmd",
|
|
49
|
-
"06-change-release-control.mmd",
|
|
50
|
-
"07-deployment-operations.mmd",
|
|
51
|
-
"08-support-incident-management.mmd",
|
|
52
|
-
"09-problem-improvement.mmd",
|
|
53
|
-
"10-risk-control-management.mmd",
|
|
54
|
-
"11-audit-evidence-capture.mmd"
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
export async function validateScaffold(options: ValidateOptions): Promise<ValidateResult> {
|
|
58
|
-
const targetDir = path.resolve(options.targetDir);
|
|
59
|
-
const requiredFiles = options.packageLayout
|
|
60
|
-
? packageRequiredFiles()
|
|
61
|
-
: installedRequiredFiles();
|
|
62
|
-
|
|
63
|
-
const missing: string[] = [];
|
|
64
|
-
|
|
65
|
-
for (const requiredFile of requiredFiles) {
|
|
66
|
-
if (!(await fileExists(path.join(targetDir, requiredFile)))) {
|
|
67
|
-
missing.push(requiredFile);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
targetDir,
|
|
73
|
-
ok: missing.length === 0,
|
|
74
|
-
missing
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function packageRequiredFiles(): string[] {
|
|
79
|
-
return [
|
|
80
|
-
...docs.map((file) => path.join("docs", file)),
|
|
81
|
-
...processFiles.map((file) => path.join("templates", "process", file)),
|
|
82
|
-
...diagrams.map((file) => path.join("templates", "process", "diagrams", file)),
|
|
83
|
-
".env.example",
|
|
84
|
-
path.join("api", "mcp.js"),
|
|
85
|
-
path.join("api", "dxcomplete.js"),
|
|
86
|
-
path.join("api", "dxcomplete-service.js"),
|
|
87
|
-
path.join("api", "auth", "callback", "google.js"),
|
|
88
|
-
path.join("dxcomplete", "workspace.json"),
|
|
89
|
-
path.join("templates", "AGENTS.md"),
|
|
90
|
-
path.join("scripts", "check-env-surface.mjs"),
|
|
91
|
-
path.join("scripts", "smoke-mcp-http.mjs"),
|
|
92
|
-
path.join("scripts", "runtime-work-order.mjs"),
|
|
93
|
-
path.join("scripts", "check-public-copy.mjs"),
|
|
94
|
-
path.join("scripts", "check-service-boundary.mjs"),
|
|
95
|
-
path.join("templates", "next", "pages", "api", "mcp.js"),
|
|
96
|
-
path.join("templates", "next", "pages", "api", "dxcomplete.js"),
|
|
97
|
-
path.join("templates", "next", "pages", "api", "dxcomplete", "[...path].js"),
|
|
98
|
-
path.join("templates", "next", "pages", "api", "auth", "callback", "google.js"),
|
|
99
|
-
path.join("templates", "next", "vercel.json"),
|
|
100
|
-
path.join("templates", "github", "workflows", "dxcomplete.yml"),
|
|
101
|
-
path.join("src", "cli.ts"),
|
|
102
|
-
path.join("src", "install-manifest.ts"),
|
|
103
|
-
path.join("src", "init.ts"),
|
|
104
|
-
path.join("src", "package-root.ts"),
|
|
105
|
-
path.join("src", "http", "server.ts"),
|
|
106
|
-
path.join("src", "http", "service.ts"),
|
|
107
|
-
path.join("src", "mcp", "server.ts"),
|
|
108
|
-
path.join("src", "runtime", "auth.ts"),
|
|
109
|
-
path.join("src", "runtime", "check.ts"),
|
|
110
|
-
path.join("src", "runtime", "config.ts"),
|
|
111
|
-
path.join("src", "runtime", "mongo.ts"),
|
|
112
|
-
path.join("src", "runtime", "records.ts"),
|
|
113
|
-
path.join("src", "runtime", "workspace.ts"),
|
|
114
|
-
path.join("src", "upgrade.ts"),
|
|
115
|
-
path.join("src", "version.ts"),
|
|
116
|
-
path.join("src", "validate.ts")
|
|
117
|
-
];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function installedRequiredFiles(): string[] {
|
|
121
|
-
return [
|
|
122
|
-
path.join("pages", "api", "mcp.js"),
|
|
123
|
-
path.join("pages", "api", "dxcomplete.js"),
|
|
124
|
-
path.join("pages", "api", "dxcomplete", "[...path].js"),
|
|
125
|
-
path.join("pages", "api", "auth", "callback", "google.js"),
|
|
126
|
-
"vercel.json",
|
|
127
|
-
path.join("dxcomplete", "workspace.json"),
|
|
128
|
-
...docs.map((file) => path.join("dxcomplete", "docs", file)),
|
|
129
|
-
...processFiles.map((file) => path.join("dxcomplete", "process", file)),
|
|
130
|
-
...diagrams.map((file) => path.join("dxcomplete", "process", "diagrams", file))
|
|
131
|
-
];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function fileExists(filePath: string): Promise<boolean> {
|
|
135
|
-
try {
|
|
136
|
-
await access(filePath, fsConstants.F_OK);
|
|
137
|
-
return true;
|
|
138
|
-
} catch {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
package/src/version.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
|
|
3
|
-
const require = createRequire(import.meta.url);
|
|
4
|
-
const packageJson = require("../package.json") as { version?: unknown };
|
|
5
|
-
|
|
6
|
-
export const DXCOMPLETE_PACKAGE_VERSION = readPackageVersion(packageJson.version);
|
|
7
|
-
export const WORKSPACE_COMPATIBILITY_VERSION = 1;
|
|
8
|
-
export const MCP_SURFACE_ID = "dxc-mcp-surface";
|
|
9
|
-
|
|
10
|
-
function readPackageVersion(value: unknown): string {
|
|
11
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
12
|
-
throw new Error("DX Complete package.json version must be a non-empty string.");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return value;
|
|
16
|
-
}
|