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 CHANGED
@@ -1,97 +1,152 @@
1
- # cli
1
+ # gitorial-cli
2
2
 
3
- The gitorial-cli is a CLI tool for helping manage and work with a Git repo following the [Gitorial format](https://github.com/gitorial-sdk).
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
- ## Install
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 this CLI via NPM:
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
- The CLI exposes various commands for managing a Gitorial.
24
+ ### `build-gitorial`
25
+
26
+ Generate a gitorial branch from your mdBook workshop branch.
16
27
 
17
28
  ```sh
18
- Commands:
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
- ### unpack
32
+ Options:
25
33
 
26
- ```sh
27
- Usage: index unpack [options]
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
- Unpack a Gitorial into another branch.
41
+ Behavior:
30
42
 
31
- Options:
32
- -p, --path <path> The local path for the git repo containing the Gitorial.
33
- -i, --inputBranch <inputBranch> The branch in the repo with the Gitorial.
34
- -o, --outputBranch <outputBranch> The branch where you want to unpack the Gitorial.
35
- -s, --subFolder <subFolder> The subfolder (relative to the <path>) where you want the unpacked Gitorial to be placed.
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
- Example: Convert an Gitorial repository from branch `gitorial` into branch `master` as an unpacked set of numbered steps in a folder named `steps`.
49
+ Generate or update an mdBook workshop branch from a gitorial branch.
40
50
 
41
51
  ```sh
42
- gitorial-cli unpack -p /path/to/project -i gitorial -o master -s steps
52
+ gitorial-cli build-mdbook -r /path/to/repo -i gitorial -o master -s src
43
53
  ```
44
54
 
45
- Output:
55
+ Options:
46
56
 
47
- ```sh
48
- # git branch: master
49
- steps/
50
- ├─ 0/
51
- ├─ 1/
52
- ├─ 2/
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
- ### repack
64
+ Behavior:
57
65
 
58
- ```sh
59
- Usage: index repack [options]
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
- Create a repacked Gitorial from an unpacked Gitorial. Must repack into a new branch.
70
+ ## Step Types
62
71
 
63
- Options:
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
- Example: Convert an "unpacked" Gitorial on branch `master` in folder `steps` to a branch `gitorial`
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
- ```sh
75
- gitorial-cli repack -p /path/to/project -i master -s steps -o gitorial
79
+ Declare type in markdown using a hidden comment:
80
+
81
+ ```md
82
+ <!-- gitorial: action -->
76
83
  ```
77
84
 
78
- ### mdBook
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
- ```sh
81
- Usage: index mdbook [options]
119
+ Notes:
82
120
 
83
- Scaffold the contents of a Gitorial in a new branch in the mdBook source format. You need to initialize an mdBook yourself
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
- Options:
86
- -p, --path <path> The local path for the git repo containing the Gitorial.
87
- -i, --inputBranch <inputBranch> The branch in the repo with the Gitorial.
88
- -o, --outputBranch <outputBranch> The branch where you want your mdBook to live
89
- -s, --subFolder <subFolder> The subfolder (relative to the <path>) where you want the mdBook source material to be placed. (default: "src")
90
- -h, --help display help for command
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
- Example: Convert a Gitorial at branch `gitorial` to an [mdBook](https://rust-lang.github.io/mdBook/) rendered at branch `mdbook`.
147
+ Round-trip commands for this repo:
94
148
 
95
149
  ```sh
96
- gitorial-cli mdbook -p /path/to/project -i gitorial -o mdbook
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": "1.0.1",
4
- "description": "CLI Tools for Creating and Managing a Gitorial",
3
+ "version": "2.0.0",
4
+ "description": "CLI tools for building and maintaining Gitorial tutorials",
5
5
  "main": "src/index.js",
6
- "bin": "src/index.js",
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 };