gitorial-cli 1.0.1 → 2.0.0
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/README.md +114 -59
- package/package.json +6 -4
- package/src/commands/build-gitorial.js +152 -0
- package/src/commands/build-mdbook.js +321 -0
- package/src/index.js +29 -35
- package/src/lib/fs.js +112 -0
- package/src/lib/git.js +45 -0
- package/src/lib/logger.js +22 -0
- package/src/lib/mdbook-templates.js +18 -0
- package/src/lib/monaco-assets.js +549 -0
- package/src/constants.js +0 -5
- package/src/mdbook.js +0 -468
- package/src/repack.js +0 -85
- package/src/unpack.js +0 -88
- package/src/utils.js +0 -79
package/README.md
CHANGED
|
@@ -1,97 +1,152 @@
|
|
|
1
|
-
# cli
|
|
1
|
+
# gitorial-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tools for building step-by-step tutorials that are easy to contribute to and easy to render as clean, commit-based Gitorials.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This CLI helps you move between two tutorial representations:
|
|
6
|
+
|
|
7
|
+
- `master` (or your workshop branch): mdBook-friendly, contributor-friendly structure
|
|
8
|
+
- `gitorial`: commit-driven tutorial flow (`section`, `action`, `template`, `solution`)
|
|
6
9
|
|
|
7
|
-
Install
|
|
10
|
+
## Install
|
|
8
11
|
|
|
9
12
|
```sh
|
|
10
13
|
npm install -g gitorial-cli
|
|
11
14
|
```
|
|
12
15
|
|
|
16
|
+
For local development in this repo:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
13
22
|
## Commands
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
### `build-gitorial`
|
|
25
|
+
|
|
26
|
+
Generate a gitorial branch from your mdBook workshop branch.
|
|
16
27
|
|
|
17
28
|
```sh
|
|
18
|
-
|
|
19
|
-
unpack [options] Unpack a Gitorial into another branch.
|
|
20
|
-
repack [options] Create a repacked Gitorial from an unpacked Gitorial. Must repack into a new branch.
|
|
21
|
-
mdbook [options] Scaffold the contents of a Gitorial in a new branch in the mdBook source format. You need to initialize an mdBook yourself
|
|
29
|
+
gitorial-cli build-gitorial -r /path/to/repo -i master -o gitorial -s src --force
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
Options:
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
- `-r, --repo <path>` repo path (default: current directory)
|
|
35
|
+
- `-i, --input <branch>` workshop branch (default: `master`)
|
|
36
|
+
- `-o, --output <branch>` gitorial branch (default: `gitorial`)
|
|
37
|
+
- `-s, --source <dir>` mdBook source directory in input branch (default: `src`)
|
|
38
|
+
- `--force` recreate output branch if it exists
|
|
39
|
+
- `--verbose` verbose logs
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
Behavior:
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-h, --help display help for command
|
|
37
|
-
```
|
|
43
|
+
- Rebuilds `output` as a fresh orphan branch.
|
|
44
|
+
- Rewrites commit history on the output branch by design.
|
|
45
|
+
- Copies full step snapshots per commit, so output branch content is tutorial snapshot content.
|
|
46
|
+
|
|
47
|
+
### `build-mdbook`
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
Generate or update an mdBook workshop branch from a gitorial branch.
|
|
40
50
|
|
|
41
51
|
```sh
|
|
42
|
-
gitorial-cli
|
|
52
|
+
gitorial-cli build-mdbook -r /path/to/repo -i gitorial -o master -s src
|
|
43
53
|
```
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
Options:
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
├─ ...
|
|
54
|
-
```
|
|
57
|
+
- `-r, --repo <path>` repo path (default: current directory)
|
|
58
|
+
- `-i, --input <branch>` gitorial branch (default: `gitorial`)
|
|
59
|
+
- `-o, --output <branch>` workshop branch (default: `master`)
|
|
60
|
+
- `-s, --source <dir>` output mdBook source directory (default: `src`)
|
|
61
|
+
- `--force` accepted but ignored (history is preserved)
|
|
62
|
+
- `--verbose` verbose logs
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
Behavior:
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
- Preserves output branch history.
|
|
67
|
+
- Replaces only the `source` directory content (for example `src/` or `example/src/`).
|
|
68
|
+
- Leaves files outside that directory untouched.
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
## Step Types
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
-p, --path <path> The local path for the git repo containing the Gitorial.
|
|
65
|
-
-i, --inputBranch <inputBranch> The branch in the repo with the unpacked Gitorial.
|
|
66
|
-
-o, --outputBranch <outputBranch> The branch where you want to repack the Gitorial. Branch must not exist.
|
|
67
|
-
-s, --subFolder <subFolder> The subfolder (relative to the <path>) where you can find the unpacked Gitorial
|
|
68
|
-
--force Force the repack, even if it would replace an existing branch. WARNING: this can delete the branch history!
|
|
69
|
-
-h, --help display help for command
|
|
70
|
-
```
|
|
72
|
+
A gitorial step must map to one of these types:
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
- `section`: intro/context step, README only
|
|
75
|
+
- `action`: non-template operational step
|
|
76
|
+
- `template`: TODO step, must be followed by a `solution`
|
|
77
|
+
- `solution`: working result of preceding template
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
Declare type in markdown using a hidden comment:
|
|
80
|
+
|
|
81
|
+
```md
|
|
82
|
+
<!-- gitorial: action -->
|
|
76
83
|
```
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
Supported forms:
|
|
86
|
+
|
|
87
|
+
- `<!-- gitorial: section -->`
|
|
88
|
+
- `<!-- gitorial: action -->`
|
|
89
|
+
- `<!-- gitorial: template -->`
|
|
90
|
+
- `<!-- gitorial: solution -->`
|
|
91
|
+
|
|
92
|
+
## mdBook Workshop Layout
|
|
93
|
+
|
|
94
|
+
Expected source layout:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
src/
|
|
98
|
+
SUMMARY.md
|
|
99
|
+
0/
|
|
100
|
+
README.md # section-only step (optional)
|
|
101
|
+
1/
|
|
102
|
+
README.md # generated step page with Monaco
|
|
103
|
+
source/
|
|
104
|
+
README.md # action/section source content
|
|
105
|
+
...
|
|
106
|
+
2/
|
|
107
|
+
README.md # generated step page with Monaco
|
|
108
|
+
template/
|
|
109
|
+
README.md
|
|
110
|
+
...
|
|
111
|
+
solution/
|
|
112
|
+
README.md
|
|
113
|
+
...
|
|
114
|
+
_gitorial/
|
|
115
|
+
monaco-setup.js
|
|
116
|
+
monaco-setup.css
|
|
117
|
+
```
|
|
79
118
|
|
|
80
|
-
|
|
81
|
-
Usage: index mdbook [options]
|
|
119
|
+
Notes:
|
|
82
120
|
|
|
83
|
-
|
|
121
|
+
- `README.md` inside each step folder is the rendered page shell.
|
|
122
|
+
- `files.json` is generated per interactive step to drive Monaco file selection.
|
|
123
|
+
- Section-only steps are represented as numbered folders with only `README.md`.
|
|
84
124
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
## Workflow Expectations
|
|
126
|
+
|
|
127
|
+
- Run commands from a clean working tree.
|
|
128
|
+
- Commands switch branches in the target repo.
|
|
129
|
+
- Use dedicated branches for workshop and gitorial.
|
|
130
|
+
|
|
131
|
+
## CI
|
|
132
|
+
|
|
133
|
+
Template workflow:
|
|
134
|
+
|
|
135
|
+
- `templates/gitorial-sync.yml`
|
|
136
|
+
- Syncs `gitorial` on pushes to `master`
|
|
137
|
+
|
|
138
|
+
This repo also includes a concrete workflow:
|
|
139
|
+
|
|
140
|
+
- `.github/workflows/sync-gitorial.yml`
|
|
141
|
+
- Builds `gitorial` from `example/src` on pushes to `master`
|
|
142
|
+
|
|
143
|
+
## Example in This Repo
|
|
144
|
+
|
|
145
|
+
See `example/` for a complete fixture with all step types.
|
|
92
146
|
|
|
93
|
-
|
|
147
|
+
Round-trip commands for this repo:
|
|
94
148
|
|
|
95
149
|
```sh
|
|
96
|
-
gitorial-
|
|
150
|
+
node src/index.js build-gitorial -r . -i master -o gitorial -s example/src --force
|
|
151
|
+
node src/index.js build-mdbook -r . -i gitorial -o master -s example/src
|
|
97
152
|
```
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitorial-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "CLI tools for building and maintaining Gitorial tutorials",
|
|
5
5
|
"main": "src/index.js",
|
|
6
|
-
"bin":
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitorial-cli": "src/index.js"
|
|
8
|
+
},
|
|
7
9
|
"scripts": {
|
|
8
10
|
"start": "node src/index.js"
|
|
9
11
|
},
|
|
@@ -22,6 +24,6 @@
|
|
|
22
24
|
"simple-git": "^3.24.0"
|
|
23
25
|
},
|
|
24
26
|
"files": [
|
|
25
|
-
"src
|
|
27
|
+
"src/**/*"
|
|
26
28
|
]
|
|
27
29
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { createLogger } = require('../lib/logger');
|
|
5
|
+
const { createGit, ensureBranchExists, createOrphanBranch } = require('../lib/git');
|
|
6
|
+
const {
|
|
7
|
+
copyDir,
|
|
8
|
+
hasNonDocFiles,
|
|
9
|
+
listNumericDirs,
|
|
10
|
+
readFirstHeading,
|
|
11
|
+
readGitorialType,
|
|
12
|
+
removeAllExcept,
|
|
13
|
+
} = require('../lib/fs');
|
|
14
|
+
|
|
15
|
+
function getStepTitle(stepDir) {
|
|
16
|
+
const candidates = [
|
|
17
|
+
path.join(stepDir, 'template', 'README.md'),
|
|
18
|
+
path.join(stepDir, 'source', 'README.md'),
|
|
19
|
+
path.join(stepDir, 'solution', 'README.md'),
|
|
20
|
+
path.join(stepDir, 'README.md'),
|
|
21
|
+
];
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
|
+
const title = readFirstHeading(candidate);
|
|
24
|
+
if (title) {
|
|
25
|
+
return title;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return path.basename(stepDir);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function copySnapshot(sourceDir, repoPath) {
|
|
32
|
+
const filter = (sourcePath, isDirectory) => {
|
|
33
|
+
const baseName = path.basename(sourcePath);
|
|
34
|
+
if (baseName === '.git') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (!isDirectory && baseName.endsWith('.diff')) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
removeAllExcept(repoPath, ['.git']);
|
|
43
|
+
copyDir(sourceDir, repoPath, filter);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveStepType(readmePath, defaultType) {
|
|
47
|
+
const declaredType = readGitorialType(readmePath);
|
|
48
|
+
if (!declaredType) {
|
|
49
|
+
return defaultType;
|
|
50
|
+
}
|
|
51
|
+
return declaredType;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertStepType(step, actualType, allowedTypes) {
|
|
55
|
+
if (!allowedTypes.includes(actualType)) {
|
|
56
|
+
throw new Error(`Step ${step} has unsupported gitorial type "${actualType}". Allowed: ${allowedTypes.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function buildGitorial(options) {
|
|
61
|
+
const logger = createLogger(options);
|
|
62
|
+
const repoPath = path.resolve(options.repo);
|
|
63
|
+
const inputBranch = options.input;
|
|
64
|
+
const outputBranch = options.output;
|
|
65
|
+
const sourceDirName = options.source;
|
|
66
|
+
|
|
67
|
+
const git = createGit(repoPath);
|
|
68
|
+
await ensureBranchExists(git, inputBranch);
|
|
69
|
+
|
|
70
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitorial-mdbook-'));
|
|
71
|
+
const sourceGit = createGit(tempDir);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
logger.info(`Cloning ${inputBranch} into temporary workspace...`);
|
|
75
|
+
await sourceGit.clone(repoPath, tempDir, ['--branch', inputBranch]);
|
|
76
|
+
|
|
77
|
+
const mdbookSourceDir = path.join(tempDir, sourceDirName);
|
|
78
|
+
if (!fs.existsSync(mdbookSourceDir)) {
|
|
79
|
+
throw new Error(`mdBook source directory not found: ${mdbookSourceDir}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const steps = listNumericDirs(mdbookSourceDir);
|
|
83
|
+
if (steps.length === 0) {
|
|
84
|
+
throw new Error(`No step folders found in ${mdbookSourceDir}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await createOrphanBranch(git, outputBranch, {
|
|
88
|
+
force: options.force,
|
|
89
|
+
fromBranch: inputBranch,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
for (const step of steps) {
|
|
93
|
+
const stepDir = path.join(mdbookSourceDir, step);
|
|
94
|
+
const stepTitle = getStepTitle(stepDir);
|
|
95
|
+
const templateDir = path.join(stepDir, 'template');
|
|
96
|
+
const solutionDir = path.join(stepDir, 'solution');
|
|
97
|
+
const sourceDir = path.join(stepDir, 'source');
|
|
98
|
+
const sectionReadme = path.join(stepDir, 'README.md');
|
|
99
|
+
|
|
100
|
+
const hasTemplate = fs.existsSync(templateDir);
|
|
101
|
+
const hasSolution = fs.existsSync(solutionDir);
|
|
102
|
+
const hasSource = fs.existsSync(sourceDir);
|
|
103
|
+
const hasSectionReadme = fs.existsSync(sectionReadme);
|
|
104
|
+
|
|
105
|
+
if (hasTemplate && hasSolution) {
|
|
106
|
+
logger.info(`Step ${step}: template/solution → ${stepTitle}`);
|
|
107
|
+
const templateType = resolveStepType(path.join(templateDir, 'README.md'), 'template');
|
|
108
|
+
const solutionType = resolveStepType(path.join(solutionDir, 'README.md'), 'solution');
|
|
109
|
+
assertStepType(step, templateType, ['template']);
|
|
110
|
+
assertStepType(step, solutionType, ['solution']);
|
|
111
|
+
|
|
112
|
+
copySnapshot(templateDir, repoPath);
|
|
113
|
+
await git.add('.');
|
|
114
|
+
await git.commit(`${templateType}: ${stepTitle}`);
|
|
115
|
+
|
|
116
|
+
copySnapshot(solutionDir, repoPath);
|
|
117
|
+
await git.add('.');
|
|
118
|
+
await git.commit(`${solutionType}: ${stepTitle}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (hasSource) {
|
|
123
|
+
logger.info(`Step ${step}: source/action → ${stepTitle}`);
|
|
124
|
+
const stepType = resolveStepType(path.join(sourceDir, 'README.md'), hasNonDocFiles(sourceDir) ? 'action' : 'section');
|
|
125
|
+
assertStepType(step, stepType, ['action', 'section']);
|
|
126
|
+
|
|
127
|
+
copySnapshot(sourceDir, repoPath);
|
|
128
|
+
await git.add('.');
|
|
129
|
+
await git.commit(`${stepType}: ${stepTitle}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (hasSectionReadme) {
|
|
134
|
+
const stepType = resolveStepType(sectionReadme, 'section');
|
|
135
|
+
assertStepType(step, stepType, ['section']);
|
|
136
|
+
logger.info(`Step ${step}: section → ${stepTitle}`);
|
|
137
|
+
fs.copyFileSync(sectionReadme, path.join(repoPath, 'README.md'));
|
|
138
|
+
await git.add('README.md');
|
|
139
|
+
await git.commit(`${stepType}: ${stepTitle}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logger.warn(`Skipping step ${step}: no template/solution/source folder found.`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
logger.info('Gitorial branch generated successfully.');
|
|
147
|
+
} finally {
|
|
148
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { buildGitorial };
|