@topogram/cli 0.3.85 → 0.3.87
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/archive/unarchive.js +56 -5
- package/src/catalog/source.js +52 -1
- package/src/cli/commands/emit/snapshot-input.js +111 -0
- package/src/cli/commands/emit.js +11 -1
- package/src/cli/commands/extractor.js +150 -13
- package/src/cli/commands/import/plan.js +101 -4
- package/src/cli/commands/query/workspace.js +2 -67
- package/src/extraction-context.js +79 -0
- package/src/extractor/packages.js +9 -2
- package/src/github-client.js +56 -2
- package/src/import/core/shared/files.js +21 -0
- package/src/remote-payload-limits.js +40 -0
- package/src/template-trust/constants.js +1 -1
- package/src/template-trust/policy.js +3 -4
- package/src/template-trust/status.js +2 -4
- package/src/topogram-config.js +47 -0
- package/src/workflows/reconcile/adoption-plan/outputs.js +31 -4
- package/src/workflows/shared.js +21 -0
package/src/topogram-config.js
CHANGED
|
@@ -83,6 +83,11 @@ export const DEFAULT_TOPOGRAM_CONFIG = {
|
|
|
83
83
|
consumers: DEFAULT_RELEASE_CONSUMER_REPOS,
|
|
84
84
|
workflows: DEFAULT_RELEASE_CONSUMER_WORKFLOWS,
|
|
85
85
|
workflowJobs: DEFAULT_RELEASE_CONSUMER_WORKFLOW_JOBS
|
|
86
|
+
},
|
|
87
|
+
limits: {
|
|
88
|
+
remoteFetchMaxBytes: 5 * 1024 * 1024,
|
|
89
|
+
catalogFetchMaxBytes: null,
|
|
90
|
+
githubFetchMaxBytes: null
|
|
86
91
|
}
|
|
87
92
|
};
|
|
88
93
|
|
|
@@ -93,6 +98,7 @@ export const DEFAULT_CATALOG_SOURCE = `https://raw.githubusercontent.com/${DEFAU
|
|
|
93
98
|
* @property {{ owner: string, repo: string }} github
|
|
94
99
|
* @property {{ owner: string, repo: string, ref: string, path: string, source: string|null }} catalog
|
|
95
100
|
* @property {{ consumers: string[], workflows: Record<string, string>, workflowJobs: Record<string, string[]> }} release
|
|
101
|
+
* @property {{ remoteFetchMaxBytes: number, catalogFetchMaxBytes: number|null, githubFetchMaxBytes: number|null }} limits
|
|
96
102
|
*/
|
|
97
103
|
|
|
98
104
|
/**
|
|
@@ -154,6 +160,18 @@ function parseJsonEnv(value) {
|
|
|
154
160
|
return JSON.parse(value);
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
/**
|
|
164
|
+
* @param {string|null|undefined} value
|
|
165
|
+
* @returns {number|null}
|
|
166
|
+
*/
|
|
167
|
+
function parsePositiveIntegerEnv(value) {
|
|
168
|
+
if (!value) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
172
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
157
175
|
/**
|
|
158
176
|
* @param {Record<string, any>} fileConfig
|
|
159
177
|
* @returns {Record<string, any>}
|
|
@@ -178,6 +196,11 @@ function envConfig(fileConfig = {}) {
|
|
|
178
196
|
consumers: consumers || fileConfig.release?.consumers,
|
|
179
197
|
workflows: workflows || fileConfig.release?.workflows,
|
|
180
198
|
workflowJobs: workflowJobs || fileConfig.release?.workflowJobs
|
|
199
|
+
},
|
|
200
|
+
limits: {
|
|
201
|
+
remoteFetchMaxBytes: parsePositiveIntegerEnv(process.env.TOPOGRAM_REMOTE_FETCH_MAX_BYTES) || fileConfig.limits?.remoteFetchMaxBytes,
|
|
202
|
+
catalogFetchMaxBytes: parsePositiveIntegerEnv(process.env.TOPOGRAM_CATALOG_FETCH_MAX_BYTES) || fileConfig.limits?.catalogFetchMaxBytes,
|
|
203
|
+
githubFetchMaxBytes: parsePositiveIntegerEnv(process.env.TOPOGRAM_GITHUB_FETCH_MAX_BYTES) || fileConfig.limits?.githubFetchMaxBytes
|
|
181
204
|
}
|
|
182
205
|
};
|
|
183
206
|
}
|
|
@@ -235,6 +258,16 @@ function normalizeStringListMap(value, fallback) {
|
|
|
235
258
|
return output;
|
|
236
259
|
}
|
|
237
260
|
|
|
261
|
+
/**
|
|
262
|
+
* @param {unknown} value
|
|
263
|
+
* @param {number|null} fallback
|
|
264
|
+
* @returns {number|null}
|
|
265
|
+
*/
|
|
266
|
+
function normalizePositiveInteger(value, fallback) {
|
|
267
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
268
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
269
|
+
}
|
|
270
|
+
|
|
238
271
|
/**
|
|
239
272
|
* @param {string} cwd
|
|
240
273
|
* @returns {TopogramRuntimeConfig}
|
|
@@ -258,6 +291,20 @@ export function topogramRuntimeConfig(cwd = process.cwd()) {
|
|
|
258
291
|
consumers: normalizeStringList(overrides.release.consumers, DEFAULT_TOPOGRAM_CONFIG.release.consumers),
|
|
259
292
|
workflows: normalizeStringMap(overrides.release.workflows, DEFAULT_TOPOGRAM_CONFIG.release.workflows),
|
|
260
293
|
workflowJobs: normalizeStringListMap(overrides.release.workflowJobs, DEFAULT_TOPOGRAM_CONFIG.release.workflowJobs)
|
|
294
|
+
},
|
|
295
|
+
limits: {
|
|
296
|
+
remoteFetchMaxBytes: normalizePositiveInteger(
|
|
297
|
+
overrides.limits.remoteFetchMaxBytes,
|
|
298
|
+
DEFAULT_TOPOGRAM_CONFIG.limits.remoteFetchMaxBytes
|
|
299
|
+
) || DEFAULT_TOPOGRAM_CONFIG.limits.remoteFetchMaxBytes,
|
|
300
|
+
catalogFetchMaxBytes: normalizePositiveInteger(
|
|
301
|
+
overrides.limits.catalogFetchMaxBytes,
|
|
302
|
+
DEFAULT_TOPOGRAM_CONFIG.limits.catalogFetchMaxBytes
|
|
303
|
+
),
|
|
304
|
+
githubFetchMaxBytes: normalizePositiveInteger(
|
|
305
|
+
overrides.limits.githubFetchMaxBytes,
|
|
306
|
+
DEFAULT_TOPOGRAM_CONFIG.limits.githubFetchMaxBytes
|
|
307
|
+
)
|
|
261
308
|
}
|
|
262
309
|
};
|
|
263
310
|
}
|
|
@@ -12,6 +12,27 @@ import { readJsonIfExists, readTextIfExists } from "../../shared.js";
|
|
|
12
12
|
import { canonicalRelativePathForItem } from "./paths.js";
|
|
13
13
|
import { applyProjectionAuthPatchToTopogram } from "./projection-patches.js";
|
|
14
14
|
|
|
15
|
+
/** @param {string} rootDir @param {string} relativePath @param {string} fieldName @returns {{ absolutePath: string, relativePath: string }} */
|
|
16
|
+
function resolveContainedTopoPath(rootDir, relativePath, fieldName) {
|
|
17
|
+
const rawPath = String(relativePath || "").replaceAll("\\", "/");
|
|
18
|
+
if (!rawPath.trim()) {
|
|
19
|
+
throw new Error(`Adoption plan ${fieldName} must be a non-empty relative path.`);
|
|
20
|
+
}
|
|
21
|
+
if (rawPath.includes("\0") || path.isAbsolute(rawPath) || /^[A-Za-z]:\//.test(rawPath)) {
|
|
22
|
+
throw new Error(`Adoption plan ${fieldName} must be relative to the topo workspace: ${relativePath}`);
|
|
23
|
+
}
|
|
24
|
+
const absoluteRoot = path.resolve(rootDir);
|
|
25
|
+
const absolutePath = path.resolve(absoluteRoot, rawPath);
|
|
26
|
+
const relativeToRoot = path.relative(absoluteRoot, absolutePath);
|
|
27
|
+
if (!relativeToRoot || relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) {
|
|
28
|
+
throw new Error(`Adoption plan ${fieldName} escapes the topo workspace: ${relativePath}`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
absolutePath,
|
|
32
|
+
relativePath: relativeToRoot.replaceAll(path.sep, "/")
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
/** @param {WorkspacePaths} paths @returns {any} */
|
|
16
37
|
export function readAdoptionPlan(paths) {
|
|
17
38
|
return readJsonIfExists(path.join(paths.topogramRoot, "candidates", "reconcile", "adoption-plan.json"));
|
|
@@ -55,11 +76,15 @@ export function buildCanonicalAdoptionOutputs(paths, candidateFiles, planItems,
|
|
|
55
76
|
if (item.suggested_action === "skip_duplicate_shape") {
|
|
56
77
|
continue;
|
|
57
78
|
}
|
|
58
|
-
const
|
|
59
|
-
if (!
|
|
79
|
+
const rawRelativeCanonicalPath = item.canonical_rel_path || canonicalRelativePathForItem(item.kind, item.item);
|
|
80
|
+
if (!rawRelativeCanonicalPath) {
|
|
60
81
|
continue;
|
|
61
82
|
}
|
|
62
|
-
const canonicalPath =
|
|
83
|
+
const { absolutePath: canonicalPath, relativePath: relativeCanonicalPath } = resolveContainedTopoPath(
|
|
84
|
+
paths.topogramRoot,
|
|
85
|
+
rawRelativeCanonicalPath,
|
|
86
|
+
"canonical_rel_path"
|
|
87
|
+
);
|
|
63
88
|
if (item.suggested_action === "apply_doc_link_patch") {
|
|
64
89
|
const baseContents = files[relativeCanonicalPath] || (fs.existsSync(canonicalPath) ? fs.readFileSync(canonicalPath, "utf8") : null);
|
|
65
90
|
if (!baseContents) {
|
|
@@ -113,7 +138,9 @@ export function buildCanonicalAdoptionOutputs(paths, candidateFiles, planItems,
|
|
|
113
138
|
}
|
|
114
139
|
const candidateContents =
|
|
115
140
|
candidateFiles[item.source_path] ||
|
|
116
|
-
(item.source_path
|
|
141
|
+
(item.source_path
|
|
142
|
+
? readTextIfExists(resolveContainedTopoPath(paths.topogramRoot, item.source_path, "source_path").absolutePath)
|
|
143
|
+
: null);
|
|
117
144
|
if (!candidateContents) {
|
|
118
145
|
continue;
|
|
119
146
|
}
|
package/src/workflows/shared.js
CHANGED
|
@@ -79,15 +79,36 @@ export function listFilesRecursive(rootDir, predicate = () => true, options = {}
|
|
|
79
79
|
return [];
|
|
80
80
|
}
|
|
81
81
|
const ignoredDirs = options.ignoredDirs || DEFAULT_IGNORED_DIRS;
|
|
82
|
+
let rootRealPath;
|
|
83
|
+
try {
|
|
84
|
+
rootRealPath = fs.realpathSync(rootDir);
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const visitedDirs = new Set([rootRealPath]);
|
|
82
89
|
/** @type {any[]} */
|
|
83
90
|
const files = [];
|
|
84
91
|
const walk = (/** @type {any} */ currentDir) => {
|
|
85
92
|
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
86
93
|
const childPath = path.join(currentDir, entry.name);
|
|
94
|
+
if (entry.isSymbolicLink()) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
87
97
|
if (entry.isDirectory()) {
|
|
88
98
|
if (ignoredDirs.has(entry.name)) {
|
|
89
99
|
continue;
|
|
90
100
|
}
|
|
101
|
+
let childRealPath;
|
|
102
|
+
try {
|
|
103
|
+
childRealPath = fs.realpathSync(childPath);
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const relativeToRoot = path.relative(rootRealPath, childRealPath);
|
|
108
|
+
if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot) || visitedDirs.has(childRealPath)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
visitedDirs.add(childRealPath);
|
|
91
112
|
walk(childPath);
|
|
92
113
|
continue;
|
|
93
114
|
}
|