Add manifest-driven scaffolding with create and scaffold-cleanup commands#97
Add manifest-driven scaffolding with create and scaffold-cleanup commands#97ericelliott wants to merge 40 commits intomainfrom
create and scaffold-cleanup commands#97Conversation
Implements the `npx aidd create` epic with manifest-driven scaffolding: - `aidd create [type|URI] <folder>` resolves named scaffolds, HTTP/HTTPS URIs (with remote-code warning + confirmation), and file:// URIs. Reads SCAFFOLD-MANIFEST.yml and executes run/prompt steps sequentially; supports `--agent <name>` (default: claude) for prompt steps; warns about remote code and halts on step failure; leaves .aidd/scaffold/ in place and suggests scaffold-cleanup on completion. - `aidd scaffold-cleanup [folder]` removes the .aidd/ working directory and reports gracefully when nothing exists to clean up. - `lib/scaffold-resolver.js` with full unit test coverage. - `lib/scaffold-runner.js` with parseManifest (gray-matter YAML parsing) and runManifest with injectable execStep for isolation. - `lib/scaffold-cleanup.js` with unit tests. - `ai/scaffolds/scaffold-example` — minimal E2E test fixture that inits npm, sets vitest test script, and installs riteway/vitest/playwright/ error-causes/@paralleldrive/cuid2 at @latest. - `ai/scaffolds/next-shadcn` — default named scaffold stub with README describing the intended Next.js + shadcn/ui setup. - `bin/create-e2e.test.js` — E2E tests covering scaffold creation, AIDD_CUSTOM_EXTENSION_URI file:// override, scaffold-cleanup, and --agent flag passthrough. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
- Fix command injection in scaffold-runner: prompt steps now use spawn([agent, promptText]) array form instead of shell interpolation, so prompt content cannot inject shell commands - Fix extension.js execution to also use array spawn (node, path) - Add 30s network timeout to defaultFetchText to prevent indefinite hangs - Extract isHttpUrl/isFileUrl predicates and DEFAULT_SCAFFOLD_TYPE constant in scaffold-resolver for readability and to eliminate repeated string checks - Remove dead _noFetch variable from scaffold-resolver test - Fix fragile cwd-default test in scaffold-cleanup using vi.spyOn(process, cwd) instead of depending on ambient filesystem state - Mark npx-aidd-create-epic as DONE and log activity https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
|
@cursor please /review |
|
✅ No technical debt markers (TODO/FIXME) in new code 9. Specific Issues SummaryCritical: High:
Medium: Low: 10. Actionable RecommendationsMust Fix (Before Merge):
// In lib/scaffold-resolver.js
import { createError } from "error-causes";
// Replace all throw new Error() with:
throw createError({
name: 'ScaffoldError',
message: 'Remote extension download cancelled by user.',
code: 'USER_CANCELLED',
uri: effectiveType
});
import { URL } from 'url';
const validateHttpUrl = (urlString) => {
try {
const url = new URL(urlString);
const hostname = url.hostname;
// Block private IP ranges
if (
hostname === 'localhost' ||
hostname.startsWith('127.') ||
hostname.startsWith('10.') ||
hostname.startsWith('192.168.') ||
hostname.startsWith('169.254.') ||
hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)
) {
throw createError({
name: 'InvalidScaffoldUri',
message: 'Private IP addresses are not allowed',
code: 'PRIVATE_IP_BLOCKED',
uri: urlString
});
}
return true;
} catch (err) {
throw createError({
name: 'InvalidScaffoldUri',
message: 'Invalid URL format',
code: 'INVALID_URL',
uri: urlString,
cause: err
});
}
};
// Use before fetching
if (isHttpUrl(effectiveType)) {
validateHttpUrl(effectiveType);
// ... rest of logic
}Should Fix (Post-Merge):
/**
* Resolves a scaffold source to local file paths.
* Supports named scaffolds, file:// URIs, and HTTP(S) URIs.
* @param {Object} options - Configuration options
* @param {string} [options.type] - Scaffold type: name, file://, or http(s):// URI
* @param {string} options.folder - Target folder path
* @param {string} [options.packageRoot] - Package root directory
* @returns {Promise<{manifestPath: string, extensionJsPath: string, readmePath: string}>}
*/
11. Vision Alignment✅ The PR aligns well with the vision document:
Final VerdictStatus:
Overall Quality: 8.5/10
Test Coverage: 10/10 - Comprehensive and well-written Code Quality: 9/10 - Clean, functional, follows project standards (minus error-causes) Security: 7/10 - Good injection prevention, but SSRF risk and missing integrity checks The implementation is solid and the test coverage is exemplary. Once the two security issues are addressed, this will be an excellent addition to the codebase. |
Define ScaffoldCancelledError, ScaffoldNetworkError, and ScaffoldStepError
in lib/scaffold-errors.js using the errorCauses() factory so every call-site
that builds a handler is exhaustiveness-checked at setup time.
- scaffold-resolver: throw createError({...ScaffoldCancelledError}) on user
cancellation; wrap fetchAndSaveExtension failures in ScaffoldNetworkError
- scaffold-runner: defaultExecStep now rejects with createError({...ScaffoldStepError})
so spawn failures carry a typed cause
- bin/aidd.js create action: replace generic catch with handleScaffoldErrors()
for typed branching (cancelled → info, network → retry hint, step → manifest hint)
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Implements manifest-driven project scaffolding for the aidd CLI by adding create and scaffold-cleanup subcommands, along with the supporting resolver/runner/cleanup libraries, scaffold fixtures, and test coverage.
Changes:
- Added scaffold source resolution (named scaffolds,
file://,http(s)://with confirmation) and a manifest runner forrun/promptsteps. - Integrated new
createandscaffold-cleanupsubcommands into the CLI. - Added scaffold fixtures (
scaffold-example,next-shadcn) plus unit + E2E test suites.
Reviewed changes
Copilot reviewed 17 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tasks/npx-aidd-create-epic.md | Marks the epic status as DONE. |
| lib/scaffold-runner.js | New manifest parser + step runner (run/prompt + optional extension.js). |
| lib/scaffold-runner.test.js | Unit tests for manifest parsing and execution ordering/failure behavior. |
| lib/scaffold-resolver.js | New resolver for named/file/http scaffolds with caching under .aidd/scaffold. |
| lib/scaffold-resolver.test.js | Unit tests for named/file/http resolution, confirmation, and env var defaulting. |
| lib/scaffold-cleanup.js | New cleanup helper to remove .aidd/ working directory. |
| lib/scaffold-cleanup.test.js | Unit tests for cleanup behavior and default folder handling. |
| bin/aidd.js | Adds create (with --agent) and scaffold-cleanup subcommands. |
| bin/create-e2e.test.js | E2E tests for create, env defaulting, --agent, and cleanup. |
| ai/scaffolds/index.md | Adds index entry for scaffold directory. |
| ai/index.md | Links to ai/scaffolds/ from the top-level AI index. |
| ai/scaffolds/scaffold-example/README.md | Documents the scaffold-example fixture scaffold. |
| ai/scaffolds/scaffold-example/SCAFFOLD-MANIFEST.yml | Defines manifest steps for the E2E fixture (npm init/pkg/test/install). |
| ai/scaffolds/scaffold-example/index.md | Generated directory index for scaffold-example. |
| ai/scaffolds/scaffold-example/bin/index.md | Generated directory index for bin/. |
| ai/scaffolds/next-shadcn/README.md | Documents default scaffold (stub). |
| ai/scaffolds/next-shadcn/SCAFFOLD-MANIFEST.yml | Placeholder manifest with a prompt step. |
| ai/scaffolds/next-shadcn/index.md | Generated directory index for next-shadcn. |
| activity-log.md | Adds an entry describing the new commands and modules. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The previous implementation fetched only README.md, SCAFFOLD-MANIFEST.yml, and bin/extension.js over HTTP(S). This prevented scaffolds from carrying arbitrary file trees, templates, package.json dependencies, config files, bin scripts, etc. Replace fetchAndSaveExtension + manual HTTP(S) fetch with a git clone (defaultGitClone) that clones the full repo into <folder>/.aidd/scaffold/. The injectable `clone` parameter keeps tests fast and network-free. Also drop the now-unused http/https node built-ins and FETCH_TIMEOUT_MS. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
1. YAML document-start marker silently drops all steps (medium severity)
The gray-matter wrapper trick ("---\n" + content + "\n---") caused
gray-matter to see two consecutive --- lines and parse empty frontmatter
when a manifest author included the standard --- marker. Replace with
matter.engines.yaml.parse(content), a direct call to js-yaml's load()
which handles --- markers correctly.
Test added: "parses steps when manifest begins with YAML document-start marker"
2. npx aidd create (no args) crashes with TypeError (high severity)
Both positionals were optional, so Commander allowed zero arguments.
folderArg became undefined and path.resolve(cwd, undefined) threw.
Flip the arg order to <folder> [type] — folder is now required and
Commander rejects the call before the action runs.
Also note: the defaultFetchText res.resume() socket-leak issue reported
separately is moot — defaultFetchText was removed in the previous commit
when HTTP fetching was replaced with git clone.
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 20 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ent GitHub release workflow
TDD: added failing tests first, then implemented to pass.
- scaffold-errors.js: add ScaffoldValidationError (code SCAFFOLD_VALIDATION_ERROR)
- scaffold-runner.js: parseManifest now validates that steps is an array of plain
objects with at least one recognized key (run/prompt); throws ScaffoldValidationError
with a descriptive message on any violation instead of silently iterating wrong values
- scaffold-runner.test.js: 5 new tests covering string steps, object steps, bare-string
items, unrecognized-key items, and the error message content
- scaffold-verifier.js + scaffold-verifier.test.js: new verifyScaffold() function
(8 tests) that checks manifest existence, valid YAML, valid step shapes, and
non-empty steps list — returns { valid, errors } for clean reporting
- bin/aidd.js: add `verify-scaffold [type]` subcommand; update `create` error handler
to cover ScaffoldValidationError
- scaffold-example/SCAFFOLD-MANIFEST.yml: add release-it install + scripts.release step
so generated projects have a release command out of the box
- scaffold-example/package.json: new file so scaffold AUTHORS can release their scaffold
as a GitHub release with `npm run release`
- tasks/npx-aidd-create-epic.md: update requirements — GitHub releases instead of git
clone for remote scaffolds; add verify-scaffold, steps validation, and scaffold
author release workflow sections
- docs/scaffold-authoring.md: new guide covering manifest format, validation, the
distinction between npm's files array (npm publish only) and GitHub release assets,
and how to publish a scaffold as a GitHub release
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
…rst, folder second
The previous fix made <folder> required (correct) but accidentally swapped the
positional order to `create <folder> [type]`. Every E2E test, both scaffold
READMEs, the epic requirements, the activity log, and the docs all call the
command as `create [type] <folder>` (e.g. `create scaffold-example my-project`).
With the wrong order, `create scaffold-example test-project` would bind
folder="scaffold-example" and type="test-project", creating a directory named
after the scaffold and trying to resolve a nonexistent scaffold named after the
folder — completely backwards.
Restores `.command("create [type] <folder>")` with action `(type, folder, opts)`.
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
…full e2e in pre-commit
Argument parsing:
The previous [type] <folder> definition caused Commander to assign the
single-argument form (create my-folder) to `type`, leaving folder missing.
Restore [typeOrFolder] [folder] with manual validation so all three calling
patterns work correctly:
create scaffold-example my-project → type=scaffold-example, folder=my-project
create my-project → type=undefined (env/default), folder=my-project
create → explicit 'missing required argument' error
Pre-commit hook:
Changed from `npm run test:unit` to `npm test` so e2e tests run on every
commit. The arg-order regression slipped through exactly because e2e was
excluded from the hook. Full suite is ~50s but catches integration bugs.
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 25 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The internal [typeOrFolder] [folder] signature (needed to work around Commander's left-to-right arg assignment) showed both as optional in --help. Add a .usage() override and .addHelpText() so the displayed usage reads `[options] [type] <folder>` with an Arguments section that explicitly marks <folder> as required and shows four calling-convention examples. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
|
@ericelliott I've opened a new pull request, #99, to work on those changes. Once the pull request is ready, I'll request review from you. |
* Initial plan * fix: use @paralleldrive/cuid2 in task requirements for consistency Co-authored-by: ericelliott <364727+ericelliott@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ericelliott <364727+ericelliott@users.noreply.github.com>
| } | ||
| if (existingContent.includes("AGENTS.md")) { | ||
| return { | ||
| action: "unchanged", |
There was a problem hiding this comment.
CLAUDE.md modified on every second install run
Medium Severity
ensureClaudeMd creates CLAUDE.md with AGENTS_MD_CONTENT on first run, but AGENTS_MD_CONTENT never contains the literal string "AGENTS.md". On the next npx aidd install, the existingContent.includes("AGENTS.md") check on the tool's own content always fails, causing it to append an unnecessary "see AGENTS.md" reference to a file it just created. This contradicts the stated intent of "never overwrites an existing CLAUDE.md" — the file is silently modified on every second invocation.
| return { | ||
| ...(paths.downloaded | ||
| ? { cleanupTip: `npx aidd scaffold-cleanup ${folder}` } | ||
| : {}), |
There was a problem hiding this comment.
Cleanup tip breaks for paths containing spaces
Low Severity
The cleanupTip is built via simple string interpolation: `npx aidd scaffold-cleanup ${folder}`. If folder is an absolute path containing spaces (e.g. /home/user/my project), the suggested command becomes npx aidd scaffold-cleanup /home/user/my project, which the shell would split into two separate arguments. The path needs to be quoted in the tip string so the user can copy-paste it safely.
…g.md docs/scaffold-authoring.md was missing the fourth validation bullet — a step with both run and prompt keys is rejected as ambiguous. Already documented in ai/scaffolds/SCAFFOLD-AUTHORING.md and implemented in lib/scaffold-runner.js lines 92-97. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
ai/scaffolds/ is for functional scaffolds (SCAFFOLD-MANIFEST.yml, bin/, etc.), not documentation. SCAFFOLD-AUTHORING.md was plain docs that had no agent-skill structure (no command pattern, no SudoLang, no frontmatter command). The canonical authoring guide already lives at docs/scaffold-authoring.md; update the README link accordingly. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 43 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…missing folder When a user runs `npx aidd create https://github.com/org/repo` without a folder argument, the URL was silently treated as the folder name, causing path.resolve() to produce a mangled path (/cwd/https:/github.com/org/repo) and the default scaffold to run in the wrong place. Return null (triggering the existing "missing required argument 'folder'" error) when the sole argument starts with https:// or file://. http:// is intentionally excluded — it is not a supported protocol. The two-arg form (npx aidd create <url> <folder>) is unaffected. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
Record the requirement that was implemented in the previous fix commit but not captured in the epic first. Process should be: update epic → write failing test → implement → watch it pass → e2e → review → commit. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
| $ npx aidd create https://github.com/org/scaffold my-project | ||
| $ npx aidd create file:///path/to/scaffold my-project | ||
| `, | ||
| ) |
There was a problem hiding this comment.
create help omits user config fallback
Medium Severity
The create command help text says [type] defaults to AIDD_CUSTOM_CREATE_URI and then "next-shadcn", but the actual resolver also falls back to ~/.aidd/config.yml (create-uri). This mismatch makes npx aidd set create-uri ... look ineffective and can lead to incorrect CLI usage.
| const folderPath = path.resolve(process.cwd(), resolvedFolder); | ||
|
|
||
| return { folderPath, resolvedFolder, type }; | ||
| }; |
There was a problem hiding this comment.
Single http:// arg treated as folder
Medium Severity
resolveCreateArgs returns null for a single https:///file:// arg (to avoid mangled folder creation) but intentionally allows a single http:// arg to fall through as a folder name. This can try to ensureDir on a path containing : and /, producing confusing filesystem behavior instead of a clear “missing folder / unsupported URI” error.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 43 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codifies the AIDD fix process as an AgentSkills.io-compliant skill at ai/skills/fix/SKILL.md (name: aidd-fix). Six sequential steps enforce: context validation before touching any file, epic-first requirement documentation, TDD discipline (failing test before implementation), e2e gate, self-review, then commit+push. Adds /fix to please.mdc command list for discoverability via /help. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
Auto-generated by the pre-commit index hook. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
- Rename /fix → /aidd-fix everywhere (please.mdc, SKILL.md Commands block) to avoid collision with framework built-in commands - Add ai/commands/aidd-fix.md — thin stub discoverable by Cursor via the .cursor/ → ai/ symlink as /aidd-fix (plain Markdown, no frontmatter) - Add Skills section to AGENTS.md and AGENTS_MD_CONTENT listing "fix bug → /aidd-fix" with per-platform discovery paths - Note: .claude/ is gitignored; Claude Code discovery via symlink TBD https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
| const folderPath = path.resolve(process.cwd(), resolvedFolder); | ||
|
|
||
| return { folderPath, resolvedFolder, type }; | ||
| }; |
There was a problem hiding this comment.
Single http URL becomes folder name
Medium Severity
resolveCreateArgs() treats a single http://... argument as a folder name (while rejecting single https:///file://). This can silently create a directory literally named like a URL and then scaffold using the default type, which looks like “create from URL” succeeded but actually ignored the URL.
| } | ||
|
|
||
| return { errors: [], valid: true }; | ||
| }; |
There was a problem hiding this comment.
Verifier rejects extension-only scaffolds
Low Severity
verifyScaffold() marks manifests with no steps as invalid ("scaffold would do nothing"), but a scaffold can still do meaningful work via bin/extension.js (which runManifest() runs even when steps is empty). This makes verify-scaffold report false negatives for extension-driven scaffolds.
|
Bugbot Autofix prepared fixes for 2 of the 2 bugs found in the latest run.
Or push these changes by commenting: Preview (bfc9138408)diff --git a/lib/scaffold-create.js b/lib/scaffold-create.js
--- a/lib/scaffold-create.js
+++ b/lib/scaffold-create.js
@@ -22,9 +22,8 @@
// If no folder was given and the sole argument looks like a URI, the user
// most likely forgot the folder argument. Return null so the caller emits
// "missing required argument 'folder'" rather than creating a directory
- // with a mangled URL path. http:// is intentionally excluded — it is not
- // a supported protocol.
- if (folder === undefined && /^(https|file):\/\//i.test(typeOrFolder)) {
+ // with a mangled URL path.
+ if (folder === undefined && /^(https?|file):\/\//i.test(typeOrFolder)) {
return null;
}
diff --git a/lib/scaffold-create.test.js b/lib/scaffold-create.test.js
--- a/lib/scaffold-create.test.js
+++ b/lib/scaffold-create.test.js
@@ -32,13 +32,13 @@
});
});
- test("does not reject http:// as a single arg (unsupported — falls through to folder)", () => {
+ test("returns null when single arg is an http:// URI (folder omitted)", () => {
const result = resolveCreateArgs("http://example.com/scaffold", undefined);
assert({
- given: "an http:// URL (unsupported protocol) as the sole arg",
- should: "not return null — http is not a recognised URI scheme",
- actual: result !== null,
- expected: true,
+ given: "only an http:// URL with no folder",
+ should: "return null to signal missing folder",
+ actual: result,
+ expected: null,
});
});
diff --git a/lib/scaffold-verifier.js b/lib/scaffold-verifier.js
--- a/lib/scaffold-verifier.js
+++ b/lib/scaffold-verifier.js
@@ -1,3 +1,4 @@
+import path from "path";
import fs from "fs-extra";
import { parseManifest } from "./scaffold-runner.js";
@@ -24,8 +25,14 @@
}
if (steps.length === 0) {
- errors.push("Manifest contains no steps — scaffold would do nothing");
- return { errors, valid: false };
+ const scaffoldDir = path.dirname(manifestPath);
+ const extensionJsPath = path.join(scaffoldDir, "bin/extension.js");
+ const extensionExists = await fs.pathExists(extensionJsPath);
+
+ if (!extensionExists) {
+ errors.push("Manifest contains no steps — scaffold would do nothing");
+ return { errors, valid: false };
+ }
}
return { errors: [], valid: true };
diff --git a/lib/scaffold-verifier.test.js b/lib/scaffold-verifier.test.js
--- a/lib/scaffold-verifier.test.js
+++ b/lib/scaffold-verifier.test.js
@@ -87,20 +87,38 @@
});
});
- test("returns valid=false when manifest has no steps", async () => {
+ test("returns valid=false when manifest has no steps and no extension", async () => {
const manifestPath = path.join(tempDir, "SCAFFOLD-MANIFEST.yml");
await fs.writeFile(manifestPath, "steps: []\n");
const result = await verifyScaffold({ manifestPath });
assert({
- given: "a manifest with an empty steps array",
+ given: "a manifest with an empty steps array and no extension.js",
should: "return valid false (scaffold would do nothing)",
actual: result.valid,
expected: false,
});
});
+ test("returns valid=true when manifest has no steps but has extension.js", async () => {
+ const manifestPath = path.join(tempDir, "SCAFFOLD-MANIFEST.yml");
+ const extensionPath = path.join(tempDir, "bin/extension.js");
+
+ await fs.writeFile(manifestPath, "steps: []\n");
+ await fs.ensureDir(path.dirname(extensionPath));
+ await fs.writeFile(extensionPath, "console.log('extension');\n");
+
+ const result = await verifyScaffold({ manifestPath });
+
+ assert({
+ given: "a manifest with no steps but with bin/extension.js",
+ should: "return valid true (extension can still do work)",
+ actual: result.valid,
+ expected: true,
+ });
+ });
+
test("returns valid=true for manifest with prompt steps", async () => {
const manifestPath = path.join(tempDir, "SCAFFOLD-MANIFEST.yml");
await fs.writeFile( |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 45 out of 49 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Generalise symlink creation so both --cursor and --claude share one
parameterised function instead of duplicating the logic.
- lib/symlinks.js: new createSymlink({ name, targetBase, force }) that
handles any editor symlink (.cursor, .claude, or future additions)
- lib/cli-core.js: remove createCursorSymlink, import createSymlink,
add claude param to executeClone
- bin/aidd.js: add --claude option, pass claude through, update Quick
Start help text to show Cursor / Claude Code / both examples
- lib/symlinks.test.js: rename from cursor-symlink.test.js; add 6 new
claude tests + 1 combined test (11 total); all cursor tests preserved
- bin/cli-help-e2e.test.js: update Quick Start assertion for new text
- tasks/claude-symlink-epic.md: new epic documenting requirements
293 unit tests, 41 e2e tests — all green.
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
| action: "created", | ||
| message: "Created CLAUDE.md with AI agent guidelines", | ||
| }; | ||
| } |
There was a problem hiding this comment.
Non-idempotent CLAUDE.md creation on repeated installs
Medium Severity
ensureClaudeMd creates CLAUDE.md with AGENTS_MD_CONTENT, which does not contain the literal string "AGENTS.md". On a subsequent npx aidd run, the existingContent.includes("AGENTS.md") check returns false, so the function appends a redundant AGENTS.md reference to a file it just created. This makes the installer non-idempotent — the second run always modifies CLAUDE.md unnecessarily. A different detection mechanism (e.g., checking for the template's heading) would avoid modifying the tool's own output on re-run.
Additional Locations (1)
| readConfigFn: noConfig, | ||
| }); | ||
|
|
||
| process.env.AIDD_CUSTOM_CREATE_URI = originalEnv; |
There was a problem hiding this comment.
Env var restore sets "undefined" string instead of deleting
Low Severity
Restoring process.env.AIDD_CUSTOM_CREATE_URI by assigning a saved value that was undefined coerces it to the string "undefined" instead of removing the variable. process.env stringifies all assigned values. This pollutes the environment for subsequent tests, potentially causing resolveExtension to treat "undefined" as a scaffold type. The cleanup needs a conditional delete when the original value was not set.
Additional Locations (1)
| const FileSystemError = { | ||
| code: "FILESYSTEM_ERROR", | ||
| message: "File system operation failed", | ||
| }; |
There was a problem hiding this comment.
Duplicate error type definitions across modules
Low Severity
ValidationError and FileSystemError are redefined as standalone objects in symlinks.js rather than reusing the identical definitions from the errorCauses call in cli-core.js. If the error codes or messages are updated in one location, the other can easily drift out of sync, causing handleCliErrors to fail to dispatch correctly for symlink errors.
- Drop "*No description available*" for files without frontmatter description; silently omit - Shorten SKILL.md description to ≤50 chars: "Fix a bug or apply review feedback." - Simplify AGENTS.md and AGENTS_MD_CONTENT Skills section to a plain index mapping (fix bug → /aidd-fix) — no duplicated docs https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| const readConfig = async ({ configFile = CONFIG_FILE } = {}) => { | ||
| try { | ||
| const content = await fs.readFile(configFile, "utf-8"); | ||
| return yaml.load(content) ?? {}; |
There was a problem hiding this comment.
Unsafe YAML parsing allows code execution
Medium Severity
The yaml.load() call in readConfig doesn't specify a schema, defaulting to DEFAULT_SCHEMA which enables YAML type tags like !!js/function and !!js/regexp that execute arbitrary JavaScript. While ~/.aidd/config.yml is user-controlled, an attacker who can influence this file (through compromised dependencies or social engineering) could inject malicious YAML that executes when the user runs npx aidd create. The same codebase correctly uses yaml.JSON_SCHEMA in scaffold-runner.js line 55 to prevent this.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 53 out of 58 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const readConfig = async ({ configFile = CONFIG_FILE } = {}) => { | ||
| try { | ||
| const content = await fs.readFile(configFile, "utf-8"); | ||
| return yaml.load(content) ?? {}; | ||
| } catch { |
There was a problem hiding this comment.
readConfig uses yaml.load(content) with the default schema, which allows YAML tags that can deserialize into non-plain JS types (e.g. RegExp/functions). Since the returned value is used as a string (config["create-uri"]), a tagged value can cause runtime type errors or unexpected behavior. Parse with a restricted schema (e.g. yaml.JSON_SCHEMA) and/or validate that the loaded config is a plain object with string values before returning it.
| const knownKeys = Object.keys(step).filter((k) => KNOWN_STEP_KEYS.has(k)); | ||
|
|
||
| if (knownKeys.length === 0) { | ||
| const found = Object.keys(step).join(", ") || "(empty)"; | ||
| throw createError({ | ||
| ...ScaffoldValidationError, | ||
| message: `Manifest step ${i + 1} has no recognized keys (run, prompt). Found: ${found}`, | ||
| }); |
There was a problem hiding this comment.
parseManifest validates the presence of run/prompt keys but does not validate that their values are strings. A manifest like - run: 42 will pass validation, then runManifest/spawn will throw a raw TypeError instead of a typed ScaffoldValidationError/ScaffoldStepError. Add a validation that step.run / step.prompt are strings (and include the step number/type in the error).
Breaks the 58-file manifest-driven scaffolding PR into 5 self-contained, independently deployable chunks with no incomplete CLI workflows exposed. https://claude.ai/code/session_01T1QH62uHcb8uFWK4apfn8N
| my-scaffold/ | ||
| ├── SCAFFOLD-MANIFEST.yml # required — list of steps to execute | ||
| ├── README.md # optional — displayed to the user before steps run | ||
| ├── bin/ |
There was a problem hiding this comment.
We are not going to have a standard extension.js because you can just run any arbitrary command during install.





Overview
This PR implements the
npx aidd createepic by adding two new CLI subcommands that enable manifest-driven project scaffolding:npx aidd create [type|URI] <folder>— Scaffolds new projects from named scaffolds, local file:// URIs, or remote HTTPS URIsnpx aidd scaffold-cleanup [folder]— Removes temporary.aidd/working directories after scaffoldingNew Requirements
Key Changes
New Modules
lib/scaffold-resolver.js.aidd/scaffold/AIDD_CUSTOM_EXTENSION_URIenvironment variable for custom defaultslib/scaffold-runner.jsrunsteps as shell commandspromptsteps via agent CLI (default:claude) with proper argument escapingbin/extension.jsafter all manifest steps if presentlib/scaffold-cleanup.js.aidd/directory from scaffolded projectsCLI Integration
Updated
bin/aidd.jsto add:createcommand with--agentflag for specifying the AI agentscaffold-cleanupcommand for post-scaffold cleanupScaffolds
Added two scaffold fixtures:
ai/scaffolds/scaffold-example/— E2E test fixturenpm init -yai/scaffolds/next-shadcn/— Default scaffold (placeholder)Tests
Added comprehensive test suites:
lib/scaffold-resolver.test.js(378 lines) — Tests named scaffolds, file:// URIs, HTTP/HTTPS URIs, remote code warnings, and environment variable handlinglib/scaffold-runner.test.js(329 lines) — Tests manifest parsing, step execution, error handling, and extension.js invocationlib/scaffold-cleanup.test.js(70 lines) — Tests directory removal and not-found casesbin/create-e2e.test.js(292 lines) — End-to-end tests forcreateandscaffold-cleanupcommands with real file I/OSecurity Considerations
Test Plan
All changes are covered by automated tests:
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
Note
High Risk
Executes scaffold-defined commands and agent prompts, and adds remote download/extraction and config writing logic; despite validation and HTTPS/confirmation safeguards, this expands the security-sensitive surface area.
Overview
Adds a manifest-driven scaffolding workflow to the
aiddCLI via newcreate,verify-scaffold,scaffold-cleanup, andset create-uricommands, including support for named scaffolds,file://scaffolds, and HTTPS-only remote scaffolds with explicit user confirmation.Introduces a YAML-based
SCAFFOLD-MANIFEST.ymlrunner (runshell steps +promptsteps executed via a configurable agent) with stricter validation/safe YAML parsing, plus GitHub repo URL resolution to latest release tarballs and a.aidd/working directory cleanup flow.Updates installation flow and docs to include Claude Code support (
--claude+.claudesymlink), automaticCLAUDE.mdcreation, new agent skill/aidd-fix, scaffold authoring documentation, and adjusts the pre-commit hook to run unit tests only (E2E run manually).Written by Cursor Bugbot for commit f355dc3. This will update automatically on new commits. Configure here.