@rayul/kordis 0.8.1 → 0.9.1

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,6 +1,6 @@
1
1
  ---
2
2
  name: kordis-publishing
3
- description: Use whenever the user wants to publish, share, or push a package to Kordis — phrases like "publish this", "share this skill to Kordis", "push this rule", "release a new version of <slug>". Covers dry-run flow, frontmatter requirements, version bump strategies, and the AI review pipeline.
3
+ description: Use whenever the user wants to publish, share, or push a package to Kordis — phrases like "publish this", "share this skill to Kordis", "push this rule", "release a new version of <slug>", "bump the version", "semver", "버전 올리기". Covers dry-run flow, frontmatter requirements, version bump strategies, semver rules, and the AI review pipeline.
4
4
  type: skill
5
5
  allowed-tools: ["Bash(kordis:*)", "Read", "Edit"]
6
6
  ---
@@ -107,6 +107,31 @@ non-patch update.
107
107
  four to the user. If `status: "pending"`, the AI reviewer hasn't decided
108
108
  yet — re-check with `kordis info <full_slug> --json` (or wait and re-run
109
109
  `kordis info`); the `pending` page is not yet served on the public URL.
110
+
111
+ ## Versioning your push (0.9.0+)
112
+
113
+ - **First publish** derives the version from the frontmatter `version:`
114
+ field if present, else defaults to `0.1.0`. Passing `--bump` on a
115
+ first publish now returns a clear `usage_error` (pre-0.9 silently
116
+ ignored it) — drop `--bump` or set the frontmatter version explicitly.
117
+ - **Subsequent pushes** default to `--bump patch`. Explicit
118
+ `--bump minor` or `--bump major` ships the matching jump.
119
+ - **Frontmatter `version:` on an update** must match the result of
120
+ the bump strategy, else the server rejects with `usage_error` naming
121
+ both values. Easier to drop the frontmatter version once a package is
122
+ published and let `--bump` drive it.
123
+ - **Prereleases** (`1.0.0-beta.1`) ship via frontmatter on first publish
124
+ or via an explicit prerelease push. `--bump` strips prerelease
125
+ suffixes; `current_version` never gets set to a prerelease via bump,
126
+ so installers on a bare `kordis pull <slug>` skip prereleases by
127
+ default. Consumers who want a prerelease pin explicitly:
128
+ `kordis pull <slug>@1.0.0-beta.1`.
129
+ - **AI review runs per-version** — every push enters the review queue
130
+ even if a prior version was already passed. Re-pushed packages can
131
+ legitimately fail review if new content trips a Stage-1 rule.
132
+
133
+ `kordis versions <slug>` lists everything ever published, with each
134
+ row's `review_status` so consumers know which versions are pull-safe.
110
135
  If `status: "rejected"`, surface `review.flags` to the user and DO NOT
111
136
  silently retry — explain why.
112
137
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: kordis-using
3
- description: Use whenever the user wants to consult or install a Kordis package — including any `<username>/<slug>` reference, "install this skill", "use the X skill", "fetch the Y rule", "set this repo up with…". Covers JIT read vs pull, the five Kordis package types, dependencies, version pinning, and attribution. Load eagerly when in doubt.
3
+ description: Use whenever the user wants to consult or install a Kordis package — including any `<username>/<slug>` reference, "install this skill", "use the X skill", "fetch the Y rule", "set this repo up with…", "pin to version", "specific version", "버전 고정", "전이 의존성", "transitive dependency". Covers JIT read vs pull, the five Kordis package types, dependencies, version pinning, range syntax, and attribution. Load eagerly when in doubt.
4
4
  type: skill
5
5
  allowed-tools: ["Bash(kordis:*)"]
6
6
  ---
@@ -42,14 +42,15 @@ immediately (e.g. to verify a package you just pushed), use `pull` with
42
42
  Returns the full body (Markdown) of a text-bearing package, plus an
43
43
  `attribution` string. `--pin <semver>` overrides any `@suffix` on the slug.
44
44
 
45
- ### `kordis pull <full_slug>[@<version>] [options]`
45
+ ### `kordis pull <full_slug>[@<version|range>] [options]`
46
46
  Writes files into the current project.
47
47
 
48
- | flag | meaning |
49
- | --------------------- | ---------------------------------------------------- |
50
- | `--tool <name>` | force a target tool format (default: auto-detect) |
51
- | `--target <path>` | install into a sub-directory (default: cwd) |
52
- | `--with-recommends` | also install recommended dependencies |
48
+ | flag | meaning |
49
+ | -------------------------- | ------------------------------------------------------------- |
50
+ | `--tool <name>` | force a target tool format (default: auto-detect) |
51
+ | `--target <path>` | install into a sub-directory (default: cwd) |
52
+ | `--with-recommends` | also install depth-0 recommended dependencies (0.9.0+) |
53
+ | `--no-with-required` | skip transitive required deps (advanced; default installs) |
53
54
 
54
55
  IMPORTANT: `pull` **fails fast** when the target tool can't be inferred from
55
56
  the cwd (no `.claude/`, `.codex/`, `.cursor/`, `AGENTS.md`, `CLAUDE.md`,
@@ -97,6 +98,39 @@ machine.
97
98
  - IMPORTANT: hooks execute arbitrary code. EXPLAIN in plain words what the
98
99
  hook does, and confirm with the user BEFORE pulling.
99
100
 
101
+ ## Pinning versions and ranges (0.9.0+)
102
+
103
+ Append `@<version|range>` to a slug on `kordis pull`, `kordis read`,
104
+ `kordis info` (and check what's available with `kordis versions`).
105
+ Supported forms:
106
+
107
+ | input | meaning |
108
+ | ---------------------------------- | ---------------------------------------------------------------- |
109
+ | `user/foo` (no @) | latest `current_version` (server's bumped default) |
110
+ | `user/foo@1.2.3` | exact version |
111
+ | `user/foo@1.0.0-beta.1` | exact prerelease (only matches if the literal exists) |
112
+ | `user/foo@^1.2` | caret — highest 1.x.y (x≥2). For 0.x.y, caret pins the minor |
113
+ | `user/foo@~1.2` | tilde — highest 1.2.y |
114
+ | `user/foo@*` | alias for `current_version` |
115
+
116
+ Comparator strings (`>=1 <2`, `||`) are NOT supported in 0.9.x —
117
+ server returns `usage_error` with the accepted-forms list. Deferred to
118
+ 0.10.x if demand emerges.
119
+
120
+ `kordis pull` walks transitive `required` deps by default — each child
121
+ is resolved against the range its parent declared. **Conflict between
122
+ two parents requiring incompatible ranges = hard fail** (exit code
123
+ 35 `dependency_conflict`); the CLI prints the conflict chain so users
124
+ can pin the offending one explicitly. Recommended deps are only
125
+ walked at depth 0 when `--with-recommends` is passed.
126
+
127
+ `kordis read @range` resolves to a concrete version (the stderr footer
128
+ shows it: `… @1.4.2 (resolved @^1 → 1.4.2)`) but does NOT walk
129
+ transitive deps — read is a one-shot context inject, no install plan.
130
+
131
+ `kordis info user/foo@^1` shows a `Requested: 1.4.2 (current: 2.0.0)`
132
+ line above `Type` when the pin differs from the registry default.
133
+
100
134
  ## Dependencies
101
135
  `kordis pull` reports `depends` and `recommends` from the package metadata.
102
136
  Surface the dependency tree to the user FIRST. If they accept,
package/dist/index.js CHANGED
@@ -143,6 +143,13 @@ var init_errors = __esm({
143
143
  */
144
144
  "jit_blocked",
145
145
  // 33
146
+ /**
147
+ * dependency_conflict — `kordis pull` resolver hit two incompatible
148
+ * version ranges in the install plan. Server returns the conflict
149
+ * chain via `flags`; CLI exits 35.
150
+ */
151
+ "dependency_conflict",
152
+ // 35
146
153
  "conflict",
147
154
  // 40 (slug taken, version exists)
148
155
  "filesystem_error",
@@ -160,6 +167,7 @@ var init_errors = __esm({
160
167
  rate_limited: 31,
161
168
  ai_review_rejected: 32,
162
169
  jit_blocked: 33,
170
+ dependency_conflict: 35,
163
171
  conflict: 40,
164
172
  filesystem_error: 50,
165
173
  not_found: 1,
@@ -320,7 +328,13 @@ var init_package = __esm({
320
328
  depends: z6.array(DependencySpecSchema),
321
329
  recommends: z6.array(DependencySpecSchema),
322
330
  categories: z6.array(z6.string()).optional().default([]),
323
- verified_by_kordis: z6.boolean()
331
+ verified_by_kordis: z6.boolean(),
332
+ /**
333
+ * 0.9.0+ — when the caller pinned a `@version` or `@range`, the
334
+ * concrete resolved version. Absent for bare-slug calls (where the
335
+ * response reflects `current_version`).
336
+ */
337
+ requested_version: SemverSchema.optional()
324
338
  });
325
339
  PackageVersionSchema = z6.object({
326
340
  id: UuidSchema,
@@ -486,7 +500,7 @@ var init_auth = __esm({
486
500
 
487
501
  // ../../packages/contracts/src/api.ts
488
502
  import { z as z9 } from "zod";
489
- var SearchQuerySchema, SearchResponseSchema, TrendingQuerySchema, RisingQuerySchema, StarResponseSchema, AssetSchema, PublishPackageRequestSchema, PublishWaitQuerySchema, PublishPackageResponseSchema, PackagePathParamsSchema, ContentQuerySchema, PackageContentResponseSchema, JitReadRequestSchema, JitReadResponseSchema, PullPackageResponseSchema, ListVersionsResponseSchema, VersionDiffQuerySchema, ListCommentsQuerySchema, ListCommentsResponseSchema, UserProfileResponseSchema, UserPackagesQuerySchema, ListUserPackagesResponseSchema, ListUserStarsResponseSchema, FollowResponseSchema, ListFollowersQuerySchema, ListFollowersResponseSchema;
503
+ var SearchQuerySchema, SearchResponseSchema, TrendingQuerySchema, RisingQuerySchema, StarResponseSchema, AssetSchema, PublishPackageRequestSchema, PublishWaitQuerySchema, PublishPackageResponseSchema, PackagePathParamsSchema, ContentQuerySchema, PackageContentResponseSchema, JitReadRequestSchema, JitReadResponseSchema, PullPackageRequestSchema, PullPackageResponseSchema, ResolveVersionQuerySchema, ResolveVersionResponseSchema, GetPackageQuerySchema, ListVersionsResponseSchema, VersionDiffQuerySchema, ListCommentsQuerySchema, ListCommentsResponseSchema, UserProfileResponseSchema, UserPackagesQuerySchema, ListUserPackagesResponseSchema, ListUserStarsResponseSchema, FollowResponseSchema, ListFollowersQuerySchema, ListFollowersResponseSchema;
490
504
  var init_api = __esm({
491
505
  "../../packages/contracts/src/api.ts"() {
492
506
  "use strict";
@@ -652,6 +666,25 @@ var init_api = __esm({
652
666
  */
653
667
  bundled_assets: z9.array(z9.string()).optional()
654
668
  });
669
+ PullPackageRequestSchema = z9.object({
670
+ /**
671
+ * Exact version (`"1.2.3"`) or range (`"^1.2"`, `"~1.2"`, `"*"`). When
672
+ * absent the resolver returns `current_version`. Unsupported syntax
673
+ * (`">="`, `"<"`, `"||"`) is rejected with usage_error.
674
+ */
675
+ version: SemverRangeSchema.optional(),
676
+ /**
677
+ * Walk `required` deps transitively (DFS, conflict = hard-fail, max
678
+ * depth 8). Default true — matches the user expectation that "install
679
+ * X" means everything X needs to function.
680
+ */
681
+ with_required: z9.boolean().optional().default(true),
682
+ /**
683
+ * Walk depth-0 `recommended` deps (transitive recommends NOT followed).
684
+ * Default false — opt-in noise.
685
+ */
686
+ with_recommends: z9.boolean().optional().default(false)
687
+ });
655
688
  PullPackageResponseSchema = z9.object({
656
689
  status: z9.enum(["installed", "already_installed", "skipped"]),
657
690
  full_slug: FullSlugSchema,
@@ -665,7 +698,11 @@ var init_api = __esm({
665
698
  full_slug: FullSlugSchema,
666
699
  version: SemverSchema,
667
700
  status: z9.enum(["installed", "already_installed", "skipped", "failed"]),
668
- paths: z9.array(z9.string()).optional()
701
+ paths: z9.array(z9.string()).optional(),
702
+ /** 0.9.0+ — range the parent asked for, e.g. `"^1.2"`. */
703
+ requested_range: SemverRangeSchema.optional(),
704
+ /** 0.9.0+ — which package required this one (null for root). */
705
+ requested_by: FullSlugSchema.nullable().optional()
669
706
  })
670
707
  ),
671
708
  recommended: z9.array(
@@ -673,12 +710,27 @@ var init_api = __esm({
673
710
  full_slug: FullSlugSchema,
674
711
  version: SemverSchema,
675
712
  status: z9.enum(["installed", "skipped"]),
676
- paths: z9.array(z9.string()).optional()
713
+ paths: z9.array(z9.string()).optional(),
714
+ requested_range: SemverRangeSchema.optional(),
715
+ requested_by: FullSlugSchema.nullable().optional()
677
716
  })
678
717
  )
679
718
  }),
680
719
  recommends_hint: z9.string().nullable()
681
720
  });
721
+ ResolveVersionQuerySchema = z9.object({
722
+ range: SemverRangeSchema
723
+ });
724
+ ResolveVersionResponseSchema = z9.object({
725
+ full_slug: FullSlugSchema,
726
+ requested_range: SemverRangeSchema,
727
+ resolved_version: SemverSchema,
728
+ current_version: SemverSchema,
729
+ is_prerelease: z9.boolean()
730
+ });
731
+ GetPackageQuerySchema = z9.object({
732
+ version: SemverRangeSchema.optional()
733
+ });
682
734
  ListVersionsResponseSchema = z9.object({
683
735
  full_slug: FullSlugSchema,
684
736
  current_version: SemverSchema,
@@ -1135,8 +1187,10 @@ var init_client = __esm({
1135
1187
  body
1136
1188
  });
1137
1189
  }
1138
- getPackage(username, slug) {
1139
- return this.request(`/api/packages/${username}/${slug}`);
1190
+ getPackage(username, slug, opts = {}) {
1191
+ return this.request(`/api/packages/${username}/${slug}`, {
1192
+ query: opts
1193
+ });
1140
1194
  }
1141
1195
  deletePackage(username, slug) {
1142
1196
  return this.request(`/api/packages/${username}/${slug}`, { method: "DELETE" });
@@ -1147,10 +1201,20 @@ var init_client = __esm({
1147
1201
  { query: opts }
1148
1202
  );
1149
1203
  }
1150
- pull(username, slug) {
1204
+ /**
1205
+ * 0.9.0+ — resolve a range to a concrete version without committing
1206
+ * to a pull. Used by `kordis info @range` and `kordis read @range`.
1207
+ */
1208
+ resolveVersion(username, slug, range) {
1209
+ return this.request(
1210
+ `/api/packages/${username}/${slug}/resolve`,
1211
+ { query: { range } }
1212
+ );
1213
+ }
1214
+ pull(username, slug, opts = {}) {
1151
1215
  return this.request(
1152
1216
  `/api/packages/${username}/${slug}/pull`,
1153
- { method: "POST" }
1217
+ { method: "POST", body: opts }
1154
1218
  );
1155
1219
  }
1156
1220
  /**
@@ -2904,6 +2968,22 @@ function pushCommand() {
2904
2968
  throw err instanceof Error ? err : new Error(String(err));
2905
2969
  }
2906
2970
  if (opts.dryRun) {
2971
+ const explicitBump = opts.bump && opts.bump !== "patch" ? opts.bump : null;
2972
+ if (explicitBump && nameFromFm) {
2973
+ const slugForCheck = opts.slug?.includes("/") ? opts.slug.split("/", 2) : bundle.username ? [bundle.username, opts.slug ?? nameFromFm] : null;
2974
+ if (slugForCheck && slugForCheck[0] && slugForCheck[1]) {
2975
+ const [u, s2] = slugForCheck;
2976
+ try {
2977
+ await bundle.api.getPackage(u, s2);
2978
+ } catch (err) {
2979
+ if (err && typeof err === "object" && "code" in err && err.code === "not_found") {
2980
+ throw new Error(
2981
+ `--bump ${explicitBump} cannot apply to a first publish (no existing version to bump from). First publish derives version from frontmatter \`version:\` or defaults to "0.1.0". Drop --bump (or set frontmatter version explicitly), then re-run.`
2982
+ );
2983
+ }
2984
+ }
2985
+ }
2986
+ }
2907
2987
  await runDryRun({ content, assets });
2908
2988
  return;
2909
2989
  }
@@ -3599,6 +3679,11 @@ function kordisHome2() {
3599
3679
  function lockfilePath() {
3600
3680
  return join8(kordisHome2(), "installed.json");
3601
3681
  }
3682
+ function lockfileDisplayPath() {
3683
+ const path2 = lockfilePath();
3684
+ const home = process.env["HOME"];
3685
+ return home && path2.startsWith(home) ? "~" + path2.slice(home.length) : path2;
3686
+ }
3602
3687
  async function readLockfile() {
3603
3688
  try {
3604
3689
  const raw = await readFile7(lockfilePath(), "utf8");
@@ -3617,16 +3702,80 @@ async function recordInstalled(fullSlug, entry) {
3617
3702
  await writeLockfile(lf);
3618
3703
  }
3619
3704
 
3705
+ // src/lib/parse-username.ts
3706
+ init_src();
3707
+ var SORT_KEYWORDS = ["latest", "popular", "recent", "updated", "stars", "downloads"];
3708
+ function parseUsername(raw) {
3709
+ if (SORT_KEYWORDS.includes(raw.toLowerCase())) {
3710
+ throw new Error(
3711
+ `"${raw}" is a sort keyword \u2014 did you mean \`--sort ${raw}\`? Username arguments must follow the Kordis username shape (lowercase alphanumeric, hyphens allowed).`
3712
+ );
3713
+ }
3714
+ const parsed = UsernameSchema.safeParse(raw);
3715
+ if (!parsed.success) {
3716
+ throw new Error(
3717
+ `invalid username "${raw}" \u2014 must be 3-32 chars, lowercase alphanumeric with single hyphens (e.g. "alice", "my-team")`
3718
+ );
3719
+ }
3720
+ return parsed.data;
3721
+ }
3722
+
3723
+ // src/lib/parse-slug-version.ts
3724
+ var SUFFIX_CHARS = /[\^~*]/;
3725
+ function parseSlugAtVersion(raw) {
3726
+ if (!raw.includes("/")) {
3727
+ try {
3728
+ const username2 = parseUsername(raw);
3729
+ throw new Error(
3730
+ `"${raw}" looks like a username, not a package \u2014 try \`kordis profile ${username2}\` for user details, or pass a full slug like "{username}/{slug}".`
3731
+ );
3732
+ } catch (err) {
3733
+ if (err instanceof Error) throw err;
3734
+ throw new Error(`"${raw}" is missing the "{username}/{slug}" form`);
3735
+ }
3736
+ }
3737
+ const atIdx = raw.indexOf("@");
3738
+ const slugPart = atIdx === -1 ? raw : raw.slice(0, atIdx);
3739
+ const versionPart = atIdx === -1 ? "" : raw.slice(atIdx + 1);
3740
+ const [username, slug] = slugPart.split("/");
3741
+ if (!username || !slug) {
3742
+ throw new Error('full_slug must be "{username}/{slug}"');
3743
+ }
3744
+ if (atIdx !== -1 && versionPart.length === 0) {
3745
+ throw new Error(
3746
+ `"${raw}" has a trailing "@" with no version \u2014 drop the "@" or pass a version/range after it`
3747
+ );
3748
+ }
3749
+ const out = {
3750
+ fullSlug: slugPart,
3751
+ username,
3752
+ slug
3753
+ };
3754
+ if (versionPart.length > 0) {
3755
+ if (SUFFIX_CHARS.test(versionPart)) {
3756
+ out.range = versionPart;
3757
+ } else {
3758
+ out.version = versionPart;
3759
+ }
3760
+ }
3761
+ return out;
3762
+ }
3763
+
3620
3764
  // src/commands/pull.ts
3621
- async function doPullInstall(fullSlug, tool, installRoot, version) {
3765
+ async function doPullInstall(fullSlug, tool, installRoot, opts = {}) {
3622
3766
  const [username, slug] = fullSlug.split("/");
3623
3767
  if (!username || !slug) throw new Error('full_slug must be "{username}/{slug}"');
3624
3768
  const bundle = await useClient();
3625
- const detail = await bundle.api.getPackage(username, slug);
3626
- await bundle.api.pull(username, slug);
3769
+ const pullRes = await bundle.api.pull(username, slug, {
3770
+ ...opts.version ? { version: opts.version } : {},
3771
+ ...opts.withRequired !== void 0 ? { with_required: opts.withRequired } : {},
3772
+ ...opts.withRecommends !== void 0 ? { with_recommends: opts.withRecommends } : {}
3773
+ });
3627
3774
  const contentRes = await bundle.api.getContent(username, slug, {
3628
3775
  format: "canonical",
3629
- ...version ? { version } : {}
3776
+ // Server resolved the range during pull; pass the concrete version
3777
+ // through to /content so we get the exact bytes recorded above.
3778
+ version: pullRes.version
3630
3779
  });
3631
3780
  const parsed = validatePackageSource(contentRes.content);
3632
3781
  if (!parsed.ok || !parsed.type || !parsed.frontmatter) {
@@ -3679,21 +3828,34 @@ async function doPullInstall(fullSlug, tool, installRoot, version) {
3679
3828
  version: installedVersion,
3680
3829
  tool,
3681
3830
  target: installRoot,
3682
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
3831
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
3832
+ ...opts.requestedRange ? { requestedRange: opts.requestedRange } : {},
3833
+ ...opts.installedBy ? { installedBy: opts.installedBy } : {}
3683
3834
  });
3684
- return { version: installedVersion, tool, installRoot, written, notes };
3835
+ return {
3836
+ version: installedVersion,
3837
+ tool,
3838
+ installRoot,
3839
+ written,
3840
+ notes,
3841
+ plan: pullRes.dependencies
3842
+ };
3685
3843
  }
3686
3844
  function pullCommand() {
3687
- return new Command5("pull").description("install a package into the current project").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--tool <name>", "target tool format (default: auto-detect)").option("--target <path>", "install into a subdirectory (default: cwd)").option("--with-recommends", "also install recommended dependencies", false).action(
3845
+ return new Command5("pull").description("install a package into the current project").argument(
3846
+ "<full_slug>",
3847
+ "package identifier {username}/{slug} or {username}/{slug}@{version|range}"
3848
+ ).option("--tool <name>", "target tool format (default: auto-detect)").option("--target <path>", "install into a subdirectory (default: cwd)").option(
3849
+ "--with-recommends",
3850
+ "also install recommended (depth-0) dependencies",
3851
+ false
3852
+ ).option(
3853
+ "--no-with-required",
3854
+ "skip transitive required dependencies (advanced; default is to install)"
3855
+ ).action(
3688
3856
  async (fullSlugArg, opts) => {
3689
- const atIdx = fullSlugArg.lastIndexOf("@");
3690
- const hasPinnedVersion = atIdx > fullSlugArg.indexOf("/");
3691
- const fullSlug = hasPinnedVersion ? fullSlugArg.slice(0, atIdx) : fullSlugArg;
3692
- const pinnedVersion = hasPinnedVersion ? fullSlugArg.slice(atIdx + 1) : void 0;
3693
- const [username, slug] = fullSlug.split("/");
3694
- if (!username || !slug) {
3695
- throw new Error('full_slug must be "{username}/{slug}" or "{username}/{slug}@{version}"');
3696
- }
3857
+ const parsed = parseSlugAtVersion(fullSlugArg);
3858
+ const pin = parsed.version ?? parsed.range;
3697
3859
  const userCwd = process.env["INIT_CWD"] ?? process.cwd();
3698
3860
  const installRoot = opts.target ?? userCwd;
3699
3861
  const detected = opts.tool ?? await detectTool(installRoot);
@@ -3702,25 +3864,68 @@ function pullCommand() {
3702
3864
  "Cannot detect target tool from the current directory.\n Pass --tool <name> explicitly. Valid: claude-code, codex, cursor,\n continue, windsurf, antigravity, copilot.\n Or run `kordis pull` inside a project that already has one of:\n .claude/, .codex/, .cursor/, AGENTS.md, CLAUDE.md, .cursorrules,\n .continue/, .windsurf/, .antigravity/, .github/copilot-instructions.md"
3703
3865
  );
3704
3866
  }
3705
- const spinLabel = pinnedVersion ? `Pulling ${fullSlug}@${pinnedVersion}` : `Pulling ${fullSlug}`;
3706
- const spin = spinner(spinLabel);
3707
- const result = await doPullInstall(fullSlug, detected, installRoot, pinnedVersion);
3867
+ const rootLabel = pin ? `${parsed.fullSlug}@${pin}` : parsed.fullSlug;
3868
+ const spin = spinner(`Pulling ${rootLabel}`);
3869
+ const result = await doPullInstall(parsed.fullSlug, detected, installRoot, {
3870
+ ...pin ? { version: pin } : {},
3871
+ withRequired: opts.withRequired,
3872
+ withRecommends: opts.withRecommends,
3873
+ installedBy: "user",
3874
+ ...pin ? { requestedRange: pin } : {}
3875
+ });
3708
3876
  spin.succeed();
3877
+ const dependencyInstalls = [];
3878
+ const installPlan = [
3879
+ ...result.plan?.required.map((n) => ({ ...n, kind: "required" })) ?? [],
3880
+ ...opts.withRecommends ? result.plan?.recommended.map((n) => ({ ...n, kind: "recommended" })) ?? [] : []
3881
+ ];
3882
+ for (const node of installPlan) {
3883
+ const subSpin = spinner(
3884
+ ` \u21B3 ${node.kind === "required" ? "dep" : "rec"} ${node.full_slug}@${node.version}`
3885
+ );
3886
+ try {
3887
+ const subRes = await doPullInstall(node.full_slug, detected, installRoot, {
3888
+ version: node.version,
3889
+ withRequired: false,
3890
+ withRecommends: false,
3891
+ installedBy: "dependency",
3892
+ ...node.requested_range ? { requestedRange: node.requested_range } : {}
3893
+ });
3894
+ subSpin.succeed();
3895
+ dependencyInstalls.push({ fullSlug: node.full_slug, result: subRes });
3896
+ } catch (err) {
3897
+ subSpin.fail();
3898
+ throw err;
3899
+ }
3900
+ }
3709
3901
  printDual({
3710
3902
  json: {
3711
3903
  status: "installed",
3712
- full_slug: fullSlug,
3904
+ full_slug: parsed.fullSlug,
3713
3905
  version: result.version,
3906
+ ...pin ? { requested_range: pin } : {},
3714
3907
  tool_detected: result.tool,
3715
3908
  installed_paths: result.written,
3716
- notes: result.notes
3909
+ notes: result.notes,
3910
+ dependencies: dependencyInstalls.map((d) => ({
3911
+ full_slug: d.fullSlug,
3912
+ version: d.result.version,
3913
+ installed_paths: d.result.written
3914
+ }))
3717
3915
  },
3718
3916
  human: () => {
3719
3917
  printHuman(
3720
- c.green(`\u2713 Installed ${c.bold(fullSlug)} v${result.version}`)
3918
+ c.green(`\u2713 Installed ${c.bold(parsed.fullSlug)} v${result.version}`)
3721
3919
  );
3722
3920
  for (const w of result.written) printHuman(` ${c.gray("wrote")} ${w}`);
3723
3921
  for (const n of result.notes) printHuman(c.yellow(` ! ${n}`));
3922
+ for (const dep of dependencyInstalls) {
3923
+ printHuman(
3924
+ c.green(` \u21B3 Installed ${c.bold(dep.fullSlug)} v${dep.result.version}`)
3925
+ );
3926
+ for (const w of dep.result.written) printHuman(` ${c.gray("wrote")} ${w}`);
3927
+ }
3928
+ printHuman(c.gray(` lockfile: ${lockfileDisplayPath()}`));
3724
3929
  }
3725
3930
  });
3726
3931
  const reminder = await maybeRenderReminder();
@@ -3765,18 +3970,22 @@ import { Command as Command6 } from "commander";
3765
3970
  function readCommand() {
3766
3971
  return new Command6("read").description("fetch a package body into the agent context (no install)").argument("<full_slug>", "package identifier {username}/{slug} or {username}/{slug}@{version}").option("--allow-pending", "allow versions whose AI review has not passed", false).option("--pin <semver>", "pin to an exact version (overrides @suffix)").action(
3767
3972
  async (rawSlug, opts) => {
3768
- const atIdx = rawSlug.lastIndexOf("@");
3769
- const fullSlug = atIdx > 0 ? rawSlug.slice(0, atIdx) : rawSlug;
3770
- const inlineVersion = atIdx > 0 ? rawSlug.slice(atIdx + 1) : void 0;
3771
- const version = opts.pin ?? inlineVersion;
3772
- const [username, slug] = fullSlug.split("/");
3773
- if (!username || !slug) {
3774
- throw new Error('full_slug must be "{username}/{slug}" (optional "@version")');
3775
- }
3973
+ const parsed = parseSlugAtVersion(rawSlug);
3974
+ const pin = opts.pin ?? parsed.version ?? parsed.range;
3776
3975
  const bundle = await useClient();
3777
- const res = await bundle.api.jitRead(username, slug, {
3976
+ let resolvedVersion;
3977
+ if (parsed.range && !opts.pin) {
3978
+ const resolved = await bundle.api.resolveVersion(
3979
+ parsed.username,
3980
+ parsed.slug,
3981
+ parsed.range
3982
+ );
3983
+ resolvedVersion = resolved.resolved_version;
3984
+ }
3985
+ const versionForRead = resolvedVersion ?? pin;
3986
+ const res = await bundle.api.jitRead(parsed.username, parsed.slug, {
3778
3987
  allow_pending: opts.allowPending,
3779
- ...version ? { version } : {}
3988
+ ...versionForRead ? { version: versionForRead } : {}
3780
3989
  });
3781
3990
  if (isJsonMode()) {
3782
3991
  printJson(res);
@@ -3795,8 +4004,9 @@ function readCommand() {
3795
4004
  `);
3796
4005
  }
3797
4006
  }
4007
+ const resolvedNote = parsed.range && resolvedVersion ? ` ${c.gray("(resolved")} ${c.gray("@" + parsed.range)} ${c.gray("\u2192")} ${resolvedVersion}${c.gray(")")}` : "";
3798
4008
  process.stderr.write(
3799
- `${c.gray("\u2014")} ${c.bold(res.full_slug)}${c.gray("@")}${res.version} ${c.gray("\xB7")} ${res.type} (tier ${res.tier}) ${c.gray("\xB7")} ${c.cyan(res.attribution)}
4009
+ `${c.gray("\u2014")} ${c.bold(res.full_slug)}${c.gray("@")}${res.version}${resolvedNote} ${c.gray("\xB7")} ${res.type} (tier ${res.tier}) ${c.gray("\xB7")} ${c.cyan(res.attribution)}
3800
4010
  `
3801
4011
  );
3802
4012
  printHuman("");
@@ -3808,27 +4018,31 @@ function readCommand() {
3808
4018
  init_src();
3809
4019
  import { Command as Command7 } from "commander";
3810
4020
  function infoCommand() {
3811
- return new Command7("info").description("show details about a package").argument("<full_slug>", "package identifier {username}/{slug}").action(async (fullSlug) => {
3812
- const [username, slug] = fullSlug.split("/");
3813
- if (!username || !slug) {
3814
- if (!fullSlug.includes("/")) {
3815
- throw new Error(
3816
- `"${fullSlug}" looks like a username, not a package \u2014 try \`kordis profile ${fullSlug}\` for user details, or pass a full slug like "{username}/{slug}".`
3817
- );
3818
- }
3819
- throw new Error('full_slug must be "{username}/{slug}"');
3820
- }
4021
+ return new Command7("info").description("show details about a package").argument(
4022
+ "<full_slug>",
4023
+ "package identifier {username}/{slug} or {username}/{slug}@{version|range}"
4024
+ ).option("--pin <semver>", "pin to an exact version or range (overrides @suffix)").action(async (fullSlug, opts) => {
4025
+ const parsed = parseSlugAtVersion(fullSlug);
4026
+ const pin = opts.pin ?? parsed.version ?? parsed.range;
3821
4027
  const bundle = await useClient();
3822
- const detail = await bundle.api.getPackage(username, slug);
4028
+ const detail = await bundle.api.getPackage(parsed.username, parsed.slug, {
4029
+ ...pin ? { version: pin } : {}
4030
+ });
3823
4031
  const typeDefaults = CAPABILITIES_OF_TYPE[detail.type];
3824
4032
  const jitEligible = detail.jit_eligible ?? typeDefaults.jit_eligible;
3825
4033
  const executable = detail.executable ?? typeDefaults.executable;
3826
4034
  printDual({
3827
4035
  json: detail,
3828
4036
  human: () => {
3829
- printHuman(c.bold(detail.full_slug) + " " + c.gray(`v${detail.current_version}`));
4037
+ const headerVersion = detail.requested_version ?? detail.current_version;
4038
+ printHuman(c.bold(detail.full_slug) + " " + c.gray(`v${headerVersion}`));
3830
4039
  printHuman(" " + detail.description);
3831
4040
  printHuman("");
4041
+ if (detail.requested_version && detail.requested_version !== detail.current_version) {
4042
+ printHuman(
4043
+ ` ${c.cyan("Requested")}: ${detail.requested_version} ${c.gray(`(current: ${detail.current_version})`)}`
4044
+ );
4045
+ }
3832
4046
  printHuman(` ${c.cyan("Type")}: ${detail.type} (tier ${detail.tier})`);
3833
4047
  printHuman(
3834
4048
  ` ${c.cyan("JIT-readable")}: ${jitEligible ? c.green("yes") : c.red("no")}`
@@ -4396,26 +4610,6 @@ function deleteCommand() {
4396
4610
 
4397
4611
  // src/commands/list.ts
4398
4612
  import { Command as Command13 } from "commander";
4399
-
4400
- // src/lib/parse-username.ts
4401
- init_src();
4402
- var SORT_KEYWORDS = ["latest", "popular", "recent", "updated", "stars", "downloads"];
4403
- function parseUsername(raw) {
4404
- if (SORT_KEYWORDS.includes(raw.toLowerCase())) {
4405
- throw new Error(
4406
- `"${raw}" is a sort keyword \u2014 did you mean \`--sort ${raw}\`? Username arguments must follow the Kordis username shape (lowercase alphanumeric, hyphens allowed).`
4407
- );
4408
- }
4409
- const parsed = UsernameSchema.safeParse(raw);
4410
- if (!parsed.success) {
4411
- throw new Error(
4412
- `invalid username "${raw}" \u2014 must be 3-32 chars, lowercase alphanumeric with single hyphens (e.g. "alice", "my-team")`
4413
- );
4414
- }
4415
- return parsed.data;
4416
- }
4417
-
4418
- // src/commands/list.ts
4419
4613
  function listCommand() {
4420
4614
  return new Command13("list").description("list packages \u2014 your own (no arg) or a public user\u2019s (with <username>)").argument("[username]", "kordis username to list (omit for self)").option("--limit <n>", "max results per page", (v) => parseInt(v, 10), 20).option("--page <n>", "self-mode page number (1-based)", (v) => parseInt(v, 10), 1).option("--cursor <token>", "public-mode pagination cursor").option("--sort <mode>", "sort: latest|popular (self) or stars|downloads|recent|updated (public)", "latest").action(async (rawUsername, opts) => {
4421
4615
  const bundle = await useClient();
@@ -4545,16 +4739,9 @@ function relativeTime(isoString) {
4545
4739
 
4546
4740
  // src/commands/outdated.ts
4547
4741
  import { Command as Command15 } from "commander";
4548
- function isNewer(a, b) {
4549
- const parse = (v) => {
4550
- const [maj = 0, min = 0, pat = 0] = v.split("-")[0].split(".").map(Number);
4551
- return [maj, min, pat];
4552
- };
4553
- const [aMaj, aMin, aPat] = parse(a);
4554
- const [bMaj, bMin, bPat] = parse(b);
4555
- if (bMaj !== aMaj) return bMaj > aMaj;
4556
- if (bMin !== aMin) return bMin > aMin;
4557
- return bPat > aPat;
4742
+ import semver from "semver";
4743
+ function effectiveRange(entry) {
4744
+ return entry.requestedRange ?? `^${entry.version}`;
4558
4745
  }
4559
4746
  function outdatedCommand() {
4560
4747
  return new Command15("outdated").description("check installed packages for available updates").action(async () => {
@@ -4575,13 +4762,37 @@ function outdatedCommand() {
4575
4762
  for (const fullSlug of slugs) {
4576
4763
  const [username, slug] = fullSlug.split("/");
4577
4764
  if (!username || !slug) continue;
4578
- const installed = lf.packages[fullSlug].version;
4765
+ const entry = lf.packages[fullSlug];
4766
+ const installed = entry.version;
4767
+ const requestedRange = effectiveRange(entry);
4579
4768
  try {
4580
4769
  const detail = await bundle.api.getPackage(username, slug);
4581
4770
  const latest = detail.current_version;
4582
- entries.push({ full_slug: fullSlug, installed, latest, outdated: isNewer(installed, latest) });
4771
+ let status;
4772
+ if (!semver.gt(latest, installed)) {
4773
+ status = "up_to_date";
4774
+ } else if (semver.satisfies(latest, requestedRange)) {
4775
+ status = "in_range_update";
4776
+ } else {
4777
+ status = "out_of_range";
4778
+ }
4779
+ entries.push({
4780
+ full_slug: fullSlug,
4781
+ installed,
4782
+ latest,
4783
+ requested_range: requestedRange,
4784
+ status,
4785
+ outdated: status !== "up_to_date"
4786
+ });
4583
4787
  } catch {
4584
- entries.push({ full_slug: fullSlug, installed, latest: installed, outdated: false });
4788
+ entries.push({
4789
+ full_slug: fullSlug,
4790
+ installed,
4791
+ latest: installed,
4792
+ requested_range: requestedRange,
4793
+ status: "up_to_date",
4794
+ outdated: false
4795
+ });
4585
4796
  }
4586
4797
  }
4587
4798
  spin.succeed();
@@ -4603,10 +4814,19 @@ function outdatedCommand() {
4603
4814
  );
4604
4815
  printHuman("");
4605
4816
  for (const e of entries) {
4606
- const status = e.outdated ? c.yellow("Outdated") : c.green("Up to date");
4607
- const arrow = e.outdated ? c.yellow(` \u2192 ${e.latest}`) : "";
4817
+ let label;
4818
+ let arrow = "";
4819
+ if (e.status === "up_to_date") {
4820
+ label = c.green("Up to date");
4821
+ } else if (e.status === "in_range_update") {
4822
+ label = c.yellow("Outdated");
4823
+ arrow = c.yellow(` \u2192 ${e.latest}`);
4824
+ } else {
4825
+ label = c.yellow(`Outdated ${c.gray(`(out of range ${e.requested_range})`)}`);
4826
+ arrow = c.yellow(` \u2192 ${e.latest}`);
4827
+ }
4608
4828
  printHuman(
4609
- ` ${c.bold(e.full_slug)} ${c.gray(e.installed)}${arrow} ${status}`
4829
+ ` ${c.bold(e.full_slug)} ${c.gray(e.installed)}${arrow} ${label}`
4610
4830
  );
4611
4831
  }
4612
4832
  printHuman("");
@@ -4623,16 +4843,9 @@ function outdatedCommand() {
4623
4843
 
4624
4844
  // src/commands/update.ts
4625
4845
  import { Command as Command16 } from "commander";
4626
- function isNewer2(a, b) {
4627
- const parse = (v) => {
4628
- const [maj = 0, min = 0, pat = 0] = v.split("-")[0].split(".").map(Number);
4629
- return [maj, min, pat];
4630
- };
4631
- const [aMaj, aMin, aPat] = parse(a);
4632
- const [bMaj, bMin, bPat] = parse(b);
4633
- if (bMaj !== aMaj) return bMaj > aMaj;
4634
- if (bMin !== aMin) return bMin > aMin;
4635
- return bPat > aPat;
4846
+ import semver2 from "semver";
4847
+ function effectiveRange2(entry) {
4848
+ return entry.requestedRange ?? `^${entry.version}`;
4636
4849
  }
4637
4850
  function updateCommand() {
4638
4851
  return new Command16("update").description("update installed packages to their latest versions").argument("[full_slug]", "package identifier {username}/{slug} (omit to update all)").option("--dry-run", "show what would be updated without doing it", false).action(async (fullSlugArg, opts) => {
@@ -4660,15 +4873,18 @@ function updateCommand() {
4660
4873
  const entry = lf.packages[fullSlug];
4661
4874
  const [username, slug] = fullSlug.split("/");
4662
4875
  if (!username || !slug) continue;
4876
+ const requestedRange = effectiveRange2(entry);
4663
4877
  try {
4664
- const detail = await bundle.api.getPackage(username, slug);
4665
- if (isNewer2(entry.version, detail.current_version)) {
4878
+ const resolved = await bundle.api.resolveVersion(username, slug, requestedRange);
4879
+ if (semver2.gt(resolved.resolved_version, entry.version)) {
4666
4880
  candidates.push({
4667
4881
  fullSlug,
4668
4882
  installed: entry.version,
4669
- latest: detail.current_version,
4883
+ target_version: resolved.resolved_version,
4884
+ requestedRange,
4670
4885
  tool: entry.tool,
4671
- target: entry.target
4886
+ target: entry.target,
4887
+ ...semver2.gt(resolved.current_version, resolved.resolved_version) ? { outOfRangeMajor: resolved.current_version } : {}
4672
4888
  });
4673
4889
  }
4674
4890
  } catch {
@@ -4686,7 +4902,7 @@ function updateCommand() {
4686
4902
  const dryEntries = candidates.map((c2) => ({
4687
4903
  full_slug: c2.fullSlug,
4688
4904
  from: c2.installed,
4689
- to: c2.latest,
4905
+ to: c2.target_version,
4690
4906
  status: "dry_run"
4691
4907
  }));
4692
4908
  printDual({
@@ -4699,8 +4915,9 @@ function updateCommand() {
4699
4915
  );
4700
4916
  printHuman("");
4701
4917
  for (const cand of candidates) {
4918
+ const tail = cand.outOfRangeMajor ? c.gray(` (registry has ${cand.outOfRangeMajor}, out of range ${cand.requestedRange})`) : "";
4702
4919
  printHuman(
4703
- ` ${c.bold(cand.fullSlug)} ${c.gray(cand.installed)} \u2192 ${c.green(cand.latest)}`
4920
+ ` ${c.bold(cand.fullSlug)} ${c.gray(cand.installed)} \u2192 ${c.green(cand.target_version)}${tail}`
4704
4921
  );
4705
4922
  }
4706
4923
  }
@@ -4709,11 +4926,17 @@ function updateCommand() {
4709
4926
  }
4710
4927
  const summary = [];
4711
4928
  for (const cand of candidates) {
4929
+ const tail = cand.outOfRangeMajor ? c.gray(` (registry has ${cand.outOfRangeMajor}, out of range ${cand.requestedRange})`) : "";
4712
4930
  printHuman(
4713
- ` ${c.cyan("\u2191")} Updating ${c.bold(cand.fullSlug)} ${c.gray(cand.installed)} \u2192 ${c.green(cand.latest)}`
4931
+ ` ${c.cyan("\u2191")} Updating ${c.bold(cand.fullSlug)} ${c.gray(cand.installed)} \u2192 ${c.green(cand.target_version)}${tail}`
4714
4932
  );
4715
4933
  try {
4716
- const result = await doPullInstall(cand.fullSlug, cand.tool, cand.target);
4934
+ const result = await doPullInstall(cand.fullSlug, cand.tool, cand.target, {
4935
+ version: cand.target_version,
4936
+ withRequired: false,
4937
+ withRecommends: false,
4938
+ requestedRange: cand.requestedRange
4939
+ });
4717
4940
  summary.push({
4718
4941
  full_slug: cand.fullSlug,
4719
4942
  from: cand.installed,
@@ -4727,7 +4950,7 @@ function updateCommand() {
4727
4950
  summary.push({
4728
4951
  full_slug: cand.fullSlug,
4729
4952
  from: cand.installed,
4730
- to: cand.latest,
4953
+ to: cand.target_version,
4731
4954
  status: "failed",
4732
4955
  error: message
4733
4956
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rayul/kordis",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "private": false,
5
5
  "description": "Command-line client for the Kordis registry — discover, install, and publish agentic-coding packages.",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "open": "^10.1.0",
33
33
  "ora": "^8.1.1",
34
34
  "prompts": "^2.4.2",
35
+ "semver": "^7.6.3",
35
36
  "yaml": "^2.6.0",
36
37
  "zod": "^3.23.8"
37
38
  },
@@ -47,6 +48,7 @@
47
48
  "@types/jszip": "^3.4.1",
48
49
  "@types/node": "^22.9.0",
49
50
  "@types/prompts": "^2.4.9",
51
+ "@types/semver": "^7.5.8",
50
52
  "tsup": "^8.3.5",
51
53
  "tsx": "^4.19.2",
52
54
  "typescript": "^5.6.3",