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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__03r-lqq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12021ot._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0clqfab._.js → [root-of-the-server]__1tg4pu6._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1lbh81u._.js → _19uwam-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1m84s36._.js → [root-of-the-server]__00-s7h8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +6 -6
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__14muoc4._.js → [root-of-the-server]__128se8m._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0jzbk4_._.js → [root-of-the-server]__190won8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rsd3ll._.js → [root-of-the-server]__1sc84bb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0ovx4lz._.js → _0no96tu._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1dkh76z._.js → _11fc-hb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1ddpc5g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1db3-8n._.js → _1if9ixi._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_1cf5ya2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_15bjxkm._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
- package/.next/standalone/.next/static/chunks/{0lshnlpy1m6bc.js → 0931avh-06lj5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0k_yvjc1yz0pq.js → 0h-3zffoy-_ji.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ds64uculrze8.js → 0p84npzxdgpc5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0d_re32wscs0b.js → 0sdgnflvw8kta.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2dbi_x0c8n8fi.js → 0z7au3njlak0j.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1o0ahew9568ls.js → 1id7o44jjegf0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{00_t-xvc59tkb.js → 276n-edifojbd.js} +2 -2
- package/.next/standalone/.next/static/chunks/{07j2u70_3qsx8.js → 2jfskwd2u8o5r.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2817_8e94ah9z.js → 2pxr7nr1nyotn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1oz0svxwt9574.js → 3i8rg2fv9nd6e.js} +2 -2
- package/.next/standalone/.next/static/chunks/{1q76jk_1_wgjq.js → 3vu_7_eor1m-k.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-17utl_jw0wj11.js → turbopack-2x-o099q64i8h.js} +1 -1
- package/.next/standalone/lib/opencode-db.ts +77 -0
- package/.next/standalone/lib/opencode-projects.ts +37 -41
- package/.next/standalone/lib/opencode-sessions.ts +59 -36
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +116 -34
- package/dist/cli.mjs +1732 -1177
- package/dist/worker.mjs +180 -165
- package/lib/opencode-db.ts +77 -0
- package/lib/opencode-projects.ts +37 -41
- package/lib/opencode-sessions.ts +59 -36
- package/package.json +9 -9
- package/src/audit/cache.ts +167 -11
- package/src/audit/cli-adapters/claude.ts +111 -1
- package/src/audit/cli-adapters/index.ts +31 -1
- package/src/audit/cli.ts +12 -0
- package/src/audit/index.ts +164 -13
- package/src/hooks/configure-wizard.ts +112 -18
- package/src/hooks/fp-reset.ts +1 -1
- package/src/hooks/handler.ts +13 -1
- package/src/hooks/manager.ts +69 -4
- package/src/hooks/onboarding-attempt.ts +4 -4
- package/src/hooks/pack-cli.ts +399 -32
- package/src/hooks/pack-store.ts +21 -8
- package/src/hooks/read-stdin.ts +78 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1l-khtb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mhmdzs._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +0 -3
- /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{0iNzkTsP6rh14Rj5WL17o → mAYN9BOxWVFREnVp3yCHp}/_ssgManifest.js +0 -0
package/src/hooks/pack-cli.ts
CHANGED
|
@@ -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
|
-
/**
|
|
67
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
526
|
+
chosen = answer.trim();
|
|
378
527
|
}
|
|
379
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1162
|
-
//
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
//
|
|
1166
|
-
//
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
|
|
1171
|
-
|
|
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]"]);
|
package/src/hooks/pack-store.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|