Skip to content

feat: arcanetmpl cli tool#121

Open
kmendell wants to merge 1 commit into
mainfrom
feat/cli
Open

feat: arcanetmpl cli tool#121
kmendell wants to merge 1 commit into
mainfrom
feat/cli

Conversation

@kmendell
Copy link
Copy Markdown
Member

@kmendell kmendell commented Apr 21, 2026

Disclaimer Greptiles Reviews use AI, make sure to check over its work.

To better help train Greptile on our codebase, if the comment is useful and valid Like the comment, if its not helpful or invalid Dislike

To have Greptile Re-Review the changes, mention greptileai.

Greptile Summary

This PR introduces the arcanetmpl / arcane-templates CLI tool by adding a new scripts/cli.ts entrypoint, extracting the registry generation, validation, staging, and template-scaffolding logic into dedicated scripts/lib/ modules, and wiring them up as named subcommands. The package is made publishable (private: false), bin fields are added, and runtime dependencies (ajv, ajv-formats, prettier) are correctly moved from devDependencies to dependencies.

Confidence Score: 5/5

Safe to merge — all findings are minor style/UX improvements with no correctness impact.

The refactoring cleanly separates concerns, the CLI routing is correct, error handling is preserved from the original scripts, and the publish configuration looks sound. All four comments are P2 (style/quality).

No files require special attention.

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: scripts/cli.ts
Line: 255-293

Comment:
**Redundant double-call pattern for flag reads**

`readStringFlagInternal` is invoked twice for every optional flag — once to test truthiness, then again to retrieve the value. Since it already returns `string | undefined`, the truthiness check is implicit in the ternary. This pattern repeats for `name`, `description`, `author`, `service`, `image`, `website`, and `icon`.

```suggestion
    id,
    name:
      readStringFlagInternal(parsed, "name") ??
      (allowPrompts ? await promptTextInternal("Template name", defaultName) : defaultName),
    description:
      readStringFlagInternal(parsed, "description") ??
      (allowPrompts
        ? await promptTextInternal("Description", defaultDescription)
        : defaultDescription),
    author:
      readStringFlagInternal(parsed, "author") ??
      (allowPrompts ? await promptTextInternal("Author", defaultAuthor) : defaultAuthor),
    tags,
    serviceName:
      readStringFlagInternal(parsed, "service") ??
      (allowPrompts
        ? await promptTextInternal("Service name", defaultService)
        : defaultService),
    image:
      readStringFlagInternal(parsed, "image") ??
      (allowPrompts ? await promptTextInternal("Container image", defaultImage) : defaultImage),
    websiteUrl:
      readStringFlagInternal(parsed, "website") ??
      (allowPrompts ? await promptTextInternal("Website URL", defaultWebsite) : defaultWebsite),
    iconUrl:
      readStringFlagInternal(parsed, "icon") ??
      (allowPrompts ? await promptTextInternal("Icon URL", defaultIcon) : defaultIcon),
    composeFileName,
    includeReadme,
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/cli.ts
Line: 159-197

Comment:
**New readline interface created per prompt**

Both `promptTextInternal` and `promptBooleanInternal` construct and immediately close a `readline.Interface` for each question. When `collectCreateOptionsInternal` chains many prompts, stdin is repeatedly paused/resumed and the `createInterface` overhead is paid multiple times. A single shared interface should be opened at the start and closed after the last prompt.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: package.json
Line: 18

Comment:
**`prepare` script hard-codes `pnpm run build`**

Now that `preinstall: only-allow pnpm` was removed (intentionally for public publishing), contributors who `npm install` or `yarn install` will hit a cryptic error because `prepare` invokes `pnpm run build`. Consider using `tsc --project tsconfig.json` directly in `prepare` so it works regardless of the package manager:

```suggestion
    "prepare": "tsc --project tsconfig.json",
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/lib/validation.ts
Line: 90-127

Comment:
**First compose failure terminates all remaining template validation**

`run()` throws on a non-zero exit code and the error propagates out of the `for` loop, so any single failing template short-circuits validation for all subsequent templates. Users would need to fix each template one at a time and re-run to find the next failure. Consider catching the error, collecting it, and re-throwing after all templates have been checked.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: arcanetmpl cli tool" | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.


Open in Devin Review

@kmendell kmendell marked this pull request as ready for review April 21, 2026 04:43
@kmendell kmendell requested a review from a team April 21, 2026 04:43
Copy link
Copy Markdown
Member Author


How to use the Graphite Merge Queue

Add the label merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@getarcaneappbot
Copy link
Copy Markdown
Contributor

getarcaneappbot commented Apr 21, 2026

Comment thread scripts/cli.ts
Comment on lines +255 to +293
name: readStringFlagInternal(parsed, "name")
? String(readStringFlagInternal(parsed, "name"))
: allowPrompts
? await promptTextInternal("Template name", defaultName)
: defaultName,
description: readStringFlagInternal(parsed, "description")
? String(readStringFlagInternal(parsed, "description"))
: allowPrompts
? await promptTextInternal("Description", defaultDescription)
: defaultDescription,
author: readStringFlagInternal(parsed, "author")
? String(readStringFlagInternal(parsed, "author"))
: allowPrompts
? await promptTextInternal("Author", defaultAuthor)
: defaultAuthor,
tags,
serviceName: readStringFlagInternal(parsed, "service")
? String(readStringFlagInternal(parsed, "service"))
: allowPrompts
? await promptTextInternal("Service name", defaultService)
: defaultService,
image: readStringFlagInternal(parsed, "image")
? String(readStringFlagInternal(parsed, "image"))
: allowPrompts
? await promptTextInternal("Container image", defaultImage)
: defaultImage,
websiteUrl: readStringFlagInternal(parsed, "website")
? String(readStringFlagInternal(parsed, "website"))
: allowPrompts
? await promptTextInternal("Website URL", defaultWebsite)
: defaultWebsite,
iconUrl: readStringFlagInternal(parsed, "icon")
? String(readStringFlagInternal(parsed, "icon"))
: allowPrompts
? await promptTextInternal("Icon URL", defaultIcon)
: defaultIcon,
composeFileName,
includeReadme,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant double-call pattern for flag reads

readStringFlagInternal is invoked twice for every optional flag — once to test truthiness, then again to retrieve the value. Since it already returns string | undefined, the truthiness check is implicit in the ternary. This pattern repeats for name, description, author, service, image, website, and icon.

Suggested change
name: readStringFlagInternal(parsed, "name")
? String(readStringFlagInternal(parsed, "name"))
: allowPrompts
? await promptTextInternal("Template name", defaultName)
: defaultName,
description: readStringFlagInternal(parsed, "description")
? String(readStringFlagInternal(parsed, "description"))
: allowPrompts
? await promptTextInternal("Description", defaultDescription)
: defaultDescription,
author: readStringFlagInternal(parsed, "author")
? String(readStringFlagInternal(parsed, "author"))
: allowPrompts
? await promptTextInternal("Author", defaultAuthor)
: defaultAuthor,
tags,
serviceName: readStringFlagInternal(parsed, "service")
? String(readStringFlagInternal(parsed, "service"))
: allowPrompts
? await promptTextInternal("Service name", defaultService)
: defaultService,
image: readStringFlagInternal(parsed, "image")
? String(readStringFlagInternal(parsed, "image"))
: allowPrompts
? await promptTextInternal("Container image", defaultImage)
: defaultImage,
websiteUrl: readStringFlagInternal(parsed, "website")
? String(readStringFlagInternal(parsed, "website"))
: allowPrompts
? await promptTextInternal("Website URL", defaultWebsite)
: defaultWebsite,
iconUrl: readStringFlagInternal(parsed, "icon")
? String(readStringFlagInternal(parsed, "icon"))
: allowPrompts
? await promptTextInternal("Icon URL", defaultIcon)
: defaultIcon,
composeFileName,
includeReadme,
};
id,
name:
readStringFlagInternal(parsed, "name") ??
(allowPrompts ? await promptTextInternal("Template name", defaultName) : defaultName),
description:
readStringFlagInternal(parsed, "description") ??
(allowPrompts
? await promptTextInternal("Description", defaultDescription)
: defaultDescription),
author:
readStringFlagInternal(parsed, "author") ??
(allowPrompts ? await promptTextInternal("Author", defaultAuthor) : defaultAuthor),
tags,
serviceName:
readStringFlagInternal(parsed, "service") ??
(allowPrompts
? await promptTextInternal("Service name", defaultService)
: defaultService),
image:
readStringFlagInternal(parsed, "image") ??
(allowPrompts ? await promptTextInternal("Container image", defaultImage) : defaultImage),
websiteUrl:
readStringFlagInternal(parsed, "website") ??
(allowPrompts ? await promptTextInternal("Website URL", defaultWebsite) : defaultWebsite),
iconUrl:
readStringFlagInternal(parsed, "icon") ??
(allowPrompts ? await promptTextInternal("Icon URL", defaultIcon) : defaultIcon),
composeFileName,
includeReadme,
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/cli.ts
Line: 255-293

Comment:
**Redundant double-call pattern for flag reads**

`readStringFlagInternal` is invoked twice for every optional flag — once to test truthiness, then again to retrieve the value. Since it already returns `string | undefined`, the truthiness check is implicit in the ternary. This pattern repeats for `name`, `description`, `author`, `service`, `image`, `website`, and `icon`.

```suggestion
    id,
    name:
      readStringFlagInternal(parsed, "name") ??
      (allowPrompts ? await promptTextInternal("Template name", defaultName) : defaultName),
    description:
      readStringFlagInternal(parsed, "description") ??
      (allowPrompts
        ? await promptTextInternal("Description", defaultDescription)
        : defaultDescription),
    author:
      readStringFlagInternal(parsed, "author") ??
      (allowPrompts ? await promptTextInternal("Author", defaultAuthor) : defaultAuthor),
    tags,
    serviceName:
      readStringFlagInternal(parsed, "service") ??
      (allowPrompts
        ? await promptTextInternal("Service name", defaultService)
        : defaultService),
    image:
      readStringFlagInternal(parsed, "image") ??
      (allowPrompts ? await promptTextInternal("Container image", defaultImage) : defaultImage),
    websiteUrl:
      readStringFlagInternal(parsed, "website") ??
      (allowPrompts ? await promptTextInternal("Website URL", defaultWebsite) : defaultWebsite),
    iconUrl:
      readStringFlagInternal(parsed, "icon") ??
      (allowPrompts ? await promptTextInternal("Icon URL", defaultIcon) : defaultIcon),
    composeFileName,
    includeReadme,
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment thread scripts/cli.ts
Comment on lines +159 to +197
async function promptTextInternal(
question: string,
fallback: string,
): Promise<string> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});

try {
const response = await rl.question(`${question} [${fallback}]: `);
const trimmed = response.trim();
return trimmed.length > 0 ? trimmed : fallback;
} finally {
rl.close();
}
}

async function promptBooleanInternal(
question: string,
fallback: boolean,
): Promise<boolean> {
const suffix = fallback ? "Y/n" : "y/N";
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});

try {
const response = await rl.question(`${question} [${suffix}]: `);
const normalized = response.trim().toLowerCase();
if (normalized.length === 0) {
return fallback;
}
return normalized === "y" || normalized === "yes";
} finally {
rl.close();
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New readline interface created per prompt

Both promptTextInternal and promptBooleanInternal construct and immediately close a readline.Interface for each question. When collectCreateOptionsInternal chains many prompts, stdin is repeatedly paused/resumed and the createInterface overhead is paid multiple times. A single shared interface should be opened at the start and closed after the last prompt.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/cli.ts
Line: 159-197

Comment:
**New readline interface created per prompt**

Both `promptTextInternal` and `promptBooleanInternal` construct and immediately close a `readline.Interface` for each question. When `collectCreateOptionsInternal` chains many prompts, stdin is repeatedly paused/resumed and the `createInterface` overhead is paid multiple times. A single shared interface should be opened at the start and closed after the last prompt.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment thread package.json
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "tsc --project tsconfig.json",
"prepare": "pnpm run build",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 prepare script hard-codes pnpm run build

Now that preinstall: only-allow pnpm was removed (intentionally for public publishing), contributors who npm install or yarn install will hit a cryptic error because prepare invokes pnpm run build. Consider using tsc --project tsconfig.json directly in prepare so it works regardless of the package manager:

Suggested change
"prepare": "pnpm run build",
"prepare": "tsc --project tsconfig.json",
Prompt To Fix With AI
This is a comment left during a code review.
Path: package.json
Line: 18

Comment:
**`prepare` script hard-codes `pnpm run build`**

Now that `preinstall: only-allow pnpm` was removed (intentionally for public publishing), contributors who `npm install` or `yarn install` will hit a cryptic error because `prepare` invokes `pnpm run build`. Consider using `tsc --project tsconfig.json` directly in `prepare` so it works regardless of the package manager:

```suggestion
    "prepare": "tsc --project tsconfig.json",
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment thread scripts/lib/validation.ts
Comment on lines +90 to +127
for (const dir of templateDirs) {
const templateDir = path.join(TEMPLATES_DIR, dir);
const composeFile = await findComposeFile(templateDir);
const composePath = path.join(templateDir, composeFile);
const envPath = path.join(templateDir, ".env");
const envExamplePath = path.join(templateDir, ".env.example");
const hasEnvFile = await exists(envPath);

if (!hasEnvFile) {
const envExample = await fs.readFile(envExamplePath, "utf8");
await fs.writeFile(envPath, envExample, "utf8");
}

try {
run(
"docker",
[
"compose",
"--project-directory",
templateDir,
"-f",
composePath,
"--env-file",
envPath,
"config",
"-q",
],
templateDir,
);
console.log(
`Compose validation passed for templates/${dir}/${composeFile}`,
);
} finally {
if (!hasEnvFile && (await exists(envPath))) {
await fs.rm(envPath);
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 First compose failure terminates all remaining template validation

run() throws on a non-zero exit code and the error propagates out of the for loop, so any single failing template short-circuits validation for all subsequent templates. Users would need to fix each template one at a time and re-run to find the next failure. Consider catching the error, collecting it, and re-throwing after all templates have been checked.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/lib/validation.ts
Line: 90-127

Comment:
**First compose failure terminates all remaining template validation**

`run()` throws on a non-zero exit code and the error propagates out of the `for` loop, so any single failing template short-circuits validation for all subsequent templates. Users would need to fix each template one at a time and re-run to find the next failure. Consider catching the error, collecting it, and re-throwing after all templates have been checked.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread package.json
Comment on lines +33 to +36
"dependencies": {
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1"
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Published CLI will crash because prettier is missing from dependencies

prettier is imported at the top level in both scripts/lib/registry.ts:4 and scripts/lib/template-scaffold.ts:3, but it is only listed in devDependencies, not in dependencies. Since the package is now public ("private": false) with bin entries pointing to dist/scripts/cli.js, anyone installing via npx arcane-templates or npm install -g arcane-templates will not get prettier installed (npm only installs dependencies, not devDependencies for published packages). Because cli.ts eagerly imports all modules at the top level, the import prettier from "prettier" statement will fail on module load, crashing every CLI command — not just generate and create which actually use prettier, but also help, validate, and stage.

Suggested change
"dependencies": {
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1"
},
"dependencies": {
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"prettier": "^3.8.3"
},
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants