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
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Everything on this machine an agent could be given to work on.
|
|
3
|
+
|
|
4
|
+
There used to be a bash script here that indexed git repositories, and three
|
|
5
|
+
things were wrong with it. It could not run on Windows, where there is no bash
|
|
6
|
+
and Python already is a hard dependency. It shelled out to `git` once per
|
|
7
|
+
repository, so a home directory with three hundred of them paid three hundred
|
|
8
|
+
processes. And it only knew about git, while the product's own claim is that a
|
|
9
|
+
house can be a folder of documents with no git anywhere — so the one kind of
|
|
10
|
+
agent that needs the most help finding its material got none.
|
|
11
|
+
|
|
12
|
+
So: one scanner, in the language the rest of the tooling is already written in,
|
|
13
|
+
that returns three kinds of place.
|
|
14
|
+
|
|
15
|
+
repo a clone. Named by its `origin` remote, not by its folder, so it
|
|
16
|
+
is findable by the name you would say out loud.
|
|
17
|
+
worktree a linked worktree — the folder an isolated agent actually works
|
|
18
|
+
in. Named `repo@branch`, a distinct thing to pick.
|
|
19
|
+
docs a folder with documents in it and no git. Named by its folder.
|
|
20
|
+
|
|
21
|
+
The remote and the branch are read out of `.git/config` and `HEAD` directly.
|
|
22
|
+
That is not an optimisation for its own sake: it is what makes a full-disk scan
|
|
23
|
+
finish while somebody is looking at the screen.
|
|
24
|
+
|
|
25
|
+
CITY_SEARCH_IN colon-separated roots to search instead of the defaults
|
|
26
|
+
(semicolon-separated is also accepted, for Windows)
|
|
27
|
+
CITY_SEARCH_DEPTH how deep under each root (default 4)
|
|
28
|
+
AGENTS_CITY_ORG only index repos of this organisation. Unset = all.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import configparser
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
import time
|
|
36
|
+
|
|
37
|
+
#: Where the work usually lives, most specific first — the ones later in the
|
|
38
|
+
#: list are broad, and a repo found under two roots keeps its first name.
|
|
39
|
+
CANDIDATAS = (
|
|
40
|
+
"codigo",
|
|
41
|
+
"code",
|
|
42
|
+
"dev",
|
|
43
|
+
"src",
|
|
44
|
+
"projects",
|
|
45
|
+
"proyectos",
|
|
46
|
+
"work",
|
|
47
|
+
"trabajo",
|
|
48
|
+
"repos",
|
|
49
|
+
"git",
|
|
50
|
+
"Documents",
|
|
51
|
+
"Documentos",
|
|
52
|
+
"Desktop",
|
|
53
|
+
"Escritorio",
|
|
54
|
+
"Developer",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
#: Never walked. Package caches and toolchains hold thousands of directories and
|
|
58
|
+
#: not one of them is somebody's work.
|
|
59
|
+
SALTAR = {
|
|
60
|
+
"node_modules",
|
|
61
|
+
"vendor",
|
|
62
|
+
".Trash",
|
|
63
|
+
".cache",
|
|
64
|
+
".cargo",
|
|
65
|
+
".asdf",
|
|
66
|
+
".rbenv",
|
|
67
|
+
".pyenv",
|
|
68
|
+
".nvm",
|
|
69
|
+
"miniconda3",
|
|
70
|
+
"anaconda3",
|
|
71
|
+
"Library",
|
|
72
|
+
"AppData",
|
|
73
|
+
"site-packages",
|
|
74
|
+
"venv",
|
|
75
|
+
".venv",
|
|
76
|
+
"__pycache__",
|
|
77
|
+
"dist",
|
|
78
|
+
"build",
|
|
79
|
+
".next",
|
|
80
|
+
".terraform",
|
|
81
|
+
"Applications",
|
|
82
|
+
"third_party",
|
|
83
|
+
"Pods",
|
|
84
|
+
"target",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#: What counts as a document. Deliberately short: the question is "is there
|
|
88
|
+
#: writing in here", not "can this folder be indexed".
|
|
89
|
+
DOCUMENTOS = (".md", ".markdown", ".txt", ".rst", ".org", ".pdf", ".docx", ".doc", ".rtf")
|
|
90
|
+
|
|
91
|
+
#: A folder needs this many documents before it is worth offering. Two loose
|
|
92
|
+
#: readmes are not a knowledge base.
|
|
93
|
+
MINIMO_DOCS = 3
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def raices():
|
|
97
|
+
"""The directories to search, in order, with duplicates and nested ones
|
|
98
|
+
dropped — walking $HOME after ~/codigo would index everything twice."""
|
|
99
|
+
puesto = os.environ.get("CITY_SEARCH_IN", "")
|
|
100
|
+
if puesto:
|
|
101
|
+
# A Windows path holds a colon (C:\...), so accept the separator that
|
|
102
|
+
# platform actually uses as well as the POSIX one.
|
|
103
|
+
crudas = puesto.split(";") if ";" in puesto else puesto.split(os.pathsep)
|
|
104
|
+
else:
|
|
105
|
+
casa = os.path.expanduser("~")
|
|
106
|
+
crudas = [os.path.join(casa, n) for n in CANDIDATAS] + [casa]
|
|
107
|
+
fuera = []
|
|
108
|
+
for r in crudas:
|
|
109
|
+
r = os.path.abspath(os.path.expanduser(r.strip()))
|
|
110
|
+
if not r or not os.path.isdir(r):
|
|
111
|
+
continue
|
|
112
|
+
if any(r == v or r.startswith(v + os.sep) for v in fuera):
|
|
113
|
+
continue
|
|
114
|
+
fuera.append(r)
|
|
115
|
+
return fuera
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def profundidad():
|
|
119
|
+
try:
|
|
120
|
+
return max(1, int(os.environ.get("CITY_SEARCH_DEPTH", "4")))
|
|
121
|
+
except ValueError:
|
|
122
|
+
return 4
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _lee_config(ruta):
|
|
126
|
+
"""The `origin` URL out of a git config file, without running git."""
|
|
127
|
+
cp = configparser.ConfigParser(strict=False)
|
|
128
|
+
try:
|
|
129
|
+
with open(ruta, "r", encoding="utf-8", errors="replace") as f:
|
|
130
|
+
cp.read_file(f)
|
|
131
|
+
except (OSError, configparser.Error):
|
|
132
|
+
return ""
|
|
133
|
+
for seccion in cp.sections():
|
|
134
|
+
# git writes it as: [remote "origin"]
|
|
135
|
+
if seccion.replace('"', "").replace("'", "").strip() == "remote origin":
|
|
136
|
+
return (cp[seccion].get("url") or "").strip()
|
|
137
|
+
return ""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _tocado(ruta):
|
|
141
|
+
"""When this place last moved, as a unix time, without running anything.
|
|
142
|
+
|
|
143
|
+
A repository's HEAD file is rewritten by every commit, checkout and pull, so
|
|
144
|
+
its mtime is the honest answer to "have I been in here lately" — and asking
|
|
145
|
+
the filesystem costs nothing, while `git log` in three hundred repositories
|
|
146
|
+
costs three hundred processes and the person is waiting.
|
|
147
|
+
"""
|
|
148
|
+
for candidato in ("HEAD", "index", ""):
|
|
149
|
+
donde = os.path.join(ruta, candidato) if candidato else ruta
|
|
150
|
+
try:
|
|
151
|
+
# Whole seconds, because that is what the cache file can hold: a
|
|
152
|
+
# scan and a read of its own cache must return the same index, or
|
|
153
|
+
# every caller has two slightly different answers to choose from.
|
|
154
|
+
return float(int(os.path.getmtime(donde)))
|
|
155
|
+
except OSError:
|
|
156
|
+
continue
|
|
157
|
+
return 0.0
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _rama(gitdir):
|
|
161
|
+
try:
|
|
162
|
+
with open(os.path.join(gitdir, "HEAD"), "r", encoding="utf-8", errors="replace") as f:
|
|
163
|
+
linea = f.read().strip()
|
|
164
|
+
except OSError:
|
|
165
|
+
return ""
|
|
166
|
+
return linea.split("refs/heads/", 1)[1].strip() if "refs/heads/" in linea else ""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _gitdir_de_worktree(fichero):
|
|
170
|
+
"""A linked worktree's `.git` is a file that points at the real gitdir."""
|
|
171
|
+
try:
|
|
172
|
+
with open(fichero, "r", encoding="utf-8", errors="replace") as f:
|
|
173
|
+
texto = f.read().strip()
|
|
174
|
+
except OSError:
|
|
175
|
+
return ""
|
|
176
|
+
if not texto.startswith("gitdir:"):
|
|
177
|
+
return ""
|
|
178
|
+
destino = texto.split(":", 1)[1].strip()
|
|
179
|
+
if not os.path.isabs(destino):
|
|
180
|
+
destino = os.path.join(os.path.dirname(fichero), destino)
|
|
181
|
+
return os.path.normpath(destino)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _comun(gitdir):
|
|
185
|
+
"""The main repository's gitdir behind a worktree's private one."""
|
|
186
|
+
ruta = os.path.join(gitdir, "commondir")
|
|
187
|
+
try:
|
|
188
|
+
with open(ruta, "r", encoding="utf-8", errors="replace") as f:
|
|
189
|
+
rel = f.read().strip()
|
|
190
|
+
except OSError:
|
|
191
|
+
return gitdir
|
|
192
|
+
return os.path.normpath(rel if os.path.isabs(rel) else os.path.join(gitdir, rel))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _nombre_de_url(url):
|
|
196
|
+
nombre = url.rstrip("/").rsplit("/", 1)[-1]
|
|
197
|
+
if ":" in nombre and "/" not in url:
|
|
198
|
+
nombre = nombre.rsplit(":", 1)[-1]
|
|
199
|
+
return nombre[:-4] if nombre.endswith(".git") else nombre
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _de_la_org(url, org):
|
|
203
|
+
if not org:
|
|
204
|
+
return True
|
|
205
|
+
aguja = "/" + org.lower() + "/"
|
|
206
|
+
u = url.lower().replace(":", "/")
|
|
207
|
+
return aguja in u or u.endswith("/" + org.lower())
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _mira_git(carpeta, marca, org):
|
|
211
|
+
"""Classify one directory that holds a `.git`. Returns a place, or None."""
|
|
212
|
+
esclon = os.path.isdir(marca)
|
|
213
|
+
gitdir = marca if esclon else _gitdir_de_worktree(marca)
|
|
214
|
+
if not gitdir:
|
|
215
|
+
return None
|
|
216
|
+
config = os.path.join(gitdir if esclon else _comun(gitdir), "config")
|
|
217
|
+
url = _lee_config(config)
|
|
218
|
+
if not url or not _de_la_org(url, org):
|
|
219
|
+
return None
|
|
220
|
+
nombre = _nombre_de_url(url)
|
|
221
|
+
if not nombre:
|
|
222
|
+
return None
|
|
223
|
+
cuando = _tocado(gitdir)
|
|
224
|
+
if esclon:
|
|
225
|
+
return {"clase": "repo", "nombre": nombre, "ruta": carpeta, "cuando": cuando}
|
|
226
|
+
rama = _rama(gitdir) or os.path.basename(carpeta)
|
|
227
|
+
return {"clase": "worktree", "nombre": f"{nombre}@{rama}", "ruta": carpeta, "cuando": cuando}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _cuenta_documentos(entradas):
|
|
231
|
+
n = 0
|
|
232
|
+
for e in entradas:
|
|
233
|
+
if e.is_file() and e.name.lower().endswith(DOCUMENTOS) and not e.name.startswith("."):
|
|
234
|
+
n += 1
|
|
235
|
+
if n >= MINIMO_DOCS:
|
|
236
|
+
break
|
|
237
|
+
return n
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _clasifica(aqui, entradas, raiz, en_repo, docs_arriba, org):
|
|
241
|
+
"""What this one directory is, if it is anything.
|
|
242
|
+
|
|
243
|
+
Returns `(place or None, inside a repository, a documents folder claimed an
|
|
244
|
+
ancestor)`. Kept out of the walk because the walk's job is to visit
|
|
245
|
+
directories and this one's is to recognise them, and a single function doing
|
|
246
|
+
both was the shape nobody could read.
|
|
247
|
+
"""
|
|
248
|
+
if ".git" in {e.name for e in entradas}:
|
|
249
|
+
return _mira_git(aqui, os.path.join(aqui, ".git"), org), True, docs_arriba
|
|
250
|
+
# Documents only outside a repository, and only the shallowest folder of a
|
|
251
|
+
# chain: a repo's own `docs/` is already reachable through the repo, and
|
|
252
|
+
# offering a vault plus every folder inside it makes the picker argue with
|
|
253
|
+
# itself.
|
|
254
|
+
if aqui == raiz or en_repo or docs_arriba:
|
|
255
|
+
return None, en_repo, docs_arriba
|
|
256
|
+
if _cuenta_documentos(entradas) < MINIMO_DOCS:
|
|
257
|
+
return None, en_repo, docs_arriba
|
|
258
|
+
sitio = {
|
|
259
|
+
"clase": "docs",
|
|
260
|
+
"nombre": os.path.basename(aqui) or aqui,
|
|
261
|
+
"ruta": aqui,
|
|
262
|
+
"cuando": _tocado(aqui),
|
|
263
|
+
}
|
|
264
|
+
return sitio, en_repo, True
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def escanea():
|
|
268
|
+
"""Walk the roots once and return every place found, ordered.
|
|
269
|
+
|
|
270
|
+
A single pass answers all three questions, because they are all answers to
|
|
271
|
+
"what is in this directory" and walking a home directory three times to ask
|
|
272
|
+
it three ways would be the slow, obvious mistake.
|
|
273
|
+
"""
|
|
274
|
+
hondo = profundidad()
|
|
275
|
+
org = os.environ.get("AGENTS_CITY_ORG", "").strip()
|
|
276
|
+
fuera = []
|
|
277
|
+
vistos = set()
|
|
278
|
+
for raiz in raices():
|
|
279
|
+
base = raiz.rstrip(os.sep).count(os.sep)
|
|
280
|
+
# (path, inside a repository, a documents folder claimed an ancestor).
|
|
281
|
+
# Both flags travel down the walk rather than being asked again per
|
|
282
|
+
# directory.
|
|
283
|
+
pila = [(raiz, False, False)]
|
|
284
|
+
while pila:
|
|
285
|
+
aqui, en_repo, docs_arriba = pila.pop()
|
|
286
|
+
try:
|
|
287
|
+
with os.scandir(aqui) as it:
|
|
288
|
+
entradas = list(it)
|
|
289
|
+
except OSError:
|
|
290
|
+
continue
|
|
291
|
+
sitio, en_repo, docs_arriba = _clasifica(
|
|
292
|
+
aqui, entradas, raiz, en_repo, docs_arriba, org
|
|
293
|
+
)
|
|
294
|
+
if sitio and sitio["ruta"] not in vistos:
|
|
295
|
+
vistos.add(sitio["ruta"])
|
|
296
|
+
fuera.append(sitio)
|
|
297
|
+
if aqui.rstrip(os.sep).count(os.sep) - base >= hondo:
|
|
298
|
+
continue
|
|
299
|
+
for e in entradas:
|
|
300
|
+
if e.name in SALTAR or e.name.startswith("."):
|
|
301
|
+
continue
|
|
302
|
+
try:
|
|
303
|
+
if e.is_dir(follow_symlinks=False):
|
|
304
|
+
pila.append((e.path, en_repo, docs_arriba))
|
|
305
|
+
except OSError:
|
|
306
|
+
continue
|
|
307
|
+
# Most recently touched first, whatever kind it is. The folder somebody was
|
|
308
|
+
# working in an hour ago is the one they came here to pick, and it should not
|
|
309
|
+
# be a hundred alphabetical rows down.
|
|
310
|
+
fuera.sort(key=lambda s: (-s.get("cuando", 0.0), s["nombre"].lower()))
|
|
311
|
+
return fuera
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ── the cache ────────────────────────────────────────────────────────────────
|
|
315
|
+
def fichero_cache():
|
|
316
|
+
base = os.environ.get("XDG_CACHE_HOME") or os.path.join(os.path.expanduser("~"), ".cache")
|
|
317
|
+
return os.path.join(base, "agents-city", "lugares.tsv")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _escribe(sitios):
|
|
321
|
+
ruta = fichero_cache()
|
|
322
|
+
os.makedirs(os.path.dirname(ruta), exist_ok=True)
|
|
323
|
+
tmp = f"{ruta}.tmp.{os.getpid()}"
|
|
324
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
325
|
+
for s in sitios:
|
|
326
|
+
f.write(f"{s['clase']}\t{s['nombre']}\t{s['ruta']}\t{int(s.get('cuando', 0))}\n")
|
|
327
|
+
os.replace(tmp, ruta)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _lee():
|
|
331
|
+
try:
|
|
332
|
+
with open(fichero_cache(), "r", encoding="utf-8") as f:
|
|
333
|
+
lineas = f.read().splitlines()
|
|
334
|
+
except OSError:
|
|
335
|
+
return None
|
|
336
|
+
sitios = []
|
|
337
|
+
for linea in lineas:
|
|
338
|
+
partes = linea.split("\t")
|
|
339
|
+
if len(partes) < 3:
|
|
340
|
+
continue
|
|
341
|
+
cuando = 0.0
|
|
342
|
+
if len(partes) > 3:
|
|
343
|
+
try:
|
|
344
|
+
cuando = float(partes[3])
|
|
345
|
+
except ValueError:
|
|
346
|
+
cuando = 0.0
|
|
347
|
+
sitios.append(
|
|
348
|
+
{"clase": partes[0], "nombre": partes[1], "ruta": partes[2], "cuando": cuando}
|
|
349
|
+
)
|
|
350
|
+
return sitios
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def caduco(dias=1):
|
|
354
|
+
try:
|
|
355
|
+
return time.time() - os.path.getmtime(fichero_cache()) > dias * 86400
|
|
356
|
+
except OSError:
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def lugares(refrescar=False, dias=1):
|
|
361
|
+
"""Every place, from the cache while it is fresh. The first crawl of a full
|
|
362
|
+
home directory is not fast and nobody should pay for it twice."""
|
|
363
|
+
if not refrescar and not caduco(dias):
|
|
364
|
+
guardado = _lee()
|
|
365
|
+
if guardado is not None:
|
|
366
|
+
return guardado
|
|
367
|
+
sitios = escanea()
|
|
368
|
+
try:
|
|
369
|
+
_escribe(sitios)
|
|
370
|
+
except OSError:
|
|
371
|
+
pass # an unwritable cache is slow, not broken
|
|
372
|
+
return sitios
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def repos(refrescar=False):
|
|
376
|
+
"""Just the git ones, as `(name, path)` — what the launcher asks for."""
|
|
377
|
+
return [(s["nombre"], s["ruta"]) for s in lugares(refrescar) if s["clase"] != "docs"]
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def ruta_de(nombre):
|
|
381
|
+
"""One place's path by name, rebuilding the index once if it is not there —
|
|
382
|
+
the repo somebody cloned a minute ago is exactly the one they came to use."""
|
|
383
|
+
for intento in (False, True):
|
|
384
|
+
for s in lugares(refrescar=intento):
|
|
385
|
+
if s["nombre"] == nombre:
|
|
386
|
+
return s["ruta"]
|
|
387
|
+
return ""
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _lee_argv(argv):
|
|
391
|
+
"""The flags, separated from the work they ask for."""
|
|
392
|
+
refrescar = False
|
|
393
|
+
formato = "tsv"
|
|
394
|
+
resto = []
|
|
395
|
+
for a in argv:
|
|
396
|
+
if a in ("--refresh", "--refrescar"):
|
|
397
|
+
refrescar = True
|
|
398
|
+
elif a == "--json":
|
|
399
|
+
formato = "json"
|
|
400
|
+
elif a == "--repos":
|
|
401
|
+
formato = "repos"
|
|
402
|
+
else:
|
|
403
|
+
resto.append(a)
|
|
404
|
+
return refrescar, formato, resto
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _imprime(sitios, formato):
|
|
408
|
+
if formato == "json":
|
|
409
|
+
json.dump(sitios, sys.stdout)
|
|
410
|
+
return
|
|
411
|
+
for s in sitios:
|
|
412
|
+
if formato == "repos":
|
|
413
|
+
if s["clase"] != "docs":
|
|
414
|
+
print(f"{s['nombre']}\t{s['ruta']}")
|
|
415
|
+
else:
|
|
416
|
+
print(f"{s['clase']}\t{s['nombre']}\t{s['ruta']}")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def main(argv):
|
|
420
|
+
refrescar, formato, resto = _lee_argv(argv)
|
|
421
|
+
if resto:
|
|
422
|
+
ruta = ruta_de(resto[0])
|
|
423
|
+
if ruta:
|
|
424
|
+
print(ruta)
|
|
425
|
+
return 0
|
|
426
|
+
if refrescar:
|
|
427
|
+
print("Looking through your disk…", file=sys.stderr)
|
|
428
|
+
sitios = lugares(refrescar=refrescar)
|
|
429
|
+
_imprime(sitios, formato)
|
|
430
|
+
if refrescar:
|
|
431
|
+
print(f"{len(sitios)} places indexed in {fichero_cache()}", file=sys.stderr)
|
|
432
|
+
return 0
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
if __name__ == "__main__":
|
|
436
|
+
sys.exit(main(sys.argv[1:]))
|