@inditextech/docouture-cli 0.1.0-SNAPSHOT.40.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 +321 -0
- package/build/commands/build.js +22 -0
- package/build/commands/completion.js +69 -0
- package/build/commands/dev.js +67 -0
- package/build/commands/doctor.js +156 -0
- package/build/commands/eject.js +71 -0
- package/build/commands/new.js +512 -0
- package/build/commands/publish.js +111 -0
- package/build/commands/teardown.js +66 -0
- package/build/commands/upgrade.js +156 -0
- package/build/commands/version.js +60 -0
- package/build/lib/agents-md.js +67 -0
- package/build/lib/antora-log.js +36 -0
- package/build/lib/antora-yml.js +47 -0
- package/build/lib/args.js +26 -0
- package/build/lib/cli-context.js +17 -0
- package/build/lib/cli-info.js +20 -0
- package/build/lib/config-resolver.js +23 -0
- package/build/lib/copy-template.js +162 -0
- package/build/lib/debug-log.js +21 -0
- package/build/lib/detect-package-manager.js +79 -0
- package/build/lib/dev-server.js +337 -0
- package/build/lib/doctor-checks.js +240 -0
- package/build/lib/global-flags.js +31 -0
- package/build/lib/kroki-compose.js +54 -0
- package/build/lib/playbook-yml.js +90 -0
- package/build/lib/repo-root.js +21 -0
- package/build/lib/run-script.js +50 -0
- package/build/lib/theme.js +51 -0
- package/build/templates/agent-support/.claude/skills/docs-internals/SKILL.md +53 -0
- package/build/templates/agent-support/.claude/skills/docs-internals/reference/antora-extensions.md +54 -0
- package/build/templates/agent-support/.claude/skills/docs-internals/reference/naming.md +41 -0
- package/build/templates/agent-support/.claude/skills/docs-internals/reference/page-patterns.md +238 -0
- package/build/templates/agent-support/.claude/skills/docs-versioning.versioned/SKILL.md +37 -0
- package/build/templates/agent-support/.claude/skills/docs-versioning.versioned/reference/releasing.md +106 -0
- package/build/templates/agent-support/.claude/skills/documenting-your-repo/SKILL.md +55 -0
- package/build/templates/agent-support/.claude/skills/documenting-your-repo/reference/content-sourcing.md +55 -0
- package/build/templates/agent-support/.claude/skills/documenting-your-repo/reference/maintenance-loop.md +68 -0
- package/build/templates/agent-support/.claude/skills/documenting-your-repo/reference/structure-planning.md +102 -0
- package/build/templates/agent-support/.claude/skills/writing-docs-pages/SKILL.md +112 -0
- package/build/templates/agent-support/.claude/skills/writing-docs-pages/reference/docouture-blocks.md +170 -0
- package/build/templates/agent-support/.claude/skills/writing-docs-pages/reference/language-basics.md +402 -0
- package/build/templates/agent-support/.opencode/skills/docs-internals/SKILL.md +53 -0
- package/build/templates/agent-support/.opencode/skills/docs-internals/reference/antora-extensions.md +54 -0
- package/build/templates/agent-support/.opencode/skills/docs-internals/reference/naming.md +41 -0
- package/build/templates/agent-support/.opencode/skills/docs-internals/reference/page-patterns.md +238 -0
- package/build/templates/agent-support/.opencode/skills/docs-versioning.versioned/SKILL.md +37 -0
- package/build/templates/agent-support/.opencode/skills/docs-versioning.versioned/reference/releasing.md +106 -0
- package/build/templates/agent-support/.opencode/skills/documenting-your-repo/SKILL.md +55 -0
- package/build/templates/agent-support/.opencode/skills/documenting-your-repo/reference/content-sourcing.md +55 -0
- package/build/templates/agent-support/.opencode/skills/documenting-your-repo/reference/maintenance-loop.md +68 -0
- package/build/templates/agent-support/.opencode/skills/documenting-your-repo/reference/structure-planning.md +102 -0
- package/build/templates/agent-support/.opencode/skills/writing-docs-pages/SKILL.md +112 -0
- package/build/templates/agent-support/.opencode/skills/writing-docs-pages/reference/docouture-blocks.md +170 -0
- package/build/templates/agent-support/.opencode/skills/writing-docs-pages/reference/language-basics.md +402 -0
- package/build/templates/agent-support/AGENTS.md +72 -0
- package/build/templates/starter/antora-playbook.local.yml +99 -0
- package/build/templates/starter/antora-playbook.versioned.yml +167 -0
- package/build/templates/starter/antora-playbook.yml +189 -0
- package/build/templates/starter/gitignore +3 -0
- package/build/templates/starter/package.json +37 -0
- package/build/templates/starter/scripts/check-links.mjs +178 -0
- package/build/templates/starter/src/antora.yml +85 -0
- package/build/templates/starter/src/modules/ROOT/images/card-placeholder.png +0 -0
- package/build/templates/starter/src/modules/ROOT/images/feature-placeholder-dark.png +0 -0
- package/build/templates/starter/src/modules/ROOT/images/feature-placeholder.png +0 -0
- package/build/templates/starter/src/modules/ROOT/images/hero-placeholder.png +0 -0
- package/build/templates/starter/src/modules/ROOT/pages/index.adoc +124 -0
- package/build/templates/starter/src/modules/main/nav.adoc +19 -0
- package/build/templates/starter/src/modules/main/pages/app.adoc +5 -0
- package/build/templates/starter/src/modules/main/pages/architecture.adoc +6 -0
- package/build/templates/starter/src/modules/main/pages/changelog/index.adoc +9 -0
- package/build/templates/starter/src/modules/main/pages/components.adoc +701 -0
- package/build/templates/starter/src/modules/main/pages/contributing.adoc +6 -0
- package/build/templates/starter/src/modules/main/pages/getting-started.adoc +169 -0
- package/build/templates/starter/src/modules/main/pages/glossary.adoc +8 -0
- package/build/templates/starter/src/modules/main/pages/index.adoc +9 -0
- package/build/templates/starter/src/modules/main/pages/prerequisites.adoc +55 -0
- package/build/templates/starter/src/modules/main/pages/quickstart.adoc +21 -0
- package/build/templates/starter/src/modules/main/pages/release-notes/index.adoc +9 -0
- package/build/templates/starter/src/modules/main/pages/requirements.adoc +8 -0
- package/build/templates/starter/src/modules/main/pages/roadmap.adoc +4 -0
- package/build/templates/starter/src/modules/main/pages/structure.adoc +5 -0
- package/build/templates/starter/src/release-version.versioned +1 -0
- package/build/templates/starter/supplemental-ui/favicon.ico +0 -0
- package/build/templates/starter/supplemental-ui/product-logo-dark.png +0 -0
- package/build/templates/starter/supplemental-ui/product-logo.png +0 -0
- package/build/templates/workflows/docouture-kroki-cache-warm.yml +101 -0
- package/build/templates/workflows/docouture-pr-verify.yml +120 -0
- package/build/templates/workflows/docouture-publish-prerelease.yml +122 -0
- package/build/templates/workflows/docouture-publish.yml +138 -0
- package/build/templates/workflows/docouture-release-preview.yml +198 -0
- package/build/templates/workflows/docouture-release.yml +414 -0
- package/package.json +38 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { copyFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { parseArgs } from '../lib/args.js';
|
|
5
|
+
import { exists } from '../lib/copy-template.js';
|
|
6
|
+
import { findRepoRoot } from '../lib/repo-root.js';
|
|
7
|
+
import { PACKAGE_NAME, RESOURCE, OVERRIDE_FILENAME, resolveBundledComposeFile } from '../lib/kroki-compose.js';
|
|
8
|
+
// `docouture eject kroki` copies the Kroki + mermaid-companion `docker compose`
|
|
9
|
+
// definition @inditextech/docouture-antora-extensions' `kroki-prewarm.js` starts
|
|
10
|
+
// automatically (GH-44) out to the site's own repository, as
|
|
11
|
+
// `docs/kroki-compose.yml`. kroki-prewarm.js checks that exact path first,
|
|
12
|
+
// before its own bundled default — see its own header, and kroki-docker.js's
|
|
13
|
+
// — so a site that runs this once and edits the result (a different image
|
|
14
|
+
// version, a companion for another diagram type, ...) never needs to fork
|
|
15
|
+
// or patch the package itself; the auto-start logic just picks up whatever
|
|
16
|
+
// is there. `docouture teardown kroki` is the matching stop side — see that
|
|
17
|
+
// command's own header.
|
|
18
|
+
//
|
|
19
|
+
// A site that never added `kroki-enabled: true` (and so never installed
|
|
20
|
+
// @inditextech/docouture-antora-extensions, or has an old version predating this
|
|
21
|
+
// resource) gets a clear error telling it what to add, rather than a raw
|
|
22
|
+
// `MODULE_NOT_FOUND` — see lib/kroki-compose.js's own header for why
|
|
23
|
+
// resolution goes through the site's own install rather than this CLI's.
|
|
24
|
+
const SUPPORTED_TARGETS = {
|
|
25
|
+
kroki: {
|
|
26
|
+
resolve: resolveBundledComposeFile,
|
|
27
|
+
description: 'Kroki + mermaid-companion docker compose definition (docs/kroki-compose.yml)',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
export async function runEject(argv, deps = {}) {
|
|
31
|
+
const { positional, flags } = parseArgs(argv);
|
|
32
|
+
const target = positional[0];
|
|
33
|
+
if (!target || !(target in SUPPORTED_TARGETS)) {
|
|
34
|
+
console.error('usage: docouture eject <target> [--dir <path>]');
|
|
35
|
+
console.error('supported targets:');
|
|
36
|
+
for (const [name, { description }] of Object.entries(SUPPORTED_TARGETS)) {
|
|
37
|
+
console.error(` ${name} — ${description}`);
|
|
38
|
+
}
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const startDir = typeof flags.dir === 'string' ? resolve(flags.dir) : resolve(process.cwd());
|
|
42
|
+
const repoRoot = await findRepoRoot(startDir);
|
|
43
|
+
const siteRoot = join(repoRoot, 'docs');
|
|
44
|
+
const packageJsonFile = join(siteRoot, 'package.json');
|
|
45
|
+
if (!(await exists(packageJsonFile))) {
|
|
46
|
+
console.error(`no package.json found at '${siteRoot}'`);
|
|
47
|
+
console.error('pass --dir <path> to a component root (the parent of docs/), or run docouture new first');
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const destFile = join(siteRoot, OVERRIDE_FILENAME);
|
|
51
|
+
if (await exists(destFile)) {
|
|
52
|
+
console.error(`'${destFile}' already exists`);
|
|
53
|
+
console.error('remove it first if you want to re-eject the bundled default');
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
const resolveFile = deps.resolveBundledComposeFile || resolveBundledComposeFile;
|
|
57
|
+
let sourceFile;
|
|
58
|
+
try {
|
|
59
|
+
sourceFile = resolveFile(packageJsonFile);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
console.error(`could not find ${RESOURCE} in ${PACKAGE_NAME}`);
|
|
63
|
+
console.error(`add it to ${packageJsonFile}'s devDependencies, e.g.: npm install --save-dev ${PACKAGE_NAME}`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
await copyFile(sourceFile, destFile);
|
|
67
|
+
console.log(`wrote ${destFile}`);
|
|
68
|
+
console.log('edit it freely — the next build automatically prefers it over the bundled default');
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=eject.js.map
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { theme } from '../lib/theme.js';
|
|
8
|
+
import { parseArgs } from '../lib/args.js';
|
|
9
|
+
import { readCliInfo } from '../lib/cli-info.js';
|
|
10
|
+
import { copyTemplate, exists, isEmptyOrMissing, renderTemplateFile, writeTemplateFile } from '../lib/copy-template.js';
|
|
11
|
+
import { detectPackageManager, packageManagerPlan, } from '../lib/detect-package-manager.js';
|
|
12
|
+
import { findRepoRoot } from '../lib/repo-root.js';
|
|
13
|
+
import { AGENTS_MD_FILENAME, hasManagedSection, mergeAgentsMd } from '../lib/agents-md.js';
|
|
14
|
+
// Matches the rule an npm package name (and, not coincidentally, an Antora
|
|
15
|
+
// component name — both end up as URL segments) can safely be: this is
|
|
16
|
+
// stricter than npm's own rule, which also allows dots and a leading `@scope/`
|
|
17
|
+
// that makes no sense for a directory name here.
|
|
18
|
+
const NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
19
|
+
// The only two versioning shapes `docouture new` scaffolds — both real,
|
|
20
|
+
// releasable configurations handled by the templated docouture-release.yml — see
|
|
21
|
+
// the docs-site-package skill's reference/versioning-modes.md. 'standalone'
|
|
22
|
+
// (the default): `main` always builds as the prerelease/preview version, and
|
|
23
|
+
// a release just force-moves a rolling `stable` tag — no historical archive,
|
|
24
|
+
// appropriate for a product where only "now" and "what's next" matter.
|
|
25
|
+
// 'versioned': `main` builds as the prerelease version too — docs/antora.yml
|
|
26
|
+
// is identical to the standalone shape on main, for both modes — but every
|
|
27
|
+
// release is instead its own immutable `vX.Y.Z` git tag, kept forever —
|
|
28
|
+
// appropriate for a library/SDK whose consumers pin an old version. (A bare,
|
|
29
|
+
// unversioned checkout — no prerelease/stable split at all — is not offered
|
|
30
|
+
// here: it only ever comes up as an ad-hoc `docouture dev` preview before a mode
|
|
31
|
+
// is chosen, never as something worth releasing.)
|
|
32
|
+
const MODES = ['standalone', 'versioned'];
|
|
33
|
+
const PACKAGE_MANAGERS = ['npm', 'pnpm'];
|
|
34
|
+
// Filenames docouture-publish.yml / docouture-publish-prerelease.yml /
|
|
35
|
+
// docouture-release.yml / docouture-release-preview.yml / docouture-pr-verify.yml /
|
|
36
|
+
// docouture-kroki-cache-warm.yml are templated under (see templates/workflows/)
|
|
37
|
+
// — kept as a literal list here so the pre-flight conflict check below can
|
|
38
|
+
// name exactly which ones would be overwritten without having to read the
|
|
39
|
+
// template directory to find out.
|
|
40
|
+
const WORKFLOW_NAMES = [
|
|
41
|
+
'docouture-publish.yml',
|
|
42
|
+
'docouture-publish-prerelease.yml',
|
|
43
|
+
'docouture-release.yml',
|
|
44
|
+
'docouture-release-preview.yml',
|
|
45
|
+
'docouture-pr-verify.yml',
|
|
46
|
+
'docouture-kroki-cache-warm.yml',
|
|
47
|
+
];
|
|
48
|
+
// Repo-root-relative paths `templates/agent-support/` lands under (see
|
|
49
|
+
// new.ts's own copyTemplate call below) — kept as a literal list, same
|
|
50
|
+
// reasoning as WORKFLOW_NAMES above, so the pre-flight conflict check can
|
|
51
|
+
// name exactly what would be overwritten. `docs-versioning` is deliberately
|
|
52
|
+
// absent here: it is scaffolded only under `--mode versioned` (see the
|
|
53
|
+
// `.versioned`-marker copy further down), so an existing standalone-mode
|
|
54
|
+
// site with no such directory yet is never blocked by this check on it.
|
|
55
|
+
const AGENT_SUPPORT_PATHS = [
|
|
56
|
+
join('.opencode', 'skills', 'documenting-your-repo'),
|
|
57
|
+
join('.opencode', 'skills', 'writing-docs-pages'),
|
|
58
|
+
join('.opencode', 'skills', 'docs-internals'),
|
|
59
|
+
join('.claude', 'skills', 'documenting-your-repo'),
|
|
60
|
+
join('.claude', 'skills', 'writing-docs-pages'),
|
|
61
|
+
join('.claude', 'skills', 'docs-internals'),
|
|
62
|
+
];
|
|
63
|
+
function titleCase(name) {
|
|
64
|
+
return name
|
|
65
|
+
.split('-')
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
68
|
+
.join(' ');
|
|
69
|
+
}
|
|
70
|
+
function isInsideGitWorkTree(dir) {
|
|
71
|
+
try {
|
|
72
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, stdio: 'ignore' });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// origin's remote URL as git reports it, or undefined if there isn't one
|
|
80
|
+
// configured yet (a fresh `git init` with no remote added) — the shared
|
|
81
|
+
// starting point for both repoWebUrl and githubPagesUrl below, so there's
|
|
82
|
+
// only one execFileSync call to fail/mock, not two.
|
|
83
|
+
function originRemoteUrl(dir) {
|
|
84
|
+
try {
|
|
85
|
+
return execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
86
|
+
.toString()
|
|
87
|
+
.trim();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// The web (https) form of this repo's `origin` remote, or undefined if
|
|
94
|
+
// there isn't one configured yet. Baked into the scaffolded package.json's
|
|
95
|
+
// `docouture.checkLinks.ignore` (see TemplateValues.repoIgnoreGlob, and
|
|
96
|
+
// scripts/check-links.mjs's own comment on that key) so the repo-link.hbs
|
|
97
|
+
// header/nav link — which 404s to an anonymous crawler whenever this repo
|
|
98
|
+
// is private, indistinguishable from one that doesn't exist — doesn't fail
|
|
99
|
+
// docouture-pr-verify.yml/docouture-release.yml out of the box. Converts an SSH
|
|
100
|
+
// remote (`git@host:owner/repo.git`) to its https equivalent the same way
|
|
101
|
+
// an https remote is just stripped of its trailing `.git`; anything else
|
|
102
|
+
// unparseable is treated the same as "no remote yet".
|
|
103
|
+
function repoWebUrl(dir) {
|
|
104
|
+
const remote = originRemoteUrl(dir);
|
|
105
|
+
if (remote === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
const ssh = /^git@([^:]+):(.+?)(\.git)?$/.exec(remote);
|
|
108
|
+
if (ssh)
|
|
109
|
+
return `https://${ssh[1]}/${ssh[2]}`;
|
|
110
|
+
if (/^https?:\/\//.test(remote))
|
|
111
|
+
return remote.replace(/\.git$/, '');
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
// The GitHub Pages project-site URL 'docouture publish gh-pages' will produce
|
|
115
|
+
// once published — GitHub's own convention is
|
|
116
|
+
// https://<owner>.github.io/<repo>/, derived here from origin's remote
|
|
117
|
+
// rather than asked for, so printNextSteps can tell a first-time user what
|
|
118
|
+
// to expect before they've published anything. Only predictable when
|
|
119
|
+
// origin is actually hosted on github.com (not an enterprise host, and not
|
|
120
|
+
// "no remote configured yet"), same restriction repoWebUrl operates under.
|
|
121
|
+
// The owner is lower-cased: GitHub Pages hostnames are always served
|
|
122
|
+
// lower-case (hostnames are case-insensitive, and GitHub canonicalizes to
|
|
123
|
+
// lower-case) regardless of the org/user's actual display case — e.g.
|
|
124
|
+
// 'InditexTech' the GitHub org still serves from inditextech.github.io.
|
|
125
|
+
// The repo path segment is left exactly as the remote spells it, since
|
|
126
|
+
// that part of the URL is case-sensitive and this is already the
|
|
127
|
+
// spelling the remote itself uses.
|
|
128
|
+
function githubPagesUrl(dir) {
|
|
129
|
+
const remote = originRemoteUrl(dir);
|
|
130
|
+
if (remote === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const ssh = /^git@github\.com:([^/]+)\/(.+?)(\.git)?$/.exec(remote);
|
|
133
|
+
if (ssh)
|
|
134
|
+
return `https://${ssh[1].toLowerCase()}.github.io/${ssh[2]}/`;
|
|
135
|
+
const https = /^https:\/\/github\.com\/([^/]+)\/(.+?)(\.git)?$/.exec(remote);
|
|
136
|
+
if (https)
|
|
137
|
+
return `https://${https[1].toLowerCase()}.github.io/${https[2]}/`;
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
// TemplateValues.repoIgnoreGlob is always present as its own array element
|
|
141
|
+
// in the scaffolded package.json (see copy-template.ts's own comment on why
|
|
142
|
+
// that array can't ever shrink an element away) — when there's no `origin`
|
|
143
|
+
// remote yet to derive a real glob from, this sentinel fills the same slot
|
|
144
|
+
// instead of an empty string. It has no `*`/`?`, so globToRegExp
|
|
145
|
+
// (check-links.mjs) compiles it to a literal-substring match that will
|
|
146
|
+
// never occur inside a real URL, rather than an empty pattern (which would
|
|
147
|
+
// match — and silently ignore — every single link).
|
|
148
|
+
const NO_REPO_REMOTE_GLOB = 'docouture-new:no-origin-remote-configured';
|
|
149
|
+
function repoIgnoreGlob(dir) {
|
|
150
|
+
const url = repoWebUrl(dir);
|
|
151
|
+
return url !== undefined ? `${url}*` : NO_REPO_REMOTE_GLOB;
|
|
152
|
+
}
|
|
153
|
+
function defaultIO() {
|
|
154
|
+
return {
|
|
155
|
+
input: process.stdin,
|
|
156
|
+
output: process.stdout,
|
|
157
|
+
isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// @inquirer/prompts pipes its own internal stream into whatever output it's
|
|
161
|
+
// given and calls .end() on ours when each individual prompt finishes —
|
|
162
|
+
// harmless against process.stdout (Node refuses to let that be ended,
|
|
163
|
+
// which is why chaining prompts against a real terminal works fine) but
|
|
164
|
+
// fatal against a plain custom stream like a test's PassThrough: it would
|
|
165
|
+
// go dark after the very first of promptWizard's three sequential prompts.
|
|
166
|
+
// Wrapping every method through except a no-op .end() makes any writable
|
|
167
|
+
// stream survive being reused across multiple prompts, the same as stdout
|
|
168
|
+
// already does.
|
|
169
|
+
function keepOutputOpen(output) {
|
|
170
|
+
return new Proxy(output, {
|
|
171
|
+
get(target, prop, receiver) {
|
|
172
|
+
if (prop === 'end') {
|
|
173
|
+
return (...args) => {
|
|
174
|
+
const cb = typeof args[args.length - 1] === 'function' ? args.pop() : undefined;
|
|
175
|
+
cb?.();
|
|
176
|
+
return receiver;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const value = Reflect.get(target, prop, target);
|
|
180
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Fills in only whatever `initial` didn't already supply — a flag or
|
|
185
|
+
// positional argument always wins over a prompt, so scripting one piece
|
|
186
|
+
// (say, --mode) while leaving the rest to the wizard works as expected.
|
|
187
|
+
// Each @inquirer/prompts call gets io's streams via its `context` argument
|
|
188
|
+
// rather than touching process.stdin/stdout directly, so tests can hand it
|
|
189
|
+
// a scripted stream pair instead of a real TTY — same substitution point
|
|
190
|
+
// the old hand-rolled readline wizard used.
|
|
191
|
+
async function promptWizard(io, initial, defaults) {
|
|
192
|
+
const context = { input: io.input, output: keepOutputOpen(io.output) };
|
|
193
|
+
// Each message ends with its own '\n' — @inquirer/prompts joins
|
|
194
|
+
// [prefix, message, ...] with plain spaces, and a message that itself
|
|
195
|
+
// ends in a newline pushes the typed/selected answer onto its own line
|
|
196
|
+
// below the question instead of trailing it inline (confirmed against
|
|
197
|
+
// @inquirer/input's and @inquirer/select's own render functions, which
|
|
198
|
+
// both just re-join on every keystroke — a literal '\n' inside the
|
|
199
|
+
// message survives that untouched).
|
|
200
|
+
const name = (await input({
|
|
201
|
+
message: 'Site slug (lowercase, hyphenated — names package.json and, unless opted in below, the Antora component too):\n',
|
|
202
|
+
default: initial.name,
|
|
203
|
+
validate: (value) => NAME_PATTERN.test(value.trim()) || "expected lowercase letters, digits and hyphens, e.g. 'my-project-docs'",
|
|
204
|
+
}, context)).trim();
|
|
205
|
+
const defaultTitle = titleCase(name);
|
|
206
|
+
const title = (await input({
|
|
207
|
+
message: 'Site title (shown in the page title and the nav header):\n',
|
|
208
|
+
default: initial.title ?? defaultTitle,
|
|
209
|
+
}, context)).trim();
|
|
210
|
+
// Off by default: GitHub Pages project sites already publish under
|
|
211
|
+
// https://<org>.github.io/<repo>/, and Antora adds a second /<name>/
|
|
212
|
+
// segment per documentation component on top of that — most repos
|
|
213
|
+
// scaffold exactly one site/component, so that second segment is pure
|
|
214
|
+
// depth most users don't want. Answering yes here keeps the historical
|
|
215
|
+
// behaviour (docs/antora.yml's `name` is the site name, a real URL
|
|
216
|
+
// segment); answering no (the default) sets it to Antora's reserved
|
|
217
|
+
// `ROOT` component name instead, which Antora special-cases to
|
|
218
|
+
// contribute no segment at all — see the docs-site-package skill's
|
|
219
|
+
// reference/playbook.md for the mechanism. Either way the site title
|
|
220
|
+
// (already asked above) is unaffected — it's a separate, always-present
|
|
221
|
+
// value.
|
|
222
|
+
const urlSegment = initial.urlSegment ??
|
|
223
|
+
(await confirm({
|
|
224
|
+
message: 'Add the site name as an extra URL path segment when published?\n' +
|
|
225
|
+
'GitHub Pages project sites already publish under https://<org>.github.io/<repo>/ — ' +
|
|
226
|
+
'Antora would add a further /<name>/ segment per documentation component on top of ' +
|
|
227
|
+
'that. Off by default; turn it on only if this site will host more than one component later.\n',
|
|
228
|
+
default: false,
|
|
229
|
+
}, context));
|
|
230
|
+
const mode = initial.mode ??
|
|
231
|
+
(await select({
|
|
232
|
+
message: 'Versioning mode:\n',
|
|
233
|
+
default: 'standalone',
|
|
234
|
+
choices: [
|
|
235
|
+
{
|
|
236
|
+
name: 'Standalone (Stable + Prerelease)',
|
|
237
|
+
value: 'standalone',
|
|
238
|
+
description: "main always builds as the prerelease/preview version; a release moves a rolling 'stable' tag — no historical archive kept.",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'Versioned (Full History)',
|
|
242
|
+
value: 'versioned',
|
|
243
|
+
description: 'main always builds as the prerelease/preview version; every release is an immutable vX.Y.Z git tag.',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
}, context));
|
|
247
|
+
const pm = initial.pm ??
|
|
248
|
+
(await select({
|
|
249
|
+
message: 'Package manager:\n',
|
|
250
|
+
default: defaults.pm,
|
|
251
|
+
choices: [
|
|
252
|
+
{ name: 'npm', value: 'npm' },
|
|
253
|
+
{ name: 'pnpm', value: 'pnpm' },
|
|
254
|
+
],
|
|
255
|
+
}, context));
|
|
256
|
+
return { name, title: title.length > 0 ? title : defaultTitle, urlSegment, mode, pm };
|
|
257
|
+
}
|
|
258
|
+
export async function runNew(argv, io = defaultIO()) {
|
|
259
|
+
const { positional, flags } = parseArgs(argv);
|
|
260
|
+
let name = positional[0];
|
|
261
|
+
let title = typeof flags.title === 'string' ? flags.title : undefined;
|
|
262
|
+
// Boolean-only flag (no --no-url-segment counterpart): the default is
|
|
263
|
+
// already "off", so the only thing worth scripting is turning it on.
|
|
264
|
+
let urlSegment = flags['url-segment'] === true ? true : undefined;
|
|
265
|
+
let mode;
|
|
266
|
+
let pmChoice;
|
|
267
|
+
if (typeof flags.mode === 'string') {
|
|
268
|
+
if (!MODES.includes(flags.mode)) {
|
|
269
|
+
console.error(`invalid --mode: '${flags.mode}' — expected 'standalone' or 'versioned'`);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
mode = flags.mode;
|
|
273
|
+
}
|
|
274
|
+
if (typeof flags.pm === 'string') {
|
|
275
|
+
if (!PACKAGE_MANAGERS.includes(flags.pm)) {
|
|
276
|
+
console.error(`invalid --pm: '${flags.pm}' — expected 'npm' or 'pnpm'`);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
pmChoice = flags.pm;
|
|
280
|
+
}
|
|
281
|
+
// Best-effort guess for the wizard's own default (an existing
|
|
282
|
+
// packageManager field/lockfile at --dir/cwd, or how docouture itself was
|
|
283
|
+
// invoked) — see lib/detect-package-manager.ts. Computed against
|
|
284
|
+
// --dir/cwd directly rather than the eventual repo root (not resolved
|
|
285
|
+
// until after the wizard runs, see below): a reasonable guess either way,
|
|
286
|
+
// and the user can always override it in the prompt or with --pm.
|
|
287
|
+
const pmGuess = detectPackageManager(typeof flags.dir === 'string' ? resolve(flags.dir) : resolve(process.cwd()));
|
|
288
|
+
// Wizard runs only in an interactive terminal, and only when not
|
|
289
|
+
// explicitly skipped with --yes — a script or CI pipe (io.isTTY false)
|
|
290
|
+
// gets exactly today's non-interactive behaviour, defaults and all.
|
|
291
|
+
const skipWizard = flags.yes === true || !io.isTTY;
|
|
292
|
+
if (!skipWizard) {
|
|
293
|
+
const answers = await promptWizard(io, { name, title, urlSegment, mode, pm: pmChoice }, { pm: pmGuess });
|
|
294
|
+
name = answers.name;
|
|
295
|
+
title = answers.title;
|
|
296
|
+
urlSegment = answers.urlSegment;
|
|
297
|
+
mode = answers.mode;
|
|
298
|
+
pmChoice = answers.pm;
|
|
299
|
+
io.output.write('\n');
|
|
300
|
+
}
|
|
301
|
+
mode = mode ?? 'standalone';
|
|
302
|
+
pmChoice = pmChoice ?? pmGuess;
|
|
303
|
+
urlSegment = urlSegment ?? false;
|
|
304
|
+
if (!name) {
|
|
305
|
+
console.error('usage: docouture new <name> [--dir <path>] [--title <title>] [--url-segment] [--mode standalone|versioned] [--pm npm|pnpm]');
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
if (!NAME_PATTERN.test(name)) {
|
|
309
|
+
console.error(`invalid name: '${name}'`);
|
|
310
|
+
console.error('expected lowercase letters, digits and hyphens, e.g. my-project-docs');
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
313
|
+
title = title ?? titleCase(name);
|
|
314
|
+
// The target is always an EXISTING repository's root, not a fresh
|
|
315
|
+
// directory `docouture new` creates. --dir/cwd can be anywhere inside that
|
|
316
|
+
// repository — findRepoRoot walks up to the actual top-level, same as
|
|
317
|
+
// every other command that operates on a whole repo (dev/build/doctor/
|
|
318
|
+
// eject/teardown/publish) — rather than requiring --dir/cwd to already
|
|
319
|
+
// be the root itself.
|
|
320
|
+
const startDir = typeof flags.dir === 'string' ? resolve(flags.dir) : resolve(process.cwd());
|
|
321
|
+
if (!isInsideGitWorkTree(startDir)) {
|
|
322
|
+
console.error(`'${startDir}' is not inside a git repository`);
|
|
323
|
+
console.error('docouture new scaffolds into an existing repository — run it from your repo root (after git init), or pass --dir <path> to one');
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
const target = await findRepoRoot(startDir);
|
|
327
|
+
const docsDir = join(target, 'docs');
|
|
328
|
+
const workflowsDir = join(target, '.github', 'workflows');
|
|
329
|
+
const agentsMdFile = join(target, AGENTS_MD_FILENAME);
|
|
330
|
+
if ((await exists(docsDir)) && !(await isEmptyOrMissing(docsDir))) {
|
|
331
|
+
console.error(`'${docsDir}' already exists and is not empty`);
|
|
332
|
+
return 1;
|
|
333
|
+
}
|
|
334
|
+
const existingWorkflows = [];
|
|
335
|
+
for (const workflowName of WORKFLOW_NAMES) {
|
|
336
|
+
if (await exists(join(workflowsDir, workflowName))) {
|
|
337
|
+
existingWorkflows.push(join('.github', 'workflows', workflowName));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const existingSkills = [];
|
|
341
|
+
for (const relativePath of AGENT_SUPPORT_PATHS) {
|
|
342
|
+
const absolutePath = join(target, relativePath);
|
|
343
|
+
if ((await exists(absolutePath)) && !(await isEmptyOrMissing(absolutePath)))
|
|
344
|
+
existingSkills.push(relativePath);
|
|
345
|
+
}
|
|
346
|
+
// AGENTS.md is never an all-or-nothing conflict the way a workflow file or
|
|
347
|
+
// a skill directory is: a foreign/human-written file (no docouture-managed
|
|
348
|
+
// block yet — see lib/agents-md.ts) is always safe to append docouture' own
|
|
349
|
+
// section to without asking. Only a file that already HAS a managed
|
|
350
|
+
// block counts as something this run would actually overwrite.
|
|
351
|
+
let existingAgentsMd;
|
|
352
|
+
let agentsMdConflict = false;
|
|
353
|
+
if (await exists(agentsMdFile)) {
|
|
354
|
+
existingAgentsMd = await readFile(agentsMdFile, 'utf8');
|
|
355
|
+
agentsMdConflict = hasManagedSection(existingAgentsMd);
|
|
356
|
+
}
|
|
357
|
+
const conflicts = [...existingWorkflows, ...existingSkills, ...(agentsMdConflict ? [AGENTS_MD_FILENAME] : [])];
|
|
358
|
+
if (conflicts.length > 0) {
|
|
359
|
+
if (skipWizard) {
|
|
360
|
+
// Non-interactive (--yes, or no TTY) — nobody to confirm with, so this
|
|
361
|
+
// stays a hard refusal, same as every other pre-flight check above,
|
|
362
|
+
// rather than silently overwriting something already there.
|
|
363
|
+
console.error(`refusing to overwrite existing file(s)/dir(s) under '${target}':`);
|
|
364
|
+
console.error(` ${conflicts.join(', ')}`);
|
|
365
|
+
console.error("run 'docouture upgrade' instead if you want to re-sync them");
|
|
366
|
+
return 1;
|
|
367
|
+
}
|
|
368
|
+
console.log(theme.bold('Already exist and would be overwritten:'));
|
|
369
|
+
for (const path of conflicts)
|
|
370
|
+
console.log(` ${path}`);
|
|
371
|
+
console.log('');
|
|
372
|
+
const proceed = await confirm({ message: 'Overwrite them?', default: false }, { input: io.input, output: keepOutputOpen(io.output) });
|
|
373
|
+
if (!proceed) {
|
|
374
|
+
console.log('aborted — nothing written');
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
io.output.write('\n');
|
|
378
|
+
}
|
|
379
|
+
// build/commands/new.js -> build/templates/{starter,workflows,agent-support}
|
|
380
|
+
// — see scripts/copy-templates.mjs, which puts the templates/ directory
|
|
381
|
+
// here at build time. Resolved from import.meta.url so this works
|
|
382
|
+
// regardless of the directory docouture is invoked from.
|
|
383
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
384
|
+
const templatesRoot = join(here, '..', 'templates');
|
|
385
|
+
const starterDir = join(templatesRoot, 'starter');
|
|
386
|
+
const workflowsTemplateDir = join(templatesRoot, 'workflows');
|
|
387
|
+
const agentSupportDir = join(templatesRoot, 'agent-support');
|
|
388
|
+
// build/commands/new.js -> build/ -> package root, 2 levels up — see
|
|
389
|
+
// readCliInfo's own comment. This is the exact version a scaffolded
|
|
390
|
+
// site's devDependency on @inditextech/docouture-cli gets pinned to below, so
|
|
391
|
+
// it always matches whatever CLI actually generated it, snapshot/local
|
|
392
|
+
// releases included.
|
|
393
|
+
const { version: cliVersion } = await readCliInfo(import.meta.url, 2);
|
|
394
|
+
// The user's own choice (--pm, wizard answer, or the auto-guess computed
|
|
395
|
+
// above if neither was given) — never re-detected against `target`, so
|
|
396
|
+
// whatever was actually chosen/confirmed is what the workflows and
|
|
397
|
+
// printed next-steps agree on.
|
|
398
|
+
const pm = packageManagerPlan(pmChoice);
|
|
399
|
+
const values = {
|
|
400
|
+
name,
|
|
401
|
+
title,
|
|
402
|
+
// Antora's own component `name` — deliberately a separate value from
|
|
403
|
+
// the site name above once `urlSegment` is false (the default):
|
|
404
|
+
// `ROOT` is Antora's reserved component name, special-cased to
|
|
405
|
+
// contribute no segment to a page's published URL at all (see
|
|
406
|
+
// how-antora-builds-urls's "Component segment" section) — which is
|
|
407
|
+
// exactly what dropping the extra GitHub Pages path segment requires.
|
|
408
|
+
// package.json's own `name` stays `name` above either way; `docouture
|
|
409
|
+
// doctor`'s checkNamesAgree knows to skip the package-name-matches-
|
|
410
|
+
// component-name check when this is the literal 'ROOT'.
|
|
411
|
+
componentName: urlSegment ? name : 'ROOT',
|
|
412
|
+
cliVersion,
|
|
413
|
+
pmName: pm.pm,
|
|
414
|
+
pmCacheName: pm.cacheName,
|
|
415
|
+
pmLockfile: pm.lockfile,
|
|
416
|
+
pmCiCmd: pm.ciCmd,
|
|
417
|
+
pmSetupStepYaml: pm.setupStepYaml,
|
|
418
|
+
repoIgnoreGlob: repoIgnoreGlob(target),
|
|
419
|
+
};
|
|
420
|
+
// The whole starter subtree — package.json, antora-playbook.yml, its own
|
|
421
|
+
// nested src/antora.yml — lands under <repo-root>/docs/ as one piece,
|
|
422
|
+
// unchanged in shape. Only .github/workflows/ is peeled out to a second
|
|
423
|
+
// copy at the true repo root, since GitHub Actions never discovers
|
|
424
|
+
// workflows anywhere else.
|
|
425
|
+
await copyTemplate(starterDir, docsDir, values);
|
|
426
|
+
await copyTemplate(workflowsTemplateDir, workflowsDir, values);
|
|
427
|
+
// AGENTS.md and both platforms' skill directories also land at the true
|
|
428
|
+
// repo root, same as workflows — an agent reads them from there, not from
|
|
429
|
+
// inside docs/. copyTemplate's own VERSIONED_MARKER/SKIP_FILENAMES skips
|
|
430
|
+
// (see copy-template.ts) leave docs-versioning.versioned and AGENTS.md
|
|
431
|
+
// untouched here; docs-versioning is copied under its real name below,
|
|
432
|
+
// only in versioned mode, and AGENTS.md is merged separately next — see
|
|
433
|
+
// lib/agents-md.ts for why it can't go through the generic overwrite walk
|
|
434
|
+
// every other file here does.
|
|
435
|
+
await copyTemplate(agentSupportDir, target, values);
|
|
436
|
+
const renderedAgentsMd = await renderTemplateFile(join(agentSupportDir, AGENTS_MD_FILENAME), values);
|
|
437
|
+
await writeFile(agentsMdFile, mergeAgentsMd(existingAgentsMd, renderedAgentsMd), 'utf8');
|
|
438
|
+
if (mode === 'versioned') {
|
|
439
|
+
await writeTemplateFile(join(starterDir, 'antora-playbook.versioned.yml'), join(docsDir, 'antora-playbook.yml'), values);
|
|
440
|
+
await writeTemplateFile(join(starterDir, 'src', 'release-version.versioned'), join(docsDir, '.release-version'), values);
|
|
441
|
+
for (const platform of ['.opencode', '.claude']) {
|
|
442
|
+
await copyTemplate(join(agentSupportDir, platform, 'skills', 'docs-versioning.versioned'), join(target, platform, 'skills', 'docs-versioning'), values);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
printNextSteps({ mode, pm, target, docsDir, workflowsDir, ghPagesUrl: githubPagesUrl(target) });
|
|
446
|
+
return 0;
|
|
447
|
+
}
|
|
448
|
+
// Everything printed after scaffolding finishes, grouped into labeled
|
|
449
|
+
// sections so it reads as a short runbook rather than a flat log of
|
|
450
|
+
// "created X" lines. Headers use the shared theme's bold, matching bin.ts's
|
|
451
|
+
// own banner — see lib/theme.ts for why that's not picocolors directly.
|
|
452
|
+
function printNextSteps(args) {
|
|
453
|
+
const { mode, pm, target, docsDir, workflowsDir, ghPagesUrl } = args;
|
|
454
|
+
const created = (path) => ` ${theme.success('✓')} ${path}`;
|
|
455
|
+
// Relative to `target` (the repo root), not process.cwd() — cwd may be a
|
|
456
|
+
// nested subdirectory findRepoRoot walked up from (see runNew), in which
|
|
457
|
+
// case relative(cwd, ...) produces a useless, alarming-looking chain of
|
|
458
|
+
// '../../..' for paths that are actually just 'docs', '.github/workflows'
|
|
459
|
+
// etc. at the repo root.
|
|
460
|
+
const writtenAt = (path) => relative(target, path) || '.';
|
|
461
|
+
// 'Written' rather than 'Created': a conflict confirmed above (see
|
|
462
|
+
// runNew) means some of these were overwritten or merged into, not
|
|
463
|
+
// created fresh — this header stays accurate either way.
|
|
464
|
+
console.log(theme.bold('Written:'));
|
|
465
|
+
console.log(created(writtenAt(docsDir)));
|
|
466
|
+
console.log(created(writtenAt(workflowsDir)));
|
|
467
|
+
console.log(created(writtenAt(join(target, 'AGENTS.md'))));
|
|
468
|
+
console.log(created(writtenAt(join(target, '.opencode', 'skills'))));
|
|
469
|
+
console.log(created(writtenAt(join(target, '.claude', 'skills'))));
|
|
470
|
+
console.log('');
|
|
471
|
+
console.log(theme.bold('Next steps:'));
|
|
472
|
+
console.log(' cd docs');
|
|
473
|
+
console.log(` ${pm.installCmd}`);
|
|
474
|
+
console.log(` ${pm.devCmd}`);
|
|
475
|
+
console.log('');
|
|
476
|
+
if (mode === 'versioned') {
|
|
477
|
+
console.log(theme.bold('Versioning: Versioned (Full History)'));
|
|
478
|
+
console.log(' main is the prerelease channel.');
|
|
479
|
+
console.log('');
|
|
480
|
+
console.log(' To cut your first release:');
|
|
481
|
+
console.log(" 1. Create the 'docs/release' label (once): gh label create docs/release");
|
|
482
|
+
console.log(' 2. Open a PR that sets the target version in docs/.release-version (e.g. "1.0.0")');
|
|
483
|
+
console.log(" 3. Label the PR 'docs/release'");
|
|
484
|
+
console.log(' 4. Merge it — docouture-release.yml runs automatically and tags vX.Y.Z');
|
|
485
|
+
console.log('');
|
|
486
|
+
console.log(' (or skip the label/PR entirely: run docouture-release.yml manually via workflow_dispatch');
|
|
487
|
+
console.log(' and type the version)');
|
|
488
|
+
console.log('');
|
|
489
|
+
console.log(' See the docs-versioning skill (.opencode/skills, .claude/skills) for the full mechanism.');
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.log(theme.bold('Versioning: Standalone (Stable + Prerelease)'));
|
|
493
|
+
console.log(' main is the prerelease channel.');
|
|
494
|
+
console.log('');
|
|
495
|
+
console.log(' To cut your first stable release:');
|
|
496
|
+
console.log(" 1. Create the 'docs/release' label (once): gh label create docs/release");
|
|
497
|
+
console.log(" 2. Merge any PR labeled 'docs/release' into main — docouture-release.yml runs automatically");
|
|
498
|
+
console.log('');
|
|
499
|
+
console.log(' (or skip the label/PR entirely: run docouture-release.yml manually via workflow_dispatch,');
|
|
500
|
+
console.log(' default input is fine)');
|
|
501
|
+
}
|
|
502
|
+
console.log('');
|
|
503
|
+
console.log(theme.bold('Before your first publish:'));
|
|
504
|
+
console.log(' See docs/src/modules/main/pages/prerequisites.adoc for what a public GitHub Pages site');
|
|
505
|
+
console.log(' needs before its first publish.');
|
|
506
|
+
if (ghPagesUrl !== undefined) {
|
|
507
|
+
console.log('');
|
|
508
|
+
console.log(' Once you cut your first release, the site publishes automatically to the');
|
|
509
|
+
console.log(` \`gh-pages\` branch — once public, it will be live at ${ghPagesUrl}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
//# sourceMappingURL=new.js.map
|