failproofai 1.0.2-beta.8 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- 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/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 +3 -2
- 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 -3
- 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 +2 -2
- 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 +4 -3
- 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 +4 -4
- 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 +4 -4
- 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.nft.json +1 -1
- 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/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/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +4 -4
- 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/server-reference-manifest.json +1 -1
- 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/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.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/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/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/[externals]__0-9ti7z._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[externals]__1lt47k4._.js → [externals]__0sg9-3p._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__0u3wrvq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zlqo2t._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1anxb5k._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +5 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +27 -0
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
- package/.next/standalone/.next/server/chunks/{_19uwam-._.js → _1-akg6w._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -7
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.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_0iu54mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +10 -0
- 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]__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]__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]__0n_lxhg._.js +5 -0
- 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]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1yh7hlv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0ehudfr._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1n2ywfz._.js → _1es2j7i._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1h33neq._.js → _1un1pug._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +9 -9
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware.js +1 -1
- 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/0nkpwjk01ei3t.js +1 -0
- package/.next/standalone/.next/static/chunks/{2iryw37qfqy5y.js → 0ofdelz65_517.js} +1 -1
- package/.next/standalone/.next/static/chunks/{34j7nmidlcuni.js → 12uhnpwgsmcev.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0i3a6wf-f_yky.js → 1_1eb_149xyt9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3927pu4ss_l4k.js → 1tx9pnxyjrkoj.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1n04wzsaes73g.js → 1x0jrl3a4_i2m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3r0udrtpn78-e.js → 1zpv6omeln7al.js} +1 -1
- package/.next/standalone/.next/static/chunks/{416zumre3fvfw.js → 20hbg-r3io_ip.js} +1 -1
- package/.next/standalone/.next/static/chunks/{11a_m8yksthtf.js → 32bhi32ym8yrz.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0u2ojcw25c_np.js → 3fr78ugeb4tjc.js} +1 -1
- package/.next/standalone/app/actions/get-hooks-config.ts +8 -1
- package/.next/standalone/app/actions/pack-actions.ts +12 -1
- package/.next/standalone/app/policies/hooks-client.tsx +41 -4
- package/.next/standalone/fp-cloud-cli/tests/test_pagination.py +204 -0
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/README.md +58 -13
- package/bin/failproofai.mjs +10 -1
- package/dist/cli.mjs +22429 -21292
- package/dist/worker.mjs +1247 -540
- package/package.json +9 -9
- package/src/audit/index.ts +24 -4
- package/src/hooks/builtin-policies.ts +933 -8
- package/src/hooks/custom-hooks-loader.ts +98 -8
- package/src/hooks/handler.ts +26 -14
- package/src/hooks/manager.ts +36 -2
- package/src/hooks/pack-cli.ts +415 -42
- package/src/hooks/pack-failclosed.ts +100 -4
- package/src/hooks/pack-manifest.ts +29 -0
- package/src/hooks/pack-param-key.ts +36 -0
- package/src/hooks/pack-store.ts +101 -9
- package/src/hooks/policy-catalog.ts +9 -1
- package/src/hooks/policy-evaluator.ts +77 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tg4pu6._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +0 -10
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-wzs7f._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wlvpe5._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/_0l7zv8b._.js +0 -23
- package/.next/standalone/.next/static/chunks/1ch5l9t2p16y4.js +0 -1
- /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{NgD5HvuyPxwgrpeIiafJL → 5-xDMxcnrcXDFPQiy3yhg}/_ssgManifest.js +0 -0
package/src/hooks/pack-cli.ts
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
-
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
13
13
|
import { INTEGRATION_TYPES } from "./types";
|
|
14
14
|
import { PACK_COMMIT_RE, PACK_VERSION_RE } from "./pack-manifest";
|
|
15
15
|
import { detectInstalledClis } from "./integrations";
|
|
16
16
|
import { parsePackIdentity, parsePackPolicy, readInstalledPacks } from "./pack-manifest";
|
|
17
17
|
import {
|
|
18
|
+
AmbiguousPackId,
|
|
18
19
|
PACK_CHECKSUMS_ASSET,
|
|
19
20
|
PACK_ENTRY_ASSET,
|
|
20
21
|
PACK_MANIFEST_ASSET,
|
|
@@ -178,7 +179,24 @@ function parseCliList(rest: string[]): { clis?: string[] } | { error: string[] }
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
const names = values.map((v) => v.trim()).filter(Boolean);
|
|
181
|
-
|
|
182
|
+
// `--cli` with nothing after it is a typo, not a scope.
|
|
183
|
+
//
|
|
184
|
+
// It used to become `clis: []`, which `installed.json` stores verbatim and
|
|
185
|
+
// `handler.ts` reads as "guard NO agent" (`pack.clis && !includes(cli)`) —
|
|
186
|
+
// so `policies add <pack> --cli` exited 0, printed "enabled (1/3, the pack's
|
|
187
|
+
// defaults)", and installed a pack that enforced nowhere. `--cli` is last on
|
|
188
|
+
// the line, or followed by the source or another flag, more easily than any
|
|
189
|
+
// other flag: `looksLikeCliName` stops at anything with a `-` or a `/`.
|
|
190
|
+
// Refused the same way `--policy` and `--category` refuse an empty list, and
|
|
191
|
+
// before anything is fetched.
|
|
192
|
+
if (names.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
error: [
|
|
195
|
+
"--cli needs at least one agent name, comma-separated",
|
|
196
|
+
`--cli takes any of: ${INTEGRATION_TYPES.join(", ")}`,
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
182
200
|
|
|
183
201
|
const known = new Set<string>(INTEGRATION_TYPES);
|
|
184
202
|
const unknown = names.filter((n) => !known.has(n));
|
|
@@ -698,20 +716,30 @@ function inferRepo(entryPath: string): string | null {
|
|
|
698
716
|
* under it is left OUT of the dirty read — see {@link skipOutDir} for the
|
|
699
717
|
* self-inflicted refusal that costs.
|
|
700
718
|
*/
|
|
701
|
-
function inferCommit(
|
|
719
|
+
function inferCommit(
|
|
720
|
+
entryPath: string,
|
|
721
|
+
outDir?: string,
|
|
722
|
+
sources?: string[],
|
|
723
|
+
): Provenance | null {
|
|
702
724
|
const cwd = resolve(entryPath, "..");
|
|
703
|
-
|
|
725
|
+
// Two readers over one runner: `raw` for output whose LEADING bytes carry
|
|
726
|
+
// meaning (a porcelain status column is a space), `git` for the rest.
|
|
727
|
+
const raw = (args: string[]): string | null => {
|
|
704
728
|
try {
|
|
705
729
|
return execFileSync("git", args, {
|
|
706
730
|
cwd,
|
|
707
731
|
encoding: "utf8",
|
|
708
732
|
stdio: ["ignore", "pipe", "ignore"],
|
|
709
733
|
timeout: 5_000,
|
|
710
|
-
})
|
|
734
|
+
});
|
|
711
735
|
} catch {
|
|
712
736
|
return null;
|
|
713
737
|
}
|
|
714
738
|
};
|
|
739
|
+
const git = (args: string[]): string | null => {
|
|
740
|
+
const out = raw(args);
|
|
741
|
+
return out === null ? null : out.trim();
|
|
742
|
+
};
|
|
715
743
|
const sha = git(["rev-parse", "HEAD"]);
|
|
716
744
|
if (!sha || !/^[0-9a-f]{40}$/.test(sha)) return null;
|
|
717
745
|
// `git()` answers "" for a clean tree and null when it could not run the
|
|
@@ -726,7 +754,150 @@ function inferCommit(entryPath: string, outDir?: string): { sha: string; dirty:
|
|
|
726
754
|
// cannot vouch for costs one `--version`, and the alternative is publishing
|
|
727
755
|
// one we could not read.
|
|
728
756
|
const status = git(["status", "--porcelain", ...skipOutDir(git, entryPath, outDir)]);
|
|
729
|
-
|
|
757
|
+
// The sources are checked BY NAME as well as the tree as a whole. A clean
|
|
758
|
+
// tree does not mean the bytes being bundled are in HEAD: `.gitignore` hides
|
|
759
|
+
// a source from the tree read entirely, and the artifact would then carry a
|
|
760
|
+
// commit that does not contain it.
|
|
761
|
+
const sourceState = sources ? sourcesInHead(git, raw, sources) : ({ ok: true } as const);
|
|
762
|
+
return {
|
|
763
|
+
sha,
|
|
764
|
+
dirty: status !== "" || !sourceState.ok,
|
|
765
|
+
...(sourceState.ok ? {} : { unpublishable: sourceState }),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* `git status --porcelain -z`, parsed.
|
|
771
|
+
*
|
|
772
|
+
* The format is NOT one record per NUL field. Each entry is `XY <path>`, and a
|
|
773
|
+
* rename or copy is followed by its ORIGINAL path as a separate NUL field
|
|
774
|
+
* carrying NO status prefix:
|
|
775
|
+
*
|
|
776
|
+
* R b.mjs\0a.mjs\0
|
|
777
|
+
*
|
|
778
|
+
* Splitting on NUL and slicing three characters off every field therefore turns
|
|
779
|
+
* `a.mjs` into `js` — a path that resolves to nothing, is in nobody's file set,
|
|
780
|
+
* and made `publish` refuse a policy file its author had simply renamed. The
|
|
781
|
+
* original has to be CONSUMED by the entry that produced it.
|
|
782
|
+
*/
|
|
783
|
+
function porcelainEntries(status: string): Array<{ code: string; path: string; from?: string }> {
|
|
784
|
+
const fields = status.split("\0");
|
|
785
|
+
const out: Array<{ code: string; path: string; from?: string }> = [];
|
|
786
|
+
for (let i = 0; i < fields.length; i += 1) {
|
|
787
|
+
const entry = fields[i];
|
|
788
|
+
if (!entry || entry.length < 4) continue;
|
|
789
|
+
const code = entry.slice(0, 2);
|
|
790
|
+
const path = entry.slice(3);
|
|
791
|
+
if (code.startsWith("R") || code.startsWith("C")) {
|
|
792
|
+
const from = fields[i + 1];
|
|
793
|
+
i += 1;
|
|
794
|
+
out.push({ code, path, ...(from ? { from } : {}) });
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
out.push({ code, path });
|
|
798
|
+
}
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** What a checkout says about the bytes being published. `unpublishable` names
|
|
803
|
+
* the sources that are not in HEAD, so the refusal can list them. */
|
|
804
|
+
export interface Provenance {
|
|
805
|
+
sha: string;
|
|
806
|
+
dirty: boolean;
|
|
807
|
+
unpublishable?: { offending: string[]; why: "missing" | "modified" };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Whether every file about to be BUNDLED is in HEAD and unmodified.
|
|
812
|
+
*
|
|
813
|
+
* The whole-tree read above answers "is this checkout clean", which is a
|
|
814
|
+
* different question and misses three ways a source reaches the artifact
|
|
815
|
+
* without reaching the commit:
|
|
816
|
+
*
|
|
817
|
+
* - **Ignored files.** `git status --porcelain` omits anything `.gitignore`
|
|
818
|
+
* matches, even with `--untracked-files=all`. A policy file the author
|
|
819
|
+
* ignored therefore read as clean, got bundled, and the manifest recorded
|
|
820
|
+
* HEAD as the commit it came from — bytes that commit does not contain.
|
|
821
|
+
* - **Untracked files in an otherwise clean tree**, for the same reason once
|
|
822
|
+
* the ignore rules are wide.
|
|
823
|
+
* - **A tag.** `inferTaggedVersion` checked only the ENTRY, while `publish`
|
|
824
|
+
* bundles every discovered policy file. A tagged commit, a clean entry and a
|
|
825
|
+
* modified sibling published that sibling under a tag naming a commit
|
|
826
|
+
* without it.
|
|
827
|
+
*
|
|
828
|
+
* One check for all of them, run against the actual source LIST, because that
|
|
829
|
+
* is the set whose bytes end up in the artifact — and provenance is a claim
|
|
830
|
+
* about those bytes and nothing else.
|
|
831
|
+
*
|
|
832
|
+
* `-z` rather than line splitting: git C-quotes a path containing a newline or
|
|
833
|
+
* a quote, so the parsed path is escape sequences that resolve to no file, and
|
|
834
|
+
* a legal filename containing ` -> ` is truncated by the rename split. NUL
|
|
835
|
+
* separation has neither problem — and a rename arrives as two NUL-separated
|
|
836
|
+
* fields rather than one arrow-joined line — which is what
|
|
837
|
+
* {@link porcelainEntries} reads.
|
|
838
|
+
*/
|
|
839
|
+
/**
|
|
840
|
+
* A path spelled the way git spells it: symlinks resolved.
|
|
841
|
+
*
|
|
842
|
+
* `rev-parse --show-toplevel` answers with the RESOLVED root, so a source named
|
|
843
|
+
* through a symlinked directory — `publish ~/policies/guards.mjs`, where
|
|
844
|
+
* `~/policies` links into a checkout — measures as outside the repository
|
|
845
|
+
* against it, and a committed, untouched file was reported as never committed
|
|
846
|
+
* with `git add -f` as the advice, which does nothing for it.
|
|
847
|
+
*
|
|
848
|
+
* It cuts the other way too, which is why this resolves the SOURCES rather than
|
|
849
|
+
* only un-resolving the root: a policy file that is itself a symlink out of the
|
|
850
|
+
* repository has a blob in HEAD holding the link's TARGET NAME, while the bytes
|
|
851
|
+
* bundled are the target's. Asking about the link's own path called that
|
|
852
|
+
* publishable and recorded a commit containing none of those bytes.
|
|
853
|
+
*
|
|
854
|
+
* Falls back to the path as given when it cannot be resolved, so a source that
|
|
855
|
+
* has since been deleted is still reported rather than throwing here.
|
|
856
|
+
*/
|
|
857
|
+
function realOrSelf(path: string): string {
|
|
858
|
+
try {
|
|
859
|
+
return realpathSync(path);
|
|
860
|
+
} catch {
|
|
861
|
+
return path;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function sourcesInHead(
|
|
866
|
+
git: (args: string[]) => string | null,
|
|
867
|
+
raw: (args: string[]) => string | null,
|
|
868
|
+
sources: string[],
|
|
869
|
+
): { ok: true } | { ok: false; offending: string[]; why: "missing" | "modified" } {
|
|
870
|
+
const paths = sources.map((f) => realOrSelf(resolve(f)));
|
|
871
|
+
// Present in the commit at all. `cat-file -e HEAD:<path>` is the cheapest
|
|
872
|
+
// question that distinguishes "tracked and committed" from "tracked but
|
|
873
|
+
// added only to the index", which `ls-files` would answer yes to.
|
|
874
|
+
const shown = git(["rev-parse", "--show-toplevel"]);
|
|
875
|
+
const top = shown === null ? null : realOrSelf(resolve(shown));
|
|
876
|
+
const missing: string[] = [];
|
|
877
|
+
for (const path of paths) {
|
|
878
|
+
const rel = top ? relative(top, path) : null;
|
|
879
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
880
|
+
missing.push(path);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if (git(["cat-file", "-e", `HEAD:${rel}`]) === null) missing.push(path);
|
|
884
|
+
}
|
|
885
|
+
if (missing.length > 0) return { ok: false, offending: missing, why: "missing" };
|
|
886
|
+
|
|
887
|
+
// `--ignored=matching` so an ignored source is REPORTED rather than omitted,
|
|
888
|
+
// which is the whole point of asking about the sources by name.
|
|
889
|
+
const status = raw([
|
|
890
|
+
"status", "--porcelain", "-z", "--untracked-files=all", "--ignored=matching",
|
|
891
|
+
"--", ...paths,
|
|
892
|
+
]);
|
|
893
|
+
if (status === null) return { ok: false, offending: paths, why: "modified" };
|
|
894
|
+
// Both halves of a rename count: either one being listed means the set on
|
|
895
|
+
// disk is not the set HEAD holds.
|
|
896
|
+
const dirty = porcelainEntries(status).flatMap((entry) =>
|
|
897
|
+
[entry.path, entry.from].filter(Boolean).map((path) => resolve(top ?? ".", path as string)),
|
|
898
|
+
);
|
|
899
|
+
if (dirty.length > 0) return { ok: false, offending: dirty, why: "modified" };
|
|
900
|
+
return { ok: true };
|
|
730
901
|
}
|
|
731
902
|
|
|
732
903
|
/**
|
|
@@ -780,20 +951,24 @@ function skipOutDir(
|
|
|
780
951
|
* name cannot — so this wins over the commit version. Absent a tag there is
|
|
781
952
|
* nothing here to infer, and the commit the tree sits at is used instead.
|
|
782
953
|
*/
|
|
783
|
-
function inferTaggedVersion(entryPath: string): string | null {
|
|
954
|
+
function inferTaggedVersion(entryPath: string, sources?: string[]): string | null {
|
|
784
955
|
const cwd = resolve(entryPath, "..");
|
|
785
|
-
const
|
|
956
|
+
const raw = (args: string[]): string | null => {
|
|
786
957
|
try {
|
|
787
958
|
return execFileSync("git", args, {
|
|
788
959
|
cwd,
|
|
789
960
|
encoding: "utf8",
|
|
790
961
|
stdio: ["ignore", "pipe", "ignore"],
|
|
791
962
|
timeout: 5_000,
|
|
792
|
-
})
|
|
963
|
+
});
|
|
793
964
|
} catch {
|
|
794
965
|
return null;
|
|
795
966
|
}
|
|
796
967
|
};
|
|
968
|
+
const git = (args: string[]): string | null => {
|
|
969
|
+
const out = raw(args);
|
|
970
|
+
return out === null ? null : out.trim();
|
|
971
|
+
};
|
|
797
972
|
const tag = git(["describe", "--tags", "--exact-match", "HEAD"]);
|
|
798
973
|
if (!tag || !PACK_VERSION_RE.test(tag)) return null;
|
|
799
974
|
// A tag NAMES a commit, so shipping edited bytes under it publishes something
|
|
@@ -802,6 +977,13 @@ function inferTaggedVersion(entryPath: string): string | null {
|
|
|
802
977
|
// upsert. Compared against "" rather than tested for truthiness for the reason
|
|
803
978
|
// `inferCommit` gives: null is "could not read the tree", and reading that as
|
|
804
979
|
// clean is how a dirty tree slips out under a tag that does not describe it.
|
|
980
|
+
// Every source that will be BUNDLED, not just the entry. `publish` discovers
|
|
981
|
+
// and bundles each policy file in the directory, so a tagged commit with a
|
|
982
|
+
// clean entry and a modified sibling published that sibling under a tag
|
|
983
|
+
// naming a commit without it. Same check the sha path uses, so the two
|
|
984
|
+
// cannot disagree about what "this commit contains these bytes" means.
|
|
985
|
+
const covered = sourcesInHead(git, raw, sources ?? [entryPath]);
|
|
986
|
+
if (!covered.ok) return null;
|
|
805
987
|
if (git(["status", "--porcelain", "--", entryPath]) !== "") return null;
|
|
806
988
|
return tag;
|
|
807
989
|
}
|
|
@@ -853,6 +1035,78 @@ function findEntry(dir: string): string[] {
|
|
|
853
1035
|
return found.sort();
|
|
854
1036
|
}
|
|
855
1037
|
|
|
1038
|
+
/**
|
|
1039
|
+
* Every relative specifier a source file names, in the four spellings a bundler
|
|
1040
|
+
* follows. Anchored to a line start for the two import FORMS, because that is
|
|
1041
|
+
* where a real one is: a `"./x.mjs"` inside a policy's own deny message is a
|
|
1042
|
+
* string, not an edge of the module graph, and the file it happens to spell
|
|
1043
|
+
* should not be dragged into a provenance check it has nothing to do with.
|
|
1044
|
+
*/
|
|
1045
|
+
const LOCAL_IMPORT_RES: readonly RegExp[] = [
|
|
1046
|
+
/(?:^|\n)\s*(?:import|export)[^;\n]*?from\s*["'](\.[^"'\n]*)["']/g,
|
|
1047
|
+
/(?:^|\n)\s*import\s*["'](\.[^"'\n]*)["']/g,
|
|
1048
|
+
/\bimport\s*\(\s*["'](\.[^"'\n]*)["']\s*\)/g,
|
|
1049
|
+
/\brequire\s*\(\s*["'](\.[^"'\n]*)["']\s*\)/g,
|
|
1050
|
+
];
|
|
1051
|
+
|
|
1052
|
+
/** What a specifier without an extension can name on disk, in resolution order. */
|
|
1053
|
+
const LOCAL_IMPORT_EXTENSIONS: readonly string[] = [
|
|
1054
|
+
"", ".mjs", ".js", ".ts", ".mts", ".cts", ".cjs", ".jsx", ".tsx", ".json",
|
|
1055
|
+
"/index.mjs", "/index.js", "/index.ts",
|
|
1056
|
+
];
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* The policy files plus everything they IMPORT.
|
|
1060
|
+
*
|
|
1061
|
+
* A file reaches the artifact by being imported, not only by being discovered.
|
|
1062
|
+
* `findEntry` recognises policy files — ones that import failproofai and call
|
|
1063
|
+
* `customPolicies.add` — while `bundleEntry` inlines whatever those files pull
|
|
1064
|
+
* in, so a plain `./patterns.mjs` of shared matchers is in nobody's source list
|
|
1065
|
+
* and in every byte of the bundle. Ignored, it is the exact bug
|
|
1066
|
+
* {@link sourcesInHead} exists to close with a different file on the end of it:
|
|
1067
|
+
* `git status` reports a clean tree, the policy file itself is committed, and
|
|
1068
|
+
* the pack ships bytes HEAD does not contain under a version naming HEAD.
|
|
1069
|
+
*
|
|
1070
|
+
* Only files that EXIST are added, so a specifier this resolves differently
|
|
1071
|
+
* from the bundler adds nothing rather than inventing a path to refuse over.
|
|
1072
|
+
* Static and CommonJS spellings only — a computed `import(name)` is not
|
|
1073
|
+
* followed here, and is not bundled either.
|
|
1074
|
+
*/
|
|
1075
|
+
function withLocalImports(sources: string[]): string[] {
|
|
1076
|
+
const seen = new Set(sources.map((f) => resolve(f)));
|
|
1077
|
+
const queue = [...seen];
|
|
1078
|
+
// A pack is a handful of files; the cap is there so a cycle or a vendored
|
|
1079
|
+
// tree cannot turn a publish into a filesystem walk.
|
|
1080
|
+
while (queue.length > 0 && seen.size < 500) {
|
|
1081
|
+
const file = queue.shift() as string;
|
|
1082
|
+
let text: string;
|
|
1083
|
+
try {
|
|
1084
|
+
if (statSync(file).size > 512 * 1024) continue;
|
|
1085
|
+
text = readFileSync(file, "utf8");
|
|
1086
|
+
} catch {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
for (const re of LOCAL_IMPORT_RES) {
|
|
1090
|
+
for (const match of text.matchAll(re)) {
|
|
1091
|
+
const base = resolve(dirname(file), match[1]);
|
|
1092
|
+
for (const ext of LOCAL_IMPORT_EXTENSIONS) {
|
|
1093
|
+
const candidate = base + ext;
|
|
1094
|
+
if (seen.has(candidate)) break;
|
|
1095
|
+
try {
|
|
1096
|
+
if (!statSync(candidate).isFile()) continue;
|
|
1097
|
+
} catch {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
seen.add(candidate);
|
|
1101
|
+
queue.push(candidate);
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return [...seen];
|
|
1108
|
+
}
|
|
1109
|
+
|
|
856
1110
|
/**
|
|
857
1111
|
* Make the git claim TRUE instead of refusing it, where doing that is safe.
|
|
858
1112
|
*
|
|
@@ -984,7 +1238,7 @@ function settleGitState(
|
|
|
984
1238
|
"Commit by hand, or name the version yourself: --version <version>.",
|
|
985
1239
|
] };
|
|
986
1240
|
}
|
|
987
|
-
const made = inferCommit(entryPath, outDir);
|
|
1241
|
+
const made = inferCommit(entryPath, outDir, packFiles);
|
|
988
1242
|
if (!made) return null;
|
|
989
1243
|
const files = `${staged} file${staged === 1 ? "" : "s"}`;
|
|
990
1244
|
return {
|
|
@@ -1025,25 +1279,45 @@ function settleGitState(
|
|
|
1025
1279
|
// AT the root is reported individually, which is why every fixture that
|
|
1026
1280
|
// publishes from the root missed it. The exclusion still applies, so the
|
|
1027
1281
|
// expansion does not drag the build output back into the read.
|
|
1282
|
+
// `-z`, not line splitting.
|
|
1283
|
+
//
|
|
1284
|
+
// The line parser this replaces was wrong twice over on paths git is entitled
|
|
1285
|
+
// to hand back. Git C-QUOTES a path containing a newline or a quote, so the
|
|
1286
|
+
// value parsed out was escape sequences resolving to no file on disk — and a
|
|
1287
|
+
// legal policy file named `guards -> final.mjs` was truncated to `final.mjs`
|
|
1288
|
+
// by the rename split. Either way the path matched none of the pack's files,
|
|
1289
|
+
// landed in `foreign`, and publish refused to commit its OWN source over a
|
|
1290
|
+
// filename it had mangled itself.
|
|
1291
|
+
//
|
|
1292
|
+
// NUL separation has neither problem: no quoting is applied, and a rename
|
|
1293
|
+
// arrives as two separate entries rather than one arrow-joined line — so the
|
|
1294
|
+
// old path is listed in its own right, which is correct here, since a rename
|
|
1295
|
+
// leaves the tree differing from HEAD at both ends.
|
|
1028
1296
|
const status = raw([
|
|
1029
1297
|
"status",
|
|
1030
1298
|
"--porcelain",
|
|
1299
|
+
"-z",
|
|
1031
1300
|
"--untracked-files=all",
|
|
1032
1301
|
...skipOutDir(git, entryPath, outDir),
|
|
1033
1302
|
]);
|
|
1034
1303
|
if (root === null || status === null) return null;
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1304
|
+
const entries = porcelainEntries(status);
|
|
1305
|
+
// The path that EXISTS is the one to stage and the one to judge. A rename's
|
|
1306
|
+
// original is staged too — otherwise the deletion half is left uncommitted
|
|
1307
|
+
// and the tree is still dirty afterwards — but it is NOT judged foreign on
|
|
1308
|
+
// its own: renaming one of the pack's own files is the pack's business, and
|
|
1309
|
+
// treating the vacated name as somebody else's file made publish refuse a
|
|
1310
|
+
// policy file its author had merely renamed.
|
|
1311
|
+
const dirty = entries.map((entry) => resolve(root, entry.path));
|
|
1312
|
+
const renamedFrom = entries
|
|
1313
|
+
.filter((entry) => entry.from)
|
|
1314
|
+
.map((entry) => resolve(root, entry.from as string));
|
|
1043
1315
|
if (dirty.length === 0) return null;
|
|
1044
1316
|
|
|
1045
1317
|
const mine = new Set(packFiles.map((f) => resolve(f)));
|
|
1046
1318
|
const foreign = dirty.filter((path) => !mine.has(path));
|
|
1319
|
+
// Staged alongside, so a rename lands whole.
|
|
1320
|
+
const toStage = [...dirty, ...renamedFrom];
|
|
1047
1321
|
if (foreign.length > 0) {
|
|
1048
1322
|
return { error: [
|
|
1049
1323
|
`The version names a commit, and this tree has uncommitted changes outside the policy files.`,
|
|
@@ -1056,14 +1330,14 @@ function settleGitState(
|
|
|
1056
1330
|
|
|
1057
1331
|
const problem = identity();
|
|
1058
1332
|
if (problem) return { error: problem };
|
|
1059
|
-
git(["add", "--", ...
|
|
1333
|
+
git(["add", "--", ...toStage]);
|
|
1060
1334
|
if (git(["commit", "-q", "-m", `publish ${id}`]) === null) {
|
|
1061
1335
|
return { error: [
|
|
1062
1336
|
"Could not commit the policy files.",
|
|
1063
1337
|
"Commit by hand, or name the version yourself: --version <version>.",
|
|
1064
1338
|
] };
|
|
1065
1339
|
}
|
|
1066
|
-
const made = inferCommit(entryPath, outDir);
|
|
1340
|
+
const made = inferCommit(entryPath, outDir, packFiles);
|
|
1067
1341
|
if (!made || made.dirty) return null;
|
|
1068
1342
|
return {
|
|
1069
1343
|
provenance: made,
|
|
@@ -1122,7 +1396,7 @@ export function versionFromCommit(sha: string): string {
|
|
|
1122
1396
|
* `--version` overrides all of it, and every refusal names it for that reason.
|
|
1123
1397
|
*/
|
|
1124
1398
|
export function versionForPublish(
|
|
1125
|
-
provenance:
|
|
1399
|
+
provenance: Provenance | null,
|
|
1126
1400
|
): { version: string } | { error: string[] } {
|
|
1127
1401
|
if (!provenance) {
|
|
1128
1402
|
return {
|
|
@@ -1133,6 +1407,28 @@ export function versionForPublish(
|
|
|
1133
1407
|
],
|
|
1134
1408
|
};
|
|
1135
1409
|
}
|
|
1410
|
+
// A source that is not in HEAD at all — ignored by `.gitignore`, or never
|
|
1411
|
+
// committed — gets its own refusal. The generic "this tree has uncommitted
|
|
1412
|
+
// changes" is wrong for it and its remedy (`git add -A && git commit`) does
|
|
1413
|
+
// not work: an ignored file stays ignored, so the advice would loop.
|
|
1414
|
+
if (provenance.unpublishable) {
|
|
1415
|
+
const { offending, why } = provenance.unpublishable;
|
|
1416
|
+
return {
|
|
1417
|
+
error: [
|
|
1418
|
+
why === "missing"
|
|
1419
|
+
? "The version names a commit, and these policy files are not in it — ignored, or never committed:"
|
|
1420
|
+
: "The version names a commit, and these policy files differ from it:",
|
|
1421
|
+
...offending.slice(0, 8).map((path) => ` ${path}`),
|
|
1422
|
+
...(offending.length > 8 ? [` …and ${offending.length - 8} more`] : []),
|
|
1423
|
+
why === "missing"
|
|
1424
|
+
? "They would be bundled into the artifact while the commit it names does not contain them."
|
|
1425
|
+
: "They would be bundled as they are now, not as that commit holds them.",
|
|
1426
|
+
why === "missing"
|
|
1427
|
+
? "Commit them — `git add -f` if they are ignored — or name the version yourself: --version <version>."
|
|
1428
|
+
: "Commit them, or name the version yourself: --version <version>.",
|
|
1429
|
+
],
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1136
1432
|
if (provenance.dirty) {
|
|
1137
1433
|
return {
|
|
1138
1434
|
error: [
|
|
@@ -1548,11 +1844,15 @@ function ghError(res: { status: number; json: Record<string, unknown> | null; te
|
|
|
1548
1844
|
* and stop. That is what `pack build` now resolves to, so the old command keeps
|
|
1549
1845
|
* working and means the same thing.
|
|
1550
1846
|
*
|
|
1551
|
-
* Two failures the manual flow let through silently, both refused
|
|
1847
|
+
* Two failures the manual flow let through silently, both refused BEFORE
|
|
1848
|
+
* anything is created or uploaded:
|
|
1552
1849
|
* - a tag that does not describe the manifest version, which installs and then
|
|
1553
1850
|
* reports a version matching no URL;
|
|
1554
|
-
* -
|
|
1555
|
-
* anonymous HTTPS with no credential to offer.
|
|
1851
|
+
* - an existing private repository, which publishes to nobody, because
|
|
1852
|
+
* installs are anonymous HTTPS with no credential to offer. `--allow-private`
|
|
1853
|
+
* is for an author who genuinely wants that and will distribute the artifact
|
|
1854
|
+
* some other way; a repository publish CREATES is public by construction, so
|
|
1855
|
+
* the refusal only ever fires on reuse.
|
|
1556
1856
|
*/
|
|
1557
1857
|
/**
|
|
1558
1858
|
* The two things `publish` cannot work out on its own, asked instead of
|
|
@@ -1656,11 +1956,24 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
1656
1956
|
// A tag on HEAD is somebody SAYING what this release is, so it wins over the
|
|
1657
1957
|
// sha, which only reports one. Both are properties of the tree, so neither
|
|
1658
1958
|
// waits on the repository being known — see the block below the usage text.
|
|
1659
|
-
|
|
1959
|
+
// The set that will actually be BUNDLED — every discovered policy file, not
|
|
1960
|
+
// just the entry, and everything those files IMPORT, which the bundler inlines
|
|
1961
|
+
// and discovery never names. Both the tag path and the sha path are handed it,
|
|
1962
|
+
// so neither can accept a version for bytes the commit does not hold.
|
|
1963
|
+
const bundledSources = withLocalImports(
|
|
1964
|
+
discovered.length > 0 ? discovered : entry ? [entry] : [],
|
|
1965
|
+
);
|
|
1966
|
+
let version =
|
|
1967
|
+
flag("version") ?? (entry ? inferTaggedVersion(entry, bundledSources) : null) ?? undefined;
|
|
1660
1968
|
let versionFromSha = false;
|
|
1661
1969
|
// The id and the repo are usually the same words, and requiring both is asking
|
|
1662
1970
|
// the same question twice. Either one alone answers for the other.
|
|
1663
1971
|
const dryRun = rest.includes("--dry-run") || !repo;
|
|
1972
|
+
// The way past the private-repository refusal below, for somebody who really
|
|
1973
|
+
// does want a pack nobody can `policies add` and will distribute the assets
|
|
1974
|
+
// another way. It buys a publish, never a working install, so the success
|
|
1975
|
+
// message still says so.
|
|
1976
|
+
const allowPrivate = rest.includes("--allow-private");
|
|
1664
1977
|
// A dry run publishes nothing, so the id only has to name the artifact it
|
|
1665
1978
|
// writes — and refusing to build one because the folder has no git remote
|
|
1666
1979
|
// yet blocked the exact thing a dry run is for: looking at the pack BEFORE
|
|
@@ -1687,6 +2000,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
1687
2000
|
"Outside one, name them:",
|
|
1688
2001
|
" [--id <publisher/name>] [--tag <tag>] [--notes <text>]",
|
|
1689
2002
|
" [--out <dir>] [--effect enforce|observe] [--dry-run]",
|
|
2003
|
+
" [--allow-private] (a private repo installs for nobody; this says so)",
|
|
1690
2004
|
"",
|
|
1691
2005
|
"With no --repo it writes the three release assets and stops.",
|
|
1692
2006
|
]);
|
|
@@ -1703,7 +2017,7 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
1703
2017
|
// it is where the LAST run's assets are sitting, and inside the checkout by
|
|
1704
2018
|
// default. See `skipOutDir`.
|
|
1705
2019
|
const outDirEarly = resolve(flag("out") ?? "dist-pack");
|
|
1706
|
-
let provenance = entry ? inferCommit(entry, outDirEarly) : null;
|
|
2020
|
+
let provenance = entry ? inferCommit(entry, outDirEarly, bundledSources) : null;
|
|
1707
2021
|
const gitLines: string[] = [];
|
|
1708
2022
|
if (!version && entry && interactive) {
|
|
1709
2023
|
// Carry the git work rather than handing it back. TTY only: a commit made
|
|
@@ -1762,6 +2076,27 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
1762
2076
|
if ("error" in ensured) return fail(ensured.error);
|
|
1763
2077
|
created = ensured.created;
|
|
1764
2078
|
repoInfo = ensured.info;
|
|
2079
|
+
// A private repository is not a smaller audience, it is no audience:
|
|
2080
|
+
// `fetchBytes` in pack-store.ts sends no Authorization header at all, by
|
|
2081
|
+
// design, so every install 404s. This used to upload all three assets, exit
|
|
2082
|
+
// 0, print `policies add <repo>` and merely append a warning underneath —
|
|
2083
|
+
// reporting success for a release nobody could install and leaving assets
|
|
2084
|
+
// behind that advertise that dead route. Refuse HERE, before the release
|
|
2085
|
+
// exists and before anything is built, so there is nothing to clean up.
|
|
2086
|
+
// A repository this command creates is made public, so this only ever
|
|
2087
|
+
// fires on reuse of one that was already private.
|
|
2088
|
+
if (repoInfo.json?.private === true && !allowPrivate) {
|
|
2089
|
+
return fail([
|
|
2090
|
+
`${repo} is PRIVATE, so nothing was published.`,
|
|
2091
|
+
"Installing a pack fetches its release assets over anonymous HTTPS with no",
|
|
2092
|
+
"credential to offer, so every install would 404 — including your own from",
|
|
2093
|
+
"another machine. Publishing would only have attached three assets",
|
|
2094
|
+
"advertising a route nobody can take.",
|
|
2095
|
+
`Make it public and re-run: gh repo edit ${repo} --visibility public`,
|
|
2096
|
+
"Or, to keep it private and hand the pack over some other way, publish it",
|
|
2097
|
+
"with --allow-private.",
|
|
2098
|
+
]);
|
|
2099
|
+
}
|
|
1765
2100
|
}
|
|
1766
2101
|
const tag = flag("tag") ?? version;
|
|
1767
2102
|
|
|
@@ -1838,8 +2173,9 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
1838
2173
|
}
|
|
1839
2174
|
|
|
1840
2175
|
|
|
1841
|
-
//
|
|
1842
|
-
//
|
|
2176
|
+
// Only ever true when --allow-private was passed — the refusal above returns
|
|
2177
|
+
// otherwise — so this is the warning that goes with a publish the author
|
|
2178
|
+
// asked for anyway, not a discovery made too late to act on.
|
|
1843
2179
|
const isPrivate = repoInfo?.json?.private === true;
|
|
1844
2180
|
|
|
1845
2181
|
// Reuse a release on this tag rather than failing on it. Re-publishing a
|
|
@@ -1924,18 +2260,24 @@ async function publish(rest: string[]): Promise<PackCliResult> {
|
|
|
1924
2260
|
(versionFromSha ? " That names the commit it was built from." : ""),
|
|
1925
2261
|
` ${assets.length} assets attached`,
|
|
1926
2262
|
"",
|
|
1927
|
-
|
|
1928
|
-
`
|
|
1929
|
-
|
|
2263
|
+
// The install lines are the whole point of the success message, and on a
|
|
2264
|
+
// private repository they are a lie — `policies add` 404s there. Print the
|
|
2265
|
+
// warning INSTEAD of them, never underneath them: a reader who copies the
|
|
2266
|
+
// first command they see must not be copying one that cannot work.
|
|
2267
|
+
...(isPrivate
|
|
2268
|
+
? [
|
|
2269
|
+
`WARNING: ${repo} is PRIVATE, so nobody can install this — you asked for`,
|
|
2270
|
+
"that with --allow-private. Installs are anonymous HTTPS with no credential",
|
|
2271
|
+
"to offer, so every `failproofai policies add` will 404. Distribute the",
|
|
2272
|
+
"three assets yourself, or make it public:",
|
|
2273
|
+
` gh repo edit ${repo} --visibility public`,
|
|
2274
|
+
]
|
|
2275
|
+
: [
|
|
2276
|
+
"Anyone can now install it:",
|
|
2277
|
+
` failproofai policies add ${repo}`,
|
|
2278
|
+
` failproofai policies show ${repo} (look first, without running it)`,
|
|
2279
|
+
]),
|
|
1930
2280
|
];
|
|
1931
|
-
if (isPrivate) {
|
|
1932
|
-
lines.push(
|
|
1933
|
-
"",
|
|
1934
|
-
`WARNING: ${repo} is PRIVATE, so nobody can install this.`,
|
|
1935
|
-
"Installs are anonymous HTTPS with no credential to offer — every one will 404.",
|
|
1936
|
-
`Make it public: gh repo edit ${repo} --visibility public`,
|
|
1937
|
-
);
|
|
1938
|
-
}
|
|
1939
2281
|
return ok(lines);
|
|
1940
2282
|
}
|
|
1941
2283
|
|
|
@@ -2102,7 +2444,18 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
2102
2444
|
const result = await addPack(resolvedSource!, selection);
|
|
2103
2445
|
const lines = [
|
|
2104
2446
|
...(result.replaced?.length
|
|
2105
|
-
? [
|
|
2447
|
+
? [
|
|
2448
|
+
// What was OBSERVED, and then the inference — not the inference
|
|
2449
|
+
// stated as fact. Identical artifact bytes are the only evidence
|
|
2450
|
+
// there is, and they cannot tell a renamed pack from a second pack
|
|
2451
|
+
// one repository builds from the same source. Absorbing is the
|
|
2452
|
+
// right default (a publisher renaming a set must not reset everyone
|
|
2453
|
+
// who narrowed it), but the record that goes away is one the user
|
|
2454
|
+
// installed on purpose, so the line has to say what to do when the
|
|
2455
|
+
// guess was wrong instead of asserting it was right.
|
|
2456
|
+
`Replaced ${result.replaced.join(", ")} — same artifact, so it was taken as this pack renamed, and your selection was carried over.`,
|
|
2457
|
+
`If ${result.replaced.length > 1 ? "those are separate packs" : "that is a separate pack"}, re-add ${result.replaced.join(", ")} to restore ${result.replaced.length > 1 ? "them" : "it"}.`,
|
|
2458
|
+
]
|
|
2106
2459
|
: []),
|
|
2107
2460
|
result.resolvedFromLatest
|
|
2108
2461
|
? `Installed ${result.id}@${result.version} from ${result.source} (newest release; pinned to ${result.tag})`
|
|
@@ -2155,9 +2508,29 @@ async function add(rest: string[]): Promise<PackCliResult> {
|
|
|
2155
2508
|
function remove(rest: string[]): PackCliResult {
|
|
2156
2509
|
const id = rest[0];
|
|
2157
2510
|
if (!id) return fail(["Usage: failproofai policies remove <publisher/name>"]);
|
|
2158
|
-
|
|
2511
|
+
// Reported with the id the MACHINE holds, not the spelling that was typed:
|
|
2512
|
+
// `remove FAILPROOFAI/POLICIES` succeeding and echoing that back teaches a
|
|
2513
|
+
// name nothing else in the product uses.
|
|
2514
|
+
let removedId: string | null;
|
|
2515
|
+
try {
|
|
2516
|
+
removedId = removePack(id);
|
|
2517
|
+
} catch (err) {
|
|
2518
|
+
// Two installed packs answer to one loosely-typed name. Refused rather
|
|
2519
|
+
// than guessed at — the loose match exists so a remembered name works, and
|
|
2520
|
+
// uninstalling something the user did not name is a worse outcome than
|
|
2521
|
+
// asking them to be exact.
|
|
2522
|
+
if (err instanceof AmbiguousPackId) {
|
|
2523
|
+
return fail([
|
|
2524
|
+
`${id} matches ${err.candidates.length} installed packs:`,
|
|
2525
|
+
...err.candidates.map((candidate) => ` ${candidate}`),
|
|
2526
|
+
"Name one of them exactly.",
|
|
2527
|
+
]);
|
|
2528
|
+
}
|
|
2529
|
+
throw err;
|
|
2530
|
+
}
|
|
2531
|
+
if (!removedId) return fail([`No installed pack with id ${id}`]);
|
|
2159
2532
|
return ok([
|
|
2160
|
-
`Removed ${
|
|
2533
|
+
`Removed ${removedId}. Its policies stop enforcing now; re-add it any time with the same command.`,
|
|
2161
2534
|
]);
|
|
2162
2535
|
}
|
|
2163
2536
|
|