agents-city 0.3.0-beta.21 → 0.3.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-plugin/marketplace.json +1 -1
- package/README.es.md +310 -70
- package/README.md +297 -69
- package/bin/agents-city.js +3 -0
- package/bin/doctor +3 -0
- package/bin/hall.html +164 -24
- package/bin/navegador.mjs +415 -0
- package/bin/serve.py +383 -127
- package/bin/shortcut +3 -0
- package/bin/test +5 -2
- package/bin/test-actualiza.py +130 -0
- package/bin/test-atajos.py +301 -0
- package/bin/test-busca.py +216 -0
- package/bin/test-cage.py +170 -2
- package/bin/test-card.py +2 -2
- package/bin/test-cities.py +45 -0
- package/bin/test-contracts.py +12 -5
- package/bin/test-doctor.py +33 -0
- package/bin/test-navegador.py +164 -0
- package/bin/test-seat.py +245 -25
- package/bin/test-serve.py +214 -9
- package/bin/test-workspace.py +63 -0
- package/bin/testlib.py +23 -0
- package/bin/update +3 -0
- package/city/web/dist/city.js +47 -47
- package/city/web/dist/index.html +1 -1
- package/city/web/dist-hall/hall.js +2193 -174
- package/city/web/src/bienvenida.ts +686 -0
- package/city/web/src/es.ts +180 -0
- package/city/web/src/hall.ts +520 -168
- package/city/web/src/idioma.ts +86 -0
- package/city/web/src/main.ts +27 -0
- package/city/web/src/motores.ts +54 -0
- package/docs/agents-first.md +8 -1
- package/docs/security.md +46 -12
- package/docs/testing.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/channel/bus.js +1 -1
- package/plugin/channel/bus.ts +1 -1
- package/plugin/channel/runtime/codex.ts +1 -1
- package/plugin/channel/runtime-gateway.js +1 -1
- package/plugin/scripts/actualiza.py +198 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/busca.py +436 -0
- package/plugin/scripts/cage.py +266 -26
- package/plugin/scripts/capabilities.py +17 -10
- package/plugin/scripts/card.py +10 -0
- package/plugin/scripts/cities.py +34 -0
- package/plugin/scripts/city-session.sh +33 -7
- package/plugin/scripts/doctor.py +122 -0
- package/plugin/scripts/find-repos.sh +12 -105
- package/plugin/scripts/read-card.py +6 -2
- package/plugin/scripts/report.py +5 -6
- package/plugin/scripts/reset.py +50 -14
- package/plugin/scripts/seat.py +445 -103
- package/plugin/scripts/workspace.py +197 -0
package/plugin/scripts/doctor.py
CHANGED
|
@@ -19,6 +19,9 @@ import argparse
|
|
|
19
19
|
import json
|
|
20
20
|
import os
|
|
21
21
|
import sys
|
|
22
|
+
|
|
23
|
+
GUIONES = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
sys.path.insert(0, GUIONES)
|
|
22
25
|
from dataclasses import dataclass
|
|
23
26
|
from typing import Callable
|
|
24
27
|
|
|
@@ -145,7 +148,126 @@ def cura_fichero(ruta, marca, dry_run=False):
|
|
|
145
148
|
return reporte
|
|
146
149
|
|
|
147
150
|
|
|
151
|
+
# ── the machine, not just the config ─────────────────────────────────────────
|
|
152
|
+
#
|
|
153
|
+
# `doctor` used to answer one narrow question — is this config file an old
|
|
154
|
+
# shape? — behind no command at all. What a person means by "doctor" is: tell
|
|
155
|
+
# me whether this thing can work here, and if not, which part. So it answers
|
|
156
|
+
# that too, and every line is derived: what is installed, what the kernel will
|
|
157
|
+
# grant, which city is selected. Nothing here writes.
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _version_de(programa, bandera='--version'):
|
|
161
|
+
import shutil as _sh
|
|
162
|
+
import subprocess as _sp
|
|
163
|
+
|
|
164
|
+
ruta = _sh.which(programa)
|
|
165
|
+
if not ruta:
|
|
166
|
+
return ''
|
|
167
|
+
try:
|
|
168
|
+
r = _sp.run([ruta, bandera], capture_output=True, text=True, timeout=10)
|
|
169
|
+
except (OSError, _sp.SubprocessError):
|
|
170
|
+
return 'installed'
|
|
171
|
+
primera = (r.stdout or r.stderr).strip().splitlines()
|
|
172
|
+
return primera[0].strip() if primera else 'installed'
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def revisa_entorno():
|
|
176
|
+
"""Every check as `(area, ok, detail)`. `ok` is None for "not applicable".
|
|
177
|
+
|
|
178
|
+
A list rather than printed lines, so the Hall could show the same answers
|
|
179
|
+
the terminal does without either of them re-deciding what healthy means.
|
|
180
|
+
"""
|
|
181
|
+
import shutil as _sh
|
|
182
|
+
import sys as _sys
|
|
183
|
+
|
|
184
|
+
import cage as _cage
|
|
185
|
+
import cities as _cities
|
|
186
|
+
|
|
187
|
+
fuera = []
|
|
188
|
+
fuera.append(('python', True, _sys.version.split()[0]))
|
|
189
|
+
# Every one of these spells its version flag differently, and tmux answers
|
|
190
|
+
# `--version` with an error that reads like a working version string.
|
|
191
|
+
for programa, bandera, obligatorio in (('tmux', '-V', True), ('bash', '--version', True),
|
|
192
|
+
('git', '--version', True),
|
|
193
|
+
('node', '--version', True),
|
|
194
|
+
('gh', '--version', False)):
|
|
195
|
+
v = _version_de(programa, bandera)
|
|
196
|
+
fuera.append((programa, bool(v) if obligatorio else (True if v else None),
|
|
197
|
+
v or ('missing' if obligatorio else 'not installed (optional)')))
|
|
198
|
+
|
|
199
|
+
motores = [m for m in ('claude', 'codex', 'opencode', 'kimi') if _sh.which(m)]
|
|
200
|
+
fuera.append(('runtimes', bool(motores),
|
|
201
|
+
', '.join(motores) if motores else 'none — a city needs at least one CLI'))
|
|
202
|
+
|
|
203
|
+
if _sys.platform == 'darwin':
|
|
204
|
+
detalle = ('seatbelt' if _sh.which('sandbox-exec') else 'sandbox-exec missing')
|
|
205
|
+
elif _sys.platform.startswith('linux'):
|
|
206
|
+
if not _sh.which('bwrap'):
|
|
207
|
+
detalle = 'bubblewrap not installed — agents run uncaged (apt install bubblewrap)'
|
|
208
|
+
elif not _cage.bwrap_sirve():
|
|
209
|
+
detalle = ('bubblewrap present but the kernel refuses unprivileged '
|
|
210
|
+
'namespaces — agents run uncaged')
|
|
211
|
+
else:
|
|
212
|
+
detalle = 'bubblewrap'
|
|
213
|
+
else:
|
|
214
|
+
detalle = f'no cage on {_sys.platform}'
|
|
215
|
+
fuera.append(('cage', _cage.disponible(), detalle))
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
usuario = _cities.usuario_actual()
|
|
219
|
+
datos = _cities.actual(usuario, crear=False)
|
|
220
|
+
if datos:
|
|
221
|
+
fuera.append(('city', True, _cities.direccion(usuario, datos)))
|
|
222
|
+
ficha = os.path.join(datos, f'{usuario}.md')
|
|
223
|
+
fuera.append(('card', os.path.isfile(ficha),
|
|
224
|
+
ficha if os.path.isfile(ficha) else f'no card at {ficha}'))
|
|
225
|
+
else:
|
|
226
|
+
fuera.append(('city', None, 'none selected yet — agents-city seat creates one'))
|
|
227
|
+
except (OSError, ValueError) as e:
|
|
228
|
+
fuera.append(('city', False, str(e)))
|
|
229
|
+
|
|
230
|
+
hall = os.path.join(os.path.dirname(os.path.dirname(GUIONES)),
|
|
231
|
+
'city', 'web', 'dist-hall', 'hall.js')
|
|
232
|
+
fuera.append(('hall bundle', os.path.isfile(hall),
|
|
233
|
+
'built' if os.path.isfile(hall) else 'not built — ./bin/hall builds it'))
|
|
234
|
+
return fuera
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def informe_entorno():
|
|
238
|
+
"""Print the environment report. Returns 0 when nothing is broken."""
|
|
239
|
+
import actualiza
|
|
240
|
+
|
|
241
|
+
print('\n Agents City doctor\n')
|
|
242
|
+
roto = 0
|
|
243
|
+
for area, bien, detalle in revisa_entorno():
|
|
244
|
+
marca = '·' if bien is None else ('ok' if bien else 'XX')
|
|
245
|
+
if bien is False:
|
|
246
|
+
roto += 1
|
|
247
|
+
print(f' {marca:>2} {area:<12} {detalle}')
|
|
248
|
+
instalada, ultima, hay = actualiza.comprueba()
|
|
249
|
+
if ultima:
|
|
250
|
+
print(f' {"!!" if hay else "ok":>2} {"version":<12} {instalada}'
|
|
251
|
+
+ (f' — {ultima} is out: agents-city update' if hay else ' (current)'))
|
|
252
|
+
else:
|
|
253
|
+
print(f' {"·":>2} {"version":<12} {instalada} (registry not checked)')
|
|
254
|
+
print()
|
|
255
|
+
if roto:
|
|
256
|
+
print(f' {roto} thing(s) need attention.\n')
|
|
257
|
+
return 1 if roto else 0
|
|
258
|
+
|
|
259
|
+
|
|
148
260
|
def main():
|
|
261
|
+
# `doctor` with no file is the environment report — what a person means by
|
|
262
|
+
# the word. The config migration keeps its own path, unchanged.
|
|
263
|
+
if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help', 'help'):
|
|
264
|
+
if len(sys.argv) > 1:
|
|
265
|
+
print(' usage: agents-city doctor [config.json [--fix]]\n\n'
|
|
266
|
+
' With no arguments: check this machine — tools, runtimes, the\n'
|
|
267
|
+
' cage, the selected city, and whether a newer version is out.\n'
|
|
268
|
+
' With a config file: detect, explain and migrate an old shape.')
|
|
269
|
+
return 0
|
|
270
|
+
return informe_entorno()
|
|
149
271
|
p = argparse.ArgumentParser(description='Detect, explain and migrate an old config shape.')
|
|
150
272
|
p.add_argument('fichero', help='the config JSON file to check')
|
|
151
273
|
p.add_argument('--fix', action='store_true', help='rewrite (default is dry-run report)')
|
|
@@ -1,111 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# cache it. One line each: <name-on-the-remote><TAB><local-path>
|
|
2
|
+
# The git half of the disk index, for the shell callers.
|
|
4
3
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# The scanner itself is `busca.py`: one implementation, in the language the rest
|
|
5
|
+
# of the tooling already needs, so it also runs where there is no bash. This is
|
|
6
|
+
# the shim that keeps `city-session.sh` and anything else in a pipeline talking
|
|
7
|
+
# to it the way they always did — one line each, <name><TAB><local-path>.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# AGENTS_CITY_ORG only index repos of this organisation. Unset = all of
|
|
14
|
-
# them; the filter is for machines that also hold work
|
|
15
|
-
# from somewhere else.
|
|
16
|
-
# CITY_SEARCH_IN colon-separated roots to search instead of the defaults
|
|
17
|
-
# CITY_SEARCH_DEPTH how deep to go under each root (default 4)
|
|
18
|
-
|
|
9
|
+
# find-repos.sh the cache, rebuilt if it is more than a day old
|
|
10
|
+
# find-repos.sh --refresh rebuild it now
|
|
11
|
+
# find-repos.sh <repo> that repo's path, or nothing
|
|
19
12
|
set -uo pipefail
|
|
20
|
-
|
|
21
|
-
ORG="${AGENTS_CITY_ORG:-}"
|
|
22
|
-
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/agents-city"
|
|
23
|
-
CACHE="$CACHE_DIR/repos.tsv"
|
|
24
|
-
RAICES_DEF="$HOME/codigo:$HOME/code:$HOME/dev:$HOME/src:$HOME/projects:$HOME/proyectos:$HOME/work:$HOME/trabajo:$HOME/repos:$HOME/git:$HOME/Documents:$HOME/Desktop:$HOME/Developer:$HOME"
|
|
25
|
-
RAICES="${CITY_SEARCH_IN:-$RAICES_DEF}"
|
|
26
|
-
PROFUNDIDAD="${CITY_SEARCH_DEPTH:-4}"
|
|
27
|
-
|
|
28
|
-
construir() {
|
|
29
|
-
mkdir -p "$CACHE_DIR"
|
|
30
|
-
local tmp="$CACHE.tmp.$$"
|
|
31
|
-
: > "$tmp"
|
|
32
|
-
local vistos=""
|
|
33
|
-
local IFS=':'
|
|
34
|
-
for raiz in $RAICES; do
|
|
35
|
-
unset IFS
|
|
36
|
-
[ -d "$raiz" ] || continue
|
|
37
|
-
# Do not walk a root another one already covered.
|
|
38
|
-
case ":$vistos:" in *":$raiz:"*) continue ;; esac
|
|
39
|
-
vistos="$vistos:$raiz"
|
|
40
|
-
|
|
41
|
-
# `.git` is a directory in a normal clone and a FILE in a linked worktree —
|
|
42
|
-
# and a worktree is exactly the folder an isolated agent works in, so missing
|
|
43
|
-
# them missed the people using agents the most. Both are indexed; a worktree
|
|
44
|
-
# is named `repo@branch` so the two are distinct things to pick.
|
|
45
|
-
find "$raiz" -maxdepth "$PROFUNDIDAD" \( -type d -o -type f \) -name .git \
|
|
46
|
-
-not -path "*/node_modules/*" \
|
|
47
|
-
-not -path "*/Library/*" \
|
|
48
|
-
-not -path "*/.Trash/*" \
|
|
49
|
-
-not -path "*/vendor/*" \
|
|
50
|
-
-not -path "*/.cache/*" \
|
|
51
|
-
-not -path "*/plugins/cache/*" \
|
|
52
|
-
-not -path "*/.cargo/*" -not -path "*/.asdf/*" -not -path "*/miniconda3/*" \
|
|
53
|
-
-not -path "*/.rbenv/*" -not -path "*/.pyenv/*" -not -path "*/.nvm/*" \
|
|
54
|
-
2>/dev/null \
|
|
55
|
-
| while read -r g; do
|
|
56
|
-
repo="$(dirname "$g")"
|
|
57
|
-
url="$(git -C "$repo" remote get-url origin 2>/dev/null)" || continue
|
|
58
|
-
[ -z "$url" ] && continue
|
|
59
|
-
# git@host:org/name.git | https://host/org/name(.git)
|
|
60
|
-
# The version excluded by an organisation whose default was the literal
|
|
61
|
-
# "<your-org>", so the index came back empty for everybody who never set
|
|
62
|
-
# the variable. Unset now means: index what is there.
|
|
63
|
-
if [ -n "$ORG" ]; then
|
|
64
|
-
case "$url" in
|
|
65
|
-
*[:/]"$ORG"/*) ;;
|
|
66
|
-
*) continue ;;
|
|
67
|
-
esac
|
|
68
|
-
fi
|
|
69
|
-
nombre="$(basename "$url" .git)"
|
|
70
|
-
if [ -f "$g" ]; then
|
|
71
|
-
rama="$(git -C "$repo" branch --show-current 2>/dev/null)"
|
|
72
|
-
[ -z "$rama" ] && rama="$(basename "$repo")"
|
|
73
|
-
nombre="$nombre@$rama"
|
|
74
|
-
fi
|
|
75
|
-
[ -n "$nombre" ] && printf '%s\t%s\n' "$nombre" "$repo"
|
|
76
|
-
done >> "$tmp"
|
|
77
|
-
IFS=':'
|
|
78
|
-
done
|
|
79
|
-
unset IFS
|
|
80
|
-
# First one wins on a duplicate: the roots run from most specific to least.
|
|
81
|
-
sort -u -t$'\t' -k1,1 "$tmp" > "$CACHE"
|
|
82
|
-
rm -f "$tmp"
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
caduca() {
|
|
86
|
-
[ ! -f "$CACHE" ] && return 0
|
|
87
|
-
[ -n "$(find "$CACHE" -mtime +1 2>/dev/null)" ] && return 0 # older than a day
|
|
88
|
-
return 1
|
|
89
|
-
}
|
|
90
|
-
|
|
13
|
+
GUION="$(dirname "$0")/busca.py"
|
|
91
14
|
case "${1:-}" in
|
|
92
|
-
--refresh|--refrescar)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
echo "$(wc -l < "$CACHE" | tr -d ' ') repos indexed in $CACHE" >&2
|
|
96
|
-
;;
|
|
97
|
-
"")
|
|
98
|
-
caduca && construir
|
|
99
|
-
cat "$CACHE"
|
|
100
|
-
;;
|
|
101
|
-
*)
|
|
102
|
-
caduca && construir
|
|
103
|
-
ruta="$(awk -F'\t' -v n="$1" '$1==n {print $2; exit}' "$CACHE")"
|
|
104
|
-
# Not there? It may have been cloned a minute ago: rebuild once and retry.
|
|
105
|
-
if [ -z "$ruta" ]; then
|
|
106
|
-
construir
|
|
107
|
-
ruta="$(awk -F'\t' -v n="$1" '$1==n {print $2; exit}' "$CACHE")"
|
|
108
|
-
fi
|
|
109
|
-
[ -n "$ruta" ] && printf '%s\n' "$ruta"
|
|
110
|
-
;;
|
|
15
|
+
--refresh|--refrescar) exec python3 "$GUION" --refresh --repos ;;
|
|
16
|
+
"") exec python3 "$GUION" --repos ;;
|
|
17
|
+
*) exec python3 "$GUION" "$1" ;;
|
|
111
18
|
esac
|
|
@@ -43,8 +43,12 @@ def main():
|
|
|
43
43
|
except OSError as e:
|
|
44
44
|
sys.exit(f'cannot read {ruta}: {e}')
|
|
45
45
|
valor = card.campo(texto, campo)
|
|
46
|
-
#
|
|
47
|
-
|
|
46
|
+
# A bracketed value is a list everywhere else in this product — `repos:`,
|
|
47
|
+
# `agents:`, `mounts.<agent>:` — and the shell wants one line. Deciding by
|
|
48
|
+
# SHAPE rather than by a list of blessed field names is what stopped this
|
|
49
|
+
# from having to be edited every time the card grew another list.
|
|
50
|
+
esLista = valor.strip().startswith('[') and valor.strip().endswith(']')
|
|
51
|
+
print(','.join(card.lista(valor)) if esLista else valor)
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
if __name__ == '__main__':
|
package/plugin/scripts/report.py
CHANGED
|
@@ -28,6 +28,7 @@ import sys
|
|
|
28
28
|
import urllib.request
|
|
29
29
|
|
|
30
30
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
31
|
+
import busca # the one disk scanner, shared with the seat and the Hall
|
|
31
32
|
import city_env
|
|
32
33
|
import parcels # the one reader of parcels.yml, shared with the seeder
|
|
33
34
|
|
|
@@ -49,14 +50,12 @@ def donde_esta(repo):
|
|
|
49
50
|
|
|
50
51
|
`parcels.yml` deliberately holds no local paths: the same file is shared by a
|
|
51
52
|
whole team and the same repo sits somewhere different on every machine. That is
|
|
52
|
-
what
|
|
53
|
+
what `busca` is for, and it caches its index for a day.
|
|
53
54
|
"""
|
|
54
55
|
if repo not in _DONDE:
|
|
55
|
-
guion = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'find-repos.sh')
|
|
56
56
|
try:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
57
|
+
_DONDE[repo] = busca.ruta_de(repo)
|
|
58
|
+
except OSError:
|
|
60
59
|
_DONDE[repo] = ''
|
|
61
60
|
return _DONDE[repo]
|
|
62
61
|
|
|
@@ -142,7 +141,7 @@ def explica_vacio(di, datos, todas, por_defecto):
|
|
|
142
141
|
di(f'{len(perdidas)} of {len(todas)} parcels are not cloned on this '
|
|
143
142
|
f'machine, so there is no folder to run anything in:')
|
|
144
143
|
di(' ' + ', '.join(sorted(set(perdidas))[:10]))
|
|
145
|
-
di('Clone them, or run ./plugin/scripts/
|
|
144
|
+
di('Clone them, or run ./plugin/scripts/busca.py --refresh if they '
|
|
146
145
|
'are here under another folder name.')
|
|
147
146
|
elif not por_defecto:
|
|
148
147
|
di('The folders are here, but nothing says how growth is counted. Put '
|
package/plugin/scripts/reset.py
CHANGED
|
@@ -151,28 +151,64 @@ def reinicia(datos, usuario="", dry_run=False):
|
|
|
151
151
|
return ctx["backup"]
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
def _resuelve_todas(pedidas, usuario):
|
|
155
|
+
"""Every requested city as a path, or (None, complaint).
|
|
156
|
+
|
|
157
|
+
`all` means every managed city this owner has. Names resolve one by one and
|
|
158
|
+
a single unknown name aborts the WHOLE run: resetting three of four cities
|
|
159
|
+
and then stopping on a typo is the worst possible outcome of a destructive
|
|
160
|
+
command, so nothing starts until every name is known.
|
|
161
|
+
"""
|
|
162
|
+
if len(pedidas) == 1 and pedidas[0].lower() == "all":
|
|
163
|
+
todas = [c["ruta"] for c in cities.lista(usuario)]
|
|
164
|
+
if not todas:
|
|
165
|
+
return None, "there are no cities here to reset"
|
|
166
|
+
return todas, ""
|
|
167
|
+
fuera, desconocidas = [], []
|
|
168
|
+
for nombre in pedidas:
|
|
169
|
+
datos = cities.resuelve(nombre, usuario)
|
|
170
|
+
if datos:
|
|
171
|
+
if datos not in fuera:
|
|
172
|
+
fuera.append(datos)
|
|
173
|
+
else:
|
|
174
|
+
desconocidas.append(nombre)
|
|
175
|
+
if desconocidas:
|
|
176
|
+
conocidas = ", ".join(c["slug"] for c in cities.lista(usuario)) or "none"
|
|
177
|
+
return None, (f"no city called {', '.join(repr(d) for d in desconocidas)}. "
|
|
178
|
+
f"Known here: {conocidas}. Nothing was reset.")
|
|
179
|
+
return fuera, ""
|
|
180
|
+
|
|
181
|
+
|
|
154
182
|
def main():
|
|
155
183
|
ap = argparse.ArgumentParser(
|
|
156
|
-
description="Reset
|
|
184
|
+
description="Reset cities to onboarding; repos stay untouched and data is backed up"
|
|
157
185
|
)
|
|
158
|
-
ap.add_argument("city",
|
|
186
|
+
ap.add_argument("city", nargs="+",
|
|
187
|
+
help="one or more city names or paths, space separated — or `all`")
|
|
159
188
|
ap.add_argument("--dry-run", action="store_true", help="show every effect, change nothing")
|
|
160
189
|
a = ap.parse_args()
|
|
161
190
|
usuario = cities.usuario_actual()
|
|
162
|
-
|
|
163
|
-
if not
|
|
164
|
-
|
|
165
|
-
print(f" No city called {a.city!r}. Known here: {conocidas}.", file=sys.stderr)
|
|
166
|
-
return 1
|
|
167
|
-
try:
|
|
168
|
-
backup = reinicia(datos, usuario, a.dry_run)
|
|
169
|
-
except ValueError as e:
|
|
170
|
-
print(f" {e}", file=sys.stderr)
|
|
191
|
+
objetivo, queja = _resuelve_todas(a.city, usuario)
|
|
192
|
+
if not objetivo:
|
|
193
|
+
print(f" {queja}", file=sys.stderr)
|
|
171
194
|
return 1
|
|
195
|
+
# More than one city at once is a bigger gesture than the command's name
|
|
196
|
+
# suggests, so it says out loud what it is about to do before doing it.
|
|
197
|
+
if len(objetivo) > 1 and not a.dry_run:
|
|
198
|
+
print(f" Resetting {len(objetivo)} cities: "
|
|
199
|
+
f"{', '.join(cities.slug_ciudad(d) for d in objetivo)}")
|
|
200
|
+
for datos in objetivo:
|
|
201
|
+
try:
|
|
202
|
+
backup = reinicia(datos, usuario, a.dry_run)
|
|
203
|
+
except ValueError as e:
|
|
204
|
+
print(f" {cities.slug_ciudad(datos)}: {e}", file=sys.stderr)
|
|
205
|
+
return 1
|
|
206
|
+
if not a.dry_run:
|
|
207
|
+
corto = backup.replace(os.path.expanduser("~"), "~")
|
|
208
|
+
print(f" City `{cities.slug_ciudad(datos)}` reset. Recovery copy: {corto}")
|
|
172
209
|
if not a.dry_run:
|
|
173
|
-
|
|
174
|
-
print(f"
|
|
175
|
-
print(f" Run `agents-city seat --city {cities.slug_ciudad(datos)}` for onboarding.")
|
|
210
|
+
primera = cities.slug_ciudad(objetivo[0])
|
|
211
|
+
print(f" Run `agents-city seat --city {primera}` for onboarding.")
|
|
176
212
|
return 0
|
|
177
213
|
|
|
178
214
|
|