szpont 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 +64 -0
- package/package.json +27 -0
- package/src/cli.js +8 -0
- package/src/launcher.js +339 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# szpont
|
|
2
|
+
|
|
3
|
+
Install-and-run for **[Diplomat](https://github.com/latekvo/Diplomat)** (Szpont
|
|
4
|
+
Yon) - the menu-bar / system-tray applet that reviews your pull requests, fixes
|
|
5
|
+
your own, resolves conflicts, and keeps one device per agent.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx szpont # or: npm i -g szpont && szpont
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That is the whole install. macOS and Linux.
|
|
12
|
+
|
|
13
|
+
## What it actually does
|
|
14
|
+
|
|
15
|
+
Diplomat is built out of the checkout it runs from - a SwiftUI bundle on macOS, a
|
|
16
|
+
PySide6 package plus the `diplomat-core` Swift binary on Linux - and it keeps
|
|
17
|
+
*being* that checkout: the Update button, the daily 06:00 timer and the commit the
|
|
18
|
+
panel reports are all git operations on it. A package holding a copy would be a
|
|
19
|
+
second Diplomat that no update could reach.
|
|
20
|
+
|
|
21
|
+
So this package is the steps in between, and every one of them runs a script that
|
|
22
|
+
is already in the repository:
|
|
23
|
+
|
|
24
|
+
| | macOS | Linux |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| get it | `git clone` into `~/.diplomat/checkout`, or fast-forward it | same |
|
|
27
|
+
| build it | `install/build-app.sh` → `Diplomat.app` | `install/build-core.sh` when the prompt binary is missing |
|
|
28
|
+
| set it up | - | a venv at `~/.diplomat/venv` with `requirements.txt` in it |
|
|
29
|
+
| start it | `open Diplomat.app` | the package's own `./diplomat`, on the venv's interpreter |
|
|
30
|
+
|
|
31
|
+
Nothing is rebuilt that does not need to be: a venv is left alone until
|
|
32
|
+
`requirements.txt` changes, and the Swift prompt binary is only built when the
|
|
33
|
+
applet would not find one. `git` is needed to fetch Diplomat, and a Swift
|
|
34
|
+
toolchain to build it - if one is missing you are told which and where to get it,
|
|
35
|
+
before anything is downloaded.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
szpont --plan # print what would be run, as JSON, and do none of it
|
|
39
|
+
szpont --no-update # launch the checkout as it stands
|
|
40
|
+
szpont -- --dump # everything after -- goes to the applet
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`DIPLOMAT_SELF_REPO` points at a checkout of your own - the same variable the
|
|
44
|
+
running applet reads, and one this launcher never updates, because a working copy
|
|
45
|
+
may have work in it. `DIPLOMAT_REPO_URL` clones from a fork.
|
|
46
|
+
|
|
47
|
+
## The same name on both indexes
|
|
48
|
+
|
|
49
|
+
`pip install szpont` installs the same launcher (plus the typed
|
|
50
|
+
[SzpontNet](https://github.com/latekvo/Diplomat/tree/main/packages/szpont)
|
|
51
|
+
bindings, which is what that name means on PyPI). The two implementations are
|
|
52
|
+
separate files in separate languages, so what keeps them one command is
|
|
53
|
+
`test/parity-with-python.mjs`: every machine shape in `test/scenarios.mjs` goes
|
|
54
|
+
through both planners, and the two answers have to be identical.
|
|
55
|
+
|
|
56
|
+
## Its own tests
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm test # plan, cli, a real clone-build-launch, and the parity check
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The end-to-end test clones from a git repository it makes on disk and runs the
|
|
63
|
+
whole sequence into stub build and launch scripts, so it needs neither the network
|
|
64
|
+
nor a Swift toolchain.
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "szpont",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Install-and-run for Diplomat (Szpont Yon): fetches the checkout, builds it, and starts the applet.",
|
|
6
|
+
"keywords": ["diplomat", "szpont", "szpontnet", "launcher", "menu-bar", "tray"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Ignacy Łątka",
|
|
9
|
+
"homepage": "https://github.com/latekvo/Diplomat",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/latekvo/Diplomat.git",
|
|
13
|
+
"directory": "packages/szpont-npm"
|
|
14
|
+
},
|
|
15
|
+
"bugs": "https://github.com/latekvo/Diplomat/issues",
|
|
16
|
+
"bin": {
|
|
17
|
+
"szpont": "src/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": ["src", "README.md"],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"os": ["darwin", "linux"],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node test/run-all.mjs"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `szpont` / `npx szpont` entry point. Everything it does is in launcher.js,
|
|
3
|
+
// which the tests import directly — this file exists so that the bin has no logic
|
|
4
|
+
// of its own to test.
|
|
5
|
+
|
|
6
|
+
import { main } from './launcher.js';
|
|
7
|
+
|
|
8
|
+
process.exitCode = main(process.argv.slice(2));
|
package/src/launcher.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// The npm twin of `packages/szpont/szpont_launcher.py`, line for line where it
|
|
2
|
+
// matters: `probe()` reads the machine, `plan()` turns those facts into the steps
|
|
3
|
+
// that will run, and `run()` runs them. Splitting it that way is what lets
|
|
4
|
+
// `test/parity-with-python.mjs` hand both implementations one set of synthetic
|
|
5
|
+
// facts and demand the same plan back — the two are published under the same name
|
|
6
|
+
// to the two indexes, and a `szpont` that meant different things depending on
|
|
7
|
+
// which one you installed from would be the whole failure mode.
|
|
8
|
+
//
|
|
9
|
+
// Why a bootstrapper at all, rather than a package holding Diplomat: see the
|
|
10
|
+
// Python module's header. Short version — Diplomat is built out of the checkout it
|
|
11
|
+
// runs from, and updates itself as one.
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
export const VERSION = '0.2.0';
|
|
20
|
+
export const DEFAULT_REPO_URL = 'https://github.com/latekvo/Diplomat.git';
|
|
21
|
+
export const SUPPORTED = ['darwin', 'linux'];
|
|
22
|
+
export const MIN_PYTHON = [3, 10];
|
|
23
|
+
|
|
24
|
+
// The venv and the managed checkout live under the state directory Diplomat
|
|
25
|
+
// already owns, so uninstalling is one `rm -rf ~/.diplomat` rather than a hunt.
|
|
26
|
+
const STATE_DIR = '.diplomat';
|
|
27
|
+
|
|
28
|
+
function which(name, env) {
|
|
29
|
+
const dirs = (env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
30
|
+
return dirs.some((dir) => {
|
|
31
|
+
try {
|
|
32
|
+
fs.accessSync(path.join(dir, name), fs.constants.X_OK);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// `python3 --version`, or null when there is no python3 to ask. Only ever called
|
|
41
|
+
// on Linux: a stock macOS has a /usr/bin/python3 that is a Command Line Tools
|
|
42
|
+
// stub, and *running* it pops the graphical installer at whoever typed `szpont`.
|
|
43
|
+
function python3Version(env) {
|
|
44
|
+
if (!which('python3', env)) return null;
|
|
45
|
+
const r = spawnSync('python3', ['--version'], { encoding: 'utf8', timeout: 30_000 });
|
|
46
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
47
|
+
const parts = r.stdout.trim().split(/\s+/);
|
|
48
|
+
return parts.length ? parts[parts.length - 1] : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function digestOf(file) {
|
|
52
|
+
try {
|
|
53
|
+
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A directory, not merely something with that name — the Python twin asks
|
|
60
|
+
// `is_dir()` here, and the two have to answer alike about the same disk.
|
|
61
|
+
function isDir(p) {
|
|
62
|
+
try {
|
|
63
|
+
return fs.statSync(p).isDirectory();
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readText(file) {
|
|
70
|
+
try {
|
|
71
|
+
return fs.readFileSync(file, 'utf8').trim();
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Whether the applet would find a `diplomat-core` to shell out to. The three
|
|
78
|
+
// candidates and their order are `promptcore.core_bin`'s; asking a different
|
|
79
|
+
// question here would rebuild a binary the applet can already see, or skip
|
|
80
|
+
// building one it cannot.
|
|
81
|
+
function coreBinPresent(home, env) {
|
|
82
|
+
if (env.DIPLOMAT_CORE_BIN && fs.existsSync(env.DIPLOMAT_CORE_BIN)) return true;
|
|
83
|
+
if (which('diplomat-core', env)) return true;
|
|
84
|
+
const data = env.XDG_DATA_HOME || path.join(home, '.local', 'share');
|
|
85
|
+
return fs.existsSync(path.join(data, 'diplomat', 'diplomat-core'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function probe(appArgs = [], { update = true, env = process.env } = {}) {
|
|
89
|
+
const home = env.HOME || os.homedir();
|
|
90
|
+
const platform = process.platform;
|
|
91
|
+
// DIPLOMAT_SELF_REPO is what the applet itself calls the checkout it lives in
|
|
92
|
+
// (selfupdate.repo_root), so pointing the launcher at a working copy and
|
|
93
|
+
// pointing the running applet at one are the same act.
|
|
94
|
+
const explicit = env.DIPLOMAT_SELF_REPO;
|
|
95
|
+
const checkout = explicit || path.join(home, STATE_DIR, 'checkout');
|
|
96
|
+
|
|
97
|
+
let state;
|
|
98
|
+
if (!fs.existsSync(checkout)) state = 'absent';
|
|
99
|
+
else if (isDir(path.join(checkout, 'packages', 'diplomat-platform'))) state = 'checkout';
|
|
100
|
+
else state = 'foreign';
|
|
101
|
+
|
|
102
|
+
const venv = path.join(home, STATE_DIR, 'venv');
|
|
103
|
+
const digest = digestOf(path.join(checkout, 'packages', 'diplomat-platform', 'linux', 'requirements.txt'));
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
platform,
|
|
107
|
+
path: env.PATH || '',
|
|
108
|
+
checkout,
|
|
109
|
+
checkout_state: state,
|
|
110
|
+
// Only a checkout this launcher created is one it may move: a working copy
|
|
111
|
+
// someone named themselves is theirs, and a launcher that fast-forwarded it
|
|
112
|
+
// would be editing a tree with work in it.
|
|
113
|
+
managed: !explicit,
|
|
114
|
+
repo_url: env.DIPLOMAT_REPO_URL || DEFAULT_REPO_URL,
|
|
115
|
+
update: Boolean(update),
|
|
116
|
+
git: which('git', env),
|
|
117
|
+
swift: which('swift', env),
|
|
118
|
+
// Linux-only questions, and asked only there — see python3Version.
|
|
119
|
+
python3: platform === 'linux' ? python3Version(env) : null,
|
|
120
|
+
core_bin: platform === 'linux' ? coreBinPresent(home, env) : null,
|
|
121
|
+
venv,
|
|
122
|
+
venv_python: fs.existsSync(path.join(venv, 'bin', 'python')),
|
|
123
|
+
venv_current: digest !== null && readText(path.join(venv, '.szpont-requirements')) === digest,
|
|
124
|
+
args: [...appArgs],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function step(id, cmd, { cwd = null, env = {}, needs = null, optional = false } = {}) {
|
|
129
|
+
return { id, cmd, cwd, env, needs, optional };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// An optional step whose tool is missing is skipped, never a blocker: a checkout
|
|
133
|
+
// that cannot be fast-forwarded is still a checkout that runs.
|
|
134
|
+
function dropped(s, facts) {
|
|
135
|
+
return Boolean(s.optional && s.needs && !facts[s.needs]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const MISSING = {
|
|
139
|
+
git: 'git is needed to fetch Diplomat',
|
|
140
|
+
swift: 'a Swift toolchain is needed to build Diplomat',
|
|
141
|
+
python3: 'python3 is needed to run the Linux applet',
|
|
142
|
+
};
|
|
143
|
+
const FIX = {
|
|
144
|
+
git: { darwin: 'xcode-select --install', linux: 'install git from your package manager' },
|
|
145
|
+
swift: {
|
|
146
|
+
darwin: 'install Xcode, or the Command Line Tools (xcode-select --install)',
|
|
147
|
+
linux: 'https://swift.org/install - swiftly is the easy path',
|
|
148
|
+
},
|
|
149
|
+
python3: { darwin: 'install python3', linux: 'install python3 from your package manager' },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function tooOld(version) {
|
|
153
|
+
const parts = String(version || '').split('.').slice(0, 2).map((p) => Number.parseInt(p, 10));
|
|
154
|
+
if (parts.length !== 2 || parts.some(Number.isNaN)) return false; // unreadable is not old
|
|
155
|
+
return parts[0] < MIN_PYTHON[0] || (parts[0] === MIN_PYTHON[0] && parts[1] < MIN_PYTHON[1]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// What no step could fix: the wrong OS, or the wrong directory.
|
|
159
|
+
function unrunnable(facts) {
|
|
160
|
+
if (!SUPPORTED.includes(facts.platform)) {
|
|
161
|
+
return {
|
|
162
|
+
tool: null,
|
|
163
|
+
reason: `Diplomat runs on macOS and Linux, not ${facts.platform}`,
|
|
164
|
+
fix: 'run it on one of those',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (facts.checkout_state === 'foreign') {
|
|
168
|
+
return {
|
|
169
|
+
tool: null,
|
|
170
|
+
reason: `${facts.checkout} exists but is not a Diplomat checkout`,
|
|
171
|
+
fix: 'remove it, or point DIPLOMAT_SELF_REPO at a real one',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The first tool this plan needs and this machine has not got, or null. Reported
|
|
178
|
+
// rather than thrown, so `--plan` on a machine missing a toolchain still prints
|
|
179
|
+
// the whole plan and names what it is short of.
|
|
180
|
+
function blockedBy(facts, steps) {
|
|
181
|
+
for (const s of steps) {
|
|
182
|
+
if (!s.needs || dropped(s, facts) || facts[s.needs]) continue;
|
|
183
|
+
return { tool: s.needs, reason: MISSING[s.needs], fix: FIX[s.needs][facts.platform] };
|
|
184
|
+
}
|
|
185
|
+
// Only the interpreter a venv is about to be *made* from: an existing venv is
|
|
186
|
+
// whatever it was built with, and this launcher does not get to re-open that.
|
|
187
|
+
if (steps.some((s) => s.id === 'venv') && tooOld(facts.python3)) {
|
|
188
|
+
return {
|
|
189
|
+
tool: 'python3',
|
|
190
|
+
reason: `the applet needs Python ${MIN_PYTHON[0]}.${MIN_PYTHON[1]}+, and python3 is ${facts.python3}`,
|
|
191
|
+
fix: 'install a newer python3',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// What running probe()'s machine would do, as data. Pure: same facts in, same plan
|
|
198
|
+
// out, on either implementation and either OS. That is what makes `--plan` worth
|
|
199
|
+
// printing before anything is cloned or built, and what the parity test compares.
|
|
200
|
+
export function plan(facts) {
|
|
201
|
+
// Nothing is worth planning past these two: a machine that cannot run Diplomat
|
|
202
|
+
// and a directory that is not it are both answers, and a plan that offered to
|
|
203
|
+
// clone anyway would be describing a download that ends in the same message.
|
|
204
|
+
const stop = unrunnable(facts);
|
|
205
|
+
if (stop) return { platform: facts.platform, checkout: facts.checkout, steps: [], blocked: stop };
|
|
206
|
+
|
|
207
|
+
const macos = path.join(facts.checkout, 'packages', 'diplomat-platform', 'macos');
|
|
208
|
+
const linux = path.join(facts.checkout, 'packages', 'diplomat-platform', 'linux');
|
|
209
|
+
const args = [...facts.args];
|
|
210
|
+
const steps = [];
|
|
211
|
+
|
|
212
|
+
if (facts.checkout_state === 'absent') {
|
|
213
|
+
steps.push(step('clone', ['git', 'clone', facts.repo_url, facts.checkout], { needs: 'git' }));
|
|
214
|
+
} else if (facts.managed && facts.update) {
|
|
215
|
+
// Best-effort and fast-forward-only. The applet updates itself daily and on a
|
|
216
|
+
// button, so this is not the update path — it is what keeps a one-shot
|
|
217
|
+
// `npx szpont` from launching last month's commit.
|
|
218
|
+
steps.push(step('update', ['git', '-C', facts.checkout, 'pull', '--ff-only', '--quiet'], {
|
|
219
|
+
needs: 'git',
|
|
220
|
+
optional: true,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (facts.platform === 'darwin') {
|
|
225
|
+
steps.push(step('build', [path.join(macos, 'install', 'build-app.sh')], { cwd: macos, needs: 'swift' }));
|
|
226
|
+
// build-app.sh rebuilds unconditionally, so the bundle always matches the
|
|
227
|
+
// source it was just cloned or pulled from; `open` then detaches it from this
|
|
228
|
+
// terminal, which is where a menu-bar app belongs.
|
|
229
|
+
const launch = ['open', path.join(macos, 'Diplomat.app')];
|
|
230
|
+
if (args.length) launch.push('--args', ...args);
|
|
231
|
+
steps.push(step('launch', launch, { cwd: macos }));
|
|
232
|
+
} else if (facts.platform === 'linux') {
|
|
233
|
+
if (!facts.core_bin) {
|
|
234
|
+
steps.push(step('build-core', [path.join(linux, 'install', 'build-core.sh')], { cwd: linux, needs: 'swift' }));
|
|
235
|
+
}
|
|
236
|
+
// A system python3 is wanted for exactly one thing, creating the venv;
|
|
237
|
+
// everything after it runs on the venv's own interpreter.
|
|
238
|
+
if (!facts.venv_python) {
|
|
239
|
+
steps.push(step('venv', ['python3', '-m', 'venv', facts.venv], { needs: 'python3' }));
|
|
240
|
+
}
|
|
241
|
+
if (!facts.venv_current) {
|
|
242
|
+
steps.push(step('deps', [
|
|
243
|
+
path.join(facts.venv, 'bin', 'python'), '-m', 'pip', 'install', '--upgrade', '--quiet',
|
|
244
|
+
'-r', path.join(linux, 'requirements.txt'),
|
|
245
|
+
]));
|
|
246
|
+
}
|
|
247
|
+
// The checkout's own launcher, with the venv's interpreter in front of it — so
|
|
248
|
+
// PySide6 resolves without the user's python having heard of Qt, and the two
|
|
249
|
+
// spellings of "start the applet" stay one script.
|
|
250
|
+
steps.push(step('launch', [path.join(linux, 'diplomat'), ...args], {
|
|
251
|
+
cwd: linux,
|
|
252
|
+
env: { PATH: path.join(facts.venv, 'bin') + path.delimiter + facts.path },
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
platform: facts.platform,
|
|
258
|
+
checkout: facts.checkout,
|
|
259
|
+
steps: steps.filter((s) => !dropped(s, facts)),
|
|
260
|
+
blocked: blockedBy(facts, steps),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Run a plan's steps in order, announcing each. Node cannot exec over itself the
|
|
265
|
+
// way the Python twin does, so the applet is a child here and this process stays
|
|
266
|
+
// up as its parent — inheriting stdio, and returning its exit code as ours.
|
|
267
|
+
export function run(steps, { checkout, venv }) {
|
|
268
|
+
for (const s of steps) {
|
|
269
|
+
process.stderr.write(`→ ${s.cmd.join(' ')}\n`);
|
|
270
|
+
const r = spawnSync(s.cmd[0], s.cmd.slice(1), {
|
|
271
|
+
cwd: s.cwd || undefined,
|
|
272
|
+
env: { ...process.env, ...s.env },
|
|
273
|
+
stdio: 'inherit',
|
|
274
|
+
});
|
|
275
|
+
if (r.error || r.status !== 0) {
|
|
276
|
+
const why = r.error ? r.error.message : `exit ${r.status}`;
|
|
277
|
+
if (s.optional) {
|
|
278
|
+
process.stderr.write(` (skipped: ${why})\n`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
process.stderr.write(`szpont: ${s.id} failed (${why})\n`);
|
|
282
|
+
return r.status === null || r.status === undefined ? 1 : r.status;
|
|
283
|
+
}
|
|
284
|
+
// The venv now matches the requirements it was just given, and recording that
|
|
285
|
+
// is what makes every later launch skip the install. Hashed here rather than
|
|
286
|
+
// at probe time: on a first run the file arrives with the clone, two steps
|
|
287
|
+
// after the only chance probe had to read it.
|
|
288
|
+
if (s.id === 'deps') {
|
|
289
|
+
const digest = digestOf(path.join(checkout, 'packages', 'diplomat-platform', 'linux', 'requirements.txt'));
|
|
290
|
+
try {
|
|
291
|
+
fs.writeFileSync(path.join(venv, '.szpont-requirements'), digest || '');
|
|
292
|
+
} catch {
|
|
293
|
+
// a re-install next time is the whole cost of not recording it
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const USAGE = `usage: szpont [--plan] [--no-update] [--version] [-- APP_ARGS...]
|
|
301
|
+
|
|
302
|
+
Fetch Diplomat if it isn't here, build it, and start it.
|
|
303
|
+
|
|
304
|
+
--plan print what would be run, as JSON, and do none of it
|
|
305
|
+
--no-update launch the checkout as it stands, without fast-forwarding it
|
|
306
|
+
--version print the launcher version
|
|
307
|
+
|
|
308
|
+
Everything after -- is passed to the applet. DIPLOMAT_SELF_REPO points at your own
|
|
309
|
+
checkout (never updated); DIPLOMAT_REPO_URL clones from somewhere other than GitHub.
|
|
310
|
+
`;
|
|
311
|
+
|
|
312
|
+
export function main(argv) {
|
|
313
|
+
const flags = { plan: false, update: true };
|
|
314
|
+
const appArgs = [];
|
|
315
|
+
let passthrough = false;
|
|
316
|
+
for (const arg of argv) {
|
|
317
|
+
if (passthrough) appArgs.push(arg);
|
|
318
|
+
else if (arg === '--') passthrough = true;
|
|
319
|
+
else if (arg === '--plan') flags.plan = true;
|
|
320
|
+
else if (arg === '--no-update') flags.update = false;
|
|
321
|
+
else if (arg === '--version') { process.stdout.write(`szpont ${VERSION}\n`); return 0; }
|
|
322
|
+
else if (arg === '--help' || arg === '-h') { process.stdout.write(USAGE); return 0; }
|
|
323
|
+
else if (arg.startsWith('-')) { process.stderr.write(`szpont: unknown option ${arg}\n${USAGE}`); return 2; }
|
|
324
|
+
else appArgs.push(arg);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const facts = probe(appArgs, { update: flags.update });
|
|
328
|
+
const steps = plan(facts);
|
|
329
|
+
|
|
330
|
+
if (flags.plan) {
|
|
331
|
+
process.stdout.write(`${JSON.stringify({ version: VERSION, facts, plan: steps }, null, 2)}\n`);
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
if (steps.blocked) {
|
|
335
|
+
process.stderr.write(`szpont: ${steps.blocked.reason}\n → ${steps.blocked.fix}\n`);
|
|
336
|
+
return 2;
|
|
337
|
+
}
|
|
338
|
+
return run(steps.steps, { checkout: facts.checkout, venv: facts.venv });
|
|
339
|
+
}
|