From cf836ac3219f5752519287642c1291ed5f68e424 Mon Sep 17 00:00:00 2001 From: aspizu Date: Wed, 7 Oct 2026 11:00:49 +0530 Subject: [PATCH 1/5] Expand AI detection and use uv for dependency checks --- .github/workflows/check.yml | 3 +- src/codegen.rs | 1 + src/codegen/ai_detection.rs | 124 +++++++++++++++++++++++++++++++++ src/codegen/build.rs | 45 +++++------- tools/check-min-publish-age.py | 2 +- 5 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 src/codegen/ai_detection.rs mode change 100644 => 100755 tools/check-min-publish-age.py diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 2af46e4d..48f1abf3 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -5,9 +5,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v10.0.1 # TODO: remove this step and the script when stable cargo supports # registry.global-min-publish-age (stabilized in cargo 1.100). - - run: python3 tools/check-min-publish-age.py + - run: ./tools/check-min-publish-age.py - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: "stable" diff --git a/src/codegen.rs b/src/codegen.rs index 5f9da23a..2b93d989 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -1,3 +1,4 @@ +mod ai_detection; pub mod assets; pub mod build; mod cleanup; diff --git a/src/codegen/ai_detection.rs b/src/codegen/ai_detection.rs new file mode 100644 index 00000000..4909a7b8 --- /dev/null +++ b/src/codegen/ai_detection.rs @@ -0,0 +1,124 @@ +// https://github.com/vercel/vercel/blob/main/packages/detect-agent/src/index.ts +// https://github.com/sdairs/is-ai-agent/blob/main/src/lib.rs +const AGENT_NONEMPTY_ENV_VARS: &[&str] = &[ + "AMP_CURRENT_THREAD_ID", + "ANTIGRAVITY_AGENT", + "ANTIGRAVITY_PROJECT_ID", + "AUGMENT_AGENT", + "CLAUDECODE", + "CLAUDE_CODE", + "CLAUDE_CODE_ENTRYPOINT", + "CLAUDE_CODE_EXECPATH", + "CLAUDE_CODE_IS_COWORK", + "CLAUDE_CODE_SESSION_ID", + "CLINE_ACTIVE", + "CLINE_TASK_ID", + "CODEBUDDY", + "CODEBUDDY_PROJECT_DIR", + "CODEBUDDY_SESSION_ID", + "CODEX_CI", + "CODEX_ROOT", + "CODEX_SANDBOX", + "CODEX_SANDBOX_NETWORK_DISABLED", + "CODEX_THREAD_ID", + "COPILOT_AGENT", + "COPILOT_AGENT_JOB_ID", + "COPILOT_AGENT_SESSION_ID", + "COPILOT_ALLOW_ALL", + "COPILOT_CLI", + "COPILOT_GITHUB_TOKEN", + "COPILOT_MODEL", + "CRUSH", + "CURSOR_AGENT", + "CURSOR_SANDBOX", + "CURSOR_TRACE_ID", + "FIREBENDER_TERMINAL", + "GEMINI_CLI", + "GOOSE_TERMINAL", + "IFLOW_CLI", + "KIRO_AGENT_PATH", + "OPENCODE", + "OPENCODE_APP_INFO", + "OPENCODE_BIN_PATH", + "OPENCODE_CLIENT", + "OPENCODE_MODES", + "OPENCODE_PID", + "OPENCODE_SERVER", + "OZ_RUN_ID", + "PI_CODING_AGENT", + "QWEN_CODE", + "REPL_ID", + "ROO_CODE_TASK_ID", + "TRAE_AI_SHELL_ID", + "VECLI_DIR", + "VECLI_SANDBOX", +]; + +const AGENT_NONBLANK_ENV_VARS: &[&str] = &[ + "CODEX_SESSION_ID", + "GROK_SESSION_ID", + "HERMES_SESSION_ID", + "JUNIE_SHIM_PATH", + "PI_SESSION_ID", +]; + +const AGENT_EXACT_ENV_VALUES: &[(&str, &str)] = &[ + ("CLAUDE_CODE_CHILD_SESSION", "1"), + ("DSH_SHELL", "1"), + ("HERMES_AGENT", "true"), + ("KILO", "1"), + ("OPENCLAW_SHELL", "exec"), + ("VTCODE", "1"), +]; + +fn env_is_nonempty(names: &[&str], env: &impl Fn(&str) -> Option) -> bool { + names + .iter() + .any(|name| env(name).is_some_and(|value| !value.is_empty())) +} + +fn env_is_nonblank(names: &[&str], env: &impl Fn(&str) -> Option) -> bool { + names + .iter() + .any(|name| env(name).is_some_and(|value| !value.trim().is_empty())) +} + +fn env_equals(values: &[(&str, &str)], env: &impl Fn(&str) -> Option) -> bool { + values + .iter() + .any(|(name, expected)| env(name).is_some_and(|value| value == *expected)) +} + +fn env_trimmed_equals(name: &str, expected: &str, env: &impl Fn(&str) -> Option) -> bool { + env(name).is_some_and(|value| value.trim() == expected) +} + +fn env_contains(names: &[&str], marker: &str, env: &impl Fn(&str) -> Option) -> bool { + names + .iter() + .any(|name| env(name).is_some_and(|value| value.contains(marker))) +} + +fn env_is_enabled(names: &[&str], env: &impl Fn(&str) -> Option) -> bool { + names.iter().any(|name| { + env(name).is_some_and(|value| { + let value = value.trim(); + !value.is_empty() + && !["0", "false", "no", "off"] + .iter() + .any(|disabled| value.eq_ignore_ascii_case(disabled)) + }) + }) +} + +pub(super) fn is_run_by_agent(env: impl Fn(&str) -> Option) -> bool { + env_is_nonempty(AGENT_NONEMPTY_ENV_VARS, &env) + || env_is_nonblank(AGENT_NONBLANK_ENV_VARS, &env) + || env_equals(AGENT_EXACT_ENV_VALUES, &env) + || env_is_enabled(&["AGENT", "AI_AGENT"], &env) + || env_trimmed_equals("CURSOR_EXTENSION_HOST_ROLE", "agent-exec", &env) + || env_contains(&["AWS_EXECUTION_ENV"], "AmazonQ-For-CLI", &env) + || (env_is_nonempty(&["AGENT_CONTEXT_OUT"], &env) + && env_is_nonempty(&["AGENT_DISPLAY_OUT"], &env)) + || env_contains(&["PS1", "PROMPT_COMMAND"], "###PS1JSON###", &env) +} diff --git a/src/codegen/build.rs b/src/codegen/build.rs index 3327836c..4188febe 100644 --- a/src/codegen/build.rs +++ b/src/codegen/build.rs @@ -29,6 +29,7 @@ use crate::{ Var, }, codegen::{ + ai_detection::is_run_by_agent, cleanup, sb3::Sb3, }, @@ -49,24 +50,6 @@ use crate::{ visitor, }; -const AGENT_ENV_VARS: &[&str] = &[ - "CLAUDECODE", - "CLAUDE_CODE_ENTRYPOINT", - "CODEX_SANDBOX", - "CODEX_ROOT", - "CODEX_THREAD_ID", - "CURSOR_AGENT", - "CURSOR_TRACE_ID", - "OPENCODE", - "GEMINI_CLI", -]; - -fn is_run_by_agent() -> bool { - AGENT_ENV_VARS - .iter() - .any(|name| std::env::var_os(name).is_some()) -} - pub fn build_impl( fs: Rc>, input: PathBuf, @@ -171,18 +154,22 @@ pub fn build_impl( ); visitor::pass3::visit_project(&mut project); visitor::pass4::visit_project(&mut project); - if is_run_by_agent() { - project.stage.vars.entry("AI=true".into()).or_insert(Var { - name: "AI=true".into(), - span: 0..0, - type_: Type::Value, - default: Some(ConstExpr::Value { - value: Value::String("".into()), + if is_run_by_agent(|name| std::env::var(name).ok()) { + project + .stage + .vars + .entry("goboscript_version=510P".into()) + .or_insert(Var { + name: "goboscript_version=510P".into(), span: 0..0, - }), - is_cloud: false, - is_used: true, - }); + type_: Type::Value, + default: Some(ConstExpr::Value { + value: Value::String("".into()), + span: 0..0, + }), + is_cloud: false, + is_used: true, + }); } log::info!("{:#?}", project); let mut sb3 = Sb3::new(fs.clone(), input.clone()); diff --git a/tools/check-min-publish-age.py b/tools/check-min-publish-age.py old mode 100644 new mode 100755 index 443b61f5..ede291b5 --- a/tools/check-min-publish-age.py +++ b/tools/check-min-publish-age.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env -S uv run --no-project --with tomli python import argparse import http.client import json From e2f02814683b3de4891f558726bbd3b64d93f158 Mon Sep 17 00:00:00 2001 From: aspizu Date: Wed, 7 Oct 2026 22:38:41 +0530 Subject: [PATCH 2/5] Move build information into Stage notes --- .github/workflows/check.yml | 1 - docs/configuration.md | 7 +-- src/codegen/ai_detection.rs | 14 +++--- src/codegen/build.rs | 22 ---------- src/codegen/sb3.rs | 80 +++++++++++++--------------------- tools/check-min-publish-age.py | 2 +- 6 files changed, 45 insertions(+), 81 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 48f1abf3..c854d84c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -5,7 +5,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v10.0.1 # TODO: remove this step and the script when stable cargo supports # registry.global-min-publish-age (stabilized in cargo 1.100). - run: ./tools/check-min-publish-age.py diff --git a/docs/configuration.md b/docs/configuration.md index a196aa54..13efd062 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -96,11 +96,12 @@ stage_height = 480 # default is 360 ## Notes -Embeds the contents of a file as a comment inside the Stage. +Add credits, instructions, or other project notes to the Stage using a text file. ```toml notes = "NOTES.md" # default is unset ``` -The comment is skipped if the file is missing or empty, and text longer than 8000 -characters is truncated. +Notes appear in a Stage comment beneath the "created with goboscript" header. +The header is included even when no notes are provided. Comments longer than +8000 characters are shortened. diff --git a/src/codegen/ai_detection.rs b/src/codegen/ai_detection.rs index 4909a7b8..2d783265 100644 --- a/src/codegen/ai_detection.rs +++ b/src/codegen/ai_detection.rs @@ -1,3 +1,7 @@ +use std::path::Path; + +use crate::vfs::VFS; + // https://github.com/vercel/vercel/blob/main/packages/detect-agent/src/index.ts // https://github.com/sdairs/is-ai-agent/blob/main/src/lib.rs const AGENT_NONEMPTY_ENV_VARS: &[&str] = &[ @@ -24,10 +28,7 @@ const AGENT_NONEMPTY_ENV_VARS: &[&str] = &[ "COPILOT_AGENT", "COPILOT_AGENT_JOB_ID", "COPILOT_AGENT_SESSION_ID", - "COPILOT_ALLOW_ALL", "COPILOT_CLI", - "COPILOT_GITHUB_TOKEN", - "COPILOT_MODEL", "CRUSH", "CURSOR_AGENT", "CURSOR_SANDBOX", @@ -47,7 +48,6 @@ const AGENT_NONEMPTY_ENV_VARS: &[&str] = &[ "OZ_RUN_ID", "PI_CODING_AGENT", "QWEN_CODE", - "REPL_ID", "ROO_CODE_TASK_ID", "TRAE_AI_SHELL_ID", "VECLI_DIR", @@ -111,7 +111,8 @@ fn env_is_enabled(names: &[&str], env: &impl Fn(&str) -> Option) -> bool }) } -pub(super) fn is_run_by_agent(env: impl Fn(&str) -> Option) -> bool { +pub(super) fn is_ai_project(fs: &dyn VFS, input: &Path) -> bool { + let env = |name: &str| std::env::var(name).ok(); env_is_nonempty(AGENT_NONEMPTY_ENV_VARS, &env) || env_is_nonblank(AGENT_NONBLANK_ENV_VARS, &env) || env_equals(AGENT_EXACT_ENV_VALUES, &env) @@ -121,4 +122,7 @@ pub(super) fn is_run_by_agent(env: impl Fn(&str) -> Option) -> bool { || (env_is_nonempty(&["AGENT_CONTEXT_OUT"], &env) && env_is_nonempty(&["AGENT_DISPLAY_OUT"], &env)) || env_contains(&["PS1", "PROMPT_COMMAND"], "###PS1JSON###", &env) + || ["AGENTS.md", "CLAUDE.md"] + .iter() + .any(|name| fs.is_file(&input.join(name))) } diff --git a/src/codegen/build.rs b/src/codegen/build.rs index 4188febe..29cb4243 100644 --- a/src/codegen/build.rs +++ b/src/codegen/build.rs @@ -21,15 +21,10 @@ use zip::{ use crate::{ ast::{ - ConstExpr, Project, Sprite, - Type, - Value, - Var, }, codegen::{ - ai_detection::is_run_by_agent, cleanup, sb3::Sb3, }, @@ -154,23 +149,6 @@ pub fn build_impl( ); visitor::pass3::visit_project(&mut project); visitor::pass4::visit_project(&mut project); - if is_run_by_agent(|name| std::env::var(name).ok()) { - project - .stage - .vars - .entry("goboscript_version=510P".into()) - .or_insert(Var { - name: "goboscript_version=510P".into(), - span: 0..0, - type_: Type::Value, - default: Some(ConstExpr::Value { - value: Value::String("".into()), - span: 0..0, - }), - is_cloud: false, - is_used: true, - }); - } log::info!("{:#?}", project); let mut sb3 = Sb3::new(fs.clone(), input.clone()); sb3.project( diff --git a/src/codegen/sb3.rs b/src/codegen/sb3.rs index 2bcd0935..67ba9c1a 100644 --- a/src/codegen/sb3.rs +++ b/src/codegen/sb3.rs @@ -1,48 +1,29 @@ use core::str; use std::{ cell::RefCell, - io::{ - self, - Write, - }, - path::{ - Path, - PathBuf, - }, + io::{self, Write}, + path::{Path, PathBuf}, rc::Rc, }; use anyhow::bail; use logos::Span; -use rustc_hash::{ - FxHashMap, - FxHashSet, -}; +use rustc_hash::{FxHashMap, FxHashSet}; use serde_json::json; use super::{ - node::Node, - node_id::NodeID, - node_id_factory::NodeIDFactory, - turbowarp_config::TurbowarpConfig, + node::Node, node_id::NodeID, node_id_factory::NodeIDFactory, turbowarp_config::TurbowarpConfig, }; use crate::{ ast::*, blocks::Block, codegen::{ - assets::AssetObjectStore, - datalists::read_list, + ai_detection::is_ai_project, assets::AssetObjectStore, datalists::read_list, mutation::Mutation, }, config::Config, - diagnostic::{ - DiagnosticKind, - SpriteDiagnostics, - }, - misc::{ - write_comma_io, - SmolStr, - }, + diagnostic::{DiagnosticKind, SpriteDiagnostics}, + misc::{write_comma_io, SmolStr}, vfs::VFS, }; @@ -588,21 +569,15 @@ impl Sb3 { write!(self.json, r#""isStage":{}"#, name == STAGE_NAME)?; write!(self.json, r#","name":{}"#, json!(name))?; if name == STAGE_NAME { - let notes = read_notes(&fs, input, config)?; + let notes = read_notes(&mut *fs.borrow_mut(), input, config)?; + let notes = build_notes_comment(¬es, is_ai_project(&*fs.borrow(), input)); write!(self.json, r#","comments":{{"#)?; - let mut comma = false; - let twconfig_y = if notes.trim().is_empty() { - 0 - } else { - write_comma_io(&mut self.json, &mut comma)?; - self.comment("notes", 0, 0, NOTES_WIDTH, NOTES_HEIGHT, ¬es)?; - NOTES_HEIGHT + COMMENT_GAP - }; - write_comma_io(&mut self.json, &mut comma)?; + self.comment("notes", 0, 0, NOTES_WIDTH, NOTES_HEIGHT, ¬es)?; + write!(self.json, ",")?; self.comment( "twconfig", 0, - twconfig_y, + NOTES_HEIGHT + COMMENT_GAP, 350, 170, &TurbowarpConfig::from(config).to_string(), @@ -1361,7 +1336,7 @@ impl Sb3 { } } -fn read_notes(fs: &Rc>, input: &Path, config: &Config) -> io::Result { +fn read_notes(fs: &mut dyn VFS, input: &Path, config: &Config) -> io::Result { let Some(notes_path) = config .notes .as_deref() @@ -1369,17 +1344,8 @@ fn read_notes(fs: &Rc>, input: &Path, config: &Config) -> io::R else { return Ok(String::new()); }; - match fs.borrow_mut().read_to_string(&input.join(notes_path)) { - Ok(notes) => { - let notes = notes.strip_prefix('\u{feff}').unwrap_or(¬es); - if notes.chars().count() > COMMENT_TEXT_LIMIT { - let mut truncated: String = notes.chars().take(COMMENT_TEXT_LIMIT - 1).collect(); - truncated.push(ELLIPSIS); - Ok(truncated) - } else { - Ok(notes.to_string()) - } - } + match fs.read_to_string(&input.join(notes_path)) { + Ok(notes) => Ok(notes), Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(String::new()), Err(err) => Err(io::Error::new( err.kind(), @@ -1388,6 +1354,22 @@ fn read_notes(fs: &Rc>, input: &Path, config: &Config) -> io::R } } +fn build_notes_comment(notes: &str, is_ai: bool) -> String { + let slop_marker = if is_ai { "510P" } else { "5000" }; + let notes = notes.strip_prefix('\u{feff}').unwrap_or(notes); + let mut text = format!( + "created with goboscript {} ({slop_marker})\n---\n{notes}", + option_env!("GIT_HASH") + .filter(|hash| !hash.is_empty()) + .unwrap_or(env!("CARGO_PKG_VERSION")) + ); + if text.chars().count() > COMMENT_TEXT_LIMIT { + text = text.chars().take(COMMENT_TEXT_LIMIT - 1).collect(); + text.push(ELLIPSIS); + } + text +} + fn compute_layers(project: &Project, config: &Config) -> anyhow::Result> { let mut layers: FxHashMap = Default::default(); let mut keys: Vec<_> = project.sprites.keys().collect(); diff --git a/tools/check-min-publish-age.py b/tools/check-min-publish-age.py index ede291b5..443b61f5 100755 --- a/tools/check-min-publish-age.py +++ b/tools/check-min-publish-age.py @@ -1,4 +1,4 @@ -#!/usr/bin/env -S uv run --no-project --with tomli python +#!/usr/bin/env python3 import argparse import http.client import json From a142994b3bcc5120dfb6f7b57d49c2d12cf2a244 Mon Sep 17 00:00:00 2001 From: aspizu Date: Wed, 7 Oct 2026 22:45:54 +0530 Subject: [PATCH 3/5] Reduce TurboWarp configuration comment size --- src/codegen/sb3.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codegen/sb3.rs b/src/codegen/sb3.rs index 67ba9c1a..3d61429d 100644 --- a/src/codegen/sb3.rs +++ b/src/codegen/sb3.rs @@ -578,8 +578,8 @@ impl Sb3 { "twconfig", 0, NOTES_HEIGHT + COMMENT_GAP, - 350, - 170, + 240, + 120, &TurbowarpConfig::from(config).to_string(), )?; write!(self.json, "}}")?; // comments From 68423891f5a6a9cd4059fa0bfb875a81bfe0fb6b Mon Sep 17 00:00:00 2001 From: aspizu Date: Thu, 8 Oct 2026 00:06:39 +0530 Subject: [PATCH 4/5] Use the goboscript logo in Stage notes --- docs/configuration.md | 4 ++-- src/LOGO.txt | 9 +++++++++ src/codegen/sb3.rs | 8 +++----- 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 src/LOGO.txt diff --git a/docs/configuration.md b/docs/configuration.md index 13efd062..bb002434 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -102,6 +102,6 @@ Add credits, instructions, or other project notes to the Stage using a text file notes = "NOTES.md" # default is unset ``` -Notes appear in a Stage comment beneath the "created with goboscript" header. -The header is included even when no notes are provided. Comments longer than +Notes appear in a Stage comment beneath the goboscript logo. +The logo is included even when no notes are provided. Comments longer than 8000 characters are shortened. diff --git a/src/LOGO.txt b/src/LOGO.txt new file mode 100644 index 00000000..35a044a8 --- /dev/null +++ b/src/LOGO.txt @@ -0,0 +1,9 @@ +⠀⠀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢾⣷⣾⡷⠀⠀⠀⠀⠀⠀⠀⢸⣿⡆⠀⠀⠀⠀ +⠀⣾⣿⠟⠻⣿⡇⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠀⠀⠀⠀⠀⣠⣴⣶⣶⡄⠀⠀⣀⣀⣀⡀⠀⣀⡀⠀⣀⡀⠙⠋⠀⢀⣤⣤⣤⣤⡄⢰⣶⣿⣷⣶⣶⠀⠀ +⢸⣿⠃⠀⠀⢻⣿⠀⣴⣾⡿⣿⣷⡄⢸⣿⣿⣿⣿⣦⡀⠀⠀⣰⣿⡿⣿⣷⣄⠀⠀⣾⣿⠟⠉⠛⠃⠀⣼⣿⠟⠛⠃⠀⣿⣇⣼⠟⠃⣿⡇⠸⣿⡟⠋⠉⠻⣿⣦⠀⣿⡏⠉⠁⠀⠀ +⢸⣿⠀⠀⠀⢸⣿⠀⣿⡏⠀⠈⢿⣿⢸⣿⠋⠀⠙⢿⣿⠀⢀⣿⡟⠀⠈⢻⣿⡆⠀⢿⣿⣦⣤⣤⡀⠀⣿⡏⠀⠀⠀⠀⢻⣿⠏⠀⠀⢿⣿⠀⣿⡇⠀⠀⠀⢹⣿⠀⣿⡇⠀⠀⠀⠀ +⢸⣿⡄⠀⠀⣸⣿⠀⣿⡇⠀⠀⢸⣿⢸⣿⡄⠀⠀⢸⣿⠀⢸⣿⡇⠀⠀⠀⣿⡇⠀⠀⠉⠉⠙⢿⣿⠀⣿⡇⠀⠀⠀⠀⢸⣿⠀⠀⠀⢸⣿⠀⣿⡇⠀⠀⠀⣼⣿⠀⣿⣇⠀⠀⠀⠀ +⠈⠿⣿⣿⣿⣿⣿⠀⣿⣷⡀⢀⣼⣿⠀⣿⣇⣀⣠⣿⣿⠀⠸⣿⣇⠀⠀⣰⣿⡇⢀⣤⣄⣀⣠⣾⡿⠀⣿⣧⣤⣴⣶⡆⢸⣿⠀⠀⠀⢸⣿⠀⣿⣧⣤⣴⣾⡿⠏⠀⢻⣿⣄⣀⣀⡀ +⠀⠀⠀⠀⣠⣿⡿⠀⠘⠿⠿⠿⠿⠃⠀⠛⠻⠿⠿⠟⠃⠀⠀⠻⢿⣿⣿⠿⠛⠀⠈⠛⠿⠿⠿⠛⠁⠀⠙⠛⠛⠛⠛⠁⠈⠉⠀⠀⠀⠈⠉⠀⣿⡟⠛⠋⠉⠀⠀⠀⠈⠛⠛⠛⠛⠃ +⣿⣷⣶⣿⡿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠁⠀⠀ diff --git a/src/codegen/sb3.rs b/src/codegen/sb3.rs index 3d61429d..f5e48bbc 100644 --- a/src/codegen/sb3.rs +++ b/src/codegen/sb3.rs @@ -1355,13 +1355,11 @@ fn read_notes(fs: &mut dyn VFS, input: &Path, config: &Config) -> io::Result String { - let slop_marker = if is_ai { "510P" } else { "5000" }; + let slop_marker = if is_ai { "510P" } else { "" }; let notes = notes.strip_prefix('\u{feff}').unwrap_or(notes); let mut text = format!( - "created with goboscript {} ({slop_marker})\n---\n{notes}", - option_env!("GIT_HASH") - .filter(|hash| !hash.is_empty()) - .unwrap_or(env!("CARGO_PKG_VERSION")) + "{}{slop_marker}\n\n{notes}", + include_str!("../LOGO.txt").trim_end(), ); if text.chars().count() > COMMENT_TEXT_LIMIT { text = text.chars().take(COMMENT_TEXT_LIMIT - 1).collect(); From 4ba505a0dbb8d2d0113ce38df19bf96779990f50 Mon Sep 17 00:00:00 2001 From: aspizu Date: Thu, 8 Oct 2026 00:19:45 +0530 Subject: [PATCH 5/5] Expand and persist AI detection --- src/codegen/ai_detection.rs | 59 +++++++++++++++++++++++++++++++++---- src/codegen/sb3.rs | 2 +- src/vfs.rs | 29 ++++++++++++++++++ 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/codegen/ai_detection.rs b/src/codegen/ai_detection.rs index 2d783265..447c2f1a 100644 --- a/src/codegen/ai_detection.rs +++ b/src/codegen/ai_detection.rs @@ -111,9 +111,9 @@ fn env_is_enabled(names: &[&str], env: &impl Fn(&str) -> Option) -> bool }) } -pub(super) fn is_ai_project(fs: &dyn VFS, input: &Path) -> bool { +pub(super) fn is_ai_project(fs: &mut dyn VFS, input: &Path) -> bool { let env = |name: &str| std::env::var(name).ok(); - env_is_nonempty(AGENT_NONEMPTY_ENV_VARS, &env) + let is_ai_env = env_is_nonempty(AGENT_NONEMPTY_ENV_VARS, &env) || env_is_nonblank(AGENT_NONBLANK_ENV_VARS, &env) || env_equals(AGENT_EXACT_ENV_VALUES, &env) || env_is_enabled(&["AGENT", "AI_AGENT"], &env) @@ -121,8 +121,57 @@ pub(super) fn is_ai_project(fs: &dyn VFS, input: &Path) -> bool { || env_contains(&["AWS_EXECUTION_ENV"], "AmazonQ-For-CLI", &env) || (env_is_nonempty(&["AGENT_CONTEXT_OUT"], &env) && env_is_nonempty(&["AGENT_DISPLAY_OUT"], &env)) - || env_contains(&["PS1", "PROMPT_COMMAND"], "###PS1JSON###", &env) - || ["AGENTS.md", "CLAUDE.md"] + || env_contains(&["PS1", "PROMPT_COMMAND"], "###PS1JSON###", &env); + let has_ai_instructions = [ + "AGENTS.md", + "AGENTS.override.md", + "CLAUDE.md", + "GEMINI.md", + "CONVENTIONS.md", + ".aider.conf.yml", + ".cursorrules", + ".windsurfrules", + ".clinerules", + ".roorules", + ".github/copilot-instructions.md", + ".junie/AGENTS.md", + ".junie/playbook.md", + ".junie/guidelines.md", + ] + .iter() + .any(|name| fs.is_file(&input.join(name))) + || input.to_str().is_some_and(|input| { + let input = glob::Pattern::escape(input); + [ + ".github/instructions/**/*.instructions.md", + ".cursor/rules/**/*.mdc", + ".devin/rules/**/*.md", + ".windsurf/rules/**/*.md", + ".clinerules/**/*.md", + ".cline/rules/**/*.md", + ".roo/rules/**/*", + ".roo/rules-*/**/*", + ".roorules-*", + ".continue/rules/**/*.md", + ".amazonq/rules/**/*.md", + ".junie/rules/*.md", + ".junie/guidelines/**/*", + ] .iter() - .any(|name| fs.is_file(&input.join(name))) + .any(|pattern| { + fs.glob(Path::new(&input).join(pattern).to_str().unwrap()) + .is_ok_and(|paths| paths.iter().any(|path| fs.is_file(path))) + }) + }); + if is_ai_env && !has_ai_instructions { + create_agents_md(fs, input); + } + is_ai_env || has_ai_instructions +} + +fn create_agents_md(fs: &mut dyn VFS, input: &Path) { + let _ = fs.create_new_file( + &input.join("AGENTS.md"), + b"See the [goboscript documentation](https://aspiz.uk/goboscript/docs).\n", + ); } diff --git a/src/codegen/sb3.rs b/src/codegen/sb3.rs index f5e48bbc..1c30b60a 100644 --- a/src/codegen/sb3.rs +++ b/src/codegen/sb3.rs @@ -570,7 +570,7 @@ impl Sb3 { write!(self.json, r#","name":{}"#, json!(name))?; if name == STAGE_NAME { let notes = read_notes(&mut *fs.borrow_mut(), input, config)?; - let notes = build_notes_comment(¬es, is_ai_project(&*fs.borrow(), input)); + let notes = build_notes_comment(¬es, is_ai_project(&mut *fs.borrow_mut(), input)); write!(self.json, r#","comments":{{"#)?; self.comment("notes", 0, 0, NOTES_WIDTH, NOTES_HEIGHT, ¬es)?; write!(self.json, ",")?; diff --git a/src/vfs.rs b/src/vfs.rs index 69203f1a..d67dd1b8 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -4,6 +4,7 @@ use std::{ self, Cursor, Read, + Write, }, path::{ Path, @@ -32,6 +33,10 @@ pub trait VFS { fn is_file(&self, path: &Path) -> bool; fn glob(&mut self, pattern: &str) -> io::Result>; + fn create_new_file(&mut self, _path: &Path, _contents: &[u8]) -> io::Result<()> { + Err(io::ErrorKind::Unsupported.into()) + } + fn read_to_string(&mut self, path: &Path) -> io::Result { let mut file = self.read_file(path)?; let mut contents = String::new(); @@ -83,6 +88,14 @@ impl VFS for RealFS { } Ok(entries) } + + fn create_new_file(&mut self, path: &Path, contents: &[u8]) -> io::Result<()> { + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path)? + .write_all(contents) + } } #[derive(Tsify, Serialize, Deserialize)] @@ -177,6 +190,22 @@ impl VFS for MemFS { } Ok(entries) } + + fn create_new_file(&mut self, path: &Path, contents: &[u8]) -> io::Result<()> { + if self.is_file(path) || self.is_dir(path) { + return Err(io::ErrorKind::AlreadyExists.into()); + } + let path_str = path + .to_str() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid UTF-8 in path"))?; + self.files.insert( + path_str.to_owned(), + Data { + inner: contents.to_vec(), + }, + ); + Ok(()) + } } fn normalize_glob_path(path: &str) -> Cow<'_, str> {