create-deka-app 0.0.2 → 0.0.4

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.
package/README.md CHANGED
@@ -15,15 +15,22 @@ yarn create deka-app myapp
15
15
  bun create deka-app myapp
16
16
  ```
17
17
 
18
- ## Status
19
-
20
- **Placeholder.** This package reserves the name while the real scaffolder is built ([deka#1091](https://github.com/dekaruntime/deka/issues/1091)). It does not scaffold a project yet; it points you at deka.
21
-
22
- Install deka directly in the meantime:
23
-
24
- ```sh
25
- curl -fsSL https://deka.gg/install.sh | sh
26
- ```
18
+ ## What it does
19
+
20
+ `npx create-deka-app myapp` takes the directory as an argument and does
21
+ everything non-interactively — no prompts:
22
+
23
+ 1. creates `myapp/` (refuses if it already exists and is non-empty)
24
+ 2. writes `myapp/package.json`, pinning `@dekaruntime/deka` as a
25
+ devDependency at this package's own version, with `dev`/`build`/`start`
26
+ scripts that call `deka`
27
+ 3. runs your package manager's install (detected automatically: npm, pnpm,
28
+ yarn or bun)
29
+ 4. runs `deka init` from the freshly installed binary — the real
30
+ scaffolder, which never overwrites a file that's already there
31
+
32
+ See [deka#1091](https://github.com/dekaruntime/deka/issues/1091) for the
33
+ design history.
27
34
 
28
35
  ## Platforms
29
36
 
package/index.js CHANGED
@@ -1,20 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // Placeholder. The real scaffolder (dekaruntime/deka#1091) installs the
3
- // platform-matched deka binary and runs `deka init`.
4
- const supported = new Set(['darwin-arm64', 'darwin-x64', 'linux-x64'])
5
- const platform = `${process.platform === 'win32' ? 'win32' : process.platform}-${process.arch === 'arm64' ? 'arm64' : 'x64'}`
2
+ import { run } from './src/cli.js'
6
3
 
7
- console.log('')
8
- console.log(' create-deka-app does not scaffold projects yet.')
9
- console.log('')
10
- console.log(' deka is a runtime and language for full-stack apps: https://deka.gg')
11
- console.log(' Install it today: curl -fsSL https://deka.gg/install.sh | sh')
12
- console.log(' Then: deka init myapp')
13
- console.log('')
14
- console.log(' Progress: https://github.com/dekaruntime/deka/issues/1091')
15
- console.log('')
16
- if (!supported.has(platform)) {
17
- console.log(` Note: ${platform} is not supported yet.`)
18
- console.log(' macOS (Apple Silicon and Intel) and Linux x64 are supported; Windows is coming.')
19
- console.log('')
20
- }
4
+ process.exit(run())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-deka-app",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Scaffold a new deka app",
5
5
  "bin": {
6
6
  "create-deka-app": "./index.js"
@@ -8,9 +8,13 @@
8
8
  "type": "module",
9
9
  "files": [
10
10
  "index.js",
11
+ "src",
11
12
  "README.md",
12
13
  "LICENSE"
13
14
  ],
15
+ "scripts": {
16
+ "test": "node --test test/*.test.js"
17
+ },
14
18
  "engines": {
15
19
  "node": ">=18"
16
20
  },
package/src/cli.js ADDED
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import path from 'node:path'
4
+ import { createApp, ScaffoldError } from './scaffold.js'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const ownPackageJson = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'))
8
+
9
+ /**
10
+ * Entry point used by both index.js (the real CLI) and the test suite
11
+ * (with argv/env/cwd/log overridden). Returns the process exit code rather
12
+ * than calling process.exit itself, so it stays testable.
13
+ */
14
+ export function run({
15
+ argv = process.argv.slice(2),
16
+ env = process.env,
17
+ cwd = process.cwd(),
18
+ log = console.log,
19
+ error = console.error,
20
+ // create-deka-app's own version. Not currently used by createApp -- the
21
+ // runtime version it pins in the generated package.json is resolved
22
+ // separately, from the npm registry, since create-deka-app's own 0.0.x
23
+ // line and @dekaruntime/deka's release line are not in lockstep. Kept
24
+ // here as the CLI's own version for whatever legitimately needs it
25
+ // (e.g. a future `--version` flag).
26
+ ownVersion = ownPackageJson.version,
27
+ } = {}) {
28
+ const targetArg = argv[0]
29
+ try {
30
+ return createApp({ targetArg, cwd, env, log })
31
+ } catch (err) {
32
+ if (err instanceof ScaffoldError) {
33
+ error(err.message)
34
+ return err.exitCode
35
+ }
36
+ throw err
37
+ }
38
+ }
@@ -0,0 +1,33 @@
1
+ // Detects which package manager invoked us, so we can shell out to the same
2
+ // one for the install step and tell the user which one we used.
3
+ //
4
+ // `npm_config_user_agent` is set by npm, pnpm, yarn and bun alike (it is how
5
+ // `npx create-deka-app`, `pnpm create deka-app`, `yarn create deka-app` and
6
+ // `bun create deka-app` all resolve to this package), and its first token
7
+ // before "/" names the tool, e.g. "pnpm/8.15.1 npm/? node/v20.11.0 darwin
8
+ // arm64". `npm_execpath` is a fallback for the rarer case where that header
9
+ // is absent but the package manager's own launcher script is still visible
10
+ // on the path it invoked us with.
11
+ const MANAGERS = {
12
+ npm: { name: 'npm', install: ['npm', ['install']] },
13
+ pnpm: { name: 'pnpm', install: ['pnpm', ['install']] },
14
+ yarn: { name: 'yarn', install: ['yarn', ['install']] },
15
+ bun: { name: 'bun', install: ['bun', ['install']] },
16
+ }
17
+
18
+ export function detectPackageManager(env = process.env) {
19
+ const userAgent = env.npm_config_user_agent
20
+ if (userAgent) {
21
+ const name = userAgent.split('/')[0].toLowerCase()
22
+ if (MANAGERS[name]) return MANAGERS[name]
23
+ }
24
+
25
+ const execPath = env.npm_execpath || ''
26
+ if (/pnpm/i.test(execPath)) return MANAGERS.pnpm
27
+ if (/yarn/i.test(execPath)) return MANAGERS.yarn
28
+ if (/bun/i.test(execPath)) return MANAGERS.bun
29
+
30
+ return MANAGERS.npm
31
+ }
32
+
33
+ export { MANAGERS }
@@ -0,0 +1,12 @@
1
+ // Platforms deka ships a binary for today. Windows is dekaruntime/deka#1092.
2
+ const SUPPORTED = new Set(['darwin-arm64', 'darwin-x64', 'linux-x64'])
3
+
4
+ export function platformKey(platform = process.platform, arch = process.arch) {
5
+ return `${platform}-${arch}`
6
+ }
7
+
8
+ export function isSupportedPlatform(platform = process.platform, arch = process.arch) {
9
+ return SUPPORTED.has(platformKey(platform, arch))
10
+ }
11
+
12
+ export { SUPPORTED }
@@ -0,0 +1,184 @@
1
+ import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs'
2
+ import { spawnSync } from 'node:child_process'
3
+ import path from 'node:path'
4
+ import { detectPackageManager } from './package-manager.js'
5
+ import { isSupportedPlatform, platformKey } from './platform.js'
6
+
7
+ // Thrown for every expected failure. `message` is written straight to
8
+ // stderr, so it always says what happened and what to do about it.
9
+ export class ScaffoldError extends Error {
10
+ constructor(message, exitCode = 1) {
11
+ super(message)
12
+ this.name = 'ScaffoldError'
13
+ this.exitCode = exitCode
14
+ }
15
+ }
16
+
17
+ export const USAGE = 'Usage: create-deka-app <directory>\n\n npx create-deka-app myapp'
18
+
19
+ function sanitizePackageName(dirName) {
20
+ const name = dirName
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9._-]+/g, '-')
23
+ .replace(/^[._-]+/, '')
24
+ .slice(0, 214)
25
+ return name.length > 0 ? name : 'deka-app'
26
+ }
27
+
28
+ function dekaBinPath(targetDir, platform) {
29
+ return path.join(targetDir, 'node_modules', '.bin', platform === 'win32' ? 'deka.cmd' : 'deka')
30
+ }
31
+
32
+ export const RUNTIME_PACKAGE = '@dekaruntime/deka'
33
+
34
+ // Builds the package.json this package writes into every scaffolded
35
+ // project. Exported so the test suite can assert on it directly without
36
+ // re-deriving the shape.
37
+ //
38
+ // `runtimeVersion` must be the @dekaruntime/deka version resolved at
39
+ // scaffold time (see resolveRuntimeVersion below) -- never this package's
40
+ // own version. create-deka-app has its own 0.0.x release line and
41
+ // @dekaruntime/deka tracks deka's releases; the two are not in lockstep
42
+ // and never will be, so passing this package's own version here pins a
43
+ // version of the runtime that may not exist (see deka#1076-era bug).
44
+ export function buildPackageJson(dirName, runtimeVersion) {
45
+ return {
46
+ name: sanitizePackageName(dirName),
47
+ private: true,
48
+ version: '0.0.0',
49
+ scripts: {
50
+ dev: 'deka dev',
51
+ build: 'deka build',
52
+ start: 'deka start',
53
+ },
54
+ devDependencies: {
55
+ [RUNTIME_PACKAGE]: runtimeVersion,
56
+ },
57
+ }
58
+ }
59
+
60
+ // Looks up the latest published @dekaruntime/deka version from the npm
61
+ // registry so the generated package.json pins something that actually
62
+ // exists, rather than assuming it matches create-deka-app's own version.
63
+ //
64
+ // Always shells out to `npm` for this lookup (not the detected package
65
+ // manager) -- npm ships with every Node.js install, so it is available
66
+ // regardless of whether the user ran this via npx, pnpm create, yarn
67
+ // create or bun create, and `npm view` is a read-only registry query with
68
+ // no project-local side effects.
69
+ //
70
+ // Falls back to the "latest" dist-tag -- never to a version we already
71
+ // know is wrong -- if the registry can't be reached (offline, registry
72
+ // outage, etc), and says so via `log` so the fallback is visible.
73
+ export function resolveRuntimeVersion({ spawn = spawnSync, cwd = process.cwd(), log = console.log } = {}) {
74
+ const result = spawn('npm', ['view', RUNTIME_PACKAGE, 'version'], { cwd, encoding: 'utf8' })
75
+
76
+ const version =
77
+ result && !result.error && result.status === 0 ? String(result.stdout || '').trim() : ''
78
+
79
+ if (!version) {
80
+ log(
81
+ `> Could not resolve the latest ${RUNTIME_PACKAGE} version from the npm registry ` +
82
+ '(offline, or the registry is unreachable); pinning "latest" instead.'
83
+ )
84
+ return 'latest'
85
+ }
86
+
87
+ return version
88
+ }
89
+
90
+ /**
91
+ * Orchestrates the scaffold. Everything the real `deka init` binary is
92
+ * responsible for (writing app/, public/, deka.json, deka.lock, printing
93
+ * next steps) is left to it; this only does what has to happen before that
94
+ * binary exists on disk.
95
+ *
96
+ * Returns 0 on success. Throws ScaffoldError (carrying an exitCode) for
97
+ * every expected failure; anything else is a bug and propagates.
98
+ */
99
+ export function createApp({
100
+ targetArg,
101
+ cwd = process.cwd(),
102
+ env = process.env,
103
+ platform = process.platform,
104
+ arch = process.arch,
105
+ spawn = spawnSync,
106
+ log = console.log,
107
+ } = {}) {
108
+ if (!targetArg) {
109
+ throw new ScaffoldError(USAGE, 1)
110
+ }
111
+
112
+ if (!isSupportedPlatform(platform, arch)) {
113
+ throw new ScaffoldError(
114
+ `create-deka-app does not support ${platformKey(platform, arch)} yet.\n` +
115
+ 'Supported today: darwin-arm64, darwin-x64, linux-x64.\n' +
116
+ 'Windows support is tracked at https://github.com/dekaruntime/deka/issues/1092.'
117
+ )
118
+ }
119
+
120
+ const targetDir = path.resolve(cwd, targetArg)
121
+
122
+ if (existsSync(targetDir)) {
123
+ const stat = statSync(targetDir)
124
+ if (!stat.isDirectory()) {
125
+ throw new ScaffoldError(`${targetDir} already exists and is not a directory.`)
126
+ }
127
+ if (readdirSync(targetDir).length > 0) {
128
+ throw new ScaffoldError(
129
+ `${targetDir} already exists and is not empty.\n` +
130
+ 'Choose a different directory, or remove its contents first.'
131
+ )
132
+ }
133
+ } else {
134
+ mkdirSync(targetDir, { recursive: true })
135
+ }
136
+
137
+ const runtimeVersion = resolveRuntimeVersion({ spawn, cwd: targetDir, log })
138
+
139
+ const pkg = buildPackageJson(path.basename(targetDir), runtimeVersion)
140
+ writeFileSync(path.join(targetDir, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`)
141
+
142
+ const pm = detectPackageManager(env)
143
+ log(`> Using ${pm.name} to install @dekaruntime/deka...`)
144
+
145
+ const [installCmd, installArgs] = pm.install
146
+ const install = spawn(installCmd, installArgs, { cwd: targetDir, stdio: 'inherit' })
147
+
148
+ if (install.error) {
149
+ throw new ScaffoldError(
150
+ `Could not run "${pm.name} install" in ${targetDir}: ${install.error.message}\n` +
151
+ `Make sure ${pm.name} is installed and on your PATH, then run it there yourself.`
152
+ )
153
+ }
154
+ if (install.status !== 0) {
155
+ throw new ScaffoldError(
156
+ `"${pm.name} install" failed in ${targetDir} (exit code ${install.status}).\n` +
157
+ 'Run it there yourself to see the full error.',
158
+ install.status ?? 1
159
+ )
160
+ }
161
+
162
+ const dekaBin = dekaBinPath(targetDir, platform)
163
+ if (!existsSync(dekaBin)) {
164
+ throw new ScaffoldError(
165
+ `${dekaBin} was not found after "${pm.name} install".\n` +
166
+ `@dekaruntime/deka may not have published a build for ${platformKey(platform, arch)} yet, ` +
167
+ 'or the install above did not complete. Check its output.'
168
+ )
169
+ }
170
+
171
+ const init = spawn(dekaBin, ['init'], { cwd: targetDir, stdio: 'inherit' })
172
+
173
+ if (init.error) {
174
+ throw new ScaffoldError(`Could not run "deka init" in ${targetDir}: ${init.error.message}`)
175
+ }
176
+ if (init.status !== 0) {
177
+ throw new ScaffoldError(
178
+ `"deka init" failed in ${targetDir} (exit code ${init.status}).`,
179
+ init.status ?? 1
180
+ )
181
+ }
182
+
183
+ return 0
184
+ }