language-operator 0.1.81 → 0.1.82
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.
- checksums.yaml +4 -4
- data/.claude/agents/README.md +16 -0
- data/.claude/agents/changelog-writer.md +181 -0
- data/.claude/hooks/README.md +43 -0
- data/.claude/hooks/node_modules/.bin/esbuild +1 -0
- data/.claude/hooks/node_modules/.bin/tsc +1 -0
- data/.claude/hooks/node_modules/.bin/tsserver +1 -0
- data/.claude/hooks/node_modules/.bin/tsx +1 -0
- data/.claude/hooks/node_modules/.package-lock.json +136 -0
- data/.claude/hooks/node_modules/@esbuild/linux-x64/README.md +3 -0
- data/.claude/hooks/node_modules/@esbuild/linux-x64/bin/esbuild +0 -0
- data/.claude/hooks/node_modules/@esbuild/linux-x64/package.json +20 -0
- data/.claude/hooks/node_modules/@types/node/LICENSE +21 -0
- data/.claude/hooks/node_modules/@types/node/README.md +15 -0
- data/.claude/hooks/node_modules/@types/node/assert/strict.d.ts +105 -0
- data/.claude/hooks/node_modules/@types/node/assert.d.ts +955 -0
- data/.claude/hooks/node_modules/@types/node/async_hooks.d.ts +623 -0
- data/.claude/hooks/node_modules/@types/node/buffer.buffer.d.ts +466 -0
- data/.claude/hooks/node_modules/@types/node/buffer.d.ts +1810 -0
- data/.claude/hooks/node_modules/@types/node/child_process.d.ts +1428 -0
- data/.claude/hooks/node_modules/@types/node/cluster.d.ts +486 -0
- data/.claude/hooks/node_modules/@types/node/compatibility/iterators.d.ts +21 -0
- data/.claude/hooks/node_modules/@types/node/console.d.ts +151 -0
- data/.claude/hooks/node_modules/@types/node/constants.d.ts +20 -0
- data/.claude/hooks/node_modules/@types/node/crypto.d.ts +4065 -0
- data/.claude/hooks/node_modules/@types/node/dgram.d.ts +564 -0
- data/.claude/hooks/node_modules/@types/node/diagnostics_channel.d.ts +576 -0
- data/.claude/hooks/node_modules/@types/node/dns/promises.d.ts +503 -0
- data/.claude/hooks/node_modules/@types/node/dns.d.ts +922 -0
- data/.claude/hooks/node_modules/@types/node/domain.d.ts +166 -0
- data/.claude/hooks/node_modules/@types/node/events.d.ts +1054 -0
- data/.claude/hooks/node_modules/@types/node/fs/promises.d.ts +1316 -0
- data/.claude/hooks/node_modules/@types/node/fs.d.ts +4676 -0
- data/.claude/hooks/node_modules/@types/node/globals.d.ts +150 -0
- data/.claude/hooks/node_modules/@types/node/globals.typedarray.d.ts +101 -0
- data/.claude/hooks/node_modules/@types/node/http.d.ts +2143 -0
- data/.claude/hooks/node_modules/@types/node/http2.d.ts +2480 -0
- data/.claude/hooks/node_modules/@types/node/https.d.ts +399 -0
- data/.claude/hooks/node_modules/@types/node/index.d.ts +115 -0
- data/.claude/hooks/node_modules/@types/node/inspector/promises.d.ts +41 -0
- data/.claude/hooks/node_modules/@types/node/inspector.d.ts +224 -0
- data/.claude/hooks/node_modules/@types/node/inspector.generated.d.ts +4226 -0
- data/.claude/hooks/node_modules/@types/node/module.d.ts +819 -0
- data/.claude/hooks/node_modules/@types/node/net.d.ts +933 -0
- data/.claude/hooks/node_modules/@types/node/os.d.ts +507 -0
- data/.claude/hooks/node_modules/@types/node/package.json +155 -0
- data/.claude/hooks/node_modules/@types/node/path/posix.d.ts +8 -0
- data/.claude/hooks/node_modules/@types/node/path/win32.d.ts +8 -0
- data/.claude/hooks/node_modules/@types/node/path.d.ts +187 -0
- data/.claude/hooks/node_modules/@types/node/perf_hooks.d.ts +621 -0
- data/.claude/hooks/node_modules/@types/node/process.d.ts +2097 -0
- data/.claude/hooks/node_modules/@types/node/punycode.d.ts +117 -0
- data/.claude/hooks/node_modules/@types/node/querystring.d.ts +152 -0
- data/.claude/hooks/node_modules/@types/node/quic.d.ts +910 -0
- data/.claude/hooks/node_modules/@types/node/readline/promises.d.ts +161 -0
- data/.claude/hooks/node_modules/@types/node/readline.d.ts +541 -0
- data/.claude/hooks/node_modules/@types/node/repl.d.ts +415 -0
- data/.claude/hooks/node_modules/@types/node/sea.d.ts +162 -0
- data/.claude/hooks/node_modules/@types/node/sqlite.d.ts +937 -0
- data/.claude/hooks/node_modules/@types/node/stream/consumers.d.ts +38 -0
- data/.claude/hooks/node_modules/@types/node/stream/promises.d.ts +211 -0
- data/.claude/hooks/node_modules/@types/node/stream/web.d.ts +296 -0
- data/.claude/hooks/node_modules/@types/node/stream.d.ts +1760 -0
- data/.claude/hooks/node_modules/@types/node/string_decoder.d.ts +67 -0
- data/.claude/hooks/node_modules/@types/node/test/reporters.d.ts +96 -0
- data/.claude/hooks/node_modules/@types/node/test.d.ts +2239 -0
- data/.claude/hooks/node_modules/@types/node/timers/promises.d.ts +108 -0
- data/.claude/hooks/node_modules/@types/node/timers.d.ts +159 -0
- data/.claude/hooks/node_modules/@types/node/tls.d.ts +1194 -0
- data/.claude/hooks/node_modules/@types/node/trace_events.d.ts +197 -0
- data/.claude/hooks/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +462 -0
- data/.claude/hooks/node_modules/@types/node/ts5.6/compatibility/float16array.d.ts +71 -0
- data/.claude/hooks/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +36 -0
- data/.claude/hooks/node_modules/@types/node/ts5.6/index.d.ts +117 -0
- data/.claude/hooks/node_modules/@types/node/ts5.7/compatibility/float16array.d.ts +72 -0
- data/.claude/hooks/node_modules/@types/node/ts5.7/index.d.ts +117 -0
- data/.claude/hooks/node_modules/@types/node/tty.d.ts +250 -0
- data/.claude/hooks/node_modules/@types/node/url.d.ts +519 -0
- data/.claude/hooks/node_modules/@types/node/util/types.d.ts +558 -0
- data/.claude/hooks/node_modules/@types/node/util.d.ts +1653 -0
- data/.claude/hooks/node_modules/@types/node/v8.d.ts +979 -0
- data/.claude/hooks/node_modules/@types/node/vm.d.ts +1180 -0
- data/.claude/hooks/node_modules/@types/node/wasi.d.ts +202 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/abortcontroller.d.ts +59 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/blob.d.ts +23 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/console.d.ts +9 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/crypto.d.ts +39 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/encoding.d.ts +11 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/events.d.ts +106 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/fetch.d.ts +54 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/importmeta.d.ts +13 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/messaging.d.ts +23 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/navigator.d.ts +25 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/performance.d.ts +45 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/storage.d.ts +24 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/streams.d.ts +115 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/timers.d.ts +44 -0
- data/.claude/hooks/node_modules/@types/node/web-globals/url.d.ts +24 -0
- data/.claude/hooks/node_modules/@types/node/worker_threads.d.ts +714 -0
- data/.claude/hooks/node_modules/@types/node/zlib.d.ts +618 -0
- data/.claude/hooks/node_modules/esbuild/LICENSE.md +21 -0
- data/.claude/hooks/node_modules/esbuild/README.md +3 -0
- data/.claude/hooks/node_modules/esbuild/bin/esbuild +0 -0
- data/.claude/hooks/node_modules/esbuild/install.js +289 -0
- data/.claude/hooks/node_modules/esbuild/lib/main.d.ts +716 -0
- data/.claude/hooks/node_modules/esbuild/lib/main.js +2242 -0
- data/.claude/hooks/node_modules/esbuild/package.json +49 -0
- data/.claude/hooks/node_modules/get-tsconfig/LICENSE +21 -0
- data/.claude/hooks/node_modules/get-tsconfig/README.md +235 -0
- data/.claude/hooks/node_modules/get-tsconfig/dist/index.cjs +7 -0
- data/.claude/hooks/node_modules/get-tsconfig/dist/index.d.cts +2088 -0
- data/.claude/hooks/node_modules/get-tsconfig/dist/index.d.mts +2088 -0
- data/.claude/hooks/node_modules/get-tsconfig/dist/index.mjs +7 -0
- data/.claude/hooks/node_modules/get-tsconfig/package.json +46 -0
- data/.claude/hooks/node_modules/resolve-pkg-maps/LICENSE +21 -0
- data/.claude/hooks/node_modules/resolve-pkg-maps/README.md +216 -0
- data/.claude/hooks/node_modules/resolve-pkg-maps/dist/index.cjs +1 -0
- data/.claude/hooks/node_modules/resolve-pkg-maps/dist/index.d.cts +11 -0
- data/.claude/hooks/node_modules/resolve-pkg-maps/dist/index.d.mts +11 -0
- data/.claude/hooks/node_modules/resolve-pkg-maps/dist/index.mjs +1 -0
- data/.claude/hooks/node_modules/resolve-pkg-maps/package.json +42 -0
- data/.claude/hooks/node_modules/tsx/LICENSE +21 -0
- data/.claude/hooks/node_modules/tsx/README.md +32 -0
- data/.claude/hooks/node_modules/tsx/dist/cjs/api/index.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/cjs/api/index.d.cts +35 -0
- data/.claude/hooks/node_modules/tsx/dist/cjs/api/index.d.mts +35 -0
- data/.claude/hooks/node_modules/tsx/dist/cjs/api/index.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/cjs/index.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/cjs/index.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/cli.cjs +54 -0
- data/.claude/hooks/node_modules/tsx/dist/cli.mjs +55 -0
- data/.claude/hooks/node_modules/tsx/dist/client-BQVF1NaW.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/client-D6NvIMSC.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/esm/api/index.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/esm/api/index.d.cts +35 -0
- data/.claude/hooks/node_modules/tsx/dist/esm/api/index.d.mts +35 -0
- data/.claude/hooks/node_modules/tsx/dist/esm/api/index.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/esm/index.cjs +2 -0
- data/.claude/hooks/node_modules/tsx/dist/esm/index.mjs +2 -0
- data/.claude/hooks/node_modules/tsx/dist/get-pipe-path-BHW2eJdv.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/get-pipe-path-BoR10qr8.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/index-7AaEi15b.mjs +14 -0
- data/.claude/hooks/node_modules/tsx/dist/index-BWFBUo6r.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/index-gbaejti9.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/index-gckBtVBf.cjs +14 -0
- data/.claude/hooks/node_modules/tsx/dist/lexer-DQCqS3nf.mjs +3 -0
- data/.claude/hooks/node_modules/tsx/dist/lexer-DgIbo0BU.cjs +3 -0
- data/.claude/hooks/node_modules/tsx/dist/loader.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/loader.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/node-features-_8ZFwP_x.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/node-features-roYmp9jK.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/package-CeBgXWuR.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/package-Dxt5kIHw.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/patch-repl.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/patch-repl.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/preflight.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/preflight.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/register-2sWVXuRQ.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/register-B7jrtLTO.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/register-CFH5oNdT.mjs +4 -0
- data/.claude/hooks/node_modules/tsx/dist/register-D46fvsV_.cjs +4 -0
- data/.claude/hooks/node_modules/tsx/dist/repl.cjs +3 -0
- data/.claude/hooks/node_modules/tsx/dist/repl.mjs +3 -0
- data/.claude/hooks/node_modules/tsx/dist/require-D4F1Lv60.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/require-DQxpCAr4.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/suppress-warnings.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/suppress-warnings.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/temporary-directory-B83uKxJF.cjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/temporary-directory-CwHp0_NW.mjs +1 -0
- data/.claude/hooks/node_modules/tsx/dist/types-Cxp8y2TL.d.ts +5 -0
- data/.claude/hooks/node_modules/tsx/package.json +68 -0
- data/.claude/hooks/node_modules/typescript/LICENSE.txt +55 -0
- data/.claude/hooks/node_modules/typescript/README.md +50 -0
- data/.claude/hooks/node_modules/typescript/SECURITY.md +41 -0
- data/.claude/hooks/node_modules/typescript/ThirdPartyNoticeText.txt +193 -0
- data/.claude/hooks/node_modules/typescript/bin/tsc +2 -0
- data/.claude/hooks/node_modules/typescript/bin/tsserver +2 -0
- data/.claude/hooks/node_modules/typescript/lib/_tsc.js +133818 -0
- data/.claude/hooks/node_modules/typescript/lib/_tsserver.js +659 -0
- data/.claude/hooks/node_modules/typescript/lib/_typingsInstaller.js +222 -0
- data/.claude/hooks/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/de/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/es/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/it/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.d.ts +22 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.decorators.d.ts +384 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.decorators.legacy.d.ts +22 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +41 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.dom.d.ts +39429 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.dom.iterable.d.ts +571 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.collection.d.ts +147 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.core.d.ts +597 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.d.ts +28 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.generator.d.ts +77 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.iterable.d.ts +605 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.promise.d.ts +81 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.proxy.d.ts +128 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.reflect.d.ts +144 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.symbol.d.ts +46 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +326 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2016.array.include.d.ts +116 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2016.d.ts +21 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2016.full.d.ts +23 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2016.intl.d.ts +31 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +21 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.d.ts +26 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.date.d.ts +31 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.full.d.ts +23 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.intl.d.ts +44 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.object.d.ts +49 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +135 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.string.d.ts +45 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +53 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +77 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +53 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2018.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2018.full.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2018.intl.d.ts +83 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2018.promise.d.ts +30 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2018.regexp.d.ts +37 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2019.array.d.ts +79 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2019.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2019.full.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2019.intl.d.ts +23 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2019.object.d.ts +33 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2019.string.d.ts +37 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2019.symbol.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.bigint.d.ts +765 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.d.ts +27 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.date.d.ts +42 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.full.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.intl.d.ts +474 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.number.d.ts +28 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.promise.d.ts +47 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +99 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.string.d.ts +44 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +41 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2021.d.ts +23 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2021.full.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2021.intl.d.ts +166 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2021.promise.d.ts +48 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2021.string.d.ts +33 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2021.weakref.d.ts +78 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2022.array.d.ts +121 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2022.d.ts +25 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2022.error.d.ts +75 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2022.full.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2022.intl.d.ts +145 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2022.object.d.ts +26 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2022.regexp.d.ts +39 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2022.string.d.ts +25 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2023.array.d.ts +924 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2023.collection.d.ts +21 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2023.d.ts +22 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2023.full.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2023.intl.d.ts +56 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +65 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.collection.d.ts +29 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.d.ts +26 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.full.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.object.d.ts +29 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.promise.d.ts +35 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.regexp.d.ts +25 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +68 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es2024.string.d.ts +29 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es5.d.ts +4601 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.es6.d.ts +23 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.array.d.ts +35 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.collection.d.ts +96 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.d.ts +29 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.decorators.d.ts +28 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.disposable.d.ts +193 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.error.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.float16.d.ts +445 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.full.d.ts +24 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.intl.d.ts +21 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.iterator.d.ts +148 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.promise.d.ts +34 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +25 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.scripthost.d.ts +322 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +41 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.webworker.d.ts +13150 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +23 -0
- data/.claude/hooks/node_modules/typescript/lib/lib.webworker.iterable.d.ts +340 -0
- data/.claude/hooks/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/tsc.js +8 -0
- data/.claude/hooks/node_modules/typescript/lib/tsserver.js +8 -0
- data/.claude/hooks/node_modules/typescript/lib/tsserverlibrary.d.ts +17 -0
- data/.claude/hooks/node_modules/typescript/lib/tsserverlibrary.js +21 -0
- data/.claude/hooks/node_modules/typescript/lib/typesMap.json +497 -0
- data/.claude/hooks/node_modules/typescript/lib/typescript.d.ts +11437 -0
- data/.claude/hooks/node_modules/typescript/lib/typescript.js +200276 -0
- data/.claude/hooks/node_modules/typescript/lib/typingsInstaller.js +8 -0
- data/.claude/hooks/node_modules/typescript/lib/watchGuard.js +53 -0
- data/.claude/hooks/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +2122 -0
- data/.claude/hooks/node_modules/typescript/package.json +120 -0
- data/.claude/hooks/node_modules/undici-types/LICENSE +21 -0
- data/.claude/hooks/node_modules/undici-types/README.md +6 -0
- data/.claude/hooks/node_modules/undici-types/agent.d.ts +32 -0
- data/.claude/hooks/node_modules/undici-types/api.d.ts +43 -0
- data/.claude/hooks/node_modules/undici-types/balanced-pool.d.ts +29 -0
- data/.claude/hooks/node_modules/undici-types/cache-interceptor.d.ts +172 -0
- data/.claude/hooks/node_modules/undici-types/cache.d.ts +36 -0
- data/.claude/hooks/node_modules/undici-types/client-stats.d.ts +15 -0
- data/.claude/hooks/node_modules/undici-types/client.d.ts +108 -0
- data/.claude/hooks/node_modules/undici-types/connector.d.ts +34 -0
- data/.claude/hooks/node_modules/undici-types/content-type.d.ts +21 -0
- data/.claude/hooks/node_modules/undici-types/cookies.d.ts +30 -0
- data/.claude/hooks/node_modules/undici-types/diagnostics-channel.d.ts +74 -0
- data/.claude/hooks/node_modules/undici-types/dispatcher.d.ts +276 -0
- data/.claude/hooks/node_modules/undici-types/env-http-proxy-agent.d.ts +22 -0
- data/.claude/hooks/node_modules/undici-types/errors.d.ts +161 -0
- data/.claude/hooks/node_modules/undici-types/eventsource.d.ts +66 -0
- data/.claude/hooks/node_modules/undici-types/fetch.d.ts +211 -0
- data/.claude/hooks/node_modules/undici-types/formdata.d.ts +108 -0
- data/.claude/hooks/node_modules/undici-types/global-dispatcher.d.ts +9 -0
- data/.claude/hooks/node_modules/undici-types/global-origin.d.ts +7 -0
- data/.claude/hooks/node_modules/undici-types/h2c-client.d.ts +73 -0
- data/.claude/hooks/node_modules/undici-types/handlers.d.ts +15 -0
- data/.claude/hooks/node_modules/undici-types/header.d.ts +160 -0
- data/.claude/hooks/node_modules/undici-types/index.d.ts +80 -0
- data/.claude/hooks/node_modules/undici-types/interceptors.d.ts +39 -0
- data/.claude/hooks/node_modules/undici-types/mock-agent.d.ts +68 -0
- data/.claude/hooks/node_modules/undici-types/mock-call-history.d.ts +111 -0
- data/.claude/hooks/node_modules/undici-types/mock-client.d.ts +27 -0
- data/.claude/hooks/node_modules/undici-types/mock-errors.d.ts +12 -0
- data/.claude/hooks/node_modules/undici-types/mock-interceptor.d.ts +94 -0
- data/.claude/hooks/node_modules/undici-types/mock-pool.d.ts +27 -0
- data/.claude/hooks/node_modules/undici-types/package.json +55 -0
- data/.claude/hooks/node_modules/undici-types/patch.d.ts +29 -0
- data/.claude/hooks/node_modules/undici-types/pool-stats.d.ts +19 -0
- data/.claude/hooks/node_modules/undici-types/pool.d.ts +41 -0
- data/.claude/hooks/node_modules/undici-types/proxy-agent.d.ts +29 -0
- data/.claude/hooks/node_modules/undici-types/readable.d.ts +68 -0
- data/.claude/hooks/node_modules/undici-types/retry-agent.d.ts +8 -0
- data/.claude/hooks/node_modules/undici-types/retry-handler.d.ts +125 -0
- data/.claude/hooks/node_modules/undici-types/snapshot-agent.d.ts +109 -0
- data/.claude/hooks/node_modules/undici-types/util.d.ts +18 -0
- data/.claude/hooks/node_modules/undici-types/utility.d.ts +7 -0
- data/.claude/hooks/node_modules/undici-types/webidl.d.ts +341 -0
- data/.claude/hooks/node_modules/undici-types/websocket.d.ts +186 -0
- data/.claude/hooks/package-lock.json +562 -0
- data/.claude/hooks/package.json +19 -0
- data/.claude/hooks/skill-activation-prompt.sh +11 -0
- data/.claude/hooks/skill-activation-prompt.ts +185 -0
- data/.claude/hooks/tsconfig.json +12 -0
- data/.claude/settings.json +33 -0
- data/.claude/skills/README.md +29 -0
- data/.claude/skills/ruby-gem-development/SKILL.md +293 -0
- data/.claude/skills/skill-rules.json +117 -0
- data/.claude/skills/thor-cli-development/SKILL.md +431 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile.lock +3 -1
- data/completions/{_aictl → _langop} +37 -37
- data/completions/{aictl.bash → langop.bash} +16 -16
- data/completions/langop.fish +114 -0
- data/components/agent/Gemfile +1 -1
- data/components/tool/Gemfile +1 -1
- data/docs/README.md +10 -10
- data/docs/agent-internals.md +1 -1
- data/docs/cheat-sheet.md +76 -76
- data/docs/cli-reference.md +71 -71
- data/docs/how-agents-work.md +1 -1
- data/docs/installation.md +30 -30
- data/docs/quickstart.md +30 -30
- data/docs/using-tools.md +11 -11
- data/docs/webhooks.md +6 -6
- data/examples/ux_helpers_demo.rb +4 -4
- data/lib/language_operator/agent/task_executor.rb +0 -37
- data/lib/language_operator/agent/web_server.rb +311 -323
- data/lib/language_operator/cli/commands/agent/base.rb +16 -15
- data/lib/language_operator/cli/commands/agent/code_operations.rb +6 -6
- data/lib/language_operator/cli/commands/agent/helpers/synthesis_watcher.rb +1 -1
- data/lib/language_operator/cli/commands/agent/lifecycle.rb +2 -2
- data/lib/language_operator/cli/commands/agent/logs.rb +2 -2
- data/lib/language_operator/cli/commands/agent/workspace.rb +8 -8
- data/lib/language_operator/cli/commands/cluster.rb +172 -158
- data/lib/language_operator/cli/commands/install.rb +878 -81
- data/lib/language_operator/cli/commands/model/base.rb +7 -6
- data/lib/language_operator/cli/commands/organization.rb +61 -0
- data/lib/language_operator/cli/commands/persona.rb +10 -8
- data/lib/language_operator/cli/commands/status.rb +83 -13
- data/lib/language_operator/cli/commands/system/exec.rb +10 -10
- data/lib/language_operator/cli/commands/system/schema.rb +6 -6
- data/lib/language_operator/cli/commands/system/synthesis_template.rb +6 -6
- data/lib/language_operator/cli/commands/system/synthesize.rb +14 -14
- data/lib/language_operator/cli/commands/system/validate_template.rb +4 -4
- data/lib/language_operator/cli/commands/tool/base.rb +1 -1
- data/lib/language_operator/cli/commands/tool/install.rb +2 -2
- data/lib/language_operator/cli/commands/tool/search.rb +3 -3
- data/lib/language_operator/cli/commands/ui.rb +173 -0
- data/lib/language_operator/cli/commands/use.rb +80 -9
- data/lib/language_operator/cli/errors/suggestions.rb +21 -21
- data/lib/language_operator/cli/formatters/progress_formatter.rb +2 -1
- data/lib/language_operator/cli/formatters/table_formatter.rb +21 -1
- data/lib/language_operator/cli/helpers/cluster_validator.rb +2 -2
- data/lib/language_operator/cli/helpers/health_checker.rb +263 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +3 -3
- data/lib/language_operator/cli/helpers/ux_helper.rb +4 -3
- data/lib/language_operator/cli/main.rb +30 -21
- data/lib/language_operator/cli/wizards/model_wizard.rb +7 -5
- data/lib/language_operator/config/cluster_config.rb +3 -3
- data/lib/language_operator/constants/kubernetes_labels.rb +1 -1
- data/lib/language_operator/kubernetes/client.rb +53 -109
- data/lib/language_operator/kubernetes/resource_builder.rb +46 -14
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/utils/org_context.rb +100 -0
- data/lib/language_operator/version.rb +1 -1
- data/synth/001/Makefile +1 -1
- data/synth/002/Makefile +1 -1
- data/synth/003/Makefile +1 -1
- data/synth/004/Makefile +1 -1
- metadata +384 -11
- data/completions/aictl.fish +0 -114
- data/lib/language_operator/agent/event_config.rb +0 -172
- data/lib/language_operator/cli/commands/quickstart.rb +0 -22
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +0 -561
- /data/bin/{aictl → langop} +0 -0
|
@@ -15,7 +15,7 @@ module LanguageOperator
|
|
|
15
15
|
HELM_REPO_URL = 'https://language-operator.github.io/charts'
|
|
16
16
|
CHART_NAME = 'language-operator/language-operator'
|
|
17
17
|
RELEASE_NAME = 'language-operator'
|
|
18
|
-
DEFAULT_NAMESPACE = 'language-operator
|
|
18
|
+
DEFAULT_NAMESPACE = 'language-operator'
|
|
19
19
|
|
|
20
20
|
# Long descriptions for commands
|
|
21
21
|
LONG_DESCRIPTIONS = {
|
|
@@ -30,16 +30,16 @@ module LanguageOperator
|
|
|
30
30
|
|
|
31
31
|
Examples:
|
|
32
32
|
# Install with defaults
|
|
33
|
-
|
|
33
|
+
langop install
|
|
34
34
|
|
|
35
35
|
# Install with custom values
|
|
36
|
-
|
|
36
|
+
langop install --values my-values.yaml
|
|
37
37
|
|
|
38
38
|
# Install specific version
|
|
39
|
-
|
|
39
|
+
langop install --version 0.1.0
|
|
40
40
|
|
|
41
41
|
# Dry run to see what would be installed
|
|
42
|
-
|
|
42
|
+
langop install --dry-run
|
|
43
43
|
DESC
|
|
44
44
|
upgrade: <<-DESC,
|
|
45
45
|
Upgrade the language-operator to a newer version using Helm.
|
|
@@ -52,29 +52,33 @@ module LanguageOperator
|
|
|
52
52
|
|
|
53
53
|
Examples:
|
|
54
54
|
# Upgrade to latest version
|
|
55
|
-
|
|
55
|
+
langop upgrade
|
|
56
56
|
|
|
57
57
|
# Upgrade with custom values
|
|
58
|
-
|
|
58
|
+
langop upgrade --values my-values.yaml
|
|
59
59
|
|
|
60
60
|
# Upgrade to specific version
|
|
61
|
-
|
|
61
|
+
langop upgrade --version 0.2.0
|
|
62
62
|
DESC
|
|
63
63
|
uninstall: <<-DESC
|
|
64
|
-
|
|
64
|
+
Completely uninstall the language-operator from your Kubernetes cluster.
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
This will:
|
|
67
|
+
1. Delete all custom resources (agents, tools, models, personas, clusters)
|
|
68
|
+
2. Uninstall the operator using Helm
|
|
69
|
+
3. Remove all CRDs (CustomResourceDefinitions)
|
|
70
|
+
|
|
71
|
+
WARNING: This will completely remove all language-operator resources from the cluster.
|
|
68
72
|
|
|
69
73
|
Examples:
|
|
70
|
-
#
|
|
71
|
-
|
|
74
|
+
# Complete uninstall with confirmation
|
|
75
|
+
langop uninstall
|
|
72
76
|
|
|
73
77
|
# Force uninstall without confirmation
|
|
74
|
-
|
|
78
|
+
langop uninstall --force
|
|
75
79
|
|
|
76
80
|
# Uninstall from specific namespace
|
|
77
|
-
|
|
81
|
+
langop uninstall --namespace my-namespace
|
|
78
82
|
DESC
|
|
79
83
|
}.freeze
|
|
80
84
|
|
|
@@ -95,16 +99,16 @@ module LanguageOperator
|
|
|
95
99
|
|
|
96
100
|
Examples:
|
|
97
101
|
# Install with defaults
|
|
98
|
-
|
|
102
|
+
langop install
|
|
99
103
|
|
|
100
104
|
# Install with custom values
|
|
101
|
-
|
|
105
|
+
langop install --values my-values.yaml
|
|
102
106
|
|
|
103
107
|
# Install specific version
|
|
104
|
-
|
|
108
|
+
langop install --version 0.1.0
|
|
105
109
|
|
|
106
110
|
# Dry run to see what would be installed
|
|
107
|
-
|
|
111
|
+
langop install --dry-run
|
|
108
112
|
DESC
|
|
109
113
|
option :values, type: :string, desc: 'Path to custom Helm values file'
|
|
110
114
|
option :namespace, type: :string, default: DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
|
|
@@ -112,6 +116,7 @@ module LanguageOperator
|
|
|
112
116
|
option :dry_run, type: :boolean, default: false, desc: 'Preview installation without applying'
|
|
113
117
|
option :wait, type: :boolean, default: true, desc: 'Wait for deployment to complete'
|
|
114
118
|
option :create_namespace, type: :boolean, default: true, desc: 'Create namespace if it does not exist'
|
|
119
|
+
option :non_interactive, type: :boolean, default: false, desc: 'Skip interactive prompts and use defaults'
|
|
115
120
|
def install
|
|
116
121
|
handle_command_error('install') do
|
|
117
122
|
# Check if helm is available
|
|
@@ -122,25 +127,33 @@ module LanguageOperator
|
|
|
122
127
|
Formatters::ProgressFormatter.warn('Language operator is already installed')
|
|
123
128
|
puts
|
|
124
129
|
puts 'To upgrade, use:'
|
|
125
|
-
puts '
|
|
130
|
+
puts ' langop upgrade'
|
|
126
131
|
return
|
|
127
132
|
end
|
|
128
133
|
|
|
129
134
|
namespace = options[:namespace]
|
|
130
135
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
# Interactive configuration unless non-interactive mode or custom values provided
|
|
137
|
+
unless options[:non_interactive] || options[:values]
|
|
138
|
+
logo(title: 'language operator installer')
|
|
139
|
+
puts 'This installer will help you configure the Language Operator for your cluster.'
|
|
140
|
+
puts
|
|
141
|
+
|
|
142
|
+
config = collect_interactive_configuration
|
|
143
|
+
values_file = generate_values_file(config)
|
|
144
|
+
# Create a mutable copy of options to avoid frozen hash error
|
|
145
|
+
@options = options.dup
|
|
146
|
+
@options[:values] = values_file
|
|
147
|
+
end
|
|
135
148
|
|
|
136
149
|
# Add Helm repository
|
|
137
|
-
add_helm_repo unless options[:dry_run]
|
|
150
|
+
add_helm_repo unless (@options || options)[:dry_run]
|
|
138
151
|
|
|
139
|
-
# Build helm install command
|
|
152
|
+
# Build helm install command (use @options if we created a mutable copy)
|
|
140
153
|
cmd = build_helm_command('install', namespace)
|
|
141
154
|
|
|
142
155
|
# Execute helm install
|
|
143
|
-
if options[:dry_run]
|
|
156
|
+
if (@options || options)[:dry_run]
|
|
144
157
|
puts 'Dry run - would execute:'
|
|
145
158
|
puts " #{cmd}"
|
|
146
159
|
puts
|
|
@@ -152,12 +165,8 @@ module LanguageOperator
|
|
|
152
165
|
raise "Helm install failed: #{output}" unless success
|
|
153
166
|
end
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
puts 'Next steps:'
|
|
158
|
-
puts ' 1. Create a cluster: aictl cluster create my-cluster'
|
|
159
|
-
puts ' 2. Create a model: aictl model create gpt4 --provider openai --model gpt-4-turbo'
|
|
160
|
-
puts ' 3. Create an agent: aictl agent create "your agent description"'
|
|
168
|
+
# Post-installation verification
|
|
169
|
+
verify_installation(config)
|
|
161
170
|
end
|
|
162
171
|
end
|
|
163
172
|
end
|
|
@@ -174,13 +183,13 @@ module LanguageOperator
|
|
|
174
183
|
|
|
175
184
|
Examples:
|
|
176
185
|
# Upgrade to latest version
|
|
177
|
-
|
|
186
|
+
langop upgrade
|
|
178
187
|
|
|
179
188
|
# Upgrade with custom values
|
|
180
|
-
|
|
189
|
+
langop upgrade --values my-values.yaml
|
|
181
190
|
|
|
182
191
|
# Upgrade to specific version
|
|
183
|
-
|
|
192
|
+
langop upgrade --version 0.2.0
|
|
184
193
|
DESC
|
|
185
194
|
option :values, type: :string, desc: 'Path to custom Helm values file'
|
|
186
195
|
option :namespace, type: :string, default: DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
|
|
@@ -197,17 +206,12 @@ module LanguageOperator
|
|
|
197
206
|
Formatters::ProgressFormatter.error('Language operator is not installed')
|
|
198
207
|
puts
|
|
199
208
|
puts 'To install, use:'
|
|
200
|
-
puts '
|
|
209
|
+
puts ' langop install'
|
|
201
210
|
exit 1
|
|
202
211
|
end
|
|
203
212
|
|
|
204
213
|
namespace = options[:namespace]
|
|
205
214
|
|
|
206
|
-
puts 'Upgrading language-operator...'
|
|
207
|
-
puts " Namespace: #{namespace}"
|
|
208
|
-
puts " Chart: #{CHART_NAME}"
|
|
209
|
-
puts
|
|
210
|
-
|
|
211
215
|
# Update Helm repository
|
|
212
216
|
update_helm_repo unless options[:dry_run]
|
|
213
217
|
|
|
@@ -227,27 +231,32 @@ module LanguageOperator
|
|
|
227
231
|
raise "Helm upgrade failed: #{output}" unless success
|
|
228
232
|
end
|
|
229
233
|
|
|
230
|
-
|
|
234
|
+
# Restart the language-operator deployment to ensure new version is running
|
|
235
|
+
restart_language_operator_deployment(namespace)
|
|
231
236
|
end
|
|
232
237
|
end
|
|
233
238
|
end
|
|
234
239
|
|
|
235
240
|
desc 'uninstall', 'Uninstall the language-operator using Helm'
|
|
236
241
|
long_desc <<-DESC
|
|
237
|
-
|
|
242
|
+
Completely uninstall the language-operator from your Kubernetes cluster.
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
|
|
244
|
+
This will:
|
|
245
|
+
1. Delete all custom resources (agents, tools, models, personas, clusters)
|
|
246
|
+
2. Uninstall the operator using Helm
|
|
247
|
+
3. Remove all CRDs (CustomResourceDefinitions)
|
|
248
|
+
|
|
249
|
+
WARNING: This will completely remove all language-operator resources from the cluster.
|
|
241
250
|
|
|
242
251
|
Examples:
|
|
243
|
-
#
|
|
244
|
-
|
|
252
|
+
# Complete uninstall with confirmation
|
|
253
|
+
langop uninstall
|
|
245
254
|
|
|
246
255
|
# Force uninstall without confirmation
|
|
247
|
-
|
|
256
|
+
langop uninstall --force
|
|
248
257
|
|
|
249
258
|
# Uninstall from specific namespace
|
|
250
|
-
|
|
259
|
+
langop uninstall --namespace my-namespace
|
|
251
260
|
DESC
|
|
252
261
|
option :namespace, type: :string, default: DEFAULT_NAMESPACE, desc: 'Kubernetes namespace'
|
|
253
262
|
option :force, type: :boolean, default: false, desc: 'Skip confirmation prompt'
|
|
@@ -256,41 +265,53 @@ module LanguageOperator
|
|
|
256
265
|
# Check if helm is available
|
|
257
266
|
check_helm_installed!
|
|
258
267
|
|
|
259
|
-
# Check if operator is installed
|
|
260
|
-
unless operator_installed?
|
|
261
|
-
Formatters::ProgressFormatter.warn('Language operator is not installed')
|
|
262
|
-
return
|
|
263
|
-
end
|
|
268
|
+
# Check if operator is installed but proceed with cleanup regardless
|
|
264
269
|
|
|
265
270
|
namespace = options[:namespace]
|
|
266
271
|
|
|
267
272
|
# Confirm deletion unless --force
|
|
268
273
|
unless options[:force]
|
|
269
|
-
puts
|
|
274
|
+
puts 'This will completely uninstall the language-operator from your cluster:'
|
|
270
275
|
puts
|
|
271
|
-
puts '
|
|
272
|
-
puts ' -
|
|
273
|
-
puts ' -
|
|
274
|
-
puts ' -
|
|
275
|
-
puts ' -
|
|
276
|
-
puts
|
|
276
|
+
puts ' - The language-operator Helm release'
|
|
277
|
+
puts ' - All existing clusters, agents, models, tools and personas'
|
|
278
|
+
puts ' - All organization namespaces and their resources'
|
|
279
|
+
puts ' - Persistent volumes (ClickHouse, PostgreSQL data)'
|
|
280
|
+
puts ' - Cluster-scoped resources (RBAC, webhooks, CRDs)'
|
|
281
|
+
puts " - The operator namespace (#{namespace})"
|
|
277
282
|
puts
|
|
278
|
-
|
|
283
|
+
puts "#{pastel.bold.red('WARNING')}: #{pastel.white.bold('This action cannot be undone!')}"
|
|
284
|
+
puts
|
|
285
|
+
return unless CLI::Helpers::UserPrompts.confirm('Continue with complete uninstall?')
|
|
279
286
|
end
|
|
280
287
|
|
|
281
|
-
#
|
|
282
|
-
|
|
288
|
+
# Step 1: Delete all custom resources
|
|
289
|
+
delete_all_custom_resources
|
|
283
290
|
|
|
284
|
-
#
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
291
|
+
# Step 2: Delete organization namespaces
|
|
292
|
+
delete_organization_namespaces
|
|
293
|
+
|
|
294
|
+
# Step 3: Uninstall Helm release if it exists
|
|
295
|
+
if operator_installed?
|
|
296
|
+
cmd = "helm uninstall #{RELEASE_NAME} --namespace #{namespace}"
|
|
297
|
+
|
|
298
|
+
Formatters::ProgressFormatter.with_spinner('Uninstalling language-operator Helm release') do
|
|
299
|
+
success, output = run_helm_command(cmd)
|
|
300
|
+
raise "Helm uninstall failed: #{output}" unless success
|
|
301
|
+
end
|
|
288
302
|
end
|
|
289
303
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
304
|
+
# Step 4: Delete PVCs
|
|
305
|
+
delete_persistent_volumes
|
|
306
|
+
|
|
307
|
+
# Step 5: Clean up cluster-scoped resources
|
|
308
|
+
delete_cluster_scoped_resources
|
|
309
|
+
|
|
310
|
+
# Step 6: Delete CRDs
|
|
311
|
+
delete_language_operator_crds
|
|
312
|
+
|
|
313
|
+
# Step 7: Delete the operator namespace
|
|
314
|
+
delete_operator_namespace(namespace)
|
|
294
315
|
end
|
|
295
316
|
end
|
|
296
317
|
|
|
@@ -358,28 +379,35 @@ module LanguageOperator
|
|
|
358
379
|
end
|
|
359
380
|
|
|
360
381
|
def build_helm_command(action, namespace)
|
|
382
|
+
# Use @options if available (from interactive mode), otherwise use options
|
|
383
|
+
opts = @options || options
|
|
384
|
+
|
|
361
385
|
cmd = ['helm', action, RELEASE_NAME]
|
|
362
386
|
|
|
363
|
-
# Add chart name for install
|
|
364
|
-
cmd << CHART_NAME if action
|
|
387
|
+
# Add chart name for install and upgrade
|
|
388
|
+
cmd << CHART_NAME if %w[install upgrade].include?(action)
|
|
365
389
|
|
|
366
390
|
# Add namespace
|
|
367
391
|
cmd << '--namespace' << namespace
|
|
368
392
|
|
|
369
393
|
# Add create-namespace for install
|
|
370
|
-
cmd << '--create-namespace' if action == 'install' &&
|
|
394
|
+
cmd << '--create-namespace' if action == 'install' && opts[:create_namespace]
|
|
371
395
|
|
|
372
|
-
# Add values file
|
|
373
|
-
|
|
396
|
+
# Add values file or reuse existing values for upgrade
|
|
397
|
+
if opts[:values]
|
|
398
|
+
cmd << '--values' << opts[:values]
|
|
399
|
+
elsif action == 'upgrade'
|
|
400
|
+
cmd << '--reuse-values'
|
|
401
|
+
end
|
|
374
402
|
|
|
375
403
|
# Add version
|
|
376
|
-
cmd << '--version' <<
|
|
404
|
+
cmd << '--version' << opts[:version] if opts[:version]
|
|
377
405
|
|
|
378
406
|
# Add wait
|
|
379
|
-
cmd << '--wait' if
|
|
407
|
+
cmd << '--wait' if opts[:wait]
|
|
380
408
|
|
|
381
409
|
# Add dry-run
|
|
382
|
-
cmd << '--dry-run' if
|
|
410
|
+
cmd << '--dry-run' if opts[:dry_run]
|
|
383
411
|
|
|
384
412
|
cmd.join(' ')
|
|
385
413
|
end
|
|
@@ -389,6 +417,775 @@ module LanguageOperator
|
|
|
389
417
|
output = stdout + stderr
|
|
390
418
|
[status.success?, output.strip]
|
|
391
419
|
end
|
|
420
|
+
|
|
421
|
+
# Delete all language-operator custom resources from all namespaces
|
|
422
|
+
def delete_all_custom_resources
|
|
423
|
+
require_relative '../../kubernetes/client'
|
|
424
|
+
require_relative '../../constants'
|
|
425
|
+
|
|
426
|
+
k8s = Kubernetes::Client.new
|
|
427
|
+
|
|
428
|
+
resource_types = [
|
|
429
|
+
Constants::RESOURCE_AGENT,
|
|
430
|
+
Constants::RESOURCE_AGENT_VERSION,
|
|
431
|
+
Constants::RESOURCE_TOOL,
|
|
432
|
+
Constants::RESOURCE_MODEL,
|
|
433
|
+
Constants::RESOURCE_PERSONA,
|
|
434
|
+
'LanguageCluster'
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
resource_types.each do |resource_type|
|
|
438
|
+
begin
|
|
439
|
+
# Delete and wait for all resources of this type to be completely removed
|
|
440
|
+
Formatters::ProgressFormatter.with_spinner("Deleting all #{resource_type} resources") do
|
|
441
|
+
# First, get resources from all namespaces
|
|
442
|
+
resources = k8s.list_resources(resource_type, namespace: nil)
|
|
443
|
+
|
|
444
|
+
# Trigger deletion of each resource
|
|
445
|
+
resources.each do |resource|
|
|
446
|
+
name = resource.dig('metadata', 'name')
|
|
447
|
+
namespace = resource.dig('metadata', 'namespace')
|
|
448
|
+
|
|
449
|
+
begin
|
|
450
|
+
k8s.delete_resource(resource_type, name, namespace)
|
|
451
|
+
rescue StandardError => e
|
|
452
|
+
# Continue deleting other resources even if one fails
|
|
453
|
+
warn "Failed to delete #{resource_type} #{name}: #{e.message}" if ENV['DEBUG']
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Then wait for all resources of this type to be completely removed
|
|
458
|
+
wait_for_resources_deletion_inline(resource_type, k8s, timeout: 300)
|
|
459
|
+
end
|
|
460
|
+
rescue StandardError => e
|
|
461
|
+
# If API group doesn't exist (CRDs already deleted), show success anyway
|
|
462
|
+
if e.message.include?('404') || e.message.include?('Not Found')
|
|
463
|
+
puts " - #{resource_type} API not available (CRDs likely already deleted)" if ENV['DEBUG']
|
|
464
|
+
Formatters::ProgressFormatter.success("Deleting all #{resource_type} resources... done")
|
|
465
|
+
else
|
|
466
|
+
Formatters::ProgressFormatter.warn("Failed to delete #{resource_type} resources: #{e.message}")
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Delete language-operator CRDs
|
|
473
|
+
def delete_language_operator_crds
|
|
474
|
+
require_relative '../../kubernetes/client'
|
|
475
|
+
require_relative '../../constants'
|
|
476
|
+
|
|
477
|
+
k8s = Kubernetes::Client.new
|
|
478
|
+
|
|
479
|
+
Formatters::ProgressFormatter.with_spinner('Deleting language-operator CRDs') do
|
|
480
|
+
# Find all CRDs with langop.io domain (CRDs are in apiextensions.k8s.io/v1)
|
|
481
|
+
all_crds = k8s.list_resources('CustomResourceDefinition', namespace: nil, api_version: 'apiextensions.k8s.io/v1')
|
|
482
|
+
langop_crds = all_crds.select do |crd|
|
|
483
|
+
name = crd.dig('metadata', 'name')
|
|
484
|
+
name&.end_with?('.langop.io')
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
puts "Found #{langop_crds.length} langop.io CRDs to delete" if ENV['DEBUG']
|
|
488
|
+
|
|
489
|
+
langop_crds.each do |crd|
|
|
490
|
+
crd_name = crd.dig('metadata', 'name')
|
|
491
|
+
puts "Deleting CRD: #{crd_name}" if ENV['DEBUG']
|
|
492
|
+
begin
|
|
493
|
+
k8s.delete_resource('CustomResourceDefinition', crd_name, nil, 'apiextensions.k8s.io/v1')
|
|
494
|
+
puts "Successfully deleted CRD: #{crd_name}" if ENV['DEBUG']
|
|
495
|
+
rescue StandardError => e
|
|
496
|
+
# Continue deleting other CRDs even if one fails
|
|
497
|
+
puts "Failed to delete CRD #{crd_name}: #{e.message}"
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
puts 'No langop.io CRDs found to delete' if langop_crds.empty? && ENV.fetch('DEBUG', nil)
|
|
502
|
+
rescue StandardError => e
|
|
503
|
+
puts "Failed to list/delete langop.io CRDs: #{e.message}"
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Delete the operator namespace
|
|
508
|
+
def delete_operator_namespace(namespace)
|
|
509
|
+
require_relative '../../kubernetes/client'
|
|
510
|
+
|
|
511
|
+
k8s = Kubernetes::Client.new
|
|
512
|
+
|
|
513
|
+
Formatters::ProgressFormatter.with_spinner("Deleting operator namespace: #{namespace}") do
|
|
514
|
+
# Check if namespace exists before trying to delete
|
|
515
|
+
if k8s.namespace_exists?(namespace)
|
|
516
|
+
k8s.delete_resource('Namespace', namespace)
|
|
517
|
+
elsif ENV['DEBUG']
|
|
518
|
+
puts "Namespace #{namespace} doesn't exist, skipping"
|
|
519
|
+
end
|
|
520
|
+
rescue StandardError => e
|
|
521
|
+
warn "Failed to delete namespace #{namespace}: #{e.message}" if ENV['DEBUG']
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Restart the language-operator deployment to ensure new version is running
|
|
526
|
+
def restart_language_operator_deployment(namespace)
|
|
527
|
+
Formatters::ProgressFormatter.with_spinner('Restarting language-operator deployment') do
|
|
528
|
+
# Use kubectl rollout restart for the deployment
|
|
529
|
+
cmd = "kubectl rollout restart deployment/language-operator --namespace #{namespace}"
|
|
530
|
+
|
|
531
|
+
output = `#{cmd} 2>&1`
|
|
532
|
+
success = $?.success?
|
|
533
|
+
|
|
534
|
+
raise "Failed to restart deployment: #{output}" unless success
|
|
535
|
+
rescue StandardError => e
|
|
536
|
+
# Don't fail the entire upgrade if restart fails
|
|
537
|
+
warn "Warning: Could not restart language-operator deployment: #{e.message}"
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Collect interactive configuration from user
|
|
542
|
+
def collect_interactive_configuration
|
|
543
|
+
config = {}
|
|
544
|
+
|
|
545
|
+
# Generate a random password as default
|
|
546
|
+
require 'securerandom'
|
|
547
|
+
default_password = SecureRandom.alphanumeric(12)
|
|
548
|
+
|
|
549
|
+
puts pastel.white.bold('Create a Login')
|
|
550
|
+
config[:admin_name] = prompt.ask('Full Name:', default: 'Default')
|
|
551
|
+
config[:admin_email] = prompt.ask('Email:', default: 'admin@example.com')
|
|
552
|
+
config[:admin_password] = prompt.ask('Password:', default: default_password)
|
|
553
|
+
|
|
554
|
+
puts
|
|
555
|
+
puts pastel.white.bold('Configure a Gateway')
|
|
556
|
+
|
|
557
|
+
# Check if gateways are available first
|
|
558
|
+
gateways = get_available_gateways
|
|
559
|
+
|
|
560
|
+
if gateways.empty?
|
|
561
|
+
puts 'No gateways found in the cluster.'
|
|
562
|
+
puts 'You can configure gateway access later after creating a gateway resource.'
|
|
563
|
+
else
|
|
564
|
+
create_gateway = prompt.yes?('Do you want to configure a gateway for external access?')
|
|
565
|
+
config[:gateway] = collect_gateway_configuration if create_gateway
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
puts
|
|
569
|
+
# Check CNI support first to determine header color
|
|
570
|
+
cni_info = detect_cni_support
|
|
571
|
+
if cni_info[:supports_network_policies]
|
|
572
|
+
puts pastel.white.bold('Network Isolation')
|
|
573
|
+
elsif cni_info[:name] == 'Unknown'
|
|
574
|
+
puts pastel.yellow.bold('Warning: network isolation support not detected')
|
|
575
|
+
else
|
|
576
|
+
puts pastel.yellow.bold("Warning: #{cni_info[:name]} does not support network policies")
|
|
577
|
+
end
|
|
578
|
+
config[:network_isolation] = collect_network_isolation_configuration(cni_info)
|
|
579
|
+
|
|
580
|
+
puts
|
|
581
|
+
config
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Collect gateway configuration
|
|
585
|
+
def collect_gateway_configuration
|
|
586
|
+
gateway_config = {}
|
|
587
|
+
|
|
588
|
+
# Get available gateways (we know there are some since we checked before calling this)
|
|
589
|
+
gateways = get_available_gateways
|
|
590
|
+
|
|
591
|
+
# Create choices for the select menu
|
|
592
|
+
choices = gateways.map { |gw| { name: "#{gw[:name]} (#{gw[:namespace]})", value: gw } }
|
|
593
|
+
|
|
594
|
+
selected_gateway = prompt.select('Select a gateway:', choices)
|
|
595
|
+
gateway_config[:gateway_name] = selected_gateway[:name]
|
|
596
|
+
gateway_config[:gateway_namespace] = selected_gateway[:namespace]
|
|
597
|
+
|
|
598
|
+
gateway_config[:hostname] = prompt.ask('Hostname for the gateway:', default: 'langop.local')
|
|
599
|
+
gateway_config[:tls] = prompt.yes?('Enable TLS/HTTPS?')
|
|
600
|
+
|
|
601
|
+
gateway_config
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Get available gateways from the cluster
|
|
605
|
+
def get_available_gateways
|
|
606
|
+
require_relative '../../kubernetes/client'
|
|
607
|
+
|
|
608
|
+
begin
|
|
609
|
+
k8s = Kubernetes::Client.new
|
|
610
|
+
gateways = k8s.list_resources('Gateway', namespace: nil, api_version: 'gateway.networking.k8s.io/v1')
|
|
611
|
+
gateways.map do |gw|
|
|
612
|
+
{
|
|
613
|
+
name: gw.dig('metadata', 'name'),
|
|
614
|
+
namespace: gw.dig('metadata', 'namespace')
|
|
615
|
+
}
|
|
616
|
+
end.compact
|
|
617
|
+
rescue StandardError => e
|
|
618
|
+
warn "Failed to list gateways: #{e.message}" if ENV['DEBUG']
|
|
619
|
+
[]
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Collect network isolation configuration
|
|
624
|
+
def collect_network_isolation_configuration(cni_info)
|
|
625
|
+
if cni_info[:supports_network_policies]
|
|
626
|
+
enable_isolation = prompt.yes?("Enforce network isolation policies with #{cni_info[:name]}?")
|
|
627
|
+
{ enabled: enable_isolation }
|
|
628
|
+
else
|
|
629
|
+
proceed = prompt.yes?('Continue without network isolation?')
|
|
630
|
+
exit 1 unless proceed
|
|
631
|
+
{ enabled: false }
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Detect CNI and NetworkPolicy support
|
|
636
|
+
def detect_cni_support
|
|
637
|
+
require_relative '../../kubernetes/client'
|
|
638
|
+
|
|
639
|
+
begin
|
|
640
|
+
k8s = Kubernetes::Client.new
|
|
641
|
+
|
|
642
|
+
# Try to detect CNI by checking nodes for CNI annotations or by looking for specific resources
|
|
643
|
+
nodes = k8s.list_resources('Node', namespace: nil)
|
|
644
|
+
|
|
645
|
+
# Check for common CNI indicators
|
|
646
|
+
cni_info = detect_cni_from_nodes(nodes)
|
|
647
|
+
|
|
648
|
+
# Only test API availability if we couldn't determine CNI-specific support
|
|
649
|
+
if cni_info[:name] == 'Unknown'
|
|
650
|
+
begin
|
|
651
|
+
k8s.list_resources('NetworkPolicy', namespace: 'kube-system', api_version: 'networking.k8s.io/v1')
|
|
652
|
+
cni_info[:supports_network_policies] = true
|
|
653
|
+
rescue StandardError
|
|
654
|
+
cni_info[:supports_network_policies] = false
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
cni_info
|
|
659
|
+
rescue StandardError => e
|
|
660
|
+
warn "Failed to detect CNI: #{e.message}" if ENV['DEBUG']
|
|
661
|
+
{ name: 'Unknown', supports_network_policies: false }
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# Detect CNI from CRDs (most reliable method)
|
|
666
|
+
def detect_cni_from_nodes(_nodes)
|
|
667
|
+
require_relative '../../kubernetes/client'
|
|
668
|
+
k8s = Kubernetes::Client.new
|
|
669
|
+
|
|
670
|
+
# Check for CNI-specific CRDs - this is the most reliable method
|
|
671
|
+
begin
|
|
672
|
+
crds = k8s.list_resources('CustomResourceDefinition', namespace: nil, api_version: 'apiextensions.k8s.io/v1')
|
|
673
|
+
|
|
674
|
+
crd_names = crds.map { |crd| crd.dig('metadata', 'name') }.compact.join(' ')
|
|
675
|
+
|
|
676
|
+
# Check for specific CNI CRDs
|
|
677
|
+
if crd_names.match?(/cilium/i)
|
|
678
|
+
return { name: 'Cilium', supports_network_policies: true }
|
|
679
|
+
elsif crd_names.match?(/(calico|projectcalico)/i)
|
|
680
|
+
return { name: 'Calico', supports_network_policies: true }
|
|
681
|
+
elsif crd_names.match?(/weave/i)
|
|
682
|
+
return { name: 'Weave Net', supports_network_policies: true }
|
|
683
|
+
elsif crd_names.match?(/antrea/i)
|
|
684
|
+
return { name: 'Antrea', supports_network_policies: true }
|
|
685
|
+
elsif crd_names.match?(/flannel/i)
|
|
686
|
+
return { name: 'Flannel', supports_network_policies: false }
|
|
687
|
+
end
|
|
688
|
+
rescue StandardError => e
|
|
689
|
+
warn "Failed to check CRDs for CNI detection: #{e.message}" if ENV['DEBUG']
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Fallback: Check for CNI-specific pods in kube-system
|
|
693
|
+
begin
|
|
694
|
+
pods = k8s.list_resources('Pod', namespace: 'kube-system')
|
|
695
|
+
pod_names = pods.map { |pod| pod.dig('metadata', 'name') }.compact.join(' ')
|
|
696
|
+
|
|
697
|
+
if pod_names.match?(/cilium/i)
|
|
698
|
+
return { name: 'Cilium', supports_network_policies: true }
|
|
699
|
+
elsif pod_names.match?(/calico/i)
|
|
700
|
+
return { name: 'Calico', supports_network_policies: true }
|
|
701
|
+
elsif pod_names.match?(/weave/i)
|
|
702
|
+
return { name: 'Weave Net', supports_network_policies: true }
|
|
703
|
+
elsif pod_names.match?(/antrea/i)
|
|
704
|
+
return { name: 'Antrea', supports_network_policies: true }
|
|
705
|
+
elsif pod_names.match?(/flannel/i)
|
|
706
|
+
return { name: 'Flannel', supports_network_policies: false }
|
|
707
|
+
elsif pod_names.match?(/aws-node/i)
|
|
708
|
+
return { name: 'AWS VPC CNI', supports_network_policies: false }
|
|
709
|
+
end
|
|
710
|
+
rescue StandardError => e
|
|
711
|
+
warn "Failed to check pods for CNI detection: #{e.message}" if ENV['DEBUG']
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# Final fallback - we couldn't identify the CNI
|
|
715
|
+
{ name: 'Unknown', supports_network_policies: false }
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Generate values.yaml file from configuration
|
|
719
|
+
def generate_values_file(config)
|
|
720
|
+
require 'tempfile'
|
|
721
|
+
require 'bcrypt'
|
|
722
|
+
require 'yaml'
|
|
723
|
+
|
|
724
|
+
# Hash the password and convert to string
|
|
725
|
+
password_hash = BCrypt::Password.create(config[:admin_password]).to_s
|
|
726
|
+
|
|
727
|
+
values = {
|
|
728
|
+
'dashboard' => {
|
|
729
|
+
'initialSetup' => {
|
|
730
|
+
'enabled' => true,
|
|
731
|
+
'adminUser' => {
|
|
732
|
+
'name' => config[:admin_name],
|
|
733
|
+
'email' => config[:admin_email],
|
|
734
|
+
'passwordHash' => password_hash
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
'features' => {
|
|
738
|
+
'signupsDisabled' => true
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
'networkIsolation' => {
|
|
742
|
+
'enabled' => config.dig(:network_isolation, :enabled) || false
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
# Add gateway configuration if provided
|
|
747
|
+
if config[:gateway]
|
|
748
|
+
values['dashboard']['gateway'] = {
|
|
749
|
+
'enabled' => true,
|
|
750
|
+
'gatewayName' => config[:gateway][:gateway_name],
|
|
751
|
+
'gatewayNamespace' => config[:gateway][:gateway_namespace],
|
|
752
|
+
'hostname' => config[:gateway][:hostname],
|
|
753
|
+
'tls' => {
|
|
754
|
+
'enabled' => config[:gateway][:tls]
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Create temporary values file
|
|
760
|
+
values_file = Tempfile.new(['langop-values', '.yaml'])
|
|
761
|
+
values_file.write(YAML.dump(values))
|
|
762
|
+
values_file.close
|
|
763
|
+
|
|
764
|
+
values_file.path
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Delete all organization namespaces
|
|
768
|
+
def delete_organization_namespaces
|
|
769
|
+
require_relative '../../kubernetes/client'
|
|
770
|
+
|
|
771
|
+
k8s = Kubernetes::Client.new
|
|
772
|
+
namespace_names = []
|
|
773
|
+
|
|
774
|
+
# Delete and wait for all organization namespaces to be completely removed
|
|
775
|
+
Formatters::ProgressFormatter.with_spinner('Deleting organization namespaces') do
|
|
776
|
+
begin
|
|
777
|
+
# Find all namespaces with organization label
|
|
778
|
+
org_namespaces = k8s.list_namespaces(
|
|
779
|
+
label_selector: 'langop.io/type=organization'
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# Trigger deletion of each namespace
|
|
783
|
+
org_namespaces.each do |namespace|
|
|
784
|
+
name = namespace.dig('metadata', 'name')
|
|
785
|
+
namespace_names << name
|
|
786
|
+
org_id = namespace.dig('metadata', 'labels', 'langop.io/organization-id')
|
|
787
|
+
|
|
788
|
+
begin
|
|
789
|
+
k8s.delete_resource('Namespace', name)
|
|
790
|
+
rescue StandardError => e
|
|
791
|
+
# Continue deleting other namespaces even if one fails
|
|
792
|
+
warn "Failed to delete organization namespace #{name} (#{org_id}): #{e.message}" if ENV['DEBUG']
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# Then wait for all organization namespaces to be completely deleted
|
|
797
|
+
wait_for_namespaces_deletion_inline(namespace_names, k8s, timeout: 300) if namespace_names.any?
|
|
798
|
+
rescue StandardError => e
|
|
799
|
+
warn "Failed to list/delete organization namespaces: #{e.message}" if ENV['DEBUG']
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
# Delete all language-operator persistent volumes
|
|
805
|
+
def delete_persistent_volumes
|
|
806
|
+
require_relative '../../kubernetes/client'
|
|
807
|
+
|
|
808
|
+
k8s = Kubernetes::Client.new
|
|
809
|
+
|
|
810
|
+
Formatters::ProgressFormatter.with_spinner('Deleting persistent volumes') do
|
|
811
|
+
# Use label selector to find language-operator PVCs
|
|
812
|
+
label_selector = 'app.kubernetes.io/instance=language-operator'
|
|
813
|
+
|
|
814
|
+
begin
|
|
815
|
+
all_pvcs = k8s.list_resources('PersistentVolumeClaim', namespace: nil, label_selector: label_selector)
|
|
816
|
+
|
|
817
|
+
all_pvcs.each do |pvc|
|
|
818
|
+
name = pvc.dig('metadata', 'name')
|
|
819
|
+
namespace = pvc.dig('metadata', 'namespace')
|
|
820
|
+
|
|
821
|
+
begin
|
|
822
|
+
k8s.delete_resource('PersistentVolumeClaim', name, namespace)
|
|
823
|
+
rescue StandardError => e
|
|
824
|
+
warn "Failed to delete PVC #{name}: #{e.message}" if ENV['DEBUG']
|
|
825
|
+
end
|
|
826
|
+
end
|
|
827
|
+
rescue StandardError => e
|
|
828
|
+
warn "Failed to list/delete PVCs: #{e.message}" if ENV['DEBUG']
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
# Delete cluster-scoped resources that Helm doesn't automatically remove
|
|
834
|
+
def delete_cluster_scoped_resources
|
|
835
|
+
require_relative '../../kubernetes/client'
|
|
836
|
+
|
|
837
|
+
k8s = Kubernetes::Client.new
|
|
838
|
+
|
|
839
|
+
Formatters::ProgressFormatter.with_spinner('Cleaning up cluster-scoped resources') do
|
|
840
|
+
# 1. Delete ClusterRoles
|
|
841
|
+
delete_cluster_roles(k8s)
|
|
842
|
+
|
|
843
|
+
# 2. Delete ClusterRoleBindings
|
|
844
|
+
delete_cluster_role_bindings(k8s)
|
|
845
|
+
|
|
846
|
+
# 3. Delete webhook configurations
|
|
847
|
+
delete_webhook_configurations(k8s)
|
|
848
|
+
|
|
849
|
+
# 4. Remove finalizers from stuck resources
|
|
850
|
+
remove_stuck_finalizers(k8s)
|
|
851
|
+
end
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
# Delete language-operator ClusterRoles
|
|
855
|
+
def delete_cluster_roles(k8s)
|
|
856
|
+
# Find ClusterRoles with language-operator labels
|
|
857
|
+
cluster_roles = k8s.list_resources('ClusterRole',
|
|
858
|
+
namespace: nil,
|
|
859
|
+
api_version: 'rbac.authorization.k8s.io/v1',
|
|
860
|
+
label_selector: 'app.kubernetes.io/name=language-operator')
|
|
861
|
+
|
|
862
|
+
cluster_roles.each do |cr|
|
|
863
|
+
name = cr.dig('metadata', 'name')
|
|
864
|
+
begin
|
|
865
|
+
k8s.delete_resource('ClusterRole', name, nil, 'rbac.authorization.k8s.io/v1')
|
|
866
|
+
rescue StandardError => e
|
|
867
|
+
warn "Failed to delete ClusterRole #{name}: #{e.message}" if ENV['DEBUG']
|
|
868
|
+
end
|
|
869
|
+
end
|
|
870
|
+
rescue StandardError => e
|
|
871
|
+
warn "Failed to list/delete ClusterRoles: #{e.message}" if ENV['DEBUG']
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Delete language-operator ClusterRoleBindings
|
|
875
|
+
def delete_cluster_role_bindings(k8s)
|
|
876
|
+
# Find ClusterRoleBindings with language-operator labels
|
|
877
|
+
crbs = k8s.list_resources('ClusterRoleBinding',
|
|
878
|
+
namespace: nil,
|
|
879
|
+
api_version: 'rbac.authorization.k8s.io/v1',
|
|
880
|
+
label_selector: 'app.kubernetes.io/name=language-operator')
|
|
881
|
+
|
|
882
|
+
crbs.each do |crb|
|
|
883
|
+
name = crb.dig('metadata', 'name')
|
|
884
|
+
begin
|
|
885
|
+
k8s.delete_resource('ClusterRoleBinding', name, nil, 'rbac.authorization.k8s.io/v1')
|
|
886
|
+
rescue StandardError => e
|
|
887
|
+
warn "Failed to delete ClusterRoleBinding #{name}: #{e.message}" if ENV['DEBUG']
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
# Get all ClusterRoleBindings to find agent-specific ones and ServiceAccount references
|
|
892
|
+
all_crbs = k8s.list_resources('ClusterRoleBinding', namespace: nil, api_version: 'rbac.authorization.k8s.io/v1')
|
|
893
|
+
all_crbs.each do |crb|
|
|
894
|
+
name = crb.dig('metadata', 'name')
|
|
895
|
+
should_delete = false
|
|
896
|
+
|
|
897
|
+
# Check if this is an agent-specific ClusterRoleBinding (pattern: language-agent-*)
|
|
898
|
+
if name&.start_with?('language-agent-')
|
|
899
|
+
should_delete = true
|
|
900
|
+
puts "Found agent-specific ClusterRoleBinding: #{name}" if ENV['DEBUG']
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# Also check if CRB references language-operator ServiceAccounts
|
|
904
|
+
unless should_delete
|
|
905
|
+
subjects = crb.dig('subjects') || []
|
|
906
|
+
has_langop_subject = subjects.any? do |subject|
|
|
907
|
+
subject['name']&.include?('language-operator')
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
if has_langop_subject
|
|
911
|
+
should_delete = true
|
|
912
|
+
puts "Found ClusterRoleBinding referencing language-operator ServiceAccount: #{name}" if ENV['DEBUG']
|
|
913
|
+
end
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
# Also check if CRB references non-existent ClusterRoles that we know about
|
|
917
|
+
unless should_delete
|
|
918
|
+
role_ref = crb.dig('roleRef')
|
|
919
|
+
if role_ref && role_ref['kind'] == 'ClusterRole' && role_ref['name'] == 'language-operator'
|
|
920
|
+
should_delete = true
|
|
921
|
+
puts "Found ClusterRoleBinding referencing deleted ClusterRole 'language-operator': #{name}" if ENV['DEBUG']
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# Delete if we found a reason to
|
|
926
|
+
next unless should_delete
|
|
927
|
+
|
|
928
|
+
begin
|
|
929
|
+
k8s.delete_resource('ClusterRoleBinding', name, nil, 'rbac.authorization.k8s.io/v1')
|
|
930
|
+
puts "Successfully deleted ClusterRoleBinding: #{name}" if ENV['DEBUG']
|
|
931
|
+
rescue StandardError => e
|
|
932
|
+
warn "Failed to delete ClusterRoleBinding #{name}: #{e.message}" if ENV['DEBUG']
|
|
933
|
+
end
|
|
934
|
+
end
|
|
935
|
+
rescue StandardError => e
|
|
936
|
+
warn "Failed to list/delete ClusterRoleBindings: #{e.message}" if ENV['DEBUG']
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
# Delete webhook configurations
|
|
940
|
+
def delete_webhook_configurations(k8s)
|
|
941
|
+
# Delete ValidatingWebhookConfigurations
|
|
942
|
+
begin
|
|
943
|
+
vwcs = k8s.list_resources('ValidatingWebhookConfiguration',
|
|
944
|
+
namespace: nil,
|
|
945
|
+
label_selector: 'app.kubernetes.io/name=language-operator')
|
|
946
|
+
|
|
947
|
+
vwcs.each do |vwc|
|
|
948
|
+
name = vwc.dig('metadata', 'name')
|
|
949
|
+
begin
|
|
950
|
+
k8s.delete_resource('ValidatingWebhookConfiguration', name)
|
|
951
|
+
rescue StandardError => e
|
|
952
|
+
warn "Failed to delete ValidatingWebhookConfiguration #{name}: #{e.message}" if ENV['DEBUG']
|
|
953
|
+
end
|
|
954
|
+
end
|
|
955
|
+
rescue StandardError => e
|
|
956
|
+
warn "Failed to list/delete ValidatingWebhookConfigurations: #{e.message}" if ENV['DEBUG']
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# Delete MutatingWebhookConfigurations
|
|
960
|
+
begin
|
|
961
|
+
mwcs = k8s.list_resources('MutatingWebhookConfiguration',
|
|
962
|
+
namespace: nil,
|
|
963
|
+
label_selector: 'app.kubernetes.io/name=language-operator')
|
|
964
|
+
|
|
965
|
+
mwcs.each do |mwc|
|
|
966
|
+
name = mwc.dig('metadata', 'name')
|
|
967
|
+
begin
|
|
968
|
+
k8s.delete_resource('MutatingWebhookConfiguration', name)
|
|
969
|
+
rescue StandardError => e
|
|
970
|
+
warn "Failed to delete MutatingWebhookConfiguration #{name}: #{e.message}" if ENV['DEBUG']
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
rescue StandardError => e
|
|
974
|
+
warn "Failed to list/delete MutatingWebhookConfigurations: #{e.message}" if ENV['DEBUG']
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Remove finalizers from stuck resources
|
|
979
|
+
def remove_stuck_finalizers(k8s)
|
|
980
|
+
# Remove finalizers from LanguageOperator CRDs if they're stuck
|
|
981
|
+
crd_names = [
|
|
982
|
+
'languageagents.langop.io',
|
|
983
|
+
'languagetools.langop.io',
|
|
984
|
+
'languagemodels.langop.io',
|
|
985
|
+
'languagepersonas.langop.io',
|
|
986
|
+
'languageclusters.langop.io',
|
|
987
|
+
'languageagentversions.langop.io'
|
|
988
|
+
]
|
|
989
|
+
|
|
990
|
+
crd_names.each do |crd_name|
|
|
991
|
+
crd = k8s.get_resource('CustomResourceDefinition', crd_name, nil, 'apiextensions.k8s.io/v1')
|
|
992
|
+
|
|
993
|
+
# Check if CRD has finalizers
|
|
994
|
+
finalizers = crd.dig('metadata', 'finalizers')
|
|
995
|
+
if finalizers && !finalizers.empty?
|
|
996
|
+
# Remove finalizers to allow deletion
|
|
997
|
+
patch = {
|
|
998
|
+
'metadata' => {
|
|
999
|
+
'finalizers' => []
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
k8s.patch_resource('CustomResourceDefinition', crd_name, patch, namespace: nil, api_version: 'apiextensions.k8s.io/v1')
|
|
1003
|
+
end
|
|
1004
|
+
rescue K8s::Error::NotFound
|
|
1005
|
+
# CRD doesn't exist, skip
|
|
1006
|
+
rescue StandardError => e
|
|
1007
|
+
warn "Failed to remove finalizers from CRD #{crd_name}: #{e.message}" if ENV['DEBUG']
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
# Remove finalizers from stuck custom resources
|
|
1011
|
+
resource_types = %w[LanguageAgent LanguageTool LanguageModel LanguagePersona LanguageCluster LanguageAgentVersion]
|
|
1012
|
+
resource_types.each do |resource_type|
|
|
1013
|
+
resources = k8s.list_resources(resource_type, namespace: nil)
|
|
1014
|
+
resources.each do |resource|
|
|
1015
|
+
finalizers = resource.dig('metadata', 'finalizers')
|
|
1016
|
+
next unless finalizers && !finalizers.empty?
|
|
1017
|
+
|
|
1018
|
+
name = resource.dig('metadata', 'name')
|
|
1019
|
+
namespace = resource.dig('metadata', 'namespace')
|
|
1020
|
+
|
|
1021
|
+
patch = {
|
|
1022
|
+
'metadata' => {
|
|
1023
|
+
'finalizers' => []
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
k8s.patch_resource(resource_type, name, patch, namespace: namespace)
|
|
1027
|
+
end
|
|
1028
|
+
rescue StandardError => e
|
|
1029
|
+
warn "Failed to remove finalizers from #{resource_type} resources: #{e.message}" if ENV['DEBUG']
|
|
1030
|
+
end
|
|
1031
|
+
rescue StandardError => e
|
|
1032
|
+
warn "Failed to remove stuck finalizers: #{e.message}" if ENV['DEBUG']
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
# Verify installation and setup
|
|
1036
|
+
def verify_installation(config)
|
|
1037
|
+
# Wait a moment for pods to start
|
|
1038
|
+
sleep 3
|
|
1039
|
+
|
|
1040
|
+
# Verify account setup
|
|
1041
|
+
account_ready = verify_account_setup
|
|
1042
|
+
|
|
1043
|
+
# Verify gateway setup if configured
|
|
1044
|
+
gateway_ready = false
|
|
1045
|
+
gateway_url = nil
|
|
1046
|
+
|
|
1047
|
+
gateway_ready, gateway_url = verify_gateway_setup(config[:gateway]) if config&.dig(:gateway)
|
|
1048
|
+
|
|
1049
|
+
# Determine next steps based on verification results
|
|
1050
|
+
show_post_install_message(config, account_ready, gateway_ready, gateway_url)
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
# Verify that the admin account was created successfully
|
|
1054
|
+
def verify_account_setup
|
|
1055
|
+
Formatters::ProgressFormatter.with_spinner('Verifying account setup') do
|
|
1056
|
+
# Check if dashboard pod is ready
|
|
1057
|
+
require_relative '../../kubernetes/client'
|
|
1058
|
+
k8s = Kubernetes::Client.new
|
|
1059
|
+
|
|
1060
|
+
begin
|
|
1061
|
+
# Check if dashboard deployment is ready
|
|
1062
|
+
deployment = k8s.get_resource('Deployment', 'language-operator-dashboard', 'language-operator')
|
|
1063
|
+
ready_replicas = deployment.dig('status', 'readyReplicas') || 0
|
|
1064
|
+
desired_replicas = deployment.dig('spec', 'replicas') || 1
|
|
1065
|
+
|
|
1066
|
+
ready_replicas >= desired_replicas
|
|
1067
|
+
rescue StandardError
|
|
1068
|
+
false
|
|
1069
|
+
end
|
|
1070
|
+
end
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# Verify gateway configuration and HTTPRoute creation
|
|
1074
|
+
def verify_gateway_setup(gateway_config)
|
|
1075
|
+
gateway_url = nil
|
|
1076
|
+
|
|
1077
|
+
success = Formatters::ProgressFormatter.with_spinner('Verifying gateway setup') do
|
|
1078
|
+
require_relative '../../kubernetes/client'
|
|
1079
|
+
k8s = Kubernetes::Client.new
|
|
1080
|
+
|
|
1081
|
+
begin
|
|
1082
|
+
# Check if HTTPRoute was created (it should be in the gateway namespace)
|
|
1083
|
+
httproute_namespace = gateway_config[:gateway_namespace]
|
|
1084
|
+
k8s.get_resource('HTTPRoute', 'language-operator-dashboard', httproute_namespace, 'gateway.networking.k8s.io/v1')
|
|
1085
|
+
|
|
1086
|
+
# Build the public URL
|
|
1087
|
+
protocol = gateway_config[:tls] ? 'https' : 'http'
|
|
1088
|
+
gateway_url = "#{protocol}://#{gateway_config[:hostname]}"
|
|
1089
|
+
|
|
1090
|
+
true
|
|
1091
|
+
rescue StandardError => e
|
|
1092
|
+
warn "Gateway verification failed: #{e.message}" if ENV['DEBUG']
|
|
1093
|
+
false
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
[success, gateway_url]
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# Show appropriate post-installation message and actions
|
|
1101
|
+
def show_post_install_message(config, _account_ready, gateway_ready, gateway_url)
|
|
1102
|
+
if gateway_ready && gateway_url
|
|
1103
|
+
# Gateway configured successfully - open public URL
|
|
1104
|
+
puts
|
|
1105
|
+
puts 'Your Language Operator dashboard is available at:'
|
|
1106
|
+
puts pastel.cyan.bold(gateway_url)
|
|
1107
|
+
puts
|
|
1108
|
+
|
|
1109
|
+
# Open browser to public URL
|
|
1110
|
+
open_browser(gateway_url) unless (@options || options)[:no_open]
|
|
1111
|
+
|
|
1112
|
+
elsif config&.dig(:gateway) && !gateway_ready
|
|
1113
|
+
# Gateway was configured but failed
|
|
1114
|
+
puts pastel.yellow('⚠') + ' Installation completed with warnings'
|
|
1115
|
+
puts
|
|
1116
|
+
puts pastel.yellow('Gateway configuration had issues. You can access the dashboard locally:')
|
|
1117
|
+
puts "Run: #{pastel.cyan('langop ui')}"
|
|
1118
|
+
|
|
1119
|
+
else
|
|
1120
|
+
# No gateway configured - show local access instructions
|
|
1121
|
+
puts
|
|
1122
|
+
puts "Run #{pastel.cyan.bold('langop ui')} to get started!"
|
|
1123
|
+
end
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
# Open browser to URL (delegates to UI command implementation)
|
|
1127
|
+
def open_browser(url)
|
|
1128
|
+
require_relative 'ui'
|
|
1129
|
+
ui_command = Ui.new
|
|
1130
|
+
ui_command.send(:open_browser, url)
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
# Wait for custom resources of a specific type to be completely deleted (inline with existing operation)
|
|
1134
|
+
def wait_for_resources_deletion_inline(resource_type, k8s, timeout: 300)
|
|
1135
|
+
start_time = Time.now
|
|
1136
|
+
|
|
1137
|
+
loop do
|
|
1138
|
+
resources = k8s.list_resources(resource_type, namespace: nil)
|
|
1139
|
+
return true if resources.empty?
|
|
1140
|
+
|
|
1141
|
+
# Check timeout
|
|
1142
|
+
if Time.now - start_time > timeout
|
|
1143
|
+
remaining_count = resources.length
|
|
1144
|
+
warn "Timeout waiting for #{resource_type} resources to be deleted (#{remaining_count} remaining)" if ENV['DEBUG']
|
|
1145
|
+
return false
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
# Sleep before next check
|
|
1149
|
+
sleep 2
|
|
1150
|
+
rescue StandardError => e
|
|
1151
|
+
# If API group doesn't exist (CRDs already deleted), consider it success
|
|
1152
|
+
return true if e.message.include?('404') || e.message.include?('Not Found')
|
|
1153
|
+
|
|
1154
|
+
# Check timeout for other errors too
|
|
1155
|
+
if Time.now - start_time > timeout
|
|
1156
|
+
warn "Timeout waiting for #{resource_type} resources: #{e.message}" if ENV['DEBUG']
|
|
1157
|
+
return false
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
sleep 2
|
|
1161
|
+
end
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
# Wait for specific namespaces to be completely deleted (inline with existing operation)
|
|
1165
|
+
def wait_for_namespaces_deletion_inline(namespace_names, k8s, timeout: 300)
|
|
1166
|
+
start_time = Time.now
|
|
1167
|
+
remaining_namespaces = namespace_names.dup
|
|
1168
|
+
|
|
1169
|
+
loop do
|
|
1170
|
+
remaining_namespaces = remaining_namespaces.select do |name|
|
|
1171
|
+
k8s.namespace_exists?(name)
|
|
1172
|
+
rescue StandardError
|
|
1173
|
+
# If we can't check, assume it's gone
|
|
1174
|
+
false
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
return true if remaining_namespaces.empty?
|
|
1178
|
+
|
|
1179
|
+
# Check timeout
|
|
1180
|
+
if Time.now - start_time > timeout
|
|
1181
|
+
warn "Timeout waiting for namespaces to be deleted: #{remaining_namespaces.join(', ')}" if ENV['DEBUG']
|
|
1182
|
+
return false
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
# Sleep before next check
|
|
1186
|
+
sleep 2
|
|
1187
|
+
end
|
|
1188
|
+
end
|
|
392
1189
|
end
|
|
393
1190
|
end
|
|
394
1191
|
end
|