oneclickpush 0.1.0-next.1 → 0.1.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 +123 -0
- package/cli.js +74 -1
- package/index.d.ts +55 -0
- package/index.js +86 -1
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -1,3 +1,126 @@
|
|
|
1
1
|
# oneclickpush
|
|
2
2
|
|
|
3
3
|
> One-click GitHub Pages deployer
|
|
4
|
+
|
|
5
|
+
Replace the contents of a remote git branch (like `gh-pages`) with the contents of a local directory
|
|
6
|
+
in one command.
|
|
7
|
+
|
|
8
|
+
[`gh-pages`](https://github.com/tschaub/gh-pages) is
|
|
9
|
+
[complicated](https://github.com/tschaub/gh-pages/tree/main/lib),
|
|
10
|
+
[buggy](https://github.com/tschaub/gh-pages/issues), and
|
|
11
|
+
[unmaintained](https://github.com/tschaub/gh-pages/issues/657#issue-4979248775); `oneclickpush` is
|
|
12
|
+
none of these, has zero dependencies and only uses Node native functions + `git`.
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
Deploy your build directory (e.g. `dist`) to the `gh-pages` branch:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
$ cd dist
|
|
20
|
+
$ npx oneclickpush
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## CLI Usage
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
Usage: oneclickpush [options]
|
|
27
|
+
|
|
28
|
+
Replace the contents of a remote git branch with that of a local directory.
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
-d, --dist <dir> Directory to publish (default: ".")
|
|
32
|
+
-e, --exclude <glob> Paths to leave out, relative to dist (default: "**/.DS_Store")
|
|
33
|
+
-b, --branch <name> Target branch (default: "gh-pages")
|
|
34
|
+
-m, --message <msg> Commit message (default: "Updates")
|
|
35
|
+
-f, --force Force push with no history
|
|
36
|
+
-n, --dry-run Show what would change, without committing or pushing
|
|
37
|
+
-v, --verbose Log git commands
|
|
38
|
+
-V, --version Print version
|
|
39
|
+
-h, --help Show this help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> [!TIP]
|
|
43
|
+
> `--exclude` takes glob patterns (`*`, `**`, `?`) and can be repeated. Patterns are matched against
|
|
44
|
+
> the full path, so use `**/name` to match at any depth.
|
|
45
|
+
|
|
46
|
+
### Examples
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Preview what would be deployed without pushing
|
|
50
|
+
npx oneclickpush -d build --dry-run
|
|
51
|
+
|
|
52
|
+
# Custom branch and commit message
|
|
53
|
+
npx oneclickpush -d public -b release -m "deploy: v1.0.0"
|
|
54
|
+
|
|
55
|
+
# Exclude source maps and test files
|
|
56
|
+
npx oneclickpush -d dist -e "**/*.map" -e "**/tests/**"
|
|
57
|
+
|
|
58
|
+
# Force push fresh history
|
|
59
|
+
npx oneclickpush -d dist --force
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Programmatic API
|
|
63
|
+
|
|
64
|
+
You can also use `oneclickpush` as an ES module:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Install the library
|
|
68
|
+
npm i oneclickpush
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
import { publish } from 'oneclickpush'
|
|
73
|
+
|
|
74
|
+
const changes = await publish({
|
|
75
|
+
dist: 'dist',
|
|
76
|
+
exclude: ['**/.DS_Store', '**/*.map'],
|
|
77
|
+
branch: 'gh-pages',
|
|
78
|
+
message: 'Deploy updates',
|
|
79
|
+
force: false,
|
|
80
|
+
dryRun: false,
|
|
81
|
+
verbose: false
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
if (changes) {
|
|
85
|
+
console.log('Published changes:\n' + changes)
|
|
86
|
+
} else {
|
|
87
|
+
console.log('Nothing to publish')
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Options
|
|
92
|
+
|
|
93
|
+
| Option | Type | Default | Description |
|
|
94
|
+
| --------- | ---------- | ------------------ | --------------------------------------------------- |
|
|
95
|
+
| `dist` | `string` | `"."` | Directory to publish |
|
|
96
|
+
| `exclude` | `string[]` | `["**/.DS_Store"]` | Paths to exclude relative to `dist` |
|
|
97
|
+
| `branch` | `string` | `"gh-pages"` | Target git branch |
|
|
98
|
+
| `message` | `string` | `"Updates"` | Git commit message |
|
|
99
|
+
| `force` | `boolean` | `false` | Force push fresh commit with no previous history |
|
|
100
|
+
| `dryRun` | `boolean` | `false` | Return staged changes without committing or pushing |
|
|
101
|
+
| `verbose` | `boolean` | `false` | Log executed git commands |
|
|
102
|
+
|
|
103
|
+
## How It Works
|
|
104
|
+
|
|
105
|
+
1. Resolves the remote origin repository URL from local git configuration.
|
|
106
|
+
2. Creates an isolated temporary directory and fetches the target branch (or initializes it if it
|
|
107
|
+
doesn't exist).
|
|
108
|
+
3. Clears the target branch index and stages the contents of `dist` (less `exclude` patterns)
|
|
109
|
+
directly via Git `work-tree`.
|
|
110
|
+
4. Checks for differences (`git diff --cached --name-status`). If there are changes, commits and
|
|
111
|
+
pushes back to `origin`.
|
|
112
|
+
5. Cleans up the temporary directory.
|
|
113
|
+
|
|
114
|
+
> [!WARNING]
|
|
115
|
+
> Requires Node 18.3+ and Git 2.28+.
|
|
116
|
+
|
|
117
|
+
### Notes
|
|
118
|
+
|
|
119
|
+
- Dot files included by default; use `-e` to exclude.
|
|
120
|
+
- Contents in target branch are emptied and replaced with the contents of `dist` (WYSIWYG).
|
|
121
|
+
- If `.nojekyll` or `CNAME` files are required, add them to `dist`; use a post-build step if needed.
|
|
122
|
+
- If the target branch does not exist, it will be created.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
ISC
|
package/cli.js
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { publish } from './index.js'
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
const HELP = `Usage: oneclickpush [options]
|
|
7
|
+
|
|
8
|
+
Replace the contents of a remote git branch with that of a
|
|
9
|
+
local directory.
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
-d, --dist <dir> Directory to publish (default: ".")
|
|
13
|
+
-e, --exclude <glob> Paths to leave out, relative to dist
|
|
14
|
+
(default: "**/.DS_Store")
|
|
15
|
+
-b, --branch <name> Target branch (default: "gh-pages")
|
|
16
|
+
-m, --message <msg> Commit message (default: "Updates")
|
|
17
|
+
-f, --force Force push with no history
|
|
18
|
+
-n, --dry-run Show what would change, without
|
|
19
|
+
committing or pushing
|
|
20
|
+
-v, --verbose Log git commands
|
|
21
|
+
-V, --version Print version
|
|
22
|
+
-h, --help Show this help
|
|
23
|
+
|
|
24
|
+
--exclude takes glob patterns (*, **, ?) and can be repeated.
|
|
25
|
+
Patterns are matched against the full path, so use **/name to
|
|
26
|
+
match at any depth.`
|
|
27
|
+
|
|
28
|
+
const { values: v } = parseArgs({
|
|
29
|
+
options: {
|
|
30
|
+
dist: { type: 'string', short: 'd', default: '.' },
|
|
31
|
+
exclude: { type: 'string', short: 'e', multiple: true, default: ['**/.DS_Store'] },
|
|
32
|
+
branch: { type: 'string', short: 'b', default: 'gh-pages' },
|
|
33
|
+
force: { type: 'boolean', short: 'f', default: false },
|
|
34
|
+
message: { type: 'string', short: 'm', default: 'Updates' },
|
|
35
|
+
'dry-run': { type: 'boolean', short: 'n', default: false },
|
|
36
|
+
verbose: { type: 'boolean', short: 'v', default: false },
|
|
37
|
+
version: { type: 'boolean', short: 'V', default: false },
|
|
38
|
+
help: { type: 'boolean', short: 'h', default: false }
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (v.help) {
|
|
43
|
+
console.log(HELP)
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (v.version) {
|
|
48
|
+
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))
|
|
49
|
+
console.log(pkg.version)
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Dead simple spinner (skipped when verbose or piped)
|
|
54
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
55
|
+
let i = 0
|
|
56
|
+
const spin =
|
|
57
|
+
process.stderr.isTTY && !v.verbose
|
|
58
|
+
? setInterval(() => process.stderr.write(`\r${frames[i++ % frames.length]}`), 100)
|
|
59
|
+
: null
|
|
60
|
+
const stop = () => spin && (clearInterval(spin), process.stderr.write('\r \r'))
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const changes = await publish({ ...v, dryRun: v['dry-run'] })
|
|
64
|
+
stop()
|
|
65
|
+
console.log(
|
|
66
|
+
!changes
|
|
67
|
+
? 'Nothing to publish'
|
|
68
|
+
: v['dry-run']
|
|
69
|
+
? `Dry run, would publish:\n${changes}`
|
|
70
|
+
: 'Published'
|
|
71
|
+
)
|
|
72
|
+
} catch (/** @type {any} */ e) {
|
|
73
|
+
stop()
|
|
74
|
+
console.error(v.verbose ? e : e.message)
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type OneclickpushOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Directory to publish
|
|
4
|
+
*/
|
|
5
|
+
dist?: string
|
|
6
|
+
/**
|
|
7
|
+
* Paths to leave out, relative to dist (default: `**\/.DS_Store`)
|
|
8
|
+
*/
|
|
9
|
+
exclude?: string[]
|
|
10
|
+
/**
|
|
11
|
+
* Target branch
|
|
12
|
+
*/
|
|
13
|
+
branch?: string
|
|
14
|
+
/**
|
|
15
|
+
* Commit message
|
|
16
|
+
*/
|
|
17
|
+
message?: string
|
|
18
|
+
/**
|
|
19
|
+
* Force push with no history
|
|
20
|
+
*/
|
|
21
|
+
force?: boolean
|
|
22
|
+
/**
|
|
23
|
+
* Show what would change, without committing or pushing
|
|
24
|
+
*/
|
|
25
|
+
dryRun?: boolean
|
|
26
|
+
/**
|
|
27
|
+
* Log git commands
|
|
28
|
+
*/
|
|
29
|
+
verbose?: boolean
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} OneclickpushOptions
|
|
33
|
+
* @prop {string} [dist="."] Directory to publish
|
|
34
|
+
* @prop {string[]} [exclude] Paths to leave out, relative to dist (default: `**\/.DS_Store`)
|
|
35
|
+
* @prop {string} [branch="gh-pages"] Target branch
|
|
36
|
+
* @prop {string} [message="Updates"] Commit message
|
|
37
|
+
* @prop {boolean} [force=false] Force push with no history
|
|
38
|
+
* @prop {boolean} [dryRun=false] Show what would change, without committing or pushing
|
|
39
|
+
* @prop {boolean} [verbose=false] Log git commands
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Replace the contents of a git branch with the contents of a directory.
|
|
43
|
+
* Returns the staged changes as `git diff --name-status` text or "" if nothing changed.
|
|
44
|
+
* @param {OneclickpushOptions} options
|
|
45
|
+
* @returns {Promise<string>}
|
|
46
|
+
*/
|
|
47
|
+
export declare function publish({
|
|
48
|
+
dist,
|
|
49
|
+
exclude,
|
|
50
|
+
branch,
|
|
51
|
+
message,
|
|
52
|
+
force,
|
|
53
|
+
dryRun,
|
|
54
|
+
verbose
|
|
55
|
+
}: OneclickpushOptions): Promise<string>
|
package/index.js
CHANGED
|
@@ -1 +1,86 @@
|
|
|
1
|
-
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { promisify } from 'node:util'
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} OneclickpushOptions
|
|
9
|
+
* @prop {string} [dist="."] Directory to publish
|
|
10
|
+
* @prop {string[]} [exclude] Paths to leave out, relative to dist (default: `**\/.DS_Store`)
|
|
11
|
+
* @prop {string} [branch="gh-pages"] Target branch
|
|
12
|
+
* @prop {string} [message="Updates"] Commit message
|
|
13
|
+
* @prop {boolean} [force=false] Force push with no history
|
|
14
|
+
* @prop {boolean} [dryRun=false] Show what would change, without committing or pushing
|
|
15
|
+
* @prop {boolean} [verbose=false] Log git commands
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Replace the contents of a git branch with the contents of a directory.
|
|
20
|
+
* Returns the staged changes as `git diff --name-status` text or "" if nothing changed.
|
|
21
|
+
* @param {OneclickpushOptions} options
|
|
22
|
+
* @returns {Promise<string>}
|
|
23
|
+
*/
|
|
24
|
+
export async function publish({
|
|
25
|
+
dist = '.',
|
|
26
|
+
exclude = ['**/.DS_Store'],
|
|
27
|
+
branch = 'gh-pages',
|
|
28
|
+
message = 'Updates',
|
|
29
|
+
force = false,
|
|
30
|
+
dryRun = false,
|
|
31
|
+
verbose = false
|
|
32
|
+
}) {
|
|
33
|
+
dist = resolve(dist)
|
|
34
|
+
const run = promisify(execFile)
|
|
35
|
+
|
|
36
|
+
/** @type {(args: string[], cwd?: string) => Promise<string>} */
|
|
37
|
+
const git = async (args, cwd) => {
|
|
38
|
+
if (verbose) console.error(`$ git ${args.join(' ')}`)
|
|
39
|
+
return (await run('git', args, { cwd })).stdout.trim()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const url = await git(['config', '--get', 'remote.origin.url'])
|
|
43
|
+
const tmp = await mkdtemp(join(tmpdir(), 'oneclickpush-'))
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Reuse existing branch history unless forcing; otherwise start fresh
|
|
47
|
+
let cloned = false
|
|
48
|
+
if (!force) {
|
|
49
|
+
try {
|
|
50
|
+
await git(['clone', '--branch', branch, '--single-branch', '--depth', '1', url, tmp])
|
|
51
|
+
cloned = true
|
|
52
|
+
// eslint-disable-next-line no-empty
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
if (!cloned) {
|
|
56
|
+
await git(['init', '-q', '-b', branch], tmp)
|
|
57
|
+
await git(['remote', 'add', 'origin', url], tmp)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Empty the index, then stage everything from dist except excluded paths
|
|
61
|
+
await git(['read-tree', '--empty'], tmp)
|
|
62
|
+
await git(
|
|
63
|
+
[
|
|
64
|
+
'--git-dir',
|
|
65
|
+
join(tmp, '.git'),
|
|
66
|
+
'--work-tree',
|
|
67
|
+
dist,
|
|
68
|
+
'add',
|
|
69
|
+
'--force',
|
|
70
|
+
'--',
|
|
71
|
+
'.',
|
|
72
|
+
...exclude.map((p) => `:(exclude,glob)${p}`)
|
|
73
|
+
],
|
|
74
|
+
dist
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const status = await git(['diff', '--cached', '--name-status'], tmp)
|
|
78
|
+
if (!status || dryRun) return status
|
|
79
|
+
|
|
80
|
+
await git(['commit', '-q', '-m', message], tmp)
|
|
81
|
+
await git(['push', '-q', ...(force ? ['--force'] : []), 'origin', branch], tmp)
|
|
82
|
+
return status
|
|
83
|
+
} finally {
|
|
84
|
+
await rm(tmp, { recursive: true, force: true })
|
|
85
|
+
}
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oneclickpush",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "One-click GitHub Pages deployer",
|
|
5
5
|
"author": "Jason Lee <jason@zerodevx.com>",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"zx": "^8.8.5"
|
|
7
|
+
"prepublishOnly": "npm run lint",
|
|
8
|
+
"emit": "tsc index.js -d --emitDeclarationOnly --allowJs && prettier --write index.d.ts",
|
|
9
|
+
"lint": "npm run emit && prettier --check . && eslint && npx publint",
|
|
10
|
+
"test": "node --test"
|
|
12
11
|
},
|
|
13
12
|
"devDependencies": {
|
|
14
13
|
"@eslint/js": "^10.0.1",
|
|
15
14
|
"eslint": "^10.12.0",
|
|
16
15
|
"globals": "^17.13.0",
|
|
17
|
-
"prettier": "^3.9.10"
|
|
16
|
+
"prettier": "^3.9.10",
|
|
17
|
+
"typescript": "^7.0.2"
|
|
18
18
|
},
|
|
19
19
|
"type": "module",
|
|
20
20
|
"exports": "./index.js",
|
|
21
|
+
"types": "./index.d.ts",
|
|
21
22
|
"bin": "cli.js",
|
|
22
23
|
"files": [
|
|
23
24
|
"index.js",
|
|
25
|
+
"index.d.ts",
|
|
24
26
|
"cli.js"
|
|
25
27
|
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.3.0"
|
|
30
|
+
},
|
|
26
31
|
"license": "ISC",
|
|
27
32
|
"repository": "github:zerodevx/oneclickpush",
|
|
28
33
|
"keywords": [
|