@nortal/ai1st-kit-cli 1.0.1 → 1.0.2

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.
@@ -1 +1 @@
1
- import{CliError as p}from"../errors.js";import{getKeychain as T}from"./keychain.js";import{probeSsh as v}from"./probe-ssh.js";import{probeHttps as g}from"./probe-https.js";import{promptForPat as w}from"./pat-prompt.js";function u(t){if(t.startsWith("https://"))return t;if(t.startsWith("git@")){const n=t.slice(4),e=n.indexOf(":");if(e<0)return t;const o=n.slice(0,e);let r=n.slice(e+1);return r.startsWith("/")&&(r=r.slice(1)),`https://${o}/${r}`}return t}function I(t){if(t.startsWith("git@"))return t;if(t.startsWith("https://")){const n=t.slice(8),e=n.indexOf("/");if(e<0)return t;const o=n.slice(0,e),r=n.slice(e+1);return`git@${o}:${r}`}return t}async function A(t){try{return(await fetch("https://gitlab.nortal.com/api/v4/user",{method:"GET",headers:{"PRIVATE-TOKEN":t},signal:AbortSignal.timeout(5e3)})).status===200}catch{return!1}}function h(){return{mode:"ssh",preferredUrlFor:t=>I(t),applyToGitEnv:()=>{}}}function f(){return{mode:"https",preferredUrlFor:t=>u(t),applyToGitEnv:()=>{}}}function _(t){return/^[A-Za-z0-9._\-]+$/.test(t)}function s(t,n){return{mode:n,preferredUrlFor:e=>{const o=u(e);if(n!=="env-token")return o;const r=new URL(o);return r.username="oauth2",r.password=t,r.toString()},applyToGitEnv:e=>{const o=Buffer.from(`oauth2:${t}`).toString("base64");e.GIT_TERMINAL_PROMPT="0",e.GIT_CONFIG_COUNT="1",e.GIT_CONFIG_KEY_0="http.extraHeader",e.GIT_CONFIG_VALUE_0=`Authorization: Basic ${o}`,e.AI1ST_GIT_TOKEN=t}}}async function y(t,n={}){const e=n.probeSshImpl??v,o=n.probeHttpsImpl??g,r=n.validatePatImpl??A,m=n.getKeychainImpl??T,d=n.promptForPatImpl??w;if(!t.interactive){const l=(process.env.GITLAB_TOKEN??"").trim();if(l.length>0)return s(l,"env-token");if((await e(t.contentRepo)).ok)return h();if((await o(t.contentRepo)).ok)return f();throw new p({code:"E_AUTH",message:"No usable credentials in non-interactive mode. Next: set GITLAB_TOKEN env var or run interactively.",headline:"No usable GitLab credentials in non-interactive mode.",todo:["Set the `GITLAB_TOKEN` environment variable to a valid PAT, or","Re-run interactively (omit `--yes`) so the CLI can prompt for credentials."]})}if((await e(t.contentRepo)).ok)return h();if((await o(t.contentRepo)).ok)return f();const a=await m(),i=await a.getPassword();if(i&&_(i)&&await r(i))return s(i,"pat-stored");i&&await a.deletePassword();const c=await d({contentRepo:t.contentRepo,keychain:a,validatePat:r});if("cancelled"in c)throw new p({code:"E_AUTH",message:"No GitLab credentials provided. Next: re-run and supply a PAT, or configure SSH for gitlab.nortal.com."});return s(c.token,"pat-prompted")}export{y as probeAndResolveAuth,u as toHttpsUrl,I as toSshUrl,A as validatePat};
1
+ import{CliError as p}from"../errors.js";import{getKeychain as T}from"./keychain.js";import{probeSsh as v}from"./probe-ssh.js";import{probeHttps as g}from"./probe-https.js";import{promptForPat as w}from"./pat-prompt.js";function u(t){if(t.startsWith("https://"))return t;if(t.startsWith("git@")){const e=t.slice(4),n=e.indexOf(":");if(n<0)return t;const o=e.slice(0,n);let r=e.slice(n+1);return r.startsWith("/")&&(r=r.slice(1)),`https://${o}/${r}`}return t}function I(t){if(t.startsWith("git@"))return t;if(t.startsWith("https://")){const e=t.slice(8),n=e.indexOf("/");if(n<0)return t;const o=e.slice(0,n),r=e.slice(n+1);return`git@${o}:${r}`}return t}async function A(t){try{return(await fetch("https://gitlab.nortal.com/api/v4/user",{method:"GET",headers:{"PRIVATE-TOKEN":t},signal:AbortSignal.timeout(5e3)})).status===200}catch{return!1}}function f(){return{mode:"ssh",preferredUrlFor:t=>I(t),applyToGitEnv:()=>{}}}function h(){return{mode:"https",preferredUrlFor:t=>u(t),applyToGitEnv:()=>{}}}function _(t){return/^[A-Za-z0-9._\-]+$/.test(t)}function s(t,e){return{mode:e,preferredUrlFor:n=>{const o=u(n);if(e!=="env-token")return o;const r=new URL(o);return r.username="oauth2",r.password=t,r.toString()},applyToGitEnv:n=>{const o=Buffer.from(`oauth2:${t}`).toString("base64");n.GIT_TERMINAL_PROMPT="0",n.GIT_CONFIG_COUNT="1",n.GIT_CONFIG_KEY_0="http.extraHeader",n.GIT_CONFIG_VALUE_0=`Authorization: Basic ${o}`,n.AI1ST_GIT_TOKEN=t}}}async function y(t,e={}){const n=e.probeSshImpl??v,o=e.probeHttpsImpl??g,r=e.validatePatImpl??A,m=e.getKeychainImpl??T,d=e.promptForPatImpl??w;if(!t.interactive){const l=(process.env.GITLAB_TOKEN??"").trim();if(l.length>0)return s(l,"env-token");if((await n(t.contentRepo)).ok)return f();if((await o(t.contentRepo)).ok)return h();throw new p({code:"E_AUTH",message:"No usable credentials in non-interactive mode. Next: set GITLAB_TOKEN env var or run interactively.",headline:"No usable GitLab credentials in non-interactive mode.",todo:["Set the `GITLAB_TOKEN` environment variable to a valid PAT, or","Re-run interactively (omit `--yes`) so the CLI can prompt for credentials."]})}if((await n(t.contentRepo)).ok)return f();if((await o(t.contentRepo)).ok)return h();const a=await m(),i=await a.getPassword();if(i&&_(i)&&await r(i))return s(i,"pat-stored");i&&await a.deletePassword(),e.onBeforePatPrompt?.();const c=await d({contentRepo:t.contentRepo,keychain:a,validatePat:r});if("cancelled"in c)throw new p({code:"E_AUTH",message:"No GitLab credentials provided. Next: re-run and supply a PAT, or configure SSH for gitlab.nortal.com."});return s(c.token,"pat-prompted")}export{y as probeAndResolveAuth,u as toHttpsUrl,I as toSshUrl,A as validatePat};
@@ -1,2 +1,2 @@
1
- import{readFile as u,writeFile as w,mkdir as f,chmod as m,stat as p}from"node:fs/promises";import{homedir as y}from"node:os";import{dirname as g,join as b}from"node:path";import{log as h}from"../log.js";const o="nortal-ai1st-kit-cli",a="gitlab-pat";let s=!1;function A(){s=!1}function i(){return b(y(),".config","nortal-ai1st","credentials.json")}async function c(t){let e;try{e=await u(t,"utf8")}catch(r){if(r.code==="ENOENT")return null;throw r}let n;try{n=JSON.parse(e)}catch(r){throw new Error(`credentials file at ${t} is not valid JSON: ${r.message}`)}if(n===null||typeof n!="object"||Array.isArray(n))throw new Error(`credentials file at ${t} must be a JSON object`);if(!s&&process.argv.includes("--verbose"))try{const r=await p(t);process.platform!=="win32"&&(r.mode&63)!==0&&(h.warn(`credentials file ${t} has wide permissions (mode ${(r.mode&511).toString(8)}); recommend chmod 0600`),s=!0)}catch{}return n}async function d(t,e){await f(g(t),{recursive:!0}),await w(t,JSON.stringify(e,null,2)+`
2
- `,"utf8"),process.platform!=="win32"&&await m(t,384)}function l(){return{async getPassword(){const t=i(),e=await c(t);if(e===null)return null;const n=e[a];return typeof n=="string"?n:null},async setPassword(t){const e=i(),r=await c(e)??{};r[a]=t,await d(e,r)},async deletePassword(){const t=i(),e=await c(t);e!==null&&(delete e[a],await d(t,e))}}}function C(t){return{async getPassword(){return t.getPassword(o,a)},async setPassword(e){await t.setPassword(o,a,e)},async deletePassword(){await t.deletePassword(o,a)}}}async function P(){if(process.env.AI1ST_KIT_FORCE_KEYCHAIN_FALLBACK==="1")return l();try{const t=await import("keytar"),e=t.default??t;return typeof e.getPassword!="function"?l():C(e)}catch{return l()}}const K={resetFileModeWarning:A};export{a as KEYCHAIN_ACCOUNT,o as KEYCHAIN_SERVICE,K as __test__,P as getKeychain};
1
+ import{readFile as d,writeFile as u,mkdir as w,chmod as f,stat as m}from"node:fs/promises";import{homedir as y}from"node:os";import{dirname as p,join as g}from"node:path";import{log as E}from"../log.js";const b="nortal-ai1st-kit-cli",o="gitlab-pat";let s=!1;function N(){s=!1}function a(){return g(y(),".config","nortal-ai1st","credentials.json")}async function i(t){let e;try{e=await d(t,"utf8")}catch(n){if(n.code==="ENOENT")return null;throw n}let r;try{r=JSON.parse(e)}catch(n){throw new Error(`credentials file at ${t} is not valid JSON: ${n.message}`)}if(r===null||typeof r!="object"||Array.isArray(r))throw new Error(`credentials file at ${t} must be a JSON object`);if(!s&&process.argv.includes("--verbose"))try{const n=await m(t);process.platform!=="win32"&&(n.mode&63)!==0&&(E.warn(`credentials file ${t} has wide permissions (mode ${(n.mode&511).toString(8)}); recommend chmod 0600`),s=!0)}catch{}return r}async function l(t,e){await w(p(t),{recursive:!0}),await u(t,JSON.stringify(e,null,2)+`
2
+ `,"utf8"),process.platform!=="win32"&&await f(t,384)}function c(){return{async getPassword(){const t=a(),e=await i(t);if(e===null)return null;const r=e[o];return typeof r=="string"?r:null},async setPassword(t){const e=a(),n=await i(e)??{};n[o]=t,await l(e,n)},async deletePassword(){const t=a(),e=await i(t);e!==null&&(delete e[o],await l(t,e))}}}function P(t){return{async getPassword(){return Promise.resolve(t.getPassword())},async setPassword(e){return Promise.resolve(t.setPassword(e))},async deletePassword(){return t.deletePassword(),Promise.resolve()}}}async function _(){if(process.env.AI1ST_KIT_FORCE_KEYCHAIN_FALLBACK==="1")return c();try{const t=await import("@napi-rs/keyring"),e="Entry"in t?t.Entry:t.default.Entry,r=new e(b,o);return typeof r.getPassword!="function"?c():P(r)}catch{return c()}}const F={resetFileModeWarning:N};export{o as KEYCHAIN_ACCOUNT,b as KEYCHAIN_SERVICE,F as __test__,_ as getKeychain};
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import{mkdir as U,readFile as V}from"node:fs/promises";import{join as K}from"node:path";import*as q from"@clack/prompts";import{Command as F}from"commander";import{computeStateDiff as Y,install as W,isEmptyDiff as G,printInstallSummary as J}from"./commands/install.js";import{readPriorLockfileOrUndefined as X}from"./commands/lifecycle.js";import{list as z,listJson as Z,printListResult as B}from"./commands/list.js";import{assertLatestContentRefMutex as H,buildInstallPlanFromFlags as Q,MODULES_ALL_SENTINEL as x,parseContentRefOrThrow as T}from"./core/flags.js";import{loadManifest as L}from"./core/manifest.js";import{confirmInteractive as E,runInteractivePrompts as ee}from"./core/prompts.js";import{assertWrapperPreconditions as te,WRAPPER_SCAFFOLD_MODULE_ID as A}from"./core/wrapper.js";import{assertTargetDirPreconditions as $,resolveTargetDir as b}from"./core/target-dir.js";import{CliError as c}from"./errors.js";import{log as l,setLogGuide as oe}from"./log.js";import{welcomeBanner as ne}from"./ui/banner.js";import{dispatchAuthStatus as re,dispatchAuthLogout as ie}from"./commands/auth.js";import{probeAndResolveAuth as se}from"./auth/index.js";import{createResolver as ae}from"./resolver/index.js";import{discoverVersions as le}from"./resolver/release-discovery.js";import{CONTENT_REPO as P,MAX_CONTENT_REF as C,MIN_CONTENT_REF as D}from"./generated/content-ref.js";import{tagSemverGte as de,tagSemverLt as ce}from"./util/semver.js";import{acquireAdvisoryLock as ue}from"./util/advisory-lock.js";import{getPackageRoot as fe}from"./util/package-root.js";import{createVerboseLogger as M}from"./util/verbose-log.js";import{ListOutputSchema as pe}from"./schemas/list-output.js";async function dt(e={}){const o=S(e),t=new F,n=await Se();t.name("ai1st-kit").description("Nortal AI1st-Kit installer").version(n),t.command("install").description("Install, update, or converge an AI1st-Kit installation").option("--mode <mode>","install mode (existing-repository | wrapper-repository)").option("--modules <csv>",'comma-separated module IDs, or "all" for every non-MCP module declared by the kit').option("--mcp <csv>","comma-separated MCP module IDs").option("--constitutions <csv>","comma-separated constitution module IDs").option("--hooks","install hooks").option("--yes","non-interactive mode (every required flag must be provided)").option("--verbose","write a structured log of the run").option("--content-ref <vX.Y.Z>","Pin to a specific AI1st-Kit version (overrides the picker / latest-resolution).").option("--latest","Resolve to the latest in-range AI1st-Kit version (skips the picker). Mutually exclusive with --content-ref.").option("--confirm-uninstall",'Acknowledge that an empty --modules and --mcp selection means "uninstall the kit and delete the lockfile".').option("--target-dir <path>","project root to install into (defaults to the current working directory)").action(async i=>{await me(i,o)}),t.command("list").description("List installed modules").option("--json","emit a stable JSON document instead of human-readable text").option("--verbose","write a structured log of the run").option("--target-dir <path>","project root to read the lockfile from (defaults to the current working directory)").action(async i=>{await Me(i)});const r=t.command("auth").description("Manage GitLab credentials");return r.command("status").description("Show current auth mode and stored token status").action(re),r.command("logout").description("Delete stored GitLab PAT from the OS keychain").action(ie),t}async function me(e,o={}){const t=S(o),n=e.yes??!1;!n&&process.stdout.isTTY===!0&&(l.plain(ne()),oe(!0));const i=process.cwd(),s=Pe(e.mode),f=await Oe({flags:e,originalCwd:i,deps:t});if(f.kind==="cancelled"){l.info("Install cancelled.");return}const d=f.targetDir;await $(d,s),s==="wrapper-repository"&&await U(d,{recursive:!0});const p=e.verbose??!1,a=M({targetDir:d,enabled:p});g(a,"info","install start",{flags:e});const m=await ue({targetDir:d,command:"install"});try{H({latest:e.latest??!1,contentRef:T(e.contentRef)});const u=await X(d);if(Ne(e.mode,u),n&&u===void 0&&e.mode===void 0)throw new c({code:"E_INVALID_INPUT",message:"install --yes requires --mode=<existing-repository|wrapper-repository>. Next: re-run with --mode=existing-repository or --mode=wrapper-repository.",headline:"`install --yes` requires `--mode=...`.",todo:["Re-run with `--mode=existing-repository` (drop in to a project) or `--mode=wrapper-repository` (umbrella folder)."]});const w=await t.probeAuth({contentRepo:P,interactive:!n}),v=await he({flags:e,priorLockfile:u,deps:t,targetDir:d,seedMode:s,auth:w});if(v.kind==="cancelled"){l.info("Install cancelled."),g(a,"info","install cancelled"),await a.flush();return}const{plan:h,manifest:R,resolver:N}=v.state,I=Y({priorLockfile:u,desiredModules:h.modules,desiredMcp:h.mcp,desiredContentRef:h.contentRef,kitConfig:R});if(I.reverseDepConflicts.length>0)throw Ae(I);if(G(I)&&!ke(u)){l.ok("Already at desired state; no changes."),g(a,"info","install end",{noOp:!0}),await a.flush();return}const k=be(h,I,u);if(k&&De(h,e),xe(I,k),!h.yes&&await Ce(k)!=="confirmed"){l.info("Install cancelled."),g(a,"info","install cancelled",{stage:"proceed-confirm"}),await a.flush();return}const y=await W({plan:h,targetDir:d,resolver:N,stateDiff:I,uninstallEmptyState:k});if(J(y),g(a,"info","install end",{installedModules:y.installedModules,removedModules:y.removedModules,conflicts:y.conflicts.length,lockfileDeleted:y.lockfileDeleted}),h.yes&&y.conflicts.length>0)throw new c({code:"E_CONFLICT",message:`${y.conflicts.length} file(s) conflict in --yes mode. Next: review the .new files listed in the summary, merge by hand, delete the .new files, and re-run with --yes once the working tree is clean.`});await a.flush()}catch(u){throw a.setEnabled(!0),g(a,"error","install error",{error:j(u)}),await a.flush(),u}finally{await m.release()}}async function he(e){const{flags:o,priorLockfile:t,deps:n,targetDir:r,seedMode:i,auth:s}=e,f=T(o.contentRef),d=o.yes??!1,p=await n.discoverVersions({auth:s,contentRepo:P,minContentRef:D,maxContentRef:C});if(p.length===0)throw new c({code:"E_RESOLVE",message:`No AI1st-Kit versions found in the compat range [${D}, ${C??"unbounded"}). Next: ask the content repo owner to publish a release tag, or upgrade the CLI: npx @nortal/ai1st-kit-cli@latest install.`,headline:"Couldn't find any AI1st-Kit versions in your compatibility range.",todo:[`The content repo has no strict vX.Y.Z tag in [${D}, ${C??"unbounded"}).`,"Upgrade the CLI (npx @nortal/ai1st-kit-cli@latest install) or contact the content-repo owner."]});return f!==void 0&&(Le(f),$e(f,p)),d?ge({flags:o,priorLockfile:t,versions:p,auth:s,deps:n,explicitContentRef:f,targetDir:r,seedMode:i}):ye({flags:o,priorLockfile:t,versions:p,auth:s,deps:n,explicitContentRef:f,targetDir:r,seedMode:i})}async function ge(e){const{flags:o,priorLockfile:t,versions:n,auth:r,deps:i,explicitContentRef:s,targetDir:f}=e,d=ve({explicitContentRef:s,latest:o.latest??!1,priorLockfile:t,versions:n}),p=Ie(o,t),a=Q(p,d);if(t===void 0&&a.missingFields.includes("mode"))throw new c({code:"E_INVALID_INPUT",message:"install --yes requires --mode=<existing-repository|wrapper-repository>. Next: re-run with --mode=existing-repository or --mode=wrapper-repository.",headline:"`install --yes` requires `--mode=...`.",todo:["Re-run with `--mode=existing-repository` (drop in to a project) or `--mode=wrapper-repository` (umbrella folder)."]});let m=a.plan;if(t!==void 0&&a.missingFields.includes("mode")){const N=t.modules[A]!==void 0?"wrapper-repository":"existing-repository";m={...a.plan,mode:N}}m.mode==="wrapper-repository"&&t===void 0&&await te(f);const u=i.buildResolver(r,d),w=m.modules.length===1&&m.modules[0]===x,v=await u.resolve("remote-git:."),h=await L(v.rootDir);let R=m;if(w&&(R=Te(m,h)),t===void 0&&R.modules.length===0&&R.mcp.length===0)throw new c({code:"E_INVALID_INPUT",message:"--yes requires an explicit module selection. Next: pass --modules=<csv> with the modules you want, or --modules=all to install every module declared by the kit.",headline:"--yes requires an explicit module selection.",todo:["Pass --modules=<csv> with the modules you want.","Use --modules=all to install every module declared by the kit."]});return we({contentRef:d,explicitContentRef:s,latest:o.latest??!1,priorInstalledRef:t?.installedRef??null}),{kind:"resolved",state:{plan:R,auth:r,resolver:u,manifest:h}}}function we(e){if(e.explicitContentRef!==void 0){l.info(`Using AI1st-Kit ${e.contentRef} (--content-ref pin)`);return}if(e.latest){l.info(`Using AI1st-Kit ${e.contentRef} (--latest)`);return}if(e.priorInstalledRef!==null&&e.priorInstalledRef===e.contentRef){l.info(`Using AI1st-Kit ${e.contentRef} (carried forward from prior install)`);return}if(e.priorInstalledRef!==null&&e.priorInstalledRef!==e.contentRef){l.warn(`Prior install pinned ${e.priorInstalledRef}, which is no longer available in the content repo; falling back to latest in range: ${e.contentRef}.`);return}l.info(`Using AI1st-Kit ${e.contentRef} (latest in range)`)}async function ye(e){const{flags:o,priorLockfile:t,versions:n,auth:r,deps:i,explicitContentRef:s,seedMode:f}=e;s!==void 0&&l.info(`Using AI1st-Kit ${s} (--content-ref pin)`);const d=Re(s,t?.installedRef??null,n),p=i.buildResolver(r,d),a=await p.resolve("remote-git:."),m=await L(a.rootDir),u=o.verbose??!1,w=await ee({manifest:m,verbose:u,seedMode:f,versions:n,...s!==void 0?{preselectedContentRef:s}:{},...t!==void 0?{priorLockfile:t}:{}});if(w.kind==="cancelled")return{kind:"cancelled"};const v=w.plan.contentRef===d?p:i.buildResolver(r,w.plan.contentRef);return{kind:"resolved",state:{plan:w.plan,auth:r,resolver:v,manifest:m}}}function ve(e){if(e.explicitContentRef!==void 0)return e.explicitContentRef;if(e.latest)return e.versions[0].tag;const o=e.priorLockfile?.installedRef;return typeof o=="string"&&e.versions.some(t=>t.tag===o)?o:e.versions[0].tag}function Re(e,o,t){return e!==void 0?e:o!==null&&t.some(n=>n.tag===o)?o:t[0].tag}function Ie(e,o){if(o===void 0)return e;const t={...e};if(e.modules===void 0){const n=_(o,"non-mcp");t.modules=n.join(",")}if(e.mcp===void 0){const n=_(o,"mcp");t.mcp=n.join(",")}return t}function _(e,o){const t=[];for(const n of Object.keys(e.modules)){if(n===A)continue;const r=e.modules[n];(o==="mcp"?r.kind==="mcp":r.kind!=="mcp")&&t.push(n)}return t.sort()}function ke(e){if(e===void 0)return!1;for(const o of Object.values(e.modules))if(Object.keys(o.customizations).length>0)return!0;return!1}function be(e,o,t){return t===void 0||e.modules.length>0||e.mcp.length>0?!1:o.modulesToKeep.length===0&&o.modulesToAdd.length===0&&o.mcpToKeep.length===0&&o.mcpToAdd.length===0}async function Ce(e){return e?E({message:"This will uninstall every module and delete the lockfile. Proceed?",initialValue:!1}):E({message:"Proceed?",initialValue:!0})}function De(e,o){const t=e.yes,n=o.confirmUninstall===!0;if(t&&!n)throw new c({code:"E_INVALID_INPUT",message:"Refusing to uninstall without --confirm-uninstall. Next: re-run with --confirm-uninstall to confirm, or omit --modules=/--mcp= to keep your current selection.",headline:"Refusing to uninstall without --confirm-uninstall.",todo:["Re-run with --confirm-uninstall to confirm.","Or omit --modules=/--mcp= to keep your current selection."]})}function Ne(e,o){if(e===void 0||o===void 0)return;const t=o.modules[A]!==void 0?"wrapper-repository":"existing-repository";if(e!==t)throw e!=="existing-repository"&&e!=="wrapper-repository"?new c({code:"E_INVALID_INPUT",message:`Unknown --mode value "${e}". Next: pass --mode=existing-repository or --mode=wrapper-repository.`,headline:`Unknown --mode value "${e}".`,todo:["Pass --mode=existing-repository or --mode=wrapper-repository."]}):new c({code:"E_INVALID_INPUT",message:`--mode=${e} contradicts the prior lockfile (mode: ${t}). Next: re-run without --mode= or with --mode=${t}.`,headline:`--mode=${e} contradicts the prior lockfile.`,todo:[`The lockfile records mode: ${t}.`,`Re-run without --mode= or with --mode=${t}.`]})}function Ae(e){const o=e.reverseDepConflicts.map(n=>` ${n.removed} -> still required by ${n.retainedDependents.join(", ")}`),t=e.reverseDepConflicts.map(n=>`Keep ${n.removed} or remove the modules that require it (${n.retainedDependents.join(", ")}).`);return new c({code:"E_RESOLVE",message:`Refusing to remove modules that other installed modules still require:
1
+ import{access as B,mkdir as X,readFile as z}from"node:fs/promises";import{join as L}from"node:path";import*as Z from"@clack/prompts";import{Command as Q}from"commander";import{computeStateDiff as ee,install as te,isEmptyDiff as oe,printInstallSummary as ne}from"./commands/install.js";import{readPriorLockfileOrUndefined as re}from"./commands/lifecycle.js";import{list as ie,listJson as se,printListResult as ae}from"./commands/list.js";import{assertLatestContentRefMutex as le,buildInstallPlanFromFlags as de,MODULES_ALL_SENTINEL as T,parseContentRefOrThrow as E}from"./core/flags.js";import{loadManifest as P}from"./core/manifest.js";import{expandPlan as ce}from"./core/planner.js";import{confirmInteractive as M,runInteractivePrompts as ue}from"./core/prompts.js";import{assertWrapperPreconditions as pe,WRAPPER_SCAFFOLD_MODULE_ID as S}from"./core/wrapper.js";import{assertTargetDirPreconditions as $,resolveTargetDir as C}from"./core/target-dir.js";import{CliError as c}from"./errors.js";import{log as d,setLogGuide as me}from"./log.js";import{welcomeBanner as fe}from"./ui/banner.js";import{dispatchAuthStatus as he,dispatchAuthLogout as we}from"./commands/auth.js";import{probeAndResolveAuth as ge}from"./auth/index.js";import{createResolver as ye}from"./resolver/index.js";import{startSpinner as _}from"./ui/spinner.js";import{buildWithinScopeRequires as j,computeAutoDepsClosure as O}from"./util/auto-deps-multiselect.js";import{discoverVersions as ve}from"./resolver/release-discovery.js";import{CONTENT_REPO as V,MAX_CONTENT_REF as x,MIN_CONTENT_REF as D}from"./generated/content-ref.js";import{tagSemverGte as ke,tagSemverLt as Ie}from"./util/semver.js";import{acquireAdvisoryLock as Re}from"./util/advisory-lock.js";import{getPackageRoot as be}from"./util/package-root.js";import{createVerboseLogger as U}from"./util/verbose-log.js";import{ListOutputSchema as Ae}from"./schemas/list-output.js";async function Lt(e={}){const o=H(e),t=new Q,n=await tt();t.name("ai1st-kit").description("Nortal AI1st-Kit installer").version(n),t.command("install").description("Install, update, or converge an AI1st-Kit installation").option("--mode <mode>","install mode (existing-repository | wrapper-repository)").option("--modules <csv>",'comma-separated module IDs, or "all" for every non-MCP module declared by the kit').option("--mcp <csv>","comma-separated MCP module IDs").option("--constitutions <csv>","comma-separated constitution module IDs").option("--hooks","install hooks").option("--yes","non-interactive mode (every required flag must be provided)").option("--verbose","write a structured log of the run").option("--content-ref <vX.Y.Z>","Pin to a specific AI1st-Kit version (overrides the picker / latest-resolution).").option("--latest","Resolve to the latest in-range AI1st-Kit version (skips the picker). Mutually exclusive with --content-ref.").option("--confirm-uninstall",'Acknowledge that an empty --modules and --mcp selection means "uninstall the kit and delete the lockfile".').option("--target-dir <path>","project root to install into (defaults to the current working directory)").action(async i=>{await Ne(i,o)}),t.command("list").description("List installed modules").option("--json","emit a stable JSON document instead of human-readable text").option("--verbose","write a structured log of the run").option("--target-dir <path>","project root to read the lockfile from (defaults to the current working directory)").action(async i=>{await Ze(i)});const r=t.command("auth").description("Manage GitLab credentials");return r.command("status").description("Show current auth mode and stored token status").action(he),r.command("logout").description("Delete stored GitLab PAT from the OS keychain").action(we),t}async function Ne(e,o={}){const t=H(o),n=e.yes??!1,r=process.cwd(),i=await xe({yesMode:n,flags:e,originalCwd:r});De(e,i);const s=!n&&process.stdout.isTTY===!0,p=e.verbose??!1,g=await Le({probeAuth:t.probeAuth,yesMode:n,interactive:s,verbose:p});s&&(d.plain(fe()),me(!0));const a=ze(e.mode),m=await et({flags:e,originalCwd:r,deps:t});if(m.kind==="cancelled"){d.info("Install cancelled.");return}const u=m.targetDir;await $(u,a),a==="wrapper-repository"&&await X(u,{recursive:!0});const l=U({targetDir:u,enabled:p});I(l,"info","install start",{flags:e});const f=await Re({targetDir:u,command:"install"});try{le({latest:e.latest??!1,contentRef:E(e.contentRef)});const h=await re(u);if(qe(e.mode,h),n&&h===void 0&&e.mode===void 0)throw new c({code:"E_INVALID_INPUT",message:"install --yes requires --mode=<existing-repository|wrapper-repository>. Next: re-run with --mode=existing-repository or --mode=wrapper-repository.",headline:"`install --yes` requires `--mode=...`.",todo:["Re-run with `--mode=existing-repository` (drop in to a project) or `--mode=wrapper-repository` (umbrella folder)."]});const y=await Ee({flags:e,priorLockfile:h,deps:t,targetDir:u,seedMode:a,auth:g,interactive:s,verbose:p});if(y.kind==="cancelled"){d.info("Install cancelled."),I(l,"info","install cancelled"),await l.flush();return}const{plan:w,manifest:R,resolver:b,userSeed:A}=y.state;Ye(w,R,A);const v=ee({priorLockfile:h,desiredModules:w.modules,desiredMcp:w.mcp,desiredContentRef:w.contentRef,kitConfig:R});if(v.reverseDepConflicts.length>0)throw Fe(v);if(oe(v)&&!Oe(h)){d.ok("Already at desired state; no changes."),I(l,"info","install end",{noOp:!0}),await l.flush();return}const N=Ve(w,v,h);if(N&&Ke(w,e),We(v,N),!w.yes&&await Ue(N)!=="confirmed"){d.info("Install cancelled."),I(l,"info","install cancelled",{stage:"proceed-confirm"}),await l.flush();return}const k=await te({plan:w,targetDir:u,resolver:b,stateDiff:v,uninstallEmptyState:N});if(ne(k),I(l,"info","install end",{installedModules:k.installedModules,removedModules:k.removedModules,conflicts:k.conflicts.length,lockfileDeleted:k.lockfileDeleted}),w.yes&&k.conflicts.length>0)throw new c({code:"E_CONFLICT",message:`${k.conflicts.length} file(s) conflict in --yes mode. Next: review the .new files listed in the summary, merge by hand, delete the .new files, and re-run with --yes once the working tree is clean.`});await l.flush()}catch(h){throw l.setEnabled(!0),I(l,"error","install error",{error:J(h)}),await l.flush(),h}finally{await f.release()}}const Ce="ai1st-kit-lock.json";async function xe(e){if(!e.yesMode||e.flags.mode!==void 0)return!1;const o=e.flags.targetDir??e.originalCwd,t=C(o,e.originalCwd);try{return await B(L(t,Ce)),!0}catch{return!1}}function De(e,o){if(e.yes===!0&&e.mode===void 0&&!o)throw new c({code:"E_INVALID_INPUT",message:"install --yes requires --mode=<existing-repository|wrapper-repository>. Next: re-run with --mode=existing-repository or --mode=wrapper-repository.",headline:"`install --yes` requires `--mode=...`.",todo:["Re-run with `--mode=existing-repository` (drop in to a project) or `--mode=wrapper-repository` (umbrella folder)."]})}function Se(e){switch(e){case"ssh":return"Authenticated via SSH";case"https":return"Authenticated via HTTPS";case"pat-stored":return"Authenticated via stored token";case"pat-prompted":return"Authenticated via new token";case"env-token":return"Authenticated via env token";default:return e}}async function Le(e){const o=e.interactive&&!e.verbose,t=_({message:"Authenticating with GitLab\u2026",enabled:o});let n=!1;try{const r=await e.probeAuth({contentRepo:V,interactive:!e.yesMode},{onBeforePatPrompt:()=>{n=!0,t.stop("Credentials needed \u2014 please paste a PAT")}});if(e.yesMode)return r;const i=Se(r.mode);return n?d.ok(i):o?t.stop(i):d.ok(i),r}catch(r){throw n||t.stop("Authentication failed",1),r}}function K(e,o){return e.find(t=>t.tag===o)?.sha}async function Te(e,o){const t=_({message:e.message,enabled:e.enabled});try{const n=await o();return t.stop(e.message),n}catch(n){throw t.stop(e.message,1),n}}async function Ee(e){const{flags:o,priorLockfile:t,deps:n,targetDir:r,seedMode:i,auth:s,interactive:p,verbose:g}=e,a=E(o.contentRef),m=o.yes??!1;if(a!==void 0&&Be(a),m){const u=await q({auth:s,deps:n,interactive:p,verbose:g});if(u.length===0)throw F();return a!==void 0&&G(a,u),Pe({flags:o,priorLockfile:t,versions:u,auth:s,deps:n,explicitContentRef:a,targetDir:r,seedMode:i,interactive:p,verbose:g})}return $e({flags:o,priorLockfile:t,versions:void 0,auth:s,deps:n,explicitContentRef:a,targetDir:r,seedMode:i,interactive:p,verbose:g})}async function q(e){return Te({message:"Discovering AI1st-Kit versions\u2026",enabled:e.interactive&&!e.verbose},()=>e.deps.discoverVersions({auth:e.auth,contentRepo:V,minContentRef:D,maxContentRef:x}))}function F(){return new c({code:"E_RESOLVE",message:`No AI1st-Kit versions found in the compat range [${D}, ${x??"unbounded"}). Next: ask the content repo owner to publish a release tag, or upgrade the CLI: npx @nortal/ai1st-kit-cli@latest install.`,headline:"Couldn't find any AI1st-Kit versions in your compatibility range.",todo:[`The content repo has no strict vX.Y.Z tag in [${D}, ${x??"unbounded"}).`,"Upgrade the CLI (npx @nortal/ai1st-kit-cli@latest install) or contact the content-repo owner."]})}async function Pe(e){const{flags:o,priorLockfile:t,versions:n,auth:r,deps:i,explicitContentRef:s,targetDir:p,interactive:g,verbose:a}=e;if(n===void 0)throw new c({code:"E_INTERNAL",message:"resolveYesModeDesiredState: versions list is required"});const m=_e({explicitContentRef:s,latest:o.latest??!1,priorLockfile:t,versions:n}),u=je(o,t),l=de(u,m);if(t===void 0&&l.missingFields.includes("mode"))throw new c({code:"E_INVALID_INPUT",message:"install --yes requires --mode=<existing-repository|wrapper-repository>. Next: re-run with --mode=existing-repository or --mode=wrapper-repository.",headline:"`install --yes` requires `--mode=...`.",todo:["Re-run with `--mode=existing-repository` (drop in to a project) or `--mode=wrapper-repository` (umbrella folder)."]});let f=l.plan;if(t!==void 0&&l.missingFields.includes("mode")){const k=t.modules[S]!==void 0?"wrapper-repository":"existing-repository";f={...l.plan,mode:k}}f.mode==="wrapper-repository"&&t===void 0&&await pe(p);const h=K(n,m),y=i.buildResolver(r,m,{interactive:g,verbose:a,...h!==void 0?{contentSha:h}:{}}),w=f.modules.length===1&&f.modules[0]===T,R=await y.resolve("remote-git:."),b=await P(R.rootDir),A=w?Ge(f,b):f;if(t===void 0&&A.modules.length===0&&A.mcp.length===0)throw new c({code:"E_INVALID_INPUT",message:"--yes requires an explicit module selection. Next: pass --modules=<csv> with the modules you want, or --modules=all to install every module declared by the kit.",headline:"--yes requires an explicit module selection.",todo:["Pass --modules=<csv> with the modules you want.","Use --modules=all to install every module declared by the kit."]});He(A,b,{modules:o.modules!==void 0,mcp:o.mcp!==void 0});const v=Je(A,b),N={modules:new Set(v.modules),mcp:new Set(v.mcp),constitutions:new Set(v.constitutions)};return Me({contentRef:m,explicitContentRef:s,latest:o.latest??!1,priorInstalledRef:t?.installedRef??null}),{kind:"resolved",state:{plan:v,auth:r,resolver:y,manifest:b,userSeed:N}}}function Me(e){if(e.explicitContentRef!==void 0){d.info(`Using AI1st-Kit ${e.contentRef} (--content-ref pin)`);return}if(e.latest){d.info(`Using AI1st-Kit ${e.contentRef} (--latest)`);return}if(e.priorInstalledRef!==null&&e.priorInstalledRef===e.contentRef){d.info(`Using AI1st-Kit ${e.contentRef} (carried forward from prior install)`);return}if(e.priorInstalledRef!==null&&e.priorInstalledRef!==e.contentRef){d.warn(`Prior install pinned ${e.priorInstalledRef}, which is no longer available in the content repo; falling back to latest in range: ${e.contentRef}.`);return}d.info(`Using AI1st-Kit ${e.contentRef} (latest in range)`)}async function $e(e){const{flags:o,priorLockfile:t,versions:n,auth:r,deps:i,explicitContentRef:s,seedMode:p,interactive:g,verbose:a}=e;s!==void 0&&d.info(`Using AI1st-Kit ${s} (--content-ref pin)`);let m=n,u,l;const f=await ue({loadManifestForTag:async y=>{const w=m!==void 0?K(m,y):void 0;u=i.buildResolver(r,y,{interactive:g,verbose:a,...w!==void 0?{contentSha:w}:{}});const R=await u.resolve("remote-git:.");return l=await P(R.rootDir),l},verbose:a,seedMode:p,...m!==void 0?{versions:m}:{loadVersions:async()=>{const y=await q({auth:r,deps:i,interactive:g,verbose:a});if(y.length===0)throw F();return s!==void 0&&G(s,y),m=y,y}},...s!==void 0?{preselectedContentRef:s}:{},...t!==void 0?{priorLockfile:t}:{}});if(f.kind==="cancelled")return{kind:"cancelled"};if(u===void 0||l===void 0)throw new c({code:"E_INTERNAL",message:"runInteractivePrompts: returned a plan without invoking loadManifestForTag (bug tripwire)"});const h={modules:new Set(f.plan.modules),mcp:new Set(f.plan.mcp),constitutions:new Set(f.plan.constitutions)};return{kind:"resolved",state:{plan:f.plan,auth:r,resolver:u,manifest:l,userSeed:h}}}function _e(e){if(e.explicitContentRef!==void 0)return e.explicitContentRef;if(e.latest)return e.versions[0].tag;const o=e.priorLockfile?.installedRef;return typeof o=="string"&&e.versions.some(t=>t.tag===o)?o:e.versions[0].tag}function je(e,o){if(o===void 0)return e;const t={...e};if(e.modules===void 0){const n=Y(o,"non-mcp");t.modules=n.join(",")}if(e.mcp===void 0){const n=Y(o,"mcp");t.mcp=n.join(",")}return t}function Y(e,o){const t=[];for(const n of Object.keys(e.modules)){if(n===S)continue;const r=e.modules[n];(o==="mcp"?r.kind==="mcp":r.kind!=="mcp")&&t.push(n)}return t.sort()}function Oe(e){if(e===void 0)return!1;for(const o of Object.values(e.modules))if(Object.keys(o.customizations).length>0)return!0;return!1}function Ve(e,o,t){return t===void 0||e.modules.length>0||e.mcp.length>0?!1:o.modulesToKeep.length===0&&o.modulesToAdd.length===0&&o.mcpToKeep.length===0&&o.mcpToAdd.length===0}async function Ue(e){return e?M({message:"This will uninstall every module and delete the lockfile. Proceed?",initialValue:!1}):M({message:"Proceed?",initialValue:!0})}function Ke(e,o){const t=e.yes,n=o.confirmUninstall===!0;if(t&&!n)throw new c({code:"E_INVALID_INPUT",message:"Refusing to uninstall without --confirm-uninstall. Next: re-run with --confirm-uninstall to confirm, or omit --modules=/--mcp= to keep your current selection.",headline:"Refusing to uninstall without --confirm-uninstall.",todo:["Re-run with --confirm-uninstall to confirm.","Or omit --modules=/--mcp= to keep your current selection."]})}function qe(e,o){if(e===void 0||o===void 0)return;const t=o.modules[S]!==void 0?"wrapper-repository":"existing-repository";if(e!==t)throw e!=="existing-repository"&&e!=="wrapper-repository"?new c({code:"E_INVALID_INPUT",message:`Unknown --mode value "${e}". Next: pass --mode=existing-repository or --mode=wrapper-repository.`,headline:`Unknown --mode value "${e}".`,todo:["Pass --mode=existing-repository or --mode=wrapper-repository."]}):new c({code:"E_INVALID_INPUT",message:`--mode=${e} contradicts the prior lockfile (mode: ${t}). Next: re-run without --mode= or with --mode=${t}.`,headline:`--mode=${e} contradicts the prior lockfile.`,todo:[`The lockfile records mode: ${t}.`,`Re-run without --mode= or with --mode=${t}.`]})}function Fe(e){const o=e.reverseDepConflicts.map(n=>` ${n.removed} -> still required by ${n.retainedDependents.join(", ")}`),t=e.reverseDepConflicts.map(n=>`Keep ${n.removed} or remove the modules that require it (${n.retainedDependents.join(", ")}).`);return new c({code:"E_RESOLVE",message:`Refusing to remove modules that other installed modules still require:
2
2
  ${o.join(`
3
3
  `)}
4
- Next: keep the required modules or remove their dependents too.`,headline:"Refusing to remove modules that other installed modules still require.",todo:t})}function xe(e,o){if(o&&l.warn("Empty desired state -- this will uninstall every module and delete the lockfile."),e.tagChange!==null){const t=e.tagChange.from??"(none)";l.info(`Content tag: ${t} -> ${e.tagChange.to}`)}e.modulesToAdd.length>0&&l.info(`Modules to add: ${e.modulesToAdd.join(", ")}`),e.modulesToRemove.length>0&&l.info(`Modules to remove: ${e.modulesToRemove.join(", ")}`),e.mcpToAdd.length>0&&l.info(`MCP to add: ${e.mcpToAdd.join(", ")}`),e.mcpToRemove.length>0&&l.info(`MCP to remove: ${e.mcpToRemove.join(", ")}`)}function Te(e,o){if(e.modules.length!==1||e.modules[0]!==x)return e;const t=Object.keys(o.modules).filter(n=>o.modules[n].kind!=="mcp").sort();return{...e,modules:t}}function Le(e){Ee(e,D,C)}function Ee(e,o,t){de(e,o)||O(e,o,t),t!==null&&!ce(e,t)&&O(e,o,t)}function O(e,o,t){const n=t??"unbounded";throw new c({code:"E_INVALID_INPUT",message:`--content-ref ${e} is outside the compat range [${o}, ${n}). Next: pass a tag in range, or upgrade the CLI.`,headline:`AI1st-Kit version ${e} is outside this CLI's compatibility range.`,todo:[`Pass a tag in [${o}, ${n}).`,"Or upgrade the CLI: npx @nortal/ai1st-kit-cli@latest install."]})}function $e(e,o){if(o.some(n=>n.tag===e))return;const t=o.slice(0,5).map(n=>n.tag).join(", ");throw new c({code:"E_INVALID_INPUT",message:`--content-ref ${e} is not a tag in the content repo. Next: available versions: ${t}.`,headline:`AI1st-Kit version ${e} was not found in the content repo.`,todo:[`Available versions: ${t}.`]})}function Pe(e){if(e!==void 0){if(e==="existing-repository")return"existing-repository";if(e==="wrapper-repository")return"wrapper-repository";throw new c({code:"E_INVALID_INPUT",message:`Unknown --mode value "${e}". Next: pass --mode=existing-repository or --mode=wrapper-repository.`})}}async function Me(e){const o=process.cwd(),t=b(e.targetDir??o,o);await $(t,void 0);const n=e.verbose??!1,r=M({targetDir:t,enabled:n});g(r,"info","list start");try{if(e.json){const i=await Z({targetDir:t}),s=pe.safeParse(i);if(!s.success)throw new c({code:"E_INTERNAL",message:"list --json output failed schema validation",cause:s.error});l.json(JSON.stringify(s.data,null,2))}else{const i=await z({targetDir:t});B(i)}g(r,"info","list end"),await r.flush()}catch(i){throw r.setEnabled(!0),g(r,"error","list error",{error:j(i)}),await r.flush(),i}}function S(e){return{probeAuth:e.probeAuth??se,buildResolver:e.buildResolver??ae,discoverVersions:e.discoverVersions??le,promptForTargetDir:e.promptForTargetDir??_e}}async function _e(e){return q.text({message:"Where should I install the AI1st-Kit?",placeholder:e.defaultValue,defaultValue:e.defaultValue})}async function Oe(e){const{flags:o,originalCwd:t,deps:n}=e;if(o.targetDir!==void 0)return{kind:"resolved",targetDir:b(o.targetDir,t)};if(o.yes===!0)return{kind:"resolved",targetDir:b(t,t)};const r=await n.promptForTargetDir({defaultValue:t});return typeof r!="string"?{kind:"cancelled"}:{kind:"resolved",targetDir:b(r,t)}}function g(e,o,t,n){e.append({ts:new Date().toISOString(),level:o,message:t,...n!==void 0?{data:n}:{}})}function j(e){return e instanceof c?{code:e.code,message:e.message,stack:e.stack??null}:e instanceof Error?{name:e.name,message:e.message,stack:e.stack??null}:{value:String(e)}}async function Se(){const e=K(fe(),"package.json"),o=await V(e,"utf8"),t=JSON.parse(o);if(t===null||typeof t!="object"||!("version"in t)||typeof t.version!="string")throw new c({code:"E_INTERNAL",message:'package.json is missing a string "version" field. Next: rebuild the CLI tarball.'});return t.version}export{Le as assertContentRefInRange,Ee as assertContentRefInRangeAgainst,dt as buildCliProgram,me as dispatchInstall,Me as dispatchList};
4
+ Next: keep the required modules or remove their dependents too.`,headline:"Refusing to remove modules that other installed modules still require.",todo:t})}function Ye(e,o,t){const n=new Set([...t.modules,...t.mcp,...t.constitutions]),i=ce(e,o).filter(s=>!n.has(s)).sort();i.length!==0&&d.info(`Also installing required dependencies: ${i.join(", ")}`)}function We(e,o){if(o&&d.warn("Empty desired state -- this will uninstall every module and delete the lockfile."),e.tagChange!==null){const t=e.tagChange.from??"(none)";d.info(`Content tag: ${t} -> ${e.tagChange.to}`)}e.modulesToAdd.length>0&&d.info(`Modules to add: ${e.modulesToAdd.join(", ")}`),e.modulesToRemove.length>0&&d.info(`Modules to remove: ${e.modulesToRemove.join(", ")}`),e.mcpToAdd.length>0&&d.info(`MCP to add: ${e.mcpToAdd.join(", ")}`),e.mcpToRemove.length>0&&d.info(`MCP to remove: ${e.mcpToRemove.join(", ")}`)}function Ge(e,o){if(e.modules.length!==1||e.modules[0]!==T)return e;const t=Object.keys(o.modules).filter(n=>o.modules[n].kind!=="mcp").sort();return{...e,modules:t}}function He(e,o,t){const n=new Set(Object.keys(o.modules).filter(a=>o.modules[a].kind!=="mcp")),r=new Set(Object.keys(o.modules).filter(a=>o.modules[a].kind==="mcp")),i=t.modules?e.modules.filter(a=>!n.has(a)):[],s=t.mcp?e.mcp.filter(a=>!r.has(a)):[];if(i.length===0&&s.length===0)return;const p=[];i.length>0&&p.push(`--modules: ${i.join(", ")}`),s.length>0&&p.push(`--mcp: ${s.join(", ")}`);const g=p.join("; ");throw new c({code:"E_INVALID_INPUT",message:`Unknown module id(s): ${g}. Next: run \`npx @nortal/ai1st-kit-cli list\` to see the modules declared by the current AI1st-Kit version.`,headline:`Unknown module id(s): ${g}.`,todo:["Re-run with module ids that the current AI1st-Kit version declares.","Run `npx @nortal/ai1st-kit-cli list` to see what is available."]})}function Je(e,o){const t=new Set(Object.keys(o.modules).filter(s=>o.modules[s].kind!=="mcp")),n=new Set(Object.keys(o.modules).filter(s=>o.modules[s].kind==="mcp")),r=O(new Set(e.modules),j(o,"non-mcp"),t),i=O(new Set(e.mcp),j(o,"mcp"),n);return{...e,modules:Array.from(r).sort(),mcp:Array.from(i).sort()}}function Be(e){Xe(e,D,x)}function Xe(e,o,t){ke(e,o)||W(e,o,t),t!==null&&!Ie(e,t)&&W(e,o,t)}function W(e,o,t){const n=t??"unbounded";throw new c({code:"E_INVALID_INPUT",message:`--content-ref ${e} is outside the compat range [${o}, ${n}). Next: pass a tag in range, or upgrade the CLI.`,headline:`AI1st-Kit version ${e} is outside this CLI's compatibility range.`,todo:[`Pass a tag in [${o}, ${n}).`,"Or upgrade the CLI: npx @nortal/ai1st-kit-cli@latest install."]})}function G(e,o){if(o.some(n=>n.tag===e))return;const t=o.slice(0,5).map(n=>n.tag).join(", ");throw new c({code:"E_INVALID_INPUT",message:`--content-ref ${e} is not a tag in the content repo. Next: available versions: ${t}.`,headline:`AI1st-Kit version ${e} was not found in the content repo.`,todo:[`Available versions: ${t}.`]})}function ze(e){if(e!==void 0){if(e==="existing-repository")return"existing-repository";if(e==="wrapper-repository")return"wrapper-repository";throw new c({code:"E_INVALID_INPUT",message:`Unknown --mode value "${e}". Next: pass --mode=existing-repository or --mode=wrapper-repository.`})}}async function Ze(e){const o=process.cwd(),t=C(e.targetDir??o,o);await $(t,void 0);const n=e.verbose??!1,r=U({targetDir:t,enabled:n});I(r,"info","list start");try{if(e.json){const i=await se({targetDir:t}),s=Ae.safeParse(i);if(!s.success)throw new c({code:"E_INTERNAL",message:"list --json output failed schema validation",cause:s.error});d.json(JSON.stringify(s.data,null,2))}else{const i=await ie({targetDir:t});ae(i)}I(r,"info","list end"),await r.flush()}catch(i){throw r.setEnabled(!0),I(r,"error","list error",{error:J(i)}),await r.flush(),i}}function H(e){return{probeAuth:e.probeAuth??ge,buildResolver:e.buildResolver??ye,discoverVersions:e.discoverVersions??ve,promptForTargetDir:e.promptForTargetDir??Qe}}async function Qe(e){return Z.text({message:"Where should I install the AI1st-Kit?",placeholder:e.defaultValue,defaultValue:e.defaultValue})}async function et(e){const{flags:o,originalCwd:t,deps:n}=e;if(o.targetDir!==void 0)return{kind:"resolved",targetDir:C(o.targetDir,t)};if(o.yes===!0)return{kind:"resolved",targetDir:C(t,t)};const r=await n.promptForTargetDir({defaultValue:t});return typeof r!="string"?{kind:"cancelled"}:{kind:"resolved",targetDir:C(r,t)}}function I(e,o,t,n){e.append({ts:new Date().toISOString(),level:o,message:t,...n!==void 0?{data:n}:{}})}function J(e){return e instanceof c?{code:e.code,message:e.message,stack:e.stack??null}:e instanceof Error?{name:e.name,message:e.message,stack:e.stack??null}:{value:String(e)}}async function tt(){const e=L(be(),"package.json"),o=await z(e,"utf8"),t=JSON.parse(o);if(t===null||typeof t!="object"||!("version"in t)||typeof t.version!="string")throw new c({code:"E_INTERNAL",message:'package.json is missing a string "version" field. Next: rebuild the CLI tarball.'});return t.version}export{Be as assertContentRefInRange,Xe as assertContentRefInRangeAgainst,De as assertYesRequiresMode,Lt as buildCliProgram,Ne as dispatchInstall,Ze as dispatchList};
@@ -1,2 +1,2 @@
1
- import*as d from"@clack/prompts";import{CliError as f}from"../errors.js";import{WRAPPER_SCAFFOLD_MODULE_ID as C}from"./wrapper.js";const E=12,R=["A wrapper repo is an umbrella folder placed next to your project repos.","It holds the AI1st-Kit (skills, agents, hooks, MCP wiring) once, and every","sibling project inherits the same setup -- kit updates land in one place,","and no kit files pollute a project repo's git history.","","Pick project-repo only when the kit must live inside a single project","(for example, a public repo whose contributors expect the kit committed)."].join(`
2
- `);async function S(e){return L(e,g())}async function L(e,t){if(e.versions.length===0)throw new f({code:"E_INTERNAL",message:"runInteractivePrompts: empty version list reached the picker"});const n=await A(e,t);if(n==="cancelled")return{kind:"cancelled"};const i=await O(e,t);if(i==="cancelled")return{kind:"cancelled"};const l=x(e.manifest);if(l.length===0)throw new f({code:"E_INVALID_INPUT",message:"This kit declares no non-MCP modules, so the interactive installer has nothing to offer. Next: ask the content repo owner to declare at least one `files` / `hooks` / `constitutions` module, or pin a content tag (--content-ref=vX.Y.Z) that predates the regression.",headline:"The selected AI1st-Kit version declares no installable modules.",todo:["Pin a different content tag with --content-ref=vX.Y.Z.","Or ask the content-repo owner to publish a kit that declares at least one non-MCP module."]});const a=b(e.priorLockfile,"non-mcp"),s=b(e.priorLockfile,"mcp"),o=I(a,l),p=e.priorLockfile!==void 0,c=await t.multiselect({message:"Modules to install?",options:l,required:!p,maxItems:E,...o.length>0?{initialValues:o}:{}});if(t.isCancel(c))return{kind:"cancelled"};const u=c,m=D(e.manifest);let h=[];if(m.length>0){const r=I(s,m),y=await t.multiselect({message:"MCP servers?",options:m,required:!1,...r.length>0?{initialValues:r}:{}});if(t.isCancel(y))return{kind:"cancelled"};h=y}const w=j(e.manifest);let k=[];if(w.length>0){const r=await t.multiselect({message:"Constitutions (optional)?",options:w,required:!1});if(t.isCancel(r))return{kind:"cancelled"};k=r}const _=Object.values(e.manifest.modules).some(r=>r?.kind==="hooks");let v=!1;if(_){const r=await t.confirm({message:"Install hooks?",initialValue:!1});if(t.isCancel(r))return{kind:"cancelled"};v=r}return n==="wrapper-repository"?{kind:"plan",plan:{mode:"wrapper-repository",modules:u,mcp:h,constitutions:k,hooks:v,yes:!1,verbose:e.verbose,contentRef:i}}:{kind:"plan",plan:{mode:"existing-repository",modules:u,mcp:h,constitutions:k,hooks:v,yes:!1,verbose:e.verbose,contentRef:i}}}async function O(e,t){if(e.preselectedContentRef!==void 0)return e.preselectedContentRef;const n=e.priorLockfile?.installedRef??null,i=e.versions[0].tag,l=e.versions.map((o,p)=>{const c=p===0,u=n!==null&&o.tag===n;return c&&u?{value:o.tag,label:`${o.tag} (latest, current)`}:c?{value:o.tag,label:`${o.tag} (latest)`}:u?{value:o.tag,label:`${o.tag} (current)`}:{value:o.tag,label:o.tag}}),a=n!==null&&e.versions.some(o=>o.tag===n)?n:i,s=await t.select({message:"Which AI1st-Kit version would you like to install?",options:l,initialValue:a});return t.isCancel(s)?"cancelled":s}async function A(e,t){if(e.seedMode!==void 0)return e.seedMode;if(e.priorLockfile!==void 0)return N(e.priorLockfile);d.log.message(R);const n=await t.select({message:"Install mode?",options:[{value:"wrapper-repository",label:"wrapper-repo (recommended -- creates an umbrella repository around project repos)"},{value:"existing-repository",label:"project-repo (install into the current project repo)"}]});return t.isCancel(n)?"cancelled":n}function N(e){return e.modules[C]!==void 0?"wrapper-repository":"existing-repository"}function I(e,t){const n=new Set(t.map(i=>i.value));return e.filter(i=>n.has(i))}function b(e,t){if(e===void 0)return[];const n=[];for(const i of Object.keys(e.modules)){if(i===C)continue;const l=e.modules[i];(t==="mcp"?l.kind==="mcp":l.kind!=="mcp")&&n.push(i)}return n.sort()}function g(){return{select:d.select,multiselect:d.multiselect,confirm:d.confirm,isCancel:d.isCancel}}const M={files:0,constitutions:1,hooks:2};function x(e){const t=Object.keys(e.modules).filter(n=>e.modules[n]?.kind!=="mcp");return t.sort((n,i)=>{const l=M[e.modules[n]?.kind??""]??99,a=M[e.modules[i]?.kind??""]??99;return l!==a?l-a:n.localeCompare(i)}),t.map(n=>({value:n,label:n}))}function D(e){return Object.keys(e.modules).filter(t=>e.modules[t]?.kind==="mcp").sort().map(t=>({value:t,label:t}))}function j(e){return[]}async function W(e){return T(e,g())}async function T(e,t){if(e.versions.length===0)throw new f({code:"E_INTERNAL",message:"runUpdatePrompts: empty version list reached the picker"});const n=typeof e.installedRef=="string"?e.installedRef:null,i=n===null?"No installed version recorded (v1 lockfile). Update to which version?":`Currently installed: ${n}. Update to which version?`,l=e.versions[0].tag,a=e.versions.map((o,p)=>{const c=p===0,u=n!==null&&o.tag===n;return c&&u?{value:o.tag,label:`${o.tag} (latest, current)`}:c?{value:o.tag,label:`${o.tag} (latest)`}:u?{value:o.tag,label:`${o.tag} (current)`}:{value:o.tag,label:o.tag}});a.push({value:P,label:"Cancel"});const s=await t.select({message:i,options:a,initialValue:l});if(t.isCancel(s))return{kind:"cancelled"};if(s===P)return{kind:"cancelled"};if(typeof s!="string")throw new f({code:"E_INTERNAL",message:`runUpdatePrompts: picker returned non-string value (typeof=${typeof s})`});return{kind:"picked",tag:s}}const P="__cancel__";async function q(e){return U(e,g())}async function U(e,t){const n=await t.confirm({message:e.message,initialValue:e.initialValue});return t.isCancel(n)?"cancelled":n===!0?"confirmed":"denied"}export{q as confirmInteractive,U as confirmInteractiveWithDeps,S as runInteractivePrompts,L as runInteractivePromptsWithDeps,W as runUpdatePrompts,T as runUpdatePromptsWithDeps};
1
+ import*as p from"@clack/prompts";import{CliError as f}from"../errors.js";import{buildWithinScopeRequires as M,runAutoDepsMultiselect as A}from"../util/auto-deps-multiselect.js";import{WRAPPER_SCAFFOLD_MODULE_ID as C}from"./wrapper.js";const D=12,O=["A wrapper repo is an umbrella folder placed next to your project repos.","It holds the AI1st-Kit (skills, agents, hooks, MCP wiring) once, and every","sibling project inherits the same setup -- kit updates land in one place,","and no kit files pollute a project repo's git history.","","Pick project-repo only when the kit must live inside a single project","(for example, a public repo whose contributors expect the kit committed)."].join(`
2
+ `);async function Y(e){return N(e,w())}async function N(e,t){if(e.versions===void 0&&e.loadVersions===void 0)throw new f({code:"E_INTERNAL",message:"runInteractivePrompts: neither `versions` nor `loadVersions` was provided"});const n=await x(e,t);if(n==="cancelled")return{kind:"cancelled"};const i=e.loadVersions!==void 0?await e.loadVersions():e.versions??[];if(i.length===0)throw new f({code:"E_INTERNAL",message:"runInteractivePrompts: empty version list reached the picker"});const s=await T({...e,versions:i},t);if(s==="cancelled")return{kind:"cancelled"};const r=await e.loadManifestForTag(s),l=U(r);if(l.length===0)throw new f({code:"E_INVALID_INPUT",message:"This kit declares no non-MCP modules, so the interactive installer has nothing to offer. Next: ask the content repo owner to declare at least one `files` / `hooks` / `constitutions` module, or pin a content tag (--content-ref=vX.Y.Z) that predates the regression.",headline:"The selected AI1st-Kit version declares no installable modules.",todo:["Pin a different content tag with --content-ref=vX.Y.Z.","Or ask the content-repo owner to publish a kit that declares at least one non-MCP module."]});const o=_(e.priorLockfile,"non-mcp"),d=_(e.priorLockfile,"mcp"),c=P(o,l),u=e.priorLockfile!==void 0,g=await t.autoDepsMultiselect({message:"Modules to install?",options:l,requires:M(r,"non-mcp"),required:!u,maxItems:D,...c.length>0?{initialIntent:c}:{}});if(g.kind==="cancelled")return{kind:"cancelled"};const y=g.values.slice().sort(),m=V(r);let h=[];if(m.length>0){const a=P(d,m),b=await t.autoDepsMultiselect({message:"MCP servers?",options:m,requires:M(r,"mcp"),required:!1,...a.length>0?{initialIntent:a}:{}});if(b.kind==="cancelled")return{kind:"cancelled"};h=b.values.slice().sort()}const I=$(r);let k=[];if(I.length>0){const a=await t.multiselect({message:"Constitutions (optional)?",options:I,required:!1});if(t.isCancel(a))return{kind:"cancelled"};k=a}const L=Object.values(r.modules).some(a=>a?.kind==="hooks");let v=!1;if(L){const a=await t.confirm({message:"Install hooks?",initialValue:!1});if(t.isCancel(a))return{kind:"cancelled"};v=a}return n==="wrapper-repository"?{kind:"plan",plan:{mode:"wrapper-repository",modules:y,mcp:h,constitutions:k,hooks:v,yes:!1,verbose:e.verbose,contentRef:s}}:{kind:"plan",plan:{mode:"existing-repository",modules:y,mcp:h,constitutions:k,hooks:v,yes:!1,verbose:e.verbose,contentRef:s}}}async function T(e,t){if(e.preselectedContentRef!==void 0)return e.preselectedContentRef;const n=e.priorLockfile?.installedRef??null,i=e.versions[0].tag,s=e.versions.map((o,d)=>{const c=d===0,u=n!==null&&o.tag===n;return c&&u?{value:o.tag,label:`${o.tag} (latest, current)`}:c?{value:o.tag,label:`${o.tag} (latest)`}:u?{value:o.tag,label:`${o.tag} (current)`}:{value:o.tag,label:o.tag}}),r=n!==null&&e.versions.some(o=>o.tag===n)?n:i,l=await t.select({message:"Which AI1st-Kit version would you like to install?",options:s,initialValue:r});return t.isCancel(l)?"cancelled":l}async function x(e,t){if(e.seedMode!==void 0)return e.seedMode;if(e.priorLockfile!==void 0)return j(e.priorLockfile);p.log.message(O);const n=await t.select({message:"Install mode?",options:[{value:"wrapper-repository",label:"wrapper-repo (recommended -- creates an umbrella repository around project repos)"},{value:"existing-repository",label:"project-repo (install into the current project repo)"}]});return t.isCancel(n)?"cancelled":n}function j(e){return e.modules[C]!==void 0?"wrapper-repository":"existing-repository"}function P(e,t){const n=new Set(t.map(i=>i.value));return e.filter(i=>n.has(i))}function _(e,t){if(e===void 0)return[];const n=[];for(const i of Object.keys(e.modules)){if(i===C)continue;const s=e.modules[i];(t==="mcp"?s.kind==="mcp":s.kind!=="mcp")&&n.push(i)}return n.sort()}function w(){return{select:p.select,multiselect:p.multiselect,autoDepsMultiselect:e=>A(e),confirm:p.confirm,isCancel:p.isCancel}}const R={files:0,constitutions:1,hooks:2};function U(e){const t=Object.keys(e.modules).filter(n=>e.modules[n]?.kind!=="mcp");return t.sort((n,i)=>{const s=R[e.modules[n]?.kind??""]??99,r=R[e.modules[i]?.kind??""]??99;return s!==r?s-r:n.localeCompare(i)}),t.map(n=>({value:n,label:n}))}function V(e){return Object.keys(e.modules).filter(t=>e.modules[t]?.kind==="mcp").sort().map(t=>({value:t,label:t}))}function $(e){return[]}async function Z(e){return K(e,w())}async function K(e,t){if(e.versions.length===0)throw new f({code:"E_INTERNAL",message:"runUpdatePrompts: empty version list reached the picker"});const n=typeof e.installedRef=="string"?e.installedRef:null,i=n===null?"No installed version recorded (v1 lockfile). Update to which version?":`Currently installed: ${n}. Update to which version?`,s=e.versions[0].tag,r=e.versions.map((o,d)=>{const c=d===0,u=n!==null&&o.tag===n;return c&&u?{value:o.tag,label:`${o.tag} (latest, current)`}:c?{value:o.tag,label:`${o.tag} (latest)`}:u?{value:o.tag,label:`${o.tag} (current)`}:{value:o.tag,label:o.tag}});r.push({value:E,label:"Cancel"});const l=await t.select({message:i,options:r,initialValue:s});if(t.isCancel(l))return{kind:"cancelled"};if(l===E)return{kind:"cancelled"};if(typeof l!="string")throw new f({code:"E_INTERNAL",message:`runUpdatePrompts: picker returned non-string value (typeof=${typeof l})`});return{kind:"picked",tag:l}}const E="__cancel__";async function B(e){return q(e,w())}async function q(e,t){const n=await t.confirm({message:e.message,initialValue:e.initialValue});return t.isCancel(n)?"cancelled":n===!0?"confirmed":"denied"}export{B as confirmInteractive,q as confirmInteractiveWithDeps,Y as runInteractivePrompts,N as runInteractivePromptsWithDeps,Z as runUpdatePrompts,K as runUpdatePromptsWithDeps};
@@ -1 +1 @@
1
- import{CliError as i}from"../errors.js";import{RemoteGitResolver as n}from"./remote-git.js";function l(r,o){const t=new n(r,o);return{async resolve(e){if(e==="remote-git:.")return t.resolve(e);throw new i({code:"E_RESOLVE",message:`Source spec "${e}" is not supported in v1.0. Next: only 'remote-git:.' is supported.`})}}}export{l as createResolver};
1
+ import{CliError as a}from"../errors.js";import{RemoteGitResolver as d}from"./remote-git.js";function m(i,c,t){const s=new d(i,c,{interactive:t?.interactive??!1,verbose:t?.verbose??!1,...t?.contentSha!==void 0?{contentSha:t.contentSha}:{}}),r=new Map;return{resolve(e){if(e!=="remote-git:.")return Promise.reject(new a({code:"E_RESOLVE",message:`Source spec "${e}" is not supported in v1.0. Next: only 'remote-git:.' is supported.`}));const o=r.get(e);if(o!==void 0)return o;const n=s.resolve(e);return n.catch(()=>r.delete(e)),r.set(e,n),n}}}export{m as createResolver};
@@ -1 +1 @@
1
- import{spawn as g}from"node:child_process";import{stat as $,rm as w}from"node:fs/promises";import{homedir as b}from"node:os";import{join as m}from"node:path";import{CONTENT_REPO as y}from"../generated/content-ref.js";import{CliError as f}from"../errors.js";import{log as v}from"../log.js";class T{auth;contentRepo;contentRef;cacheRoot;revParseHeadFn;constructor(c,i,t={}){this.auth=c,this.contentRepo=t.contentRepo??y,this.contentRef=i,this.cacheRoot=t.cacheRoot??m(b(),".cache","nortal-ai1st"),this.revParseHeadFn=t.gitRevParseHeadImpl??S}async resolve(c){if(c!=="remote-git:.")throw new f({code:"E_RESOLVE",message:`Source spec "${c}" is not supported in v1.0. Next: only 'remote-git:.' is supported.`});const i=this.auth.preferredUrlFor(this.contentRepo),t={...process.env};this.auth.applyToGitEnv(t);let o=await R(i,`refs/tags/${this.contentRef}^{}`,t);if(o||(o=await R(i,`refs/tags/${this.contentRef}`,t)),!o)throw new f({code:"E_RESOLVE",message:`Tag '${this.contentRef}' not found in content repo '${this.contentRepo}'. Next: check the bound content ref or your access. Runbook: https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook`,headline:`Tag '${this.contentRef}' not found in content repo '${this.contentRepo}'.`,todo:["Check the bound content ref or your access."],runbook:"https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook"});const e=m(this.cacheRoot,o),a=m(e,"kit.config.yaml");if(!await d(a)){await w(e,{recursive:!0,force:!0});try{await E(i,this.contentRef,e,t)}catch(s){throw new f({code:"E_RESOLVE",message:`Failed to clone content repo: ${s.message}. Next: check network/auth. Runbook: https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook`,cause:s})}if(await this.verifyClonedSha(e,o,t),!await d(a)){v.warn(`Cache at '${e}' is missing kit.config.yaml after clone \u2014 re-cloning once.`),await w(e,{recursive:!0,force:!0});try{await E(i,this.contentRef,e,t)}catch(s){throw new f({code:"E_RESOLVE",message:`Failed to clone content repo (retry): ${s.message}. Runbook: https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook`,cause:s})}if(await this.verifyClonedSha(e,o,t),!await d(a))throw new f({code:"E_RESOLVE",message:`Cloned content repo at '${e}' is missing kit.config.yaml. Runbook: https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook`})}}return{rootDir:e,source:"remote-git:.",resolved:`${this.contentRepo}#${o}`}}async verifyClonedSha(c,i,t){const o=await this.revParseHeadFn(c,t);if(o!==i)throw new f({code:"E_RESOLVE",message:`Cloned tag '${this.contentRef}' resolved to commit ${o} but ls-remote reported ${i}. The tag may have moved during the clone. Next: re-run; if persistent, contact the content-repo owner.`})}}async function d(l){try{return(await $(l)).isFile()}catch{return!1}}async function R(l,c,i){return new Promise((t,o)=>{let e="",a="";const n=["ls-remote",l,c];let s=!1;const u=g("git",n,{shell:!1,env:i,signal:AbortSignal.timeout(1e4)});u.stdout?.on("data",r=>{e+=r.toString("utf8")}),u.stderr?.on("data",r=>{a+=r.toString("utf8")}),u.on("error",r=>{s||(s=!0,o(new Error(`git ls-remote failed to spawn: ${r.message}`)))}),u.on("exit",r=>{if(s)return;if(s=!0,r!==0){o(r===null?new Error("git ls-remote timed out after 10s"):new Error(`git ls-remote exited ${r}: ${a.trim().slice(-200)}`));return}const p=e.split(/\r?\n/).find(k=>k.length>0);if(!p){t(null);return}const h=p.split(/\s+/)[0];h&&/^[0-9a-f]{40}$/.test(h)?t(h):t(null)})})}async function E(l,c,i,t){return new Promise((o,e)=>{let a="",n=!1;const u=g("git",["clone","--depth=1","--branch",c,l,i],{shell:!1,env:t,signal:AbortSignal.timeout(12e4)});u.stderr?.on("data",r=>{a+=r.toString("utf8")}),u.on("error",r=>{n||(n=!0,e(new Error(`git clone failed to spawn: ${r.message}`)))}),u.on("exit",r=>{if(!n){if(n=!0,r===0){o();return}if(r===null){e(new Error("git clone timed out after 120s"));return}e(new Error(`git clone exited ${r}: ${a.trim().slice(-200)}`))}})})}async function S(l,c){return new Promise((i,t)=>{let o="",e="";const a=g("git",["-C",l,"rev-parse","HEAD"],{shell:!1,env:c});a.stdout?.on("data",n=>{o+=n.toString("utf8")}),a.stderr?.on("data",n=>{e+=n.toString("utf8")}),a.on("error",n=>{t(new Error(`git rev-parse failed to spawn: ${n.message}`))}),a.on("exit",n=>{if(n!==0){t(new Error(`git rev-parse exited ${n}: ${e.trim().slice(-200)}`));return}const s=o.trim();if(!/^[0-9a-f]{40}$/.test(s)){t(new Error(`git rev-parse produced unexpected output: ${s.slice(0,80)}`));return}i(s)})})}export{T as RemoteGitResolver,E as gitCloneShallow,R as gitLsRemoteTag,S as gitRevParseHead};
1
+ import{spawn as g}from"node:child_process";import{stat as $,rm as w}from"node:fs/promises";import{homedir as k}from"node:os";import{join as p}from"node:path";import{CONTENT_REPO as S}from"../generated/content-ref.js";import{CliError as u}from"../errors.js";import{log as R}from"../log.js";import{startSpinner as b}from"../ui/spinner.js";class L{auth;contentRepo;contentRef;cacheRoot;revParseHeadFn;interactive;verbose;contentSha;constructor(t,i,e={}){this.auth=t,this.contentRepo=e.contentRepo??S,this.contentRef=i,this.cacheRoot=e.cacheRoot??p(k(),".cache","nortal-ai1st"),this.revParseHeadFn=e.gitRevParseHeadImpl??x,this.interactive=e.interactive??!1,this.verbose=e.verbose??!1,this.contentSha=e.contentSha}async resolve(t){if(t!=="remote-git:.")throw new u({code:"E_RESOLVE",message:`Source spec "${t}" is not supported in v1.0. Next: only 'remote-git:.' is supported.`});const i=this.auth.preferredUrlFor(this.contentRepo),e={...process.env};this.auth.applyToGitEnv(e);const n=this.contentSha??await this.resolveTagToSha(i,e);if(!n)throw new u({code:"E_RESOLVE",message:`Tag '${this.contentRef}' not found in content repo '${this.contentRepo}'. Next: check the bound content ref or your access. Runbook: https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook`,headline:`Tag '${this.contentRef}' not found in content repo '${this.contentRepo}'.`,todo:["Check the bound content ref or your access."],runbook:"https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook"});const o=p(this.cacheRoot,n),s=p(o,"kit.config.yaml");if(!await m(s)){await w(o,{recursive:!0,force:!0});const l=`Fetching AI1st-Kit content (${this.contentRef})\u2026`;if(await this.cloneWithSpinner({url:i,cacheDir:o,env:e,sha:n,message:l,doneMessage:l,errorMessage:c=>`Failed to clone content repo: ${c}. Next: check network/auth. Runbook: https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook`}),!await m(s)){R.warn(`Cache at '${o}' is missing kit.config.yaml after clone \u2014 re-cloning once.`),await w(o,{recursive:!0,force:!0});const c=`Re-fetching AI1st-Kit content (${this.contentRef})\u2026`;if(await this.cloneWithSpinner({url:i,cacheDir:o,env:e,sha:n,message:c,doneMessage:c,errorMessage:r=>`Failed to clone content repo (retry): ${r}. Runbook: https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook`}),!await m(s))throw new u({code:"E_RESOLVE",message:`Cloned content repo at '${o}' is missing kit.config.yaml. Runbook: https://gitlab.nortal.com/ai-first/public/n-ai1st-kit/-/wikis/install-runbook`})}}return{rootDir:o,source:"remote-git:.",resolved:`${this.contentRepo}#${n}`}}async resolveTagToSha(t,i){const e=this.interactive&&!this.verbose,n=`Resolving tag ${this.contentRef}\u2026`,o=b({message:n,enabled:e});try{let s=await v(t,`refs/tags/${this.contentRef}^{}`,i);return s||(s=await v(t,`refs/tags/${this.contentRef}`,i)),s===null?o.stop(n,1):o.stop(n),s}catch(s){throw o.stop(n,1),s}}async cloneWithSpinner(t){const i=this.interactive&&!this.verbose,e=b({message:t.message,enabled:i});try{await y(t.url,this.contentRef,t.cacheDir,t.env)}catch(n){throw e.stop(t.message,1),new u({code:"E_RESOLVE",message:t.errorMessage(n.message),cause:n})}await this.verifyClonedSha(t.cacheDir,t.sha,t.env),i?e.stop(t.doneMessage):this.verbose&&R.ok(t.doneMessage)}async verifyClonedSha(t,i,e){const n=await this.revParseHeadFn(t,e);if(n!==i)throw new u({code:"E_RESOLVE",message:`Cloned tag '${this.contentRef}' resolved to commit ${n} but ls-remote reported ${i}. The tag may have moved during the clone. Next: re-run; if persistent, contact the content-repo owner.`})}}async function m(h){try{return(await $(h)).isFile()}catch{return!1}}async function v(h,t,i){return new Promise((e,n)=>{let o="",s="";const a=["ls-remote",h,t];let l=!1;const c=g("git",a,{shell:!1,env:i,signal:AbortSignal.timeout(1e4)});c.stdout?.on("data",r=>{o+=r.toString("utf8")}),c.stderr?.on("data",r=>{s+=r.toString("utf8")}),c.on("error",r=>{l||(l=!0,n(new Error(`git ls-remote failed to spawn: ${r.message}`)))}),c.on("exit",r=>{if(l)return;if(l=!0,r!==0){n(r===null?new Error("git ls-remote timed out after 10s"):new Error(`git ls-remote exited ${r}: ${s.trim().slice(-200)}`));return}const d=o.split(/\r?\n/).find(E=>E.length>0);if(!d){e(null);return}const f=d.split(/\s+/)[0];f&&/^[0-9a-f]{40}$/.test(f)?e(f):e(null)})})}async function y(h,t,i,e){return new Promise((n,o)=>{let s="",a=!1;const c=g("git",["clone","--depth=1","--branch",t,h,i],{shell:!1,env:e,signal:AbortSignal.timeout(12e4)});c.stderr?.on("data",r=>{s+=r.toString("utf8")}),c.on("error",r=>{a||(a=!0,o(new Error(`git clone failed to spawn: ${r.message}`)))}),c.on("exit",r=>{if(!a){if(a=!0,r===0){n();return}if(r===null){o(new Error("git clone timed out after 120s"));return}o(new Error(`git clone exited ${r}: ${s.trim().slice(-200)}`))}})})}async function x(h,t){return new Promise((i,e)=>{let n="",o="";const s=g("git",["-C",h,"rev-parse","HEAD"],{shell:!1,env:t});s.stdout?.on("data",a=>{n+=a.toString("utf8")}),s.stderr?.on("data",a=>{o+=a.toString("utf8")}),s.on("error",a=>{e(new Error(`git rev-parse failed to spawn: ${a.message}`))}),s.on("exit",a=>{if(a!==0){e(new Error(`git rev-parse exited ${a}: ${o.trim().slice(-200)}`));return}const l=n.trim();if(!/^[0-9a-f]{40}$/.test(l)){e(new Error(`git rev-parse produced unexpected output: ${l.slice(0,80)}`));return}i(l)})})}export{L as RemoteGitResolver,y as gitCloneShallow,v as gitLsRemoteTag,x as gitRevParseHead};
@@ -0,0 +1 @@
1
+ import*as t from"@clack/prompts";function o(r){if(!r.enabled)return a;const e=t.spinner();return e.start(r.message),{message:s=>e.message(s),stop:(s,n)=>{n!==void 0&&n!==0?e.error(s):e.stop(s)}}}const a={message:()=>{},stop:()=>{}};export{o as startSpinner};
@@ -0,0 +1,4 @@
1
+ import{Prompt as _}from"@clack/core";import e from"picocolors";function E(o,h,l){const c=new Set,s=t=>{if(!c.has(t)&&l.has(t)){c.add(t);for(const n of h.get(t)??[])s(n)}};for(const t of o)s(t);return c}const u="\u2502",b="\u2514",C="\u25C6",A="\u25FC",k="\u25FB",P="\u25C6",g="\u25C7",x="\u25A0",I="\u25B2",R=!1;async function V(o){const h=new Set(o.options.map(t=>t.value)),l=new Set(o.initialIntent??[]),s=await new class extends _{cursor=0;viewportTop=0;intent=new Set(l);constructor(){super({render:()=>this.renderFrame(),validate:t=>{if(o.required===!0&&(!t||t.length===0))return"Please select at least one option."}},R),this.recompute(),this.on("cursor",t=>{const n=o.options.length;if(n!==0)switch(t){case"up":case"left":this.cursor=(this.cursor-1+n)%n;break;case"down":case"right":this.cursor=(this.cursor+1)%n;break;case"space":this.toggleCursor();break}})}recompute(){this.value=Array.from(E(this.intent,o.requires,h)).sort()}toggleCursor(){const t=o.options[this.cursor];t!==void 0&&(this.intent.has(t.value)?this.intent.delete(t.value):this.intent.add(t.value),this.recompute())}renderFrame(){const t=j(this.state),n=[];if(n.push(e.gray(u)),n.push(`${t} ${o.message}`),this.state==="submit"){const i=(this.value??[]).map(p=>{const a=o.options.find(v=>v.value===p);return e.dim(a?.label??p)}),r=i.length>0?i.join(e.dim(", ")):e.dim("none");return n.push(`${e.gray(u)} ${r}`),n.join(`
2
+ `)}if(this.state==="cancel")return n.push(`${e.gray(u)} ${e.dim("cancelled")}`),n.join(`
3
+ `);const T=new Set(this.value??[]),S=o.maxItems??o.options.length;this.adjustViewport(S);const w=Math.min(this.viewportTop+S,o.options.length);this.viewportTop>0&&n.push(`${e.cyan(u)} ${e.dim(`... ${this.viewportTop} more above`)}`);for(let i=this.viewportTop;i<w;i++){const r=o.options[i],p=i===this.cursor,a=T.has(r.value),v=a&&!this.intent.has(r.value),d=r.label??r.value,y=r.hint&&p?e.dim(` (${r.hint})`):"",$=v?e.dim(" (required)"):"";let f,m;a?(f=e.green(A),m=p?d:e.dim(d)):p?(f=e.cyan(C),m=d):(f=e.dim(k),m=e.dim(d)),n.push(`${e.cyan(u)} ${f} ${m}${y}${$}`)}if(w<o.options.length){const i=o.options.length-w;n.push(`${e.cyan(u)} ${e.dim(`... ${i} more below`)}`)}return this.state==="error"&&n.push(`${e.yellow(u)} ${e.yellow(this.error)}`),n.push(e.cyan(b)),n.join(`
4
+ `)}adjustViewport(t){this.cursor<this.viewportTop?this.viewportTop=this.cursor:this.cursor>=this.viewportTop+t&&(this.viewportTop=this.cursor-t+1),this.viewportTop<0&&(this.viewportTop=0)}}().prompt();return typeof s=="symbol"?{kind:"cancelled"}:{kind:"picked",values:s.slice().sort()}}function q(o,h){const l=s=>{const t=o.modules[s];return t===void 0?!1:h==="mcp"?t.kind==="mcp":t.kind!=="mcp"},c=new Map;for(const s of Object.keys(o.modules)){if(!l(s))continue;const t=(o.modules[s].requires??[]).filter(l);c.set(s,t)}return c}function j(o){switch(o){case"submit":return e.green(g);case"cancel":return e.red(x);case"error":return e.yellow(I);default:return e.cyan(P)}}export{q as buildWithinScopeRequires,E as computeAutoDepsClosure,V as runAutoDepsMultiselect};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nortal/ai1st-kit-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,16 +36,15 @@
36
36
  "vitest": "^4.1.5"
37
37
  },
38
38
  "dependencies": {
39
+ "@clack/core": "^1.3.1",
39
40
  "@clack/prompts": "^1.3.0",
41
+ "@napi-rs/keyring": "^1.3.0",
40
42
  "commander": "^12.1.0",
41
43
  "picocolors": "^1.1.1",
42
44
  "picomatch": "^4.0.4",
43
45
  "yaml": "^2.8.4",
44
46
  "zod": "^3.25.76"
45
47
  },
46
- "optionalDependencies": {
47
- "keytar": "^7.9.0"
48
- },
49
48
  "publishConfig": {
50
49
  "access": "public"
51
50
  }