feat(scaffold): add create, verify-scaffold, scaffold-cleanup, and set commands#124
feat(scaffold): add create, verify-scaffold, scaffold-cleanup, and set commands#124ericelliott wants to merge 5 commits intomainfrom
create, verify-scaffold, scaffold-cleanup, and set commands#124Conversation
Add manifest-driven scaffolding lib modules and fixtures: - lib/scaffold-resolver.js — resolves named, file://, and https:// sources - lib/scaffold-runner.js — parses and executes SCAFFOLD-MANIFEST.yml steps - lib/scaffold-verifier.js — pre-flight manifest validation - lib/scaffold-cleanup.js — removes .aidd/ working directory - lib/scaffold-create.js — orchestrates the create flow - lib/scaffold-verify-cmd.js — verify-scaffold handler - lib/scaffold-errors.js — typed error definitions - lib/aidd-config.js — reads/writes ~/.aidd/config.yml - bin/create-e2e.test.js — e2e tests for create command - ai/scaffolds/scaffold-example — example scaffold fixture - ai/scaffolds/next-shadcn — next.js + shadcn scaffold - docs/scaffold-authoring.md — scaffold authoring guide - tasks/npx-aidd-create-epic.md — task epic
- lib/scaffold-commands.js: registerScaffoldCommands(program) registers create, verify-scaffold, scaffold-cleanup, and set subcommands - lib/scaffold-commands.test.js: smoke tests confirm all four command names and key options are registered on the Commander program - bin/aidd.js: one import + one call (no other changes) - package.json: add js-yaml dep, @types/js-yaml devDep, engines.node>=18
Previously, resolveExtension used path.join(folder, '.aidd/scaffold') as the download directory, creating two separate targets: - create: downloaded to <project>/.aidd/scaffold - verify-scaffold: worked around this by passing os.homedir() as folder Now downloads always go to ~/.aidd/scaffold (SCAFFOLD_DOWNLOAD_DIR), injectable via scaffoldDownloadDir for test isolation. Changes: - scaffold-resolver.js: add SCAFFOLD_DOWNLOAD_DIR constant, replace folder arg with scaffoldDownloadDir, remove ensureDir(folder) from http branch - scaffold-cleanup.js: target ~/.aidd/scaffold via scaffoldDir arg (injectable) - scaffold-verify-cmd.js: remove os.homedir() workaround, remove unused import - scaffold-create.js: remove folder from resolveExtensionFn call, simplify cleanupTip to 'npx aidd scaffold-cleanup' (no path needed) - scaffold-commands.js: remove [folder] arg from scaffold-cleanup command Tests updated to match new behavior throughout.
…old target scaffold-cleanup no longer accepts a folder arg — it always targets ~/.aidd/scaffold. Update e2e tests to seed and assert against that path instead of <folder>/.aidd/.
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 isHttpUrl = (url) => | ||
| url.startsWith("http://") || url.startsWith("https://"); | ||
| const isInsecureHttpUrl = (url) => url.startsWith("http://"); | ||
| const isFileUrl = (url) => url.startsWith("file://"); |
There was a problem hiding this comment.
Case-sensitive URL checks inconsistent with argument parser
Low Severity
The URL detection helpers isHttpUrl, isInsecureHttpUrl, and isFileUrl in scaffold-resolver.js use case-sensitive startsWith checks (e.g., url.startsWith("https://")), while resolveCreateArgs in scaffold-create.js uses a case-insensitive regex (/^(https?|file):\/\//i). If a user passes a mixed-case URL like HTTPS://example.com as the type argument with a folder, resolveCreateArgs accepts it, but resolveExtension fails to recognize it as a URL and falls through to the resolveNamed path, producing a confusing "SCAFFOLD-MANIFEST.yml not found" error instead of a proper network/validation flow.
Additional Locations (1)
| assert({ | ||
| given: "scaffold-example scaffold runs", | ||
| should: "install @paralleldrive/cuid2 as a dev dependency", | ||
| actual: "@paralleldrive/cuid2" in devDeps, |
There was a problem hiding this comment.
Instead of asserting each key one at a time, why not have one big expected key that is full json? This means we can condense many of the asserts here into one.
There was a problem hiding this comment.
-
Collapse the 7 devDependency tests into a parameterized loop or single assertion. This eliminates ~80 lines of near-identical code and makes adding new expected dependencies a one-line change.
-
Isolate
scaffold-cleanuptests from real home directory. Consider usingXDG_DATA_HOMEor an env var to redirect~/.aiddto a temp directory during tests. Touching real user paths is risky.
Medium priority:
-
Add error-path tests: What happens when the target directory already exists? When the scaffold name is invalid? When the
file://URI doesn't exist? These are the most likely real-world failure modes. -
Verify
--agentactually invokes the agent. Capture stdout from theechoagent and assert it contains the prompt string"hello world"— otherwise the test only proves the directory was created, not that the agent ran.
Low priority:
-
Consider replacing
execwithexecFilein tests to avoid shell interpolation (and recommend the same for the production CLI if it usesexec). -
The
beforeAll/afterAllpattern is pragmatically fine for the slow scaffold-example tests, but thebeforeEach/afterEachinscaffold-cleanupand--agentcould be refactored to use a localsetup()+onTestFinishedpattern to follow the RITEway principles (explicit - no beforeEach etc.).
| let tempDir; | ||
| let configFile; | ||
|
|
||
| beforeEach(async () => { | ||
| tempDir = path.join(os.tmpdir(), `aidd-config-test-${Date.now()}`); | ||
| await fs.ensureDir(tempDir); | ||
| configFile = path.join(tempDir, "config.yml"); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await fs.remove(tempDir); | ||
| }); |
There was a problem hiding this comment.
Should we refactor this as a setup function with onTestFinished and call it in each test to keep them explicit and avoid shared mutable state?
There was a problem hiding this comment.
- [High] Remove
@ts-nocheck— Fix type errors with targeted assertions instead of blanket suppression. This is the single most impactful improvement. - [Medium] Avoid boolean-derived assertions —
actual: agentOption !== undefinedtests shape, not behavior. Consider testing the option's properties directly (e.g., assert on the option'slongvalue or flags string) rather than a boolean existence check. - [Low] Mocking note — The five
vi.mockcalls are purely defensive. IfregisterScaffoldCommandscould be refactored to accept action handlers via dependency injection, the mocks could be eliminated entirely. Not urgent, but worth keeping in mind for future refactors.
| assert({ | ||
| given: "an https:// URL as type and an explicit folder", | ||
| should: "set type to the URL", | ||
| actual: result.type, | ||
| expected: "https://github.com/org/repo", | ||
| }); | ||
| assert({ | ||
| given: "an https:// URL as type and an explicit folder", | ||
| should: "set resolvedFolder to the folder argument", | ||
| actual: result.resolvedFolder, | ||
| expected: "my-project", | ||
| }); |
There was a problem hiding this comment.
Again here, should we collapse assertions like this on one expected object?
There was a problem hiding this comment.
-
Remove dead code: Delete the unused
__filenamebinding. -
Add tarball integrity verification: After downloading, verify a SHA-256 checksum if one is available in the release metadata. This addresses A03/A08.
-
Consistent error typing: Consider wrapping errors from
defaultResolveReleaseanddefaultDownloadAndExtractwithcreateErrorand the appropriate error types at the point of creation, not just at the catch site inresolveExtension. -
Guard double-reject in tar extraction: Use a
settledflag orPromise.withResolvers()to ensure only the first error triggers rejection:let settled = false; const settle = (fn) => (...args) => { if (!settled) { settled = true; fn(...args); } }; child.on("close", (code) => { ... settle(resolve)() }); child.on("error", settle(reject)); child.stdin.on("error", settle(reject));
-
isGitHubRepoUrledge case: A URL likehttps://github.com/owner/repo/(trailing slash) passes becausefilter(Boolean)strips the empty segment. This is correct behavior, but a URL likehttps://github.com/owner/repo.gitwould also pass withparts.length === 2— is that intentional? If not, consider stripping.gitsuffixes or explicitly checking. Make sure we have unit tests that explicitly define the behavior here. -
Consider adding a JSDoc for
resolveExtension— it's the main public API of this module, and its options object has many parameters with non-obvious interactions (fallback chain, DI overrides).
There was a problem hiding this comment.
Similar here.
One of the most common "slop" that agents produce is too granular tests. Meaning, if they have an object:
const expectedValue = {
foo: true,
bar: "hello"
}
They frequently test:
assert({
// ...
expected: expectedValue.foo
});
etc. instead of
assert({
// ...
expected: {
foo: true,
bar: "hello"
}
});
We should probably add a constrain to our review & implementation skills / rules for that.
There was a problem hiding this comment.
Same here, need to collapse the "slop" tests.
|
|
||
| const manifestExists = await fs.pathExists(manifestPath); | ||
| if (!manifestExists) { | ||
| errors.push(`SCAFFOLD-MANIFEST.yml not found: ${manifestPath}`); |
There was a problem hiding this comment.
Is it intentional to NOT use error-causes here?
| let tempDir; | ||
|
|
||
| beforeEach(async () => { | ||
| tempDir = path.join(os.tmpdir(), `aidd-verifier-test-${Date.now()}`); | ||
| await fs.ensureDir(tempDir); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await fs.remove(tempDir); | ||
| }); |
There was a problem hiding this comment.
Same as above. Can be made explicit with onTestFinished.
|
@ericelliott Also going forward, we should probably break these 4k+ LoC PRs down. |


Summary
Adds manifest-driven project scaffolding to the
aiddCLI via a single newlib/scaffold-commands.jsmodule.bin/aidd.jsgains one import and one call.All remote scaffolds are downloaded to
~/.aidd/scaffold— a single, predictablelocation regardless of which command triggered the download.
New commands
npx aidd create [type] <folder>— scaffold from a named scaffold,file://, orhttps://npx aidd verify-scaffold [type]— validate a scaffold manifest before running itnpx aidd scaffold-cleanup— remove~/.aidd/scaffoldafter scaffoldingnpx aidd set create-uri <uri>— persist a default scaffold URI to~/.aidd/config.ymlNew modules
lib/scaffold-commands.jslib/scaffold-resolver.jsfile://, andhttps://sources; always downloads to~/.aidd/scaffoldlib/scaffold-runner.jsSCAFFOLD-MANIFEST.ymlstepslib/scaffold-verifier.jslib/scaffold-cleanup.js~/.aidd/scaffoldlib/scaffold-create.jslib/scaffold-verify-cmd.jsverify-scaffoldcommand handlerlib/scaffold-errors.jslib/aidd-config.js~/.aidd/config.ymlChanges to
bin/aidd.jsExactly two lines added — one import, one call: