@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.40",
3
+ "version": "0.3.41",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
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
- ? "Review implementation/ and run topogram trust template in the generated starter."
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: "Review implementation/ and run topogram trust template in the generated starter.",
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
- console.log("Run `topogram trust diff` to review implementation changes, then `topogram trust template` to trust the current files.");
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
- console.log("After review, run `topogram trust template` to trust the current files.");
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);
@@ -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(`Template '${templateId}' contains unsupported symlink '${relativeRoot}'.`);
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(`Template '${templateId}' contains unsupported symlink '${relativePath}'.`);
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(`Template '${manifest.id}' contains unsupported symlink 'topogram'.`);
380
+ throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, "topogram"));
372
381
  }
373
382
  if (fs.existsSync(projectConfigPath) && fs.lstatSync(projectConfigPath).isSymbolicLink()) {
374
- throw new Error(`Template '${manifest.id}' contains unsupported symlink 'topogram.project.json'.`);
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(`Template '${manifest.id}' contains unsupported symlink 'implementation'.`);
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);
@@ -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(`Template implementation contains unsupported symlink '${relativePath}'.`);
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(`Template implementation module '${implementation.module}' must be under implementation/.`);
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}. Review implementation/ and run 'topogram trust template' to trust the current files.`
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
- const currentContent = hashImplementationContent(implementationInfo.configDir);
494
- contentStatus.trustedDigest = trustRecord.content.digest;
495
- contentStatus.currentDigest = currentContent.digest;
496
- const trustedByPath = new Map((trustRecord.content.files || []).map((file) => [file.path, file]));
497
- const currentByPath = new Map(currentContent.files.map((file) => [file.path, file]));
498
- const diff = diffContentFiles(trustedByPath, currentByPath);
499
- contentStatus.added = diff.added;
500
- contentStatus.removed = diff.removed;
501
- contentStatus.changed = diff.changed;
502
- if (trustRecord.content.digest !== currentContent.digest) {
503
- issues.push(`${TEMPLATE_TRUST_FILE} implementation content changed since it was last trusted`);
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
- const currentContent = hashImplementationContent(implementationInfo.configDir);
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 }>} */