Compare commits

..
Author SHA1 Message Date
Paul Bakaus ca40a4900f Test gitignore paths independently
Prepared with AI assistance by OpenAI Codex.
2026-09-21 13:49:57 -07:00
Paul Bakaus 107de24e64 Fix nested Impeccable gitignore patterns
Prepared with AI assistance by OpenAI Codex.
2026-09-21 13:42:11 -07:00
17 changed files with 50 additions and 435 deletions
+19 -20
View File
@@ -358,31 +358,30 @@ As you run commands, Impeccable writes working files under `.impeccable/`: criti
```gitignore ```gitignore
# impeccable-ignore-start # impeccable-ignore-start
# Ephemeral output, runtime state, and per-dev overrides. # Ephemeral output, runtime state, and per-dev overrides.
# Unanchored: .impeccable may sit at the repo root or under a nested # The **/ prefix covers .impeccable at the repo root or in a nested workspace.
# workspace (apps/web/.impeccable/...); anchored patterns would miss it.
# Shared artifacts stay tracked: config.json, live/config.json, # Shared artifacts stay tracked: config.json, live/config.json,
# design.json, surfaces/*.md, critique/*.md. # design.json, surfaces/*.md, critique/*.md.
.impeccable/config.local.json **/.impeccable/config.local.json
.impeccable/hook.cache.json **/.impeccable/hook.cache.json
.impeccable/hook.pending.json **/.impeccable/hook.pending.json
.impeccable/*.png **/.impeccable/*.png
.impeccable/review/ **/.impeccable/review/
.impeccable/questions/ **/.impeccable/questions/
.impeccable/live/server.json **/.impeccable/live/server.json
.impeccable/live/sessions/ **/.impeccable/live/sessions/
.impeccable/live/previews/ **/.impeccable/live/previews/
.impeccable/live/annotations/ **/.impeccable/live/annotations/
.impeccable/live/cache/ **/.impeccable/live/cache/
.impeccable/live/manual-edit-apply-transaction.json **/.impeccable/live/manual-edit-apply-transaction.json
.impeccable/live/manual-edit-events.jsonl **/.impeccable/live/manual-edit-events.jsonl
.impeccable/live/manual-edit-evidence/ **/.impeccable/live/manual-edit-evidence/
.impeccable/live/pending-manual-edits.json **/.impeccable/live/pending-manual-edits.json
.impeccable/live/deferred-svelte-component-accepts.json **/.impeccable/live/deferred-svelte-component-accepts.json
.impeccable/live/*.png **/.impeccable/live/*.png
# impeccable-ignore-end # impeccable-ignore-end
``` ```
The block is wrapped in `# impeccable-ignore-start` / `# impeccable-ignore-end` markers so you can recognize and refresh it later. Patterns are unanchored on purpose: in a monorepo the active project (and its `.impeccable/` directory) often lives under a nested workspace path like `apps/web/`, and a root-anchored pattern would miss it. The block is wrapped in `# impeccable-ignore-start` / `# impeccable-ignore-end` markers so you can recognize and refresh it later. The `**/` prefix makes each pattern match whether the active project's `.impeccable/` directory is at the repository root or under a nested workspace path like `apps/web/`.
**Keep these tracked** (they are shared project artifacts, do not add them to `.gitignore`): **Keep these tracked** (they are shared project artifacts, do not add them to `.gitignore`):
+1 -1
View File
@@ -52,7 +52,7 @@ fn run(args: &[String], io: &mut Io) -> i32 {
"detect" => impeccable_detect::run_detect(rest, io, &engines()), "detect" => impeccable_detect::run_detect(rest, io, &engines()),
"ignores" | "ignore" => impeccable_detect::run_ignores(rest, io), "ignores" | "ignore" => impeccable_detect::run_ignores(rest, io),
"skills" => impeccable_skills::run(rest, io), "skills" => impeccable_skills::run(rest, io),
"help" | "install" | "link" | "update" | "check" | "verify-bundle" => impeccable_skills::run(args, io), "help" | "install" | "link" | "update" | "check" => impeccable_skills::run(args, io),
// skill scripts // skill scripts
"context" => impeccable_context::run_context(rest, io), "context" => impeccable_context::run_context(rest, io),
"pin" => impeccable_context::run_pin(rest, io), "pin" => impeccable_context::run_pin(rest, io),
-1
View File
@@ -67,7 +67,6 @@ Commands:
link Symlink skills from a local checkout or submodule link Symlink skills from a local checkout or submodule
update Update skills to the latest version update Update skills to the latest version
check Check if skill updates are available check Check if skill updates are available
verify-bundle Verify a local skill bundle against the pinned signing keys
Options: Options:
--help Show this help message --help Show this help message
+9 -9
View File
@@ -31,15 +31,15 @@ pub(crate) fn release_version(location: &str) -> Result<String, String> {
}) })
} }
#[derive(Debug, Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)] #[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(crate) struct Envelope { struct Envelope {
schema: u32, schema: u32,
pub(crate) key_id: String, key_id: String,
pub(crate) version: String, version: String,
pub(crate) artifact: String, artifact: String,
pub(crate) size: u64, size: u64,
pub(crate) sha256: String, sha256: String,
signature: String, signature: String,
} }
@@ -62,7 +62,7 @@ pub(crate) fn verify_reader(
signature: &[u8], signature: &[u8],
version: &str, version: &str,
keys: &TrustedKeys, keys: &TrustedKeys,
) -> Result<Envelope, String> { ) -> Result<(), String> {
if signature.len() as u64 > MAX_SIGNATURE_BYTES { if signature.len() as u64 > MAX_SIGNATURE_BYTES {
return Err("Bundle signature is too large".into()); return Err("Bundle signature is too large".into());
} }
@@ -108,7 +108,7 @@ pub(crate) fn verify_reader(
if size != envelope.size || format!("{:x}", hash.finalize()) != envelope.sha256 { if size != envelope.size || format!("{:x}", hash.finalize()) != envelope.sha256 {
return Err("Bundle digest or size does not match its signature".into()); return Err("Bundle digest or size does not match its signature".into());
} }
Ok(envelope) Ok(())
} }
#[cfg(test)] #[cfg(test)]
-1
View File
@@ -55,7 +55,6 @@ pub fn run(args: &[String], io: &mut Io) -> R<()> {
"link" => link(&rest, io), "link" => link(&rest, io),
"update" => update(&rest, io), "update" => update(&rest, io),
"check" => check(io), "check" => check(io),
"verify-bundle" => crate::verify_bundle::run(&rest, io),
other => { other => {
io.err(&format!("Unknown skills command: {other}\n")); io.err(&format!("Unknown skills command: {other}\n"));
io.err("Run 'impeccable --help' for available commands.\n"); io.err("Run 'impeccable --help' for available commands.\n");
-1
View File
@@ -34,7 +34,6 @@ pub mod hook_manifest;
pub mod prompt; pub mod prompt;
pub mod providers; pub mod providers;
pub mod util; pub mod util;
mod verify_bundle;
use impeccable_common::Io; use impeccable_common::Io;
-230
View File
@@ -1,230 +0,0 @@
//! Offline verification of local release assets using the installer's trust roots.
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use impeccable_common::Io;
use crate::bundle_signature::{self, Envelope, TrustedKeys, MAX_SIGNATURE_BYTES};
use crate::{Flow, R};
const USAGE: &str = "Usage: impeccable verify-bundle <zip> --version <expected-version> [options]
Verify a local skill bundle's signature and SHA-256 using the pinned signing keys.
Runs offline without extracting files, installing skills, or enabling hooks.
Options:
--version <version> Required expected skill version (for example, 4.3.1)
--signature <path> Signature manifest (default: <zip>.sig.json)
--json Print verified metadata as JSON
-h, --help Show this help message
Exit codes: 0 verified, 1 verification or file error, 2 invalid arguments.
";
struct Options {
bundle: PathBuf,
signature: PathBuf,
version: String,
json: bool,
}
fn parse(args: &[String]) -> Result<Options, String> {
let (mut bundle, mut signature, mut version) = (None, None, None);
let mut json = false;
let mut positional = false;
let mut args = args.iter();
while let Some(arg) = args.next() {
if !positional && arg == "--" {
positional = true;
} else if !positional && arg == "--json" {
json = true;
} else if !positional
&& (arg == "--version"
|| arg == "--signature"
|| arg.starts_with("--version=")
|| arg.starts_with("--signature="))
{
let (name, value) = match arg.split_once('=') {
Some(pair) => pair,
None => (arg.as_str(), args.next().map(String::as_str).unwrap_or("")),
};
if value.is_empty() || value.starts_with('-') {
return Err(format!("{name} requires a value"));
}
let slot = if name == "--version" {
&mut version
} else {
&mut signature
};
if slot.replace(value.to_string()).is_some() {
return Err(format!("{name} may only be specified once"));
}
} else if !positional && arg.starts_with('-') {
return Err(format!("Unknown option: {arg}"));
} else if bundle.replace(PathBuf::from(arg)).is_some() {
return Err("Expected exactly one local bundle path".into());
}
}
let bundle = bundle.ok_or("A local bundle path is required")?;
let version = version.ok_or("--version is required; specify the expected skill release")?;
// Reuse the installer's exact release-version grammar.
bundle_signature::release_version(&format!(
"https://github.com/pbakaus/impeccable/releases/download/skill-v{version}/universal.zip"
))?;
let signature = signature.map(PathBuf::from).unwrap_or_else(|| {
let mut path = bundle.as_os_str().to_os_string();
path.push(".sig.json");
PathBuf::from(path)
});
Ok(Options {
bundle,
signature,
version,
json,
})
}
fn verify(options: &Options, cwd: &Path, keys: &TrustedKeys) -> Result<Envelope, String> {
let read_error = |path: &Path, error| format!("Could not read {}: {error}", path.display());
let file =
File::open(cwd.join(&options.signature)).map_err(|e| read_error(&options.signature, e))?;
let mut signature = Vec::new();
file.take(MAX_SIGNATURE_BYTES + 1)
.read_to_end(&mut signature)
.map_err(|e| read_error(&options.signature, e))?;
let file = File::open(cwd.join(&options.bundle)).map_err(|e| read_error(&options.bundle, e))?;
bundle_signature::verify_reader(
&mut BufReader::new(file),
&signature,
&options.version,
keys,
)
}
pub(crate) fn run(args: &[String], io: &mut Io) -> R<()> {
if args
.iter()
.take_while(|a| a.as_str() != "--")
.any(|a| a == "--help" || a == "-h")
{
io.out(USAGE);
return Ok(());
}
let options = parse(args).map_err(|e| {
io.err(&format!("{e}\n\n{USAGE}"));
Flow::Exit(2)
})?;
let verified = bundle_signature::trusted_keys()
.and_then(|keys| verify(&options, &io.cwd, &keys))
.map_err(|e| Flow::Throw(format!("{}{e}", bundle_signature::ERROR_PREFIX)))?;
if options.json {
io.out(&format!(
"{}\n",
serde_json::json!({
"verified": true, "version": verified.version, "artifact": verified.artifact,
"keyId": verified.key_id, "size": verified.size, "sha256": verified.sha256,
})
));
} else {
io.out(&format!(
"Verified {} (skill-v{}).\nSigning key: {}\nSHA-256: {}\nSize: {} bytes\n",
verified.artifact, verified.version, verified.key_id, verified.sha256, verified.size
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verifies_local_pair_and_rejects_tampering_without_writes() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../../tests/fixtures/bundle-signature.json"
))
.unwrap();
let root = crate::util::mkdtemp(
&std::env::temp_dir()
.join("verify-bundle-")
.to_string_lossy(),
)
.unwrap();
let root = Path::new(&root);
let bundle = fixture["bundle"].as_str().unwrap().as_bytes();
let signature = serde_json::to_vec(&fixture["envelope"]).unwrap();
std::fs::write(root.join("renamed.zip"), bundle).unwrap();
std::fs::write(root.join("manifest.json"), &signature).unwrap();
let options = Options {
bundle: "renamed.zip".into(),
signature: "manifest.json".into(),
version: "4.2.0".into(),
json: true,
};
let keys = serde_json::from_value(fixture["keys"].clone()).unwrap();
let result = verify(&options, root, &keys).unwrap();
assert_eq!(result.version, "4.2.0");
assert_eq!(result.sha256, fixture["envelope"]["sha256"]);
assert!(verify(&options, root, &bundle_signature::trusted_keys().unwrap()).is_err());
let wrong_release = Options {
version: "4.2.1".into(),
..options
};
assert!(verify(&wrong_release, root, &keys).is_err());
let options = Options {
version: "4.2.0".into(),
..wrong_release
};
std::fs::write(root.join("renamed.zip"), b"tampered").unwrap();
assert!(verify(&options, root, &keys).is_err());
std::fs::write(root.join("renamed.zip"), bundle).unwrap();
std::fs::write(
root.join("manifest.json"),
vec![b' '; MAX_SIGNATURE_BYTES as usize + 1],
)
.unwrap();
assert!(verify(&options, root, &keys)
.unwrap_err()
.contains("too large"));
std::fs::remove_file(root.join("manifest.json")).unwrap();
assert!(verify(&options, root, &keys)
.unwrap_err()
.contains("Could not read"));
assert_eq!(std::fs::read(root.join("renamed.zip")).unwrap(), bundle);
assert_eq!(std::fs::read_dir(root).unwrap().count(), 1);
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn arguments_are_strict_and_default_to_adjacent_signature() {
let parse_args =
|args: &[&str]| parse(&args.iter().map(|a| a.to_string()).collect::<Vec<_>>());
let options = parse_args(&["bundle.zip", "--version=4.3.1", "--json"]).unwrap();
assert_eq!(options.signature, PathBuf::from("bundle.zip.sig.json"));
assert!(options.json);
let options = parse_args(&[
"--version",
"4.3.1",
"--signature",
"sig.json",
"--",
"-bundle.zip",
])
.unwrap();
assert_eq!(options.bundle, PathBuf::from("-bundle.zip"));
assert_eq!(options.signature, PathBuf::from("sig.json"));
for args in [
vec![],
vec!["bundle.zip"],
vec!["bundle.zip", "--version"],
vec!["bundle.zip", "--version=04.3.1"],
vec!["bundle.zip", "--version=4.3.1", "--version=4.2.0"],
vec!["bundle.zip", "--version=4.3.1", "extra.zip"],
vec!["bundle.zip", "--version=4.3.1", "--skip-signature"],
vec!["bundle.zip", "--version=4.3.1", "--signature="],
] {
assert!(parse_args(&args).is_err(), "{args:?}");
}
}
}
@@ -1,73 +0,0 @@
use impeccable_common::Io;
#[test]
fn verify_bundle_help_is_offline() {
let (mut io, capture) = Io::captured("", std::env::temp_dir(), Default::default());
let code = impeccable_skills::run(&["verify-bundle".into(), "--help".into()], &mut io);
assert_eq!(code, 0);
assert!(String::from_utf8(capture.stdout.borrow().clone())
.unwrap()
.contains("--version"));
assert!(String::from_utf8(capture.stdout.borrow().clone())
.unwrap()
.contains("--signature"));
assert!(String::from_utf8(capture.stderr.borrow().clone())
.unwrap()
.is_empty());
}
#[test]
fn verify_bundle_requires_an_independent_expected_version() {
let (mut io, capture) = Io::captured("", std::env::temp_dir(), Default::default());
let code = impeccable_skills::run(&["verify-bundle".into(), "universal.zip".into()], &mut io);
assert_eq!(code, 2);
assert!(String::from_utf8(capture.stderr.borrow().clone())
.unwrap()
.contains("--version"));
assert!(String::from_utf8(capture.stdout.borrow().clone())
.unwrap()
.is_empty());
}
#[test]
fn local_override_cannot_bypass_signature_verification_in_json_mode() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../../../tests/fixtures/bundle-signature.json"
))
.unwrap();
let root = impeccable_skills::util::mkdtemp(
&std::env::temp_dir()
.join("verify-command-")
.to_string_lossy(),
)
.unwrap();
let root = std::path::PathBuf::from(root);
std::fs::write(root.join("bundle.zip"), fixture["bundle"].as_str().unwrap()).unwrap();
std::fs::write(
root.join("bundle.zip.sig.json"),
serde_json::to_vec(&fixture["envelope"]).unwrap(),
)
.unwrap();
let env = [(
"IMPECCABLE_BUNDLE_PATH".into(),
root.to_string_lossy().into_owned(),
)]
.into();
let (mut io, capture) = Io::captured("", root.clone(), env);
let code = impeccable_skills::run(
&[
"verify-bundle".into(),
"bundle.zip".into(),
"--version=4.2.0".into(),
"--json".into(),
],
&mut io,
);
assert_eq!(code, 1);
assert!(capture.stdout.borrow().is_empty());
assert!(String::from_utf8(capture.stderr.borrow().clone())
.unwrap()
.contains("Unknown bundle signing key"));
assert_eq!(std::fs::read_dir(&root).unwrap().count(), 2);
std::fs::remove_dir_all(root).unwrap();
}
+4 -41
View File
@@ -13,45 +13,6 @@ HTTPS. Missing signatures, unknown keys, changed metadata, and changed ZIP
bytes stop the operation before extraction or writes to installed skills. bytes stop the operation before extraction or writes to installed skills.
The temporary download directory is removed on failure. The temporary download directory is removed on failure.
## Verify a downloaded release offline
Use an approved engine that includes `verify-bundle`, and obtain the ZIP and
`universal.zip.sig.json` from the same versioned skill release. Then run:
```sh
impeccable verify-bundle /path/to/universal.zip --version 4.3.1
```
The expected version is required and must be the skill version, not the CLI
or engine version. The signature defaults to `<zip>.sig.json`; use
`--signature /path/to/manifest.json` when stored separately. Both
`--version=4.3.1` and `--version 4.3.1` are accepted.
For an audit record:
```sh
impeccable verify-bundle /path/to/universal.zip --version 4.3.1 --json
```
Successful JSON contains `verified: true`, `version`, `artifact`, `keyId`,
`size`, and `sha256`. Only authenticated metadata is printed. Exit codes are
0 for successful verification, 1 for verification or file errors, and 2 for
invalid arguments. Errors go to stderr, including with `--json`; stdout is
empty on failure.
This command reads local files only. It does not download, extract, install,
or enable hooks, and it ignores local bundle overrides. It uses the same
compiled-in public keys and verifier as remote installation; there is no
custom-key or skip-verification option. Approve the engine and its keyring
through your organization's trust process first. Invoking through `npx` may
still download the npm package or engine; use an already provisioned native
binary for a fully offline workflow.
Verification authenticates the bytes and their declared release. It does not
inspect ZIP contents or prove that skill instructions are safe. Requiring an
expected version rejects a different release, but cannot tell you whether the
version you chose is the newest. Verify again if the files change before use.
## Sign a release ## Sign a release
Install the 1Password CLI and enable its desktop app integration. The signing Install the 1Password CLI and enable its desktop app integration. The signing
@@ -92,10 +53,12 @@ review the exact released `universal.zip`, then run:
node scripts/sign-bundle.mjs 4.2.0 /path/to/universal.zip node scripts/sign-bundle.mjs 4.2.0 /path/to/universal.zip
``` ```
Check the resulting sidecar against the verifier and compiled public key: Check the resulting sidecar against the Rust verifier and compiled public key:
```sh ```sh
impeccable verify-bundle /path/to/universal.zip --version 4.2.0 IMPECCABLE_VERIFY_BUNDLE=/path/to/universal.zip \
IMPECCABLE_VERIFY_BUNDLE_VERSION=4.2.0 \
cargo test -p impeccable-skills verifies_reviewed_release_with_production_keyring -- --ignored
``` ```
This creates only the local sidecar. It neither uploads it nor replaces the This creates only the local sidecar. It neither uploads it nor replaces the
-20
View File
@@ -1831,23 +1831,3 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
#### E2E harness contract (`tests/live-e2e.test.mjs`, `tests/live-e2e/*`) #### E2E harness contract (`tests/live-e2e.test.mjs`, `tests/live-e2e/*`)
- Fake agent polls `GET /poll?token&timeout=5000` (no lease override → 30 s lease), replies via `POST /poll` with `{token,type:'done',sourceEventType:'generate',id,file}`, `steer_done {message,file}`, `error`, accept/discard completions with `data:{carbonize:true,_acceptResult}`/`{_acceptResult}`, manual apply via `live-poll.mjs --reply <id> done --data <json>`. Variant format: 3 variants (font-weights 300/900/600 for render proof), params `lightness` (range), `face` (steps), `italic` (toggle). Scenarios: core, manual, annotations, exit, missed-done, params, mount-failure, republish, storage-loss (fixtures README). Fixture `runtime` block schema is authoritative for what a reimplementation must satisfy end-to-end. - Fake agent polls `GET /poll?token&timeout=5000` (no lease override → 30 s lease), replies via `POST /poll` with `{token,type:'done',sourceEventType:'generate',id,file}`, `steer_done {message,file}`, `error`, accept/discard completions with `data:{carbonize:true,_acceptResult}`/`{_acceptResult}`, manual apply via `live-poll.mjs --reply <id> done --data <json>`. Variant format: 3 variants (font-weights 300/900/600 for render proof), params `lightness` (range), `face` (steps), `italic` (toggle). Scenarios: core, manual, annotations, exit, missed-done, params, mount-failure, republish, storage-loss (fixtures README). Fixture `runtime` block schema is authoritative for what a reimplementation must satisfy end-to-end.
### `verify-bundle`: offline release verification
`impeccable verify-bundle <zip> --version <expected-version>` (also under
`impeccable skills`) authenticates local bytes with the remote installer's
compiled-in Ed25519 keyring before reporting success. No network, extraction,
installation, hooks, or writes occur. `IMPECCABLE_BUNDLE_PATH` does not bypass
verification. The expected skill version is required; no `v` or `skill-v`
prefix. The default signature path is `<zip>.sig.json`; `--signature <path>`
overrides it. Value options also accept `=`, and `--` ends option parsing.
Unknown options, duplicate value options, extra paths, and invalid versions
exit 2. `--help` / `-h` prints static help and exits 0.
Success exits 0 and prints the authenticated artifact, version, key ID,
SHA-256, and size. `--json` instead prints one object with `verified: true`,
`version`, `artifact`, `keyId`, `size`, and `sha256`. File/signature failures
exit 1 with `Could not verify skill bundle: ...` on stderr and empty stdout,
including in JSON mode. Signature reads are capped at 16 KiB plus one byte to
detect oversized input; bundle hashing streams through the shared verifier.
See [BUNDLE-SIGNING.md](BUNDLE-SIGNING.md) for trust scope and examples.
-7
View File
@@ -164,10 +164,3 @@ installed. The binary's `CLI_VERSION` moves from `3.6.0` to `4.0.0` with the
CLI 4.0.0 release; it is what the binary prints when run directly. CLI 4.0.0 release; it is what the binary prints when run directly.
- `cli-version`. - `cli-version`.
## Recorded 2026-09-18: offline skill bundle verification
- `cli-help`: adds the `verify-bundle` command to root help.
- `skills-verify-bundle-help`, `skills-verify-bundle-namespace-help`, and
`skills-verify-bundle-version-required`: new offline command help and
required expected-version behavior. Existing installer output is unchanged.
-3
View File
@@ -7,9 +7,6 @@
* the top-level verb and the legacy `skills` namespace. * the top-level verb and the legacy `skills` namespace.
*/ */
export default [ export default [
{ id: 'skills-verify-bundle-help', verb: 'verify-bundle', args: ['--help'] },
{ id: 'skills-verify-bundle-version-required', verb: 'verify-bundle', args: ['universal.zip'] },
{ id: 'skills-verify-bundle-namespace-help', verb: 'skills', args: ['verify-bundle', '--help'] },
{ id: 'skills-install-help', verb: 'install', args: ['--help'] }, { id: 'skills-install-help', verb: 'install', args: ['--help'] },
{ id: 'skills-install-help-short', verb: 'install', args: ['-h'] }, { id: 'skills-install-help-short', verb: 'install', args: ['-h'] },
{ id: 'skills-link-help', verb: 'link', args: ['--help'] }, { id: 'skills-link-help', verb: 'link', args: ['--help'] },
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"stdout": "Usage: impeccable <command> [options]\n\nCommands:\n detect [file-or-dir-or-url...] Scan for UI anti-patterns and design quality issues\n ignores Manage detector ignore rules, files, and values\n help List all available skills and commands\n install Install impeccable skills into your project or global harness\n link Symlink skills from a local checkout or submodule\n update Update skills to the latest version\n check Check if skill updates are available\n verify-bundle Verify a local skill bundle against the pinned signing keys\n\nOptions:\n --help Show this help message\n --version Show version number\n\nCompatibility:\n impeccable skills <command> Legacy namespace; still supported.\n", "stdout": "Usage: impeccable <command> [options]\n\nCommands:\n detect [file-or-dir-or-url...] Scan for UI anti-patterns and design quality issues\n ignores Manage detector ignore rules, files, and values\n help List all available skills and commands\n install Install impeccable skills into your project or global harness\n link Symlink skills from a local checkout or submodule\n update Update skills to the latest version\n check Check if skill updates are available\n\nOptions:\n --help Show this help message\n --version Show version number\n\nCompatibility:\n impeccable skills <command> Legacy namespace; still supported.\n",
"stderr": "", "stderr": "",
"exit": 0, "exit": 0,
"signal": null, "signal": null,
@@ -1,7 +0,0 @@
{
"stdout": "Usage: impeccable verify-bundle <zip> --version <expected-version> [options]\n\nVerify a local skill bundle's signature and SHA-256 using the pinned signing keys.\nRuns offline without extracting files, installing skills, or enabling hooks.\n\nOptions:\n --version <version> Required expected skill version (for example, 4.3.1)\n --signature <path> Signature manifest (default: <zip>.sig.json)\n --json Print verified metadata as JSON\n -h, --help Show this help message\n\nExit codes: 0 verified, 1 verification or file error, 2 invalid arguments.\n",
"stderr": "",
"exit": 0,
"signal": null,
"files": {}
}
@@ -1,7 +0,0 @@
{
"stdout": "Usage: impeccable verify-bundle <zip> --version <expected-version> [options]\n\nVerify a local skill bundle's signature and SHA-256 using the pinned signing keys.\nRuns offline without extracting files, installing skills, or enabling hooks.\n\nOptions:\n --version <version> Required expected skill version (for example, 4.3.1)\n --signature <path> Signature manifest (default: <zip>.sig.json)\n --json Print verified metadata as JSON\n -h, --help Show this help message\n\nExit codes: 0 verified, 1 verification or file error, 2 invalid arguments.\n",
"stderr": "",
"exit": 0,
"signal": null,
"files": {}
}
@@ -1,7 +0,0 @@
{
"stdout": "",
"stderr": "--version is required; specify the expected skill release\n\nUsage: impeccable verify-bundle <zip> --version <expected-version> [options]\n\nVerify a local skill bundle's signature and SHA-256 using the pinned signing keys.\nRuns offline without extracting files, installing skills, or enabling hooks.\n\nOptions:\n --version <version> Required expected skill version (for example, 4.3.1)\n --signature <path> Signature manifest (default: <zip>.sig.json)\n --json Print verified metadata as JSON\n -h, --help Show this help message\n\nExit codes: 0 verified, 1 verification or file error, 2 invalid arguments.\n",
"exit": 2,
"signal": null,
"files": {}
}
+16 -6
View File
@@ -26,15 +26,25 @@ describe('README gitignore block', () => {
writeFileSync(join(tmp, '.gitignore'), block); writeFileSync(join(tmp, '.gitignore'), block);
execFileSync('git', ['init'], { cwd: tmp }); execFileSync('git', ['init'], { cwd: tmp });
const ignored = execFileSync('git', [ for (const rel of [
'check-ignore',
'.impeccable/review/desktop.png', '.impeccable/review/desktop.png',
'.impeccable/questions/fb63f8a6.log', '.impeccable/questions/fb63f8a6.log',
], { cwd: tmp, encoding: 'utf-8' }); 'apps/web/.impeccable/review/desktop.png',
assert.match(ignored, /\.impeccable\/review\/desktop\.png/); 'apps/web/.impeccable/questions/fb63f8a6.log',
assert.match(ignored, /\.impeccable\/questions\/fb63f8a6\.log/); ]) {
const ignored = execFileSync('git', ['check-ignore', rel], {
cwd: tmp,
encoding: 'utf-8',
});
assert.equal(ignored.trim(), rel, `${rel} should be ignored independently`);
}
for (const rel of ['.impeccable/config.json', '.impeccable/critique/report.md']) { for (const rel of [
'.impeccable/config.json',
'.impeccable/critique/report.md',
'apps/web/.impeccable/config.json',
'apps/web/.impeccable/critique/report.md',
]) {
const result = spawnSync('git', ['check-ignore', rel], { cwd: tmp }); const result = spawnSync('git', ['check-ignore', rel], { cwd: tmp });
assert.notEqual(result.status, 0, `${rel} should not be ignored`); assert.notEqual(result.status, 0, `${rel} should not be ignored`);
} }