@peterseibel/hug 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/.claude/settings.local.json +20 -0
- package/CLAUDE.md +72 -0
- package/DEVELOPMENT.md +86 -0
- package/README.md +111 -0
- package/bin/hug +458 -0
- package/lib/common.sh +108 -0
- package/package.json +13 -0
- package/plans/hug-cli-tool.md +132 -0
- package/plans/hug-config.md +83 -0
- package/plans/multi-project-deployment.md +78 -0
- package/templates/blank/Code.js +2 -0
- package/templates/blank/appsscript.json +5 -0
- package/templates/webapp/Code.js +4 -0
- package/templates/webapp/appsscript.json +9 -0
- package/templates/webapp/index.html +14 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"WebFetch(domain:registry.npmjs.org)",
|
|
5
|
+
"WebFetch(domain:script.googleapis.com)",
|
|
6
|
+
"WebFetch(domain:oauth2.googleapis.com)",
|
|
7
|
+
"Bash(./node_modules/.bin/clasp clone:*)",
|
|
8
|
+
"WebFetch(domain:developers.google.com)",
|
|
9
|
+
"WebFetch(domain:github.com)",
|
|
10
|
+
"WebSearch",
|
|
11
|
+
"WebFetch(domain:www.npmjs.com)",
|
|
12
|
+
"Bash(./node_modules/.bin/clasp --version && ./node_modules/.bin/clasp deploy --help 2>&1; ./node_modules/.bin/clasp redeploy --help 2>&1)",
|
|
13
|
+
"Bash(./node_modules/.bin/clasp list-versions:*)",
|
|
14
|
+
"Bash(./node_modules/.bin/clasp create-version:*)",
|
|
15
|
+
"Bash(./node_modules/.bin/clasp list-deployments:*)",
|
|
16
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
17
|
+
"Bash(gh api:*)"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`hug` is a lightweight bash CLI that wraps [clasp](https://github.com/google/clasp) to provide opinionated project management for Google Apps Script projects. It handles project creation (from templates or by importing existing projects), forking projects for branch-based workflows, and a push→version→deploy workflow.
|
|
8
|
+
|
|
9
|
+
## Project structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
bin/hug # CLI entry point (bash, subcommand dispatch)
|
|
13
|
+
lib/common.sh # Shared functions (clasp resolution, deployment helpers)
|
|
14
|
+
templates/blank/ # Minimal Apps Script project template
|
|
15
|
+
templates/webapp/ # Webapp template (doGet + index.html)
|
|
16
|
+
package.json # npm package with bin field pointing to bin/hug
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Key commands
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
./bin/hug init [--template blank|webapp] [name] # create new project from template
|
|
23
|
+
./bin/hug init --scriptId <id> [name] # import existing project
|
|
24
|
+
./bin/hug fork # new Apps Script project from current code
|
|
25
|
+
./bin/hug config set KEY=VALUE # manage config.js
|
|
26
|
+
./bin/hug deploy "description" # push + version + deploy
|
|
27
|
+
./bin/hug deploy --rollback <version> # roll back
|
|
28
|
+
./bin/hug push / pull [-f] / open # clasp passthrough (pull checks git status)
|
|
29
|
+
./bin/hug versions / deployments # list versions/deployments
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Design decisions
|
|
33
|
+
|
|
34
|
+
### Deployments
|
|
35
|
+
|
|
36
|
+
Apps Script deployments are mainly useful for web apps (stable URL) and API
|
|
37
|
+
executables. Hug takes an opinionated approach: one deployment per project,
|
|
38
|
+
managed via `hug deploy`. If multiple deployments exist (e.g. created directly
|
|
39
|
+
via clasp), hug handles them gracefully — `select_deployment` in `common.sh`
|
|
40
|
+
prompts the user to pick one. But hug doesn't provide commands to create or
|
|
41
|
+
delete individual deployments.
|
|
42
|
+
|
|
43
|
+
Multiple deployments on a single script share script properties, so they all
|
|
44
|
+
operate on the same underlying data (e.g. the same spreadsheet). This makes
|
|
45
|
+
them suitable for variant UIs against the same data, but not for dev/prod
|
|
46
|
+
separation. For dev/prod, use `hug fork` + git branches instead — each branch
|
|
47
|
+
gets its own script project with independent properties and deployments.
|
|
48
|
+
|
|
49
|
+
### Config
|
|
50
|
+
|
|
51
|
+
`hug config` manages a local `config.js` file (JS constants object) that gets
|
|
52
|
+
pushed with the code. This avoids the heavy GCP setup required by `clasp run`
|
|
53
|
+
for setting script properties. Config values are in source/git, which is fine
|
|
54
|
+
for spreadsheet IDs etc. but not for secrets. Each git branch can have different
|
|
55
|
+
config values, pairing well with `hug fork`.
|
|
56
|
+
|
|
57
|
+
### Auth
|
|
58
|
+
|
|
59
|
+
Hug does not wrap `clasp login`. Auth credentials are stored globally in
|
|
60
|
+
`~/.clasprc.json` and persist across projects. Instead, all clasp invocations
|
|
61
|
+
go through `run_clasp` which detects auth errors and suggests running
|
|
62
|
+
`npx clasp login`.
|
|
63
|
+
|
|
64
|
+
## Development
|
|
65
|
+
|
|
66
|
+
There is no build step or test suite. The CLI is pure bash. To test locally:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
./bin/hug --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
clasp is a dependency (`@google/clasp`). The CLI finds it at `./node_modules/.bin/clasp` first, then falls back to a global `clasp`.
|
package/DEVELOPMENT.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
## Local testing
|
|
4
|
+
|
|
5
|
+
There's no build step or test suite. To test locally, run commands directly:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
./bin/hug --help
|
|
9
|
+
./bin/hug init --template webapp /tmp/test-project
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
To test as if installed globally, link the package:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm link
|
|
16
|
+
hug --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
To unlink:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm unlink -g @peterseibel/hug
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Project structure
|
|
26
|
+
|
|
27
|
+
- `bin/hug` — CLI entry point (bash). All subcommands live here.
|
|
28
|
+
- `lib/common.sh` — Shared functions sourced by `bin/hug`.
|
|
29
|
+
- `templates/` — Project templates copied by `hug init`.
|
|
30
|
+
- `plans/` — Implementation plans. `plans/done/` holds completed plans.
|
|
31
|
+
|
|
32
|
+
## Publishing to npm
|
|
33
|
+
|
|
34
|
+
The package is scoped as `@peterseibel/hug`.
|
|
35
|
+
|
|
36
|
+
### First time
|
|
37
|
+
|
|
38
|
+
Make sure you're logged in to npm and that your scope is configured:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm login
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Scoped packages are private by default. To publish as public:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm publish --access public
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Subsequent releases
|
|
51
|
+
|
|
52
|
+
1. Bump the version in `package.json` (or use `npm version`):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm version patch # 0.1.0 -> 0.1.1
|
|
56
|
+
npm version minor # 0.1.1 -> 0.2.0
|
|
57
|
+
npm version major # 0.2.0 -> 1.0.0
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`npm version` creates a git commit and tag automatically.
|
|
61
|
+
|
|
62
|
+
2. Publish:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm publish
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
3. Push the version commit and tag:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git push && git push --tags
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Dependencies
|
|
75
|
+
|
|
76
|
+
The only runtime dependency is `@google/clasp`. It's listed in `package.json`
|
|
77
|
+
so it gets installed when users `npm install -g @peterseibel/hug`.
|
|
78
|
+
|
|
79
|
+
In user projects created by `hug init`, clasp is installed as a dev dependency
|
|
80
|
+
via `ensure_clasp` in `lib/common.sh`.
|
|
81
|
+
|
|
82
|
+
## Auth
|
|
83
|
+
|
|
84
|
+
Clasp credentials live in `~/.clasprc.json` (global, not per-project). To test
|
|
85
|
+
commands that talk to the Apps Script API, you need to have run `clasp login`
|
|
86
|
+
at least once.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# hug
|
|
2
|
+
|
|
3
|
+
A lightweight wrapper around [clasp](https://github.com/google/clasp) for managing Google Apps Script projects.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @peterseibel/hug
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or clone this repo and link it:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone <repo-url> && cd hug
|
|
15
|
+
npm install && npm link
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
You need to be logged in to clasp:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx clasp login
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
### Create a new project
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
hug init my-app # blank project
|
|
32
|
+
hug init --template webapp my-app # webapp with doGet + index.html
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This creates the directory, copies template files, installs clasp, and creates the Apps Script project.
|
|
36
|
+
|
|
37
|
+
### Import an existing project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
hug init --scriptId <scriptId> my-project
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Imports an existing Apps Script project into a new directory and sets up npm/clasp.
|
|
44
|
+
|
|
45
|
+
### Fork a project
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
hug fork
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Creates a new Apps Script project from the current local code. Useful with git branches — fork on a branch to get a separate Apps Script project you can develop against independently.
|
|
52
|
+
|
|
53
|
+
### Configure
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
hug config # list config values
|
|
57
|
+
hug config set SPREADSHEET_ID=1Bx.. # set a value
|
|
58
|
+
hug config set FOO=bar BAZ=qux # set multiple values
|
|
59
|
+
hug config unset FOO # remove a value
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Manages a `config.js` file that gets pushed with your code. Apps Script code can access values via `CONFIG.SPREADSHEET_ID`, etc. Useful for pointing different branches/forks at different resources.
|
|
63
|
+
|
|
64
|
+
Note: config values are stored in source. Don't put secrets here.
|
|
65
|
+
|
|
66
|
+
### Push / Pull / Open
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
hug push # push local files to Apps Script
|
|
70
|
+
hug pull # pull remote files (refuses if uncommitted changes)
|
|
71
|
+
hug pull -f # pull even with uncommitted changes
|
|
72
|
+
hug open # open in the Apps Script editor
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Deploy
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
hug deploy "description of changes"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Pushes code, creates a version, and updates the existing deployment (or creates one if none exists).
|
|
82
|
+
|
|
83
|
+
### Roll back
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
hug deploy --rollback <versionNumber>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### List versions and deployments
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
hug versions
|
|
93
|
+
hug deployments
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Templates
|
|
97
|
+
|
|
98
|
+
- **blank** — minimal `appsscript.json` + empty `Code.js`
|
|
99
|
+
- **webapp** — `doGet()` serving an `index.html`, with webapp config in the manifest
|
|
100
|
+
|
|
101
|
+
## Branch-per-environment pattern
|
|
102
|
+
|
|
103
|
+
Use `hug fork` with git branches to maintain separate Apps Script projects:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git checkout -b staging
|
|
107
|
+
hug fork # new Apps Script project, updates .clasp.json
|
|
108
|
+
hug config set SPREADSHEET_ID=1Bx.. # point at a staging spreadsheet
|
|
109
|
+
hug deploy # deploys to the staging project
|
|
110
|
+
git checkout main # .clasp.json and config.js switch back to production
|
|
111
|
+
```
|
package/bin/hug
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
HUG_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
source "$HUG_ROOT/lib/common.sh"
|
|
7
|
+
|
|
8
|
+
VERSION="0.1.0"
|
|
9
|
+
|
|
10
|
+
usage() {
|
|
11
|
+
cat <<EOF
|
|
12
|
+
hug $VERSION — a lightweight wrapper around clasp
|
|
13
|
+
|
|
14
|
+
Usage: hug <command> [options]
|
|
15
|
+
|
|
16
|
+
Project commands:
|
|
17
|
+
init [--template blank|webapp] [name] Create a new project from a template
|
|
18
|
+
init --scriptId <id> [name] Import an existing Apps Script project
|
|
19
|
+
fork Create a new Apps Script project from current code
|
|
20
|
+
|
|
21
|
+
Development commands:
|
|
22
|
+
push Push local files to Apps Script
|
|
23
|
+
pull [-f|--force] Pull remote files (refuses if uncommitted changes)
|
|
24
|
+
open Open the project in the Apps Script editor
|
|
25
|
+
|
|
26
|
+
Configuration:
|
|
27
|
+
config List config values
|
|
28
|
+
config set KEY=VALUE ... Set config values (writes config.js)
|
|
29
|
+
config unset KEY ... Remove config values
|
|
30
|
+
|
|
31
|
+
Deployment commands:
|
|
32
|
+
deploy [description] Push, version, and update deployment
|
|
33
|
+
deploy --rollback <version> Roll back to a previous version
|
|
34
|
+
versions List versions
|
|
35
|
+
deployments List deployments
|
|
36
|
+
|
|
37
|
+
Run 'hug <command> --help' for details on a specific command.
|
|
38
|
+
EOF
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ─── init ────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
cmd_init() {
|
|
44
|
+
local template="" name="" script_id="" force=false
|
|
45
|
+
|
|
46
|
+
while [ $# -gt 0 ]; do
|
|
47
|
+
case "$1" in
|
|
48
|
+
--template) template="$2"; shift 2 ;;
|
|
49
|
+
--scriptId) script_id="$2"; shift 2 ;;
|
|
50
|
+
-f|--force) force=true; shift ;;
|
|
51
|
+
--help)
|
|
52
|
+
echo "Usage: hug init [--template blank|webapp] [-f|--force] [name]"
|
|
53
|
+
echo " hug init --scriptId <id> [-f|--force] [name]"
|
|
54
|
+
echo ""
|
|
55
|
+
echo "Creates a new Apps Script project. If name is given, creates a directory."
|
|
56
|
+
echo "Refuses if the directory already exists (use -f to override)."
|
|
57
|
+
echo ""
|
|
58
|
+
echo "Options:"
|
|
59
|
+
echo " --template Start from a template: blank (default), webapp"
|
|
60
|
+
echo " --scriptId Import an existing Apps Script project by ID"
|
|
61
|
+
echo " -f, --force Use an existing directory even if it already exists"
|
|
62
|
+
return 0 ;;
|
|
63
|
+
*) name="$1"; shift ;;
|
|
64
|
+
esac
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
# Validate flags
|
|
68
|
+
if [ -n "$script_id" ] && [ -n "$template" ]; then
|
|
69
|
+
echo "Error: --scriptId and --template are mutually exclusive" >&2
|
|
70
|
+
return 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
local project_dir="."
|
|
74
|
+
if [ -n "$name" ]; then
|
|
75
|
+
project_dir="$name"
|
|
76
|
+
if [ -d "$project_dir" ] && [ "$force" = false ]; then
|
|
77
|
+
echo "Error: directory '$project_dir' already exists. Use -f to override." >&2
|
|
78
|
+
return 1
|
|
79
|
+
fi
|
|
80
|
+
mkdir -p "$project_dir"
|
|
81
|
+
elif [ -f .clasp.json ] && [ "$force" = false ]; then
|
|
82
|
+
echo "Error: .clasp.json already exists in this directory. Use -f to override." >&2
|
|
83
|
+
return 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
cd "$project_dir"
|
|
87
|
+
ensure_clasp
|
|
88
|
+
local clasp
|
|
89
|
+
clasp=$(find_clasp)
|
|
90
|
+
|
|
91
|
+
if [ -n "$script_id" ]; then
|
|
92
|
+
# Import mode: clone an existing project
|
|
93
|
+
echo "Importing project $script_id..."
|
|
94
|
+
run_clasp "$clasp" clone "$script_id"
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
echo "Project imported. Next steps:"
|
|
98
|
+
echo " hug pull Fetch the latest code"
|
|
99
|
+
echo " hug open Open in the Apps Script editor"
|
|
100
|
+
else
|
|
101
|
+
# Template mode: create a new project
|
|
102
|
+
template="${template:-blank}"
|
|
103
|
+
|
|
104
|
+
local template_dir="$HUG_ROOT/templates/$template"
|
|
105
|
+
if [ ! -d "$template_dir" ]; then
|
|
106
|
+
echo "Error: unknown template '$template'. Available: blank, webapp" >&2
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
echo "Copying $template template..."
|
|
111
|
+
cp -r "$template_dir"/* ./
|
|
112
|
+
|
|
113
|
+
local title="${name:-$(basename "$(pwd)")}"
|
|
114
|
+
local type="standalone"
|
|
115
|
+
if [ "$template" = "webapp" ]; then
|
|
116
|
+
type="webapp"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
echo "Creating Apps Script project '$title' (type: $type)..."
|
|
120
|
+
run_clasp "$clasp" create --type "$type" --title "$title"
|
|
121
|
+
|
|
122
|
+
echo ""
|
|
123
|
+
echo "Project ready. Next steps:"
|
|
124
|
+
echo " hug push Push code to Apps Script"
|
|
125
|
+
echo " hug open Open in the Apps Script editor"
|
|
126
|
+
if [ "$template" = "webapp" ]; then
|
|
127
|
+
echo " hug deploy Push, version, and create a deployment"
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# ─── fork ─────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
cmd_fork() {
|
|
135
|
+
local force=false
|
|
136
|
+
while [ $# -gt 0 ]; do
|
|
137
|
+
case "$1" in
|
|
138
|
+
-f|--force) force=true; shift ;;
|
|
139
|
+
--help)
|
|
140
|
+
echo "Usage: hug fork [-f|--force]"
|
|
141
|
+
echo ""
|
|
142
|
+
echo "Creates a new Apps Script project from the current local code."
|
|
143
|
+
echo "Replaces .clasp.json with a new project, then pushes code to it."
|
|
144
|
+
echo ""
|
|
145
|
+
echo "Refuses if the current project is container-bound (use -f to override)."
|
|
146
|
+
echo ""
|
|
147
|
+
echo "Useful with git branches: fork on a branch to get a separate"
|
|
148
|
+
echo "Apps Script project for each branch."
|
|
149
|
+
return 0 ;;
|
|
150
|
+
*) shift ;;
|
|
151
|
+
esac
|
|
152
|
+
done
|
|
153
|
+
|
|
154
|
+
if [ ! -f .clasp.json ]; then
|
|
155
|
+
echo "Error: no .clasp.json found. Run 'hug init' first." >&2
|
|
156
|
+
return 1
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
if [ "$force" = false ] && grep -q '"parentId"' .clasp.json 2>/dev/null; then
|
|
160
|
+
local parent_id
|
|
161
|
+
parent_id=$(grep '"parentId"' .clasp.json | sed 's/.*"parentId"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
|
|
162
|
+
echo "Error: this is a container-bound script. Forking will create a standalone" >&2
|
|
163
|
+
echo "project, but the code may depend on its container (e.g. by calling." >&2
|
|
164
|
+
echo "getActiveSpreadsheet())." >&2
|
|
165
|
+
echo "" >&2
|
|
166
|
+
echo "Container: https://drive.google.com/file/d/$parent_id" >&2
|
|
167
|
+
echo "" >&2
|
|
168
|
+
echo "Use 'hug fork --force' to fork anyway." >&2
|
|
169
|
+
return 1
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
local clasp
|
|
173
|
+
clasp=$(find_clasp)
|
|
174
|
+
|
|
175
|
+
local title
|
|
176
|
+
title="$(basename "$(pwd)")"
|
|
177
|
+
|
|
178
|
+
echo "Creating new Apps Script project '$title'..."
|
|
179
|
+
rm -f .clasp.json
|
|
180
|
+
run_clasp "$clasp" create --type standalone --title "$title"
|
|
181
|
+
|
|
182
|
+
echo "Pushing code to new project..."
|
|
183
|
+
run_clasp "$clasp" push
|
|
184
|
+
|
|
185
|
+
echo ""
|
|
186
|
+
echo "Fork complete. This directory now points to a new Apps Script project."
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# ─── deploy ──────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
cmd_deploy() {
|
|
192
|
+
if [ $# -gt 0 ] && [ "$1" = "--help" ]; then
|
|
193
|
+
echo "Usage: hug deploy [description]"
|
|
194
|
+
echo " hug deploy --rollback <version>"
|
|
195
|
+
echo ""
|
|
196
|
+
echo "Pushes code, creates a version, and updates the deployment."
|
|
197
|
+
echo "If no deployment exists, creates one."
|
|
198
|
+
return 0
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
local clasp
|
|
202
|
+
clasp=$(find_clasp)
|
|
203
|
+
|
|
204
|
+
# Rollback mode
|
|
205
|
+
if [ "${1:-}" = "--rollback" ]; then
|
|
206
|
+
if [ $# -ne 2 ]; then
|
|
207
|
+
echo "Usage: hug deploy --rollback <version>" >&2
|
|
208
|
+
return 1
|
|
209
|
+
fi
|
|
210
|
+
local version="$2"
|
|
211
|
+
local line
|
|
212
|
+
line=$(select_deployment "$clasp")
|
|
213
|
+
if [ -z "$line" ]; then
|
|
214
|
+
echo "Error: no non-HEAD deployment found to roll back" >&2
|
|
215
|
+
return 1
|
|
216
|
+
fi
|
|
217
|
+
local id desc
|
|
218
|
+
id=$(deployment_id "$line")
|
|
219
|
+
desc=$(deployment_desc "$line")
|
|
220
|
+
echo "Rolling back to version $version (deployment $id)..."
|
|
221
|
+
update_deployment "$clasp" "$id" "$version" "$desc"
|
|
222
|
+
echo "Done."
|
|
223
|
+
return 0
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
local description="${*:-}"
|
|
227
|
+
|
|
228
|
+
echo "Pushing..."
|
|
229
|
+
run_clasp "$clasp" push
|
|
230
|
+
|
|
231
|
+
echo "Creating version..."
|
|
232
|
+
local version_output
|
|
233
|
+
version_output=$(run_clasp "$clasp" create-version "$description")
|
|
234
|
+
echo "$version_output"
|
|
235
|
+
local version
|
|
236
|
+
version=$(echo "$version_output" | grep -oE 'version [0-9]+' | grep -oE '[0-9]+')
|
|
237
|
+
|
|
238
|
+
if [ -z "$version" ]; then
|
|
239
|
+
echo "Error: could not parse version number from: $version_output" >&2
|
|
240
|
+
return 1
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
local line
|
|
244
|
+
line=$(select_deployment "$clasp") || true
|
|
245
|
+
|
|
246
|
+
if [ -z "$line" ]; then
|
|
247
|
+
echo "No existing deployment found. Creating one..."
|
|
248
|
+
run_clasp "$clasp" create-deployment -V "$version" -d "$description"
|
|
249
|
+
else
|
|
250
|
+
local id desc
|
|
251
|
+
id=$(deployment_id "$line")
|
|
252
|
+
desc=$(deployment_desc "$line")
|
|
253
|
+
echo "Updating deployment $id to version $version..."
|
|
254
|
+
update_deployment "$clasp" "$id" "$version" "$desc"
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
echo "Done."
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# ─── config ──────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
# Read config.js and output associative key-value lines: KEY=VALUE
|
|
263
|
+
_read_config() {
|
|
264
|
+
if [ ! -f config.js ]; then
|
|
265
|
+
return 1
|
|
266
|
+
fi
|
|
267
|
+
grep -E "^ [A-Za-z_][A-Za-z0-9_]*:" config.js | sed "s/^ \([A-Za-z_][A-Za-z0-9_]*\): *['\"]\\(.*\\)['\"],\$/\\1=\\2/"
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Write config.js from KEY=VALUE lines on stdin
|
|
271
|
+
_write_config() {
|
|
272
|
+
local entries=""
|
|
273
|
+
while IFS='=' read -r key value; do
|
|
274
|
+
entries="${entries} ${key}: '${value}',
|
|
275
|
+
"
|
|
276
|
+
done
|
|
277
|
+
|
|
278
|
+
if [ -z "$entries" ]; then
|
|
279
|
+
rm -f config.js
|
|
280
|
+
return
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
cat > config.js <<EOF
|
|
284
|
+
const CONFIG = {
|
|
285
|
+
${entries}};
|
|
286
|
+
EOF
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
cmd_config() {
|
|
290
|
+
if [ "${1:-}" = "--help" ]; then
|
|
291
|
+
echo "Usage: hug config"
|
|
292
|
+
echo " hug config set KEY=VALUE ..."
|
|
293
|
+
echo " hug config unset KEY ..."
|
|
294
|
+
echo ""
|
|
295
|
+
echo "Manages config.js, a JS constants file pushed with your code."
|
|
296
|
+
echo "Apps Script code can read values via CONFIG.KEY."
|
|
297
|
+
echo ""
|
|
298
|
+
echo "Note: config values are stored in source. Don't put secrets here."
|
|
299
|
+
return 0
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
local subcmd="${1:-}"
|
|
303
|
+
|
|
304
|
+
case "$subcmd" in
|
|
305
|
+
"")
|
|
306
|
+
# List config
|
|
307
|
+
if [ ! -f config.js ]; then
|
|
308
|
+
echo "No config.js found. Use 'hug config set KEY=VALUE' to create one."
|
|
309
|
+
return 0
|
|
310
|
+
fi
|
|
311
|
+
_read_config | while IFS='=' read -r key value; do
|
|
312
|
+
echo "$key=$value"
|
|
313
|
+
done
|
|
314
|
+
;;
|
|
315
|
+
|
|
316
|
+
set)
|
|
317
|
+
shift
|
|
318
|
+
if [ $# -eq 0 ]; then
|
|
319
|
+
echo "Usage: hug config set KEY=VALUE ..." >&2
|
|
320
|
+
return 1
|
|
321
|
+
fi
|
|
322
|
+
|
|
323
|
+
# Load existing config
|
|
324
|
+
local existing=""
|
|
325
|
+
if [ -f config.js ]; then
|
|
326
|
+
existing=$(_read_config)
|
|
327
|
+
fi
|
|
328
|
+
|
|
329
|
+
# Parse new values and merge
|
|
330
|
+
local new_pairs=""
|
|
331
|
+
for arg in "$@"; do
|
|
332
|
+
if ! echo "$arg" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*='; then
|
|
333
|
+
echo "Error: invalid format '$arg'. Use KEY=VALUE." >&2
|
|
334
|
+
return 1
|
|
335
|
+
fi
|
|
336
|
+
local key="${arg%%=*}"
|
|
337
|
+
local value="${arg#*=}"
|
|
338
|
+
# Remove existing entry for this key if present
|
|
339
|
+
existing=$(echo "$existing" | grep -v "^${key}=" || true)
|
|
340
|
+
new_pairs="${new_pairs}${key}=${value}
|
|
341
|
+
"
|
|
342
|
+
done
|
|
343
|
+
|
|
344
|
+
# Combine and write
|
|
345
|
+
{ printf '%s\n%s' "$existing" "$new_pairs" | grep -v '^$' || true; } | _write_config
|
|
346
|
+
echo "Updated config.js."
|
|
347
|
+
;;
|
|
348
|
+
|
|
349
|
+
unset)
|
|
350
|
+
shift
|
|
351
|
+
if [ $# -eq 0 ]; then
|
|
352
|
+
echo "Usage: hug config unset KEY ..." >&2
|
|
353
|
+
return 1
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
if [ ! -f config.js ]; then
|
|
357
|
+
echo "No config.js found." >&2
|
|
358
|
+
return 1
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
local remaining
|
|
362
|
+
remaining=$(_read_config)
|
|
363
|
+
for key in "$@"; do
|
|
364
|
+
remaining=$(echo "$remaining" | grep -v "^${key}=" || true)
|
|
365
|
+
done
|
|
366
|
+
|
|
367
|
+
{ echo "$remaining" | grep -v '^$' || true; } | _write_config
|
|
368
|
+
|
|
369
|
+
if [ ! -f config.js ]; then
|
|
370
|
+
echo "Removed config.js (no keys remaining)."
|
|
371
|
+
else
|
|
372
|
+
echo "Updated config.js."
|
|
373
|
+
fi
|
|
374
|
+
;;
|
|
375
|
+
|
|
376
|
+
*)
|
|
377
|
+
echo "Error: unknown config subcommand '$subcmd'" >&2
|
|
378
|
+
echo "Run 'hug config --help' for usage." >&2
|
|
379
|
+
return 1
|
|
380
|
+
;;
|
|
381
|
+
esac
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# ─── passthrough commands ────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
cmd_push() {
|
|
387
|
+
local clasp; clasp=$(find_clasp)
|
|
388
|
+
run_clasp "$clasp" push "$@"
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
cmd_pull() {
|
|
392
|
+
local force=false
|
|
393
|
+
while [ $# -gt 0 ]; do
|
|
394
|
+
case "$1" in
|
|
395
|
+
-f|--force) force=true; shift ;;
|
|
396
|
+
--help)
|
|
397
|
+
echo "Usage: hug pull [-f|--force]"
|
|
398
|
+
echo ""
|
|
399
|
+
echo "Pulls remote files from Apps Script, overwriting local files."
|
|
400
|
+
echo "Refuses to run if there are uncommitted changes (use -f to override)."
|
|
401
|
+
return 0 ;;
|
|
402
|
+
*) break ;;
|
|
403
|
+
esac
|
|
404
|
+
done
|
|
405
|
+
|
|
406
|
+
if [ "$force" = false ] && ! git diff --quiet 2>/dev/null; then
|
|
407
|
+
echo "Error: you have uncommitted changes. Commit or stash them first," >&2
|
|
408
|
+
echo "or use 'hug pull --force' to overwrite." >&2
|
|
409
|
+
return 1
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
local clasp; clasp=$(find_clasp)
|
|
413
|
+
run_clasp "$clasp" pull "$@"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
cmd_open() {
|
|
417
|
+
local clasp; clasp=$(find_clasp)
|
|
418
|
+
run_clasp "$clasp" open "$@"
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
cmd_versions() {
|
|
422
|
+
local clasp; clasp=$(find_clasp)
|
|
423
|
+
run_clasp "$clasp" list-versions "$@"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
cmd_deployments() {
|
|
427
|
+
local clasp; clasp=$(find_clasp)
|
|
428
|
+
run_clasp "$clasp" list-deployments "$@"
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
# ─── dispatch ────────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
if [ $# -eq 0 ]; then
|
|
434
|
+
usage
|
|
435
|
+
exit 1
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
command="$1"
|
|
439
|
+
shift
|
|
440
|
+
|
|
441
|
+
case "$command" in
|
|
442
|
+
init) cmd_init "$@" ;;
|
|
443
|
+
fork) cmd_fork "$@" ;;
|
|
444
|
+
config) cmd_config "$@" ;;
|
|
445
|
+
deploy) cmd_deploy "$@" ;;
|
|
446
|
+
push) cmd_push "$@" ;;
|
|
447
|
+
pull) cmd_pull "$@" ;;
|
|
448
|
+
open) cmd_open "$@" ;;
|
|
449
|
+
versions) cmd_versions "$@" ;;
|
|
450
|
+
deployments) cmd_deployments "$@" ;;
|
|
451
|
+
--version|-v) echo "hug $VERSION" ;;
|
|
452
|
+
--help|-h) usage ;;
|
|
453
|
+
*)
|
|
454
|
+
echo "Error: unknown command '$command'" >&2
|
|
455
|
+
echo "Run 'hug --help' for usage." >&2
|
|
456
|
+
exit 1
|
|
457
|
+
;;
|
|
458
|
+
esac
|
package/lib/common.sh
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# common.sh — shared functions for hug CLI
|
|
2
|
+
|
|
3
|
+
# Ensure package.json exists and clasp is installed locally
|
|
4
|
+
ensure_clasp() {
|
|
5
|
+
if [ ! -f package.json ]; then
|
|
6
|
+
echo "Initializing npm project..."
|
|
7
|
+
npm init -y --quiet >/dev/null
|
|
8
|
+
fi
|
|
9
|
+
if [ ! -d node_modules/.bin ] || [ ! -x node_modules/.bin/clasp ]; then
|
|
10
|
+
echo "Installing clasp..."
|
|
11
|
+
npm install --save-dev @google/clasp --quiet >/dev/null
|
|
12
|
+
fi
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Resolve the path to clasp, preferring local install
|
|
16
|
+
find_clasp() {
|
|
17
|
+
if [ -x "./node_modules/.bin/clasp" ]; then
|
|
18
|
+
echo "./node_modules/.bin/clasp"
|
|
19
|
+
elif command -v clasp &>/dev/null; then
|
|
20
|
+
echo "clasp"
|
|
21
|
+
else
|
|
22
|
+
echo "Error: clasp not found. Run 'npm install @google/clasp' first." >&2
|
|
23
|
+
return 1
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Run clasp and check for auth errors on failure
|
|
28
|
+
run_clasp() {
|
|
29
|
+
local clasp="$1"
|
|
30
|
+
shift
|
|
31
|
+
|
|
32
|
+
local output
|
|
33
|
+
output=$("$clasp" "$@" 2>&1) && { echo "$output"; return 0; }
|
|
34
|
+
local exit_code=$?
|
|
35
|
+
|
|
36
|
+
echo "$output" >&2
|
|
37
|
+
|
|
38
|
+
if echo "$output" | grep -qiE "authorize|unauthorized|unauthenticated|login|credential|ENOENT.*clasprc|401"; then
|
|
39
|
+
echo "" >&2
|
|
40
|
+
echo "Hint: you may need to log in. Run 'npx clasp login' to authenticate." >&2
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
return $exit_code
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Resolve the hug package root (where templates/ lives)
|
|
47
|
+
hug_root() {
|
|
48
|
+
local source="${BASH_SOURCE[0]}"
|
|
49
|
+
while [ -L "$source" ]; do
|
|
50
|
+
local dir
|
|
51
|
+
dir="$(cd -P "$(dirname "$source")" && pwd)"
|
|
52
|
+
source="$(readlink "$source")"
|
|
53
|
+
[[ "$source" != /* ]] && source="$dir/$source"
|
|
54
|
+
done
|
|
55
|
+
cd -P "$(dirname "$source")/.." && pwd
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Get all non-HEAD deployment lines, select one interactively if multiple
|
|
59
|
+
select_deployment() {
|
|
60
|
+
local clasp="$1"
|
|
61
|
+
|
|
62
|
+
local lines
|
|
63
|
+
lines=$(run_clasp "$clasp" list-deployments | grep '^-' | grep -v '@HEAD')
|
|
64
|
+
|
|
65
|
+
if [ -z "$lines" ]; then
|
|
66
|
+
echo ""
|
|
67
|
+
return
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
local count
|
|
71
|
+
count=$(echo "$lines" | wc -l | tr -d ' ')
|
|
72
|
+
|
|
73
|
+
if [ "$count" -eq 1 ]; then
|
|
74
|
+
echo "$lines"
|
|
75
|
+
else
|
|
76
|
+
echo "Multiple deployments found. Choose one:" >&2
|
|
77
|
+
echo "$lines" | nl -w1 -s') ' >&2
|
|
78
|
+
printf "Choice: " >&2
|
|
79
|
+
read -r choice
|
|
80
|
+
local selected
|
|
81
|
+
selected=$(echo "$lines" | sed -n "${choice}p")
|
|
82
|
+
if [ -z "$selected" ]; then
|
|
83
|
+
echo "Error: invalid choice" >&2
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
echo "$selected"
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Extract deployment ID from a deployment line
|
|
91
|
+
deployment_id() {
|
|
92
|
+
echo "$1" | awk '{print $2}'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Extract description from a deployment line (may be empty)
|
|
96
|
+
deployment_desc() {
|
|
97
|
+
echo "$1" | sed -n 's/^- [^ ]* @[^ ]* - \(.*\)/\1/p'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Update a deployment, preserving its description if it has one
|
|
101
|
+
update_deployment() {
|
|
102
|
+
local clasp="$1" id="$2" version="$3" desc="$4"
|
|
103
|
+
if [ -n "$desc" ]; then
|
|
104
|
+
run_clasp "$clasp" update-deployment "$id" -V "$version" -d "$desc"
|
|
105
|
+
else
|
|
106
|
+
run_clasp "$clasp" update-deployment "$id" -V "$version"
|
|
107
|
+
fi
|
|
108
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@peterseibel/hug",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight wrapper around clasp for managing Google Apps Script projects",
|
|
5
|
+
"bin": {
|
|
6
|
+
"hug": "bin/hug"
|
|
7
|
+
},
|
|
8
|
+
"keywords": ["google-apps-script", "clasp", "gas"],
|
|
9
|
+
"license": "ISC",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@google/clasp": "^3.2.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Plan: Transform this repo into `hug` — a general-purpose clasp wrapper
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
This repo currently contains a specific Apps Script app (BHS LC lottery form). We're turning it into `hug`, a lightweight CLI tool that wraps `clasp` to provide opinionated project management for Google Apps Script projects. The app-specific code will be removed; the deployment workflow in the `hug` script becomes the foundation.
|
|
6
|
+
|
|
7
|
+
## Phase 1: Clean up — remove app-specific files
|
|
8
|
+
|
|
9
|
+
**Delete:**
|
|
10
|
+
- `Code.js` — app business logic
|
|
11
|
+
- `index.html` — app UI
|
|
12
|
+
- `appsscript.json` — app-specific manifest
|
|
13
|
+
- `dev.clasp.json`, `prod.clasp.json` — app-specific project configs
|
|
14
|
+
- `.clasp.json` — app-specific (will be recreated per-project by hug)
|
|
15
|
+
- `plans/multi-project-deployment.md` — app-specific plan (concepts will be incorporated into hug's design)
|
|
16
|
+
|
|
17
|
+
**Keep:**
|
|
18
|
+
- `hug` script (will be rewritten)
|
|
19
|
+
- `package.json` (will be rewritten)
|
|
20
|
+
- `CLAUDE.md`, `README.md` (will be rewritten)
|
|
21
|
+
- `.gitignore`
|
|
22
|
+
- `.claude/` directory
|
|
23
|
+
|
|
24
|
+
## Phase 2: Project structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
hug/
|
|
28
|
+
├── bin/
|
|
29
|
+
│ └── hug # CLI entry point (bash)
|
|
30
|
+
├── templates/
|
|
31
|
+
│ ├── blank/
|
|
32
|
+
│ │ ├── appsscript.json
|
|
33
|
+
│ │ └── Code.js # empty/minimal
|
|
34
|
+
│ └── webapp/
|
|
35
|
+
│ ├── appsscript.json # with webapp config
|
|
36
|
+
│ ├── Code.js # doGet() stub
|
|
37
|
+
│ └── index.html # minimal HTML page
|
|
38
|
+
├── lib/
|
|
39
|
+
│ └── common.sh # shared bash functions (clasp path resolution, error handling, deployment helpers)
|
|
40
|
+
├── package.json # bin field points to bin/hug, clasp as dependency
|
|
41
|
+
├── .gitignore
|
|
42
|
+
├── CLAUDE.md
|
|
43
|
+
└── README.md
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Phase 3: CLI commands
|
|
47
|
+
|
|
48
|
+
The `hug` script becomes a subcommand-based CLI. All commands assume clasp is available (bundled as a dependency).
|
|
49
|
+
|
|
50
|
+
### `hug init [--template blank|webapp] [name]`
|
|
51
|
+
- Creates a new Apps Script project directory (or uses current dir)
|
|
52
|
+
- Copies template files into it
|
|
53
|
+
- Runs `npm init -y` and `npm install @google/clasp`
|
|
54
|
+
- Runs `clasp create --type standalone --title <name>` (or `--type webapp` for webapp template)
|
|
55
|
+
- Result: a ready-to-push project
|
|
56
|
+
|
|
57
|
+
### `hug import`
|
|
58
|
+
- For adopting an existing Apps Script project
|
|
59
|
+
- Runs `clasp clone <scriptId>` into current directory
|
|
60
|
+
- Sets up the standard project structure (package.json with clasp dep, .gitignore)
|
|
61
|
+
|
|
62
|
+
### `hug clone <scriptId> [directory]`
|
|
63
|
+
- Creates a new Apps Script project, then pushes existing code to it
|
|
64
|
+
- Basically: `clasp create` + copy files + `clasp push`
|
|
65
|
+
- Useful for forking an existing project into a new script
|
|
66
|
+
|
|
67
|
+
### `hug deploy [description]`
|
|
68
|
+
- The current push→version→update-deployment workflow (what `hug` does now)
|
|
69
|
+
- If no deployment exists yet, creates one
|
|
70
|
+
- Preserves existing deployment descriptions
|
|
71
|
+
|
|
72
|
+
### `hug deploy --rollback <version>`
|
|
73
|
+
- Roll back a deployment to a previous version (existing functionality)
|
|
74
|
+
|
|
75
|
+
### `hug push`
|
|
76
|
+
- Thin wrapper: just runs `clasp push`
|
|
77
|
+
- Convenience so you don't need to remember the clasp path
|
|
78
|
+
|
|
79
|
+
### `hug pull`
|
|
80
|
+
- Thin wrapper: just runs `clasp pull`
|
|
81
|
+
|
|
82
|
+
### `hug open`
|
|
83
|
+
- Thin wrapper: runs `clasp open`
|
|
84
|
+
|
|
85
|
+
### `hug versions`
|
|
86
|
+
- Lists versions (`clasp list-versions`)
|
|
87
|
+
|
|
88
|
+
### `hug deployments`
|
|
89
|
+
- Lists deployments (`clasp list-deployments`)
|
|
90
|
+
|
|
91
|
+
## Phase 4: Rewrite `bin/hug`
|
|
92
|
+
|
|
93
|
+
- Move current `hug` script to `bin/hug`
|
|
94
|
+
- Refactor into subcommand dispatch (case statement)
|
|
95
|
+
- Extract shared logic (clasp path resolution, deployment selection) into `lib/common.sh`
|
|
96
|
+
- Add `--help` for each subcommand
|
|
97
|
+
- The existing deploy logic stays largely intact, just reorganized
|
|
98
|
+
|
|
99
|
+
## Phase 5: Templates
|
|
100
|
+
|
|
101
|
+
### `templates/blank/`
|
|
102
|
+
- `appsscript.json`: minimal manifest (V8 runtime, America/Los_Angeles timezone)
|
|
103
|
+
- `Code.js`: empty file with a comment
|
|
104
|
+
|
|
105
|
+
### `templates/webapp/`
|
|
106
|
+
- `appsscript.json`: manifest with `webapp` section (executeAs USER_DEPLOYING, access ANYONE or DOMAIN)
|
|
107
|
+
- `Code.js`: `doGet()` that serves `index.html`
|
|
108
|
+
- `index.html`: minimal HTML5 boilerplate
|
|
109
|
+
|
|
110
|
+
## Phase 6: Update package.json
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"name": "hug-clasp",
|
|
115
|
+
"version": "0.1.0",
|
|
116
|
+
"description": "A lightweight wrapper around clasp for managing Google Apps Script projects",
|
|
117
|
+
"bin": { "hug": "bin/hug" },
|
|
118
|
+
"dependencies": { "@google/clasp": "^3.2.0" }
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Phase 7: Rewrite docs
|
|
123
|
+
|
|
124
|
+
- `README.md`: document all commands with examples, installation instructions
|
|
125
|
+
- `CLAUDE.md`: update to reflect new project structure and purpose
|
|
126
|
+
|
|
127
|
+
## Verification
|
|
128
|
+
|
|
129
|
+
1. Run `./bin/hug --help` — should show available commands
|
|
130
|
+
2. Run `./bin/hug init --template webapp test-project` in a temp dir — should scaffold a project
|
|
131
|
+
3. Run `./bin/hug deploy "test"` in an existing clasp project — should do push→version→deploy
|
|
132
|
+
4. Run `./bin/hug versions` and `./bin/hug deployments` — should proxy to clasp
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Plan: Add `hug config` for managing project configuration
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
After forking a project with `hug fork`, you need to configure it to point at
|
|
6
|
+
different resources (e.g. a different spreadsheet). Using `clasp run` with
|
|
7
|
+
script properties would be the "right" approach but requires heavy GCP setup
|
|
8
|
+
(link project, enable API, create OAuth client, re-login with creds).
|
|
9
|
+
|
|
10
|
+
Instead, use a simple local `config.js` file that gets pushed with the code.
|
|
11
|
+
`hug config` manages this file, and Apps Script code reads from the `CONFIG`
|
|
12
|
+
object directly. Each git branch can have different config values, which pairs
|
|
13
|
+
well with `hug fork`.
|
|
14
|
+
|
|
15
|
+
Config values end up in source/git, which is fine for things like spreadsheet
|
|
16
|
+
IDs but means sensitive values shouldn't go here.
|
|
17
|
+
|
|
18
|
+
## Files to modify
|
|
19
|
+
|
|
20
|
+
- `bin/hug` — add `cmd_config` and dispatch entry
|
|
21
|
+
- `README.md` — document the config command
|
|
22
|
+
- `CLAUDE.md` — update command list and design decisions
|
|
23
|
+
|
|
24
|
+
Templates are not modified — `config.js` is created on demand by `hug config set`.
|
|
25
|
+
|
|
26
|
+
## Changes
|
|
27
|
+
|
|
28
|
+
### 1. Add `cmd_config` to `bin/hug`
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
hug config # list all config values
|
|
32
|
+
hug config set KEY=VALUE ... # set one or more values
|
|
33
|
+
hug config unset KEY ... # remove one or more values
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Implementation:
|
|
37
|
+
|
|
38
|
+
**`hug config`** (no args): read `config.js`, parse and display key-value pairs.
|
|
39
|
+
If no `config.js` exists, say "No config.js found."
|
|
40
|
+
|
|
41
|
+
**`hug config set K=V ...`**: parse KEY=VALUE args. If `config.js` exists, read
|
|
42
|
+
it and merge new values. If not, create it. Write the file as:
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
const CONFIG = {
|
|
46
|
+
SPREADSHEET_ID: '1BxiM...',
|
|
47
|
+
SHEET_NAME: 'Data',
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**`hug config unset K ...`**: read `config.js`, remove specified keys, rewrite.
|
|
52
|
+
If no keys remain, delete the file.
|
|
53
|
+
|
|
54
|
+
The parser for reading `config.js` can be simple — grep for lines matching
|
|
55
|
+
`KEY: 'VALUE'` or `KEY: "VALUE"` patterns. No need for a full JS parser since
|
|
56
|
+
hug controls the file format.
|
|
57
|
+
|
|
58
|
+
### 2. Add dispatch entry
|
|
59
|
+
|
|
60
|
+
Add `config` to the case statement and usage block:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Configuration:
|
|
64
|
+
config List config values
|
|
65
|
+
config set KEY=VALUE ... Set config values (writes config.js)
|
|
66
|
+
config unset KEY ... Remove config values
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Update docs
|
|
70
|
+
|
|
71
|
+
- README: add config section with example workflow (init, config set, push)
|
|
72
|
+
- CLAUDE.md: add config to command list and design decisions
|
|
73
|
+
|
|
74
|
+
## Verification
|
|
75
|
+
|
|
76
|
+
- `hug config --help` shows usage
|
|
77
|
+
- `hug config` with no config.js prints "No config.js found."
|
|
78
|
+
- `hug config set FOO=bar BAZ=qux` creates config.js with both values
|
|
79
|
+
- `hug config` lists FOO and BAZ
|
|
80
|
+
- `hug config set FOO=updated` changes FOO, keeps BAZ
|
|
81
|
+
- `hug config unset FOO` removes FOO, keeps BAZ
|
|
82
|
+
- `hug config unset BAZ` removes last key and deletes config.js
|
|
83
|
+
- config.js is valid JavaScript that can be pushed to Apps Script
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Multi-Project Deployment Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Run the same codebase against different spreadsheets — e.g. a dev/staging instance and a production instance — without hardcoding any spreadsheet IDs in the code.
|
|
6
|
+
|
|
7
|
+
## Background: Container-Bound vs. Standalone Scripts
|
|
8
|
+
|
|
9
|
+
The current script is **container-bound**: it was created via Extensions > Apps Script from within a spreadsheet, so `SpreadsheetApp.getActiveSpreadsheet()` works automatically. This binding is stored in the spreadsheet, not the script, and there's no official way to convert a container-bound project to standalone.
|
|
10
|
+
|
|
11
|
+
To share code across multiple independent spreadsheets, the script needs to be **standalone**, using `SpreadsheetApp.openById(id)` instead of `getActiveSpreadsheet()`.
|
|
12
|
+
|
|
13
|
+
## Approach: Script Properties for Per-Project Config
|
|
14
|
+
|
|
15
|
+
Script Properties (`PropertiesService.getScriptProperties()`) are stored per script project, not per deployment. This means each standalone script project gets its own independent property store — perfect for storing the spreadsheet ID.
|
|
16
|
+
|
|
17
|
+
### Code changes
|
|
18
|
+
|
|
19
|
+
Replace:
|
|
20
|
+
```javascript
|
|
21
|
+
SpreadsheetApp.getActiveSpreadsheet()
|
|
22
|
+
```
|
|
23
|
+
With:
|
|
24
|
+
```javascript
|
|
25
|
+
const id = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
|
|
26
|
+
SpreadsheetApp.openById(id);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Expose a `configure` function to set properties via `clasp run`:
|
|
30
|
+
```javascript
|
|
31
|
+
const configure = (config) => {
|
|
32
|
+
const props = PropertiesService.getScriptProperties();
|
|
33
|
+
Object.entries(config).forEach(([key, value]) => props.setProperty(key, value));
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Workflow for Setting Up a New Project Instance
|
|
38
|
+
|
|
39
|
+
1. **Create a new standalone script project:**
|
|
40
|
+
```bash
|
|
41
|
+
clasp create --type standalone --title "Lottery Form - Dev"
|
|
42
|
+
```
|
|
43
|
+
This updates `.clasp.json` with the new `scriptId`.
|
|
44
|
+
|
|
45
|
+
2. **Push the code:**
|
|
46
|
+
```bash
|
|
47
|
+
clasp push
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
3. **Configure the project** by running the `configure` function with a JSON config file:
|
|
51
|
+
```bash
|
|
52
|
+
clasp run configure --params "$(< config.dev.json)"
|
|
53
|
+
```
|
|
54
|
+
Where `config.dev.json` looks like:
|
|
55
|
+
```json
|
|
56
|
+
[{"SPREADSHEET_ID": "1BxiM..."}]
|
|
57
|
+
```
|
|
58
|
+
Note: `--params` always takes a JSON array; each element is one argument to the function.
|
|
59
|
+
|
|
60
|
+
4. **Create a deployment:**
|
|
61
|
+
```bash
|
|
62
|
+
clasp create-deployment -d "dev"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
To switch a project to a different spreadsheet later, just re-run the `clasp run configure` step — no code changes or new version needed.
|
|
66
|
+
|
|
67
|
+
## Managing Multiple Projects Locally
|
|
68
|
+
|
|
69
|
+
Since `.clasp.json` only holds one `scriptId` at a time, keep separate config files for each project and swap them as needed:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
.clasp.json.prod
|
|
73
|
+
.clasp.json.dev
|
|
74
|
+
config.prod.json
|
|
75
|
+
config.dev.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or use separate git branches, one per project instance.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<base target="_top">
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<style>
|
|
8
|
+
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 2rem auto; padding: 0 1rem; }
|
|
9
|
+
</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<h1>Hello, world!</h1>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|