@@ -582,5 +582,198 @@ def _print_status(state: RuntimeState) -> None:
582582 click .echo (f"step-ca container: { state .step_ca_container_id } " )
583583
584584
585+ _INIT_SPEC_TEMPLATE = """\
586+ metadata:
587+ name: {name}
588+ version: "1.0"
589+ lifecycle: {lifecycle}
590+
591+ substrate:
592+ orchestrator: swarm
593+ ntp:
594+ enabled: true
595+ servers:
596+ - pool.ntp.org
597+ networks:
598+ platform:
599+ type: bridge
600+ subnet: 172.28.0.0/16
601+ description: "Platform management network"
602+ core:
603+ type: bridge
604+ subnet: 10.0.0.0/24
605+ description: "In-world core network"
606+ gateway:
607+ platform_ip: 172.28.0.1
608+ core_ip: 10.0.0.1
609+ description: "Gateway stub"
610+
611+ dns:
612+ root:
613+ enabled: true
614+ type: authoritative
615+ server: coredns
616+ listen_ip: 10.0.0.2
617+ soa_primary_ns: root.internal
618+ soa_email: admin.internal
619+ serial_policy: timestamp
620+ platform_zone:
621+ name: platform.internal
622+ type: authoritative
623+ listen_ip: 10.0.0.3
624+ tlds:
625+ - name: internal
626+ description: "Default in-world TLD"
627+ type: authoritative
628+ listen_ip: 10.0.0.4
629+
630+ pki:
631+ root_ca:
632+ cn: "{name} Root CA"
633+ o: "{name}"
634+ c: "US"
635+ key_storage_mode: ephemeral
636+ cert_lifetime_days: 3650
637+ acme:
638+ enabled: true
639+ listen_ip: 10.0.0.6
640+ canonical_name: ca.platform.internal
641+ dnssec_enabled: true
642+ dnssec_ksk_lifetime_days: 365
643+ dnssec_zsk_lifetime_days: 30
644+
645+ identity_platform:
646+ oidc_provider: keycloak
647+ listen_ip: 10.0.0.7
648+ canonical_name: auth.platform.internal
649+ realm_name: platform
650+ admin_user:
651+ username: admin
652+ email: admin@platform.internal
653+ scopes:
654+ - "netengines:read"
655+ - "netengines:write"
656+ - "netengines:admin"
657+
658+ world_registry:
659+ enabled: true
660+ listen_ip: 10.0.0.8
661+ canonical_name: registry.platform.internal
662+ organizations: []
663+ operators: []
664+ whois:
665+ enabled: true
666+ listen_ip: 10.0.0.9
667+ port: 43
668+
669+ domain_registry:
670+ enabled: true
671+ listen_ip: 10.0.0.10
672+ canonical_name: domainreg.platform.internal
673+ tld_delegations: []
674+ address_space: []
675+ registrar:
676+ enabled: true
677+ listen_ip: 10.0.0.11
678+ canonical_name: registrar.platform.internal
679+
680+ identity_inworld:
681+ oidc_provider: keycloak
682+ listen_ip: 10.0.0.12
683+ canonical_name: auth.internal
684+ realm_name: inworld
685+ org_users: []
686+ scopes:
687+ - profile
688+ - email
689+ - openid
690+
691+ ands:
692+ profiles: {{}}
693+ instances: []
694+
695+ world_services:
696+ mail:
697+ enabled: false
698+ storage:
699+ enabled: false
700+
701+ org_apps:
702+ enabled: true
703+ catalog: []
704+ deployments: []
705+
706+ gateway_portal:
707+ enabled: true
708+ real_internet:
709+ mode: isolated
710+ cross_world:
711+ mode: none
712+
713+ operator:
714+ api:
715+ enabled: true
716+ listen_ip: 172.28.0.11
717+ port: 8080
718+ canonical_name: api.platform.internal
719+ auth:
720+ provider: oidc
721+ issuer: "https://auth.platform.internal/realms/platform"
722+ required_scope: "netengines:read"
723+ """
724+
725+
726+ @cli .command ()
727+ @click .option ("--name" , default = None , help = "World name (e.g. my-world)." )
728+ @click .option (
729+ "--lifecycle" ,
730+ type = click .Choice (["ephemeral" , "persistent" ]),
731+ default = None ,
732+ help = "World lifecycle mode." ,
733+ )
734+ @click .option ("--output" , "-o" , default = None , help = "Output file path (default: <name>.yaml)." )
735+ @click .option (
736+ "--yes" , "-y" , is_flag = True , default = False , help = "Accept all defaults without prompting."
737+ )
738+ def init (name : str | None , lifecycle : str | None , output : str | None , yes : bool ) -> None :
739+ """Scaffold a new world spec file and print next steps."""
740+ if not name :
741+ if yes :
742+ name = "my-world"
743+ else :
744+ name = click .prompt ("World name" , default = "my-world" )
745+
746+ if not lifecycle :
747+ if yes :
748+ lifecycle = "ephemeral"
749+ else :
750+ lifecycle = click .prompt (
751+ "Lifecycle" ,
752+ type = click .Choice (["ephemeral" , "persistent" ]),
753+ default = "ephemeral" ,
754+ )
755+
756+ out_path = Path (output ) if output else Path (f"{ name } .yaml" )
757+
758+ if out_path .exists () and not yes :
759+ click .confirm (f"{ out_path } already exists — overwrite?" , abort = True )
760+
761+ spec_content = _INIT_SPEC_TEMPLATE .format (name = name , lifecycle = lifecycle )
762+ out_path .write_text (spec_content )
763+
764+ click .echo (f"\n Created { out_path } \n " )
765+ click .echo ("Next steps:\n " )
766+ click .echo (" 1. Start a local Postgres + pgmq instance:" )
767+ click .echo (" docker compose up -d db\n " )
768+ click .echo (" 2. Boot your world:" )
769+ click .echo (f" netengine up { out_path } \n " )
770+ click .echo (" 3. Check status at any time:" )
771+ click .echo (" netengine status\n " )
772+ click .echo (" 4. Tear down when done:" )
773+ click .echo (" netengine down\n " )
774+ click .echo (f"Edit { out_path } to add orgs, ANDs, mail, storage, and more." )
775+ click .echo ("See examples/ for reference specs." )
776+
777+
585778if __name__ == "__main__" :
586779 cli ()
0 commit comments