@inditextech/docouture-cli 0.1.0-SNAPSHOT.68.1 → 0.1.0-SNAPSHOT.74.1

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/build/bin.js CHANGED
@@ -7,6 +7,7 @@ import { runBuild } from './commands/build.js';
7
7
  import { runDoctor } from './commands/doctor.js';
8
8
  import { runPublish } from './commands/publish.js';
9
9
  import { runUpgrade } from './commands/upgrade.js';
10
+ import { runBranchModel } from './commands/branch-model.js';
10
11
  import { runEject } from './commands/eject.js';
11
12
  import { runTeardown } from './commands/teardown.js';
12
13
  import { runCompletion } from './commands/completion.js';
@@ -22,6 +23,7 @@ const COMMAND_ORDER = [
22
23
  'publish',
23
24
  'doctor',
24
25
  'upgrade',
26
+ 'branch-model',
25
27
  'eject',
26
28
  'teardown',
27
29
  'completion',
@@ -31,7 +33,9 @@ const COMMAND_INFO = {
31
33
  usageLine: 'new <name>',
32
34
  summary: 'Scaffold a new documentation site',
33
35
  help: `Usage:
34
- docouture new <name> [--dir <path>] [--title <title>] [--mode standalone|versioned] [--pm npm|pnpm] [--yes]
36
+ docouture new <name> [--dir <path>] [--title <title>] [--mode standalone|versioned]
37
+ [--flow trunk-based|git-flow] [--branch <name>] [--integration-branch <name>]
38
+ [--release-branch <name>] [--pm npm|pnpm] [--yes]
35
39
 
36
40
  Scaffold an Antora documentation site into docs/ (and its workflows into
37
41
  .github/workflows/), plus AGENTS.md and agent skills (.opencode/skills/,
@@ -47,6 +51,10 @@ Options:
47
51
  --dir <path> Repository root to scaffold into (default: cwd, or its enclosing repo)
48
52
  --title <title> Site title (default: title-cased from <name>)
49
53
  --mode <mode> 'standalone' (default) or 'versioned'
54
+ --flow <flow> 'trunk-based' (default) or 'git-flow' — see 'docouture branch-model --help'
55
+ --branch <name> Trunk-based only: the one branch (default: main)
56
+ --integration-branch <name> Git-flow only: the prerelease branch (default: develop)
57
+ --release-branch <name> Git-flow only: the release branch (default: main)
50
58
  --pm <pm> 'npm' or 'pnpm' (default: auto-detected from a lockfile/packageManager
51
59
  field, or how docouture itself was invoked)
52
60
  --yes Skip the interactive wizard, use defaults for anything unset
@@ -127,7 +135,9 @@ Check that the environment and site configuration are healthy: Node
127
135
  version, the four names that must agree (component name, start page,
128
136
  content path, package name), git history, that antora is installed,
129
137
  and (advisory only) whether AGENTS.md and the scaffolded skills are
130
- still present.
138
+ still present, whether the docs/release label exists, and whether the
139
+ declared branching model (docs/package.json's docouture.branching)
140
+ agrees with what antora-playbook.yml/docouture-release.yml actually say.
131
141
 
132
142
  Options:
133
143
  --dir <path> Repository root (default: cwd, or its enclosing repo)
@@ -153,6 +163,45 @@ Options:
153
163
  --dir <path> Repository root (default: cwd, or its enclosing repo)
154
164
  --title <title> Override the title read back from docs/antora.yml
155
165
  --dry-run List what would be written without changing anything
166
+ `,
167
+ },
168
+ 'branch-model': {
169
+ usageLine: 'branch-model <model>',
170
+ summary: 'Switch between trunk-based and git-flow branching',
171
+ help: `Usage:
172
+ docouture branch-model <trunk-based|git-flow> [--branch <name>]
173
+ [--integration-branch <name>] [--release-branch <name>] [--dir <path>] [--dry-run]
174
+
175
+ Switch an already-scaffolded repository between the trunk-based and
176
+ git-flow branching models — see docs/src/modules/main/pages/
177
+ guides-branching-model.adoc for the full mechanism. Direction is
178
+ inferred from the site's current branch names (read live from
179
+ antora-playbook.yml/docouture-release.yml, never from a stored config)
180
+ versus the <model> argument given here — the same command handles
181
+ both directions.
182
+
183
+ trunk-based -> git-flow: the current single branch becomes the release
184
+ branch by default (least disruption to anything already tagged off
185
+ it) — --integration-branch <name> (the new prerelease branch) is
186
+ required, since there is nothing on disk to infer that name from.
187
+
188
+ git-flow -> trunk-based: lossy — --branch <name> is required and must
189
+ match one of the two current branches exactly; a third, invented name
190
+ is refused.
191
+
192
+ Re-renders .github/workflows/ (same machinery 'docouture upgrade' uses)
193
+ and patches antora-playbook.yml's content.sources[0].branches and
194
+ docs/package.json's docouture.branching field. Does NOT rename actual
195
+ git branches, touch branch-protection/ruleset rules, or change GitHub's
196
+ configured default branch — these stay manual steps, printed as a
197
+ reminder after every real run.
198
+
199
+ Options:
200
+ --branch <name> Trunk-based target only
201
+ --integration-branch <name> Git-flow target only: the prerelease branch
202
+ --release-branch <name> Git-flow target only: the release branch
203
+ --dir <path> Repository root (default: cwd, or its enclosing repo)
204
+ --dry-run List what would be written without changing anything
156
205
  `,
157
206
  },
158
207
  eject: {
@@ -207,8 +256,8 @@ Examples:
207
256
  // 'docouture <command> --help' uses for a single command.
208
257
  const CLI_SUMMARY = 'Scaffold, build, and publish Antora documentation sites';
209
258
  // Left column width of the command table below, including the 2-space
210
- // indent — wide enough for the longest usageLine ('completion <shell>').
211
- const COMMAND_COLUMN_WIDTH = 22;
259
+ // indent — wide enough for the longest usageLine ('branch-model <model>').
260
+ const COMMAND_COLUMN_WIDTH = 24;
212
261
  const USAGE = `Usage: docouture <command> [options]
213
262
 
214
263
  Commands:
@@ -234,6 +283,7 @@ const RUNNERS = {
234
283
  publish: runPublish,
235
284
  doctor: runDoctor,
236
285
  upgrade: runUpgrade,
286
+ 'branch-model': runBranchModel,
237
287
  eject: runEject,
238
288
  teardown: runTeardown,
239
289
  completion: async (argv) => runCompletion(argv),
@@ -0,0 +1,211 @@
1
+ 'use strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { dirname, join, relative, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { parseArgs } from '../lib/args.js';
7
+ import { readCliInfo } from '../lib/cli-info.js';
8
+ import { copyTemplate, exists } from '../lib/copy-template.js';
9
+ import { detectPackageManager, packageManagerPlan } from '../lib/detect-package-manager.js';
10
+ import { findRepoRoot } from '../lib/repo-root.js';
11
+ import { readBranches, writeBranches } from '../lib/playbook-yml.js';
12
+ import { cacheWarmBranchesYaml, detectBranches } from '../lib/branch-detect.js';
13
+ const BRANCH_MODELS = ['trunk-based', 'git-flow'];
14
+ // Same duplication precedent upgrade.ts's own isInsideGitWorkTree comment
15
+ // documents (doctor-checks.ts already sets it) — a small piece of new.ts's
16
+ // logic reused rather than importing the wizard-heavy module it lives in.
17
+ function isInsideGitWorkTree(dir) {
18
+ try {
19
+ execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, stdio: 'ignore' });
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function readAntoraField(content, key) {
27
+ const match = new RegExp(`^${key}:.*$`, 'm').exec(content);
28
+ if (!match)
29
+ return null;
30
+ const value = match[0].slice(key.length + 1).trim();
31
+ return value.length > 0 ? value : null;
32
+ }
33
+ // The one JSON write this command performs — a single field
34
+ // (`docouture.branching`), read-modify-write rather than a regex patch
35
+ // (unlike antora-playbook.yml/docouture-release.yml, package.json is
36
+ // already JSON, so there is no comment-preservation reason to avoid
37
+ // JSON.parse/stringify here the way playbook-yml.ts avoids a full YAML
38
+ // round-trip). Re-serializes the whole file at 2-space indent — matching
39
+ // every template's own style — rather than only touching the one line, so
40
+ // key ordering is preserved (JSON.parse/stringify both preserve insertion
41
+ // order) but incidental whitespace a human introduced is not.
42
+ async function writeBranching(packageJsonFile, branching) {
43
+ const content = await readFile(packageJsonFile, 'utf8');
44
+ const pkg = JSON.parse(content);
45
+ pkg.docouture = { ...pkg.docouture, branching };
46
+ await writeFile(packageJsonFile, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
47
+ }
48
+ export async function runBranchModel(argv) {
49
+ const { positional, flags } = parseArgs(argv);
50
+ const target = positional[0];
51
+ if (!target || !BRANCH_MODELS.includes(target)) {
52
+ console.error(`usage: docouture branch-model <trunk-based|git-flow> [--branch <name>] [--integration-branch <name>] [--release-branch <name>] [--dir <path>] [--dry-run]`);
53
+ console.error(`invalid target: '${target ?? ''}' — expected 'trunk-based' or 'git-flow'`);
54
+ return 1;
55
+ }
56
+ const targetModel = target;
57
+ const dryRun = flags['dry-run'] === true;
58
+ const branchFlag = typeof flags.branch === 'string' ? flags.branch : undefined;
59
+ const integrationBranchFlag = typeof flags['integration-branch'] === 'string' ? flags['integration-branch'] : undefined;
60
+ const releaseBranchFlag = typeof flags['release-branch'] === 'string' ? flags['release-branch'] : undefined;
61
+ const startDir = typeof flags.dir === 'string' ? resolve(flags.dir) : resolve(process.cwd());
62
+ if (!isInsideGitWorkTree(startDir)) {
63
+ console.error(`'${startDir}' is not inside a git repository`);
64
+ console.error('docouture branch-model re-configures an already-scaffolded repository — run it from your repo root, or pass --dir <path> to one');
65
+ return 1;
66
+ }
67
+ const repoRoot = await findRepoRoot(startDir);
68
+ const siteRoot = join(repoRoot, 'docs');
69
+ if (!(await exists(siteRoot))) {
70
+ console.error(`no site found at '${siteRoot}'`);
71
+ console.error('pass --dir <path> to a component root (the parent of docs/), or run docouture new first');
72
+ return 1;
73
+ }
74
+ // Always derived live, never read back from a stored config — see
75
+ // lib/branch-detect.ts's own comment on why. Falls back to nothing
76
+ // special for a site scaffolded before this feature existed: its
77
+ // antora-playbook.yml/docouture-release.yml already have the exact same
78
+ // shape (one literal branch, `main` in both places), so this is the only
79
+ // path, not a legacy-only fallback.
80
+ const detected = await detectBranches(siteRoot, repoRoot);
81
+ if (!detected.prerelease || !detected.release) {
82
+ console.error('could not determine this site\u2019s current branch names');
83
+ console.error(` antora-playbook.yml content.sources[0].branches: ${detected.prerelease ?? '(not found)'}`);
84
+ console.error(` docouture-release.yml checkout ref: ${detected.release ?? '(not found)'}`);
85
+ console.error('is this a docouture-scaffolded site? (run docouture new first, or check --dir)');
86
+ return 1;
87
+ }
88
+ const currentModel = detected.prerelease === detected.release ? 'trunk-based' : 'git-flow';
89
+ let newPrerelease;
90
+ let newRelease;
91
+ if (targetModel === 'trunk-based') {
92
+ if (currentModel === 'git-flow') {
93
+ if (!branchFlag) {
94
+ console.error('switching from git-flow to trunk-based requires --branch <name>');
95
+ console.error(` must match one of the current two branches: '${detected.prerelease}' or '${detected.release}'`);
96
+ return 1;
97
+ }
98
+ if (branchFlag !== detected.prerelease && branchFlag !== detected.release) {
99
+ console.error(`--branch '${branchFlag}' does not match either current branch`);
100
+ console.error(` current prerelease branch: '${detected.prerelease}'`);
101
+ console.error(` current release branch: '${detected.release}'`);
102
+ console.error('refusing to invent a third branch name — collapsing two branches into one is lossy: pick one of the two that already exist');
103
+ return 1;
104
+ }
105
+ newPrerelease = branchFlag;
106
+ newRelease = branchFlag;
107
+ }
108
+ else {
109
+ const branch = branchFlag ?? detected.release;
110
+ newPrerelease = branch;
111
+ newRelease = branch;
112
+ }
113
+ }
114
+ else {
115
+ if (currentModel === 'trunk-based') {
116
+ // Least-disruption default (see the issue's own design note): the
117
+ // current single branch becomes *release* — whatever already got
118
+ // tagged off it keeps working unchanged — and a NEW name is required
119
+ // for the integration/prerelease branch, since there is nothing
120
+ // already on disk to infer that from.
121
+ if (!integrationBranchFlag) {
122
+ console.error('switching from trunk-based to git-flow requires --integration-branch <name>');
123
+ console.error(` the current branch ('${detected.release}') becomes the release branch by default`);
124
+ console.error(' (override with --release-branch if you want a different name for it too)');
125
+ return 1;
126
+ }
127
+ newPrerelease = integrationBranchFlag;
128
+ newRelease = releaseBranchFlag ?? detected.release;
129
+ }
130
+ else {
131
+ newPrerelease = integrationBranchFlag ?? detected.prerelease;
132
+ newRelease = releaseBranchFlag ?? detected.release;
133
+ }
134
+ }
135
+ if (targetModel === currentModel && newPrerelease === detected.prerelease && newRelease === detected.release) {
136
+ console.log(`already ${currentModel} (${newPrerelease === newRelease ? newPrerelease : `${newPrerelease}/${newRelease}`}) — nothing to do`);
137
+ return 0;
138
+ }
139
+ const here = dirname(fileURLToPath(import.meta.url));
140
+ const templatesRoot = join(here, '..', 'templates');
141
+ const workflowsTemplateDir = join(templatesRoot, 'workflows');
142
+ const workflowsDir = join(repoRoot, '.github', 'workflows');
143
+ const { version: cliVersion } = await readCliInfo(import.meta.url, 2);
144
+ const pm = packageManagerPlan(detectPackageManager(repoRoot));
145
+ const descriptorPath = join(siteRoot, 'src', 'antora.yml');
146
+ let name = 'docs';
147
+ let title = 'Docs';
148
+ if (await exists(descriptorPath)) {
149
+ const content = await readFile(descriptorPath, 'utf8');
150
+ name = readAntoraField(content, 'name') ?? name;
151
+ title = readAntoraField(content, 'title') ?? title;
152
+ }
153
+ const values = {
154
+ name,
155
+ title,
156
+ // Same reasoning as upgrade.ts's own comment: neither placeholder
157
+ // appears in any file this command re-renders (workflows only), so
158
+ // there is nothing meaningful to compute either from.
159
+ componentName: 'unused-by-branch-model',
160
+ cliVersion,
161
+ pmName: pm.pm,
162
+ pmCacheName: pm.cacheName,
163
+ pmLockfile: pm.lockfile,
164
+ pmCiCmd: pm.ciCmd,
165
+ pmSetupStepYaml: pm.setupStepYaml,
166
+ pmPackageManagerField: 'unused-by-branch-model',
167
+ repoIgnoreGlob: 'unused-by-branch-model',
168
+ prereleaseBranch: newPrerelease,
169
+ releaseBranch: newRelease,
170
+ // docs/package.json is patched directly below (writeBranching), not via
171
+ // copyTemplate — this placeholder only ever appears in that file.
172
+ branching: 'unused-by-branch-model',
173
+ cacheWarmBranchesYaml: cacheWarmBranchesYaml(newPrerelease, newRelease),
174
+ };
175
+ const playbookFile = join(siteRoot, 'antora-playbook.yml');
176
+ const packageJsonFile = join(siteRoot, 'package.json');
177
+ if (dryRun) {
178
+ const plannedWorkflows = await copyTemplate(workflowsTemplateDir, workflowsDir, values, { dryRun: true });
179
+ console.log(`would switch ${currentModel} -> ${targetModel}`);
180
+ console.log(` prerelease branch: '${detected.prerelease}' -> '${newPrerelease}'`);
181
+ console.log(` release branch: '${detected.release}' -> '${newRelease}'`);
182
+ console.log('would write:');
183
+ for (const path of [...plannedWorkflows, playbookFile, packageJsonFile]) {
184
+ console.log(` ${relative(repoRoot, path)}`);
185
+ }
186
+ return 0;
187
+ }
188
+ await copyTemplate(workflowsTemplateDir, workflowsDir, values);
189
+ const playbookContent = await readFile(playbookFile, 'utf8');
190
+ // Only rewritten if the prerelease branch actually changed and the line
191
+ // is still there in the shape readBranches/writeBranches expect —
192
+ // guards against a hand-edited playbook this regex can't safely touch.
193
+ if (newPrerelease !== detected.prerelease && readBranches(playbookContent)) {
194
+ await writeFile(playbookFile, writeBranches(playbookContent, newPrerelease), 'utf8');
195
+ }
196
+ await writeBranching(packageJsonFile, targetModel);
197
+ console.log(`switched ${currentModel} -> ${targetModel}`);
198
+ console.log(` prerelease branch: '${detected.prerelease}' -> '${newPrerelease}'`);
199
+ console.log(` release branch: '${detected.release}' -> '${newRelease}'`);
200
+ console.log(`updated ${relative(repoRoot, workflowsDir)}`);
201
+ console.log(`updated ${relative(repoRoot, playbookFile)}`);
202
+ console.log(`updated ${relative(repoRoot, packageJsonFile)}`);
203
+ console.log('');
204
+ console.log('this command does NOT:');
205
+ console.log(' - rename actual git branches');
206
+ console.log(' - touch branch-protection/ruleset rules');
207
+ console.log(" - change GitHub's configured default branch");
208
+ console.log('these are still manual steps — see docs/src/modules/main/pages/guides-branching-model.adoc');
209
+ return 0;
210
+ }
211
+ //# sourceMappingURL=branch-model.js.map
@@ -18,6 +18,7 @@ export const COMMANDS = [
18
18
  'publish',
19
19
  'doctor',
20
20
  'upgrade',
21
+ 'branch-model',
21
22
  'eject',
22
23
  'teardown',
23
24
  'completion',
@@ -7,7 +7,8 @@ import { findRepoRoot } from '../lib/repo-root.js';
7
7
  import { getContext } from '../lib/cli-context.js';
8
8
  import { theme } from '../lib/theme.js';
9
9
  import { readSourceUrl, readStartPageComponent, readStartPath } from '../lib/playbook-yml.js';
10
- import { checkAgentFilesPresent, checkAntoraAvailable, checkGitHasCommit, checkNamesAgree, checkNodeVersion, checkReleaseLabelExists, } from '../lib/doctor-checks.js';
10
+ import { detectBranches, inferBranching } from '../lib/branch-detect.js';
11
+ import { checkAgentFilesPresent, checkAntoraAvailable, checkBranchingAgrees, checkGitHasCommit, checkNamesAgree, checkNodeVersion, checkReleaseLabelExists, } from '../lib/doctor-checks.js';
11
12
  async function readJson(file) {
12
13
  try {
13
14
  return JSON.parse(await readFile(file, 'utf8'));
@@ -148,6 +149,15 @@ export async function runDoctor(argv) {
148
149
  record(report, 'release', releaseResult, 'warn');
149
150
  if (!json)
150
151
  printAdvisory(releaseResult);
152
+ log('branching');
153
+ const detectedBranches = await detectBranches(siteRoot, target);
154
+ const branchingResult = checkBranchingAgrees({
155
+ declaredBranching: pkg?.docouture?.branching ?? null,
156
+ actualBranching: inferBranching(detectedBranches),
157
+ });
158
+ record(report, 'branching', branchingResult, 'warn');
159
+ if (!json)
160
+ printAdvisory(branchingResult);
151
161
  if (json) {
152
162
  process.stdout.write(`${JSON.stringify({ status: status === 0 ? 'ok' : 'fail', checks: report }, null, 2)}\n`);
153
163
  }
@@ -11,6 +11,7 @@ import { copyTemplate, exists, isEmptyOrMissing, renderTemplateFile, writeTempla
11
11
  import { detectPackageManager, packageManagerPlan, } from '../lib/detect-package-manager.js';
12
12
  import { findRepoRoot } from '../lib/repo-root.js';
13
13
  import { AGENTS_MD_FILENAME, hasManagedSection, mergeAgentsMd } from '../lib/agents-md.js';
14
+ import { cacheWarmBranchesYaml } from '../lib/branch-detect.js';
14
15
  // Matches the rule an npm package name (and, not coincidentally, an Antora
15
16
  // component name — both end up as URL segments) can safely be: this is
16
17
  // stricter than npm's own rule, which also allows dots and a leading `@scope/`
@@ -30,6 +31,15 @@ const NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
30
31
  // here: it only ever comes up as an ad-hoc `docouture dev` preview before a mode
31
32
  // is chosen, never as something worth releasing.)
32
33
  const MODES = ['standalone', 'versioned'];
34
+ // The two branching models `docouture new` can scaffold for — see the
35
+ // guides-branching-model guide, and GH #175. 'trunk-based' (the default):
36
+ // one branch plays both roles (prerelease AND release) — the degenerate
37
+ // case, not a second code path anywhere prereleaseBranch/releaseBranch are
38
+ // used. 'git-flow': two independently-named branches, e.g. `develop`
39
+ // (prerelease) and `main` (release) — see antora-playbook.yml's own comment
40
+ // on what "prerelease branch" means, and docouture-release.yml's on
41
+ // "release branch".
42
+ const BRANCH_MODELS = ['trunk-based', 'git-flow'];
33
43
  const PACKAGE_MANAGERS = ['npm', 'pnpm'];
34
44
  // Filenames docouture-publish.yml / docouture-publish-prerelease.yml /
35
45
  // docouture-release.yml / docouture-release-preview.yml / docouture-pr-verify.yml /
@@ -229,6 +239,47 @@ async function promptWizard(io, initial, defaults) {
229
239
  },
230
240
  ],
231
241
  }, context));
242
+ // Branching model — see BRANCH_MODELS' own comment. Unconditional, same
243
+ // as Versioning mode above; the follow-up branch-name question(s) below
244
+ // are this wizard's first CONDITIONAL prompt.
245
+ const branching = initial.branching ??
246
+ (await select({
247
+ message: 'Branching model:\n',
248
+ default: 'trunk-based',
249
+ choices: [
250
+ {
251
+ name: 'Trunk-based',
252
+ value: 'trunk-based',
253
+ description: 'One long-lived branch plays both the prerelease and release roles — e.g. main.',
254
+ },
255
+ {
256
+ name: 'Git-flow',
257
+ value: 'git-flow',
258
+ description: 'Two independently-named branches — an integration branch (e.g. develop) for prerelease, ' +
259
+ 'a release branch (e.g. main) for cutting releases.',
260
+ },
261
+ ],
262
+ }, context));
263
+ let prereleaseBranch;
264
+ let releaseBranch;
265
+ if (branching === 'git-flow') {
266
+ releaseBranch = (await input({
267
+ message: 'Release branch (docouture-release.yml checks this out and cuts tags from it):\n',
268
+ default: initial.releaseBranch ?? 'main',
269
+ }, context)).trim();
270
+ prereleaseBranch = (await input({
271
+ message: 'Integration/prerelease branch (antora-playbook.yml tracks this as the live prerelease version):\n',
272
+ default: initial.prereleaseBranch ?? 'develop',
273
+ }, context)).trim();
274
+ }
275
+ else {
276
+ const branch = (await input({
277
+ message: 'Branch (plays both the prerelease and release roles):\n',
278
+ default: initial.branch ?? 'main',
279
+ }, context)).trim();
280
+ prereleaseBranch = branch;
281
+ releaseBranch = branch;
282
+ }
232
283
  const pm = initial.pm ??
233
284
  (await select({
234
285
  message: 'Package manager:\n',
@@ -238,7 +289,16 @@ async function promptWizard(io, initial, defaults) {
238
289
  { name: 'pnpm', value: 'pnpm' },
239
290
  ],
240
291
  }, context));
241
- return { name, title: title.length > 0 ? title : defaultTitle, urlSegment, mode, pm };
292
+ return {
293
+ name,
294
+ title: title.length > 0 ? title : defaultTitle,
295
+ urlSegment,
296
+ mode,
297
+ branching,
298
+ prereleaseBranch,
299
+ releaseBranch,
300
+ pm,
301
+ };
242
302
  }
243
303
  export async function runNew(argv, io = defaultIO()) {
244
304
  const { positional, flags } = parseArgs(argv);
@@ -248,6 +308,12 @@ export async function runNew(argv, io = defaultIO()) {
248
308
  // already "off", so the only thing worth scripting is turning it on.
249
309
  let urlSegment = flags['url-segment'] === true ? true : undefined;
250
310
  let mode;
311
+ let branching;
312
+ let prereleaseBranch;
313
+ let releaseBranch;
314
+ const branchFlag = typeof flags.branch === 'string' ? flags.branch : undefined;
315
+ const integrationBranchFlag = typeof flags['integration-branch'] === 'string' ? flags['integration-branch'] : undefined;
316
+ const releaseBranchFlag = typeof flags['release-branch'] === 'string' ? flags['release-branch'] : undefined;
251
317
  let pmChoice;
252
318
  if (typeof flags.mode === 'string') {
253
319
  if (!MODES.includes(flags.mode)) {
@@ -256,6 +322,13 @@ export async function runNew(argv, io = defaultIO()) {
256
322
  }
257
323
  mode = flags.mode;
258
324
  }
325
+ if (typeof flags.flow === 'string') {
326
+ if (!BRANCH_MODELS.includes(flags.flow)) {
327
+ console.error(`invalid --flow: '${flags.flow}' — expected 'trunk-based' or 'git-flow'`);
328
+ return 1;
329
+ }
330
+ branching = flags.flow;
331
+ }
259
332
  if (typeof flags.pm === 'string') {
260
333
  if (!PACKAGE_MANAGERS.includes(flags.pm)) {
261
334
  console.error(`invalid --pm: '${flags.pm}' — expected 'npm' or 'pnpm'`);
@@ -275,19 +348,48 @@ export async function runNew(argv, io = defaultIO()) {
275
348
  // gets exactly today's non-interactive behaviour, defaults and all.
276
349
  const skipWizard = flags.yes === true || !io.isTTY;
277
350
  if (!skipWizard) {
278
- const answers = await promptWizard(io, { name, title, urlSegment, mode, pm: pmChoice }, { pm: pmGuess });
351
+ const answers = await promptWizard(io, {
352
+ name,
353
+ title,
354
+ urlSegment,
355
+ mode,
356
+ branching,
357
+ branch: branchFlag,
358
+ prereleaseBranch: integrationBranchFlag,
359
+ releaseBranch: releaseBranchFlag,
360
+ pm: pmChoice,
361
+ }, { pm: pmGuess });
279
362
  name = answers.name;
280
363
  title = answers.title;
281
364
  urlSegment = answers.urlSegment;
282
365
  mode = answers.mode;
366
+ branching = answers.branching;
367
+ prereleaseBranch = answers.prereleaseBranch;
368
+ releaseBranch = answers.releaseBranch;
283
369
  pmChoice = answers.pm;
284
370
  io.output.write('\n');
285
371
  }
286
372
  mode = mode ?? 'standalone';
373
+ branching = branching ?? 'trunk-based';
287
374
  pmChoice = pmChoice ?? pmGuess;
288
375
  urlSegment = urlSegment ?? false;
376
+ // Non-interactive path (--yes, or no TTY): resolve from flags/defaults
377
+ // the same way the wizard would have, rather than leaving these unset —
378
+ // trunk-based collapses to one branch name playing both roles, same as
379
+ // BRANCH_MODELS' own comment describes.
380
+ if (branching === 'git-flow') {
381
+ prereleaseBranch = prereleaseBranch ?? integrationBranchFlag ?? 'develop';
382
+ releaseBranch = releaseBranch ?? releaseBranchFlag ?? 'main';
383
+ }
384
+ else {
385
+ const branch = branchFlag ?? 'main';
386
+ prereleaseBranch = prereleaseBranch ?? branch;
387
+ releaseBranch = releaseBranch ?? branch;
388
+ }
289
389
  if (!name) {
290
- console.error('usage: docouture new <name> [--dir <path>] [--title <title>] [--url-segment] [--mode standalone|versioned] [--pm npm|pnpm]');
390
+ console.error('usage: docouture new <name> [--dir <path>] [--title <title>] [--url-segment] [--mode standalone|versioned] ' +
391
+ '[--flow trunk-based|git-flow] [--branch <name>] [--integration-branch <name>] [--release-branch <name>] ' +
392
+ '[--pm npm|pnpm]');
291
393
  return 1;
292
394
  }
293
395
  if (!NAME_PATTERN.test(name)) {
@@ -396,6 +498,10 @@ export async function runNew(argv, io = defaultIO()) {
396
498
  pmSetupStepYaml: pm.setupStepYaml,
397
499
  pmPackageManagerField: pm.packageManagerField,
398
500
  repoIgnoreGlob: repoIgnoreGlob(target),
501
+ prereleaseBranch,
502
+ releaseBranch,
503
+ branching,
504
+ cacheWarmBranchesYaml: cacheWarmBranchesYaml(prereleaseBranch, releaseBranch),
399
505
  };
400
506
  // The whole starter subtree — package.json, antora-playbook.yml, its own
401
507
  // nested src/antora.yml — lands under <repo-root>/docs/ as one piece,
@@ -418,7 +524,17 @@ export async function runNew(argv, io = defaultIO()) {
418
524
  await writeTemplateFile(join(starterDir, 'antora-playbook.versioned.yml'), join(docsDir, 'antora-playbook.yml'), values);
419
525
  await writeTemplateFile(join(starterDir, 'src', 'release-version.versioned'), join(docsDir, '.release-version'), values);
420
526
  }
421
- printNextSteps({ mode, pm, target, docsDir, workflowsDir, ghPagesUrl: githubPagesUrl(target) });
527
+ printNextSteps({
528
+ mode,
529
+ branching,
530
+ prereleaseBranch,
531
+ releaseBranch,
532
+ pm,
533
+ target,
534
+ docsDir,
535
+ workflowsDir,
536
+ ghPagesUrl: githubPagesUrl(target),
537
+ });
422
538
  return 0;
423
539
  }
424
540
  // Everything printed after scaffolding finishes, grouped into labeled
@@ -426,7 +542,7 @@ export async function runNew(argv, io = defaultIO()) {
426
542
  // "created X" lines. Headers use the shared theme's bold, matching bin.ts's
427
543
  // own banner — see lib/theme.ts for why that's not picocolors directly.
428
544
  function printNextSteps(args) {
429
- const { mode, pm, target, docsDir, workflowsDir, ghPagesUrl } = args;
545
+ const { mode, branching, prereleaseBranch, releaseBranch, pm, target, docsDir, workflowsDir, ghPagesUrl } = args;
430
546
  const created = (path) => ` ${theme.success('✓')} ${path}`;
431
547
  // Relative to `target` (the repo root), not process.cwd() — cwd may be a
432
548
  // nested subdirectory findRepoRoot walked up from (see runNew), in which
@@ -453,9 +569,22 @@ function printNextSteps(args) {
453
569
  console.log(' docouture-documenting-changes, docouture-authoring-guides, docouture-writing-docs-pages,');
454
570
  console.log(' docouture-docs-internals and, for a versioned-mode site, docouture-docs-versioning)');
455
571
  console.log('');
572
+ if (branching === 'git-flow') {
573
+ console.log(theme.bold('Branching: Git-flow'));
574
+ console.log(` ${prereleaseBranch} is the prerelease/integration branch; ${releaseBranch} is the release branch.`);
575
+ console.log(` docouture-release.yml only runs against ${releaseBranch}; ordinary content merges into`);
576
+ console.log(` ${prereleaseBranch} publish continuously via docouture-publish-prerelease.yml.`);
577
+ }
578
+ else {
579
+ console.log(theme.bold('Branching: Trunk-based'));
580
+ console.log(` ${releaseBranch} plays both the prerelease and release roles.`);
581
+ }
582
+ console.log(' See docs/src/modules/main/pages/guides-branching-model.adoc for the full lifecycle,');
583
+ console.log(" and 'docouture branch-model' to switch later.");
584
+ console.log('');
456
585
  if (mode === 'versioned') {
457
586
  console.log(theme.bold('Versioning: Versioned (Full History)'));
458
- console.log(' main is the prerelease channel.');
587
+ console.log(` ${prereleaseBranch} is the prerelease channel.`);
459
588
  console.log('');
460
589
  console.log(' To cut your first release:');
461
590
  console.log(" 1. Create the 'docs/release' label (once): gh label create docs/release");
@@ -470,11 +599,11 @@ function printNextSteps(args) {
470
599
  }
471
600
  else {
472
601
  console.log(theme.bold('Versioning: Standalone (Stable + Prerelease)'));
473
- console.log(' main is the prerelease channel.');
602
+ console.log(` ${prereleaseBranch} is the prerelease channel.`);
474
603
  console.log('');
475
604
  console.log(' To cut your first stable release:');
476
605
  console.log(" 1. Create the 'docs/release' label (once): gh label create docs/release");
477
- console.log(" 2. Merge any PR labeled 'docs/release' into main — docouture-release.yml runs automatically");
606
+ console.log(` 2. Merge any PR labeled 'docs/release' into ${releaseBranch} — docouture-release.yml runs automatically`);
478
607
  console.log('');
479
608
  console.log(' (or skip the label/PR entirely: run docouture-release.yml manually via workflow_dispatch,');
480
609
  console.log(' default input is fine)');
@@ -10,6 +10,7 @@ import { detectPackageManager, packageManagerPlan } from '../lib/detect-package-
10
10
  import { resolveConfig } from '../lib/config-resolver.js';
11
11
  import { findRepoRoot } from '../lib/repo-root.js';
12
12
  import { AGENTS_MD_FILENAME, mergeAgentsMd } from '../lib/agents-md.js';
13
+ import { cacheWarmBranchesYaml, detectBranches } from '../lib/branch-detect.js';
13
14
  // Same check `new.ts` uses, duplicated rather than imported — it's three
14
15
  // lines, and `doctor-checks.ts` already sets the precedent of duplicating a
15
16
  // small piece of `new.ts`'s own logic instead of reaching across into a
@@ -93,6 +94,18 @@ export async function runUpgrade(argv) {
93
94
  // --title flag > the value already recorded in docs/antora.yml > a plain
94
95
  // title-cased fallback computed from the name above.
95
96
  const { title } = resolveConfig({ title: titleCase(name) }, { title: fromYmlTitle }, { title: typeof flags.title === 'string' ? flags.title : undefined });
97
+ // Re-derived live too, same reasoning as name/title above — upgrade
98
+ // re-copies every workflow template (unlike docs/), and those templates
99
+ // now carry __DOCOUTURE_PRERELEASE_BRANCH__/__DOCOUTURE_RELEASE_BRANCH__
100
+ // (GH #175) — real branch names are required here, not stubs, or an
101
+ // upgrade would silently overwrite a git-flow site's workflows back to
102
+ // whatever these placeholders' un-substituted default would be. See
103
+ // lib/branch-detect.ts's own comment on why these are derived live
104
+ // rather than read back from a stored config.
105
+ const siteRoot = join(target, 'docs');
106
+ const detected = await detectBranches(siteRoot, target);
107
+ const prereleaseBranch = detected.prerelease ?? 'main';
108
+ const releaseBranch = detected.release ?? 'main';
96
109
  const values = {
97
110
  name,
98
111
  title,
@@ -119,6 +132,13 @@ export async function runUpgrade(argv) {
119
132
  // pattern matching every URL, which would be a silent, dangerous no-op
120
133
  // if this value were ever actually substituted somewhere.
121
134
  repoIgnoreGlob: 'unused-by-upgrade',
135
+ prereleaseBranch,
136
+ releaseBranch,
137
+ // docs/package.json (the only template file this placeholder appears
138
+ // in) is never re-copied by upgrade — same reasoning as
139
+ // pmPackageManagerField/repoIgnoreGlob above.
140
+ branching: 'unused-by-upgrade',
141
+ cacheWarmBranchesYaml: cacheWarmBranchesYaml(prereleaseBranch, releaseBranch),
122
142
  };
123
143
  const workflowsDir = join(target, '.github', 'workflows');
124
144
  const agentsMdFile = join(target, AGENTS_MD_FILENAME);