@topogram/cli 0.3.63 → 0.3.64
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/package.json +1 -1
- package/src/adoption/plan.d.ts +6 -0
- package/src/adoption/reporting.d.ts +10 -0
- package/src/adoption/review-groups.d.ts +6 -0
- package/src/agent-brief.d.ts +3 -0
- package/src/agent-brief.js +495 -0
- package/src/agent-ops/query-builders.d.ts +26 -0
- package/src/archive/archive.d.ts +2 -0
- package/src/archive/compact.d.ts +1 -0
- package/src/archive/unarchive.d.ts +1 -0
- package/src/catalog.d.ts +10 -0
- package/src/catalog.js +62 -66
- package/src/cli/catalog-alias.d.ts +1 -0
- package/src/cli/command-parser.js +38 -0
- package/src/cli/command-parsers/core.js +102 -0
- package/src/cli/command-parsers/generator.js +39 -0
- package/src/cli/command-parsers/import.js +44 -0
- package/src/cli/command-parsers/legacy-workflow.js +21 -0
- package/src/cli/command-parsers/project.js +47 -0
- package/src/cli/command-parsers/sdlc.js +47 -0
- package/src/cli/command-parsers/shared.js +51 -0
- package/src/cli/command-parsers/template.js +48 -0
- package/src/cli/commands/agent.js +47 -0
- package/src/cli/commands/catalog.js +617 -0
- package/src/cli/commands/check.js +268 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/emit.js +149 -0
- package/src/cli/commands/generate.js +96 -0
- package/src/cli/commands/generator-policy.js +785 -0
- package/src/cli/commands/generator.js +443 -0
- package/src/cli/commands/import-runner.js +157 -0
- package/src/cli/commands/import.js +1734 -0
- package/src/cli/commands/inspect.js +55 -0
- package/src/cli/commands/new.js +94 -0
- package/src/cli/commands/package.js +815 -0
- package/src/cli/commands/query.js +1302 -0
- package/src/cli/commands/release-rollout.js +257 -0
- package/src/cli/commands/release-shared.js +528 -0
- package/src/cli/commands/release-status.js +429 -0
- package/src/cli/commands/release.js +107 -0
- package/src/cli/commands/sdlc.js +168 -0
- package/src/cli/commands/setup.js +76 -0
- package/src/cli/commands/source.js +291 -0
- package/src/cli/commands/template-runner.js +198 -0
- package/src/cli/commands/template.js +2145 -0
- package/src/cli/commands/trust.js +219 -0
- package/src/cli/commands/version.js +40 -0
- package/src/cli/commands/widget.js +168 -0
- package/src/cli/commands/workflow.js +63 -0
- package/src/cli/dispatcher.js +392 -0
- package/src/cli/help-dispatch.js +188 -0
- package/src/cli/help.js +296 -0
- package/src/cli/migration-guidance.js +59 -0
- package/src/cli/options.js +96 -0
- package/src/cli/output-safety.js +107 -0
- package/src/cli/path-normalization.js +29 -0
- package/src/cli.js +47 -11711
- package/src/example-implementation.d.ts +2 -0
- package/src/format.d.ts +1 -0
- package/src/generator/check.d.ts +1 -0
- package/src/generator/context/bundle.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/native/parity-bundle.js +2 -1
- package/src/generator/surfaces/web/html-escape.js +22 -0
- package/src/generator/surfaces/web/react.js +10 -8
- package/src/generator/surfaces/web/sveltekit.js +7 -5
- package/src/generator/surfaces/web/vanilla.js +8 -4
- package/src/generator.d.ts +2 -0
- package/src/github-client.js +520 -0
- package/src/import/core/shared.js +20 -62
- package/src/import/extractors/api/flutter-dio.js +4 -8
- package/src/import/extractors/api/react-native-repository.js +4 -8
- package/src/import/index.d.ts +4 -0
- package/src/import/provenance.d.ts +4 -0
- package/src/new-project.js +100 -11
- package/src/npm-safety.js +79 -0
- package/src/parser.d.ts +1 -0
- package/src/path-helpers.d.ts +1 -0
- package/src/path-helpers.js +20 -0
- package/src/project-config.js +1 -0
- package/src/reconcile/docs.d.ts +8 -0
- package/src/reconcile/journeys.d.ts +1 -0
- package/src/resolver.d.ts +1 -0
- package/src/runtime-support.js +29 -0
- package/src/sdlc/adopt.d.ts +1 -0
- package/src/sdlc/check.d.ts +1 -0
- package/src/sdlc/explain.d.ts +1 -0
- package/src/sdlc/release.d.ts +1 -0
- package/src/sdlc/scaffold.d.ts +1 -0
- package/src/sdlc/transition.d.ts +1 -0
- package/src/text-helpers.d.ts +6 -0
- package/src/text-helpers.js +245 -0
- package/src/topogram-config.js +306 -0
- package/src/validator.d.ts +2 -0
- package/src/workflows/adoption/index.js +26 -0
- package/src/workflows/docs-generate.js +262 -0
- package/src/workflows/docs-scan.js +703 -0
- package/src/workflows/docs.js +15 -0
- package/src/workflows/import-app/api.js +799 -0
- package/src/workflows/import-app/db.js +538 -0
- package/src/workflows/import-app/index.js +30 -0
- package/src/workflows/import-app/shared.js +218 -0
- package/src/workflows/import-app/ui.js +443 -0
- package/src/workflows/import-app/workflow.js +159 -0
- package/src/workflows/reconcile/adoption-plan.js +742 -0
- package/src/workflows/reconcile/auth.js +692 -0
- package/src/workflows/reconcile/bundle-core.js +600 -0
- package/src/workflows/reconcile/bundle-shared.js +75 -0
- package/src/workflows/reconcile/candidate-model.js +477 -0
- package/src/workflows/reconcile/canonical-surface.js +264 -0
- package/src/workflows/reconcile/gap-report.js +333 -0
- package/src/workflows/reconcile/ids.js +6 -0
- package/src/workflows/reconcile/impacts.js +625 -0
- package/src/workflows/reconcile/index.js +7 -0
- package/src/workflows/reconcile/renderers.js +461 -0
- package/src/workflows/reconcile/summary.js +90 -0
- package/src/workflows/reconcile/workflow.js +309 -0
- package/src/workflows/shared.js +189 -0
- package/src/workflows/types.d.ts +93 -0
- package/src/workflows.d.ts +1 -0
- package/src/workflows.js +10 -7652
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { loadImplementationProvider } from "../../example-implementation.js";
|
|
4
|
+
import { stableStringify } from "../../format.js";
|
|
5
|
+
import { parsePath } from "../../parser.js";
|
|
6
|
+
import {
|
|
7
|
+
formatProjectConfigErrors,
|
|
8
|
+
loadProjectConfig,
|
|
9
|
+
projectConfigOrDefault,
|
|
10
|
+
validateProjectConfig,
|
|
11
|
+
validateProjectOutputOwnership
|
|
12
|
+
} from "../../project-config.js";
|
|
13
|
+
import { resolveWorkspace } from "../../resolver.js";
|
|
14
|
+
import { validateProjectImplementationTrust } from "../../template-trust.js";
|
|
15
|
+
import { formatValidationErrors } from "../../validator.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Record<string, any>} AnyRecord
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {{ message: string, loc: any }} ValidationError
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
export function printCheckHelp() {
|
|
29
|
+
console.log("Usage: topogram check [path] [--json]");
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log("Validates Topogram files, project configuration, topology, generator compatibility, generator policy, output ownership, and template policy.");
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log("Defaults: path is ./topogram.");
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log("Examples:");
|
|
36
|
+
console.log(" topogram check");
|
|
37
|
+
console.log(" topogram check --json");
|
|
38
|
+
console.log(" topogram check ./topogram");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {AnyRecord} component
|
|
43
|
+
* @returns {{ uses_api: string|null, uses_database: string|null }}
|
|
44
|
+
*/
|
|
45
|
+
function topologyComponentReferences(component) {
|
|
46
|
+
return {
|
|
47
|
+
uses_api: component.uses_api || null,
|
|
48
|
+
uses_database: component.uses_database || null
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {AnyRecord} component
|
|
54
|
+
* @returns {unknown}
|
|
55
|
+
*/
|
|
56
|
+
function topologyComponentPort(component) {
|
|
57
|
+
return Object.prototype.hasOwnProperty.call(component, "port") ? component.port : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {AnyRecord|null|undefined} config
|
|
62
|
+
* @returns {{ outputs: Array<AnyRecord>, runtimes: Array<AnyRecord>, edges: Array<AnyRecord> }}
|
|
63
|
+
*/
|
|
64
|
+
function summarizeProjectTopology(config) {
|
|
65
|
+
const outputs = Object.entries(config?.outputs || {})
|
|
66
|
+
.map(([name, output]) => ({
|
|
67
|
+
name,
|
|
68
|
+
path: /** @type {AnyRecord} */ (output)?.path || null,
|
|
69
|
+
ownership: /** @type {AnyRecord} */ (output)?.ownership || null
|
|
70
|
+
}))
|
|
71
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
72
|
+
const runtimeInputs = /** @type {Array<AnyRecord>} */ (config?.topology?.runtimes || []);
|
|
73
|
+
const runtimes = runtimeInputs
|
|
74
|
+
.map((component) => ({
|
|
75
|
+
id: component.id,
|
|
76
|
+
kind: component.kind,
|
|
77
|
+
projection: component.projection,
|
|
78
|
+
generator: {
|
|
79
|
+
id: component.generator?.id || null,
|
|
80
|
+
version: component.generator?.version || null
|
|
81
|
+
},
|
|
82
|
+
port: topologyComponentPort(component),
|
|
83
|
+
references: topologyComponentReferences(component)
|
|
84
|
+
}))
|
|
85
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
86
|
+
/** @type {Array<AnyRecord>} */
|
|
87
|
+
const edges = runtimes.flatMap((component) => {
|
|
88
|
+
const references = [];
|
|
89
|
+
if (component.references.uses_api) {
|
|
90
|
+
references.push({
|
|
91
|
+
from: component.id,
|
|
92
|
+
to: component.references.uses_api,
|
|
93
|
+
type: "calls_api"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (component.references.uses_database) {
|
|
97
|
+
references.push({
|
|
98
|
+
from: component.id,
|
|
99
|
+
to: component.references.uses_database,
|
|
100
|
+
type: "uses_database"
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return references;
|
|
104
|
+
}).sort((left, right) => `${left.from}:${left.type}:${left.to}`.localeCompare(`${right.from}:${right.type}:${right.to}`));
|
|
105
|
+
return {
|
|
106
|
+
outputs,
|
|
107
|
+
runtimes,
|
|
108
|
+
edges
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {AnyRecord|null|undefined} topology
|
|
114
|
+
* @returns {AnyRecord|null}
|
|
115
|
+
*/
|
|
116
|
+
function publicProjectTopology(topology) {
|
|
117
|
+
if (!topology || typeof topology !== "object") {
|
|
118
|
+
return topology || null;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
...Object.fromEntries(Object.entries(topology).filter(([key]) => key !== "components")),
|
|
122
|
+
runtimes: topology.runtimes || []
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {AnyRecord} component
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function formatTopologyComponent(component) {
|
|
131
|
+
const generator = component.generator.id
|
|
132
|
+
? `${component.generator.id}${component.generator.version ? `@${component.generator.version}` : ""}`
|
|
133
|
+
: "unbound generator";
|
|
134
|
+
const port = component.port == null ? "no port" : `port ${component.port}`;
|
|
135
|
+
const refs = Object.entries(component.references)
|
|
136
|
+
.filter(([, value]) => Boolean(value))
|
|
137
|
+
.map(([key, value]) => `${key} ${value}`);
|
|
138
|
+
const suffix = refs.length ? ` -> ${refs.join(", ")}` : "";
|
|
139
|
+
return ` - ${component.id}: ${component.kind} ${component.projection} via ${generator} (${port})${suffix}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {{ outputs: Array<AnyRecord>, runtimes: Array<AnyRecord>, edges: Array<AnyRecord> }} topology
|
|
144
|
+
* @returns {void}
|
|
145
|
+
*/
|
|
146
|
+
export function printTopologySummary(topology) {
|
|
147
|
+
console.log("Project topology:");
|
|
148
|
+
if (topology.outputs.length > 0) {
|
|
149
|
+
console.log(" Outputs:");
|
|
150
|
+
for (const output of topology.outputs) {
|
|
151
|
+
console.log(` - ${output.name}: ${output.path || "unset"} (${output.ownership || "unknown"})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (topology.runtimes.length > 0) {
|
|
155
|
+
console.log(" Runtimes:");
|
|
156
|
+
for (const component of topology.runtimes) {
|
|
157
|
+
console.log(formatTopologyComponent(component));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (topology.edges.length > 0) {
|
|
161
|
+
console.log(" Edges:");
|
|
162
|
+
for (const edge of topology.edges) {
|
|
163
|
+
console.log(` - ${edge.from} ${edge.type} ${edge.to}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {{ inputPath: string, ast: AnyRecord, resolved: AnyRecord, projectConfigInfo: AnyRecord|null|undefined, projectValidation: AnyRecord }} input
|
|
170
|
+
* @returns {AnyRecord}
|
|
171
|
+
*/
|
|
172
|
+
export function checkSummaryPayload({ inputPath, ast, resolved, projectConfigInfo, projectValidation }) {
|
|
173
|
+
const files = /** @type {Array<AnyRecord>} */ (ast.files || []);
|
|
174
|
+
const statementCount = files.flatMap((file) => file.statements).length;
|
|
175
|
+
const projectInfo = projectConfigInfo || {
|
|
176
|
+
configPath: null,
|
|
177
|
+
compatibility: false,
|
|
178
|
+
config: { topology: null }
|
|
179
|
+
};
|
|
180
|
+
const topogramErrors = resolved.ok
|
|
181
|
+
? []
|
|
182
|
+
: /** @type {Array<AnyRecord>} */ (resolved.validation.errors || []);
|
|
183
|
+
const projectErrors = /** @type {Array<AnyRecord>} */ (projectValidation.errors || []);
|
|
184
|
+
const resolvedTopology = summarizeProjectTopology(projectInfo.config);
|
|
185
|
+
return {
|
|
186
|
+
ok: resolved.ok && projectValidation.ok,
|
|
187
|
+
inputPath,
|
|
188
|
+
topogram: {
|
|
189
|
+
files: files.length,
|
|
190
|
+
statements: statementCount,
|
|
191
|
+
valid: resolved.ok
|
|
192
|
+
},
|
|
193
|
+
project: {
|
|
194
|
+
configPath: projectInfo.configPath,
|
|
195
|
+
compatibility: Boolean(projectInfo.compatibility),
|
|
196
|
+
valid: projectValidation.ok,
|
|
197
|
+
topology: publicProjectTopology(projectInfo.config.topology),
|
|
198
|
+
resolvedTopology
|
|
199
|
+
},
|
|
200
|
+
errors: [
|
|
201
|
+
...topogramErrors.map((error) => ({
|
|
202
|
+
source: "topogram",
|
|
203
|
+
message: error.message,
|
|
204
|
+
loc: error.loc
|
|
205
|
+
})),
|
|
206
|
+
...projectErrors.map((error) => ({
|
|
207
|
+
source: "project",
|
|
208
|
+
message: error.message,
|
|
209
|
+
loc: error.loc
|
|
210
|
+
}))
|
|
211
|
+
]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {...{ ok?: boolean, errors?: ValidationError[] }|null|undefined} results
|
|
217
|
+
* @returns {{ ok: boolean, errors: ValidationError[] }}
|
|
218
|
+
*/
|
|
219
|
+
export function combineProjectValidationResults(...results) {
|
|
220
|
+
/** @type {ValidationError[]} */
|
|
221
|
+
const errors = [];
|
|
222
|
+
for (const result of results) {
|
|
223
|
+
errors.push(...(result?.errors || []));
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
ok: errors.length === 0,
|
|
227
|
+
errors
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {string|null|undefined} inputPath
|
|
233
|
+
* @param {{ json?: boolean }} [options]
|
|
234
|
+
* @returns {Promise<number>}
|
|
235
|
+
*/
|
|
236
|
+
export async function runCheckCommand(inputPath, options = {}) {
|
|
237
|
+
const topogramPath = inputPath || "./topogram";
|
|
238
|
+
const ast = parsePath(topogramPath);
|
|
239
|
+
const resolved = resolveWorkspace(ast);
|
|
240
|
+
const implementation = await loadImplementationProvider(topogramPath).catch(() => null);
|
|
241
|
+
const explicitProjectConfig = loadProjectConfig(topogramPath);
|
|
242
|
+
const projectConfigInfo = explicitProjectConfig ||
|
|
243
|
+
(implementation ? projectConfigOrDefault(topogramPath, resolved.ok ? resolved.graph : null, implementation) : null);
|
|
244
|
+
const projectValidation = projectConfigInfo
|
|
245
|
+
? combineProjectValidationResults(
|
|
246
|
+
validateProjectConfig(projectConfigInfo.config, resolved.ok ? resolved.graph : null, { configDir: projectConfigInfo.configDir }),
|
|
247
|
+
validateProjectOutputOwnership(projectConfigInfo),
|
|
248
|
+
validateProjectImplementationTrust(projectConfigInfo)
|
|
249
|
+
)
|
|
250
|
+
: { ok: false, errors: [{ message: "Missing topogram.project.json or compatible topogram.implementation.json", loc: null }] };
|
|
251
|
+
const payload = checkSummaryPayload({ inputPath: topogramPath, ast, resolved, projectConfigInfo, projectValidation });
|
|
252
|
+
if (options.json) {
|
|
253
|
+
console.log(stableStringify(payload));
|
|
254
|
+
} else if (payload.ok) {
|
|
255
|
+
console.log(`Topogram check passed for ${topogramPath}.`);
|
|
256
|
+
console.log(`Validated ${payload.topogram.files} file(s) and ${payload.topogram.statements} statement(s).`);
|
|
257
|
+
console.log(`Project config: ${payload.project.configPath || "compatibility defaults"}`);
|
|
258
|
+
printTopologySummary(payload.project.resolvedTopology);
|
|
259
|
+
} else {
|
|
260
|
+
if (!resolved.ok) {
|
|
261
|
+
console.error(formatValidationErrors(resolved.validation));
|
|
262
|
+
}
|
|
263
|
+
if (!projectValidation.ok) {
|
|
264
|
+
console.error(formatProjectConfigErrors(projectValidation, projectConfigInfo?.configPath || "topogram.project.json"));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return payload.ok ? 0 : 1;
|
|
268
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
catalogSourceOrDefault,
|
|
8
|
+
isCatalogSourceDisabled
|
|
9
|
+
} from "../../catalog.js";
|
|
10
|
+
import { LOCAL_NPMRC_ENV, localNpmrcStatus } from "../../npm-safety.js";
|
|
11
|
+
import { loadProjectConfig } from "../../project-config.js";
|
|
12
|
+
import {
|
|
13
|
+
buildCatalogDoctorAuth,
|
|
14
|
+
buildCatalogDoctorPayload
|
|
15
|
+
} from "./catalog.js";
|
|
16
|
+
import {
|
|
17
|
+
checkDoctorNode,
|
|
18
|
+
checkDoctorNpm,
|
|
19
|
+
checkDoctorPackageAccess,
|
|
20
|
+
CLI_PACKAGE_NAME,
|
|
21
|
+
inspectTopogramCliLockfile,
|
|
22
|
+
isLocalCliDependencySpec,
|
|
23
|
+
normalizeRegistryUrl,
|
|
24
|
+
NPMJS_REGISTRY,
|
|
25
|
+
npmConfigGet,
|
|
26
|
+
readInstalledCliPackageVersion,
|
|
27
|
+
readProjectCliDependencySpec
|
|
28
|
+
} from "./package.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
export function printDoctorHelp() {
|
|
34
|
+
console.log("Usage: topogram doctor [--json] [--catalog <path-or-source>]");
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log("Checks local runtime, npm, public package access, CLI lockfile metadata, and catalog access.");
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log("Fresh install check:");
|
|
39
|
+
console.log(" npm install --save-dev @topogram/cli");
|
|
40
|
+
console.log(" npx topogram doctor");
|
|
41
|
+
console.log(" npx topogram template list");
|
|
42
|
+
console.log(" npx topogram new ./my-app --template hello-web");
|
|
43
|
+
console.log("");
|
|
44
|
+
console.log("Related setup commands:");
|
|
45
|
+
console.log(" topogram setup package-auth");
|
|
46
|
+
console.log(" topogram setup catalog-auth");
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log("Examples:");
|
|
49
|
+
console.log(" topogram doctor");
|
|
50
|
+
console.log(" topogram doctor --json");
|
|
51
|
+
console.log(" topogram doctor --catalog ./topograms.catalog.json");
|
|
52
|
+
console.log(" topogram catalog doctor");
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log("Use `catalog doctor` when you only want catalog/package-access diagnostics. Use `doctor --catalog` for the full environment check plus catalog diagnostics.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {unknown} error
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function messageFromError(error) {
|
|
62
|
+
return error instanceof Error ? error.message : String(error);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} inputPath
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function normalizeTopogramPath(inputPath) {
|
|
70
|
+
const absolute = path.resolve(inputPath);
|
|
71
|
+
if (path.basename(absolute) === "topogram") {
|
|
72
|
+
return absolute;
|
|
73
|
+
}
|
|
74
|
+
const candidate = path.join(absolute, "topogram");
|
|
75
|
+
return fs.existsSync(candidate) ? candidate : absolute;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string|null} source
|
|
80
|
+
* @returns {{ ok: boolean, node: ReturnType<typeof checkDoctorNode>, npm: ReturnType<typeof checkDoctorNpm>, localNpmrc: ReturnType<typeof localNpmrcStatus>, packageRegistry: { required: boolean, reason: string|null, registry: string, configuredRegistry: string|null, registryConfigured: boolean, nodeAuthTokenEnv: boolean, packageName: string, packageSpec: string|null, packageAccess: { ok: boolean, checkedVersion: string|null, diagnostics: any[] } }, lockfile: ReturnType<typeof inspectTopogramCliLockfile>, catalog: ReturnType<typeof buildCatalogDoctorPayload>, diagnostics: any[], errors: string[] }}
|
|
81
|
+
*/
|
|
82
|
+
export function buildDoctorPayload(source) {
|
|
83
|
+
const projectCliDependency = readProjectCliDependencySpec(process.cwd());
|
|
84
|
+
const packageRegistryRequired = !isLocalCliDependencySpec(projectCliDependency);
|
|
85
|
+
const node = checkDoctorNode();
|
|
86
|
+
const npm = checkDoctorNpm();
|
|
87
|
+
const configuredRegistry = npm.available ? npmConfigGet("@topogram:registry") : null;
|
|
88
|
+
const registryConfigured = !configuredRegistry ||
|
|
89
|
+
normalizeRegistryUrl(configuredRegistry) === normalizeRegistryUrl(NPMJS_REGISTRY);
|
|
90
|
+
const registryDiagnostics = [];
|
|
91
|
+
if (packageRegistryRequired && npm.available && !registryConfigured) {
|
|
92
|
+
registryDiagnostics.push({
|
|
93
|
+
code: "package_registry_registry_not_configured",
|
|
94
|
+
severity: "error",
|
|
95
|
+
message: `npm is configured to resolve @topogram packages from '${configuredRegistry}', not ${NPMJS_REGISTRY}.`,
|
|
96
|
+
path: ".npmrc",
|
|
97
|
+
suggestedFix: "Remove the custom @topogram registry config or set it to https://registry.npmjs.org, then rerun `topogram doctor`."
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const packageSpec = packageRegistryRequired ? `${CLI_PACKAGE_NAME}@${readInstalledCliPackageVersion()}` : null;
|
|
101
|
+
const packageAccess = packageRegistryRequired && npm.available
|
|
102
|
+
? checkDoctorPackageAccess(packageSpec || CLI_PACKAGE_NAME)
|
|
103
|
+
: packageRegistryRequired ? {
|
|
104
|
+
ok: false,
|
|
105
|
+
checkedVersion: null,
|
|
106
|
+
diagnostics: [{
|
|
107
|
+
code: "npm_not_found",
|
|
108
|
+
severity: "error",
|
|
109
|
+
message: "npm is required to inspect the Topogram CLI package.",
|
|
110
|
+
path: null,
|
|
111
|
+
suggestedFix: "Install Node.js/npm, then rerun `topogram doctor`."
|
|
112
|
+
}]
|
|
113
|
+
} : {
|
|
114
|
+
ok: true,
|
|
115
|
+
checkedVersion: null,
|
|
116
|
+
diagnostics: []
|
|
117
|
+
};
|
|
118
|
+
const catalog = buildDoctorCatalogPayload(source);
|
|
119
|
+
const lockfile = inspectTopogramCliLockfile(process.cwd());
|
|
120
|
+
const diagnostics = [
|
|
121
|
+
...node.diagnostics,
|
|
122
|
+
...npm.diagnostics,
|
|
123
|
+
...registryDiagnostics,
|
|
124
|
+
...packageAccess.diagnostics,
|
|
125
|
+
...lockfile.diagnostics,
|
|
126
|
+
...catalog.diagnostics
|
|
127
|
+
];
|
|
128
|
+
const errors = diagnostics
|
|
129
|
+
.filter((diagnostic) => diagnostic.severity === "error")
|
|
130
|
+
.map((diagnostic) => diagnostic.message);
|
|
131
|
+
return {
|
|
132
|
+
ok: errors.length === 0,
|
|
133
|
+
node,
|
|
134
|
+
npm,
|
|
135
|
+
localNpmrc: localNpmrcStatus(process.cwd()),
|
|
136
|
+
packageRegistry: {
|
|
137
|
+
required: packageRegistryRequired,
|
|
138
|
+
reason: packageRegistryRequired ? null : `Project uses local CLI dependency '${projectCliDependency}'.`,
|
|
139
|
+
registry: NPMJS_REGISTRY,
|
|
140
|
+
configuredRegistry,
|
|
141
|
+
registryConfigured,
|
|
142
|
+
nodeAuthTokenEnv: Boolean(process.env.NODE_AUTH_TOKEN),
|
|
143
|
+
packageName: CLI_PACKAGE_NAME,
|
|
144
|
+
packageSpec,
|
|
145
|
+
packageAccess
|
|
146
|
+
},
|
|
147
|
+
lockfile,
|
|
148
|
+
catalog,
|
|
149
|
+
diagnostics,
|
|
150
|
+
errors
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string|null} source
|
|
156
|
+
* @returns {ReturnType<typeof buildCatalogDoctorPayload>}
|
|
157
|
+
*/
|
|
158
|
+
function buildDoctorCatalogPayload(source) {
|
|
159
|
+
const resolvedSource = resolveDoctorCatalogSource(source);
|
|
160
|
+
if (isCatalogSourceDisabled(resolvedSource)) {
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
source: resolvedSource,
|
|
164
|
+
auth: buildCatalogDoctorAuth(resolvedSource),
|
|
165
|
+
catalog: {
|
|
166
|
+
reachable: false,
|
|
167
|
+
version: null,
|
|
168
|
+
entries: 0
|
|
169
|
+
},
|
|
170
|
+
packages: [],
|
|
171
|
+
diagnostics: [{
|
|
172
|
+
code: "catalog_check_skipped",
|
|
173
|
+
severity: "warning",
|
|
174
|
+
message: "Catalog access check was skipped for this project.",
|
|
175
|
+
path: null,
|
|
176
|
+
suggestedFix: "Pass --catalog <source> to check a catalog explicitly."
|
|
177
|
+
}],
|
|
178
|
+
errors: []
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return buildCatalogDoctorPayload(resolvedSource);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {string|null} source
|
|
186
|
+
* @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
function resolveDoctorCatalogSource(source) {
|
|
189
|
+
if (source) {
|
|
190
|
+
return source;
|
|
191
|
+
}
|
|
192
|
+
const projectConfigInfo = loadProjectConfig(normalizeTopogramPath(process.cwd()));
|
|
193
|
+
if (projectConfigInfo) {
|
|
194
|
+
const catalog = projectConfigInfo.config?.template?.catalog;
|
|
195
|
+
if (catalog && typeof catalog.source === "string" && catalog.source) {
|
|
196
|
+
return catalog.source;
|
|
197
|
+
}
|
|
198
|
+
return "none";
|
|
199
|
+
}
|
|
200
|
+
return catalogSourceOrDefault(null);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {ReturnType<typeof buildDoctorPayload>} payload
|
|
205
|
+
* @returns {void}
|
|
206
|
+
*/
|
|
207
|
+
function printDoctorSetupGuidance(payload) {
|
|
208
|
+
console.log("Setup guidance:");
|
|
209
|
+
if (payload.packageRegistry.required) {
|
|
210
|
+
console.log(`- CLI package access: public @topogram packages should install from ${payload.packageRegistry.registry} without auth.`);
|
|
211
|
+
} else {
|
|
212
|
+
console.log("- CLI package auth: skipped because this project uses a local Topogram CLI dependency.");
|
|
213
|
+
}
|
|
214
|
+
if (isCatalogSourceDisabled(payload.catalog.source)) {
|
|
215
|
+
console.log("- Catalog auth: skipped because catalog discovery is disabled for this project.");
|
|
216
|
+
} else {
|
|
217
|
+
console.log("- Catalog auth: the default catalog is public; private catalogs should use GITHUB_TOKEN or GH_TOKEN. Local `gh auth login` is only a no-token fallback.");
|
|
218
|
+
}
|
|
219
|
+
console.log("- Template package auth: private template packages may need registry-specific npm auth during npm install.");
|
|
220
|
+
console.log(`- Local .npmrc: ignored by default. Use --allow-local-npmrc or ${LOCAL_NPMRC_ENV}=1 only after reviewing the file.`);
|
|
221
|
+
console.log("- Catalog disabled mode: TOPOGRAM_CATALOG_SOURCE=none skips catalog aliases, including the default hello-web starter.");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {ReturnType<typeof buildDoctorPayload>} payload
|
|
226
|
+
* @returns {void}
|
|
227
|
+
*/
|
|
228
|
+
export function printDoctor(payload) {
|
|
229
|
+
console.log(payload.ok ? "Topogram doctor passed." : "Topogram doctor found issues.");
|
|
230
|
+
console.log(`Node: ${payload.node.version} (${payload.node.ok ? "ok" : `requires ${payload.node.minimum}`})`);
|
|
231
|
+
console.log(`npm: ${payload.npm.available ? `${payload.npm.version || "available"} (ok)` : "not found"}`);
|
|
232
|
+
console.log(`local .npmrc: ${payload.localNpmrc.exists ? (payload.localNpmrc.enabled ? "enabled" : "ignored") : "not found"}`);
|
|
233
|
+
if (payload.localNpmrc.exists) {
|
|
234
|
+
console.log(`local .npmrc reason: ${payload.localNpmrc.reason}`);
|
|
235
|
+
}
|
|
236
|
+
console.log(`npm registry: ${payload.packageRegistry.required ? (payload.packageRegistry.registryConfigured ? "ok" : "misconfigured") : "not required"}`);
|
|
237
|
+
if (payload.packageRegistry.reason) {
|
|
238
|
+
console.log(`npm registry reason: ${payload.packageRegistry.reason}`);
|
|
239
|
+
}
|
|
240
|
+
if (payload.packageRegistry.configuredRegistry) {
|
|
241
|
+
console.log(`Configured @topogram registry: ${payload.packageRegistry.configuredRegistry}`);
|
|
242
|
+
}
|
|
243
|
+
console.log(`CLI package access: ${payload.packageRegistry.required ? (payload.packageRegistry.packageAccess.ok ? `${payload.packageRegistry.packageSpec} ok` : `${payload.packageRegistry.packageSpec} failed`) : "not checked"}`);
|
|
244
|
+
if (payload.lockfile.checked && payload.lockfile.packageVersion) {
|
|
245
|
+
console.log(`CLI lockfile: ${payload.lockfile.packageVersion}${payload.lockfile.refreshRecommended ? " (refresh recommended)" : " (ok)"}`);
|
|
246
|
+
}
|
|
247
|
+
console.log(`Catalog source: ${payload.catalog.source}`);
|
|
248
|
+
console.log(`Catalog reachable: ${payload.catalog.catalog.reachable ? "yes" : "no"}`);
|
|
249
|
+
if (payload.catalog.catalog.reachable) {
|
|
250
|
+
console.log(`Catalog entries: ${payload.catalog.catalog.entries}`);
|
|
251
|
+
const failedPackages = payload.catalog.packages.filter((item) => !item.ok).length;
|
|
252
|
+
console.log(`Catalog package access: ${failedPackages === 0 ? "ok" : `${failedPackages} failed`}`);
|
|
253
|
+
}
|
|
254
|
+
if (payload.catalog.source !== "none" || payload.catalog.catalog.reachable || payload.packageRegistry.required) {
|
|
255
|
+
console.log("Project provenance: run `topogram source status --local` for catalog, template, trust, and baseline details.");
|
|
256
|
+
}
|
|
257
|
+
printDoctorSetupGuidance(payload);
|
|
258
|
+
if (payload.diagnostics.length > 0) {
|
|
259
|
+
console.log("Diagnostics:");
|
|
260
|
+
for (const diagnostic of payload.diagnostics) {
|
|
261
|
+
const label = diagnostic.severity === "warning" ? "Warning" : "Error";
|
|
262
|
+
console.log(`- ${label}: ${diagnostic.message}`);
|
|
263
|
+
if (diagnostic.suggestedFix) {
|
|
264
|
+
console.log(` Fix: ${diagnostic.suggestedFix}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { loadImplementationProvider } from "../../example-implementation.js";
|
|
7
|
+
import { stableStringify } from "../../format.js";
|
|
8
|
+
import { buildOutputFiles, generateWorkspace } from "../../generator.js";
|
|
9
|
+
import { parsePath } from "../../parser.js";
|
|
10
|
+
import {
|
|
11
|
+
formatProjectConfigErrors,
|
|
12
|
+
loadProjectConfig,
|
|
13
|
+
projectConfigOrDefault,
|
|
14
|
+
validateProjectConfig
|
|
15
|
+
} from "../../project-config.js";
|
|
16
|
+
import { resolveWorkspace } from "../../resolver.js";
|
|
17
|
+
import { formatValidationErrors } from "../../validator.js";
|
|
18
|
+
import {
|
|
19
|
+
assertProjectOutputAllowsWrite,
|
|
20
|
+
assertSafeGeneratedOutputDir,
|
|
21
|
+
GENERATED_OUTPUT_SENTINEL,
|
|
22
|
+
generatedOutputSentinel,
|
|
23
|
+
topogramInputPathForGeneration
|
|
24
|
+
} from "../output-safety.js";
|
|
25
|
+
|
|
26
|
+
const IMPLEMENTATION_PROVIDER_TARGETS = new Set([
|
|
27
|
+
"persistence-scaffold",
|
|
28
|
+
"hono-server",
|
|
29
|
+
"express-server",
|
|
30
|
+
"sveltekit-app",
|
|
31
|
+
"environment-plan",
|
|
32
|
+
"environment-bundle",
|
|
33
|
+
"deployment-plan",
|
|
34
|
+
"deployment-bundle",
|
|
35
|
+
"runtime-smoke-plan",
|
|
36
|
+
"runtime-smoke-bundle",
|
|
37
|
+
"runtime-check-plan",
|
|
38
|
+
"runtime-check-bundle",
|
|
39
|
+
"compile-check-plan",
|
|
40
|
+
"compile-check-bundle",
|
|
41
|
+
"app-bundle-plan",
|
|
42
|
+
"app-bundle",
|
|
43
|
+
"native-parity-plan",
|
|
44
|
+
"native-parity-bundle"
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const IMPLEMENTATION_OPTIONAL_TARGETS = new Set([
|
|
48
|
+
"app-bundle-plan",
|
|
49
|
+
"app-bundle",
|
|
50
|
+
"environment-plan",
|
|
51
|
+
"environment-bundle",
|
|
52
|
+
"compile-check-plan",
|
|
53
|
+
"compile-check-bundle"
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} target
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function targetRequiresImplementationProvider(target) {
|
|
61
|
+
return IMPLEMENTATION_PROVIDER_TARGETS.has(target);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {{
|
|
66
|
+
* inputPath: string,
|
|
67
|
+
* projectRoot: string,
|
|
68
|
+
* target: string,
|
|
69
|
+
* write?: boolean,
|
|
70
|
+
* outDir?: string|null,
|
|
71
|
+
* selectors?: Record<string, any>,
|
|
72
|
+
* outputSelectors?: Record<string, any>,
|
|
73
|
+
* profileId?: string|null,
|
|
74
|
+
* fromSnapshotPath?: string|null,
|
|
75
|
+
* fromTopogramPath?: string|null
|
|
76
|
+
* }} options
|
|
77
|
+
* @returns {Promise<number>}
|
|
78
|
+
*/
|
|
79
|
+
export async function runEmitCommand(options) {
|
|
80
|
+
const ast = parsePath(options.inputPath);
|
|
81
|
+
const explicitProjectConfig = loadProjectConfig(options.projectRoot) || loadProjectConfig(options.inputPath);
|
|
82
|
+
const shouldLoadImplementation = targetRequiresImplementationProvider(options.target) &&
|
|
83
|
+
(!IMPLEMENTATION_OPTIONAL_TARGETS.has(options.target) || Boolean(explicitProjectConfig?.config?.implementation));
|
|
84
|
+
const implementation = shouldLoadImplementation
|
|
85
|
+
? await loadImplementationProvider(explicitProjectConfig?.configDir || options.projectRoot)
|
|
86
|
+
: null;
|
|
87
|
+
const resolvedForConfig = targetRequiresImplementationProvider(options.target) || explicitProjectConfig
|
|
88
|
+
? resolveWorkspace(ast)
|
|
89
|
+
: null;
|
|
90
|
+
if (resolvedForConfig && !resolvedForConfig.ok) {
|
|
91
|
+
console.error(formatValidationErrors(resolvedForConfig.validation));
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
const projectConfigInfo = resolvedForConfig
|
|
95
|
+
? (explicitProjectConfig || projectConfigOrDefault(options.projectRoot, resolvedForConfig.graph, implementation))
|
|
96
|
+
: null;
|
|
97
|
+
const projectConfigValidation = projectConfigInfo
|
|
98
|
+
? validateProjectConfig(projectConfigInfo.config, resolvedForConfig?.graph || null, { configDir: projectConfigInfo.configDir })
|
|
99
|
+
: { ok: true, errors: [] };
|
|
100
|
+
if (!projectConfigValidation.ok) {
|
|
101
|
+
console.error(formatProjectConfigErrors(projectConfigValidation, projectConfigInfo?.configPath || "topogram.project.json"));
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = generateWorkspace(ast, {
|
|
106
|
+
target: options.target,
|
|
107
|
+
...(options.selectors || {}),
|
|
108
|
+
profileId: options.profileId,
|
|
109
|
+
fromSnapshot: options.fromSnapshotPath ? JSON.parse(fs.readFileSync(options.fromSnapshotPath, "utf8")) : null,
|
|
110
|
+
fromSnapshotPath: options.fromSnapshotPath,
|
|
111
|
+
fromTopogramPath: options.fromTopogramPath,
|
|
112
|
+
topogramInputPath: topogramInputPathForGeneration(options.inputPath),
|
|
113
|
+
implementation,
|
|
114
|
+
projectConfig: projectConfigInfo?.config || null,
|
|
115
|
+
configDir: projectConfigInfo?.configDir || options.projectRoot,
|
|
116
|
+
projectRoot: projectConfigInfo?.configDir || options.projectRoot
|
|
117
|
+
});
|
|
118
|
+
if (!result.ok) {
|
|
119
|
+
console.error(formatValidationErrors(result.validation));
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.write) {
|
|
124
|
+
const resolvedOutDir = path.resolve(options.outDir || "artifacts");
|
|
125
|
+
assertProjectOutputAllowsWrite(projectConfigInfo, resolvedOutDir);
|
|
126
|
+
assertSafeGeneratedOutputDir(resolvedOutDir, options.inputPath);
|
|
127
|
+
const outputFiles = buildOutputFiles(result, options.outputSelectors || {});
|
|
128
|
+
outputFiles.unshift({
|
|
129
|
+
path: GENERATED_OUTPUT_SENTINEL,
|
|
130
|
+
contents: generatedOutputSentinel(options.target)
|
|
131
|
+
});
|
|
132
|
+
fs.rmSync(resolvedOutDir, { recursive: true, force: true });
|
|
133
|
+
fs.mkdirSync(resolvedOutDir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
for (const file of outputFiles) {
|
|
136
|
+
const destination = path.join(resolvedOutDir, file.path);
|
|
137
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
138
|
+
const contents =
|
|
139
|
+
typeof file.contents === "string" ? file.contents : `${stableStringify(file.contents)}\n`;
|
|
140
|
+
fs.writeFileSync(destination, contents, "utf8");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(`Wrote ${outputFiles.length} file(s) to ${resolvedOutDir}`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(typeof result.artifact === "string" ? result.artifact : stableStringify(result.artifact));
|
|
148
|
+
return 0;
|
|
149
|
+
}
|