oneclickpush 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026, Jason Lee <jason@zerodevx.com>
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,126 @@
1
- # Temporary Holding Version
1
+ # oneclickpush
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util'
3
+ import { readFileSync } from 'node:fs'
4
+ import { publish } from './index.js'
5
+
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 ADDED
@@ -0,0 +1,86 @@
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,6 +1,38 @@
1
1
  {
2
2
  "name": "oneclickpush",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "One-click GitHub Pages deployer",
5
+ "author": "Jason Lee <jason@zerodevx.com>",
6
+ "scripts": {
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"
11
+ },
12
+ "devDependencies": {
13
+ "@eslint/js": "^10.0.1",
14
+ "eslint": "^10.12.0",
15
+ "globals": "^17.13.0",
16
+ "prettier": "^3.9.10",
17
+ "typescript": "^7.0.2"
18
+ },
19
+ "type": "module",
20
+ "exports": "./index.js",
21
+ "types": "./index.d.ts",
22
+ "bin": "cli.js",
23
+ "files": [
24
+ "index.js",
25
+ "index.d.ts",
26
+ "cli.js"
27
+ ],
28
+ "engines": {
29
+ "node": ">=18.3.0"
30
+ },
31
+ "license": "ISC",
32
+ "repository": "github:zerodevx/oneclickpush",
33
+ "keywords": [
34
+ "one-click",
35
+ "gh-pages",
36
+ "deploy"
37
+ ]
38
+ }