oneclickpush 0.1.0-next.1 → 0.2.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 +139 -0
- package/cli.js +79 -1
- package/index.d.ts +67 -0
- package/index.js +111 -1
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -1,3 +1,142 @@
|
|
|
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 built-in Node APIs + `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
|
+
-r, --remote <name|url> Remote name or repo URL to publish to (default: "origin")
|
|
36
|
+
-a, --author <author> Commit author, as "Name <email>"
|
|
37
|
+
-f, --force Force push new commit with no history
|
|
38
|
+
-n, --dry-run Show what would change, without committing or pushing
|
|
39
|
+
-v, --verbose Log git commands
|
|
40
|
+
-V, --version Print version
|
|
41
|
+
-h, --help Show this help
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> [!TIP]
|
|
45
|
+
> `--exclude` takes glob patterns (`*`, `**`, `?`) and can be repeated. Patterns are matched against
|
|
46
|
+
> the full path, so use `**/name` to match at any depth.
|
|
47
|
+
|
|
48
|
+
### Examples
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Preview what would be deployed without pushing
|
|
52
|
+
npx oneclickpush -d build --dry-run
|
|
53
|
+
|
|
54
|
+
# Custom branch and commit message
|
|
55
|
+
npx oneclickpush -d public -b release -m "deploy: v1.0.0"
|
|
56
|
+
|
|
57
|
+
# Exclude source maps and test files
|
|
58
|
+
npx oneclickpush -d dist -e "**/*.map" -e "**/tests/**"
|
|
59
|
+
|
|
60
|
+
# Deploy to a different remote or repository URL
|
|
61
|
+
npx oneclickpush -d dist -r upstream
|
|
62
|
+
npx oneclickpush -d dist -r git@github.com:user/repo.git
|
|
63
|
+
|
|
64
|
+
# Set commit author (useful in CI environments)
|
|
65
|
+
npx oneclickpush -d dist -a "Deploy Bot <bot@example.com>"
|
|
66
|
+
|
|
67
|
+
# Force push fresh history
|
|
68
|
+
npx oneclickpush -d dist --force
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Programmatic API
|
|
72
|
+
|
|
73
|
+
You can also use `oneclickpush` as an ES module:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Install the library
|
|
77
|
+
npm i oneclickpush
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
import { publish } from 'oneclickpush'
|
|
82
|
+
|
|
83
|
+
const changes = await publish({
|
|
84
|
+
dist: 'dist',
|
|
85
|
+
exclude: ['**/.DS_Store', '**/*.map'],
|
|
86
|
+
branch: 'gh-pages',
|
|
87
|
+
message: 'Deploy updates',
|
|
88
|
+
remote: 'origin',
|
|
89
|
+
author: 'Deploy Bot <bot@example.com>',
|
|
90
|
+
force: false,
|
|
91
|
+
dryRun: false,
|
|
92
|
+
verbose: false
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (changes) {
|
|
96
|
+
console.log('Published changes:\n' + changes)
|
|
97
|
+
} else {
|
|
98
|
+
console.log('Nothing to publish')
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Options
|
|
103
|
+
|
|
104
|
+
| Option | Type | Default | Description |
|
|
105
|
+
| --------- | ---------- | ------------------ | --------------------------------------------------- |
|
|
106
|
+
| `dist` | `string` | `"."` | Directory to publish |
|
|
107
|
+
| `exclude` | `string[]` | `["**/.DS_Store"]` | Paths to exclude relative to `dist` |
|
|
108
|
+
| `branch` | `string` | `"gh-pages"` | Target git branch |
|
|
109
|
+
| `message` | `string` | `"Updates"` | Git commit message |
|
|
110
|
+
| `remote` | `string` | `"origin"` | Remote name or repository URL/path to publish to |
|
|
111
|
+
| `author` | `string` | — | Commit author and committer, as `"Name <email>"` |
|
|
112
|
+
| `force` | `boolean` | `false` | Force push fresh commit with no previous history |
|
|
113
|
+
| `dryRun` | `boolean` | `false` | Return staged changes without committing or pushing |
|
|
114
|
+
| `verbose` | `boolean` | `false` | Log executed git commands |
|
|
115
|
+
|
|
116
|
+
## How It Works
|
|
117
|
+
|
|
118
|
+
1. Resolves the target repository URL from git remote configuration or uses the provided remote
|
|
119
|
+
URL/path directly.
|
|
120
|
+
2. Creates an isolated temporary directory and fetches the target branch (or initializes it if it
|
|
121
|
+
doesn't exist).
|
|
122
|
+
3. Clears the target branch index and stages the contents of `dist` (minus `exclude` patterns)
|
|
123
|
+
directly via Git `work-tree`.
|
|
124
|
+
4. Checks for differences (`git diff --cached --name-status`). If there are changes, commits and
|
|
125
|
+
pushes back to the remote repository.
|
|
126
|
+
5. Cleans up the temporary directory.
|
|
127
|
+
|
|
128
|
+
> [!WARNING]
|
|
129
|
+
> Requires Node 18.3+ and Git 2.28+.
|
|
130
|
+
|
|
131
|
+
### Notes
|
|
132
|
+
|
|
133
|
+
- Dotfiles (except `.DS_Store`) are included by default; use `-e` to exclude.
|
|
134
|
+
- Contents in target branch are emptied and replaced with the contents of `dist` (WYSIWYG).
|
|
135
|
+
- If `.nojekyll` or `CNAME` files are required, add them to `dist`; use a post-build step if needed.
|
|
136
|
+
- If the target branch does not exist, it will be created.
|
|
137
|
+
- When `--remote` is a repository URL or path, running from within an existing git repository is not
|
|
138
|
+
required.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
ISC
|
package/cli.js
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
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
|
+
-r, --remote <name|url> Remote name or repo URL to publish to
|
|
18
|
+
(default: "origin")
|
|
19
|
+
-a, --author <author> Commit author, as "Name <email>"
|
|
20
|
+
-f, --force Force push new commit with no history
|
|
21
|
+
-n, --dry-run Show what would change, without
|
|
22
|
+
committing or pushing
|
|
23
|
+
-v, --verbose Log git commands
|
|
24
|
+
-V, --version Print version
|
|
25
|
+
-h, --help Show this help
|
|
26
|
+
|
|
27
|
+
--exclude takes glob patterns (*, **, ?) and can be repeated.
|
|
28
|
+
Patterns are matched against the full path, so use **/name to
|
|
29
|
+
match at any depth.`
|
|
30
|
+
|
|
31
|
+
const { values: v } = parseArgs({
|
|
32
|
+
options: {
|
|
33
|
+
dist: { type: 'string', short: 'd', default: '.' },
|
|
34
|
+
exclude: { type: 'string', short: 'e', multiple: true, default: ['**/.DS_Store'] },
|
|
35
|
+
branch: { type: 'string', short: 'b', default: 'gh-pages' },
|
|
36
|
+
message: { type: 'string', short: 'm', default: 'Updates' },
|
|
37
|
+
remote: { type: 'string', short: 'r', default: 'origin' },
|
|
38
|
+
author: { type: 'string', short: 'a' },
|
|
39
|
+
force: { type: 'boolean', short: 'f', default: false },
|
|
40
|
+
'dry-run': { type: 'boolean', short: 'n', default: false },
|
|
41
|
+
verbose: { type: 'boolean', short: 'v', default: false },
|
|
42
|
+
version: { type: 'boolean', short: 'V', default: false },
|
|
43
|
+
help: { type: 'boolean', short: 'h', default: false }
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (v.help) {
|
|
48
|
+
console.log(HELP)
|
|
49
|
+
process.exit(0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (v.version) {
|
|
53
|
+
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'))
|
|
54
|
+
console.log(pkg.version)
|
|
55
|
+
process.exit(0)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Dead simple spinner (skipped when verbose or piped)
|
|
59
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
60
|
+
let i = 0
|
|
61
|
+
const spin =
|
|
62
|
+
process.stderr.isTTY && !v.verbose
|
|
63
|
+
? setInterval(() => process.stderr.write(`\r${frames[i++ % frames.length]}`), 100)
|
|
64
|
+
: null
|
|
65
|
+
const stop = () => spin && (clearInterval(spin), process.stderr.write('\r \r'))
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const changes = await publish({ ...v, dryRun: v['dry-run'] })
|
|
69
|
+
stop()
|
|
70
|
+
console.log(
|
|
71
|
+
!changes
|
|
72
|
+
? 'Nothing to publish'
|
|
73
|
+
: v['dry-run']
|
|
74
|
+
? `Dry run, would publish:\n${changes}`
|
|
75
|
+
: 'Published'
|
|
76
|
+
)
|
|
77
|
+
} catch (/** @type {any} */ e) {
|
|
78
|
+
stop()
|
|
79
|
+
console.error(v.verbose ? e : e.message)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
* Remote name of the current repo, or a repository URL or path
|
|
20
|
+
*/
|
|
21
|
+
remote?: string
|
|
22
|
+
/**
|
|
23
|
+
* Commit author and committer as "Name <email>"
|
|
24
|
+
*/
|
|
25
|
+
author?: string
|
|
26
|
+
/**
|
|
27
|
+
* Force push new commit with no history
|
|
28
|
+
*/
|
|
29
|
+
force?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Show what would change, without committing or pushing
|
|
32
|
+
*/
|
|
33
|
+
dryRun?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* Log git commands
|
|
36
|
+
*/
|
|
37
|
+
verbose?: boolean
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {object} OneclickpushOptions
|
|
41
|
+
* @prop {string} [dist="."] Directory to publish
|
|
42
|
+
* @prop {string[]} [exclude] Paths to leave out, relative to dist (default: `**\/.DS_Store`)
|
|
43
|
+
* @prop {string} [branch="gh-pages"] Target branch
|
|
44
|
+
* @prop {string} [message="Updates"] Commit message
|
|
45
|
+
* @prop {string} [remote="origin"] Remote name of the current repo, or a repository URL or path
|
|
46
|
+
* @prop {string} [author] Commit author and committer as "Name <email>"
|
|
47
|
+
* @prop {boolean} [force=false] Force push new commit with no history
|
|
48
|
+
* @prop {boolean} [dryRun=false] Show what would change, without committing or pushing
|
|
49
|
+
* @prop {boolean} [verbose=false] Log git commands
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Replace the contents of a git branch with the contents of a directory.
|
|
53
|
+
* Returns the staged changes as `git diff --name-status` text or "" if nothing changed.
|
|
54
|
+
* @param {OneclickpushOptions} options
|
|
55
|
+
* @returns {Promise<string>}
|
|
56
|
+
*/
|
|
57
|
+
export declare function publish({
|
|
58
|
+
dist,
|
|
59
|
+
exclude,
|
|
60
|
+
branch,
|
|
61
|
+
message,
|
|
62
|
+
remote,
|
|
63
|
+
author,
|
|
64
|
+
force,
|
|
65
|
+
dryRun,
|
|
66
|
+
verbose
|
|
67
|
+
}: OneclickpushOptions): Promise<string>
|
package/index.js
CHANGED
|
@@ -1 +1,111 @@
|
|
|
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 {string} [remote="origin"] Remote name of the current repo, or a repository URL or path
|
|
14
|
+
* @prop {string} [author] Commit author and committer as "Name <email>"
|
|
15
|
+
* @prop {boolean} [force=false] Force push new commit with no history
|
|
16
|
+
* @prop {boolean} [dryRun=false] Show what would change, without committing or pushing
|
|
17
|
+
* @prop {boolean} [verbose=false] Log git commands
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Replace the contents of a git branch with the contents of a directory.
|
|
22
|
+
* Returns the staged changes as `git diff --name-status` text or "" if nothing changed.
|
|
23
|
+
* @param {OneclickpushOptions} options
|
|
24
|
+
* @returns {Promise<string>}
|
|
25
|
+
*/
|
|
26
|
+
export async function publish({
|
|
27
|
+
dist = '.',
|
|
28
|
+
exclude = ['**/.DS_Store'],
|
|
29
|
+
branch = 'gh-pages',
|
|
30
|
+
message = 'Updates',
|
|
31
|
+
remote = 'origin',
|
|
32
|
+
author,
|
|
33
|
+
force = false,
|
|
34
|
+
dryRun = false,
|
|
35
|
+
verbose = false
|
|
36
|
+
}) {
|
|
37
|
+
dist = resolve(dist)
|
|
38
|
+
const run = promisify(execFile)
|
|
39
|
+
|
|
40
|
+
// Env vars beat git config, so this also overrides any identity already set in CI
|
|
41
|
+
/** @type {Record<string, string>} */
|
|
42
|
+
let identity = {}
|
|
43
|
+
if (author) {
|
|
44
|
+
const m = /^(.+?)\s*<(.+)>$/.exec(author)
|
|
45
|
+
if (!m) throw new Error('--author must look like "Name <email>"')
|
|
46
|
+
identity = {
|
|
47
|
+
GIT_AUTHOR_NAME: m[1],
|
|
48
|
+
GIT_AUTHOR_EMAIL: m[2],
|
|
49
|
+
GIT_COMMITTER_NAME: m[1],
|
|
50
|
+
GIT_COMMITTER_EMAIL: m[2]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @type {(args: string[], cwd?: string, env?: Record<string, string>) => Promise<string>} */
|
|
55
|
+
const git = async (args, cwd, env) => {
|
|
56
|
+
if (verbose) console.error(`$ git ${args.join(' ')}`)
|
|
57
|
+
return (await run('git', args, { cwd, env: { ...process.env, ...env } })).stdout.trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A remote name resolves to its url; anything else is used as a url or path as-is
|
|
61
|
+
let url = remote
|
|
62
|
+
try {
|
|
63
|
+
url = await git(['remote', 'get-url', remote])
|
|
64
|
+
} catch {
|
|
65
|
+
if (/^[\w-]+$/.test(remote)) throw new Error(`Remote "${remote}" not found`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tmp = await mkdtemp(join(tmpdir(), 'oneclickpush-'))
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Reuse existing branch history unless forcing; otherwise start fresh
|
|
72
|
+
let cloned = false
|
|
73
|
+
if (!force) {
|
|
74
|
+
try {
|
|
75
|
+
await git(['clone', '--branch', branch, '--single-branch', '--depth', '1', url, tmp])
|
|
76
|
+
cloned = true
|
|
77
|
+
// eslint-disable-next-line no-empty
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
if (!cloned) {
|
|
81
|
+
await git(['init', '-q', '-b', branch], tmp)
|
|
82
|
+
await git(['remote', 'add', 'origin', url], tmp)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Empty the index, then stage everything from dist except excluded paths
|
|
86
|
+
await git(['read-tree', '--empty'], tmp)
|
|
87
|
+
await git(
|
|
88
|
+
[
|
|
89
|
+
'--git-dir',
|
|
90
|
+
join(tmp, '.git'),
|
|
91
|
+
'--work-tree',
|
|
92
|
+
dist,
|
|
93
|
+
'add',
|
|
94
|
+
'--force',
|
|
95
|
+
'--',
|
|
96
|
+
'.',
|
|
97
|
+
...exclude.map((p) => `:(exclude,glob)${p}`)
|
|
98
|
+
],
|
|
99
|
+
dist
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const status = await git(['diff', '--cached', '--name-status'], tmp)
|
|
103
|
+
if (!status || dryRun) return status
|
|
104
|
+
|
|
105
|
+
await git(['commit', '-q', '-m', message], tmp, identity)
|
|
106
|
+
await git(['push', '-q', ...(force ? ['--force'] : []), 'origin', branch], tmp)
|
|
107
|
+
return status
|
|
108
|
+
} finally {
|
|
109
|
+
await rm(tmp, { recursive: true, force: true })
|
|
110
|
+
}
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oneclickpush",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": [
|