create-absolutejs 0.13.5 → 0.13.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,9 @@
1
- export declare const initializeGit: (projectName: string) => Promise<void>;
1
+ import type { GithubLinkOption } from '../types';
2
+ type InitializeGitProps = {
3
+ projectName: string;
4
+ githubLink: GithubLinkOption;
5
+ githubRepoUrl: string | undefined;
6
+ githubVisibility: 'public' | 'private' | undefined;
7
+ };
8
+ export declare const initializeGit: ({ projectName, githubLink, githubRepoUrl, githubVisibility }: InitializeGitProps) => Promise<void>;
9
+ export {};
@@ -1,7 +1,45 @@
1
+ import { basename } from 'path';
1
2
  import { spinner } from '@clack/prompts';
2
3
  import { $ } from 'bun';
3
- import { green, red } from 'picocolors';
4
- export const initializeGit = async (projectName) => {
4
+ import { dim, green, red, yellow } from 'picocolors';
5
+ const note = (message) => console.log(`${dim('│')} ${yellow('▲')} ${message}`);
6
+ const connectExistingRepo = async (projectName, repoUrl) => {
7
+ const spin = spinner();
8
+ spin.start(`Connecting to ${repoUrl}…`);
9
+ await $ `git remote add origin ${`${repoUrl}.git`}`
10
+ .cwd(projectName)
11
+ .quiet()
12
+ .nothrow();
13
+ const push = await $ `git push -u origin main`
14
+ .cwd(projectName)
15
+ .quiet()
16
+ .nothrow();
17
+ if (push.exitCode === 0) {
18
+ spin.stop(green(`Pushed to ${repoUrl}`));
19
+ return;
20
+ }
21
+ spin.stop(yellow(`Added remote ${repoUrl}, but the push didn't complete`));
22
+ note('Finish it manually with: git push -u origin main');
23
+ };
24
+ const createGithubRepo = async (projectName, visibility, repoUrl) => {
25
+ const repoName = basename(projectName);
26
+ const label = repoUrl ?? repoName;
27
+ const visibilityFlag = visibility === 'public' ? '--public' : '--private';
28
+ const spin = spinner();
29
+ spin.start(`Creating ${visibility} repository ${label} on GitHub…`);
30
+ const res = await $ `gh repo create ${repoName} ${visibilityFlag} --source=. --remote=origin --push`
31
+ .cwd(projectName)
32
+ .quiet()
33
+ .nothrow();
34
+ if (res.exitCode === 0) {
35
+ spin.stop(green(`Created and pushed ${label}`));
36
+ return;
37
+ }
38
+ spin.stop(red('Failed to create the GitHub repository'));
39
+ note(res.stderr.toString().trim() || 'gh repo create failed');
40
+ note('Create it manually, then: git remote add origin <url> && git push -u origin main');
41
+ };
42
+ export const initializeGit = async ({ projectName, githubLink, githubRepoUrl, githubVisibility }) => {
5
43
  const spin = spinner();
6
44
  try {
7
45
  spin.start('Initializing git repository…');
@@ -14,4 +52,10 @@ export const initializeGit = async (projectName) => {
14
52
  spin.cancel(red('Failed to initialize git'));
15
53
  throw err;
16
54
  }
55
+ if (githubLink === 'existing' && githubRepoUrl) {
56
+ await connectExistingRepo(projectName, githubRepoUrl);
57
+ }
58
+ else if (githubLink === 'create' && githubVisibility) {
59
+ await createGithubRepo(projectName, githubVisibility, githubRepoUrl);
60
+ }
17
61
  };
@@ -2,6 +2,7 @@ import type { CreateConfiguration } from '../../types';
2
2
  type CreatePackageJsonProps = Pick<CreateConfiguration, 'authOption' | 'useTailwind' | 'databaseEngine' | 'databaseHost' | 'plugins' | 'orm' | 'frontendDirectories' | 'codeQualityTool'> & {
3
3
  projectName: string;
4
4
  latest: boolean;
5
+ repositoryUrl: string | undefined;
5
6
  };
6
- export declare const createPackageJson: ({ projectName, authOption, plugins, databaseEngine, orm, databaseHost, useTailwind, latest, frontendDirectories, codeQualityTool }: CreatePackageJsonProps) => Promise<void>;
7
+ export declare const createPackageJson: ({ projectName, authOption, plugins, databaseEngine, orm, databaseHost, useTailwind, latest, frontendDirectories, codeQualityTool, repositoryUrl }: CreatePackageJsonProps) => Promise<void>;
7
8
  export {};
@@ -16,7 +16,7 @@ const dbClientCommands = {
16
16
  postgresql: 'psql -h localhost -U user -d database',
17
17
  singlestore: 'singlestore -u root -ppassword -D database'
18
18
  };
19
- export const createPackageJson = async ({ projectName, authOption, plugins, databaseEngine, orm, databaseHost, useTailwind, latest, frontendDirectories, codeQualityTool }) => {
19
+ export const createPackageJson = async ({ projectName, authOption, plugins, databaseEngine, orm, databaseHost, useTailwind, latest, frontendDirectories, codeQualityTool, repositoryUrl }) => {
20
20
  const flags = computeFlags(frontendDirectories);
21
21
  const isLocal = !databaseHost || databaseHost === 'none';
22
22
  /* ── Collect all package names that need versions ─────────── */
@@ -250,7 +250,16 @@ export const createPackageJson = async ({ projectName, authOption, plugins, data
250
250
  name: projectName,
251
251
  scripts,
252
252
  type: 'module',
253
- version: '0.0.0'
253
+ version: '0.0.0',
254
+ // When the project is linked to a GitHub repo, ship the standard npm
255
+ // support metadata up front so the package never reads as incomplete.
256
+ ...(repositoryUrl
257
+ ? {
258
+ bugs: { url: `${repositoryUrl}/issues` },
259
+ homepage: repositoryUrl,
260
+ repository: { type: 'git', url: `${repositoryUrl}.git` }
261
+ }
262
+ : {})
254
263
  };
255
264
  writeFileSync(join(projectName, 'package.json'), JSON.stringify(packageJson, null, 2));
256
265
  };
@@ -1,8 +1,9 @@
1
- import { AuthOption, DatabaseEngine, DatabaseHost } from '../../types';
1
+ import { AuthOption, DatabaseEngine, DatabaseHost, ORM } from '../../types';
2
2
  type GenerateTypesProps = {
3
3
  databaseEngine: DatabaseEngine;
4
4
  databaseHost: DatabaseHost;
5
5
  authOption: AuthOption;
6
+ orm?: ORM;
6
7
  };
7
- export declare const generateDatabaseTypes: ({ databaseEngine, databaseHost, authOption }: GenerateTypesProps) => string;
8
+ export declare const generateDatabaseTypes: ({ databaseEngine, databaseHost, authOption, orm }: GenerateTypesProps) => string;
8
9
  export {};
@@ -1,5 +1,110 @@
1
1
  import { isDrizzleDialect } from '../../typeGuards';
2
- export const generateDatabaseTypes = ({ databaseEngine, databaseHost, authOption }) => {
2
+ // Driver type used as `DatabaseType` on the raw-SQL (no-ORM) path. Keyed by
3
+ // `${engine}:${host}` where host is 'none' for a locally-hosted engine. These
4
+ // MUST stay in sync with the `dbType` values in handlerTemplates.ts and the
5
+ // `const db = ...` expression in generateDBBlock.ts, otherwise the generated
6
+ // handler's `db` parameter type won't match the value the server constructs.
7
+ const SQL_DRIVER_TYPES = {
8
+ 'cockroachdb:none': {
9
+ importLine: `import type { SQL } from 'bun';`,
10
+ typeName: 'SQL'
11
+ },
12
+ 'gel:none': {
13
+ importLine: `import type { Client } from 'gel';`,
14
+ typeName: 'Client'
15
+ },
16
+ 'mariadb:none': {
17
+ importLine: `import type { SQL } from 'bun';`,
18
+ typeName: 'SQL'
19
+ },
20
+ 'mongodb:none': {
21
+ importLine: `import type { Db } from 'mongodb';`,
22
+ typeName: 'Db'
23
+ },
24
+ 'mssql:none': {
25
+ importLine: `import type { ConnectionPool } from 'mssql';`,
26
+ typeName: 'ConnectionPool'
27
+ },
28
+ 'mysql:none': {
29
+ importLine: `import type { SQL } from 'bun';`,
30
+ typeName: 'SQL'
31
+ },
32
+ 'mysql:planetscale': {
33
+ importLine: `import type { Client } from '@planetscale/database';`,
34
+ typeName: 'Client'
35
+ },
36
+ 'postgresql:neon': {
37
+ importLine: `import type { Pool } from '@neondatabase/serverless';`,
38
+ typeName: 'Pool'
39
+ },
40
+ 'postgresql:none': {
41
+ importLine: `import type { SQL } from 'bun';`,
42
+ typeName: 'SQL'
43
+ },
44
+ 'postgresql:planetscale': {
45
+ importLine: `import type { Pool } from 'pg';`,
46
+ typeName: 'Pool'
47
+ },
48
+ 'singlestore:none': {
49
+ importLine: `import type { Pool } from 'mysql2/promise';`,
50
+ typeName: 'Pool'
51
+ },
52
+ 'sqlite:none': {
53
+ importLine: `import type { Database } from 'bun:sqlite';`,
54
+ typeName: 'Database'
55
+ },
56
+ 'sqlite:turso': {
57
+ importLine: `import type { Client } from '@libsql/client';`,
58
+ typeName: 'Client'
59
+ }
60
+ };
61
+ // Raw-SQL (no-ORM) `databaseTypes.ts`. The drizzle path infers User/NewUser
62
+ // from the schema's `$inferSelect`/`$inferInsert`; without an ORM there is no
63
+ // schema module, so we hand-roll types that mirror the columns emitted by
64
+ // generateSqliteSchema / the relational DDL. Returns rows are untyped at the
65
+ // driver level, so these are the source of truth for consumers (auth config,
66
+ // handlers, the example page).
67
+ const generateSqlDatabaseTypes = ({ databaseEngine, databaseHost, authOption }) => {
68
+ const host = databaseHost && databaseHost !== 'none' ? databaseHost : 'none';
69
+ const driver = SQL_DRIVER_TYPES[`${databaseEngine}:${host}`];
70
+ const driverImport = driver ? `${driver.importLine}\n` : '';
71
+ const databaseTypeLine = driver
72
+ ? `export type DatabaseType = ${driver.typeName};`
73
+ : 'export type DatabaseType = unknown;';
74
+ const entityTypes = authOption === 'abs'
75
+ ? `export type User = {
76
+ auth_sub: string;
77
+ created_at: Date;
78
+ metadata: Record<string, unknown>;
79
+ };
80
+
81
+ export type NewUser = {
82
+ auth_sub: string;
83
+ metadata?: Record<string, unknown>;
84
+ };`
85
+ : `export type CountHistory = {
86
+ uid: number;
87
+ count: number;
88
+ created_at: Date;
89
+ };
90
+
91
+ export type NewCountHistory = {
92
+ count: number;
93
+ };`;
94
+ return `${driverImport}
95
+ ${databaseTypeLine}
96
+
97
+ ${entityTypes}
98
+ `;
99
+ };
100
+ export const generateDatabaseTypes = ({ databaseEngine, databaseHost, authOption, orm }) => {
101
+ if (orm !== 'drizzle') {
102
+ return generateSqlDatabaseTypes({
103
+ authOption,
104
+ databaseEngine,
105
+ databaseHost
106
+ });
107
+ }
3
108
  let dbImport = '';
4
109
  let dbTypeLine = '';
5
110
  if (databaseHost === 'neon') {
@@ -0,0 +1,3 @@
1
+ import { AuthOption, DatabaseEngine } from '../../types';
2
+ export declare const supportsRelationalSchema: (engine: DatabaseEngine) => boolean;
3
+ export declare const generateRelationalSchema: (databaseEngine: DatabaseEngine, authOption: AuthOption) => string;
@@ -0,0 +1,52 @@
1
+ const DIALECTS = {
2
+ cockroachdb: {
3
+ json: 'JSONB',
4
+ pk: 'INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY',
5
+ string: 'VARCHAR(255)',
6
+ timestamp: 'TIMESTAMP NOT NULL DEFAULT now()'
7
+ },
8
+ mariadb: {
9
+ json: 'JSON',
10
+ pk: 'INT AUTO_INCREMENT PRIMARY KEY',
11
+ string: 'VARCHAR(255)',
12
+ timestamp: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'
13
+ },
14
+ mysql: {
15
+ json: 'JSON',
16
+ pk: 'INT AUTO_INCREMENT PRIMARY KEY',
17
+ string: 'VARCHAR(255)',
18
+ timestamp: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'
19
+ },
20
+ postgresql: {
21
+ json: 'JSONB',
22
+ pk: 'INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY',
23
+ string: 'VARCHAR(255)',
24
+ timestamp: 'TIMESTAMP NOT NULL DEFAULT now()'
25
+ },
26
+ singlestore: {
27
+ json: 'JSON',
28
+ pk: 'INT AUTO_INCREMENT PRIMARY KEY',
29
+ string: 'VARCHAR(255)',
30
+ timestamp: 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'
31
+ }
32
+ };
33
+ export const supportsRelationalSchema = (engine) => engine !== undefined && engine in DIALECTS;
34
+ export const generateRelationalSchema = (databaseEngine, authOption) => {
35
+ if (databaseEngine === undefined || !(databaseEngine in DIALECTS)) {
36
+ throw new Error(`Internal error: no relational DDL for engine "${databaseEngine}"`);
37
+ }
38
+ const d = DIALECTS[databaseEngine];
39
+ return authOption === 'abs'
40
+ ? `CREATE TABLE IF NOT EXISTS users (
41
+ auth_sub ${d.string} PRIMARY KEY,
42
+ created_at ${d.timestamp},
43
+ metadata ${d.json} DEFAULT ('{}')
44
+ );
45
+ `
46
+ : `CREATE TABLE IF NOT EXISTS count_history (
47
+ uid ${d.pk},
48
+ count INT NOT NULL,
49
+ created_at ${d.timestamp}
50
+ );
51
+ `;
52
+ };
@@ -7,6 +7,7 @@ export const getUser = async (db: DatabaseType, authSub: string) => {
7
7
  };
8
8
 
9
9
  export const createUser = async (db: DatabaseType, newUserData: NewUser) => {
10
+ const { auth_sub: authSub, metadata: userIdentity } = newUserData;
10
11
  ${queries.insertUser}
11
12
  }`;
12
13
  const buildSqlCountTemplate = ({ importLines, dbType, queries }) => `
@@ -8,6 +8,7 @@ import { createDrizzleConfig } from '../configurations/generateDrizzleConfig';
8
8
  import { generateDatabaseTypes } from './generateDatabaseTypes';
9
9
  import { generateDrizzleSchema } from './generateDrizzleSchema';
10
10
  import { generateDBHandlers } from './generateHandlers';
11
+ import { generateRelationalSchema, supportsRelationalSchema } from './generateRelationalSchema';
11
12
  import { generateSqliteSchema } from './generateSqliteSchema';
12
13
  import { scaffoldDocker } from './scaffoldDocker';
13
14
  export const scaffoldDatabase = async ({ projectName, databaseEngine, databaseHost, databaseDirectory, backendDirectory, authOption, orm, typesDirectory }) => {
@@ -26,6 +27,30 @@ export const scaffoldDatabase = async ({ projectName, databaseEngine, databaseHo
26
27
  usesAuth
27
28
  });
28
29
  writeFileSync(join(handlerDirectory, handlerFileName), dbHandlers, 'utf-8');
30
+ // Raw-SQL (no-ORM) auth handlers import `DatabaseType`/`NewUser` from
31
+ // types/databaseTypes (so do the auth config and the example page), but
32
+ // the drizzle-backed type module below is only written on the drizzle
33
+ // path. Generate a self-contained, driver-typed version here so the
34
+ // non-drizzle auth scaffold actually type-checks and builds.
35
+ if (usesAuth && orm !== 'drizzle') {
36
+ mkdirSync(typesDirectory, { recursive: true });
37
+ const sqlTypes = generateDatabaseTypes({
38
+ authOption,
39
+ databaseEngine,
40
+ databaseHost,
41
+ orm
42
+ });
43
+ writeFileSync(join(typesDirectory, 'databaseTypes.ts'), sqlTypes);
44
+ }
45
+ // Hosted relational engines on the raw-SQL path get no migration tooling
46
+ // (no drizzle-kit, no local sqlite3). Emit a plain DDL file so the tables
47
+ // the handlers query can be created against the hosted database.
48
+ const isRemoteHost = databaseHost !== undefined && databaseHost !== 'none';
49
+ if (isRemoteHost &&
50
+ orm !== 'drizzle' &&
51
+ supportsRelationalSchema(databaseEngine)) {
52
+ writeFileSync(join(projectDatabaseDirectory, 'schema.sql'), generateRelationalSchema(databaseEngine, authOption));
53
+ }
29
54
  if (databaseEngine === 'sqlite') {
30
55
  void ((orm === undefined || orm === 'none') &&
31
56
  (await checkSqliteInstalled()));
@@ -58,7 +83,8 @@ export const scaffoldDatabase = async ({ projectName, databaseEngine, databaseHo
58
83
  const drizzleTypes = generateDatabaseTypes({
59
84
  authOption,
60
85
  databaseEngine,
61
- databaseHost
86
+ databaseHost,
87
+ orm
62
88
  });
63
89
  writeFileSync(join(typesDirectory, 'databaseTypes.ts'), drizzleTypes);
64
90
  return { dockerFreshInstall: false };
@@ -20,8 +20,11 @@ const connectionMap = {
20
20
  planetscale: { expr: 'new Client({ url: getEnv("DATABASE_URL") })' }
21
21
  },
22
22
  postgresql: {
23
+ // Raw-SQL (no-ORM) neon uses the driver's `Pool`, whose `.query(text,
24
+ // params)` → `{ rows }` API matches the generated handler. The drizzle
25
+ // path is handled separately below and still uses `neon()` http.
23
26
  neon: {
24
- expr: 'neon(getEnv("DATABASE_URL"));'
27
+ expr: 'new Pool({ connectionString: getEnv("DATABASE_URL") })'
25
28
  },
26
29
  none: { expr: 'new SQL(getEnv("DATABASE_URL"))' },
27
30
  planetscale: {
@@ -107,7 +107,10 @@ export const generateImportsBlock = ({ deps, flags, orm, authOption, databaseEng
107
107
  const getPostgresqlNoOrmImports = () => {
108
108
  if (isRemoteHost && databaseHost === 'neon')
109
109
  return [
110
- ...connectorImports[databaseHost],
110
+ // No-ORM neon constructs `new Pool(...)` (see generateDBBlock),
111
+ // whose `.query()` API matches the generated SQL handler. The
112
+ // `neon()` http client is only used on the drizzle path.
113
+ `import { Pool } from '@neondatabase/serverless'`,
111
114
  `import { getEnv } from '@absolutejs/absolute'`
112
115
  ];
113
116
  if (isRemoteHost && databaseHost === 'planetscale')
@@ -122,7 +122,7 @@ export const generateReactExamplePage = (authOption) => {
122
122
  : `<Dropdown />`;
123
123
  const closing = authOption === 'abs' ? `};` : `);`;
124
124
  return `
125
- ${authOption === 'abs' ? `import { User } from '../../../types/databaseTypes';\nimport { extractPropFromIdentity, ProviderConfiguration } from '@absolutejs/auth';` : ''}
125
+ ${authOption === 'abs' ? `import type { User } from '../../../types/databaseTypes';\nimport { extractPropFromIdentity } from '@absolutejs/auth';\nimport type { ProviderConfiguration } from '@absolutejs/auth';` : ''}
126
126
  import { App } from '../components/App';
127
127
  import { Dropdown } from '../components/Dropdown';
128
128
  import { Head } from '../components/Head';
package/dist/prompt.js CHANGED
@@ -6,6 +6,7 @@ import { getDatabaseHost } from './questions/databaseHost';
6
6
  import { getDirectoryConfiguration } from './questions/directoryConfiguration';
7
7
  import { getFrontendDirectoryConfigurations } from './questions/frontendDirectoryConfigurations';
8
8
  import { getFrontends } from './questions/frontends';
9
+ import { getGithubLink } from './questions/githubLink';
9
10
  import { getHtmlScriptingOption } from './questions/htmlScriptingOption';
10
11
  import { getInitializeGit } from './questions/initializeGitNow';
11
12
  import { getInstallDependencies } from './questions/installDependenciesNow';
@@ -56,6 +57,25 @@ export const prompt = async (argumentConfiguration) => {
56
57
  const plugins = argumentConfiguration.plugins?.filter((plugin) => plugin !== undefined) ?? (await getPlugins());
57
58
  // 14. Initialize Git repository
58
59
  const initializeGitNow = argumentConfiguration.initializeGitNow ?? (await getInitializeGit());
60
+ // 14b. Optionally connect the new project to GitHub
61
+ const resolveGithubLink = async () => {
62
+ if (!initializeGitNow) {
63
+ return {
64
+ githubLink: 'skip',
65
+ githubRepoUrl: undefined,
66
+ githubVisibility: undefined
67
+ };
68
+ }
69
+ if (argumentConfiguration.githubLink) {
70
+ return {
71
+ githubLink: argumentConfiguration.githubLink,
72
+ githubRepoUrl: argumentConfiguration.githubRepoUrl,
73
+ githubVisibility: argumentConfiguration.githubVisibility
74
+ };
75
+ }
76
+ return getGithubLink(projectName);
77
+ };
78
+ const { githubLink, githubRepoUrl, githubVisibility } = await resolveGithubLink();
59
79
  // 15. Install dependencies
60
80
  const installDependenciesNow = argumentConfiguration.installDependenciesNow ??
61
81
  (await getInstallDependencies());
@@ -71,6 +91,9 @@ export const prompt = async (argumentConfiguration) => {
71
91
  directoryConfig,
72
92
  frontendDirectories,
73
93
  frontends,
94
+ githubLink,
95
+ githubRepoUrl,
96
+ githubVisibility,
74
97
  initializeGitNow,
75
98
  installDependenciesNow,
76
99
  orm,
@@ -0,0 +1,7 @@
1
+ import type { GithubLinkOption } from '../types';
2
+ export type GithubLinkResult = {
3
+ githubLink: GithubLinkOption;
4
+ githubRepoUrl: string | undefined;
5
+ githubVisibility: 'public' | 'private' | undefined;
6
+ };
7
+ export declare const getGithubLink: (projectName: string) => Promise<GithubLinkResult>;
@@ -0,0 +1,84 @@
1
+ import { basename } from 'path';
2
+ import { isCancel, select, text } from '@clack/prompts';
3
+ import { dim, yellow } from 'picocolors';
4
+ import { abort } from '../utils/abort';
5
+ import { getGhLogin, hasGh, isGhAuthenticated, normalizeRepoInput } from '../utils/github';
6
+ const SKIP = {
7
+ githubLink: 'skip',
8
+ githubRepoUrl: undefined,
9
+ githubVisibility: undefined
10
+ };
11
+ const warn = (message) => console.log(`${dim('│')}\n${yellow('▲')} ${message}`);
12
+ const linkExistingRepo = async () => {
13
+ const entered = await text({
14
+ message: 'Repository (owner/name or full GitHub URL):',
15
+ placeholder: 'your-org/your-repo',
16
+ validate: (value) => normalizeRepoInput(value)
17
+ ? undefined
18
+ : 'Enter owner/name or a github.com URL'
19
+ });
20
+ if (isCancel(entered))
21
+ abort();
22
+ const result = {
23
+ githubLink: 'existing',
24
+ githubRepoUrl: normalizeRepoInput(entered)?.httpsUrl,
25
+ githubVisibility: undefined
26
+ };
27
+ return result;
28
+ };
29
+ const createNewRepo = async (projectName) => {
30
+ if (!(await hasGh())) {
31
+ warn('GitHub CLI (gh) not found — skipping GitHub setup. Install it from https://cli.github.com, then push manually.');
32
+ return SKIP;
33
+ }
34
+ if (!(await isGhAuthenticated())) {
35
+ warn('GitHub CLI is not authenticated — run `gh auth login`, then re-run. Skipping GitHub setup.');
36
+ return SKIP;
37
+ }
38
+ const login = await getGhLogin();
39
+ if (!login) {
40
+ warn("Couldn't resolve your GitHub account — skipping GitHub setup.");
41
+ return SKIP;
42
+ }
43
+ const repoName = basename(projectName);
44
+ const visibility = await select({
45
+ message: `Visibility for github.com/${login}/${repoName}:`,
46
+ options: [
47
+ { label: 'Private', value: 'private' },
48
+ { label: 'Public', value: 'public' }
49
+ ]
50
+ });
51
+ if (isCancel(visibility))
52
+ abort();
53
+ const result = {
54
+ githubLink: 'create',
55
+ githubRepoUrl: `https://github.com/${login}/${repoName}`,
56
+ githubVisibility: visibility === 'public' ? 'public' : 'private'
57
+ };
58
+ return result;
59
+ };
60
+ export const getGithubLink = async (projectName) => {
61
+ const choice = await select({
62
+ message: 'Connect this project to GitHub?',
63
+ options: [
64
+ {
65
+ hint: 'set the remote to a repo you already own',
66
+ label: 'Link an existing repository',
67
+ value: 'existing'
68
+ },
69
+ {
70
+ hint: 'create one for you with the GitHub CLI',
71
+ label: 'Create a new repository',
72
+ value: 'create'
73
+ },
74
+ { label: 'Skip', value: 'skip' }
75
+ ]
76
+ });
77
+ if (isCancel(choice))
78
+ abort();
79
+ if (choice === 'existing')
80
+ return linkExistingRepo();
81
+ if (choice === 'create')
82
+ return createNewRepo(projectName);
83
+ return SKIP;
84
+ };
@@ -5,7 +5,7 @@ type ScaffoldProps = {
5
5
  latest: boolean;
6
6
  envVariables: string[] | undefined;
7
7
  };
8
- export declare const scaffold: ({ response: { projectName, codeQualityTool, initializeGitNow, databaseEngine, databaseHost, useHTMLScripts, useTailwind, databaseDirectory, absProviders, orm, frontends, plugins, authOption, buildDirectory, assetsDirectory, tailwind, installDependenciesNow, frontendDirectories }, latest, envVariables, packageManager }: ScaffoldProps) => Promise<{
8
+ export declare const scaffold: ({ response: { projectName, codeQualityTool, initializeGitNow, githubLink, githubRepoUrl, githubVisibility, databaseEngine, databaseHost, useHTMLScripts, useTailwind, databaseDirectory, absProviders, orm, frontends, plugins, authOption, buildDirectory, assetsDirectory, tailwind, installDependenciesNow, frontendDirectories }, latest, envVariables, packageManager }: ScaffoldProps) => Promise<{
9
9
  dockerFreshInstall: boolean;
10
10
  }>;
11
11
  export {};
package/dist/scaffold.js CHANGED
@@ -10,7 +10,7 @@ import { scaffoldConfigurationFiles } from './generators/configurations/scaffold
10
10
  import { scaffoldDatabase } from './generators/db/scaffoldDatabase';
11
11
  import { scaffoldBackend } from './generators/project/scaffoldBackend';
12
12
  import { scaffoldFrontends } from './generators/project/scaffoldFrontends';
13
- export const scaffold = async ({ response: { projectName, codeQualityTool, initializeGitNow, databaseEngine, databaseHost, useHTMLScripts, useTailwind, databaseDirectory, absProviders, orm, frontends, plugins, authOption, buildDirectory, assetsDirectory, tailwind, installDependenciesNow, frontendDirectories }, latest, envVariables, packageManager }) => {
13
+ export const scaffold = async ({ response: { projectName, codeQualityTool, initializeGitNow, githubLink, githubRepoUrl, githubVisibility, databaseEngine, databaseHost, useHTMLScripts, useTailwind, databaseDirectory, absProviders, orm, frontends, plugins, authOption, buildDirectory, assetsDirectory, tailwind, installDependenciesNow, frontendDirectories }, latest, envVariables, packageManager }) => {
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const templatesDirectory = join(__dirname, '/templates');
16
16
  const { frontendDirectory, backendDirectory, projectAssetsDirectory, typesDirectory } = initalizeRoot(projectName, templatesDirectory);
@@ -36,6 +36,7 @@ export const scaffold = async ({ response: { projectName, codeQualityTool, initi
36
36
  orm,
37
37
  plugins,
38
38
  projectName,
39
+ repositoryUrl: githubRepoUrl,
39
40
  useTailwind
40
41
  });
41
42
  scaffoldBackend({
@@ -102,7 +103,12 @@ export const server = treaty<Server>(serverUrl)
102
103
  projectName
103
104
  });
104
105
  if (initializeGitNow) {
105
- await initializeGit(projectName);
106
+ await initializeGit({
107
+ githubLink,
108
+ githubRepoUrl,
109
+ githubVisibility,
110
+ projectName
111
+ });
106
112
  }
107
113
  return { dockerFreshInstall };
108
114
  };
@@ -1,10 +1,11 @@
1
- import type { AuthOption, AvailableDrizzleDialect, CodeQualityTool, DatabaseEngine, DatabaseHost, Frontend, ORM } from './types';
1
+ import type { AuthOption, AvailableDrizzleDialect, CodeQualityTool, DatabaseEngine, DatabaseHost, Frontend, GithubLinkOption, ORM } from './types';
2
2
  export declare const isCodeQualityTool: (value: string | undefined) => value is CodeQualityTool;
3
3
  export declare const isDatabaseEngine: (value: string | undefined) => value is DatabaseEngine;
4
4
  export declare const isDatabaseHost: (value: string | undefined) => value is DatabaseHost;
5
5
  export declare const isDirectoryConfig: (value: string) => value is "default" | "custom";
6
6
  export declare const isDrizzleDialect: (value: string | undefined) => value is AvailableDrizzleDialect;
7
7
  export declare const isFrontend: (value: string | undefined) => value is Frontend;
8
+ export declare const isGithubLinkOption: (value: string | undefined) => value is GithubLinkOption;
8
9
  export declare const isORM: (value: string | undefined) => value is ORM;
9
10
  export declare const isPrismaDialect: (value: string | undefined) => value is string;
10
11
  export declare const isValidAuthOption: (value: string | undefined) => value is AuthOption;
@@ -5,6 +5,7 @@ export const isDatabaseHost = (value) => availableDatabaseHosts.some((host) => h
5
5
  export const isDirectoryConfig = (value) => value === 'default' || value === 'custom';
6
6
  export const isDrizzleDialect = (value) => availableDrizzleDialects.some((dialect) => dialect === value);
7
7
  export const isFrontend = (value) => value !== undefined && Object.keys(frontendLabels).includes(value);
8
+ export const isGithubLinkOption = (value) => value === 'existing' || value === 'create' || value === 'skip';
8
9
  export const isORM = (value) => value === 'drizzle' || value === 'prisma' || value === undefined;
9
10
  export const isPrismaDialect = (value) => availablePrismaDialects.some((dialect) => dialect === value);
10
11
  export const isValidAuthOption = (value) => value === 'abs' || value === 'none' || value === undefined;
package/dist/types.d.ts CHANGED
@@ -40,6 +40,7 @@ export type TailwindConfig = {
40
40
  input: string;
41
41
  output: string;
42
42
  } | undefined;
43
+ export type GithubLinkOption = 'existing' | 'create' | 'skip';
43
44
  export type CreateConfiguration = {
44
45
  absProviders: ProviderOption[] | undefined;
45
46
  assetsDirectory: string;
@@ -51,6 +52,9 @@ export type CreateConfiguration = {
51
52
  frontends: Frontend[];
52
53
  useHTMLScripts: boolean;
53
54
  initializeGitNow: boolean;
55
+ githubLink: GithubLinkOption;
56
+ githubRepoUrl: string | undefined;
57
+ githubVisibility: 'public' | 'private' | undefined;
54
58
  installDependenciesNow: boolean;
55
59
  codeQualityTool: CodeQualityTool;
56
60
  orm: ORM;
@@ -0,0 +1,16 @@
1
+ /** The authenticated GitHub login, or undefined when it can't be resolved. */
2
+ export declare const getGhLogin: () => Promise<string | undefined>;
3
+ /** True when the GitHub CLI (`gh`) is on the PATH. */
4
+ export declare const hasGh: () => Promise<boolean>;
5
+ /** True when `gh` has an authenticated account. */
6
+ export declare const isGhAuthenticated: () => Promise<boolean>;
7
+ /**
8
+ * Accepts `owner/name`, `https://github.com/owner/name(.git)`, or
9
+ * `git@github.com:owner/name(.git)` and returns a normalized https URL plus
10
+ * the parsed owner/name. Returns undefined when the input isn't recognizable.
11
+ */
12
+ export declare const normalizeRepoInput: (input: string | undefined) => {
13
+ httpsUrl: string;
14
+ name: string;
15
+ owner: string;
16
+ } | undefined;
@@ -0,0 +1,41 @@
1
+ import { platform } from 'process';
2
+ import { $ } from 'bun';
3
+ /** The authenticated GitHub login, or undefined when it can't be resolved. */
4
+ export const getGhLogin = async () => {
5
+ const res = await $ `gh api user --jq .login`.quiet().nothrow();
6
+ if (res.exitCode !== 0)
7
+ return undefined;
8
+ return res.stdout.toString().trim() || undefined;
9
+ };
10
+ /** True when the GitHub CLI (`gh`) is on the PATH. */
11
+ export const hasGh = async () => (platform === 'win32'
12
+ ? await $ `where gh`.quiet().nothrow()
13
+ : await $ `command -v gh`.quiet().nothrow()).exitCode === 0;
14
+ /** True when `gh` has an authenticated account. */
15
+ export const isGhAuthenticated = async () => (await $ `gh auth status`.quiet().nothrow()).exitCode === 0;
16
+ /**
17
+ * Accepts `owner/name`, `https://github.com/owner/name(.git)`, or
18
+ * `git@github.com:owner/name(.git)` and returns a normalized https URL plus
19
+ * the parsed owner/name. Returns undefined when the input isn't recognizable.
20
+ */
21
+ export const normalizeRepoInput = (input) => {
22
+ const trimmed = (input ?? '').trim().replace(/\.git$/, '');
23
+ const patterns = [
24
+ /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)$/i,
25
+ /^git@github\.com:([^/\s]+)\/([^/\s]+)$/i,
26
+ /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/
27
+ ];
28
+ for (const pattern of patterns) {
29
+ const match = trimmed.match(pattern);
30
+ const owner = match?.[1];
31
+ const name = match?.[2];
32
+ if (owner && name) {
33
+ return {
34
+ httpsUrl: `https://github.com/${owner}/${name}`,
35
+ name,
36
+ owner
37
+ };
38
+ }
39
+ }
40
+ return undefined;
41
+ };
@@ -3,7 +3,8 @@ import { parseArgs } from 'util';
3
3
  import { isValidProviderOption, providers } from '@absolutejs/auth';
4
4
  import { DEFAULT_ARG_LENGTH } from '../constants';
5
5
  import { availableAuthProviders, availableDatabaseEngines, availableDatabaseHosts, availableDirectoryConfigurations, availableDrizzleDialects, availableORMs, availablePrismaDialects } from '../data';
6
- import { isValidAuthOption, isDatabaseEngine, isDatabaseHost, isDirectoryConfig, isDrizzleDialect, isORM, isPrismaDialect } from '../typeGuards';
6
+ import { isValidAuthOption, isDatabaseEngine, isDatabaseHost, isDirectoryConfig, isDrizzleDialect, isGithubLinkOption, isORM, isPrismaDialect } from '../typeGuards';
7
+ import { normalizeRepoInput } from './github';
7
8
  export const parseCommandLineOptions = () => {
8
9
  const { values, positionals } = parseArgs({
9
10
  allowNegative: true,
@@ -25,6 +26,7 @@ export const parseCommandLineOptions = () => {
25
26
  env: { multiple: true, type: 'string' },
26
27
  'eslint+prettier': { type: 'boolean' },
27
28
  git: { type: 'boolean' },
29
+ github: { type: 'string' },
28
30
  help: { default: false, short: 'h', type: 'boolean' },
29
31
  html: { type: 'boolean' },
30
32
  'html-dir': { type: 'string' },
@@ -37,6 +39,8 @@ export const parseCommandLineOptions = () => {
37
39
  plugin: { multiple: true, type: 'string' },
38
40
  react: { type: 'boolean' },
39
41
  'react-dir': { type: 'string' },
42
+ repo: { type: 'string' },
43
+ 'repo-visibility': { type: 'string' },
40
44
  skip: { type: 'boolean' },
41
45
  svelte: { type: 'boolean' },
42
46
  'svelte-dir': { type: 'string' },
@@ -232,6 +236,10 @@ export const parseCommandLineOptions = () => {
232
236
  validEnv.push(entry);
233
237
  }
234
238
  values.env = validEnv.length ? validEnv : undefined;
239
+ const repoVisibility = values['repo-visibility'];
240
+ const githubVisibility = repoVisibility === 'public' || repoVisibility === 'private'
241
+ ? repoVisibility
242
+ : undefined;
235
243
  const argumentConfiguration = {
236
244
  absProviders: absProviders.length ? absProviders : undefined,
237
245
  assetsDirectory: values.assets,
@@ -244,6 +252,11 @@ export const parseCommandLineOptions = () => {
244
252
  directoryConfig,
245
253
  frontendDirectories,
246
254
  frontends: selectedFrontends.length ? selectedFrontends : undefined,
255
+ githubLink: isGithubLinkOption(values.github)
256
+ ? values.github
257
+ : undefined,
258
+ githubRepoUrl: normalizeRepoInput(values.repo)?.httpsUrl,
259
+ githubVisibility,
247
260
  initializeGitNow: values.git,
248
261
  installDependenciesNow: values.install,
249
262
  orm,
package/package.json CHANGED
@@ -52,5 +52,5 @@
52
52
  "typecheck": "bun run tsc --noEmit"
53
53
  },
54
54
  "type": "module",
55
- "version": "0.13.5"
55
+ "version": "0.13.6"
56
56
  }