@topogram/cli 0.3.40 → 0.3.41
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/cli.js +29 -5
- package/src/new-project.js +15 -6
- package/src/template-trust.js +66 -18
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
getTemplateTrustStatus,
|
|
33
33
|
implementationRequiresTrust,
|
|
34
34
|
TEMPLATE_TRUST_FILE,
|
|
35
|
+
templateTrustRecoveryGuidance,
|
|
35
36
|
validateProjectImplementationTrust,
|
|
36
37
|
writeTemplateTrustRecord
|
|
37
38
|
} from "./template-trust.js";
|
|
@@ -5951,6 +5952,15 @@ function diagnosticForTemplateCreateFailure(message, templateSpec, step) {
|
|
|
5951
5952
|
step
|
|
5952
5953
|
});
|
|
5953
5954
|
}
|
|
5955
|
+
if (message.includes("unsupported symlink")) {
|
|
5956
|
+
return templateCheckDiagnostic({
|
|
5957
|
+
code: "template_symlink_unsupported",
|
|
5958
|
+
message,
|
|
5959
|
+
path: path.isAbsolute(templateSpec) ? templateSpec : null,
|
|
5960
|
+
suggestedFix: "Replace template symlinks with real files or directories, then rerun `topogram new` or `topogram template check`.",
|
|
5961
|
+
step
|
|
5962
|
+
});
|
|
5963
|
+
}
|
|
5954
5964
|
return templateCheckDiagnostic({
|
|
5955
5965
|
code: "template_create_failed",
|
|
5956
5966
|
message,
|
|
@@ -5968,13 +5978,15 @@ function diagnosticForTemplateCreateFailure(message, templateSpec, step) {
|
|
|
5968
5978
|
*/
|
|
5969
5979
|
function diagnosticForStarterCheckFailure(error, step, configPath) {
|
|
5970
5980
|
const locFile = typeof error?.loc?.file === "string" ? error.loc.file : null;
|
|
5971
|
-
const isTrust = error.message.includes(TEMPLATE_TRUST_FILE)
|
|
5981
|
+
const isTrust = error.message.includes(TEMPLATE_TRUST_FILE) ||
|
|
5982
|
+
error.message.includes("unsupported symlink") ||
|
|
5983
|
+
error.message.includes("must be under implementation/");
|
|
5972
5984
|
return templateCheckDiagnostic({
|
|
5973
5985
|
code: isTrust ? "template_trust_invalid" : "starter_check_failed",
|
|
5974
5986
|
message: error.message,
|
|
5975
5987
|
path: locFile || configPath,
|
|
5976
5988
|
suggestedFix: isTrust
|
|
5977
|
-
?
|
|
5989
|
+
? templateTrustRecoveryGuidance(error.message)
|
|
5978
5990
|
: "Fix the generated Topogram source or topogram.project.json so topogram check passes.",
|
|
5979
5991
|
step
|
|
5980
5992
|
});
|
|
@@ -6176,7 +6188,7 @@ function buildTemplateCheckPayload(templateSpec) {
|
|
|
6176
6188
|
code: "template_trust_invalid",
|
|
6177
6189
|
message: issue,
|
|
6178
6190
|
path: trustStatus.trustPath,
|
|
6179
|
-
suggestedFix:
|
|
6191
|
+
suggestedFix: templateTrustRecoveryGuidance(issue),
|
|
6180
6192
|
step: "executable-implementation-trust"
|
|
6181
6193
|
}));
|
|
6182
6194
|
steps.push(templateCheckStep("executable-implementation-trust", trustStatus.ok, {
|
|
@@ -7853,7 +7865,10 @@ try {
|
|
|
7853
7865
|
console.log(`Removed: ${filePath}`);
|
|
7854
7866
|
}
|
|
7855
7867
|
if (!status.ok) {
|
|
7856
|
-
|
|
7868
|
+
const guidance = templateTrustRecoveryGuidance(status.issues);
|
|
7869
|
+
if (guidance) {
|
|
7870
|
+
console.log(guidance);
|
|
7871
|
+
}
|
|
7857
7872
|
}
|
|
7858
7873
|
}
|
|
7859
7874
|
process.exit(status.ok ? 0 : 1);
|
|
@@ -7882,6 +7897,12 @@ try {
|
|
|
7882
7897
|
for (const issue of diff.status.issues) {
|
|
7883
7898
|
console.log(`Issue: ${issue}`);
|
|
7884
7899
|
}
|
|
7900
|
+
if (!diff.ok) {
|
|
7901
|
+
const guidance = templateTrustRecoveryGuidance(diff.status.issues);
|
|
7902
|
+
if (guidance) {
|
|
7903
|
+
console.log(guidance);
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7885
7906
|
} else {
|
|
7886
7907
|
console.log(diff.ok ? "Template trust diff: no implementation changes." : "Template trust diff: review required");
|
|
7887
7908
|
for (const file of diff.files) {
|
|
@@ -7906,7 +7927,10 @@ try {
|
|
|
7906
7927
|
}
|
|
7907
7928
|
if (!diff.ok) {
|
|
7908
7929
|
console.log("");
|
|
7909
|
-
|
|
7930
|
+
const guidance = templateTrustRecoveryGuidance(diff.status.issues);
|
|
7931
|
+
if (guidance) {
|
|
7932
|
+
console.log(guidance);
|
|
7933
|
+
}
|
|
7910
7934
|
}
|
|
7911
7935
|
}
|
|
7912
7936
|
process.exit(diff.ok ? 0 : 1);
|
package/src/new-project.js
CHANGED
|
@@ -32,6 +32,15 @@ const SURFACE_ORDER = new Map([
|
|
|
32
32
|
["native", 40]
|
|
33
33
|
]);
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} templateId
|
|
37
|
+
* @param {string} relativePath
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function unsupportedTemplateSymlinkMessage(templateId, relativePath) {
|
|
41
|
+
return `Template '${templateId}' contains unsupported symlink '${relativePath}'. Template packs must copy real files because Topogram records hashes for copied topogram/ and implementation/ content; symlinks can point outside the trusted template root. Replace the symlink with a real file or directory before running topogram new or topogram template check.`;
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
/**
|
|
36
45
|
* @typedef {Object} CreateNewProjectOptions
|
|
37
46
|
* @property {string} targetPath
|
|
@@ -342,7 +351,7 @@ function assertTemplateTreeHasNoSymlinks(root, currentDir, label, templateId) {
|
|
|
342
351
|
const rootStat = fs.lstatSync(currentDir);
|
|
343
352
|
const relativeRoot = path.relative(root, currentDir).replace(/\\/g, "/") || label;
|
|
344
353
|
if (rootStat.isSymbolicLink()) {
|
|
345
|
-
throw new Error(
|
|
354
|
+
throw new Error(unsupportedTemplateSymlinkMessage(templateId, relativeRoot));
|
|
346
355
|
}
|
|
347
356
|
if (!rootStat.isDirectory()) {
|
|
348
357
|
return;
|
|
@@ -351,7 +360,7 @@ function assertTemplateTreeHasNoSymlinks(root, currentDir, label, templateId) {
|
|
|
351
360
|
const entryPath = path.join(currentDir, entry.name);
|
|
352
361
|
const relativePath = path.relative(root, entryPath).replace(/\\/g, "/");
|
|
353
362
|
if (entry.isSymbolicLink()) {
|
|
354
|
-
throw new Error(
|
|
363
|
+
throw new Error(unsupportedTemplateSymlinkMessage(templateId, relativePath));
|
|
355
364
|
}
|
|
356
365
|
if (entry.isDirectory()) {
|
|
357
366
|
assertTemplateTreeHasNoSymlinks(root, entryPath, label, templateId);
|
|
@@ -368,10 +377,10 @@ function validateTemplateRoot(templateRoot) {
|
|
|
368
377
|
const topogramRoot = path.join(templateRoot, "topogram");
|
|
369
378
|
const projectConfigPath = path.join(templateRoot, "topogram.project.json");
|
|
370
379
|
if (fs.existsSync(topogramRoot) && fs.lstatSync(topogramRoot).isSymbolicLink()) {
|
|
371
|
-
throw new Error(
|
|
380
|
+
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, "topogram"));
|
|
372
381
|
}
|
|
373
382
|
if (fs.existsSync(projectConfigPath) && fs.lstatSync(projectConfigPath).isSymbolicLink()) {
|
|
374
|
-
throw new Error(
|
|
383
|
+
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, "topogram.project.json"));
|
|
375
384
|
}
|
|
376
385
|
if (!fs.existsSync(topogramRoot) || !fs.statSync(topogramRoot).isDirectory()) {
|
|
377
386
|
throw new Error(`Template '${manifest.id}' is missing topogram/.`);
|
|
@@ -383,7 +392,7 @@ function validateTemplateRoot(templateRoot) {
|
|
|
383
392
|
if (manifest.includesExecutableImplementation) {
|
|
384
393
|
const implementationRoot = path.join(templateRoot, "implementation");
|
|
385
394
|
if (fs.existsSync(implementationRoot) && fs.lstatSync(implementationRoot).isSymbolicLink()) {
|
|
386
|
-
throw new Error(
|
|
395
|
+
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, "implementation"));
|
|
387
396
|
}
|
|
388
397
|
if (!fs.existsSync(implementationRoot) || !fs.statSync(implementationRoot).isDirectory()) {
|
|
389
398
|
throw new Error(
|
|
@@ -1169,7 +1178,7 @@ function collectFiles(root, currentDir, files) {
|
|
|
1169
1178
|
}
|
|
1170
1179
|
const entryPath = path.join(currentDir, entry.name);
|
|
1171
1180
|
if (entry.isSymbolicLink()) {
|
|
1172
|
-
throw new Error(`Template-owned files cannot include symlink '${path.relative(root, entryPath).replace(/\\/g, "/")}'.`);
|
|
1181
|
+
throw new Error(`Template-owned files cannot include symlink '${path.relative(root, entryPath).replace(/\\/g, "/")}'. Template-owned files must be real files so Topogram can hash the exact content being trusted. Replace the symlink with a real file, then run topogram trust status, topogram trust diff, and topogram trust template after review.`);
|
|
1173
1182
|
}
|
|
1174
1183
|
if (entry.isDirectory()) {
|
|
1175
1184
|
collectFiles(root, entryPath, files);
|
package/src/template-trust.js
CHANGED
|
@@ -19,6 +19,7 @@ export const TEMPLATE_TRUST_POLICY = "topogram-template-executable-implementatio
|
|
|
19
19
|
|
|
20
20
|
const IGNORED_IMPLEMENTATION_ENTRIES = new Set([".DS_Store", "node_modules", ".tmp"]);
|
|
21
21
|
const MAX_TEXT_DIFF_BYTES = 256 * 1024;
|
|
22
|
+
const TRUST_REVIEW_COMMANDS = "`topogram trust status`, `topogram trust diff`, and `topogram trust template`";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* @param {string} parent
|
|
@@ -46,6 +47,45 @@ function normalizeRelativePath(value) {
|
|
|
46
47
|
return value.replace(/\\/g, "/");
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} relativePath
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function unsupportedImplementationSymlinkMessage(relativePath) {
|
|
55
|
+
return `Template implementation contains unsupported symlink '${relativePath}'. Implementation trust hashes real files under implementation/; symlinks can point outside the trusted root. Replace symlinks with real files under implementation/, then run ${TRUST_REVIEW_COMMANDS} after review.`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} modulePath
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function implementationOutsideRootMessage(modulePath) {
|
|
63
|
+
return `Template implementation module '${modulePath}' must be under implementation/ for template-attached projects. Keep executable template code inside implementation/ so the trust record covers what topogram generate may load. Move the module back under implementation/, then run ${TRUST_REVIEW_COMMANDS} after review.`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string|string[]} issueOrIssues
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function templateTrustRecoveryGuidance(issueOrIssues) {
|
|
71
|
+
const issues = Array.isArray(issueOrIssues) ? issueOrIssues : [issueOrIssues];
|
|
72
|
+
const text = issues.join("\n");
|
|
73
|
+
if (issues.length > 0 && issues.every((issue) =>
|
|
74
|
+
issue.includes("topogram trust status") &&
|
|
75
|
+
issue.includes("topogram trust diff") &&
|
|
76
|
+
issue.includes("topogram trust template")
|
|
77
|
+
)) {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
if (text.includes("unsupported symlink")) {
|
|
81
|
+
return `Replace symlinks with real files under implementation/, then run ${TRUST_REVIEW_COMMANDS} after review.`;
|
|
82
|
+
}
|
|
83
|
+
if (text.includes("must be under implementation/")) {
|
|
84
|
+
return `Keep executable template code under implementation/ so it can be hashed and trusted; move the module back under implementation/, then run ${TRUST_REVIEW_COMMANDS} after review.`;
|
|
85
|
+
}
|
|
86
|
+
return `Run \`topogram trust status\` and \`topogram trust diff\` to review implementation changes; after review, run \`topogram trust template\` to trust the current files.`;
|
|
87
|
+
}
|
|
88
|
+
|
|
49
89
|
/**
|
|
50
90
|
* @param {string} value
|
|
51
91
|
* @returns {string}
|
|
@@ -187,7 +227,7 @@ function collectImplementationFiles(implementationRoot, currentDir, files) {
|
|
|
187
227
|
const entryPath = path.join(currentDir, entry.name);
|
|
188
228
|
const relativePath = normalizeRelativePath(path.relative(implementationRoot, entryPath));
|
|
189
229
|
if (entry.isSymbolicLink()) {
|
|
190
|
-
throw new Error(
|
|
230
|
+
throw new Error(unsupportedImplementationSymlinkMessage(relativePath));
|
|
191
231
|
}
|
|
192
232
|
if (entry.isDirectory()) {
|
|
193
233
|
collectImplementationFiles(implementationRoot, entryPath, files);
|
|
@@ -343,7 +383,7 @@ export function writeTemplateTrustRecord(configDir, projectConfig) {
|
|
|
343
383
|
};
|
|
344
384
|
if (!implementationModuleIsUnderRoot(implementationInfo)) {
|
|
345
385
|
const implementation = implementationTrustFingerprint(implementationConfig);
|
|
346
|
-
throw new Error(
|
|
386
|
+
throw new Error(implementationOutsideRootMessage(implementation.module));
|
|
347
387
|
}
|
|
348
388
|
const implementation = implementationTrustFingerprint(implementationConfig);
|
|
349
389
|
const record = buildTrustRecord(configDir, projectConfig, implementation);
|
|
@@ -362,8 +402,9 @@ export function assertTrustedImplementation(implementationInfo, projectConfig =
|
|
|
362
402
|
return;
|
|
363
403
|
}
|
|
364
404
|
const firstIssue = status.issues[0] || "implementation trust is invalid";
|
|
405
|
+
const guidance = templateTrustRecoveryGuidance(firstIssue);
|
|
365
406
|
throw new Error(
|
|
366
|
-
`${firstIssue}.
|
|
407
|
+
guidance ? `${firstIssue}. ${guidance}` : firstIssue
|
|
367
408
|
);
|
|
368
409
|
}
|
|
369
410
|
|
|
@@ -451,9 +492,7 @@ export function getTemplateTrustStatus(implementationInfo, projectConfig = null)
|
|
|
451
492
|
const contentStatus = { trustedDigest: null, currentDigest: null, added: [], removed: [], changed: [] };
|
|
452
493
|
|
|
453
494
|
if (templateAttached && !moduleInsideImplementation) {
|
|
454
|
-
issues.push(
|
|
455
|
-
`Template implementation module '${fingerprint.module}' must be under implementation/ for template-attached projects`
|
|
456
|
-
);
|
|
495
|
+
issues.push(implementationOutsideRootMessage(fingerprint.module));
|
|
457
496
|
}
|
|
458
497
|
|
|
459
498
|
if (!trustRecord) {
|
|
@@ -490,17 +529,21 @@ export function getTemplateTrustStatus(implementationInfo, projectConfig = null)
|
|
|
490
529
|
} else if (trustRecord.content.algorithm !== "sha256") {
|
|
491
530
|
issues.push(`${TEMPLATE_TRUST_FILE} uses unsupported content hash algorithm '${trustRecord.content.algorithm}'`);
|
|
492
531
|
} else {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
532
|
+
try {
|
|
533
|
+
const currentContent = hashImplementationContent(implementationInfo.configDir);
|
|
534
|
+
contentStatus.trustedDigest = trustRecord.content.digest;
|
|
535
|
+
contentStatus.currentDigest = currentContent.digest;
|
|
536
|
+
const trustedByPath = new Map((trustRecord.content.files || []).map((file) => [file.path, file]));
|
|
537
|
+
const currentByPath = new Map(currentContent.files.map((file) => [file.path, file]));
|
|
538
|
+
const diff = diffContentFiles(trustedByPath, currentByPath);
|
|
539
|
+
contentStatus.added = diff.added;
|
|
540
|
+
contentStatus.removed = diff.removed;
|
|
541
|
+
contentStatus.changed = diff.changed;
|
|
542
|
+
if (trustRecord.content.digest !== currentContent.digest) {
|
|
543
|
+
issues.push(`${TEMPLATE_TRUST_FILE} implementation content changed since it was last trusted`);
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
504
547
|
}
|
|
505
548
|
}
|
|
506
549
|
}
|
|
@@ -538,7 +581,12 @@ export function getTemplateTrustDiff(implementationInfo, projectConfig = null) {
|
|
|
538
581
|
if (!status.requiresTrust || !status.trustRecord?.content) {
|
|
539
582
|
return { ok: status.ok, requiresTrust: status.requiresTrust, status, files: [] };
|
|
540
583
|
}
|
|
541
|
-
|
|
584
|
+
let currentContent;
|
|
585
|
+
try {
|
|
586
|
+
currentContent = hashImplementationContent(implementationInfo.configDir);
|
|
587
|
+
} catch (_error) {
|
|
588
|
+
return { ok: false, requiresTrust: status.requiresTrust, status, files: [] };
|
|
589
|
+
}
|
|
542
590
|
const trustedByPath = new Map((status.trustRecord.content.files || []).map((file) => [file.path, file]));
|
|
543
591
|
const currentByPath = new Map(currentContent.files.map((file) => [file.path, file]));
|
|
544
592
|
/** @type {Array<{ path: string, kind: "added"|"removed"|"changed", trusted: { path: string, sha256: string|null, size: number|null }|null, current: { path: string, sha256: string|null, size: number|null, binary: boolean, diffOmitted: boolean }|null, binary: boolean, diffOmitted: boolean, unifiedDiff: string|null }>} */
|