agents-city 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-plugin/marketplace.json +16 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.es.md +2226 -0
- package/README.md +2196 -0
- package/benchmarks/committee/README.md +39 -0
- package/benchmarks/committee/metrics.py +51 -0
- package/benchmarks/committee/run.py +64 -0
- package/benchmarks/committee/traces/01-single.json +8 -0
- package/benchmarks/committee/traces/02-mesh.json +16 -0
- package/benchmarks/committee/traces/03-chair.json +15 -0
- package/benchmarks/latency/README.md +35 -0
- package/benchmarks/latency/fake-claude-cli.mjs +119 -0
- package/benchmarks/latency/fake-native-server.mjs +340 -0
- package/benchmarks/latency/fake-tui.py +91 -0
- package/benchmarks/latency/live.py +380 -0
- package/benchmarks/stress/README.md +20 -0
- package/benchmarks/stress/run.py +487 -0
- package/bin/agents +4 -0
- package/bin/agents-city.js +119 -0
- package/bin/benchmark +17 -0
- package/bin/bus +6 -0
- package/bin/cities +3 -0
- package/bin/city +86 -0
- package/bin/committee +6 -0
- package/bin/demo +200 -0
- package/bin/doctor +3 -0
- package/bin/exit +5 -0
- package/bin/hall +73 -0
- package/bin/hall.html +498 -0
- package/bin/logs +3 -0
- package/bin/navegador.mjs +243 -0
- package/bin/report.py +14 -0
- package/bin/reset +3 -0
- package/bin/road +3 -0
- package/bin/seat +10 -0
- package/bin/serve.py +1579 -0
- package/bin/setup.py +753 -0
- package/bin/shortcut +3 -0
- package/bin/skills +3 -0
- package/bin/test +133 -0
- package/bin/test-actualiza.py +130 -0
- package/bin/test-adapter.py +183 -0
- package/bin/test-admision.py +99 -0
- package/bin/test-atajos.py +301 -0
- package/bin/test-avatar.py +58 -0
- package/bin/test-benchmark.py +40 -0
- package/bin/test-broker.py +334 -0
- package/bin/test-cage.py +374 -0
- package/bin/test-card.py +402 -0
- package/bin/test-channel.py +379 -0
- package/bin/test-cities.py +453 -0
- package/bin/test-claude-runtime.py +252 -0
- package/bin/test-committee.py +340 -0
- package/bin/test-contracts.py +551 -0
- package/bin/test-crecimiento.py +164 -0
- package/bin/test-demo.py +308 -0
- package/bin/test-doctor.py +139 -0
- package/bin/test-domains.py +151 -0
- package/bin/test-evidencia.py +50 -0
- package/bin/test-exit.py +128 -0
- package/bin/test-hall-protocol.py +93 -0
- package/bin/test-launch.py +169 -0
- package/bin/test-live-feed.py +508 -0
- package/bin/test-navegador.py +140 -0
- package/bin/test-pairing.py +99 -0
- package/bin/test-parcels.py +283 -0
- package/bin/test-runtime-failures.py +252 -0
- package/bin/test-runtime-ui.py +488 -0
- package/bin/test-runtime.py +268 -0
- package/bin/test-rutas.py +80 -0
- package/bin/test-seat.py +1598 -0
- package/bin/test-security.py +150 -0
- package/bin/test-serve.py +1251 -0
- package/bin/test-stress.py +64 -0
- package/bin/test-widgets.py +143 -0
- package/bin/test-workspace.py +251 -0
- package/bin/testlib.py +118 -0
- package/bin/tokens.py +14 -0
- package/bin/update +3 -0
- package/bus/scripts/deploy.sh +60 -0
- package/bus/scripts/mint-token.sh +68 -0
- package/bus/scripts/setup-dev.sh +79 -0
- package/bus/scripts/test-channel.ts +8 -0
- package/bus/scripts/test-hub.ts +266 -0
- package/bus/scripts/test-local.sh +40 -0
- package/bus/scripts/test-queue.ts +132 -0
- package/bus/worker/package-lock.json +1971 -0
- package/bus/worker/package.json +17 -0
- package/bus/worker/src/index.ts +485 -0
- package/bus/worker/tsconfig.json +13 -0
- package/bus/worker/wrangler.toml +32 -0
- package/city/oven/README.md +59 -0
- package/city/oven/collect.py +43 -0
- package/city/oven/oven.html +4 -0
- package/city/scripts/history.py +120 -0
- package/city/scripts/seed.py +442 -0
- package/city/web/assets/modelos/License.txt +28 -0
- package/city/web/assets/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/assets/modelos/building-a.glb +0 -0
- package/city/web/assets/modelos/building-b.glb +0 -0
- package/city/web/assets/modelos/building-c.glb +0 -0
- package/city/web/assets/modelos/building-d.glb +0 -0
- package/city/web/assets/modelos/building-e.glb +0 -0
- package/city/web/assets/modelos/building-f.glb +0 -0
- package/city/web/assets/modelos/building-g.glb +0 -0
- package/city/web/assets/modelos/building-h.glb +0 -0
- package/city/web/assets/sprites/building-a.png +0 -0
- package/city/web/assets/sprites/building-b.png +0 -0
- package/city/web/assets/sprites/building-c.png +0 -0
- package/city/web/assets/sprites/building-d.png +0 -0
- package/city/web/assets/sprites/building-e.png +0 -0
- package/city/web/assets/sprites/building-f.png +0 -0
- package/city/web/assets/sprites/building-g.png +0 -0
- package/city/web/assets/sprites/building-h.png +0 -0
- package/city/web/assets/sprites/building-i.png +0 -0
- package/city/web/assets/sprites/building-j.png +0 -0
- package/city/web/assets/sprites/building-k.png +0 -0
- package/city/web/assets/sprites/building-l.png +0 -0
- package/city/web/assets/sprites/building-m.png +0 -0
- package/city/web/assets/sprites/building-n.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-a.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-b.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-c.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-d.png +0 -0
- package/city/web/assets/sprites/building-skyscraper-e.png +0 -0
- package/city/web/assets/sprites/catalogo.json +47 -0
- package/city/web/assets/sprites/detail-awning-wide.png +0 -0
- package/city/web/assets/sprites/detail-awning.png +0 -0
- package/city/web/assets/sprites/detail-overhang-wide.png +0 -0
- package/city/web/assets/sprites/detail-overhang.png +0 -0
- package/city/web/assets/sprites/detail-parasol-a.png +0 -0
- package/city/web/assets/sprites/detail-parasol-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-b.png +0 -0
- package/city/web/assets/sprites/low-detail-building-c.png +0 -0
- package/city/web/assets/sprites/low-detail-building-d.png +0 -0
- package/city/web/assets/sprites/low-detail-building-e.png +0 -0
- package/city/web/assets/sprites/low-detail-building-f.png +0 -0
- package/city/web/assets/sprites/low-detail-building-g.png +0 -0
- package/city/web/assets/sprites/low-detail-building-h.png +0 -0
- package/city/web/assets/sprites/low-detail-building-i.png +0 -0
- package/city/web/assets/sprites/low-detail-building-j.png +0 -0
- package/city/web/assets/sprites/low-detail-building-k.png +0 -0
- package/city/web/assets/sprites/low-detail-building-l.png +0 -0
- package/city/web/assets/sprites/low-detail-building-m.png +0 -0
- package/city/web/assets/sprites/low-detail-building-n.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/assets/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/assets/sprites/medidas.json +248 -0
- package/city/web/dist/city.js +939 -0
- package/city/web/dist/index.html +294 -0
- package/city/web/dist/modelos/License.txt +28 -0
- package/city/web/dist/modelos/PROCEDENCIA.txt +3 -0
- package/city/web/dist/modelos/building-a.glb +0 -0
- package/city/web/dist/modelos/building-b.glb +0 -0
- package/city/web/dist/modelos/building-c.glb +0 -0
- package/city/web/dist/modelos/building-d.glb +0 -0
- package/city/web/dist/modelos/building-e.glb +0 -0
- package/city/web/dist/modelos/building-f.glb +0 -0
- package/city/web/dist/modelos/building-g.glb +0 -0
- package/city/web/dist/modelos/building-h.glb +0 -0
- package/city/web/dist/sprites/building-a.png +0 -0
- package/city/web/dist/sprites/building-b.png +0 -0
- package/city/web/dist/sprites/building-c.png +0 -0
- package/city/web/dist/sprites/building-d.png +0 -0
- package/city/web/dist/sprites/building-e.png +0 -0
- package/city/web/dist/sprites/building-f.png +0 -0
- package/city/web/dist/sprites/building-g.png +0 -0
- package/city/web/dist/sprites/building-h.png +0 -0
- package/city/web/dist/sprites/building-i.png +0 -0
- package/city/web/dist/sprites/building-j.png +0 -0
- package/city/web/dist/sprites/building-k.png +0 -0
- package/city/web/dist/sprites/building-l.png +0 -0
- package/city/web/dist/sprites/building-m.png +0 -0
- package/city/web/dist/sprites/building-n.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-a.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-b.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-c.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-d.png +0 -0
- package/city/web/dist/sprites/building-skyscraper-e.png +0 -0
- package/city/web/dist/sprites/catalogo.json +47 -0
- package/city/web/dist/sprites/detail-awning-wide.png +0 -0
- package/city/web/dist/sprites/detail-awning.png +0 -0
- package/city/web/dist/sprites/detail-overhang-wide.png +0 -0
- package/city/web/dist/sprites/detail-overhang.png +0 -0
- package/city/web/dist/sprites/detail-parasol-a.png +0 -0
- package/city/web/dist/sprites/detail-parasol-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-b.png +0 -0
- package/city/web/dist/sprites/low-detail-building-c.png +0 -0
- package/city/web/dist/sprites/low-detail-building-d.png +0 -0
- package/city/web/dist/sprites/low-detail-building-e.png +0 -0
- package/city/web/dist/sprites/low-detail-building-f.png +0 -0
- package/city/web/dist/sprites/low-detail-building-g.png +0 -0
- package/city/web/dist/sprites/low-detail-building-h.png +0 -0
- package/city/web/dist/sprites/low-detail-building-i.png +0 -0
- package/city/web/dist/sprites/low-detail-building-j.png +0 -0
- package/city/web/dist/sprites/low-detail-building-k.png +0 -0
- package/city/web/dist/sprites/low-detail-building-l.png +0 -0
- package/city/web/dist/sprites/low-detail-building-m.png +0 -0
- package/city/web/dist/sprites/low-detail-building-n.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-a.png +0 -0
- package/city/web/dist/sprites/low-detail-building-wide-b.png +0 -0
- package/city/web/dist/sprites/medidas.json +248 -0
- package/city/web/dist-hall/hall.js +1363 -0
- package/city/web/index.html +294 -0
- package/city/web/package.json +18 -0
- package/city/web/sella.py +21 -0
- package/city/web/src/activity-actors.ts +47 -0
- package/city/web/src/activity.ts +172 -0
- package/city/web/src/ayuntamiento.ts +580 -0
- package/city/web/src/draw.ts +640 -0
- package/city/web/src/game-speech.ts +79 -0
- package/city/web/src/hall.ts +1959 -0
- package/city/web/src/main.ts +2252 -0
- package/city/web/src/oven.ts +124 -0
- package/city/web/src/people.ts +244 -0
- package/city/web/src/presencia.ts +193 -0
- package/city/web/src/puertas.ts +180 -0
- package/city/web/src/repo-roles.ts +59 -0
- package/city/worker/package.json +16 -0
- package/city/worker/schema.sql +117 -0
- package/city/worker/src/index.ts +702 -0
- package/city/worker/src/square.ts +218 -0
- package/city/worker/wrangler.toml +58 -0
- package/demo/ada.md +50 -0
- package/demo/bruno.md +62 -0
- package/demo/camila.md +56 -0
- package/demo/city/ada.md +40 -0
- package/demo/city/city.yml +8 -0
- package/demo/city/parcels.yml +15 -0
- package/demo/city/roads.json +4 -0
- package/demo/city/units.yml +10 -0
- package/demo/clinica/city.yml +8 -0
- package/demo/clinica/parcels.yml +15 -0
- package/demo/clinica/roads.json +4 -0
- package/demo/clinica/units.yml +9 -0
- package/demo/clinica/vera.md +46 -0
- package/demo/dante.md +54 -0
- package/demo/despacho/city.yml +8 -0
- package/demo/despacho/marta.md +46 -0
- package/demo/despacho/parcels.yml +15 -0
- package/demo/despacho/roads.json +4 -0
- package/demo/despacho/units.yml +9 -0
- package/demo/elsa.md +50 -0
- package/demo/farid.md +52 -0
- package/demo/greta.md +50 -0
- package/demo/hugo.md +49 -0
- package/demo/iris.md +49 -0
- package/demo/jonas.md +51 -0
- package/demo/kira.md +51 -0
- package/demo/luca.md +49 -0
- package/demo/parcels.yml +92 -0
- package/demo/seed.py +20 -0
- package/demo/show.py +161 -0
- package/demo/stories.py +824 -0
- package/demo/units.yml +33 -0
- package/docs/agents-first.md +106 -0
- package/docs/glossary.md +55 -0
- package/docs/map-live-layers.md +73 -0
- package/docs/security.md +265 -0
- package/docs/self-host.md +151 -0
- package/docs/testing.md +205 -0
- package/package.json +73 -0
- package/plugin/.claude-plugin/plugin.json +62 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/channel/activity-cli.ts +16 -0
- package/plugin/channel/adapter-prompts.ts +113 -0
- package/plugin/channel/adapter.js +4292 -0
- package/plugin/channel/adapter.ts +133 -0
- package/plugin/channel/bus.js +20188 -0
- package/plugin/channel/bus.ts +131 -0
- package/plugin/channel/city-config.ts +125 -0
- package/plugin/channel/claude-channel.ts +58 -0
- package/plugin/channel/cli-args.ts +75 -0
- package/plugin/channel/client.js +4200 -0
- package/plugin/channel/client.ts +38 -0
- package/plugin/channel/committee/activity.ts +256 -0
- package/plugin/channel/committee/collection.ts +154 -0
- package/plugin/channel/committee/decision.ts +175 -0
- package/plugin/channel/committee/floor.ts +135 -0
- package/plugin/channel/committee/guards.ts +30 -0
- package/plugin/channel/committee/history.ts +38 -0
- package/plugin/channel/committee/render.ts +102 -0
- package/plugin/channel/committee/service.ts +72 -0
- package/plugin/channel/committee/storage.ts +92 -0
- package/plugin/channel/committee/types.ts +163 -0
- package/plugin/channel/committee/view.ts +63 -0
- package/plugin/channel/committee-cli.ts +109 -0
- package/plugin/channel/delivery-metrics.ts +61 -0
- package/plugin/channel/delivery-queue.ts +118 -0
- package/plugin/channel/hub/activity-controller.ts +72 -0
- package/plugin/channel/hub/activity-feed.ts +169 -0
- package/plugin/channel/hub/committee-controller.ts +60 -0
- package/plugin/channel/hub/connections.ts +48 -0
- package/plugin/channel/hub/diagnostics.ts +67 -0
- package/plugin/channel/hub/envelope-validity.ts +109 -0
- package/plugin/channel/hub/envelopes.ts +66 -0
- package/plugin/channel/hub/lifecycle.ts +91 -0
- package/plugin/channel/hub/local-roads.ts +89 -0
- package/plugin/channel/hub/remote-roads.ts +165 -0
- package/plugin/channel/hub/road-controller.ts +99 -0
- package/plugin/channel/hub-client.ts +149 -0
- package/plugin/channel/local-hub.js +5995 -0
- package/plugin/channel/local-hub.ts +349 -0
- package/plugin/channel/map-reporter.ts +48 -0
- package/plugin/channel/package-lock.json +1731 -0
- package/plugin/channel/package.json +19 -0
- package/plugin/channel/protocol.ts +98 -0
- package/plugin/channel/road-cli.ts +18 -0
- package/plugin/channel/run.sh +15 -0
- package/plugin/channel/runtime/claude.ts +458 -0
- package/plugin/channel/runtime/codex-config.ts +77 -0
- package/plugin/channel/runtime/codex.ts +669 -0
- package/plugin/channel/runtime/command.ts +62 -0
- package/plugin/channel/runtime/factory.ts +15 -0
- package/plugin/channel/runtime/json-rpc.ts +154 -0
- package/plugin/channel/runtime/kimi.ts +277 -0
- package/plugin/channel/runtime/opencode.ts +248 -0
- package/plugin/channel/runtime/process.ts +84 -0
- package/plugin/channel/runtime/types.ts +45 -0
- package/plugin/channel/runtime-files.ts +73 -0
- package/plugin/channel/runtime-gateway.js +6396 -0
- package/plugin/channel/runtime-gateway.ts +358 -0
- package/plugin/channel/runtime-metrics.ts +52 -0
- package/plugin/channel/runtime-subscription.ts +180 -0
- package/plugin/channel/terminal-delivery.ts +131 -0
- package/plugin/channel/untrusted.ts +60 -0
- package/plugin/commands/committee.md +31 -0
- package/plugin/commands/exit.md +32 -0
- package/plugin/commands/goals.md +20 -0
- package/plugin/commands/join.md +18 -0
- package/plugin/commands/notice.md +20 -0
- package/plugin/commands/propose.md +19 -0
- package/plugin/commands/round.md +17 -0
- package/plugin/commands/session.md +22 -0
- package/plugin/commands/settings.md +25 -0
- package/plugin/commands/setup.md +21 -0
- package/plugin/commands/team.md +23 -0
- package/plugin/domains/custom.md +27 -0
- package/plugin/domains/finance.md +31 -0
- package/plugin/domains/healthcare.md +34 -0
- package/plugin/domains/legal.md +31 -0
- package/plugin/domains/marketing.md +33 -0
- package/plugin/domains/operations.md +31 -0
- package/plugin/domains/research.md +32 -0
- package/plugin/domains/sales.md +30 -0
- package/plugin/domains/software.md +37 -0
- package/plugin/hooks/activity.sh +6 -0
- package/plugin/hooks/digging.sh +68 -0
- package/plugin/hooks/growth.sh +42 -0
- package/plugin/hooks/hooks.json +106 -0
- package/plugin/hooks/notice-on-pr.sh +35 -0
- package/plugin/hooks/notice-on-stop.sh +80 -0
- package/plugin/hooks/notice-pending.sh +58 -0
- package/plugin/hooks/solo-en-ciudad.sh +23 -0
- package/plugin/hooks/tokens.sh +47 -0
- package/plugin/roles/examples/account-executive.md +25 -0
- package/plugin/roles/examples/ai-manager.md +34 -0
- package/plugin/roles/examples/associate.md +24 -0
- package/plugin/roles/examples/brand-lead.md +20 -0
- package/plugin/roles/examples/cfo.md +20 -0
- package/plugin/roles/examples/city-lead.md +25 -0
- package/plugin/roles/examples/clinical-director.md +26 -0
- package/plugin/roles/examples/clinical-ops.md +25 -0
- package/plugin/roles/examples/clinician.md +25 -0
- package/plugin/roles/examples/compliance.md +27 -0
- package/plugin/roles/examples/content.md +24 -0
- package/plugin/roles/examples/controller.md +27 -0
- package/plugin/roles/examples/cpto.md +41 -0
- package/plugin/roles/examples/customer-success.md +25 -0
- package/plugin/roles/examples/data-engineer.md +37 -0
- package/plugin/roles/examples/data.md +60 -0
- package/plugin/roles/examples/dev.md +37 -0
- package/plugin/roles/examples/devops.md +39 -0
- package/plugin/roles/examples/enablement.md +24 -0
- package/plugin/roles/examples/ethics.md +24 -0
- package/plugin/roles/examples/fin-analytics.md +27 -0
- package/plugin/roles/examples/health-compliance.md +25 -0
- package/plugin/roles/examples/health-data.md +26 -0
- package/plugin/roles/examples/knowledge.md +25 -0
- package/plugin/roles/examples/lifecycle.md +26 -0
- package/plugin/roles/examples/llm-engineer.md +34 -0
- package/plugin/roles/examples/managing-partner.md +20 -0
- package/plugin/roles/examples/methods.md +23 -0
- package/plugin/roles/examples/operations-lead.md +23 -0
- package/plugin/roles/examples/ops.md +24 -0
- package/plugin/roles/examples/patient-safety.md +26 -0
- package/plugin/roles/examples/performance.md +27 -0
- package/plugin/roles/examples/po.md +36 -0
- package/plugin/roles/examples/process-owner.md +23 -0
- package/plugin/roles/examples/product-design.md +39 -0
- package/plugin/roles/examples/program-manager.md +24 -0
- package/plugin/roles/examples/quality.md +25 -0
- package/plugin/roles/examples/research-director.md +24 -0
- package/plugin/roles/examples/research-ops.md +25 -0
- package/plugin/roles/examples/researcher.md +25 -0
- package/plugin/roles/examples/revenue-lead.md +25 -0
- package/plugin/roles/examples/revops.md +26 -0
- package/plugin/roles/examples/seo.md +27 -0
- package/plugin/roles/examples/specialist.md +24 -0
- package/plugin/scripts/actualiza.py +198 -0
- package/plugin/scripts/admision.py +165 -0
- package/plugin/scripts/apaga.py +232 -0
- package/plugin/scripts/atajos.py +506 -0
- package/plugin/scripts/avatar.py +211 -0
- package/plugin/scripts/broker.py +531 -0
- package/plugin/scripts/cage.py +492 -0
- package/plugin/scripts/capabilities.py +210 -0
- package/plugin/scripts/card.py +322 -0
- package/plugin/scripts/cities.py +520 -0
- package/plugin/scripts/city-env.sh +90 -0
- package/plugin/scripts/city-runtime.sh +87 -0
- package/plugin/scripts/city-session.sh +552 -0
- package/plugin/scripts/city_env.py +67 -0
- package/plugin/scripts/crecimiento.py +140 -0
- package/plugin/scripts/deliberations.py +48 -0
- package/plugin/scripts/doctor.py +294 -0
- package/plugin/scripts/domains.py +201 -0
- package/plugin/scripts/evidencia.py +64 -0
- package/plugin/scripts/find-repos.sh +111 -0
- package/plugin/scripts/gh.py +159 -0
- package/plugin/scripts/hall_protocol.py +147 -0
- package/plugin/scripts/hook_activity.py +139 -0
- package/plugin/scripts/launch.py +94 -0
- package/plugin/scripts/logs.py +98 -0
- package/plugin/scripts/pairing.py +185 -0
- package/plugin/scripts/parcels.py +132 -0
- package/plugin/scripts/read-card.py +55 -0
- package/plugin/scripts/report.py +201 -0
- package/plugin/scripts/reset.py +180 -0
- package/plugin/scripts/roads.py +230 -0
- package/plugin/scripts/roles.py +274 -0
- package/plugin/scripts/runtime_log.py +69 -0
- package/plugin/scripts/runtime_processes.py +132 -0
- package/plugin/scripts/rutas.py +83 -0
- package/plugin/scripts/seat.py +1696 -0
- package/plugin/scripts/tokens.py +194 -0
- package/plugin/scripts/trust-repos.py +59 -0
- package/plugin/scripts/ui.py +280 -0
- package/plugin/scripts/units.py +80 -0
- package/plugin/scripts/workspace.py +561 -0
- package/plugin/skills/city/SKILL.md +191 -0
- package/templates/blank.md +39 -0
- package/templates/finance.md +44 -0
- package/templates/legal.md +45 -0
- package/templates/marketing.md +48 -0
- package/templates/product.md +45 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A city on your desktop: one icon, its name under it, double-click to enter.
|
|
3
|
+
|
|
4
|
+
A city already has an identity, a name and a colour. What it did not have was a
|
|
5
|
+
door somebody can find without remembering a command — and a tool you have to
|
|
6
|
+
remember how to start is a tool you stop starting. So each city can put a real
|
|
7
|
+
desktop shortcut next to the others: a macOS `.app` bundle, or a Linux
|
|
8
|
+
`.desktop` entry, carrying an icon drawn from the city's own identity.
|
|
9
|
+
|
|
10
|
+
Everything here is stdlib. The icon is a PNG written by hand (zlib plus four
|
|
11
|
+
chunks) so no image library is required, and on macOS `iconutil` — which ships
|
|
12
|
+
with the system — turns it into the `.icns` a bundle wants. If any of that is
|
|
13
|
+
missing the shortcut is still written with whatever icon the desktop gives it,
|
|
14
|
+
because a door with a plain icon beats no door.
|
|
15
|
+
|
|
16
|
+
The shortcut runs the same front door you would type: `agents-city seat --city
|
|
17
|
+
<name>`, or the hall with `--hall`. It is not a second implementation of
|
|
18
|
+
anything; it is a labelled button on the one that exists.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import binascii
|
|
22
|
+
import hashlib
|
|
23
|
+
import os
|
|
24
|
+
import plistlib
|
|
25
|
+
import shutil
|
|
26
|
+
import struct
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
import zlib
|
|
30
|
+
|
|
31
|
+
GUIONES = os.path.dirname(os.path.abspath(__file__))
|
|
32
|
+
sys.path.insert(0, GUIONES)
|
|
33
|
+
import cities # noqa: E402
|
|
34
|
+
|
|
35
|
+
#: The palette a city's icon is tinted from — the map's own house colours, so a
|
|
36
|
+
#: city looks on the desktop like it looks on its map.
|
|
37
|
+
PALETA = (
|
|
38
|
+
(0x3B, 0x82, 0xF6),
|
|
39
|
+
(0x8B, 0x5C, 0xF6),
|
|
40
|
+
(0xF5, 0x9E, 0x0B),
|
|
41
|
+
(0x3F, 0xB8, 0xA0),
|
|
42
|
+
(0xEF, 0x44, 0x44),
|
|
43
|
+
(0x10, 0xB9, 0x81),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
LADO = 512
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def color_de(identidad):
|
|
50
|
+
"""One city, one colour, forever — derived, never stored, never random."""
|
|
51
|
+
marca = hashlib.sha256(str(identidad).encode()).digest()
|
|
52
|
+
return PALETA[marca[0] % len(PALETA)]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _trozo(etiqueta, datos):
|
|
56
|
+
return (
|
|
57
|
+
struct.pack(">I", len(datos))
|
|
58
|
+
+ etiqueta
|
|
59
|
+
+ datos
|
|
60
|
+
+ struct.pack(">I", binascii.crc32(etiqueta + datos) & 0xFFFFFFFF)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def png(pixeles, lado):
|
|
65
|
+
"""A PNG from RGBA rows, by hand: zlib and four chunks, no dependencies."""
|
|
66
|
+
crudo = b"".join(b"\x00" + bytes(fila) for fila in pixeles)
|
|
67
|
+
return b"".join(
|
|
68
|
+
[
|
|
69
|
+
b"\x89PNG\r\n\x1a\n",
|
|
70
|
+
_trozo(b"IHDR", struct.pack(">IIBBBBB", lado, lado, 8, 6, 0, 0, 0)),
|
|
71
|
+
_trozo(b"IDAT", zlib.compress(crudo, 9)),
|
|
72
|
+
_trozo(b"IEND", b""),
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _casa(x, y, lado, color):
|
|
78
|
+
"""One isometric house on a rounded tile — the map's own shape, in small.
|
|
79
|
+
|
|
80
|
+
Drawn arithmetically rather than from an asset: the icon must exist for a
|
|
81
|
+
city created on a machine that never cloned this repo.
|
|
82
|
+
"""
|
|
83
|
+
r, g, b = color
|
|
84
|
+
cx, cy = lado / 2, lado / 2
|
|
85
|
+
borde = lado * 0.08
|
|
86
|
+
# The rounded square the whole icon sits on, as macOS and GNOME both expect.
|
|
87
|
+
dx = max(abs(x - cx) - (lado / 2 - borde - lado * 0.06), 0)
|
|
88
|
+
dy = max(abs(y - cy) - (lado / 2 - borde - lado * 0.06), 0)
|
|
89
|
+
if (dx * dx + dy * dy) ** 0.5 > lado * 0.06:
|
|
90
|
+
return (0, 0, 0, 0)
|
|
91
|
+
# An isometric box: the roof rhombus, and the same rhombus swept downwards
|
|
92
|
+
# to make the two walls. Three tints of one colour, because a silhouette
|
|
93
|
+
# with no shading is a blob at 32 px, which is the size that matters.
|
|
94
|
+
fondo = (int(r * 0.18), int(g * 0.18), int(b * 0.22), 255)
|
|
95
|
+
u = (x - cx) / (lado * 0.34)
|
|
96
|
+
v = (y - cy + lado * 0.06) / (lado * 0.34)
|
|
97
|
+
alto = 0.62 # how tall the walls stand under the roof
|
|
98
|
+
dentro_v = min(max(v, 0.0), alto) # clamp: v above the roof, below the base
|
|
99
|
+
if abs(u) + 2 * abs(v - dentro_v) > 1:
|
|
100
|
+
return fondo
|
|
101
|
+
if v <= 0: # the roof, catching the light
|
|
102
|
+
return (min(255, r + 45), min(255, g + 45), min(255, b + 45), 255)
|
|
103
|
+
f = 0.78 if u < 0 else 0.52 # left wall lit, right wall in shade
|
|
104
|
+
return (int(r * f), int(g * f), int(b * f), 255)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def icono_png(identidad, lado=LADO):
|
|
108
|
+
"""The city's icon as PNG bytes: a house in the city's own colour."""
|
|
109
|
+
color = color_de(identidad)
|
|
110
|
+
filas = []
|
|
111
|
+
for y in range(lado):
|
|
112
|
+
fila = bytearray()
|
|
113
|
+
for x in range(lado):
|
|
114
|
+
fila += bytes(_casa(x + 0.5, y + 0.5, lado, color))
|
|
115
|
+
filas.append(fila)
|
|
116
|
+
return png(filas, lado)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def ico(identidad, lado=256):
|
|
120
|
+
"""The city's icon as Windows `.ico` bytes.
|
|
121
|
+
|
|
122
|
+
An ICO is a tiny header around image data, and since Vista that data may be
|
|
123
|
+
a PNG as-is — so the same hand-written PNG serves Windows with six bytes of
|
|
124
|
+
ceremony, no converter and no dependency.
|
|
125
|
+
"""
|
|
126
|
+
imagen = icono_png(identidad, lado)
|
|
127
|
+
ancho = 0 if lado >= 256 else lado # 0 means 256 in an ICO directory
|
|
128
|
+
cabecera = struct.pack("<HHH", 0, 1, 1)
|
|
129
|
+
entrada = struct.pack(
|
|
130
|
+
"<BBBBHHII", ancho, ancho, 0, 0, 1, 32, len(imagen), len(cabecera) + 16
|
|
131
|
+
)
|
|
132
|
+
return cabecera + entrada + imagen
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
#: The sizes an .icns carries, as (pixels, filename). Each one is DRAWN at its
|
|
136
|
+
#: own size rather than resampled from a big one: the shape is arithmetic, so a
|
|
137
|
+
#: 32px icon drawn as 32px has crisper edges than a downscaled 512px one — and
|
|
138
|
+
#: it costs no resampler, which is why nothing here shells out to `sips`.
|
|
139
|
+
_TAMANOS_ICNS = (
|
|
140
|
+
(16, "icon_16x16.png"),
|
|
141
|
+
(32, "icon_16x16@2x.png"),
|
|
142
|
+
(32, "icon_32x32.png"),
|
|
143
|
+
(64, "icon_32x32@2x.png"),
|
|
144
|
+
(128, "icon_128x128.png"),
|
|
145
|
+
(256, "icon_128x128@2x.png"),
|
|
146
|
+
(256, "icon_256x256.png"),
|
|
147
|
+
(512, "icon_256x256@2x.png"),
|
|
148
|
+
(512, "icon_512x512.png"),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _icns(identidad, destino_icns):
|
|
153
|
+
"""Build a macOS .icns for this city, or False when the system cannot.
|
|
154
|
+
|
|
155
|
+
`iconutil` ships with macOS and is the only outside program involved: the
|
|
156
|
+
images themselves are drawn here, at each size the bundle wants.
|
|
157
|
+
"""
|
|
158
|
+
if not shutil.which("iconutil"):
|
|
159
|
+
return False
|
|
160
|
+
juego = destino_icns + ".iconset"
|
|
161
|
+
os.makedirs(juego, exist_ok=True)
|
|
162
|
+
try:
|
|
163
|
+
dibujados = {}
|
|
164
|
+
for lado, nombre in _TAMANOS_ICNS:
|
|
165
|
+
if lado not in dibujados:
|
|
166
|
+
dibujados[lado] = icono_png(identidad, lado)
|
|
167
|
+
with open(os.path.join(juego, nombre), "wb") as f:
|
|
168
|
+
f.write(dibujados[lado])
|
|
169
|
+
hecho = subprocess.run(
|
|
170
|
+
["iconutil", "-c", "icns", juego, "-o", destino_icns],
|
|
171
|
+
capture_output=True,
|
|
172
|
+
timeout=60,
|
|
173
|
+
)
|
|
174
|
+
return hecho.returncode == 0 and os.path.isfile(destino_icns)
|
|
175
|
+
except (OSError, subprocess.SubprocessError):
|
|
176
|
+
return False
|
|
177
|
+
finally:
|
|
178
|
+
shutil.rmtree(juego, ignore_errors=True)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def en_wsl():
|
|
182
|
+
"""Whether this Linux is really Windows wearing a Linux coat.
|
|
183
|
+
|
|
184
|
+
It matters here more than anywhere else in the product: under WSL a
|
|
185
|
+
`~/Desktop` is the Linux home's desktop, which nobody ever looks at. The
|
|
186
|
+
desktop a person actually sees belongs to Windows.
|
|
187
|
+
"""
|
|
188
|
+
if os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP"):
|
|
189
|
+
return True
|
|
190
|
+
try:
|
|
191
|
+
with open("/proc/version", encoding="utf-8", errors="replace") as f:
|
|
192
|
+
return "microsoft" in f.read().lower()
|
|
193
|
+
except OSError:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _powershell():
|
|
198
|
+
return shutil.which("powershell.exe") or shutil.which("pwsh.exe")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def escritorio_windows():
|
|
202
|
+
"""The Windows desktop, as a path this side of WSL can write, or ''.
|
|
203
|
+
|
|
204
|
+
Asked of Windows itself rather than assembled from a username guess:
|
|
205
|
+
a redirected desktop (OneDrive, a domain profile) lives nowhere near
|
|
206
|
+
C:\\Users\\<name>\\Desktop, and guessing would write into a folder the
|
|
207
|
+
person never sees.
|
|
208
|
+
"""
|
|
209
|
+
consola = _powershell()
|
|
210
|
+
if not consola or not shutil.which("wslpath"):
|
|
211
|
+
return ""
|
|
212
|
+
try:
|
|
213
|
+
salida = subprocess.run(
|
|
214
|
+
[consola, "-NoProfile", "-Command", "[Environment]::GetFolderPath('Desktop')"],
|
|
215
|
+
capture_output=True,
|
|
216
|
+
text=True,
|
|
217
|
+
timeout=30,
|
|
218
|
+
)
|
|
219
|
+
ventana = salida.stdout.strip()
|
|
220
|
+
if salida.returncode != 0 or not ventana:
|
|
221
|
+
return ""
|
|
222
|
+
traducida = subprocess.run(
|
|
223
|
+
["wslpath", "-u", ventana], capture_output=True, text=True, timeout=15
|
|
224
|
+
)
|
|
225
|
+
ruta = traducida.stdout.strip()
|
|
226
|
+
return ruta if traducida.returncode == 0 and os.path.isdir(ruta) else ""
|
|
227
|
+
except (OSError, subprocess.SubprocessError):
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def escritorio():
|
|
232
|
+
"""Where the desktop is, or '' when this machine has none."""
|
|
233
|
+
forzado = os.environ.get("CITY_DESKTOP")
|
|
234
|
+
if forzado:
|
|
235
|
+
ruta = os.path.expanduser(forzado)
|
|
236
|
+
return ruta if os.path.isdir(ruta) else ""
|
|
237
|
+
if en_wsl():
|
|
238
|
+
# The Windows desktop first: it is the one with icons on it. A WSL
|
|
239
|
+
# install with no interop still gets its Linux desktop, which at least
|
|
240
|
+
# a Linux desktop environment inside WSLg can show.
|
|
241
|
+
ventana = escritorio_windows()
|
|
242
|
+
if ventana:
|
|
243
|
+
return ventana
|
|
244
|
+
casa = os.path.expanduser("~/Desktop")
|
|
245
|
+
return casa if os.path.isdir(casa) else ""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def orden_de_ciudad(datos, hall=False):
|
|
249
|
+
"""The command the shortcut runs — the same one a person would type."""
|
|
250
|
+
nombre = cities.nombre(datos)
|
|
251
|
+
programa = "agents-city"
|
|
252
|
+
return f"{programa} {'hall' if hall else 'seat'} --city {_entrecomilla(nombre)}"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _entrecomilla(valor):
|
|
256
|
+
return "'" + str(valor).replace("'", "'\\''") + "'"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _guion_mac(orden):
|
|
260
|
+
"""Open Terminal and run the city there — a city is a tmux session, and a
|
|
261
|
+
session with nowhere to draw itself is not an open city."""
|
|
262
|
+
escapada = orden.replace("\\", "\\\\").replace('"', '\\"')
|
|
263
|
+
return (
|
|
264
|
+
"#!/bin/sh\n"
|
|
265
|
+
"# Written by agents-city. Delete this bundle to remove the shortcut.\n"
|
|
266
|
+
"exec /usr/bin/osascript "
|
|
267
|
+
f'-e "tell application \\"Terminal\\" to do script \\"{escapada}\\"" '
|
|
268
|
+
'-e "tell application \\"Terminal\\" to activate"\n'
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _crea_mac(datos, carpeta, nombre, orden, identidad):
|
|
273
|
+
paquete = os.path.join(carpeta, f"{nombre}.app")
|
|
274
|
+
if os.path.exists(paquete):
|
|
275
|
+
shutil.rmtree(paquete, ignore_errors=True)
|
|
276
|
+
macos = os.path.join(paquete, "Contents", "MacOS")
|
|
277
|
+
recursos = os.path.join(paquete, "Contents", "Resources")
|
|
278
|
+
os.makedirs(macos)
|
|
279
|
+
os.makedirs(recursos)
|
|
280
|
+
ejecutable = os.path.join(macos, "abrir-ciudad")
|
|
281
|
+
with open(ejecutable, "w", encoding="utf-8") as f:
|
|
282
|
+
f.write(_guion_mac(orden))
|
|
283
|
+
os.chmod(ejecutable, 0o755)
|
|
284
|
+
info = {
|
|
285
|
+
"CFBundleName": nombre,
|
|
286
|
+
"CFBundleDisplayName": nombre,
|
|
287
|
+
"CFBundleExecutable": "abrir-ciudad",
|
|
288
|
+
"CFBundleIdentifier": f"com.arkatai.agents-city.{cities.slug(datos) or 'city'}",
|
|
289
|
+
"CFBundlePackageType": "APPL",
|
|
290
|
+
"CFBundleInfoDictionaryVersion": "6.0",
|
|
291
|
+
"CFBundleShortVersionString": "1.0",
|
|
292
|
+
"LSApplicationCategoryType": "public.app-category.developer-tools",
|
|
293
|
+
}
|
|
294
|
+
if _icns(identidad, os.path.join(recursos, "city.icns")):
|
|
295
|
+
info["CFBundleIconFile"] = "city"
|
|
296
|
+
else:
|
|
297
|
+
# No iconutil: keep the plain PNG in the bundle so the icon still
|
|
298
|
+
# exists on disk for anybody who wants to set it by hand.
|
|
299
|
+
with open(os.path.join(recursos, "city.png"), "wb") as f:
|
|
300
|
+
f.write(icono_png(identidad))
|
|
301
|
+
with open(os.path.join(paquete, "Contents", "Info.plist"), "wb") as f:
|
|
302
|
+
plistlib.dump(info, f)
|
|
303
|
+
return paquete
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _crea_linux(datos, carpeta, nombre, orden, identidad):
|
|
307
|
+
iconos = os.path.join(
|
|
308
|
+
os.path.expanduser(os.environ.get("XDG_DATA_HOME") or "~/.local/share"),
|
|
309
|
+
"agents-city",
|
|
310
|
+
"icons",
|
|
311
|
+
)
|
|
312
|
+
os.makedirs(iconos, exist_ok=True)
|
|
313
|
+
icono = os.path.join(iconos, f"{cities.slug(datos) or 'city'}.png")
|
|
314
|
+
with open(icono, "wb") as f:
|
|
315
|
+
f.write(icono_png(identidad, 256))
|
|
316
|
+
atajo = os.path.join(carpeta, f"{cities.slug(datos) or 'city'}.desktop")
|
|
317
|
+
with open(atajo, "w", encoding="utf-8") as f:
|
|
318
|
+
f.write(
|
|
319
|
+
"[Desktop Entry]\n"
|
|
320
|
+
"Type=Application\n"
|
|
321
|
+
"Version=1.0\n"
|
|
322
|
+
f"Name={nombre}\n"
|
|
323
|
+
"Comment=Open this Agents City city\n"
|
|
324
|
+
f"Exec={orden}\n"
|
|
325
|
+
f"Icon={icono}\n"
|
|
326
|
+
"Terminal=true\n"
|
|
327
|
+
"Categories=Development;\n"
|
|
328
|
+
)
|
|
329
|
+
os.chmod(atajo, 0o755)
|
|
330
|
+
# GNOME refuses to launch a .desktop it does not trust; best effort, and a
|
|
331
|
+
# failure here still leaves a file the person can mark themselves.
|
|
332
|
+
if shutil.which("gio"):
|
|
333
|
+
try:
|
|
334
|
+
subprocess.run(
|
|
335
|
+
["gio", "set", atajo, "metadata::trusted", "true"],
|
|
336
|
+
capture_output=True,
|
|
337
|
+
timeout=10,
|
|
338
|
+
)
|
|
339
|
+
except (OSError, subprocess.SubprocessError):
|
|
340
|
+
pass
|
|
341
|
+
return atajo
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _crea_wsl(datos, carpeta, nombre, orden, identidad):
|
|
345
|
+
"""A real Windows shortcut for a city that lives inside WSL.
|
|
346
|
+
|
|
347
|
+
The city is a tmux session in Linux, so the shortcut has to cross back: it
|
|
348
|
+
launches `wsl.exe -d <distro>` running the same front-door command in a
|
|
349
|
+
login shell. A `.lnk` is a COM object, so Windows builds it — through its
|
|
350
|
+
own PowerShell — and that is also what lets it carry an icon. Without
|
|
351
|
+
interop we still write a `.cmd`, which double-clicks fine and only lacks
|
|
352
|
+
the icon; a door with a plain icon beats no door.
|
|
353
|
+
"""
|
|
354
|
+
distro = os.environ.get("WSL_DISTRO_NAME", "")
|
|
355
|
+
dentro = f"cd ~ && {orden}".replace('"', '\\"')
|
|
356
|
+
eleccion = f"-d {distro} " if distro else ""
|
|
357
|
+
argumentos = f'{eleccion}-- bash -lic "{dentro}"'
|
|
358
|
+
icono_win = ""
|
|
359
|
+
consola = _powershell()
|
|
360
|
+
perfil = os.path.join(carpeta, "..")
|
|
361
|
+
iconos = os.path.join(perfil, ".agents-city-icons")
|
|
362
|
+
try:
|
|
363
|
+
os.makedirs(iconos, exist_ok=True)
|
|
364
|
+
icono_win = os.path.join(iconos, f"{cities.slug(datos) or 'city'}.ico")
|
|
365
|
+
with open(icono_win, "wb") as f:
|
|
366
|
+
f.write(ico(identidad))
|
|
367
|
+
except OSError:
|
|
368
|
+
icono_win = ""
|
|
369
|
+
|
|
370
|
+
if consola and shutil.which("wslpath"):
|
|
371
|
+
atajo = os.path.join(carpeta, f"{nombre}.lnk")
|
|
372
|
+
try:
|
|
373
|
+
destino_win = subprocess.run(
|
|
374
|
+
["wslpath", "-w", atajo], capture_output=True, text=True, timeout=15
|
|
375
|
+
).stdout.strip()
|
|
376
|
+
icono_ruta = (
|
|
377
|
+
subprocess.run(
|
|
378
|
+
["wslpath", "-w", icono_win], capture_output=True, text=True, timeout=15
|
|
379
|
+
).stdout.strip()
|
|
380
|
+
if icono_win
|
|
381
|
+
else ""
|
|
382
|
+
)
|
|
383
|
+
guion = (
|
|
384
|
+
"$s = (New-Object -ComObject WScript.Shell).CreateShortcut('"
|
|
385
|
+
+ destino_win.replace("'", "''")
|
|
386
|
+
+ "'); $s.TargetPath = 'wsl.exe'; $s.Arguments = '"
|
|
387
|
+
+ argumentos.replace("'", "''")
|
|
388
|
+
+ "'; $s.Description = 'Open the "
|
|
389
|
+
+ nombre.replace("'", "''")
|
|
390
|
+
+ " city'; "
|
|
391
|
+
+ (f"$s.IconLocation = '{icono_ruta}';" if icono_ruta else "")
|
|
392
|
+
+ " $s.Save()"
|
|
393
|
+
)
|
|
394
|
+
hecho = subprocess.run(
|
|
395
|
+
[consola, "-NoProfile", "-Command", guion],
|
|
396
|
+
capture_output=True,
|
|
397
|
+
text=True,
|
|
398
|
+
timeout=60,
|
|
399
|
+
)
|
|
400
|
+
if hecho.returncode == 0 and os.path.exists(atajo):
|
|
401
|
+
return atajo
|
|
402
|
+
except (OSError, subprocess.SubprocessError):
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
atajo = os.path.join(carpeta, f"{nombre}.cmd")
|
|
406
|
+
with open(atajo, "w", encoding="utf-8", newline="\r\n") as f:
|
|
407
|
+
f.write(
|
|
408
|
+
"@echo off\r\n"
|
|
409
|
+
":: Written by agents-city. Delete this file to remove the shortcut.\r\n"
|
|
410
|
+
f"wsl.exe {argumentos}\r\n"
|
|
411
|
+
)
|
|
412
|
+
os.chmod(atajo, 0o755)
|
|
413
|
+
return atajo
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def crea(datos, hall=False, carpeta=""):
|
|
417
|
+
"""Put this city on the desktop. Returns (path, '') or ('', why)."""
|
|
418
|
+
carpeta = carpeta or escritorio()
|
|
419
|
+
if not carpeta:
|
|
420
|
+
return "", "this machine has no Desktop folder; pass --to <folder>"
|
|
421
|
+
if not os.path.isdir(datos):
|
|
422
|
+
return "", f"there is no city at {datos}"
|
|
423
|
+
nombre = cities.nombre(datos) or "City"
|
|
424
|
+
orden = orden_de_ciudad(datos, hall)
|
|
425
|
+
identidad = cities.identidad(datos) or nombre
|
|
426
|
+
try:
|
|
427
|
+
if sys.platform == "darwin":
|
|
428
|
+
return _crea_mac(datos, carpeta, nombre, orden, identidad), ""
|
|
429
|
+
if en_wsl():
|
|
430
|
+
return _crea_wsl(datos, carpeta, nombre, orden, identidad), ""
|
|
431
|
+
return _crea_linux(datos, carpeta, nombre, orden, identidad), ""
|
|
432
|
+
except OSError as e:
|
|
433
|
+
return "", f"could not write the shortcut: {e}"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def quita(datos, carpeta=""):
|
|
437
|
+
"""Remove this city's shortcut. Returns True when one was there."""
|
|
438
|
+
carpeta = carpeta or escritorio()
|
|
439
|
+
if not carpeta:
|
|
440
|
+
return False
|
|
441
|
+
candidatos = [
|
|
442
|
+
os.path.join(carpeta, f"{cities.nombre(datos)}.app"),
|
|
443
|
+
os.path.join(carpeta, f"{cities.slug(datos) or 'city'}.desktop"),
|
|
444
|
+
os.path.join(carpeta, f"{cities.nombre(datos)}.lnk"),
|
|
445
|
+
os.path.join(carpeta, f"{cities.nombre(datos)}.cmd"),
|
|
446
|
+
]
|
|
447
|
+
quitados = False
|
|
448
|
+
for ruta in candidatos:
|
|
449
|
+
if os.path.isdir(ruta):
|
|
450
|
+
shutil.rmtree(ruta, ignore_errors=True)
|
|
451
|
+
quitados = True
|
|
452
|
+
elif os.path.isfile(ruta):
|
|
453
|
+
os.unlink(ruta)
|
|
454
|
+
quitados = True
|
|
455
|
+
return quitados
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _uso():
|
|
459
|
+
print(
|
|
460
|
+
" usage: agents-city shortcut [city] [--hall] [--remove] [--to <folder>]\n\n"
|
|
461
|
+
" Put a city on your desktop: its icon, its name, double-click to enter.\n"
|
|
462
|
+
" Without a city name it uses the one you are in.\n\n"
|
|
463
|
+
" --hall open the town hall in the browser instead of the seat\n"
|
|
464
|
+
" --remove take the shortcut off the desktop\n"
|
|
465
|
+
" --to DIR write it somewhere else than ~/Desktop\n"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def main():
|
|
470
|
+
args = sys.argv[1:]
|
|
471
|
+
if any(a in ("-h", "--help", "help") for a in args):
|
|
472
|
+
_uso()
|
|
473
|
+
return 0
|
|
474
|
+
hall = "--hall" in args
|
|
475
|
+
quitar = "--remove" in args
|
|
476
|
+
carpeta = ""
|
|
477
|
+
if "--to" in args:
|
|
478
|
+
i = args.index("--to")
|
|
479
|
+
carpeta = os.path.expanduser(args[i + 1]) if len(args) > i + 1 else ""
|
|
480
|
+
del args[i:i + 2]
|
|
481
|
+
pedida = next((a for a in args if not a.startswith("-")), "")
|
|
482
|
+
usuario = cities.usuario_actual()
|
|
483
|
+
datos = cities.resuelve(pedida, usuario) if pedida else cities.actual(usuario)
|
|
484
|
+
if not datos:
|
|
485
|
+
print(f" No city called {pedida!r}.", file=sys.stderr)
|
|
486
|
+
return 1
|
|
487
|
+
if quitar:
|
|
488
|
+
print(
|
|
489
|
+
f" Shortcut removed for {cities.nombre(datos)}."
|
|
490
|
+
if quita(datos, carpeta)
|
|
491
|
+
else f" {cities.nombre(datos)} had no shortcut on the desktop."
|
|
492
|
+
)
|
|
493
|
+
return 0
|
|
494
|
+
ruta, mal = crea(datos, hall, carpeta)
|
|
495
|
+
if mal:
|
|
496
|
+
print(f" {mal}", file=sys.stderr)
|
|
497
|
+
return 1
|
|
498
|
+
print(
|
|
499
|
+
f" {cities.nombre(datos)} is on your desktop: {ruta.replace(os.path.expanduser('~'), '~')}"
|
|
500
|
+
f"\n Double-click it to open {'the hall' if hall else 'the city'}."
|
|
501
|
+
)
|
|
502
|
+
return 0
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
if __name__ == "__main__":
|
|
506
|
+
sys.exit(main())
|