@vibelord/lanes 1.1.1 → 1.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 +36 -5
- package/bin/lanes-install.mjs +166 -13
- package/lib/plan.mjs +113 -3
- package/package.json +1 -1
- package/payload/lanes/README.md +45 -22
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +2459 -164
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +19 -19
- package/payload/lanes/client/ghostty/lanes-palette.conf +19 -19
- package/payload/lanes/client/lanes-launch +289 -40
- package/payload/lanes/client/lanes-window +15 -3
- package/payload/lanes/container/hooks/claude-settings.json +1 -0
- package/payload/lanes/container/lane +106 -92
- package/payload/lanes/container/lane-edit +100 -0
- package/payload/lanes/container/lane-mcp +1091 -0
- package/payload/lanes/container/lane-signal +109 -29
- package/payload/lanes/container/lane_core.py +1140 -0
- package/payload/lanes/container/vimrc +31 -0
- package/payload/lanes/docs/ARCHITECTURE.md +618 -13
- package/payload/lanes/docs/DESIGN.md +122 -44
- package/payload/lanes/docs/INSTALL.md +157 -17
- package/payload/lanes/docs/RUNBOOK.md +603 -72
- package/payload/lanes/docs/SHARING.md +3 -2
- package/payload/lanes/docs/USERGUIDE.md +777 -136
- package/payload/lanes/install/enable-web.sh +2 -0
- package/payload/lanes/install/host-install.sh +163 -7
- package/payload/lanes/install/services.py +148 -18
- package/payload/lanes/install.sh +68 -7
- package/payload/lanes/libexec/agent_state.py +74 -1
- package/payload/lanes/libexec/cursor_sync.py +6 -0
- package/payload/lanes/libexec/device_sync.py +47 -10
- package/payload/lanes/libexec/economy_poll.py +1 -0
- package/payload/lanes/libexec/enrold.py +41 -22
- package/payload/lanes/libexec/keys.py +54 -23
- package/payload/lanes/libexec/lanes_lib.py +620 -22
- package/payload/lanes/libexec/members.py +16 -2
- package/payload/lanes/libexec/notify.py +104 -24
- package/payload/lanes/libexec/registrations.py +216 -18
- package/payload/lanes/libexec/session_actions.py +182 -14
- package/payload/lanes/libexec/sidebar +11 -3
- package/payload/lanes/libexec/sidebar.py +2135 -361
- package/payload/lanes/libexec/statusd.py +137 -10
- package/payload/lanes/libexec/studio_host.py +8 -1
- package/payload/lanes/libexec/tokens.py +121 -30
- package/payload/lanes/libexec/tokens.sh +86 -2
- package/payload/lanes/libexec/update.py +972 -0
- package/payload/lanes/libexec/users.py +614 -0
- package/payload/lanes/libexec/view.sh +34 -6
- package/payload/lanes/systemd/lanes-ensure.service +3 -0
- package/payload/lanes/systemd/lanes-gc.service +1 -1
- package/payload/lanes/systemd/lanes-update.service +17 -0
- package/payload/lanes/systemd/lanes-update.timer +17 -0
- package/payload/lanes/web/README.md +83 -24
- package/payload/lanes/web/browser.js +3 -5
- package/payload/lanes/web/mobile.css +17 -17
- package/payload/lanes/web/mobile.js +44 -32
- package/payload/lanes/workspaces.example/README.md +3 -1
- package/payload/lanes/zellij/config.kdl +106 -56
- package/payload/manifest.json +5 -3
|
@@ -20,8 +20,14 @@ def session_key(entry):
|
|
|
20
20
|
return entry.get('container'), binding.get('socket'), binding.get('session')
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def binding_digest(container, socket, name):
|
|
24
|
+
"""sha256 hex of one tmux binding: the tail of `session_id` (its first 10 characters) and the name of the binding's
|
|
25
|
+
`handback/` file."""
|
|
26
|
+
return hashlib.sha256(json.dumps([container, socket, name]).encode()).hexdigest()
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
def session_id(workspace, container, socket, name):
|
|
24
|
-
digest =
|
|
30
|
+
digest = binding_digest(container, socket, name)[:10]
|
|
25
31
|
return f'{workspace}--x-{L.slug(name)[:40]}-{digest}'
|
|
26
32
|
|
|
27
33
|
|
|
@@ -78,6 +84,48 @@ def make_entry(workspace, container, socket, name, pane, previous=None):
|
|
|
78
84
|
return entry
|
|
79
85
|
|
|
80
86
|
|
|
87
|
+
def handback_path(container, socket, name):
|
|
88
|
+
return L.STATE / 'handback' / (binding_digest(container, socket, name) + '.json')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def hand_back(record, identity):
|
|
92
|
+
"""What a session `registrations.unregister` releases keeps as an UNREGISTERED one (cockpit v3 S7): its name in
|
|
93
|
+
`user-sessions/<id>.json` (make_entry's name authority), the board and id it was registered under in
|
|
94
|
+
`handback/<binding digest>.json` (`handed_back`: statusd's first listing of it), and its views in
|
|
95
|
+
`resolve.dynamic/<id>`. `identity` is the {session_id, created} tmux named for the binding: the hand-back is that
|
|
96
|
+
session's alone. Raises OSError (the caller has already saved the record)."""
|
|
97
|
+
container, socket, name = session_key(record)
|
|
98
|
+
binding = {'container': container, 'holder': 'tmux', 'tmux': {'socket': socket, 'session': name}}
|
|
99
|
+
L.atomic_write(L.STATE / 'user-sessions' / (record['id'] + '.json'),
|
|
100
|
+
json.dumps(dict({'id': record['id'], 'workspace': record['workspace']}, **binding,
|
|
101
|
+
name=record['name'])) + '\n')
|
|
102
|
+
L.atomic_write(handback_path(container, socket, name),
|
|
103
|
+
json.dumps(dict({'id': record['id'], 'workspace': record['workspace']}, **binding,
|
|
104
|
+
identity=dict(identity))) + '\n')
|
|
105
|
+
make_entry(record['workspace'], container, socket, name,
|
|
106
|
+
{'session_id': identity['session_id'], 'session_created': identity['created']}, record)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def handed_back(container, socket, name, pane):
|
|
110
|
+
"""statusd's seed for a session it lists with no listing of its own from the previous cycle -> (board, previous)
|
|
111
|
+
for `make_entry`, or (None, None). A session that was REGISTERED has none, and statusd would mint its id from
|
|
112
|
+
whichever board its directory names now — another board's id, under the raw tmux name, whenever the agent moved
|
|
113
|
+
(fix cycle 1 of S7, DEF-1826). `hand_back` left the board and id it was registered under; they apply only to the
|
|
114
|
+
session tmux named then (same binding, same session id and creation time), so a session re-created under that name
|
|
115
|
+
later is attributed as any other. One read of a small file, for a session's first listing only. Never raises on a
|
|
116
|
+
damaged file (statusd would skip a live session for the cycle): anything unexpected is no hand-back."""
|
|
117
|
+
meta = L.read_json(handback_path(container, socket, name), {})
|
|
118
|
+
if not isinstance(meta, dict) or not isinstance(meta.get('tmux'), dict) or \
|
|
119
|
+
session_key(meta) != (container, socket, name):
|
|
120
|
+
return None, None
|
|
121
|
+
identity = meta.get('identity') if isinstance(meta.get('identity'), dict) else {}
|
|
122
|
+
if (not identity.get('session_id') or identity.get('session_id') != pane.get('session_id')
|
|
123
|
+
or identity.get('created') != pane.get('session_created')
|
|
124
|
+
or not all(isinstance(meta.get(field), str) and L.IDENT.match(meta[field]) for field in ('id', 'workspace'))):
|
|
125
|
+
return None, None
|
|
126
|
+
return meta['workspace'], {'id': meta['id']}
|
|
127
|
+
|
|
128
|
+
|
|
81
129
|
def new_shell(workspace):
|
|
82
130
|
registry = L.load_registry()
|
|
83
131
|
board = next((w for w in registry['workspaces'] if w['id'] == workspace), None)
|
|
@@ -143,12 +191,17 @@ def rename_session(workspace, entry, new_name):
|
|
|
143
191
|
return dict(entry, name=new_name)
|
|
144
192
|
|
|
145
193
|
|
|
146
|
-
def rename_registered(manifest_path, repo_id, registry_name, new_name):
|
|
194
|
+
def rename_registered(manifest_path, repo_id, registry_name, new_name, declared_display=None):
|
|
147
195
|
"""Display override for one declared terminal. The reader is `lanes_lib._workspace_lanes`, which looks at
|
|
148
196
|
`repositories[i].displayNames` keyed by the terminal's declared name — the manifest's top level (and the
|
|
149
197
|
lane's terminals.json `source`) has no such key. `registry_name` is the lane's title (repository prefix +
|
|
150
|
-
declared name)
|
|
151
|
-
|
|
198
|
+
declared name). This is the HOST layer and wins over the container's own `lanes.terminals[<name>].displayName`
|
|
199
|
+
(LNS-33), which the lane carries as `declared_display` and which this never writes. Renaming back to the lane's
|
|
200
|
+
default — `declared_display` when the container declares one, else the title — clears the override; any other
|
|
201
|
+
name (the title included, when the container declares a different name) is kept as one, or the reader would
|
|
202
|
+
fall back to the container's name and the rename would silently do nothing. The lane id never changes
|
|
203
|
+
(lanes_lib.lane_id_for). The first write keeps a `<manifest>.bak-<UTC timestamp>` copy beside it. Callers run
|
|
204
|
+
`lanes sync`."""
|
|
152
205
|
L._title(new_name)
|
|
153
206
|
path = Path(manifest_path)
|
|
154
207
|
original = path.read_text()
|
|
@@ -159,8 +212,8 @@ def rename_registered(manifest_path, repo_id, registry_name, new_name):
|
|
|
159
212
|
prefix = repo.get('prefix') or ''
|
|
160
213
|
declared = registry_name[len(prefix):] if prefix and registry_name.startswith(prefix) else registry_name
|
|
161
214
|
names = repo.setdefault('displayNames', {})
|
|
162
|
-
if new_name == registry_name:
|
|
163
|
-
names.pop(declared, None) # removing the override restores the default
|
|
215
|
+
if new_name == (declared_display or registry_name):
|
|
216
|
+
names.pop(declared, None) # removing the override restores the lane's default name
|
|
164
217
|
else:
|
|
165
218
|
names[declared] = new_name
|
|
166
219
|
if not any(path.parent.glob(path.name + '.bak-*')):
|
|
@@ -169,12 +222,49 @@ def rename_registered(manifest_path, repo_id, registry_name, new_name):
|
|
|
169
222
|
L.atomic_write(path, json.dumps(data, indent=2, ensure_ascii=False) + '\n', mode=0o644)
|
|
170
223
|
|
|
171
224
|
|
|
172
|
-
def
|
|
225
|
+
def declared_in(lane):
|
|
226
|
+
"""Where a DECLARED lane is declared, as a person edits it: a repository lane's `source` (its terminals.json, the
|
|
227
|
+
container's spelling) or `catalog.json` for a lane of an SSH workspace (`sshWorkspaces`); None for anything else —
|
|
228
|
+
an added session (registered-sessions.json is the registrations module's), an unregistered one, a zmx row."""
|
|
229
|
+
if lane.get('registered_session') or lane.get('closable'):
|
|
230
|
+
return None
|
|
231
|
+
return lane.get('source') or ('catalog.json' if lane.get('holder') == 'ssh' else None)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def declared_text(lane, dash='—'):
|
|
235
|
+
"""What `u` (sidebar) and `lanes unregister` say about a declared lane no editor of ours writes — a lane of an SSH
|
|
236
|
+
workspace, declared in `catalog.json` (`declared_in`): `declared in <where> — edit the file`. A repository lane
|
|
237
|
+
(its terminals.json `source`) is taken out by `unregister_lane` instead (cockpit v3 S7 + M4, wired at the fan-in).
|
|
238
|
+
`dash` is the caller's `dash` mark (the sidebar's ASCII footer is seven-bit)."""
|
|
239
|
+
return f'declared in {declared_in(lane)} {dash} edit the file'
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def promoted_id(workspace_id, repo_id, name):
|
|
243
|
+
"""The id `lanes sync` gives the terminal `promote_to_registered` declares as `name`: lanes_lib.lane_id_for — the
|
|
244
|
+
one slug helper sync uses — over the title `_workspace_lanes` builds, the repository's `prefix` + the declared name.
|
|
245
|
+
The prefix is read from the board's manifest (`repo_id` is the source lane's `repo`); a manifest that cannot be
|
|
246
|
+
read predicts the unprefixed id. A PREDICTION: sync suffixes an id another lane of the board already holds
|
|
247
|
+
(`-2`, `build_registry`), so a reader never trusts it without the binding (the sidebar's `settle_pending`)."""
|
|
248
|
+
prefix = ''
|
|
249
|
+
if repo_id:
|
|
250
|
+
try:
|
|
251
|
+
manifest = L.read_site_json(L.manifest_path(workspace_id))
|
|
252
|
+
repo = next((r for r in manifest.get('repositories') or []
|
|
253
|
+
if isinstance(r, dict) and r.get('id') == repo_id), {})
|
|
254
|
+
prefix = repo.get('prefix') or ''
|
|
255
|
+
except (L.LanesError, OSError, ValueError, KeyError, TypeError):
|
|
256
|
+
prefix = ''
|
|
257
|
+
return L.lane_id_for(workspace_id, {'name': prefix + name})
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def promote_to_registered(workspace_id, entry, registry, name=None):
|
|
173
261
|
"""Promote an unregistered tmux session to a registered lane by writing its binding to terminals.json.
|
|
174
262
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
263
|
+
`name` is the terminal's declared name — the sidebar's `P` asks for it (cockpit v3 S8); without it, the session's
|
|
264
|
+
own name. The session must have holder=='tmux' and the workspace must have exactly one repository source
|
|
265
|
+
(unambiguous write target), and neither a published lane nor that file may bind the session already. Returns the id
|
|
266
|
+
`lanes sync` is expected to give the lane (`promoted_id`). After this call, callers should run `lanes sync`. Raises
|
|
267
|
+
LanesError for unsupported configurations or invalid session state."""
|
|
178
268
|
if entry.get('holder') != 'tmux':
|
|
179
269
|
raise L.LanesError('Only tmux sessions can be promoted (zmx and exec holders are not supported)')
|
|
180
270
|
# A binding any board already holds — a repository declaration or an adopted registered session — is
|
|
@@ -189,9 +279,9 @@ def promote_to_registered(workspace_id, entry, registry):
|
|
|
189
279
|
raise L.LanesError('Workspace not found')
|
|
190
280
|
# Find the terminals.json source — must be a single unambiguous file. A source is (container, spelling): the
|
|
191
281
|
# same spelling in two containers (one repository cloned in two boxes) is two files, so it is still ambiguous.
|
|
192
|
-
sources =
|
|
193
|
-
for lane in board.get('lanes', []) if lane.get('source')
|
|
194
|
-
unique_sources = list(
|
|
282
|
+
sources = {(lane.get('container') or board.get('container') or '', lane['source']): lane.get('repo')
|
|
283
|
+
for lane in board.get('lanes', []) if lane.get('source')}
|
|
284
|
+
unique_sources = list(sources)
|
|
195
285
|
if not unique_sources:
|
|
196
286
|
raise L.LanesError(
|
|
197
287
|
'No registry source found for this workspace; add the lane to terminals.json manually')
|
|
@@ -210,11 +300,19 @@ def promote_to_registered(workspace_id, entry, registry):
|
|
|
210
300
|
session_name = tmux.get('session', '')
|
|
211
301
|
if not socket_name or not session_name:
|
|
212
302
|
raise L.LanesError('Session has no tmux binding; cannot promote')
|
|
303
|
+
name = L._title(entry.get('name', session_name) if name is None else name)
|
|
213
304
|
data = json.loads(terminals_path.read_text())
|
|
214
305
|
terminals = data.get('terminals') or []
|
|
215
306
|
lane_block = data.setdefault('lanes', {})
|
|
216
307
|
lane_terminals = lane_block.setdefault('terminals', {})
|
|
217
|
-
name
|
|
308
|
+
# A session this file already binds — under any name — is promoted already: the next sync lists its lane. A
|
|
309
|
+
# second terminal bound to it would give one tmux session two lanes (DEF-1555's shape), and the registry cannot
|
|
310
|
+
# say so between the write and the sync that lists it: a `P` on another device, or on this one after the sidebar
|
|
311
|
+
# let the row go (S8 fix cycle 1, DEF-1762). So the file is asked, as build_registry reads it. Nothing is written.
|
|
312
|
+
for terminal in terminals:
|
|
313
|
+
bound = L.declared_tmux(data, terminal.get('name')) or {}
|
|
314
|
+
if (bound.get('socketName'), bound.get('sessionName')) == (socket_name, session_name):
|
|
315
|
+
raise L.LanesError(f"Already declared as {terminal.get('name')!r}; lanes sync lists it")
|
|
218
316
|
# Guard: do not overwrite an existing registered terminal with the same name
|
|
219
317
|
if any(t.get('name') == name for t in terminals):
|
|
220
318
|
raise L.LanesError(f'A terminal named {name!r} already exists; rename it first')
|
|
@@ -229,6 +327,76 @@ def promote_to_registered(workspace_id, entry, registry):
|
|
|
229
327
|
# Remove the user-sessions metadata — the terminal is now governed by the registry
|
|
230
328
|
meta_path = L.STATE / 'user-sessions' / (entry['id'] + '.json')
|
|
231
329
|
meta_path.unlink(missing_ok=True)
|
|
330
|
+
return promoted_id(workspace_id, sources[unique_sources[0]], name)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class DeclaredRefused(L.LanesError):
|
|
334
|
+
"""unregister_declared / unregister_lane refused: `code` is the machine code lane-mcp's `lanes_unregister`
|
|
335
|
+
answers for the same case (last_terminal, adopted_lane, not_declared_here, source_link, …)."""
|
|
336
|
+
|
|
337
|
+
def __init__(self, message, code):
|
|
338
|
+
super().__init__(message)
|
|
339
|
+
self.code = code
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def unregister_declared(source_path, name, root=None):
|
|
343
|
+
"""Remove the declared terminal `name` from the terminals.json at `source_path` (the HOST path: L.host_path of the
|
|
344
|
+
lane's `source`) — its `terminals[]` item and its `lanes.terminals` entry — with the editor lane-mcp's
|
|
345
|
+
`lanes_unregister` runs in the container (lane_core.terminals_remove through edit_file): one span, re-read before
|
|
346
|
+
the atomic write, the file's mode kept, never the root `throttle` object, never a rename or reorder, never the
|
|
347
|
+
last terminal. `root` is the container's home volume: a symbolic link at or above the file below it refuses
|
|
348
|
+
(`source_link`; from the host it would resolve outside the container, the DEF-1796 class). The session keeps
|
|
349
|
+
running and shows as an unregistered session once `lanes sync` has run (callers run it; lanes-statusd's
|
|
350
|
+
hot-sync also starts one within ~15 s of the edit). -> {'path', 'name', 'changed': bool}. DeclaredRefused
|
|
351
|
+
otherwise. The sidebar's `u` and `lanes unregister` on a declared lane call unregister_lane, which finds both
|
|
352
|
+
arguments (cockpit v3 S7's hook, wired to this editor at the fan-in)."""
|
|
353
|
+
core = L.lane_core()
|
|
354
|
+
|
|
355
|
+
def transform(text):
|
|
356
|
+
if text is None:
|
|
357
|
+
raise core.LaneError(f'{source_path} does not exist', 'source_missing')
|
|
358
|
+
return core.terminals_remove(text, name)
|
|
359
|
+
try:
|
|
360
|
+
before, after, _backup = core.edit_file(Path(source_path), transform, root=root)
|
|
361
|
+
except core.LaneError as error:
|
|
362
|
+
raise DeclaredRefused(str(error), error.code or 'refused') from error
|
|
363
|
+
return {'path': str(source_path), 'name': name, 'changed': before != after}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def unregister_lane(lane_id, registry=None):
|
|
367
|
+
"""unregister_declared for a lane of the published registry: a DECLARED tmux lane (a terminals.json `source`),
|
|
368
|
+
never an adopted session (`registered_session`: registered-sessions.json is its authority) or an unregistered
|
|
369
|
+
one. The file is reached through the container's home volume (the registry's `containers`, else
|
|
370
|
+
containers.json) and the terminal found the way lane-mcp finds it (lane_core.declared_name: its tmux binding in
|
|
371
|
+
`lanes.terminals` or the `throttle` block, else the one name that ends its `registry_name`).
|
|
372
|
+
-> {'lane', 'source', 'path', 'name', 'changed'}."""
|
|
373
|
+
core = L.lane_core()
|
|
374
|
+
registry = L.load_registry() if registry is None else registry
|
|
375
|
+
lane = next((item for w in registry['workspaces'] for item in w['lanes'] if item['id'] == lane_id), None)
|
|
376
|
+
if lane is None:
|
|
377
|
+
raise DeclaredRefused(f'{lane_id} is not a lane of the registry; reload', 'not_in_container')
|
|
378
|
+
if lane.get('registered_session'):
|
|
379
|
+
raise DeclaredRefused(f'{lane_id} was adopted (registered-sessions.json), not declared in a terminals.json',
|
|
380
|
+
'adopted_lane')
|
|
381
|
+
if not lane.get('source'):
|
|
382
|
+
raise DeclaredRefused(f'{lane_id} names no terminals.json', 'not_declared_here')
|
|
383
|
+
info = (registry.get('containers') or {}).get(lane.get('container')) \
|
|
384
|
+
or (L.read_json(L.STATE / 'containers.json', {}) or {}).get(lane.get('container')) or {}
|
|
385
|
+
path = L.host_path(info, lane['source'])
|
|
386
|
+
try:
|
|
387
|
+
_ident, text = core._snapshot(path, info['home'])
|
|
388
|
+
except core.LaneError as error:
|
|
389
|
+
raise DeclaredRefused(str(error), error.code or 'refused') from error
|
|
390
|
+
if text is None:
|
|
391
|
+
raise DeclaredRefused(f'{lane["source"]} does not exist in {lane.get("container")}', 'source_missing')
|
|
392
|
+
try:
|
|
393
|
+
doc = core.strict_json(text, lane['source'])
|
|
394
|
+
except core.LaneError as error:
|
|
395
|
+
raise DeclaredRefused(str(error), error.code or 'refused') from error
|
|
396
|
+
name = core.declared_name(doc, lane)
|
|
397
|
+
if name is None:
|
|
398
|
+
raise DeclaredRefused(f'{lane["source"]} declares no terminal that matches {lane_id}', 'not_declared_here')
|
|
399
|
+
return dict(unregister_declared(path, name, root=info['home']), lane=lane_id, source=lane['source'])
|
|
232
400
|
|
|
233
401
|
|
|
234
402
|
def close_session(workspace, entry):
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
# Never leave the sidebar pane empty: restart the TUI if it ever exits.
|
|
2
|
+
# Never leave the sidebar pane empty: restart the TUI if it ever exits (after a 1 s pause: no restart storm).
|
|
3
|
+
# Exit 75 is the sidebar's self-swap (cockpit v3 U5): an update flipped ~/.local/share/lanes/current to a new tree, and
|
|
4
|
+
# the idle sidebar saved its cursor, scroll, seen marks and filter (views/<device>/<ws>.<slot>.sidebar.state) for its
|
|
5
|
+
# successor. Re-execute this wrapper at once — no pause — from $here, the LOGICAL directory it was started from: `cd`
|
|
6
|
+
# and `pwd -L` keep a `current` symlink in it, so a layout's `…/current/libexec/sidebar` now runs the new tree's
|
|
7
|
+
# wrapper and sidebar.py. (A sidebar started from a `pkg/<version>` path never exits 75: its re-exec would run the
|
|
8
|
+
# same tree, so it shows the footer's version note instead.)
|
|
3
9
|
trap '' INT QUIT
|
|
4
|
-
here=$(cd "$(dirname "$0")" && pwd)
|
|
10
|
+
here=$(cd "$(dirname "$0")" && pwd -L)
|
|
5
11
|
while :; do
|
|
6
|
-
|
|
12
|
+
rc=0
|
|
13
|
+
python3 -S -E "$here/sidebar.py" "$@" || rc=$?
|
|
14
|
+
if [ "$rc" = 75 ]; then exec "$here/sidebar" "$@"; fi
|
|
7
15
|
sleep 1
|
|
8
16
|
done
|