@pennixrv/trellis 0.7.1-beta.1 → 0.7.1-beta.11

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.
Files changed (127) hide show
  1. package/README.md +10 -7
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +6 -4
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/ablate.d.ts.map +1 -1
  6. package/dist/commands/ablate.js +2 -0
  7. package/dist/commands/ablate.js.map +1 -1
  8. package/dist/commands/backup.d.ts +4 -0
  9. package/dist/commands/backup.d.ts.map +1 -0
  10. package/dist/commands/backup.js +75 -0
  11. package/dist/commands/backup.js.map +1 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +687 -676
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/mem.d.ts.map +1 -1
  16. package/dist/commands/mem.js +49 -7
  17. package/dist/commands/mem.js.map +1 -1
  18. package/dist/commands/task-progress.d.ts +1 -0
  19. package/dist/commands/task-progress.d.ts.map +1 -1
  20. package/dist/commands/task-progress.js +67 -25
  21. package/dist/commands/task-progress.js.map +1 -1
  22. package/dist/commands/teamwork/spawn.d.ts.map +1 -1
  23. package/dist/commands/teamwork/spawn.js +5 -2
  24. package/dist/commands/teamwork/spawn.js.map +1 -1
  25. package/dist/commands/teamwork/teammate-adapters.d.ts +0 -2
  26. package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
  27. package/dist/commands/teamwork/teammate-adapters.js +0 -2
  28. package/dist/commands/teamwork/teammate-adapters.js.map +1 -1
  29. package/dist/commands/teamwork/teammate-launch.d.ts +1 -2
  30. package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
  31. package/dist/commands/teamwork/teammate-launch.js +0 -2
  32. package/dist/commands/teamwork/teammate-launch.js.map +1 -1
  33. package/dist/commands/uninstall.d.ts.map +1 -1
  34. package/dist/commands/uninstall.js +92 -84
  35. package/dist/commands/uninstall.js.map +1 -1
  36. package/dist/commands/update.d.ts.map +1 -1
  37. package/dist/commands/update.js +589 -591
  38. package/dist/commands/update.js.map +1 -1
  39. package/dist/commands/workflow.d.ts.map +1 -1
  40. package/dist/commands/workflow.js +94 -89
  41. package/dist/commands/workflow.js.map +1 -1
  42. package/dist/configurators/shared.js +2 -2
  43. package/dist/configurators/shared.js.map +1 -1
  44. package/dist/constants/paths.d.ts +3 -10
  45. package/dist/constants/paths.d.ts.map +1 -1
  46. package/dist/constants/paths.js +4 -13
  47. package/dist/constants/paths.js.map +1 -1
  48. package/dist/migrations/manifests/0.7.1-beta.10.json +9 -0
  49. package/dist/migrations/manifests/0.7.1-beta.11.json +9 -0
  50. package/dist/migrations/manifests/0.7.1-beta.2.json +522 -0
  51. package/dist/migrations/manifests/0.7.1-beta.3.json +9 -0
  52. package/dist/migrations/manifests/0.7.1-beta.4.json +9 -0
  53. package/dist/migrations/manifests/0.7.1-beta.5.json +9 -0
  54. package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
  55. package/dist/migrations/manifests/0.7.1-beta.7.json +8 -0
  56. package/dist/migrations/manifests/0.7.1-beta.8.json +481 -0
  57. package/dist/migrations/manifests/0.7.1-beta.9.json +191 -0
  58. package/dist/templates/claude/hooks/statusline.py +9 -4
  59. package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +2 -0
  60. package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +2 -0
  61. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
  62. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
  63. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
  64. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
  65. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
  66. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
  67. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
  68. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +2 -1
  69. package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +2 -2
  70. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +4 -0
  71. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +4 -2
  72. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +2 -2
  73. package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +7 -2
  74. package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +3 -1
  75. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +11 -6
  76. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +50 -6
  77. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +49 -9
  78. package/dist/templates/common/commands/start.md +2 -2
  79. package/dist/templates/common/skills/before-dev.md +1 -4
  80. package/dist/templates/common/skills/break-loop.md +7 -18
  81. package/dist/templates/common/skills/check.md +7 -11
  82. package/dist/templates/common/skills/{brainstorm.md → request-intake.md} +5 -3
  83. package/dist/templates/common/skills/update-spec.md +5 -7
  84. package/dist/templates/omp/extensions/trellis/index.ts.txt +1 -1
  85. package/dist/templates/opencode/lib/session-utils.js +301 -209
  86. package/dist/templates/snow/SNOW.md +1 -1
  87. package/dist/templates/trellis/agents/teammate.md +16 -5
  88. package/dist/templates/trellis/index.d.ts +3 -0
  89. package/dist/templates/trellis/index.d.ts.map +1 -1
  90. package/dist/templates/trellis/index.js +6 -0
  91. package/dist/templates/trellis/index.js.map +1 -1
  92. package/dist/templates/trellis/scripts/add_session.py +1 -1
  93. package/dist/templates/trellis/scripts/common/active_task.py +3 -47
  94. package/dist/templates/trellis/scripts/common/paths.py +9 -25
  95. package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
  96. package/dist/templates/trellis/scripts/common/task_context.py +11 -51
  97. package/dist/templates/trellis/scripts/common/task_continuity.py +63 -64
  98. package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
  99. package/dist/templates/trellis/scripts/common/task_move.py +345 -0
  100. package/dist/templates/trellis/scripts/common/task_store.py +28 -55
  101. package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
  102. package/dist/templates/trellis/scripts/common/task_workflow.py +72 -31
  103. package/dist/templates/trellis/scripts/common/tasks.py +6 -5
  104. package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
  105. package/dist/templates/trellis/scripts/task.py +41 -23
  106. package/dist/templates/trellis/workflow.md +4 -2
  107. package/dist/utils/ablation-store.d.ts +1 -1
  108. package/dist/utils/ablation-store.d.ts.map +1 -1
  109. package/dist/utils/ablation-store.js +58 -24
  110. package/dist/utils/ablation-store.js.map +1 -1
  111. package/dist/utils/asset-backup.d.ts +138 -0
  112. package/dist/utils/asset-backup.d.ts.map +1 -0
  113. package/dist/utils/asset-backup.js +514 -0
  114. package/dist/utils/asset-backup.js.map +1 -0
  115. package/dist/utils/atomic-write.d.ts +1 -1
  116. package/dist/utils/atomic-write.d.ts.map +1 -1
  117. package/dist/utils/atomic-write.js +6 -3
  118. package/dist/utils/atomic-write.js.map +1 -1
  119. package/dist/utils/manifest-prune.d.ts +3 -1
  120. package/dist/utils/manifest-prune.d.ts.map +1 -1
  121. package/dist/utils/manifest-prune.js +7 -3
  122. package/dist/utils/manifest-prune.js.map +1 -1
  123. package/dist/utils/template-hash.d.ts +5 -4
  124. package/dist/utils/template-hash.d.ts.map +1 -1
  125. package/dist/utils/template-hash.js +24 -17
  126. package/dist/utils/template-hash.js.map +1 -1
  127. package/package.json +2 -2
@@ -6,6 +6,7 @@ import chalk from "chalk";
6
6
  import figlet from "figlet";
7
7
  import inquirer from "inquirer";
8
8
  import { createWorkflowStructure } from "../configurators/workflow.js";
9
+ import { withAssetMaintenance } from "../utils/asset-backup.js";
9
10
  import { getInitToolChoices, resolveCliFlag, configurePlatform, getConfiguredPlatforms, getPlatformsWithPythonHooks, } from "../configurators/index.js";
10
11
  import { getPythonCommandForPlatform, setResolvedPythonCommand, } from "../configurators/shared.js";
11
12
  import { AI_TOOLS } from "../types/ai-tools.js";
@@ -23,6 +24,16 @@ import { fetchTemplateIndex, probeRegistryIndex, downloadTemplateById, downloadR
23
24
  import { setupProxy, maskProxyUrl } from "../utils/proxy.js";
24
25
  import { toPosix } from "../utils/posix.js";
25
26
  import { updateHashes } from "../utils/template-hash.js";
27
+ import { getTaskProgress } from "./task-progress.js";
28
+ function tasksAreEmpty(cwd) {
29
+ const progress = getTaskProgress(cwd);
30
+ return (!progress.partial &&
31
+ progress.planning +
32
+ progress.in_progress +
33
+ progress.completed +
34
+ progress.cancelled ===
35
+ 0);
36
+ }
26
37
  const MIN_PYTHON_MAJOR = 3;
27
38
  const MIN_PYTHON_MINOR = 9;
28
39
  const PYTHON_VERSION_RE = /Python (\d+)\.(\d+)/;
@@ -221,7 +232,7 @@ export function slugifyDeveloperName(name) {
221
232
  * anything. Shared by both creator bootstrap and joiner onboarding flows.
222
233
  */
223
234
  function writeTaskSkeleton(cwd, taskName, taskJson, prdContent) {
224
- const taskDir = path.join(cwd, PATHS.TASKS, taskName);
235
+ const taskDir = path.join(cwd, PATHS.TASKS, "planning", taskName);
225
236
  if (fs.existsSync(taskDir))
226
237
  return true; // idempotent
227
238
  try {
@@ -554,7 +565,7 @@ code every session.
554
565
 
555
566
  File layout (mention when they ask "where does what live"):
556
567
  - \`.trellis/.runtime/sessions/<session>.json\` — session active-task state, gitignored
557
- - \`.trellis/tasks/<task>/{implement,check}.jsonl\` — per-task context manifests
568
+ - \`.trellis/tasks/<task-dir>/{implement,check}.jsonl\` — per-task context manifests
558
569
  - \`.trellis/spec/\` — project-wide conventions (source of truth)
559
570
  - \`.trellis/workspace/${developer}/journal-*.md\` — their session log,
560
571
  rotated at ~2000 lines
@@ -827,746 +838,746 @@ function writeMonorepoConfig(cwd, packages) {
827
838
  fs.writeFileSync(configPath, content.trimEnd() + "\n" + lines.join("\n") + "\n", "utf-8");
828
839
  }
829
840
  export async function init(options) {
830
- // Refuse to run in $HOME — running here would scoop platform runtime data
831
- // (Claude/Codex/OpenCode session histories etc.) into the trellis hash
832
- // manifest, and a subsequent `trellis uninstall` would wipe it.
833
- if (isCwdHomedir() && !homedirBypassEnabled()) {
834
- console.error(chalk.red(homedirGuardMessage("init")));
835
- process.exit(1);
836
- }
837
- // Deprecated alias: --windsurf → --devin (Windsurf was renamed to Devin).
838
- // Normalize here too so programmatic callers (not just the CLI action) map
839
- // correctly. The CLI action prints the deprecation notice.
840
- if (options.windsurf) {
841
- options.devin = true;
842
- delete options.windsurf;
843
- }
844
- const cwd = process.cwd();
845
- const isFirstInit = !fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW));
846
- // Captured here (before createWorkflowStructure + init_developer run) so
847
- // the three-branch dispatch at the bottom can tell "fresh clone joiner"
848
- // (.trellis/ exists, .developer missing) apart from "creator first init".
849
- const hadDeveloperFileAtStart = fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW, FILE_NAMES.DEVELOPER));
850
- // Generate ASCII art banner dynamically using FIGlet "Rebel" font
851
- const banner = figlet.textSync("Trellis", { font: "Rebel" });
852
- console.log(chalk.cyan(`\n${banner.trimEnd()}`));
853
- console.log(chalk.gray("\n All-in-one AI framework & toolkit for Claude Code & Cursor\n"));
854
- // Set up proxy before any network calls
855
- const proxyUrl = setupProxy();
856
- if (proxyUrl) {
857
- console.log(chalk.gray(` Using proxy: ${maskProxyUrl(proxyUrl)}\n`));
858
- }
859
- // Set write mode based on options
860
- let writeMode = "ask";
861
- if (options.force) {
862
- writeMode = "force";
863
- console.log(chalk.gray("Mode: Force overwrite existing files\n"));
864
- }
865
- else if (options.skipExisting) {
866
- writeMode = "skip";
867
- console.log(chalk.gray("Mode: Skip existing files\n"));
868
- }
869
- else if (options.yes) {
870
- // -y implies non-interactive: never prompt on conflicts. Default to skip
871
- // (preserve user files) — explicit --force is required to overwrite.
872
- writeMode = "skip";
873
- console.log(chalk.gray("Mode: Non-interactive (skip existing files)\n"));
874
- }
875
- setWriteMode(writeMode);
876
- // Detect developer name from git config or options
877
- let developerName = options.user;
878
- if (!developerName) {
879
- // Only detect from git if current directory is a git repo
880
- const isGitRepo = fs.existsSync(path.join(cwd, ".git"));
881
- if (isGitRepo) {
882
- try {
883
- developerName = execSync("git config user.name", {
884
- cwd,
885
- encoding: "utf-8",
886
- }).trim();
887
- }
888
- catch {
889
- // Git not available or no user.name configured
841
+ return withAssetMaintenance(process.cwd(), async () => {
842
+ // Refuse to run in $HOME — running here would scoop platform runtime data
843
+ // (Claude/Codex/OpenCode session histories etc.) into the trellis hash
844
+ // manifest, and a subsequent `trellis uninstall` would wipe it.
845
+ if (isCwdHomedir() && !homedirBypassEnabled()) {
846
+ console.error(chalk.red(homedirGuardMessage("init")));
847
+ process.exit(1);
848
+ }
849
+ // Deprecated alias: --windsurf → --devin (Windsurf was renamed to Devin).
850
+ // Normalize here too so programmatic callers (not just the CLI action) map
851
+ // correctly. The CLI action prints the deprecation notice.
852
+ if (options.windsurf) {
853
+ options.devin = true;
854
+ delete options.windsurf;
855
+ }
856
+ const cwd = process.cwd();
857
+ const isFirstInit = !fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW));
858
+ // Captured here (before createWorkflowStructure + init_developer run) so
859
+ // the three-branch dispatch at the bottom can tell "fresh clone joiner"
860
+ // (.trellis/ exists, .developer missing) apart from "creator first init".
861
+ const hadDeveloperFileAtStart = fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW, FILE_NAMES.DEVELOPER));
862
+ // Generate ASCII art banner dynamically using FIGlet "Rebel" font
863
+ const banner = figlet.textSync("Trellis", { font: "Rebel" });
864
+ console.log(chalk.cyan(`\n${banner.trimEnd()}`));
865
+ console.log(chalk.gray("\n All-in-one AI framework & toolkit for Claude Code & Cursor\n"));
866
+ // Set up proxy before any network calls
867
+ const proxyUrl = setupProxy();
868
+ if (proxyUrl) {
869
+ console.log(chalk.gray(` Using proxy: ${maskProxyUrl(proxyUrl)}\n`));
870
+ }
871
+ // Set write mode based on options
872
+ let writeMode = "ask";
873
+ if (options.force) {
874
+ writeMode = "force";
875
+ console.log(chalk.gray("Mode: Force overwrite existing files\n"));
876
+ }
877
+ else if (options.skipExisting) {
878
+ writeMode = "skip";
879
+ console.log(chalk.gray("Mode: Skip existing files\n"));
880
+ }
881
+ else if (options.yes) {
882
+ // -y implies non-interactive: never prompt on conflicts. Default to skip
883
+ // (preserve user files) — explicit --force is required to overwrite.
884
+ writeMode = "skip";
885
+ console.log(chalk.gray("Mode: Non-interactive (skip existing files)\n"));
886
+ }
887
+ setWriteMode(writeMode);
888
+ // Detect developer name from git config or options
889
+ let developerName = options.user;
890
+ if (!developerName) {
891
+ // Only detect from git if current directory is a git repo
892
+ const isGitRepo = fs.existsSync(path.join(cwd, ".git"));
893
+ if (isGitRepo) {
894
+ try {
895
+ developerName = execSync("git config user.name", {
896
+ cwd,
897
+ encoding: "utf-8",
898
+ }).trim();
899
+ }
900
+ catch {
901
+ // Git not available or no user.name configured
902
+ }
890
903
  }
891
904
  }
892
- }
893
- if (developerName) {
894
- console.log(chalk.blue("👤 Developer:"), chalk.gray(developerName));
895
- }
896
- const { command: pythonCmd } = resolveSupportedPython();
897
- // ==========================================================================
898
- // Re-init fast path: skip full flow when .trellis/ already exists
899
- // ==========================================================================
900
- // Aborted-init recovery (issue #204): if .trellis/ exists but tasks/ is
901
- // empty, the previous init never reached bootstrap creation. Fall through
902
- // to the full flow so the main-dispatch tasksEmpty fallback fires —
903
- // handleReinit's joiner branch would otherwise mis-route the recovery.
904
- const tasksDirEarly = path.join(cwd, PATHS.TASKS);
905
- const tasksEmptyEarly = !fs.existsSync(tasksDirEarly) || fs.readdirSync(tasksDirEarly).length === 0;
906
- const hasTemplateRequest = !!options.template || !!options.registry;
907
- if (!isFirstInit &&
908
- !options.force &&
909
- !options.skipExisting &&
910
- !tasksEmptyEarly &&
911
- !hasTemplateRequest) {
912
- const reinitDone = await handleReinit(cwd, options, developerName, pythonCmd);
913
- if (reinitDone)
914
- return;
915
- // reinitDone === false means user chose "full re-initialize" → fall through
916
- }
917
- if (!developerName && !options.yes) {
918
- // Ask for developer name if not detected and not in yes mode
919
- console.log(chalk.gray("\nTrellis supports team collaboration - each developer has their own\n" +
920
- `workspace directory (${PATHS.WORKSPACE}/{name}/) to track AI sessions.\n` +
921
- "Tip: Usually this is your git username (git config user.name).\n"));
922
- developerName = await askInput("Your name: ");
923
- while (!developerName) {
924
- console.log(chalk.yellow("Name is required"));
905
+ if (developerName) {
906
+ console.log(chalk.blue("👤 Developer:"), chalk.gray(developerName));
907
+ }
908
+ const { command: pythonCmd } = resolveSupportedPython();
909
+ // ==========================================================================
910
+ // Re-init fast path: skip full flow when .trellis/ already exists
911
+ // ==========================================================================
912
+ // Aborted-init recovery (issue #204): if .trellis/ exists but tasks/ is
913
+ // empty, the previous init never reached bootstrap creation. Fall through
914
+ // to the full flow so the main-dispatch tasksEmpty fallback fires —
915
+ // handleReinit's joiner branch would otherwise mis-route the recovery.
916
+ const tasksEmptyEarly = tasksAreEmpty(cwd);
917
+ const hasTemplateRequest = !!options.template || !!options.registry;
918
+ if (!isFirstInit &&
919
+ !options.force &&
920
+ !options.skipExisting &&
921
+ !tasksEmptyEarly &&
922
+ !hasTemplateRequest) {
923
+ const reinitDone = await handleReinit(cwd, options, developerName, pythonCmd);
924
+ if (reinitDone)
925
+ return;
926
+ // reinitDone === false means user chose "full re-initialize" → fall through
927
+ }
928
+ if (!developerName && !options.yes) {
929
+ // Ask for developer name if not detected and not in yes mode
930
+ console.log(chalk.gray("\nTrellis supports team collaboration - each developer has their own\n" +
931
+ `workspace directory (${PATHS.WORKSPACE}/{name}/) to track AI sessions.\n` +
932
+ "Tip: Usually this is your git username (git config user.name).\n"));
925
933
  developerName = await askInput("Your name: ");
934
+ while (!developerName) {
935
+ console.log(chalk.yellow("Name is required"));
936
+ developerName = await askInput("Your name: ");
937
+ }
938
+ console.log(chalk.blue("👤 Developer:"), chalk.gray(developerName));
926
939
  }
927
- console.log(chalk.blue("👤 Developer:"), chalk.gray(developerName));
928
- }
929
- // Detect project type (silent - no output)
930
- const detectedType = detectProjectType(cwd);
931
- // Parse custom registry source early (needed by both monorepo + single-repo flows)
932
- let registry;
933
- let registrySourceForConfig;
934
- if (options.registry) {
935
- try {
936
- registry = parseRegistrySource(options.registry);
937
- registrySourceForConfig = options.registry;
940
+ // Detect project type (silent - no output)
941
+ const detectedType = detectProjectType(cwd);
942
+ // Parse custom registry source early (needed by both monorepo + single-repo flows)
943
+ let registry;
944
+ let registrySourceForConfig;
945
+ if (options.registry) {
946
+ try {
947
+ registry = parseRegistrySource(options.registry);
948
+ registrySourceForConfig = options.registry;
949
+ }
950
+ catch (error) {
951
+ console.log(chalk.red(error instanceof Error ? error.message : "Invalid registry source"));
952
+ return;
953
+ }
938
954
  }
939
- catch (error) {
940
- console.log(chalk.red(error instanceof Error ? error.message : "Invalid registry source"));
941
- return;
955
+ // Determine template strategy from flags (needed before monorepo template downloads)
956
+ let templateStrategy = "skip";
957
+ if (options.overwrite) {
958
+ templateStrategy = "overwrite";
942
959
  }
943
- }
944
- // Determine template strategy from flags (needed before monorepo template downloads)
945
- let templateStrategy = "skip";
946
- if (options.overwrite) {
947
- templateStrategy = "overwrite";
948
- }
949
- else if (options.append) {
950
- templateStrategy = "append";
951
- }
952
- // ==========================================================================
953
- // Monorepo Detection
954
- // ==========================================================================
955
- let monorepoPackages;
956
- let remoteSpecPackages;
957
- if (options.monorepo !== false) {
958
- // options.monorepo: true = --monorepo, false = --no-monorepo, undefined = auto
959
- const detected = detectMonorepo(cwd);
960
- if (options.monorepo === true && !detected) {
961
- console.log(chalk.red("Error: --monorepo specified but no multi-package layout detected."));
962
- console.log("");
963
- console.log(chalk.gray("Checked:"));
964
- console.log(chalk.gray(" ✗ pnpm-workspace.yaml"));
965
- console.log(chalk.gray(" ✗ package.json workspaces"));
966
- console.log(chalk.gray(" ✗ Cargo.toml [workspace]"));
967
- console.log(chalk.gray(" ✗ go.work"));
968
- console.log(chalk.gray(" ✗ pyproject.toml [tool.uv.workspace]"));
969
- console.log(chalk.gray(" ✗ .gitmodules"));
970
- console.log(chalk.gray(" ✗ sibling .git directories (need ≥ 2)"));
971
- console.log("");
972
- console.log("To configure manually, add to .trellis/config.yaml:");
973
- console.log("");
974
- console.log(chalk.cyan(" packages:"));
975
- console.log(chalk.cyan(" frontend:"));
976
- console.log(chalk.cyan(" path: ./frontend"));
977
- console.log(chalk.cyan(" git: true # if it has its own .git"));
978
- console.log(chalk.cyan(" backend:"));
979
- console.log(chalk.cyan(" path: ./backend"));
980
- console.log(chalk.cyan(" git: true"));
981
- return;
960
+ else if (options.append) {
961
+ templateStrategy = "append";
982
962
  }
983
- if (detected && detected.length > 0) {
984
- let enableMonorepo = false;
985
- if (options.monorepo === true || options.yes) {
986
- enableMonorepo = true;
987
- }
988
- else {
989
- // Show detected packages and ask
990
- console.log(chalk.blue("\n🔍 Detected monorepo packages:"));
991
- for (const pkg of detected) {
992
- const tag = pkg.isSubmodule
993
- ? chalk.gray(" (submodule)")
994
- : pkg.isGitRepo
995
- ? chalk.gray(" (git repo)")
996
- : "";
997
- console.log(chalk.gray(` - ${pkg.name}`) +
998
- chalk.gray(` (${pkg.path})`) +
999
- chalk.gray(` [${pkg.type}]`) +
1000
- tag);
1001
- }
963
+ // ==========================================================================
964
+ // Monorepo Detection
965
+ // ==========================================================================
966
+ let monorepoPackages;
967
+ let remoteSpecPackages;
968
+ if (options.monorepo !== false) {
969
+ // options.monorepo: true = --monorepo, false = --no-monorepo, undefined = auto
970
+ const detected = detectMonorepo(cwd);
971
+ if (options.monorepo === true && !detected) {
972
+ console.log(chalk.red("Error: --monorepo specified but no multi-package layout detected."));
1002
973
  console.log("");
1003
- const { useMonorepo } = await inquirer.prompt([
1004
- {
1005
- type: "confirm",
1006
- name: "useMonorepo",
1007
- message: "Enable monorepo mode?",
1008
- default: true,
1009
- },
1010
- ]);
1011
- enableMonorepo = useMonorepo;
974
+ console.log(chalk.gray("Checked:"));
975
+ console.log(chalk.gray(" ✗ pnpm-workspace.yaml"));
976
+ console.log(chalk.gray(" ✗ package.json workspaces"));
977
+ console.log(chalk.gray(" ✗ Cargo.toml [workspace]"));
978
+ console.log(chalk.gray(" ✗ go.work"));
979
+ console.log(chalk.gray(" ✗ pyproject.toml [tool.uv.workspace]"));
980
+ console.log(chalk.gray(" ✗ .gitmodules"));
981
+ console.log(chalk.gray(" ✗ sibling .git directories (need ≥ 2)"));
982
+ console.log("");
983
+ console.log("To configure manually, add to .trellis/config.yaml:");
984
+ console.log("");
985
+ console.log(chalk.cyan(" packages:"));
986
+ console.log(chalk.cyan(" frontend:"));
987
+ console.log(chalk.cyan(" path: ./frontend"));
988
+ console.log(chalk.cyan(" git: true # if it has its own .git"));
989
+ console.log(chalk.cyan(" backend:"));
990
+ console.log(chalk.cyan(" path: ./backend"));
991
+ console.log(chalk.cyan(" git: true"));
992
+ return;
1012
993
  }
1013
- if (enableMonorepo) {
1014
- monorepoPackages = detected;
1015
- remoteSpecPackages = new Set();
1016
- // Per-package template selection (unless -y mode: all use blank spec)
1017
- if (!options.yes && !options.template) {
994
+ if (detected && detected.length > 0) {
995
+ let enableMonorepo = false;
996
+ if (options.monorepo === true || options.yes) {
997
+ enableMonorepo = true;
998
+ }
999
+ else {
1000
+ // Show detected packages and ask
1001
+ console.log(chalk.blue("\n🔍 Detected monorepo packages:"));
1018
1002
  for (const pkg of detected) {
1019
- const { specSource } = await inquirer.prompt([
1020
- {
1021
- type: "list",
1022
- name: "specSource",
1023
- message: `Spec source for ${pkg.name} (${pkg.path}):`,
1024
- choices: [
1025
- { name: "From scratch (Trellis default)", value: "blank" },
1026
- { name: "Download remote template", value: "remote" },
1027
- ],
1028
- default: "blank",
1029
- },
1030
- ]);
1031
- if (specSource === "remote") {
1032
- // Use existing template download flow, targeting spec/<name>/
1033
- const destDir = path.join(cwd, PATHS.SPEC, sanitizePkgName(pkg.name));
1034
- console.log(chalk.blue(`📦 Select template for ${pkg.name}...`));
1035
- // Fetch templates if not already done
1036
- const templates = await fetchTemplateIndex();
1037
- const specTemplates = templates
1038
- .filter((t) => t.type === "spec")
1039
- .map((t) => ({
1040
- name: `${t.id} (${t.name})`,
1041
- value: t.id,
1042
- }));
1043
- if (specTemplates.length > 0) {
1044
- const { templateId } = await inquirer.prompt([
1045
- {
1046
- type: "list",
1047
- name: "templateId",
1048
- message: `Select template for ${pkg.name}:`,
1049
- choices: specTemplates,
1050
- },
1051
- ]);
1052
- const result = await downloadTemplateById(cwd, templateId, templateStrategy, templates.find((t) => t.id === templateId), undefined, destDir);
1053
- if (result.success) {
1054
- console.log(chalk.green(` ${result.message}`));
1055
- remoteSpecPackages.add(sanitizePkgName(pkg.name));
1003
+ const tag = pkg.isSubmodule
1004
+ ? chalk.gray(" (submodule)")
1005
+ : pkg.isGitRepo
1006
+ ? chalk.gray(" (git repo)")
1007
+ : "";
1008
+ console.log(chalk.gray(` - ${pkg.name}`) +
1009
+ chalk.gray(` (${pkg.path})`) +
1010
+ chalk.gray(` [${pkg.type}]`) +
1011
+ tag);
1012
+ }
1013
+ console.log("");
1014
+ const { useMonorepo } = await inquirer.prompt([
1015
+ {
1016
+ type: "confirm",
1017
+ name: "useMonorepo",
1018
+ message: "Enable monorepo mode?",
1019
+ default: true,
1020
+ },
1021
+ ]);
1022
+ enableMonorepo = useMonorepo;
1023
+ }
1024
+ if (enableMonorepo) {
1025
+ monorepoPackages = detected;
1026
+ remoteSpecPackages = new Set();
1027
+ // Per-package template selection (unless -y mode: all use blank spec)
1028
+ if (!options.yes && !options.template) {
1029
+ for (const pkg of detected) {
1030
+ const { specSource } = await inquirer.prompt([
1031
+ {
1032
+ type: "list",
1033
+ name: "specSource",
1034
+ message: `Spec source for ${pkg.name} (${pkg.path}):`,
1035
+ choices: [
1036
+ { name: "From scratch (Trellis default)", value: "blank" },
1037
+ { name: "Download remote template", value: "remote" },
1038
+ ],
1039
+ default: "blank",
1040
+ },
1041
+ ]);
1042
+ if (specSource === "remote") {
1043
+ // Use existing template download flow, targeting spec/<name>/
1044
+ const destDir = path.join(cwd, PATHS.SPEC, sanitizePkgName(pkg.name));
1045
+ console.log(chalk.blue(`📦 Select template for ${pkg.name}...`));
1046
+ // Fetch templates if not already done
1047
+ const templates = await fetchTemplateIndex();
1048
+ const specTemplates = templates
1049
+ .filter((t) => t.type === "spec")
1050
+ .map((t) => ({
1051
+ name: `${t.id} (${t.name})`,
1052
+ value: t.id,
1053
+ }));
1054
+ if (specTemplates.length > 0) {
1055
+ const { templateId } = await inquirer.prompt([
1056
+ {
1057
+ type: "list",
1058
+ name: "templateId",
1059
+ message: `Select template for ${pkg.name}:`,
1060
+ choices: specTemplates,
1061
+ },
1062
+ ]);
1063
+ const result = await downloadTemplateById(cwd, templateId, templateStrategy, templates.find((t) => t.id === templateId), undefined, destDir);
1064
+ if (result.success) {
1065
+ console.log(chalk.green(` ${result.message}`));
1066
+ remoteSpecPackages.add(sanitizePkgName(pkg.name));
1067
+ }
1068
+ else {
1069
+ console.log(chalk.yellow(` ${result.message}`));
1070
+ console.log(chalk.gray(" Falling back to blank spec..."));
1071
+ }
1056
1072
  }
1057
1073
  else {
1058
- console.log(chalk.yellow(` ${result.message}`));
1059
- console.log(chalk.gray(" Falling back to blank spec..."));
1074
+ console.log(chalk.gray(" No templates available. Using blank spec."));
1060
1075
  }
1061
1076
  }
1062
- else {
1063
- console.log(chalk.gray(" No templates available. Using blank spec."));
1064
- }
1065
1077
  }
1066
1078
  }
1067
- }
1068
- else if (options.template) {
1069
- // --template as default for all packages
1070
- for (const pkg of detected) {
1071
- const destDir = path.join(cwd, PATHS.SPEC, sanitizePkgName(pkg.name));
1072
- const result = await downloadTemplateById(cwd, options.template, templateStrategy, undefined, registry, destDir);
1073
- if (result.success && !result.skipped) {
1074
- remoteSpecPackages.add(sanitizePkgName(pkg.name));
1079
+ else if (options.template) {
1080
+ // --template as default for all packages
1081
+ for (const pkg of detected) {
1082
+ const destDir = path.join(cwd, PATHS.SPEC, sanitizePkgName(pkg.name));
1083
+ const result = await downloadTemplateById(cwd, options.template, templateStrategy, undefined, registry, destDir);
1084
+ if (result.success && !result.skipped) {
1085
+ remoteSpecPackages.add(sanitizePkgName(pkg.name));
1086
+ }
1075
1087
  }
1076
1088
  }
1077
1089
  }
1078
1090
  }
1079
1091
  }
1080
- }
1081
- // Tool definitions derived from platform registry
1082
- const TOOLS = getInitToolChoices();
1083
- // Build tools from explicit flags
1084
- const explicitTools = TOOLS.filter((t) => options[t.key]).map((t) => t.key);
1085
- let tools;
1086
- if (explicitTools.length > 0) {
1087
- // Explicit flags take precedence (works with or without -y)
1088
- tools = explicitTools;
1089
- }
1090
- else if (options.yes) {
1091
- // No explicit tools + -y: default to Cursor and Claude
1092
- tools = TOOLS.filter((t) => t.defaultChecked).map((t) => t.key);
1093
- }
1094
- else {
1095
- // Interactive mode
1096
- const answers = await inquirer.prompt([
1097
- {
1098
- type: "checkbox",
1099
- name: "tools",
1100
- message: "Select AI tools to configure:",
1101
- choices: TOOLS.map((t) => ({
1102
- name: t.name,
1103
- value: t.key,
1104
- checked: t.defaultChecked,
1105
- })),
1106
- },
1107
- ]);
1108
- tools = answers.tools;
1109
- }
1110
- // Treat unknown project type as fullstack
1111
- const projectType = detectedType === "unknown" ? "fullstack" : detectedType;
1112
- if (tools.length === 0) {
1113
- console.log(chalk.yellow("No tools selected. At least one tool is required."));
1114
- return;
1115
- }
1116
- // Opt-in Claude Code statusLine: confirm interactively when the flag wasn't passed
1117
- await maybePromptStatuslineOptIn(options, tools);
1118
- // ==========================================================================
1119
- // Template Selection (single-repo only; monorepo handles templates above)
1120
- // ==========================================================================
1121
- let selectedTemplate = null;
1122
- // Pre-fetched templates list (used to pass selected SpecTemplate to downloadTemplateById)
1123
- let fetchedTemplates = [];
1124
- let registryBackend;
1125
- // Determine the index URL based on registry
1126
- const indexUrl = registry
1127
- ? `${registry.rawBaseUrl}/index.json`
1128
- : TEMPLATE_INDEX_URL;
1129
- if (monorepoPackages) {
1130
- // Monorepo: template selection already handled above
1131
- }
1132
- else if (options.template) {
1133
- // Template specified via --template flag
1134
- selectedTemplate = options.template;
1135
- if (registry) {
1136
- const probeResult = await probeRegistryIndex(indexUrl, registry);
1137
- registryBackend = probeResult.backend;
1138
- if (probeResult.error) {
1139
- console.log(chalk.red(`Error: ${probeResult.error.message}`));
1140
- return;
1141
- }
1142
- if (probeResult.isNotFound) {
1143
- console.log(chalk.red("Error: Registry has no index.json. Remove --template to use direct download mode."));
1144
- return;
1145
- }
1146
- fetchedTemplates = probeResult.templates;
1092
+ // Tool definitions derived from platform registry
1093
+ const TOOLS = getInitToolChoices();
1094
+ // Build tools from explicit flags
1095
+ const explicitTools = TOOLS.filter((t) => options[t.key]).map((t) => t.key);
1096
+ let tools;
1097
+ if (explicitTools.length > 0) {
1098
+ // Explicit flags take precedence (works with or without -y)
1099
+ tools = explicitTools;
1147
1100
  }
1148
- }
1149
- else if (!options.yes) {
1150
- // Interactive mode: show template selection
1151
- const timeoutSec = TIMEOUTS.INDEX_FETCH_MS / 1000;
1152
- const sourceLabel = registry ? registry.gigetSource : TEMPLATE_INDEX_URL;
1153
- console.log(chalk.gray(` Fetching available templates from ${sourceLabel}`));
1154
- let elapsed = 0;
1155
- const ticker = setInterval(() => {
1156
- elapsed++;
1157
- process.stdout.write(`\r${chalk.gray(` Loading... ${elapsed}s/${timeoutSec}s`)}`);
1158
- }, 1000);
1159
- process.stdout.write(chalk.gray(` Loading... 0s/${timeoutSec}s`));
1160
- let templates;
1161
- let registryProbeNotFound = false;
1162
- let registryProbeError;
1163
- if (registry) {
1164
- const probeResult = await probeRegistryIndex(indexUrl, registry);
1165
- templates = probeResult.templates;
1166
- registryProbeNotFound = probeResult.isNotFound;
1167
- registryProbeError = probeResult.error;
1168
- registryBackend = probeResult.backend;
1101
+ else if (options.yes) {
1102
+ // No explicit tools + -y: default to Cursor and Claude
1103
+ tools = TOOLS.filter((t) => t.defaultChecked).map((t) => t.key);
1169
1104
  }
1170
1105
  else {
1171
- templates = await fetchTemplateIndex(indexUrl);
1106
+ // Interactive mode
1107
+ const answers = await inquirer.prompt([
1108
+ {
1109
+ type: "checkbox",
1110
+ name: "tools",
1111
+ message: "Select AI tools to configure:",
1112
+ choices: TOOLS.map((t) => ({
1113
+ name: t.name,
1114
+ value: t.key,
1115
+ checked: t.defaultChecked,
1116
+ })),
1117
+ },
1118
+ ]);
1119
+ tools = answers.tools;
1172
1120
  }
1173
- clearInterval(ticker);
1174
- // Clear the loading line
1175
- process.stdout.write("\r\x1b[2K");
1176
- fetchedTemplates = templates;
1177
- if (templates.length === 0 && registry && registryProbeNotFound) {
1178
- // Custom registry: confirmed no index.json — will try direct download later
1179
- console.log(chalk.gray(" No index.json found at registry. Will download as direct spec template."));
1180
- }
1181
- else if (templates.length === 0 && registry) {
1182
- // Custom registry: transient error (not a 404) — abort, don't misclassify
1183
- console.log(chalk.red(` ${registryProbeError?.message ?? "Could not reach registry. Check your connection and try again."}`));
1121
+ // Treat unknown project type as fullstack
1122
+ const projectType = detectedType === "unknown" ? "fullstack" : detectedType;
1123
+ if (tools.length === 0) {
1124
+ console.log(chalk.yellow("No tools selected. At least one tool is required."));
1184
1125
  return;
1185
1126
  }
1186
- else if (templates.length === 0) {
1187
- console.log(chalk.gray(" Could not fetch templates (offline or server unavailable)."));
1188
- console.log(chalk.gray(" Using blank templates.\n"));
1127
+ // Opt-in Claude Code statusLine: confirm interactively when the flag wasn't passed
1128
+ await maybePromptStatuslineOptIn(options, tools);
1129
+ // ==========================================================================
1130
+ // Template Selection (single-repo only; monorepo handles templates above)
1131
+ // ==========================================================================
1132
+ let selectedTemplate = null;
1133
+ // Pre-fetched templates list (used to pass selected SpecTemplate to downloadTemplateById)
1134
+ let fetchedTemplates = [];
1135
+ let registryBackend;
1136
+ // Determine the index URL based on registry
1137
+ const indexUrl = registry
1138
+ ? `${registry.rawBaseUrl}/index.json`
1139
+ : TEMPLATE_INDEX_URL;
1140
+ if (monorepoPackages) {
1141
+ // Monorepo: template selection already handled above
1189
1142
  }
1190
- if (templates.length > 0) {
1191
- // Build template choices
1192
- const specTemplates = templates
1193
- .filter((t) => t.type === "spec")
1194
- .map((t) => ({
1195
- name: `${t.id} (${t.name})`,
1196
- value: t.id,
1197
- }));
1198
- const templateChoices = registry
1199
- ? specTemplates
1200
- : [
1201
- {
1202
- name: "from scratch (default)",
1203
- value: "blank",
1204
- },
1205
- ...specTemplates,
1206
- {
1207
- name: "custom (enter a registry source)",
1208
- value: "__custom__",
1209
- },
1210
- ];
1211
- // Loop to allow returning from custom source input back to the picker
1212
- let templatePicked = false;
1213
- while (templateChoices.length > 0 && !templatePicked) {
1214
- const templateAnswer = await inquirer.prompt([
1215
- {
1216
- type: "list",
1217
- name: "template",
1218
- message: "Select a spec template:",
1219
- choices: templateChoices,
1220
- default: registry ? undefined : "blank",
1221
- },
1222
- ]);
1223
- if (templateAnswer.template === "__custom__") {
1224
- // Prompt for custom registry source (empty → back to picker)
1225
- const customSource = await askInput("Enter registry source (e.g., gh:myorg/myrepo/specs), or press Enter to go back: ");
1226
- if (!customSource) {
1227
- continue; // Back to picker
1228
- }
1229
- try {
1230
- registry = parseRegistrySource(customSource);
1231
- registrySourceForConfig = customSource;
1232
- fetchedTemplates = []; // Reset so direct-download guard works correctly
1233
- // Probe index.json to detect marketplace vs direct download
1234
- const customIndexUrl = `${registry.rawBaseUrl}/index.json`;
1235
- console.log(chalk.gray(` Checking for templates at ${registry.gigetSource}...`));
1236
- const customProbe = await probeRegistryIndex(customIndexUrl, registry);
1237
- const customTemplates = customProbe.templates;
1238
- registryBackend = customProbe.backend;
1239
- if (customTemplates.length > 0) {
1240
- // Marketplace mode: show picker with custom templates
1241
- fetchedTemplates = customTemplates;
1242
- const customChoices = customTemplates
1243
- .filter((t) => t.type === "spec")
1244
- .map((t) => ({
1245
- name: `${t.id} (${t.name})`,
1246
- value: t.id,
1247
- }));
1248
- if (customChoices.length > 0) {
1249
- const customAnswer = await inquirer.prompt([
1250
- {
1251
- type: "list",
1252
- name: "template",
1253
- message: "Select a spec template:",
1254
- choices: customChoices,
1255
- },
1256
- ]);
1257
- selectedTemplate = customAnswer.template;
1258
- // Check if spec directory already exists and ask what to do
1259
- const specDir = path.join(cwd, PATHS.SPEC);
1260
- if (fs.existsSync(specDir) &&
1261
- !options.overwrite &&
1262
- !options.append) {
1263
- const actionAnswer = await inquirer.prompt([
1143
+ else if (options.template) {
1144
+ // Template specified via --template flag
1145
+ selectedTemplate = options.template;
1146
+ if (registry) {
1147
+ const probeResult = await probeRegistryIndex(indexUrl, registry);
1148
+ registryBackend = probeResult.backend;
1149
+ if (probeResult.error) {
1150
+ console.log(chalk.red(`Error: ${probeResult.error.message}`));
1151
+ return;
1152
+ }
1153
+ if (probeResult.isNotFound) {
1154
+ console.log(chalk.red("Error: Registry has no index.json. Remove --template to use direct download mode."));
1155
+ return;
1156
+ }
1157
+ fetchedTemplates = probeResult.templates;
1158
+ }
1159
+ }
1160
+ else if (!options.yes) {
1161
+ // Interactive mode: show template selection
1162
+ const timeoutSec = TIMEOUTS.INDEX_FETCH_MS / 1000;
1163
+ const sourceLabel = registry ? registry.gigetSource : TEMPLATE_INDEX_URL;
1164
+ console.log(chalk.gray(` Fetching available templates from ${sourceLabel}`));
1165
+ let elapsed = 0;
1166
+ const ticker = setInterval(() => {
1167
+ elapsed++;
1168
+ process.stdout.write(`\r${chalk.gray(` Loading... ${elapsed}s/${timeoutSec}s`)}`);
1169
+ }, 1000);
1170
+ process.stdout.write(chalk.gray(` Loading... 0s/${timeoutSec}s`));
1171
+ let templates;
1172
+ let registryProbeNotFound = false;
1173
+ let registryProbeError;
1174
+ if (registry) {
1175
+ const probeResult = await probeRegistryIndex(indexUrl, registry);
1176
+ templates = probeResult.templates;
1177
+ registryProbeNotFound = probeResult.isNotFound;
1178
+ registryProbeError = probeResult.error;
1179
+ registryBackend = probeResult.backend;
1180
+ }
1181
+ else {
1182
+ templates = await fetchTemplateIndex(indexUrl);
1183
+ }
1184
+ clearInterval(ticker);
1185
+ // Clear the loading line
1186
+ process.stdout.write("\r\x1b[2K");
1187
+ fetchedTemplates = templates;
1188
+ if (templates.length === 0 && registry && registryProbeNotFound) {
1189
+ // Custom registry: confirmed no index.json — will try direct download later
1190
+ console.log(chalk.gray(" No index.json found at registry. Will download as direct spec template."));
1191
+ }
1192
+ else if (templates.length === 0 && registry) {
1193
+ // Custom registry: transient error (not a 404) — abort, don't misclassify
1194
+ console.log(chalk.red(` ${registryProbeError?.message ?? "Could not reach registry. Check your connection and try again."}`));
1195
+ return;
1196
+ }
1197
+ else if (templates.length === 0) {
1198
+ console.log(chalk.gray(" Could not fetch templates (offline or server unavailable)."));
1199
+ console.log(chalk.gray(" Using blank templates.\n"));
1200
+ }
1201
+ if (templates.length > 0) {
1202
+ // Build template choices
1203
+ const specTemplates = templates
1204
+ .filter((t) => t.type === "spec")
1205
+ .map((t) => ({
1206
+ name: `${t.id} (${t.name})`,
1207
+ value: t.id,
1208
+ }));
1209
+ const templateChoices = registry
1210
+ ? specTemplates
1211
+ : [
1212
+ {
1213
+ name: "from scratch (default)",
1214
+ value: "blank",
1215
+ },
1216
+ ...specTemplates,
1217
+ {
1218
+ name: "custom (enter a registry source)",
1219
+ value: "__custom__",
1220
+ },
1221
+ ];
1222
+ // Loop to allow returning from custom source input back to the picker
1223
+ let templatePicked = false;
1224
+ while (templateChoices.length > 0 && !templatePicked) {
1225
+ const templateAnswer = await inquirer.prompt([
1226
+ {
1227
+ type: "list",
1228
+ name: "template",
1229
+ message: "Select a spec template:",
1230
+ choices: templateChoices,
1231
+ default: registry ? undefined : "blank",
1232
+ },
1233
+ ]);
1234
+ if (templateAnswer.template === "__custom__") {
1235
+ // Prompt for custom registry source (empty → back to picker)
1236
+ const customSource = await askInput("Enter registry source (e.g., gh:myorg/myrepo/specs), or press Enter to go back: ");
1237
+ if (!customSource) {
1238
+ continue; // Back to picker
1239
+ }
1240
+ try {
1241
+ registry = parseRegistrySource(customSource);
1242
+ registrySourceForConfig = customSource;
1243
+ fetchedTemplates = []; // Reset so direct-download guard works correctly
1244
+ // Probe index.json to detect marketplace vs direct download
1245
+ const customIndexUrl = `${registry.rawBaseUrl}/index.json`;
1246
+ console.log(chalk.gray(` Checking for templates at ${registry.gigetSource}...`));
1247
+ const customProbe = await probeRegistryIndex(customIndexUrl, registry);
1248
+ const customTemplates = customProbe.templates;
1249
+ registryBackend = customProbe.backend;
1250
+ if (customTemplates.length > 0) {
1251
+ // Marketplace mode: show picker with custom templates
1252
+ fetchedTemplates = customTemplates;
1253
+ const customChoices = customTemplates
1254
+ .filter((t) => t.type === "spec")
1255
+ .map((t) => ({
1256
+ name: `${t.id} (${t.name})`,
1257
+ value: t.id,
1258
+ }));
1259
+ if (customChoices.length > 0) {
1260
+ const customAnswer = await inquirer.prompt([
1264
1261
  {
1265
1262
  type: "list",
1266
- name: "action",
1267
- message: `Directory ${PATHS.SPEC} already exists. What do you want to do?`,
1268
- choices: [
1269
- { name: "Skip (keep existing)", value: "skip" },
1270
- {
1271
- name: "Overwrite (replace all)",
1272
- value: "overwrite",
1273
- },
1274
- {
1275
- name: "Append (add missing files only)",
1276
- value: "append",
1277
- },
1278
- ],
1279
- default: "skip",
1263
+ name: "template",
1264
+ message: "Select a spec template:",
1265
+ choices: customChoices,
1280
1266
  },
1281
1267
  ]);
1282
- templateStrategy = actionAnswer.action;
1268
+ selectedTemplate = customAnswer.template;
1269
+ // Check if spec directory already exists and ask what to do
1270
+ const specDir = path.join(cwd, PATHS.SPEC);
1271
+ if (fs.existsSync(specDir) &&
1272
+ !options.overwrite &&
1273
+ !options.append) {
1274
+ const actionAnswer = await inquirer.prompt([
1275
+ {
1276
+ type: "list",
1277
+ name: "action",
1278
+ message: `Directory ${PATHS.SPEC} already exists. What do you want to do?`,
1279
+ choices: [
1280
+ { name: "Skip (keep existing)", value: "skip" },
1281
+ {
1282
+ name: "Overwrite (replace all)",
1283
+ value: "overwrite",
1284
+ },
1285
+ {
1286
+ name: "Append (add missing files only)",
1287
+ value: "append",
1288
+ },
1289
+ ],
1290
+ default: "skip",
1291
+ },
1292
+ ]);
1293
+ templateStrategy = actionAnswer.action;
1294
+ }
1283
1295
  }
1296
+ templatePicked = true;
1297
+ }
1298
+ else if (customProbe.isNotFound) {
1299
+ // No index.json → direct download mode
1300
+ templatePicked = true;
1301
+ }
1302
+ else {
1303
+ // Transient error (not 404) — loop back, don't misclassify
1304
+ console.log(chalk.yellow(` ${customProbe.error?.message ?? "Could not reach registry. Try again or enter a different source."}`));
1305
+ registry = undefined; // Reset so we don't fall through to direct download
1306
+ registrySourceForConfig = undefined;
1284
1307
  }
1285
- templatePicked = true;
1286
- }
1287
- else if (customProbe.isNotFound) {
1288
- // No index.json → direct download mode
1289
- templatePicked = true;
1290
1308
  }
1291
- else {
1292
- // Transient error (not 404) — loop back, don't misclassify
1293
- console.log(chalk.yellow(` ${customProbe.error?.message ?? "Could not reach registry. Try again or enter a different source."}`));
1294
- registry = undefined; // Reset so we don't fall through to direct download
1295
- registrySourceForConfig = undefined;
1309
+ catch (error) {
1310
+ console.log(chalk.red(error instanceof Error
1311
+ ? error.message
1312
+ : "Invalid registry source"));
1313
+ // Loop back to picker
1296
1314
  }
1297
1315
  }
1298
- catch (error) {
1299
- console.log(chalk.red(error instanceof Error
1300
- ? error.message
1301
- : "Invalid registry source"));
1302
- // Loop back to picker
1303
- }
1304
- }
1305
- else {
1306
- templatePicked = true;
1307
- if (templateAnswer.template !== "blank") {
1308
- selectedTemplate = templateAnswer.template;
1309
- // Check if spec directory already exists and ask what to do
1310
- const specDir = path.join(cwd, PATHS.SPEC);
1311
- if (fs.existsSync(specDir) &&
1312
- !options.overwrite &&
1313
- !options.append) {
1314
- const actionAnswer = await inquirer.prompt([
1315
- {
1316
- type: "list",
1317
- name: "action",
1318
- message: `Directory ${PATHS.SPEC} already exists. What do you want to do?`,
1319
- choices: [
1320
- { name: "Skip (keep existing)", value: "skip" },
1321
- { name: "Overwrite (replace all)", value: "overwrite" },
1322
- {
1323
- name: "Append (add missing files only)",
1324
- value: "append",
1325
- },
1326
- ],
1327
- default: "skip",
1328
- },
1329
- ]);
1330
- templateStrategy = actionAnswer.action;
1316
+ else {
1317
+ templatePicked = true;
1318
+ if (templateAnswer.template !== "blank") {
1319
+ selectedTemplate = templateAnswer.template;
1320
+ // Check if spec directory already exists and ask what to do
1321
+ const specDir = path.join(cwd, PATHS.SPEC);
1322
+ if (fs.existsSync(specDir) &&
1323
+ !options.overwrite &&
1324
+ !options.append) {
1325
+ const actionAnswer = await inquirer.prompt([
1326
+ {
1327
+ type: "list",
1328
+ name: "action",
1329
+ message: `Directory ${PATHS.SPEC} already exists. What do you want to do?`,
1330
+ choices: [
1331
+ { name: "Skip (keep existing)", value: "skip" },
1332
+ { name: "Overwrite (replace all)", value: "overwrite" },
1333
+ {
1334
+ name: "Append (add missing files only)",
1335
+ value: "append",
1336
+ },
1337
+ ],
1338
+ default: "skip",
1339
+ },
1340
+ ]);
1341
+ templateStrategy = actionAnswer.action;
1342
+ }
1331
1343
  }
1332
1344
  }
1333
1345
  }
1334
1346
  }
1335
1347
  }
1336
- }
1337
- // -y mode with --registry (no --template): probe index.json to detect mode
1338
- // Skip when monorepo mode already handled templates above
1339
- if (options.yes && registry && !selectedTemplate && !monorepoPackages) {
1340
- const probeResult = await probeRegistryIndex(`${registry.rawBaseUrl}/index.json`, registry);
1341
- registryBackend = probeResult.backend;
1342
- if (probeResult.templates.length > 0) {
1343
- // Marketplace mode requires interactive selection — can't auto-select
1344
- console.log(chalk.red("Error: Registry is a marketplace with multiple templates. " +
1345
- "Use --template <id> to specify which one, or remove -y for interactive selection."));
1346
- return;
1347
- }
1348
- if (!probeResult.isNotFound) {
1349
- // Transient error (not 404) — abort, don't misclassify as direct-download
1350
- console.log(chalk.red(`Error: ${probeResult.error?.message ?? "Could not reach registry. Check your connection and try again."}`));
1351
- return;
1348
+ // -y mode with --registry (no --template): probe index.json to detect mode
1349
+ // Skip when monorepo mode already handled templates above
1350
+ if (options.yes && registry && !selectedTemplate && !monorepoPackages) {
1351
+ const probeResult = await probeRegistryIndex(`${registry.rawBaseUrl}/index.json`, registry);
1352
+ registryBackend = probeResult.backend;
1353
+ if (probeResult.templates.length > 0) {
1354
+ // Marketplace mode requires interactive selection — can't auto-select
1355
+ console.log(chalk.red("Error: Registry is a marketplace with multiple templates. " +
1356
+ "Use --template <id> to specify which one, or remove -y for interactive selection."));
1357
+ return;
1358
+ }
1359
+ if (!probeResult.isNotFound) {
1360
+ // Transient error (not 404) — abort, don't misclassify as direct-download
1361
+ console.log(chalk.red(`Error: ${probeResult.error?.message ?? "Could not reach registry. Check your connection and try again."}`));
1362
+ return;
1363
+ }
1364
+ // isNotFound=true → no index.json, proceed with direct download (fetchedTemplates stays empty)
1352
1365
  }
1353
- // isNotFound=true → no index.json, proceed with direct download (fetchedTemplates stays empty)
1354
- }
1355
- // ==========================================================================
1356
- // Download Remote Template (if selected or direct registry download)
1357
- // ==========================================================================
1358
- let useRemoteTemplate = false;
1359
- let registrySpecConfigToPersist = null;
1360
- if (selectedTemplate) {
1361
- // Marketplace mode: download specific template by ID
1362
- console.log(chalk.blue(`📦 Downloading template "${selectedTemplate}"...`));
1363
- console.log(chalk.gray(" This may take a moment on slow connections."));
1364
- // Find pre-fetched SpecTemplate to avoid double-fetch
1365
- const prefetched = fetchedTemplates.find((t) => t.id === selectedTemplate);
1366
- const result = await downloadTemplateById(cwd, selectedTemplate, templateStrategy, prefetched, registry, undefined, registryBackend);
1367
- if (result.success) {
1368
- if (result.skipped) {
1369
- console.log(chalk.gray(` ${result.message}`));
1366
+ // ==========================================================================
1367
+ // Download Remote Template (if selected or direct registry download)
1368
+ // ==========================================================================
1369
+ let useRemoteTemplate = false;
1370
+ let registrySpecConfigToPersist = null;
1371
+ if (selectedTemplate) {
1372
+ // Marketplace mode: download specific template by ID
1373
+ console.log(chalk.blue(`📦 Downloading template "${selectedTemplate}"...`));
1374
+ console.log(chalk.gray(" This may take a moment on slow connections."));
1375
+ // Find pre-fetched SpecTemplate to avoid double-fetch
1376
+ const prefetched = fetchedTemplates.find((t) => t.id === selectedTemplate);
1377
+ const result = await downloadTemplateById(cwd, selectedTemplate, templateStrategy, prefetched, registry, undefined, registryBackend);
1378
+ if (result.success) {
1379
+ if (result.skipped) {
1380
+ console.log(chalk.gray(` ${result.message}`));
1381
+ }
1382
+ else {
1383
+ console.log(chalk.green(` ${result.message}`));
1384
+ useRemoteTemplate = true;
1385
+ if (registry) {
1386
+ registrySpecConfigToPersist = {
1387
+ source: registrySourceForConfig ?? registry.gigetSource,
1388
+ template: selectedTemplate,
1389
+ };
1390
+ }
1391
+ }
1370
1392
  }
1371
1393
  else {
1372
- console.log(chalk.green(` ${result.message}`));
1373
- useRemoteTemplate = true;
1374
- if (registry) {
1394
+ console.log(chalk.yellow(` ${result.message}`));
1395
+ console.log(chalk.gray(" Falling back to blank templates..."));
1396
+ const retryCmd = registry
1397
+ ? `trellis init --registry ${registry.gigetSource} --template ${selectedTemplate}`
1398
+ : `trellis init --template ${selectedTemplate}`;
1399
+ console.log(chalk.gray(` You can retry later: ${retryCmd}`));
1400
+ }
1401
+ }
1402
+ else if (registry && fetchedTemplates.length === 0) {
1403
+ // Direct download mode: registry has no index.json, download directory directly
1404
+ console.log(chalk.blue(`📦 Downloading spec from ${registry.gigetSource}...`));
1405
+ console.log(chalk.gray(" This may take a moment on slow connections."));
1406
+ // Ask about existing spec dir in interactive mode
1407
+ if (!options.yes && !options.overwrite && !options.append) {
1408
+ const specDir = path.join(cwd, PATHS.SPEC);
1409
+ if (fs.existsSync(specDir)) {
1410
+ const actionAnswer = await inquirer.prompt([
1411
+ {
1412
+ type: "list",
1413
+ name: "action",
1414
+ message: `Directory ${PATHS.SPEC} already exists. What do you want to do?`,
1415
+ choices: [
1416
+ { name: "Skip (keep existing)", value: "skip" },
1417
+ { name: "Overwrite (replace all)", value: "overwrite" },
1418
+ { name: "Append (add missing files only)", value: "append" },
1419
+ ],
1420
+ default: "skip",
1421
+ },
1422
+ ]);
1423
+ templateStrategy = actionAnswer.action;
1424
+ }
1425
+ }
1426
+ const result = await downloadRegistryDirect(cwd, registry, templateStrategy, undefined, registryBackend);
1427
+ if (result.success) {
1428
+ if (result.skipped) {
1429
+ console.log(chalk.gray(` ${result.message}`));
1430
+ }
1431
+ else {
1432
+ console.log(chalk.green(` ${result.message}`));
1433
+ useRemoteTemplate = true;
1375
1434
  registrySpecConfigToPersist = {
1376
1435
  source: registrySourceForConfig ?? registry.gigetSource,
1377
- template: selectedTemplate,
1378
1436
  };
1379
1437
  }
1380
1438
  }
1381
- }
1382
- else {
1383
- console.log(chalk.yellow(` ${result.message}`));
1384
- console.log(chalk.gray(" Falling back to blank templates..."));
1385
- const retryCmd = registry
1386
- ? `trellis init --registry ${registry.gigetSource} --template ${selectedTemplate}`
1387
- : `trellis init --template ${selectedTemplate}`;
1388
- console.log(chalk.gray(` You can retry later: ${retryCmd}`));
1389
- }
1390
- }
1391
- else if (registry && fetchedTemplates.length === 0) {
1392
- // Direct download mode: registry has no index.json, download directory directly
1393
- console.log(chalk.blue(`📦 Downloading spec from ${registry.gigetSource}...`));
1394
- console.log(chalk.gray(" This may take a moment on slow connections."));
1395
- // Ask about existing spec dir in interactive mode
1396
- if (!options.yes && !options.overwrite && !options.append) {
1397
- const specDir = path.join(cwd, PATHS.SPEC);
1398
- if (fs.existsSync(specDir)) {
1399
- const actionAnswer = await inquirer.prompt([
1400
- {
1401
- type: "list",
1402
- name: "action",
1403
- message: `Directory ${PATHS.SPEC} already exists. What do you want to do?`,
1404
- choices: [
1405
- { name: "Skip (keep existing)", value: "skip" },
1406
- { name: "Overwrite (replace all)", value: "overwrite" },
1407
- { name: "Append (add missing files only)", value: "append" },
1408
- ],
1409
- default: "skip",
1410
- },
1411
- ]);
1412
- templateStrategy = actionAnswer.action;
1413
- }
1414
- }
1415
- const result = await downloadRegistryDirect(cwd, registry, templateStrategy, undefined, registryBackend);
1416
- if (result.success) {
1417
- if (result.skipped) {
1418
- console.log(chalk.gray(` ${result.message}`));
1419
- }
1420
1439
  else {
1421
- console.log(chalk.green(` ${result.message}`));
1422
- useRemoteTemplate = true;
1423
- registrySpecConfigToPersist = {
1424
- source: registrySourceForConfig ?? registry.gigetSource,
1425
- };
1440
+ console.log(chalk.yellow(` ${result.message}`));
1441
+ console.log(chalk.gray(" Falling back to blank templates..."));
1442
+ console.log(chalk.gray(` You can retry later: trellis init --registry ${registry.gigetSource}`));
1426
1443
  }
1427
1444
  }
1428
- else {
1429
- console.log(chalk.yellow(` ${result.message}`));
1430
- console.log(chalk.gray(" Falling back to blank templates..."));
1431
- console.log(chalk.gray(` You can retry later: trellis init --registry ${registry.gigetSource}`));
1432
- }
1433
- }
1434
- // ==========================================================================
1435
- // Resolve workflow template (default: native bundled)
1436
- // ==========================================================================
1437
- const workflowIdInput = options.workflow?.trim();
1438
- const workflowId = workflowIdInput && workflowIdInput.length > 0
1439
- ? workflowIdInput
1440
- : NATIVE_WORKFLOW_ID;
1441
- let workflowMdOverride;
1442
- if (workflowId !== NATIVE_WORKFLOW_ID || options.workflowSource) {
1443
- const resolved = await resolveWorkflowTemplate(workflowId, {
1444
- source: options.workflowSource,
1445
- });
1446
- if (resolved.id !== NATIVE_WORKFLOW_ID) {
1447
- workflowMdOverride = resolved.content;
1448
- console.log(chalk.blue(`🧭 Using workflow template: ${chalk.cyan(resolved.id)}`));
1449
- }
1450
- }
1451
- // ==========================================================================
1452
- // Create Workflow Structure
1453
- // ==========================================================================
1454
- // Record every successful write from here through createRootFiles. The
1455
- // captured set is the source of truth for `.template-hashes.json`'s
1456
- // platform/root entries — replacing the previous "walk every managed dir"
1457
- // approach that swept user-owned runtime files into the manifest
1458
- // (.codex/sessions/, .claude/projects/, pre-existing AGENTS.md).
1459
- const writtenPaths = startRecordingWrites(cwd);
1460
- try {
1461
- // Create workflow structure with project type
1462
- console.log(chalk.blue("📁 Creating workflow structure..."));
1463
- await createWorkflowStructure(cwd, {
1464
- projectType,
1465
- skipSpecTemplates: useRemoteTemplate,
1466
- packages: monorepoPackages,
1467
- remoteSpecPackages,
1468
- workflowMdOverride,
1469
- });
1470
- // Write monorepo packages to config.yaml (non-destructive patch)
1471
- if (monorepoPackages) {
1472
- writeMonorepoConfig(cwd, monorepoPackages);
1473
- console.log(chalk.blue("📦 Monorepo packages written to config.yaml"));
1445
+ // ==========================================================================
1446
+ // Resolve workflow template (default: native bundled)
1447
+ // ==========================================================================
1448
+ const workflowIdInput = options.workflow?.trim();
1449
+ const workflowId = workflowIdInput && workflowIdInput.length > 0
1450
+ ? workflowIdInput
1451
+ : NATIVE_WORKFLOW_ID;
1452
+ let workflowMdOverride;
1453
+ if (workflowId !== NATIVE_WORKFLOW_ID || options.workflowSource) {
1454
+ const resolved = await resolveWorkflowTemplate(workflowId, {
1455
+ source: options.workflowSource,
1456
+ });
1457
+ if (resolved.id !== NATIVE_WORKFLOW_ID) {
1458
+ workflowMdOverride = resolved.content;
1459
+ console.log(chalk.blue(`🧭 Using workflow template: ${chalk.cyan(resolved.id)}`));
1460
+ }
1474
1461
  }
1475
- // Write version file for update tracking
1476
- const versionPath = path.join(cwd, DIR_NAMES.WORKFLOW, ".version");
1477
- fs.writeFileSync(versionPath, VERSION);
1478
- // Configure selected tools by copying entire directories (dogfooding)
1479
- for (const tool of tools) {
1480
- const platformId = resolveCliFlag(tool);
1481
- if (platformId) {
1482
- console.log(chalk.blue(`📝 Configuring ${AI_TOOLS[platformId].name}...`));
1483
- await configurePlatform(platformId, cwd, {
1484
- withStatusline: options.withStatusline,
1485
- });
1486
- if (platformId === "claude-code" && options.withStatusline) {
1487
- console.log(chalk.gray(" ↳ Trellis statusLine installed (--with-statusline)"));
1462
+ // ==========================================================================
1463
+ // Create Workflow Structure
1464
+ // ==========================================================================
1465
+ // Record every successful write from here through createRootFiles. The
1466
+ // captured set is the source of truth for `.template-hashes.json`'s
1467
+ // platform/root entries — replacing the previous "walk every managed dir"
1468
+ // approach that swept user-owned runtime files into the manifest
1469
+ // (.codex/sessions/, .claude/projects/, pre-existing AGENTS.md).
1470
+ const writtenPaths = startRecordingWrites(cwd);
1471
+ try {
1472
+ // Create workflow structure with project type
1473
+ console.log(chalk.blue("📁 Creating workflow structure..."));
1474
+ await createWorkflowStructure(cwd, {
1475
+ projectType,
1476
+ skipSpecTemplates: useRemoteTemplate,
1477
+ packages: monorepoPackages,
1478
+ remoteSpecPackages,
1479
+ workflowMdOverride,
1480
+ });
1481
+ // Write monorepo packages to config.yaml (non-destructive patch)
1482
+ if (monorepoPackages) {
1483
+ writeMonorepoConfig(cwd, monorepoPackages);
1484
+ console.log(chalk.blue("📦 Monorepo packages written to config.yaml"));
1485
+ }
1486
+ // Write version file for update tracking
1487
+ const versionPath = path.join(cwd, DIR_NAMES.WORKFLOW, ".version");
1488
+ fs.writeFileSync(versionPath, VERSION);
1489
+ // Configure selected tools by copying entire directories (dogfooding)
1490
+ for (const tool of tools) {
1491
+ const platformId = resolveCliFlag(tool);
1492
+ if (platformId) {
1493
+ console.log(chalk.blue(`📝 Configuring ${AI_TOOLS[platformId].name}...`));
1494
+ await configurePlatform(platformId, cwd, {
1495
+ withStatusline: options.withStatusline,
1496
+ });
1497
+ if (platformId === "claude-code" && options.withStatusline) {
1498
+ console.log(chalk.gray(" ↳ Trellis statusLine installed (--with-statusline)"));
1499
+ }
1488
1500
  }
1489
1501
  }
1502
+ const pythonPlatforms = getPlatformsWithPythonHooks();
1503
+ const hasSelectedPythonPlatform = pythonPlatforms.some((id) => tools.includes(AI_TOOLS[id].cliFlag));
1504
+ if (hasSelectedPythonPlatform) {
1505
+ logPythonAdaptationNotice(pythonCmd);
1506
+ }
1507
+ // Create root files (skip if exists)
1508
+ await createRootFiles(cwd);
1490
1509
  }
1491
- const pythonPlatforms = getPlatformsWithPythonHooks();
1492
- const hasSelectedPythonPlatform = pythonPlatforms.some((id) => tools.includes(AI_TOOLS[id].cliFlag));
1493
- if (hasSelectedPythonPlatform) {
1494
- logPythonAdaptationNotice(pythonCmd);
1495
- }
1496
- // Create root files (skip if exists)
1497
- await createRootFiles(cwd);
1498
- }
1499
- finally {
1500
- stopRecordingWrites();
1501
- }
1502
- if (registrySpecConfigToPersist) {
1503
- writeSpecRegistryConfig(cwd, registrySpecConfigToPersist);
1504
- }
1505
- // Initialize template hashes for modification tracking
1506
- const hashedCount = initializeHashes(cwd, { trackedPaths: writtenPaths });
1507
- if (useRemoteTemplate) {
1508
- const specFilesToHash = new Map();
1509
- for (const relativePath of collectSpecPaths(cwd)) {
1510
- const content = fs.readFileSync(path.join(cwd, relativePath), "utf-8");
1511
- specFilesToHash.set(relativePath, content);
1510
+ finally {
1511
+ stopRecordingWrites();
1512
1512
  }
1513
- if (specFilesToHash.size > 0) {
1514
- updateHashes(cwd, specFilesToHash);
1513
+ if (registrySpecConfigToPersist) {
1514
+ writeSpecRegistryConfig(cwd, registrySpecConfigToPersist);
1515
1515
  }
1516
- }
1517
- if (hashedCount > 0) {
1518
- console.log(chalk.gray(`📋 Tracking ${hashedCount} template files for updates`));
1519
- }
1520
- // Non-native workflow is user-managed local content. Drop the
1521
- // `.trellis/workflow.md` hash entry so `trellis update` classifies it as
1522
- // modified and does not silently restore native bytes. See design.md
1523
- // "Durable-state contract".
1524
- if (workflowMdOverride !== undefined && workflowId !== NATIVE_WORKFLOW_ID) {
1525
- removeHash(cwd, PATHS.WORKFLOW_GUIDE_FILE);
1526
- }
1527
- // Initialize developer identity (silent - no output)
1528
- if (developerName) {
1529
- try {
1530
- const scriptPath = path.join(cwd, PATHS.SCRIPTS, "init_developer.py");
1531
- execSync(`${pythonCmd} "${scriptPath}" "${developerName}"`, {
1532
- cwd,
1533
- stdio: "pipe", // Silent
1534
- });
1516
+ // Initialize template hashes for modification tracking
1517
+ const hashedCount = initializeHashes(cwd, { trackedPaths: writtenPaths });
1518
+ if (useRemoteTemplate) {
1519
+ const specFilesToHash = new Map();
1520
+ for (const relativePath of collectSpecPaths(cwd)) {
1521
+ const content = fs.readFileSync(path.join(cwd, relativePath), "utf-8");
1522
+ specFilesToHash.set(relativePath, content);
1523
+ }
1524
+ if (specFilesToHash.size > 0) {
1525
+ updateHashes(cwd, specFilesToHash);
1526
+ }
1535
1527
  }
1536
- catch {
1537
- // Silent failure - user can run init_developer.py manually
1528
+ if (hashedCount > 0) {
1529
+ console.log(chalk.gray(`📋 Tracking ${hashedCount} template files for updates`));
1538
1530
  }
1539
- // Three-branch dispatch using flags captured at init() start (before
1540
- // createWorkflowStructure/init_developer ran, so they reflect the disk
1541
- // state of the user's checkout, not the state this init just produced):
1542
- // isFirstInit=true → creator bootstrap (new project)
1543
- // isFirstInit=false + no .developer file → joiner onboarding (fresh clone)
1544
- // isFirstInit=false + .developer exists → same-dev re-init, no task
1545
- //
1546
- // Tasks-empty fallback (issue #204): if .trellis/ exists but tasks dir is
1547
- // empty, the previous init aborted before creating the bootstrap task. Run
1548
- // bootstrap creation regardless of isFirstInit. writeTaskSkeleton is
1549
- // idempotent so repeated triggers are safe.
1550
- //
1551
- // Runs OUTSIDE the init_developer try/catch (which uses stdio: "pipe")
1552
- // so joiner failures surface as warnings instead of being silently
1553
- // swallowed.
1554
- const tasksDir = path.join(cwd, PATHS.TASKS);
1555
- const tasksEmpty = !fs.existsSync(tasksDir) || fs.readdirSync(tasksDir).length === 0;
1556
- if (isFirstInit || tasksEmpty) {
1557
- createBootstrapTask(cwd, developerName, pythonCmd, projectType, monorepoPackages);
1531
+ // Non-native workflow is user-managed local content. Drop the
1532
+ // `.trellis/workflow.md` hash entry so `trellis update` classifies it as
1533
+ // modified and does not silently restore native bytes. See design.md
1534
+ // "Durable-state contract".
1535
+ if (workflowMdOverride !== undefined && workflowId !== NATIVE_WORKFLOW_ID) {
1536
+ removeHash(cwd, PATHS.WORKFLOW_GUIDE_FILE);
1558
1537
  }
1559
- else if (!hadDeveloperFileAtStart) {
1538
+ // Initialize developer identity (silent - no output)
1539
+ if (developerName) {
1560
1540
  try {
1561
- if (!createJoinerOnboardingTask(cwd, developerName, pythonCmd)) {
1562
- console.warn(chalk.yellow("⚠ Failed to create joiner onboarding task"));
1563
- }
1541
+ const scriptPath = path.join(cwd, PATHS.SCRIPTS, "init_developer.py");
1542
+ execSync(`${pythonCmd} "${scriptPath}" "${developerName}"`, {
1543
+ cwd,
1544
+ stdio: "pipe", // Silent
1545
+ });
1564
1546
  }
1565
- catch (err) {
1566
- console.warn(chalk.yellow(`⚠ Joiner onboarding setup failed: ${err instanceof Error ? err.message : String(err)}`));
1547
+ catch {
1548
+ // Silent failure - user can run init_developer.py manually
1549
+ }
1550
+ // Three-branch dispatch using flags captured at init() start (before
1551
+ // createWorkflowStructure/init_developer ran, so they reflect the disk
1552
+ // state of the user's checkout, not the state this init just produced):
1553
+ // isFirstInit=true → creator bootstrap (new project)
1554
+ // isFirstInit=false + no .developer file → joiner onboarding (fresh clone)
1555
+ // isFirstInit=false + .developer exists → same-dev re-init, no task
1556
+ //
1557
+ // Tasks-empty fallback (issue #204): if .trellis/ exists but tasks dir is
1558
+ // empty, the previous init aborted before creating the bootstrap task. Run
1559
+ // bootstrap creation regardless of isFirstInit. writeTaskSkeleton is
1560
+ // idempotent so repeated triggers are safe.
1561
+ //
1562
+ // Runs OUTSIDE the init_developer try/catch (which uses stdio: "pipe")
1563
+ // so joiner failures surface as warnings instead of being silently
1564
+ // swallowed.
1565
+ const tasksEmpty = tasksAreEmpty(cwd);
1566
+ if (isFirstInit || tasksEmpty) {
1567
+ createBootstrapTask(cwd, developerName, pythonCmd, projectType, monorepoPackages);
1568
+ }
1569
+ else if (!hadDeveloperFileAtStart) {
1570
+ try {
1571
+ if (!createJoinerOnboardingTask(cwd, developerName, pythonCmd)) {
1572
+ console.warn(chalk.yellow("⚠ Failed to create joiner onboarding task"));
1573
+ }
1574
+ }
1575
+ catch (err) {
1576
+ console.warn(chalk.yellow(`⚠ Joiner onboarding setup failed: ${err instanceof Error ? err.message : String(err)}`));
1577
+ }
1567
1578
  }
1568
1579
  }
1569
- }
1580
+ });
1570
1581
  }
1571
1582
  /**
1572
1583
  * Simple readline-based input (no flickering like inquirer)