Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
aefd96b
Fix editor modal a11y issues
myovchev May 14, 2026
c5458b8
Fix manager a11y problems
myovchev May 14, 2026
ee474a8
Fix page manager a11y problems
myovchev May 14, 2026
89c8650
fix media manager a11y issues
myovchev May 14, 2026
bedd312
fix a11y issues in style editor and user settings
myovchev May 14, 2026
cbe9437
Fix login a11y issues
myovchev May 14, 2026
61191e9
eliminate a modal issue
myovchev May 14, 2026
ef751fd
Remove bad aria in rich text
myovchev May 14, 2026
96bd7ec
Fix wrong aria in layout
myovchev May 14, 2026
3f46cfd
changelog
myovchev May 14, 2026
0001bc3
Fix totp a11y issues, doc context state safety
myovchev May 15, 2026
c9823af
create apostrophe skeleton
myovchev May 18, 2026
b813e54
core logic, missing core envs
myovchev May 19, 2026
d0d2da2
Merge branch 'main' into feature-cli
myovchev May 19, 2026
31b572e
telemetry phase
myovchev May 20, 2026
acfa7eb
UI layer, CLI entrypoint
myovchev May 20, 2026
fc1dd92
UI cleanup, edge cases
myovchev May 21, 2026
1c445d8
Additional UI testing
myovchev May 21, 2026
df1b999
Sync minor requirements
myovchev May 21, 2026
eb3c3a7
Wire umami transport
myovchev May 21, 2026
d66a537
Adapt the timeout
myovchev May 21, 2026
79f6e76
Expose cli runner for integration
myovchev May 21, 2026
d3f4e36
Expose interactive flow (UI + logic)
myovchev May 21, 2026
1efa496
Delegate interactive installer, bump major version
myovchev May 21, 2026
3a6d5c7
Expose and wire the telemetry command, better preview UX
myovchev May 21, 2026
4be542d
sample data schema + spec
myovchev May 22, 2026
0cd5c7a
DB core installer helper, db-connect dependency
myovchev May 22, 2026
1d0e326
Adapt db flow connection detection
myovchev May 22, 2026
5314bf2
Drop db UI flow
myovchev May 22, 2026
72ffdf0
Import sample data implementation
myovchev May 22, 2026
b30aff9
telemetry subcommands tests
myovchev May 22, 2026
89a32ae
Sample data import done
myovchev May 22, 2026
2e16069
Docs, kill-switch bug, license
myovchev May 27, 2026
3f0ed73
Ignore dev specs
myovchev May 27, 2026
042b661
Switch to initial v1.0.0
myovchev May 27, 2026
cefdb7b
Fix edge case reporting issue
myovchev Jun 2, 2026
06e460e
Hardening input validation
myovchev Jun 2, 2026
830c43a
CLI minor bump
myovchev Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/env-secrets-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apostrophe": minor
---

The session secret and the uploadfs `disabledFileKey` can now be supplied via the `APOS_SESSION_SECRET` and `APOS_UPLOADFS_DISABLED_FILE_KEY` environment variables. As with other Apostrophe environment variables, these take precedence over the corresponding `app.js` configuration.
6 changes: 6 additions & 0 deletions .changeset/soft-hats-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@apostrophecms/cli": major
---

**Breaking:** `apos create` is now an interactive guided installer (it delegates to `create-apostrophe`). The `<shortname>` positional argument and the `--starter` and `--mongodb-uri` options have been removed - project name, starter kit, and database are now chosen through prompts. For scripted installs, use `npm create apostrophe@latest -- --unattended` instead.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ coverage/
.nyc_output
claude-tools/logs/
.claude
specs/
2 changes: 2 additions & 0 deletions packages/apostrophe/modules/@apostrophecms/express/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,8 @@ module.exports = {
name: self.apos.shortName + '.sid',
cookie: {}
});
// Env overrides config, per Apostrophe convention.
sessionOptions.secret = process.env.APOS_SESSION_SECRET || sessionOptions.secret;
_.defaults(sessionOptions.cookie, {
path: '/',
httpOnly: true,
Expand Down
3 changes: 3 additions & 0 deletions packages/apostrophe/modules/@apostrophecms/uploadfs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ module.exports = {
const uploadfsSettings = {};
_.merge(uploadfsSettings, uploadfsDefaultSettings);
_.merge(uploadfsSettings, options);
if (process.env.APOS_UPLOADFS_DISABLED_FILE_KEY) {
uploadfsSettings.disabledFileKey = process.env.APOS_UPLOADFS_DISABLED_FILE_KEY;
}
if (process.env.APOS_S3_BUCKET) {
_.merge(uploadfsSettings, {
backend: 's3',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/bin/apostrophe
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ async function execute () {
util.checkDependencies();

require('../lib/commands/create')(program);
require('../lib/commands/telemetry')(program);
require('../lib/commands/add')(program, versionInfo.core);

await confUtils.checkConf();
Expand Down
205 changes: 8 additions & 197 deletions packages/cli/lib/commands/create.js
Original file line number Diff line number Diff line change
@@ -1,202 +1,13 @@
require('shelljs/global');
// Utilities from shelljs
/* globals cd rm */
const prompts = require('prompts');
const path = require('path');
const util = require('../util');
const config = require('../../config');
const fs = require('fs');
const { stripIndent } = require('common-tags');
// Thin delegate: hands off to create-apostrophe's guided installer. Owns
// no UI of its own — Commander supplies `--help` for this subcommand.

module.exports = function (program) {
program
.command('create <shortname-without-spaces>')
.description(
stripIndent`
Create an Apostrophe project (using the apostrophe essentials starter kit by default).
Example: \`apos create my-website\`
`
)
.option(
'--starter <url>',
'Use a specific git repository to use as the project starter.\n You can also use the short name of any Apostrophe starter kit, e.g. "ecommerce"'
)
.option(
'--mongodb-uri <url>',
'Add a connection string to connect to a hosted MongoDB instance.'
)
.action(async function (shortName, options) {
if (!/^[\w-]+$/.test(shortName)) {
await util.error('create', 'The shortname must only contain letters, numbers, hyphens, and underscores.');
return false;
}

// If options.starter is undefined, use config.BOILERPLATE
const input = options.starter ? options.starter : config.BOILERPLATE;

// Check for complete repo URL, starterkit name, or incomplete URL fallback
let boilerplateUrl;
if (/^\w+:/.test(input)) {
boilerplateUrl = input;
} else if (input.includes('/')) {
boilerplateUrl = `https://github.com/${
input.startsWith('/') ? input.slice(1) : input
}`;
} else {
boilerplateUrl = `https://github.com/apostrophecms/starter-kit-${input}.git`;
}
util.log(
'create',
`Grabbing the ${boilerplateUrl} starter from Github [1/4]`
);

// Clone the boilerplate project
if ((await util.spawnSafe('git', [ 'clone', boilerplateUrl, shortName ])).code !== 0) {
await util.error('create', 'Error cloning starter code.');
return false;
}

cd(shortName);

// Remove the initial .git directory.
rm('-rf', '.git/');

// Auto-detect Astro projects by checking for backend directory
const isAstroProject = fs.existsSync('backend');
const backendPath = isAstroProject ? 'backend' : '.';
const resolvePath = (relativePath) => {
if (backendPath === '.') {
return relativePath;
}
return path.join(backendPath, relativePath);
};

util.log('create', `Adding your project shortname (${shortName}) [2/4]`);

// Do some token replaces to rename the project
// replaces the shortname in app.js
replaceInConfig(/(shortName:).*?,/gi, `$1 '${shortName}',`, resolvePath);
// replaces the shortname in package.json
replaceInConfig(/("name":).*?,/g, `$1 "${shortName}",`, resolvePath);

// Generate session secret
let secret = util.secret();

const expressIndexPath = resolvePath('lib/modules/apostrophe-express/index.js');
if (fs.existsSync(expressIndexPath)) {
util.replaceInFiles(
[ expressIndexPath ],
/secret: undefined/,
`secret: '${secret}'`
);
}

// Set disabledFileKey for uploadfs
secret = util.secret();

util.replaceInFiles(
[ resolvePath('app.js') ],
/disabledFileKey: undefined/,
`disabledFileKey: '${secret}'`
);

// Remove lock file and install packages.
util.log('create', 'Installing packages [3/4]');

const packageLockPath = resolvePath('package-lock.json');
const yarnLockPath = resolvePath('yarn.lock');

if (fs.existsSync(packageLockPath)) {
rm(packageLockPath);
}
if (fs.existsSync(yarnLockPath)) {
rm(yarnLockPath);
}

// Run npm install in the appropriate directories
if (isAstroProject) {
// Install backend packages
try {
await util.spawnWithSpinner('npm install --prefix backend', {
spinnerMessage:
'Installing backend packages. This will take a little while...'
});
} catch (error) {
await util.error('create', 'Error installing backend packages');

console.error(error);
}

// Install frontend packages
try {
await util.spawnWithSpinner('npm install --prefix frontend', {
spinnerMessage:
'Installing frontend packages. This will take a little while...'
});
} catch (error) {
await util.error('create', 'Error installing frontend packages');

console.error(error);
}
} else {
try {
await util.spawnWithSpinner('npm install', {
spinnerMessage:
'Installing packages. This will take a little while...'
});
} catch (error) {
await util.error('create', 'Error installing packages');

console.error(error);
}
}

const cwd = process.cwd();
const aposPath = path.join(cwd, resolvePath('node_modules/apostrophe'));

// Append the installed Apostrophe version, if in an active project.
if (!fs.existsSync(aposPath)) {
await util.error('create', 'Error installing new project packages.');
return false;
}
const version = require(`${aposPath}/package.json`).version;
await util.getMajorVersion('create', version);

// Create an admin user (note this will prompt for password)
util.log('create', 'Creating an admin user [4/4]');
util.log('create', 'Choose a password for the admin user');

const response = await prompts({
type: 'password',
name: 'pw',
message: '🔏 Please enter a password:'
});

const userTask = '@apostrophecms/user:add';
const appJsPath = resolvePath('app.js');
const createUserArgs = [ appJsPath, userTask, 'admin', 'admin' ];

const createUserEnv = { ...process.env };
if (options.mongodbUri) {
createUserEnv.APOS_MONGODB_URI = options.mongodbUri;
}
util.log('create', `Creating admin user: node ${createUserArgs.join(' ')}`);
const createUserResult = await util.spawnSafe('node', createUserArgs, {
env: createUserEnv,
input: `${response.pw ?? ''}\n`
});
if (createUserResult.code !== 0) {
await util.error('create', `Admin user creation exited with code ${createUserResult.code}.`);
return false;
}
util.log('create', 'All done! 🎉 Login as "admin" at the /login URL.');

await util.success('create');
return true;
.command('create')
.description('Create an Apostrophe project — launches the guided installer.')
.action(async function () {
const { runInteractive } = await import('create-apostrophe');
const code = await runInteractive();
process.exit(typeof code === 'number' ? code : 0);
});
};

function replaceInConfig(regex, replacement, resolvePath) {
const files = [ resolvePath('app.js'), resolvePath('package.json') ];
util.replaceInFiles(files, regex, replacement);
}
24 changes: 24 additions & 0 deletions packages/cli/lib/commands/telemetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Thin delegate: forwards each `apos telemetry <sub>` to create-apostrophe.
// Commander supplies `--help` for the parent and each subcommand.

module.exports = function (program) {
const telemetry = program
.command('telemetry')
.description('Manage telemetry preference for ApostropheCMS CLI.');

for (const [ sub, summary ] of [
[ 'status', 'Show current telemetry preference.' ],
[ 'on', 'Opt in.' ],
[ 'off', 'Opt out.' ],
[ 'preview', 'Print the exact payload that would be sent.' ]
]) {
telemetry
.command(sub)
.description(summary)
.action(async function () {
const { runTelemetryCommand } = await import('create-apostrophe');
const code = await runTelemetryCommand({ sub });
process.exit(typeof code === 'number' ? code : 0);
});
}
};
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"commander": "^5.1.0",
"common-tags": "^1.8.0",
"conf": "^6.2.4",
"create-apostrophe": "workspace:^",
"lodash": "^4.18.1",
"ora": "^5.4.1",
"pkginfo": "^0.4.1",
Expand Down
9 changes: 9 additions & 0 deletions packages/create-apostrophe/.mocharc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

// ESM specs, discovered recursively under test/. Mocha 11 runs .mjs/.js
// ESM natively (package is "type": "module").
module.exports = {
spec: [ 'test/**/*.test.js' ],
recursive: true,
timeout: 10000
};
Loading
Loading