failproofai 1.0.2-beta.4 → 1.0.2-beta.6

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.
Files changed (155) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +5 -5
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  32. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  34. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/index.html +1 -1
  38. package/.next/standalone/.next/server/app/index.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  40. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  48. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  51. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  62. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  63. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  66. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  67. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  68. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__03r-lqq._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12021ot._.js +3 -0
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0clqfab._.js → [root-of-the-server]__1tg4pu6._.js} +2 -2
  74. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{_1lbh81u._.js → _19uwam-._.js} +1 -1
  76. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +7 -7
  77. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1m84s36._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +6 -6
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__14muoc4._.js → [root-of-the-server]__128se8m._.js} +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0jzbk4_._.js → [root-of-the-server]__190won8._.js} +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rsd3ll._.js → [root-of-the-server]__1sc84bb._.js} +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/{_0ovx4lz._.js → _0no96tu._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/{_1dkh76z._.js → _11fc-hb._.js} +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/{_1db3-8n._.js → _1if9ixi._.js} +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/lib_1cf5ya2._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_15bjxkm._.js +1 -1
  108. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  109. package/.next/standalone/.next/server/pages/404.html +1 -1
  110. package/.next/standalone/.next/server/pages/500.html +1 -1
  111. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  112. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  113. package/.next/standalone/.next/static/chunks/{0lshnlpy1m6bc.js → 0931avh-06lj5.js} +1 -1
  114. package/.next/standalone/.next/static/chunks/{0k_yvjc1yz0pq.js → 0h-3zffoy-_ji.js} +1 -1
  115. package/.next/standalone/.next/static/chunks/{0ds64uculrze8.js → 0p84npzxdgpc5.js} +1 -1
  116. package/.next/standalone/.next/static/chunks/{0d_re32wscs0b.js → 0sdgnflvw8kta.js} +1 -1
  117. package/.next/standalone/.next/static/chunks/{2dbi_x0c8n8fi.js → 0z7au3njlak0j.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/{1o0ahew9568ls.js → 1id7o44jjegf0.js} +1 -1
  119. package/.next/standalone/.next/static/chunks/{00_t-xvc59tkb.js → 276n-edifojbd.js} +2 -2
  120. package/.next/standalone/.next/static/chunks/{07j2u70_3qsx8.js → 2jfskwd2u8o5r.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{2817_8e94ah9z.js → 2pxr7nr1nyotn.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/{1oz0svxwt9574.js → 3i8rg2fv9nd6e.js} +2 -2
  123. package/.next/standalone/.next/static/chunks/{1q76jk_1_wgjq.js → 3vu_7_eor1m-k.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/{turbopack-17utl_jw0wj11.js → turbopack-2x-o099q64i8h.js} +1 -1
  125. package/.next/standalone/lib/opencode-db.ts +77 -0
  126. package/.next/standalone/lib/opencode-projects.ts +37 -41
  127. package/.next/standalone/lib/opencode-sessions.ts +59 -36
  128. package/.next/standalone/package.json +9 -9
  129. package/.next/standalone/server.js +1 -1
  130. package/bin/failproofai.mjs +116 -34
  131. package/dist/cli.mjs +1732 -1177
  132. package/dist/worker.mjs +180 -165
  133. package/lib/opencode-db.ts +77 -0
  134. package/lib/opencode-projects.ts +37 -41
  135. package/lib/opencode-sessions.ts +59 -36
  136. package/package.json +9 -9
  137. package/src/audit/cache.ts +167 -11
  138. package/src/audit/cli-adapters/claude.ts +111 -1
  139. package/src/audit/cli-adapters/index.ts +31 -1
  140. package/src/audit/cli.ts +12 -0
  141. package/src/audit/index.ts +164 -13
  142. package/src/hooks/configure-wizard.ts +112 -18
  143. package/src/hooks/fp-reset.ts +1 -1
  144. package/src/hooks/handler.ts +13 -1
  145. package/src/hooks/manager.ts +69 -4
  146. package/src/hooks/onboarding-attempt.ts +4 -4
  147. package/src/hooks/pack-cli.ts +399 -32
  148. package/src/hooks/pack-store.ts +21 -8
  149. package/src/hooks/read-stdin.ts +78 -8
  150. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l-khtb._.js +0 -3
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mhmdzs._.js +0 -3
  152. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +0 -3
  153. /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_buildManifest.js +0 -0
  154. /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_clientMiddlewareManifest.js +0 -0
  155. /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_ssgManifest.js +0 -0
@@ -6,10 +6,10 @@
6
6
  * This layer decides nothing; it formats.
7
7
  */
8
8
  import { execFileSync } from "node:child_process";
9
- import { homedir } from "node:os";
9
+ import { homedir, tmpdir } from "node:os";
10
10
  import { createHash } from "node:crypto";
11
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
12
- import { resolve } from "node:path";
11
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
12
+ import { basename, dirname, join, resolve } from "node:path";
13
13
  import { INTEGRATION_TYPES } from "./types";
14
14
  import { PACK_VERSION_RE } from "./pack-manifest";
15
15
  import { detectInstalledClis } from "./integrations";
@@ -20,9 +20,7 @@ import {
20
20
  PACK_MANIFEST_ASSET,
21
21
  addPack,
22
22
  checkPackArtifact,
23
- CORE_ALIASES,
24
23
  fetchPackPreview,
25
- CORE_SOURCE,
26
24
  packTagMatchesVersion,
27
25
  removePack,
28
26
  setPackPolicyEnabled,
@@ -63,34 +61,153 @@ function parseList(rest: string[], flag: string): string[] | undefined {
63
61
  return raw.split(",").map((s) => s.trim()).filter(Boolean);
64
62
  }
65
63
 
66
- /** Flags that take a separate value, so it is never mistaken for the source. */
67
- const VALUE_FLAGS = new Set(["--only", "--policy", "--category", "--cli"]);
64
+ /**
65
+ * Flags that take a separate value, so it is never mistaken for the source.
66
+ *
67
+ * Must list every flag that can REACH this lane, not just the ones this lane
68
+ * acts on. `--scope` and `--custom` are handled by the caller and passed
69
+ * through untouched — and their values were being read as the pack to install,
70
+ * so `policies add --scope project acme/x` tried to fetch a pack called
71
+ * "project". A flag this file ignores still has a value this file must skip.
72
+ */
73
+ const VALUE_FLAGS = new Set([
74
+ "--only",
75
+ "--policy",
76
+ "--category",
77
+ "--cli",
78
+ "--scope",
79
+ "--custom",
80
+ "-c",
81
+ ]);
82
+
83
+ /**
84
+ * Every index `--cli` claims, so the source is not found among its values.
85
+ *
86
+ * `--cli` is the one flag that takes MORE than one token, because the other
87
+ * lane spells it `--cli claude codex` and somebody typing that here should not
88
+ * be quietly given a different answer. Shared with `packAddSource` so the two
89
+ * agree by construction: a token consumed as a CLI name is never also read as
90
+ * the pack to install.
91
+ */
92
+ /**
93
+ * Could this token be an agent name rather than the pack to install?
94
+ *
95
+ * `--cli` consumes several tokens, so it has to know where its own list ends,
96
+ * and "not a flag" is not enough: `--cli claude codex acme/x` would swallow the
97
+ * SOURCE and leave the command with nothing to install. A pack source always
98
+ * carries a slash — `parsePackSource` accepts nothing else — and no agent name
99
+ * has one, which separates them exactly.
100
+ *
101
+ * Deliberately NOT "is it a known agent". An unknown name has to be consumed to
102
+ * be REJECTED: stopping at `claud` would hand it to the source parser instead,
103
+ * and the reply would be about pack syntax rather than about the typo.
104
+ */
105
+ function looksLikeCliName(token: string): boolean {
106
+ return !token.startsWith("-") && !token.includes("/");
107
+ }
108
+
109
+ function cliValueIndices(rest: string[]): Set<number> {
110
+ const taken = new Set<number>();
111
+ for (let i = 0; i < rest.length; i += 1) {
112
+ if (rest[i] !== "--cli" && !rest[i].startsWith("--cli=")) continue;
113
+ if (rest[i].startsWith("--cli=")) continue; // its value is in the same token
114
+ for (let j = i + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
115
+ taken.add(j);
116
+ }
117
+ }
118
+ return taken;
119
+ }
68
120
 
69
121
  /** Find the positional source without mistaking a flag's separate value for it. */
70
122
  export function packAddSource(rest: string[]): string | undefined {
71
- const consumed = new Set<number>();
123
+ const consumed = cliValueIndices(rest);
72
124
  for (let i = 0; i < rest.length; i += 1) {
73
125
  if (VALUE_FLAGS.has(rest[i])) consumed.add(i + 1);
74
126
  }
75
- return rest.find((arg, index) => !arg.startsWith("--") && !consumed.has(index));
127
+ // ONE dash, not two. `-c` passed a `startsWith("--")` filter and was returned
128
+ // as the pack to install — and no GitHub owner or repo may begin with a
129
+ // hyphen, so nothing legitimate is excluded by widening it.
130
+ return rest.find((arg, index) => !arg.startsWith("-") && !consumed.has(index));
131
+ }
132
+
133
+ /**
134
+ * The agent CLIs named by `--cli`, in either spelling, all of them validated.
135
+ *
136
+ * Two separate silent failures lived here, and both reported success:
137
+ *
138
+ * - An unknown name was accepted. `--cli claud` installed the pack, printed
139
+ * "Installed", exited 0, and guarded NOTHING — the misspelling matched no
140
+ * agent, so the pack applied to none of them.
141
+ * - A space-separated list was truncated to its first entry.
142
+ * `--cli claude codex` recorded `["claude"]` and dropped codex on the floor,
143
+ * because this lane split on commas while the other split on spaces.
144
+ *
145
+ * Both now take either spelling and refuse a name that is not an agent. The set
146
+ * comes from `INTEGRATION_TYPES` rather than a list written out here, because
147
+ * the copy in `bin/failproofai.mjs` is already a second hand-maintained one and
148
+ * a third would be the one that drifts.
149
+ */
150
+ function parseCliList(rest: string[]): { clis?: string[] } | { error: string[] } {
151
+ const idx = rest.findIndex((a) => a === "--cli" || a.startsWith("--cli="));
152
+ if (idx === -1) return {};
153
+ const values: string[] = [];
154
+ if (rest[idx].startsWith("--cli=")) {
155
+ values.push(...rest[idx].slice("--cli=".length).split(","));
156
+ } else {
157
+ for (let j = idx + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
158
+ values.push(...rest[j].split(","));
159
+ }
160
+ }
161
+ const names = values.map((v) => v.trim()).filter(Boolean);
162
+ if (names.length === 0) return { clis: [] };
163
+
164
+ const known = new Set<string>(INTEGRATION_TYPES);
165
+ const unknown = names.filter((n) => !known.has(n));
166
+ if (unknown.length > 0) {
167
+ // Thrown, not warned. A pack scoped to an agent that does not exist is
168
+ // enforcing on nothing, and saying so afterwards is no use to the script
169
+ // that already read exit 0 and moved on.
170
+ const near = (bad: string) =>
171
+ INTEGRATION_TYPES.find((k) => k.startsWith(bad.slice(0, 3)) || bad.startsWith(k.slice(0, 3)));
172
+ const hint = unknown.map((u) => (near(u) ? `${u} (did you mean ${near(u)}?)` : u)).join(", ");
173
+ return {
174
+ error: [
175
+ `Not an agent: ${hint}`,
176
+ `--cli takes any of: ${INTEGRATION_TYPES.join(", ")}`,
177
+ ],
178
+ };
179
+ }
180
+ return { clis: names };
76
181
  }
77
182
 
78
183
  /** Names taken by our own policies, so a selection can be checked before install. */
184
+ /**
185
+ * Exported for tests: the parse is where both `--cli` bugs lived, and asserting
186
+ * it through a real `add` would need a release server for a question that is
187
+ * purely about argument shapes.
188
+ */
189
+ export function selectionFromForTest(rest: string[]) {
190
+ return selectionFrom(rest);
191
+ }
192
+
79
193
  function selectionFrom(rest: string[]): {
80
194
  only?: string[];
81
195
  categories?: string[];
82
196
  all?: boolean;
83
197
  clis?: string[];
198
+ /** Present when the selection itself is unusable — see `parseCliList`. */
199
+ error?: string[];
84
200
  } {
85
201
  // `--policy` reads right for one ("give me this policy"), `--only` for a set.
86
202
  // They are the same switch; taking both means neither is the wrong guess.
87
203
  const only = parseList(rest, "--policy") ?? parseList(rest, "--only");
88
204
  const categories = parseList(rest, "--category");
89
- const clis = parseList(rest, "--cli");
205
+ const parsedClis = parseCliList(rest);
206
+ if ("error" in parsedClis) return { error: parsedClis.error };
90
207
  return {
91
208
  ...(only ? { only } : {}),
92
209
  ...(categories ? { categories } : {}),
93
- ...(clis ? { clis } : {}),
210
+ ...(parsedClis.clis ? { clis: parsedClis.clis } : {}),
94
211
  ...(rest.includes("--all") ? { all: true } : {}),
95
212
  };
96
213
  }
@@ -175,6 +292,29 @@ async function build(rest: string[]): Promise<PackCliResult> {
175
292
  ]);
176
293
  }
177
294
 
295
+ // Two policies cannot share a name, and this is where that has to be caught.
296
+ //
297
+ // Nothing downstream can resolve the ambiguity: a name is what `--policy`
298
+ // selects, what the picker toggles, and what the enabled list stores — so a
299
+ // duplicate makes one of the two unreachable and the other's on/off state
300
+ // decide for both. Bundling several files makes it easy to reach by accident:
301
+ // a starter written by `publish --init` into a folder that already had a
302
+ // policy of the same name published as exactly this, twice over, with no
303
+ // complaint at build time.
304
+ const byName = new Map<string, number>();
305
+ for (const hook of hooks) byName.set(hook.name, (byName.get(hook.name) ?? 0) + 1);
306
+ const duplicates = [...byName.entries()].filter(([, n]) => n > 1).map(([name]) => name);
307
+ if (duplicates.length > 0) {
308
+ return fail([
309
+ duplicates.length === 1
310
+ ? `Two policies are called ${JSON.stringify(duplicates[0])}.`
311
+ : `${duplicates.length} names are used by more than one policy: ${duplicates.join(", ")}.`,
312
+ "A name is what --policy selects and what the picker toggles, so a pack",
313
+ "cannot carry the same one twice — one of them would be unreachable.",
314
+ "Rename or delete one of each pair.",
315
+ ]);
316
+ }
317
+
178
318
  const policies: unknown[] = [];
179
319
  for (const [index, hook] of hooks.entries()) {
180
320
  // `category` and `defaultEnabled` are pack-manifest fields a plain custom
@@ -274,7 +414,7 @@ export async function runPolicyPicker(
274
414
  {
275
415
  what: "No policies are installed yet.",
276
416
  hint: "Take ours, or anyone's:",
277
- cmd: "failproofai policies add core",
417
+ cmd: "failproofai policies add FailproofAI/policies",
278
418
  },
279
419
  opts,
280
420
  ),
@@ -354,6 +494,15 @@ export async function runPolicyPicker(
354
494
  * policy that already RUNS and already blocks something real, so the first
355
495
  * action is editing a working thing rather than authoring an empty one.
356
496
  */
497
+ /**
498
+ * A policy filename, whatever spelling it arrived in. Discovery takes
499
+ * `.mjs`/`.js`/`.ts` and nothing else, so a name with no extension has to gain
500
+ * one or the file it names can never be found.
501
+ */
502
+ function withPolicyExtension(name: string): string {
503
+ return /\.(mjs|js|ts)$/.test(name) ? name : `${name}.mjs`;
504
+ }
505
+
357
506
  async function scaffold(target: string | null): Promise<PackCliResult> {
358
507
  // Ask what the pack is called when nothing was named and there is somebody to
359
508
  // ask. The answer becomes the FILENAME and the header, so what lands on disk
@@ -374,9 +523,14 @@ async function scaffold(target: string | null): Promise<PackCliResult> {
374
523
  stdout: process.stdout,
375
524
  });
376
525
  if (answer === null) return ok(["Nothing written."]);
377
- chosen = `./${answer.trim().replace(/\.(mjs|js|ts)$/, "")}.mjs`;
526
+ chosen = answer.trim();
378
527
  }
379
- const path = resolve(chosen ?? "./my-policies.mjs");
528
+ // Both paths land here, because only one of them used to. A name typed at
529
+ // the prompt got `.mjs`; the same name passed as `--init myguards` was taken
530
+ // literally and wrote a file called `myguards` — which discovery skips (it
531
+ // takes .mjs/.js/.ts) and which no ESM loader will import. The starter file
532
+ // was unreachable by every command meant to pick it up.
533
+ const path = resolve(withPolicyExtension(chosen ?? "my-policies"));
380
534
  if (existsSync(path)) {
381
535
  return fail([`${path} already exists — name a different file, or edit that one.`]);
382
536
  }
@@ -416,7 +570,7 @@ customPolicies.add({
416
570
  " then ask your agent to force-push — it gets refused",
417
571
  "",
418
572
  "Then publish it:",
419
- ` failproofai publish ${path} --repo <you>/<repo> --version 1.0.0`,
573
+ " failproofai publish --repo <you>/<repo>",
420
574
  ]);
421
575
  }
422
576
 
@@ -582,6 +736,7 @@ async function ensureRepo(
582
736
  name: string,
583
737
  id: string,
584
738
  token: string,
739
+ sourceDir?: string,
585
740
  ): Promise<{ info: Awaited<ReturnType<typeof gh>>; created: boolean } | { error: string[] }> {
586
741
  let info = await gh(`${GITHUB_API}/repos/${owner}/${name}`, token);
587
742
  let created = false;
@@ -601,7 +756,15 @@ async function ensureRepo(
601
756
  name,
602
757
  private: false,
603
758
  description: `failproofai policy pack — ${id}`,
604
- auto_init: true,
759
+ // NOT auto_init. The author already has the commit that holds these
760
+ // policies; a repository seeded with its own "Initial commit" shares
761
+ // no history with it, so the `git push` that every publish is followed
762
+ // by is rejected as unrelated — and the way out (`pull --allow-
763
+ // unrelated-histories`, or a force-push over the seed) is a worse
764
+ // first five minutes than an empty repository, which accepts the push
765
+ // as-is. Releases do not need a commit to hang from: the tag is
766
+ // created against whatever the release API is given.
767
+ auto_init: false,
605
768
  }),
606
769
  });
607
770
  if (made.status >= 400) {
@@ -617,6 +780,14 @@ async function ensureRepo(
617
780
  };
618
781
  }
619
782
  created = true;
783
+ // A repository created with no commits has no default branch, and the
784
+ // release API tags the default branch — so something has to land on it
785
+ // before the release is cut. Pushing the author's own history is the
786
+ // version that leaves a usable repository behind; seeding is the fallback
787
+ // for a pack that is not in a git checkout at all.
788
+ if (!(sourceDir && pushExistingHistory(sourceDir, owner, name, token))) {
789
+ await seedDefaultBranch(owner, name, id, token);
790
+ }
620
791
  info = await gh(`${GITHUB_API}/repos/${owner}/${name}`, token);
621
792
  }
622
793
  if (info.status >= 400) {
@@ -625,6 +796,102 @@ async function ensureRepo(
625
796
  return { info, created };
626
797
  }
627
798
 
799
+ /**
800
+ * Push the checkout the policies live in to the repository just created for
801
+ * them, and report whether it landed.
802
+ *
803
+ * Why publish pushes at all: it created the repository, so it owns the one
804
+ * moment when pushing cannot conflict with anything. The repository used to be
805
+ * created with `auto_init`, which meant GitHub wrote an "Initial commit" the
806
+ * author did not have — so the `git push` that every publish is followed by was
807
+ * rejected as unrelated history, every time, for everybody. Seeding it and
808
+ * pushing it are the same job; doing only the first left the author to
809
+ * reconcile two histories by hand.
810
+ *
811
+ * It also makes the tag mean something. The release tags the default branch, so
812
+ * against a seeded repository `1.0.0` named a README commit that did not
813
+ * contain a single one of the policies it shipped.
814
+ *
815
+ * The token goes in through GIT_ASKPASS, never in the URL or the argv: a remote
816
+ * with a credential in it is written into `.git/config` in plaintext, and one on
817
+ * a command line is readable from `ps` by every user on the box.
818
+ */
819
+ export const __pushExistingHistoryForTest = (
820
+ dir: string,
821
+ owner: string,
822
+ name: string,
823
+ token: string,
824
+ ): boolean => pushExistingHistory(dir, owner, name, token);
825
+
826
+ function pushExistingHistory(dir: string, owner: string, name: string, token: string): boolean {
827
+ const git = (args: string[], env?: Record<string, string>): string | null => {
828
+ try {
829
+ return execFileSync("git", args, {
830
+ cwd: dir,
831
+ encoding: "utf8",
832
+ stdio: ["ignore", "pipe", "ignore"],
833
+ timeout: 60_000,
834
+ env: env ? { ...process.env, ...env } : process.env,
835
+ }).trim();
836
+ } catch {
837
+ return null;
838
+ }
839
+ };
840
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
841
+ // No commits yet (`HEAD` unborn) means there is nothing to push and nothing
842
+ // to conflict with later either.
843
+ if (!branch || branch === "HEAD" || !git(["rev-parse", "--verify", "HEAD"])) return false;
844
+
845
+ const askpass = join(mkdtempSync(join(tmpdir(), "fpai-askpass-")), "askpass.sh");
846
+ try {
847
+ writeFileSync(askpass, `#!/bin/sh\ncase "$1" in\n*Username*) echo x-access-token ;;\n*) echo "$FPAI_GH_TOKEN" ;;\nesac\n`, { mode: 0o700 });
848
+ const env = { GIT_ASKPASS: askpass, FPAI_GH_TOKEN: token, GIT_TERMINAL_PROMPT: "0" };
849
+ const url = `${GITHUB_GIT}/${owner}/${name}.git`;
850
+ // Name the remote FIRST when there is not one, so the push can be `-u` and
851
+ // leave the branch tracking it. Pushing to a bare URL and setting the
852
+ // upstream afterwards cannot work: `--set-upstream-to` needs a
853
+ // remote-tracking ref, and a URL push writes none.
854
+ const adopt = !git(["remote", "get-url", "origin"]);
855
+ if (adopt) git(["remote", "add", "origin", url]);
856
+ // An author who already has an `origin` keeps it, and the push goes to the
857
+ // URL directly rather than quietly redirecting their remote.
858
+ const pushed = adopt
859
+ ? git(["push", "-u", "origin", `${branch}:${branch}`], env)
860
+ : git(["push", url, `${branch}:${branch}`], env);
861
+ if (pushed === null) {
862
+ // Leave nothing behind that was not there before.
863
+ if (adopt) git(["remote", "remove", "origin"]);
864
+ return false;
865
+ }
866
+ return true;
867
+ } finally {
868
+ try {
869
+ rmSync(dirname(askpass), { recursive: true, force: true });
870
+ } catch {
871
+ /* a leftover temp dir is not worth failing a publish over */
872
+ }
873
+ }
874
+ }
875
+
876
+ /**
877
+ * The minimum that gives a repository a default branch: one commit, made
878
+ * through the API. Only for a pack published from somewhere that is not a git
879
+ * checkout — otherwise the author's own history does this job better.
880
+ */
881
+ async function seedDefaultBranch(owner: string, name: string, id: string, token: string): Promise<void> {
882
+ await gh(`${GITHUB_API}/repos/${owner}/${name}/contents/README.md`, token, {
883
+ method: "PUT",
884
+ contentType: "application/json",
885
+ body: JSON.stringify({
886
+ message: `${id}: create pack repository`,
887
+ content: Buffer.from(
888
+ `# ${name}\n\nA [failproofai](https://github.com/failproofai/failproofai) policy pack.\n\n\`\`\`bash\nfailproofai policies add ${owner}/${name}\n\`\`\`\n`,
889
+ "utf8",
890
+ ).toString("base64"),
891
+ }),
892
+ });
893
+ }
894
+
628
895
  /**
629
896
  * Where `bun` is, if it is anywhere.
630
897
  *
@@ -736,6 +1003,9 @@ function publishEntryArg(rest: string[]): string | undefined {
736
1003
 
737
1004
  const GITHUB_API = process.env.FAILPROOFAI_GITHUB_API ?? "https://api.github.com";
738
1005
  const GITHUB_UPLOADS = process.env.FAILPROOFAI_GITHUB_UPLOADS ?? "https://uploads.github.com";
1006
+ /** Where the git remote lives, so the push path can be tested against a real
1007
+ * local bare repository rather than only reasoned about. */
1008
+ const GITHUB_GIT = process.env.FAILPROOFAI_GITHUB_GIT ?? "https://github.com";
739
1009
 
740
1010
  /**
741
1011
  * The credential, from the places a developer already keeps one.
@@ -822,6 +1092,60 @@ function ghError(res: { status: number; json: Record<string, unknown> | null; te
822
1092
  * - a private repository, which publishes to nobody, because installs are
823
1093
  * anonymous HTTPS with no credential to offer.
824
1094
  */
1095
+ /**
1096
+ * The two things `publish` cannot work out on its own, asked instead of
1097
+ * demanded as flags.
1098
+ *
1099
+ * Everything else it already derives: the policy files by content, the version
1100
+ * by counting the repository's own releases, the id from the repository, the
1101
+ * credential from the environment. What is left is genuinely a question —
1102
+ * WHERE this should live, when the folder has no git remote naming somewhere —
1103
+ * and a confirmation, because publishing is public and a version number can
1104
+ * never be reused.
1105
+ *
1106
+ * Only ever on a TTY. A pipe, a CI job or a test gets the old behaviour: flags
1107
+ * decide, and a missing `--repo` still means "build the assets and stop"
1108
+ * rather than a prompt nobody can answer.
1109
+ */
1110
+ async function askWhereToPublish(
1111
+ suggestion: string,
1112
+ io: { stdin: TTYIn; stdout: TTYOut },
1113
+ ): Promise<string | null> {
1114
+ const answer = await promptText({
1115
+ // The suggestion goes in the HINT, not just in `defaultValue`: the hint is
1116
+ // what renders as the placeholder, while `defaultValue` is only applied on
1117
+ // an empty submit and is never shown. Passing it as the default alone left
1118
+ // the one keystroke that finishes this prompt — return — invisible.
1119
+ message: "Where should this publish?",
1120
+ hint: `${suggestion} · created if it does not exist`,
1121
+ defaultValue: suggestion,
1122
+ validate: (v) => {
1123
+ const t = v.trim();
1124
+ if (!/^[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*$/.test(t)) return "owner/repo";
1125
+ return null;
1126
+ },
1127
+ stdin: io.stdin,
1128
+ stdout: io.stdout,
1129
+ });
1130
+ return answer === null ? null : answer.trim();
1131
+ }
1132
+
1133
+ /**
1134
+ * The account the credential belongs to, for the default answer above.
1135
+ *
1136
+ * Best-effort: a failure here costs a nicer default and nothing else, so it
1137
+ * never blocks or reports. The prompt still has the folder name to fall back
1138
+ * on, which is what the repository would most likely be called anyway.
1139
+ */
1140
+ async function credentialLogin(token: string): Promise<string | null> {
1141
+ try {
1142
+ const me = await gh(`${GITHUB_API}/user`, token);
1143
+ return typeof me.json?.login === "string" ? me.json.login : null;
1144
+ } catch {
1145
+ return null;
1146
+ }
1147
+ }
1148
+
825
1149
  async function publish(rest: string[]): Promise<PackCliResult> {
826
1150
  const flag = (name: string): string | undefined => {
827
1151
  const i = rest.findIndex((a) => a === `--${name}` || a.startsWith(`--${name}=`));
@@ -838,7 +1162,32 @@ async function publish(rest: string[]): Promise<PackCliResult> {
838
1162
  // it is that file and only that file.
839
1163
  const discovered = entry ? [] : findEntry(process.cwd());
840
1164
  if (!entry && discovered.length > 0) entry = discovered[0];
841
- const repo = flag("repo") ?? (entry ? inferRepo(entry) : null) ?? undefined;
1165
+ let repo = flag("repo") ?? (entry ? inferRepo(entry) : null) ?? undefined;
1166
+ // Nothing names a destination and there is somebody to ask. This is the
1167
+ // whole of "just run `failproofai publish`": the folder is a git repo full
1168
+ // of policies, and the one thing it cannot know is where they should go.
1169
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1170
+ const askingWhere = interactive && !repo && entry && !rest.includes("--dry-run");
1171
+ if (askingWhere) {
1172
+ // What is about to be published, before being asked where to put it. The
1173
+ // question is answerable without it — but "publish" with no arguments
1174
+ // otherwise gives no sign of WHICH files it found, and finding the wrong
1175
+ // ones is the mistake that matters here.
1176
+ const found = discovered.length > 0 ? discovered : [entry!];
1177
+ process.stdout.write(
1178
+ `\n ${found.length} policy ${found.length === 1 ? "file" : "files"}: ` +
1179
+ `${found.map((f) => basename(f)).join(", ")}\n\n`,
1180
+ );
1181
+ const token = githubToken();
1182
+ const login = token ? await credentialLogin(token) : null;
1183
+ const folder = basename(resolve(entry!, ".."));
1184
+ const answered = await askWhereToPublish(
1185
+ `${login ?? "your-account"}/${folder}`,
1186
+ { stdin: process.stdin as unknown as TTYIn, stdout: process.stdout as unknown as TTYOut },
1187
+ );
1188
+ if (answered === null) return ok(["Nothing was published."]);
1189
+ repo = answered;
1190
+ }
842
1191
  // A tag on HEAD is somebody SAYING what this release is, so it wins over a
843
1192
  // counted one. Everything else is decided after the repository is known,
844
1193
  // because the count comes from what that repository has already published.
@@ -846,8 +1195,19 @@ async function publish(rest: string[]): Promise<PackCliResult> {
846
1195
  let versionCounted = false;
847
1196
  // The id and the repo are usually the same words, and requiring both is asking
848
1197
  // the same question twice. Either one alone answers for the other.
849
- const id = flag("id") ?? repo;
850
1198
  const dryRun = rest.includes("--dry-run") || !repo;
1199
+ // A dry run publishes nothing, so the id only has to name the artifact it
1200
+ // writes — and refusing to build one because the folder has no git remote
1201
+ // yet blocked the exact thing a dry run is for: looking at the pack BEFORE
1202
+ // committing to a repository for it. The folder name is what the repository
1203
+ // would be called anyway, and `--id` or `--repo` overrides it the moment
1204
+ // either is known.
1205
+ //
1206
+ // `local/` rather than a guessed account: the owner is genuinely not known
1207
+ // yet, and a plausible-looking one would be baked into a manifest somebody
1208
+ // could upload by hand.
1209
+ const id =
1210
+ flag("id") ?? repo ?? (dryRun && entry ? `local/${basename(resolve(entry, ".."))}` : undefined);
851
1211
  const [owner, name] = (repo ?? "/").split("/");
852
1212
  if (repo && (!owner || !name || repo.split("/").length !== 2)) {
853
1213
  return fail([`--repo must be <owner>/<repo>, got ${JSON.stringify(repo)}`]);
@@ -899,7 +1259,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
899
1259
  "It needs write access to releases on that repository, and nothing else.",
900
1260
  ]);
901
1261
  }
902
- const ensured = await ensureRepo(owner, name, id, token);
1262
+ const ensured = await ensureRepo(owner, name, id, token, resolve(entry, ".."));
903
1263
  if ("error" in ensured) return fail(ensured.error);
904
1264
  created = ensured.created;
905
1265
  repoInfo = ensured.info;
@@ -953,7 +1313,13 @@ async function publish(rest: string[]): Promise<PackCliResult> {
953
1313
  ? ["Dry run — nothing was published.", `Drop --dry-run to release it on ${repo}.`]
954
1314
  : [
955
1315
  "Nothing was published: name a repository to release it on.",
956
- ` failproofai publish ${entry} --repo <owner>/<repo> --version ${version}`,
1316
+ // Just the repository. It used to spell out the entry file and the
1317
+ // version too — naming ONE of the files it had that moment finished
1318
+ // bundling, and pinning a version it works out by counting the
1319
+ // repository's own releases. Both were wrong the moment they were
1320
+ // printed, and both taught the reader that publishing needs flags
1321
+ // it does not need.
1322
+ " failproofai publish --repo <owner>/<repo>",
957
1323
  ]),
958
1324
  ]);
959
1325
  }
@@ -1157,18 +1523,19 @@ async function pickFromSource(
1157
1523
  async function add(rest: string[]): Promise<PackCliResult> {
1158
1524
  const source = packAddSource(rest);
1159
1525
  const selection = selectionFrom(rest);
1160
-
1161
- // Our own pack, by the short name. `core` is a SPELLING of a GitHub source,
1162
- // not a second delivery path: it resolves to CORE_SOURCE and is fetched,
1163
- // verified and pinned exactly like anybody else's.
1164
- //
1165
- // The package used to carry a copy so this worked with no network. It no
1166
- // longer does, on purpose. A pack that ships inside the binary is a policy set
1167
- // we picked for you and wrote to your disk before you asked for it, and it
1168
- // gave our own policies a route no third-party pack could use — which is the
1169
- // opposite of the thing this whole lane exists to make possible.
1170
- const resolvedSource =
1171
- source && CORE_ALIASES.has(source.toLowerCase()) ? CORE_SOURCE : source;
1526
+ // Refused before anything is fetched or written: a pack scoped to an agent
1527
+ // that does not exist enforces on nothing, and saying so afterwards is no use
1528
+ // to the script that already read exit 0 and moved on.
1529
+ if (selection.error) return fail(selection.error);
1530
+
1531
+ // No short name for our own pack any more. `core` resolved to CORE_SOURCE and
1532
+ // was fetched, verified and pinned like anybody else's — but a spelling only
1533
+ // WE get to use still made our policies read as part of the tool rather than
1534
+ // as one pack among others, which is the distinction this lane exists to
1535
+ // erase. Ours is typed the same way yours is: `FailproofAI/policies`.
1536
+ // `parsePackSource` still recognises the retired spellings, so typing one
1537
+ // gets told what to type instead.
1538
+ const resolvedSource = source;
1172
1539
 
1173
1540
  if (!resolvedSource) {
1174
1541
  return fail(["Usage: failproofai policies add <source> [--policy a,b] [--category x,y] [--all]"]);
@@ -91,6 +91,15 @@ function baseUrl(): string {
91
91
  const OWNER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
92
92
  const TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
93
93
 
94
+ export const CORE_SOURCE = "FailproofAI/policies";
95
+
96
+ /**
97
+ * Spellings that USED to resolve to our own pack, kept only so typing one gets
98
+ * an answer rather than a parse error. They are not resolved any more — see the
99
+ * throw in `parsePackSource`.
100
+ */
101
+ export const RETIRED_CORE_ALIASES = new Set(["core", "failproofai", "official"]);
102
+
94
103
  /**
95
104
  * Parse a pack source into owner, repo and an optional tag.
96
105
  *
@@ -136,6 +145,17 @@ export function parsePackSpec(source: string): PackSpec {
136
145
  const tag = at > 0 ? withoutScheme.slice(at + 1) : null;
137
146
  const slash = repoPart.indexOf("/");
138
147
  if (slash <= 0 || slash === repoPart.length - 1) {
148
+ // `core` / `failproofai` / `official` used to be spellings of our own pack.
149
+ // They are gone on purpose: a short name only WE get to use makes our
150
+ // policies look like part of the tool rather than a pack like any other,
151
+ // which is exactly the distinction this whole surface exists to remove.
152
+ // Named explicitly here so the spelling that used to work says what to type
153
+ // instead of failing as an unparseable source.
154
+ if (RETIRED_CORE_ALIASES.has(repoPart.trim().toLowerCase())) {
155
+ throw new Error(
156
+ `${JSON.stringify(source)} is no longer a pack name — ours is a pack like anyone else's now. Use ${CORE_SOURCE} instead.`,
157
+ );
158
+ }
139
159
  throw new Error(
140
160
  `pack source must be owner/repo, owner/repo@tag, or a github.com URL — got ${JSON.stringify(source)}`,
141
161
  );
@@ -325,8 +345,6 @@ interface FetchedPack {
325
345
  * gives our own policies a delivery path nobody else's pack can use. One lane,
326
346
  * the same for everyone — ours are just the pack whose short name we spell.
327
347
  */
328
- export const CORE_SOURCE = "FailproofAI/policies";
329
- export const CORE_ALIASES = new Set(["core", "failproofai", "official"]);
330
348
 
331
349
  export interface PackAddOptions {
332
350
  only?: string[];
@@ -358,12 +376,7 @@ export async function addPackFromSource(
358
376
  source: string,
359
377
  opts: PackAddOptions = {},
360
378
  ): Promise<AddedPack> {
361
- // `core` is a spelling of CORE_SOURCE, not a second delivery path. Resolved
362
- // HERE rather than in the CLI so the dashboard and the CLI cannot disagree
363
- // about what the short name means — they did once, and a name the CLI could
364
- // resolve failed in the browser.
365
- const resolved = CORE_ALIASES.has(source.trim().toLowerCase()) ? CORE_SOURCE : source;
366
- const result = await addPack(resolved, opts);
379
+ const result = await addPack(source, opts);
367
380
  return {
368
381
  id: result.id,
369
382
  version: result.version,