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 29 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>
…on exit code
- defaultConfirm: add error + close event handlers to readline interface;
promise rejects instead of hanging when stdin closes before answer
- resolveExtension: catch confirm() rejections (stdin-close) and wrap in
ScaffoldCancelledError rather than letting them surface as raw errors
- defaultDownloadAndExtract: add child.stdin.on('error', reject) guard
to prevent EPIPE from crashing Node when tar exits before consuming stdin
- create + verify-scaffold commands: process.exit(0) on ScaffoldCancelledError
(graceful abort is not a failure — exit 1 only for errors)
- Archive completed remediation epic 2 and remove from plan.md
262 unit tests passing.
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 42 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.
Named scaffolds (e.g. scaffold-example) do not download a tarball, so no .aidd/scaffold/ directory is created and the cleanup tip should not appear. Updated the E2E assertion to expect false, matching the conditional-tip behaviour introduced in the remediation pass. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
…n CI runCreate called fs.ensureDir(folder) unconditionally, causing unit tests that pass fake absolute paths (e.g. /abs/, /absolute/) to attempt real directory creation and fail with EACCES in restricted CI environments. Added ensureDirFn injectable parameter (default: fs.ensureDir) and updated all runCreate test callsites to pass noopEnsureDir. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 42 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The implementation auto-resolves https://github.com/owner/repo to the latest release tarball via the GitHub API. The authoring doc still showed the old direct tarball URL format; updated the example and description to match the actual behaviour. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
- Rename AIDD_CUSTOM_EXTENSION_URI → AIDD_CUSTOM_CREATE_URI everywhere - Add lib/aidd-config.js: readConfig/writeConfig for .aidd-config.json - resolveExtension reads config file as fallback (type > env var > config > default) with injectable readConfigFn for unit testing - Add \`npx aidd set <key> <value>\` command (validates key is "create-uri") writes to .aidd-config.json in cwd; readable by subsequent \`npx aidd create\` - 9 new unit tests (4 aidd-config, 2 resolver config-fallback, 2 resolver precedence) https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 40 out of 44 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const defaultResolveRelease = async (repoUrl) => { | ||
| const { pathname } = new URL(repoUrl); | ||
| const [, owner, repo] = pathname.split("/"); | ||
| const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`; | ||
| const response = await fetch(apiUrl, { | ||
| headers: { | ||
| Accept: "application/vnd.github+json", | ||
| "User-Agent": "aidd-cli", | ||
| }, | ||
| }); | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `GitHub API returned ${response.status} for ${repoUrl} — no releases found or repo is private`, | ||
| ); | ||
| } | ||
| const release = await response.json(); | ||
| if (!release.tarball_url) { | ||
| throw new Error(`No tarball URL in latest release of ${repoUrl}`); | ||
| } | ||
| return release.tarball_url; | ||
| }; |
There was a problem hiding this comment.
The GitHub API request does not include authentication, which means it's subject to GitHub's unauthenticated rate limits (60 requests per hour per IP). For a CLI tool that might be used frequently, consider documenting that users can set a GITHUB_TOKEN environment variable for higher rate limits, or handle 403 rate limit responses with a clear error message explaining the limit and suggesting authentication.
|
|
||
| - 🚀 - `npx aidd create` - New `create [type|URI] <folder>` subcommand with manifest-driven scaffolding (run/prompt steps, --agent flag, remote code warning) | ||
| - 🚀 - `npx aidd scaffold-cleanup` - New cleanup subcommand removes `.aidd/` working directory | ||
| - 🔧 - Extension resolver - Supports named scaffolds, `file://`, `http://`, `https://` with confirmation prompt for remote code |
There was a problem hiding this comment.
The activity log states that the extension resolver "Supports named scaffolds, file://, http://, https://" but the implementation at scaffold-resolver.js:177-182 explicitly rejects http:// URIs with an error. The activity log should be updated to remove http:// from the list of supported protocols since only https:// is accepted for security reasons.
| - 🔧 - Extension resolver - Supports named scaffolds, `file://`, `http://`, `https://` with confirmation prompt for remote code | |
| - 🔧 - Extension resolver - Supports named scaffolds, `file://`, `https://` with confirmation prompt for remote code |
| }); | ||
|
|
||
| return verifyScaffoldFn({ manifestPath: paths.manifestPath }); | ||
| }; |
There was a problem hiding this comment.
verify-scaffold leaks downloaded HTTP/HTTPS scaffolds
Medium Severity
The verify-scaffold command downloads HTTP/HTTPS scaffolds to .aidd/scaffold/ in the current directory but never cleans them up. Unlike the create command which suggests running scaffold-cleanup, verify-scaffold silently leaves the downloaded files behind. A verification command should not leave persistent side effects on the filesystem.
Additional Locations (1)
…-custom docs - lib/aidd-config.js: rewrite to use YAML at ~/.aidd/config.yml (token-friendly for AI context); injectable configFile param for testability - lib/aidd-config.test.js: update tests for YAML format + injectable path - bin/aidd.js: apply ~/.aidd/config.yml to process.env at startup so set create-uri acts as a persistent env var; update set command description + output - lib/scaffold-resolver.js: revert readConfigFn — resolveExtension reads only type arg or AIDD_CUSTOM_CREATE_URI env var, no config file lookup - lib/scaffold-resolver.test.js: remove config-fallback tests, restore original two-test shape (renamed env var only) - README.md: add "Customizing aidd Framework for your Project" section documenting aidd-custom/, config.yml, and AGENTS.md; document set create-uri command - AGENTS.md: add "Project Customizations" section referencing aidd-custom/AGENTS.md NOTE: design of set create-uri persistence mechanism still under discussion — user is evaluating whether ~/.aidd/config.yml is the right approach vs eval pattern. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
The set create-uri command no longer writes to ~/.aidd/config.yml. Instead it prints `export AIDD_CUSTOM_CREATE_URI=<uri>` to stdout (machine-readable for eval) with guidance on stderr. Usage: eval "$(npx aidd set create-uri <uri>)" # apply in current shell # or copy the printed export into ~/.bashrc / ~/.zshrc - Delete lib/aidd-config.js and lib/aidd-config.test.js (not needed) - Remove startup config-reading block from bin/aidd.js - set action: stdout = export statement, stderr = chalk guidance - Update README to document eval pattern instead of config file resolveExtension still reads only <type> arg or AIDD_CUSTOM_CREATE_URI env var. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 38 out of 42 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| - `steps` is not an array (e.g. a string or plain object) | ||
| - Any step is not a plain object (e.g. a bare string or `null`) | ||
| - Any step has no recognized keys (`run` or `prompt`) |
There was a problem hiding this comment.
The validation rules documentation at lines 40-44 does not mention that a step with both run and prompt keys is also rejected as ambiguous. However, this validation is implemented in the code (lib/scaffold-runner.js lines 92-97) and documented elsewhere (ai/scaffolds/SCAFFOLD-AUTHORING.md line 42).
Add a fourth bullet point to the validation rules:
- Any step has both
runandpromptkeys (ambiguous step)
| - Any step has no recognized keys (`run` or `prompt`) | |
| - Any step has no recognized keys (`run` or `prompt`) | |
| - Any step has both `run` and `prompt` keys (ambiguous step) |
| ## ⚙️ Customizing aidd Framework for your Project | ||
|
|
||
| After installing the aidd system, create an `aidd-custom/` directory at your project root to extend or override the defaults without touching the built-in `ai/` files. Changes in `aidd-custom/` supersede the project root in case of any conflict. | ||
|
|
||
| ``` | ||
| your-project/ | ||
| ├── ai/ # built-in aidd framework files (don't edit) | ||
| ├── aidd-custom/ # your project-specific customizations | ||
| │ ├── config.yml # project-level aidd settings | ||
| │ └── AGENTS.md # project-specific agent instructions | ||
| └── ... | ||
| ``` | ||
|
|
||
| ### `aidd-custom/config.yml` | ||
|
|
||
| Store project-level aidd settings as YAML (token-friendly for AI context injection): | ||
|
|
||
| ```yaml | ||
| # aidd-custom/config.yml | ||
| stack: next-shadcn | ||
| team: my-org | ||
| ``` | ||
|
|
||
| ### `aidd-custom/AGENTS.md` | ||
|
|
||
| Project-specific instructions for AI agents. Write rules, constraints, and context that apply only to your project. AI agents are instructed to read `aidd-custom/AGENTS.md` after `AGENTS.md` — directives here override the defaults. | ||
|
|
||
| ```markdown | ||
| # Project Agent Instructions | ||
|
|
||
| ## Stack | ||
| We use Next.js 15 App Router with Tailwind CSS and shadcn/ui. | ||
|
|
||
| ## Conventions | ||
| - All server actions live in `lib/actions/` | ||
| - Use `createRoute` from `aidd/server` for API routes | ||
| ``` |
There was a problem hiding this comment.
The README.md introduces a new section "Customizing aidd Framework for your Project" (lines 448-484) describing an aidd-custom/ directory feature that appears to be documented but not implemented in this PR.
The documentation describes aidd-custom/config.yml and aidd-custom/AGENTS.md as a way to extend or override defaults, and AGENTS.md references this feature (lines 47-49). However, there's no code in this PR that actually reads or processes the aidd-custom/ directory.
This creates a discrepancy where the feature is documented but not functional. Either:
- Remove the documentation if the feature is not yet implemented
- Add the implementation to make the feature work as documented
- Clarify that this is a planned feature and mark it accordingly in the documentation
| # Scaffold Authoring Guide | ||
|
|
||
| A scaffold is a small repository that teaches `npx aidd create` how to bootstrap a new project. This guide covers the file layout, manifest format, how to validate your scaffold locally, and how to publish it as a GitHub release so consumers can reference it by URL. | ||
|
|
||
| --- | ||
|
|
||
| ## File layout | ||
|
|
||
| ``` | ||
| my-scaffold/ | ||
| ├── SCAFFOLD-MANIFEST.yml # required — list of steps to execute | ||
| ├── README.md # optional — displayed to the user before steps run | ||
| ├── bin/ | ||
| │ └── extension.js # optional — Node.js script run after all steps | ||
| └── package.json # required for publishing — see below | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## SCAFFOLD-MANIFEST.yml | ||
|
|
||
| The manifest is a YAML file with a single `steps` key containing an ordered list of step objects. Each step must have exactly one of: | ||
|
|
||
| | Key | Type | Description | | ||
| |-----|------|-------------| | ||
| | `run` | string | Shell command executed in the project directory | | ||
| | `prompt` | string | Sent to the configured AI agent CLI (default: `claude`) | | ||
|
|
||
| ```yaml | ||
| steps: | ||
| - run: npm init -y | ||
| - run: npm pkg set scripts.test="vitest run" | ||
| - run: npm pkg set scripts.release="release-it" | ||
| - run: npm install --save-dev vitest@latest release-it@latest | ||
| - prompt: Set up a basic project structure with src/ and tests/ | ||
| ``` | ||
|
|
||
| ### Validation rules | ||
|
|
||
| `npx aidd create` validates the manifest before executing any steps. Your manifest will be rejected with a clear error if: | ||
|
|
||
| - `steps` is not an array (e.g. a string or plain object) | ||
| - Any step is not a plain object (e.g. a bare string or `null`) | ||
| - Any step has no recognized keys (`run` or `prompt`) | ||
|
|
||
| Run `npx aidd verify-scaffold <name-or-uri>` at any time to check your manifest without executing it: | ||
|
|
||
| ```bash | ||
| # Verify a named built-in scaffold | ||
| npx aidd verify-scaffold scaffold-example | ||
|
|
||
| # Verify a local scaffold by file URI | ||
| npx aidd verify-scaffold file:///path/to/my-scaffold | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## The `package.json` `files` array vs GitHub release assets | ||
|
|
||
| These two concepts are independent: | ||
|
|
||
| ### `files` in `package.json` → controls npm publishing | ||
|
|
||
| When you run `npm publish`, npm reads the `files` array to decide which paths are included in the package tarball uploaded to the npm registry. Files not listed here are excluded from `npm install`. | ||
|
|
||
| ```json | ||
| { | ||
| "files": [ | ||
| "SCAFFOLD-MANIFEST.yml", | ||
| "README.md", | ||
| "bin/**/*" | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ### GitHub release assets → controlled by git + release workflow | ||
|
|
||
| A GitHub release contains: | ||
|
|
||
| 1. **Auto-generated source tarballs** (`Source code (zip)` / `Source code (tar.gz)`) — these are snapshots of everything in the git repository at the tagged commit. The `files` array in `package.json` has **no effect** on this. | ||
| 2. **Manually uploaded release assets** — anything you explicitly upload via the GitHub UI or a release workflow step. | ||
|
|
||
| For scaffold distribution, consumers download the source tarball from a GitHub release. This means every file you commit to the repository at the release tag will be available. The `files` array only matters if you also publish the scaffold to npm. | ||
|
|
||
| --- | ||
|
|
||
| ## Adding a release command to your scaffold's `package.json` | ||
|
|
||
| Include `release-it` as a dev dependency and wire up a `release` script: | ||
|
|
||
| ```json | ||
| { | ||
| "name": "my-aidd-scaffold", | ||
| "version": "1.0.0", | ||
| "type": "module", | ||
| "scripts": { | ||
| "release": "release-it" | ||
| }, | ||
| "files": [ | ||
| "SCAFFOLD-MANIFEST.yml", | ||
| "README.md", | ||
| "bin/**/*" | ||
| ], | ||
| "devDependencies": { | ||
| "release-it": "latest" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Running `npm run release` will: | ||
|
|
||
| 1. Bump the version | ||
| 2. Create a git tag (`v1.0.0`) | ||
| 3. Push the tag to GitHub | ||
| 4. Create a GitHub release with auto-generated release notes | ||
|
|
||
| Scaffold consumers can then reference your scaffold by its GitHub release tarball URL: | ||
|
|
||
| ```bash | ||
| npx aidd create https://github.com/your-org/my-scaffold my-project | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Distributing via GitHub releases (recommended) vs git clone | ||
|
|
||
| | | GitHub release tarball | git clone | | ||
| |---|---|---| | ||
| | **Versioned** | Yes — pinned to a tag | No — always HEAD | | ||
| | **Reproducible** | Yes | No | | ||
| | **Download size** | Small — source only | Large — includes git history | | ||
| | **No git required on consumer** | Yes (HTTP download) | No (requires git) | | ||
|
|
||
| The AIDD resolver will download and extract the release tarball rather than cloning the repository, giving users a fast, versioned, reproducible scaffold install. | ||
|
|
||
| --- | ||
|
|
||
| ## Testing your scaffold locally | ||
|
|
||
| Use a `file://` URI to test your scaffold without publishing: | ||
|
|
||
| ```bash | ||
| npx aidd verify-scaffold file:///path/to/my-scaffold | ||
| npx aidd create file:///path/to/my-scaffold my-test-project | ||
| ``` |
There was a problem hiding this comment.
There are two very similar scaffold authoring documentation files: docs/scaffold-authoring.md and ai/scaffolds/SCAFFOLD-AUTHORING.md. Both files cover the same topic with nearly identical content but slightly different organization and wording. This creates a maintenance burden as changes need to be kept in sync across both files, and developers may be unsure which one to reference or update.
Consider consolidating these into a single canonical documentation file, or clearly differentiate their purposes (e.g., one for external users vs. one for internal contributors) with appropriate cross-references.
lib/scaffold-resolver.js
Outdated
| // Reject path traversal: the resolved directory must stay inside scaffoldsRoot | ||
| if (!typeDir.startsWith(scaffoldsRoot + path.sep)) { |
There was a problem hiding this comment.
The path traversal validation at line 121 has a potential edge case issue. The check uses !typeDir.startsWith(scaffoldsRoot + path.sep) which would incorrectly reject a valid scaffold type when typeDir equals scaffoldsRoot exactly (i.e., when type is an empty string or ".").
For example, if type is "." or "", path.resolve(scaffoldsRoot, type) would return scaffoldsRoot itself, which would not start with scaffoldsRoot + path.sep.
The condition should be !typeDir.startsWith(scaffoldsRoot + path.sep) && typeDir !== scaffoldsRoot or use a more robust path containment check like:
const relative = path.relative(scaffoldsRoot, typeDir);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw createError(...);
}However, this may be intentional behavior (rejecting empty scaffold names), in which case an explicit validation and error message for empty types would be clearer.
| // Reject path traversal: the resolved directory must stay inside scaffoldsRoot | |
| if (!typeDir.startsWith(scaffoldsRoot + path.sep)) { | |
| // Explicitly reject empty or "." scaffold types for clarity | |
| if (!type || type === ".") { | |
| throw createError({ | |
| ...ScaffoldValidationError, | |
| message: `Invalid scaffold type "${type}": scaffold type name must be non-empty and not ".".`, | |
| }); | |
| } | |
| // Reject path traversal: the resolved directory must stay inside scaffoldsRoot | |
| const relative = path.relative(scaffoldsRoot, typeDir); | |
| if (relative.startsWith("..") || path.isAbsolute(relative)) { |
…lver Priority chain: CLI <type> arg > AIDD_CUSTOM_CREATE_URI env var > ~/.aidd/config.yml > default Key design decision: config is read directly by resolveExtension (via injectable readConfigFn) rather than being applied to process.env at startup. The env var remains a distinct, higher-priority override for CI and one-off use. Changes: - lib/aidd-config.js: readConfig/writeConfig for ~/.aidd/config.yml using YAML (token-friendly for AI context); injectable configFile param for testability - lib/aidd-config.test.js: 8 tests covering read/write/merge/overwrite/dir-creation - lib/scaffold-resolver.js: add readConfigFn = readConfig param; effectiveType now reads type || AIDD_CUSTOM_CREATE_URI || config["create-uri"] || DEFAULT_SCAFFOLD_TYPE - lib/scaffold-resolver.test.js: add config-fallback and env-var-precedence tests; inject readConfigFn: noConfig in existing tests to prevent real config interference - bin/aidd.js: set command writes to ~/.aidd/config.yml via writeConfig; no startup env injection (config is consumed directly in the resolver) - tasks/npx-aidd-create-epic.md: document rename, set command, architectural decision - README.md: document set create-uri, priority chain, ~/.aidd/config.yml https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
| const folderPath = path.resolve(process.cwd(), resolvedFolder); | ||
|
|
||
| return { folderPath, resolvedFolder, type }; | ||
| }; |
There was a problem hiding this comment.
URL-only argument silently treated as folder name
Medium Severity
resolveCreateArgs lacks a heuristic to detect when a single argument is a URL/URI rather than a folder name. If a user runs npx aidd create https://github.com/org/repo (forgetting the folder), the URL is silently interpreted as the folder name — creating a directory with a mangled URL path and scaffolding the wrong (default) scaffold into it. A check for http://, https://, or file:// prefixes on the single-arg path would catch this and return null (triggering the missing-folder error) or treat it as the type.
Additional Locations (1)
| should: "resolve to next-shadcn named scaffold", | ||
| actual: paths.readmePath.includes("next-shadcn"), | ||
| expected: true, | ||
| }); |
There was a problem hiding this comment.
Env var restoration sets string "undefined" polluting tests
Low Severity
The env var restore pattern process.env.AIDD_CUSTOM_CREATE_URI = originalEnv sets the variable to the literal string "undefined" when originalEnv was undefined, because process.env coerces values to strings. This leaves a truthy env var that pollutes subsequent tests. The correct approach is to conditionally delete the env var when the original was undefined. Additionally, these tests don't use try/finally for cleanup, so a test failure skips restoration entirely.
Additional Locations (1)
…ify isInsecureHttpUrl
- AGENTS.md: note ~/.aidd/config.yml as user-level config distinct from project-level
aidd-custom/config.yml; instruct agents not to modify it without being asked
- README.md: add ~/.aidd/config.yml YAML example alongside set create-uri command
- docs/scaffold-authoring.md: fix "tarball URL" prose — users pass a bare repo URL;
the resolver auto-resolves to the latest release tarball internally
- tasks/npx-aidd-create-epic.md: same prose fix in scaffold author release workflow
- lib/scaffold-resolver.js: simplify isInsecureHttpUrl — url.startsWith("http://")
can never also start with "https://", so the second check was always redundant
https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
verify-scaffold was silently leaving downloaded HTTP/HTTPS scaffold files in .aidd/scaffold/ of the current working directory, which may not even exist yet at verification time (it's a pre-create check). Changes: - lib/scaffold-verify-cmd.js: pass os.homedir() as folder so the resolver puts downloads at ~/.aidd/scaffold/ (i.e. path.join(AIDD_HOME, "scaffold")); wrap resolve+verify in try/finally to always call cleanupFn regardless of outcome; export VERIFY_SCAFFOLD_DIR for test assertions - lib/scaffold-verify-cmd.test.js: inject cleanupFn: noCleanup in all existing tests; add 3 new tests — folder is os.homedir(), cleanup called on success, cleanup called even when resolveExtension throws - tasks/npx-aidd-create-epic.md: document fix with requirements https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
…leading error for type "." type "." resolves to scaffoldsRoot itself (not outside it), so the old startsWith(scaffoldsRoot + sep) message "Resolved outside the scaffolds directory" was factually wrong. Using path.relative() cleanly separates the two failure modes with accurate messages. - !relative (e.g. type "."): "resolves to the scaffolds root directory, not a specific scaffold" - relative starts with ".." or is absolute: "resolves outside the scaffolds directory" - valid type (e.g. "next-shadcn"): passes unchanged Note: type "" is treated as falsy by the || fallback chain and uses the default scaffold — not a bug, intentional behavior preserved. tasks/npx-aidd-create-epic.md: document both cases in requirements. lib/scaffold-resolver.test.js: add failing tests first (TDD order), then fix. https://claude.ai/code/session_01VxNxs3Ly9UQh9TZpJpjG63
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 40 out of 44 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (let i = 0; i < steps.length; i++) { | ||
| const step = steps[i]; | ||
|
|
||
| if (step === null || typeof step !== "object" || Array.isArray(step)) { | ||
| throw createError({ | ||
| ...ScaffoldValidationError, | ||
| message: `Manifest step ${i + 1} must be an object, got ${ | ||
| step === null ? "null" : Array.isArray(step) ? "array" : typeof step | ||
| }`, | ||
| }); | ||
| } | ||
|
|
||
| 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}`, | ||
| }); | ||
| } | ||
|
|
||
| if (knownKeys.length > 1) { | ||
| throw createError({ | ||
| ...ScaffoldValidationError, | ||
| message: `Manifest step ${i + 1} has ambiguous keys: ${knownKeys.join(" and ")}. Each step must have exactly one of: run, prompt`, | ||
| }); | ||
| } |
There was a problem hiding this comment.
parseManifest validates the shape of each step, but it doesn’t validate the value types for run/prompt. Because YAML can encode non-strings (arrays/objects/numbers), a manifest could produce spawn argument type errors or unexpected execution semantics. Consider enforcing typeof step.run === "string" and typeof step.prompt === "string" (and rejecting empty strings) as part of validation.
| const manifestExists = await fs.pathExists(manifestPath); | ||
| if (!manifestExists) { | ||
| errors.push("SCAFFOLD-MANIFEST.yml not found at expected path"); | ||
| return { errors, valid: false }; | ||
| } |
There was a problem hiding this comment.
verifyScaffold returns a generic missing-manifest error without including the actual manifestPath. Including the path makes the output actionable (especially for file:// and downloaded scaffolds) and aligns with the goal of “descriptive” verification errors.
| * Throws on resolution errors (cancelled, network, etc.) — caller handles display. | ||
| */ | ||
| const runVerifyScaffold = async ({ | ||
| type, | ||
| packageRoot = __dirname, | ||
| resolveExtensionFn = defaultResolveExtension, | ||
| verifyScaffoldFn = defaultVerifyScaffold, |
There was a problem hiding this comment.
runVerifyScaffold resolves remote scaffolds into process.cwd()/.aidd/scaffold/, which means verify-scaffold can unexpectedly create a .aidd/ directory in whatever folder the user runs it from (and it doesn’t suggest cleanup). Consider downloading into a temp directory and removing it automatically after verification, or at least documenting/surfacing the cleanup path.
| - Given no `<type>` and no `AIDD_CUSTOM_CREATE_URI`, should use the bundled `ai/scaffolds/next-shadcn` extension | ||
| - Given `AIDD_CUSTOM_CREATE_URI` env var is set and no `<type>` arg, should use the env URI (supports `http://`, `https://`, and `file://` schemes) | ||
| - Given `--agent <name>` flag, should use that agent CLI for `prompt` steps (default: `claude`) |
There was a problem hiding this comment.
This epic still claims AIDD_CUSTOM_CREATE_URI supports http:// URIs, but the implementation now rejects insecure HTTP and requires https://. Updating this requirement text will keep the task spec consistent with the current security posture.
|
|
||
| - 🚀 - `npx aidd create` - New `create [type|URI] <folder>` subcommand with manifest-driven scaffolding (run/prompt steps, --agent flag, remote code warning) | ||
| - 🚀 - `npx aidd scaffold-cleanup` - New cleanup subcommand removes `.aidd/` working directory | ||
| - 🔧 - Extension resolver - Supports named scaffolds, `file://`, `http://`, `https://` with confirmation prompt for remote code |
There was a problem hiding this comment.
The activity log says the resolver supports http:// URIs, but the current resolver enforces HTTPS-only for remote scaffolds. Please update this entry to avoid documenting insecure HTTP support.
| - 🔧 - Extension resolver - Supports named scaffolds, `file://`, `http://`, `https://` with confirmation prompt for remote code | |
| - 🔧 - Extension resolver - Supports named scaffolds, `file://` and `https://` (remote scaffolds require HTTPS) with confirmation prompt for remote code |
| "dependencies": { | ||
| "@paralleldrive/cuid2": "^3.1.0", | ||
| "@sinclair/typebox": "^0.34.41", | ||
| "chalk": "^4.1.2", | ||
| "commander": "^11.1.0", | ||
| "error-causes": "^3.0.2", | ||
| "fs-extra": "^11.1.1", | ||
| "gray-matter": "^4.0.3", | ||
| "js-sha3": "^0.9.3" | ||
| "js-sha3": "^0.9.3", | ||
| "js-yaml": "^4.1.1" | ||
| }, |
There was a problem hiding this comment.
lib/scaffold-resolver.js relies on the global fetch, which requires Node 18+. Since package.json doesn’t declare an engines.node constraint, users on older Node versions may get a runtime ReferenceError: fetch is not defined. Consider adding an engines field (or a fetch polyfill dependency) to make the requirement explicit.
| await ensureDirFn(folder); | ||
|
|
||
| const paths = await resolveExtensionFn({ | ||
| folder, | ||
| packageRoot, | ||
| type, | ||
| }); | ||
|
|
There was a problem hiding this comment.
runCreate creates the target folder before calling resolveExtension. If the user cancels a remote scaffold confirmation (or resolution fails), this leaves an empty project directory behind even though no scaffolding occurred. Consider deferring ensureDirFn(folder) until after resolveExtensionFn succeeds (or only creating the folder immediately before runManifestFn).
| await ensureDirFn(folder); | |
| const paths = await resolveExtensionFn({ | |
| folder, | |
| packageRoot, | |
| type, | |
| }); | |
| const paths = await resolveExtensionFn({ | |
| folder, | |
| packageRoot, | |
| type, | |
| }); | |
| await ensureDirFn(folder); |
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.
| readConfigFn: noConfig, | ||
| }); | ||
|
|
||
| process.env.AIDD_CUSTOM_CREATE_URI = originalEnv; |
There was a problem hiding this comment.
Env var restoration pollutes process.env with string "undefined"
Medium Severity
When AIDD_CUSTOM_CREATE_URI was never set, originalEnv is undefined. Assigning process.env.AIDD_CUSTOM_CREATE_URI = undefined in Node.js converts it to the string "undefined" rather than deleting the key. Since "undefined" is truthy, the || fallback chain in resolveExtension treats it as a valid scaffold type, potentially causing subsequent tests in the same process to resolve the wrong scaffold or fail unexpectedly.




Overview
This PR implements the
npx aidd createepic by adding two new CLI subcommands that enable manifest-driven project scaffolding:create [type|URI] <folder>— Scaffolds new projects from named scaffolds, local file:// URIs, or remote HTTP(S) URIsscaffold-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
Medium Risk
Adds new CLI flows that download/extract and execute scaffold-defined commands, so regressions could impact developer machines and DX; mitigated by HTTPS-only, confirmation prompts, manifest validation, and broad test coverage.
Overview
Adds manifest-driven project scaffolding to the CLI via new subcommands:
create(runsSCAFFOLD-MANIFEST.ymlsteps, supports--agent),verify-scaffold(validates a scaffold without running it),scaffold-cleanup(removes.aidd/working dirs), andset(persists defaults to~/.aidd/config.yml).Introduces a scaffold resolver/runner pipeline that supports named scaffolds,
file://URIs, and HTTPS remote sources (including auto-resolving bare GitHub repo URLs to latest release tarballs), with explicit remote-code confirmation, path-traversal guards, safer YAML parsing/validation, and typed error handling; also bundlesnext-shadcn(stub) andscaffold-examplescaffolds plus extensive unit/E2E coverage.Improves developer workflow/docs: pre-commit now runs unit tests only (E2E manual),
AGENTS.md/CLAUDE.mdguidance updated/auto-created, and README/docs expanded forcreate, scaffold authoring, andaidd-custom/overrides.Written by Cursor Bugbot for commit 683d794. This will update automatically on new commits. Configure here.