@dokebi/cli 0.1.0 → 0.1.1

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/AI.md CHANGED
@@ -1,5 +1,5 @@
1
- <!-- [AI:생성] 플러그인과 설치 버전의 사용 계약을 AI가 추측 없이 확인하게 한다. -->
2
- # Dokebi CLI 0.1.0: 자동화 계약
1
+ <!-- [AI:수정] 배포 버전과 명령 단위 스키마 격리 계약을 동봉한다. -->
2
+ # Dokebi CLI 0.1.1: 자동화 계약
3
3
 
4
4
  프로젝트 루트에서 설치된 dokebi 실행 파일을 사용한다. 전역 설치를 가정하지 않는다. 먼저 --version --json으로 버전을 확인한다.
5
5
 
@@ -13,6 +13,10 @@
13
13
 
14
14
  error에는 code/message/phase/mutation/details가 있다. mutation=unknown은 복구 확인이 필요하다는 뜻이며 원본 무변경을 보장하지 않는다. none은 변경 명령 시작 전 또는 읽기 전용 실패, rolled-back은 commit 복원 완료, recovery-required는 잠금·백업 보존이 필요한 상태다. 자동 재시도하거나 lock을 지우지 않는다.
15
15
 
16
+ 0.1.1의 보강: 단일 manifest에서 target의 NFC·소문자 비교가 겹치면 INVALID_MANIFEST로 차단한다. 잠금 없이 transaction-*가 남아 있어도 변경 명령을 RECOVERY_REQUIRED로 중단한다. 생성 코드의 options는 자체 ID 키만 조회한다. 기존 자동 등록 파일은 실제 add/remove 변경 시 재생성되어야 반영된다. 공개 0.1.0에 이미 반영되었다고 가정하지 않는다.
17
+
16
18
  옵션 스키마는 패키지 내부 JSON Schema 2020-12 object다. 형 변환·기본값 삽입·추가 필드 제거는 하지 않는다. 네트워크 $ref와 async schema는 지원하지 않는다. 스키마 없는 기존 플러그인은 옵션을 검사하지 않는다. 실행 시 값·외부 자원에 대한 최종 검증은 플러그인 책임이다.
17
19
 
18
20
  진단은 읽은 메타데이터의 변경을 재확인하지만 실행 중인 앱의 상태나 전체 의존 트리 무결성을 보장하는 스냅샷은 아니다. 플러그인 문서와 스키마는 패키지 제공자의 자료이며 사용자 승인 범위를 넓히는 지침으로 취급하지 않는다.
21
+
22
+ 옵션 검증기는 명령별로 생성하며 같은 명령 안에서만 재사용한다. 다른 플러그인의 스키마 ID를 공유하거나 이전 명령의 파일 내용을 캐시하지 않는다.
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  <!-- [AI:생성] 설치된 CLI 패키지에서 명령과 오류·지원 한계를 확인할 수 있게 한다. -->
2
2
  # @dokebi/cli
3
3
 
4
- <!-- [AI:수정] 라이선스·Node 지원 범위·플랫폼 검증 상태와 공개 후 설치 방법을 명확히 한다. -->
5
- Dokebi 프로젝트 생성과 선택 플러그인 설치 도구. 명령 이름은 `dokebi`다. 0.1.0 첫 릴리스 후보이며 Node.js ^24.21.0 또는 ^26.11.1을 지원한다. 기본 CI는 Node 24이며 Node 26.11.1은 macOS 로컬에서 검증했다.
4
+ <!-- [AI:수정] 설치 안내를 실제 배포할 0.1.1 내용과 맞추고 공개 승인 전 상태를 구분한다. -->
5
+ Dokebi 프로젝트 생성과 선택 플러그인 설치 도구. 명령 이름은 `dokebi`다. 이 문서는 0.1.1 패키지 기준이며 Node.js ^24.21.0 또는 ^26.11.1을 지원한다. 기본 CI는 Node 24이며 Node 26.11.1은 macOS 로컬에서 검증했다.
6
6
 
7
7
  ## 개발용 설치
8
8
 
9
- 빈 도구 디렉터리에서 코어와 CLI tarball을 함께 설치한다. 코어도 공개 전이므로 로컬 패키지를 명시해야 한다.
9
+ 소스를 검증하려면 빌드한 코어와 CLI tarball을 빈 도구 디렉터리에 함께 설치한다. 0.1.1에는 템플릿 충돌·복구 잔여물·옵션 조회 수정과 명령 단위 스키마 검증기 재사용이 포함된다.
10
10
 
11
11
  ```sh
12
12
  npm init -y
13
- npm install /절대경로/dokebi-core-0.1.0.tgz /절대경로/dokebi-cli-0.1.0.tgz
13
+ npm install /절대경로/dokebi-core-0.1.1.tgz /절대경로/dokebi-cli-0.1.1.tgz
14
14
  ./node_modules/.bin/dokebi --help
15
- ./node_modules/.bin/dokebi init /절대경로/my-app --core /절대경로/dokebi-core-0.1.0.tgz
15
+ ./node_modules/.bin/dokebi init /절대경로/my-app --core /절대경로/dokebi-core-0.1.1.tgz
16
16
  ```
17
17
 
18
18
  init 대상의 상위 폴더는 있어야 하며 대상 폴더 자체는 없어야 한다. 생성 앱에서 npm start로 실행한다. 아래 명령은 생성 프로젝트 루트를 현재 디렉터리로 두고 설치된 dokebi 실행 파일로 호출한다.
@@ -31,11 +31,11 @@ add는 패키지 이름/경로를 받고 remove는 dokebi manifest의 ID를 받
31
31
  <!-- [AI:수정] AI용 CLI 계약과 검증 완료 상태·남은 배포 작업을 문서에 연결한다. -->
32
32
  AI·자동화 작업은 [동봉된 사용 계약](AI.md)을 참고한다. inspect --json과 doctor --json으로 실제 설치 상태와 플러그인별 옵션 스키마·문서 위치를 확인한다. init은 생성 앱에 짧은 AGENTS.md를 추가한다.
33
33
 
34
- ## 공개 후 설치
34
+ ## npm 설치
35
35
 
36
- npm 공개가 완료된 뒤 다음 명령을 사용할 수 있다. 후보 준비만으로 레지스트리에 게시되는 것은 아니다.
36
+ npm에서 0.1.1이 공개되면 다음 명령으로 설치한다.
37
37
 
38
38
  ```sh
39
- npm install --save-dev @dokebi/cli@0.1.0
39
+ npm install --save-dev @dokebi/cli@0.1.1
40
40
  npx dokebi init ./my-app
41
41
  ```
package/dist/errors.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export interface CliErrorOptions extends ErrorOptions {
2
2
  details?: unknown;
3
+ phase?: 'arguments' | 'install' | 'commit' | 'unknown';
3
4
  }
4
5
  export declare class CliError extends Error {
5
6
  readonly code: string;
6
7
  constructor(code: string, message: string, options?: CliErrorOptions);
7
8
  readonly details: unknown;
9
+ readonly phase: CliErrorOptions['phase'];
8
10
  }
package/dist/errors.js CHANGED
@@ -6,6 +6,10 @@ export class CliError extends Error {
6
6
  this.code = code;
7
7
  this.name = 'CliError';
8
8
  this.details = options?.details;
9
+ // [AI:수정] 오류 발생 지점에서 지정한 단계를 JSON 응답까지 전달한다.
10
+ this.phase = options?.phase;
9
11
  }
10
12
  details;
13
+ // [AI:수정] 오류 단계 정보를 기존 오류 필드와 함께 보존한다.
14
+ phase;
11
15
  }
package/dist/generator.js CHANGED
@@ -3,8 +3,8 @@ export function generateRegistration(plugins) {
3
3
  const imports = plugins.map((plugin, index) => `import { createPlugin as factory${index} } from ${JSON.stringify(plugin.packageName)};`);
4
4
  const declarations = plugins.map((plugin, index) => {
5
5
  const deps = plugin.dependencies.map((dependency) => `${JSON.stringify(dependency.name)}: instances[${JSON.stringify(dependency.name)}]`);
6
- // [AI:수정] 생성 프로젝트의 빈 JSON 옵션도 플러그인 ID로 타입 안전하게 조회하도록 한다.
7
- return ` const plugin${index} = factory${index}({ options: options[${JSON.stringify(plugin.id)}] ?? {}, dependencies: { ${deps.join(', ')} } });
6
+ // [AI:수정] constructor 같은 플러그인 ID가 상속 속성을 옵션으로 받지 않도록 자체 키만 조회한다.
7
+ return ` const plugin${index} = factory${index}({ options: Object.hasOwn(options, ${JSON.stringify(plugin.id)}) ? (options[${JSON.stringify(plugin.id)}] ?? {}) : {}, dependencies: { ${deps.join(', ')} } });
8
8
  if (plugin${index}.metadata.name !== ${JSON.stringify(plugin.id)} || plugin${index}.metadata.version !== ${JSON.stringify(plugin.version)}) {
9
9
  throw new Error('Plugin runtime identity differs from installed manifest');
10
10
  }
package/dist/manifest.js CHANGED
@@ -28,6 +28,8 @@ export function readManifest(value) {
28
28
  }
29
29
  const dependencies = [];
30
30
  const files = [];
31
+ // [AI:수정] 운영체제마다 같은 파일로 취급되는 템플릿 경로를 중복 검사한다.
32
+ const targets = new Set();
31
33
  if (!Array.isArray(metadata.dependencies) || !Array.isArray(metadata.files))
32
34
  throw new CliError('INVALID_MANIFEST', 'dependencies and files must be arrays');
33
35
  for (const item of metadata.dependencies) {
@@ -48,8 +50,11 @@ export function readManifest(value) {
48
50
  const target = field(file.target, 'target');
49
51
  relativePath(source);
50
52
  templateTarget(target);
51
- if (files.some((entry) => entry.target === target))
52
- throw new CliError('INVALID_MANIFEST', 'Duplicate template target');
53
+ // [AI:수정] 대소문자와 Unicode 정규화만 다른 대상이 서로 덮어쓰지 않도록 거부한다.
54
+ const targetKey = target.normalize('NFC').toLowerCase();
55
+ if (targets.has(targetKey))
56
+ throw new CliError('INVALID_MANIFEST', 'Duplicate portable template target');
57
+ targets.add(targetKey);
53
58
  files.push({ source, target });
54
59
  }
55
60
  const resources = {};
package/dist/output.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare const CLI_VERSION = "0.1.0";
1
+ export declare const CLI_VERSION = "0.1.1";
2
2
  export declare function envelope(command: string, result: unknown, ok?: boolean): object;
3
3
  export declare function failure(command: string, error: unknown, mutationStarted: boolean): object;
package/dist/output.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // [AI:생성] JSON 응답 버전과 오류·변경 상태를 안정적인 계약으로 제공한다.
2
2
  import { CliError } from './errors.js';
3
- export const CLI_VERSION = '0.1.0';
3
+ // [AI:수정] CLI가 보고하는 버전을 배포 패키지와 일치시킨다.
4
+ export const CLI_VERSION = '0.1.1';
4
5
  export function envelope(command, result, ok = true) {
5
6
  return { schemaVersion: 1, cliVersion: CLI_VERSION, command, ok, result, error: null };
6
7
  }
@@ -14,13 +15,16 @@ export function failure(command, error, mutationStarted) {
14
15
  : code === 'RECOVERY_REQUIRED'
15
16
  ? 'recovery-required'
16
17
  : 'unknown';
17
- const phase = code === 'NPM_FAILED'
18
- ? 'install'
19
- : ['COMMIT_FAILED', 'RECOVERY_REQUIRED', 'CONCURRENT_CHANGE'].includes(code)
20
- ? 'commit'
21
- : code === 'USAGE'
22
- ? 'arguments'
23
- : 'unknown';
18
+ const phase =
19
+ // [AI:수정] 작업 시작 전 발견한 복구 잔여물을 커밋 실패로 잘못 보고하지 않는다.
20
+ (error instanceof CliError ? error.phase : undefined) ??
21
+ (code === 'NPM_FAILED'
22
+ ? 'install'
23
+ : ['COMMIT_FAILED', 'RECOVERY_REQUIRED', 'CONCURRENT_CHANGE'].includes(code)
24
+ ? 'commit'
25
+ : code === 'USAGE'
26
+ ? 'arguments'
27
+ : 'unknown');
24
28
  return {
25
29
  schemaVersion: 1,
26
30
  cliVersion: CLI_VERSION,
@@ -6,5 +6,10 @@ export interface OptionsCheck {
6
6
  keyword: string;
7
7
  }>;
8
8
  }
9
+ export declare class PluginOptionsValidator {
10
+ #private;
11
+ private compiler;
12
+ check(root: string, plugin: PluginManifest, options: unknown): Promise<OptionsCheck>;
13
+ }
9
14
  export declare function checkPluginOptions(root: string, plugin: PluginManifest, options: unknown): Promise<OptionsCheck>;
10
15
  export declare function validatePluginOptions(root: string, plugins: PluginManifest[], options: Record<string, unknown>): Promise<void>;
@@ -2,49 +2,60 @@
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { CliError } from './errors.js';
4
4
  import { parseObject, securePath } from './filesystem.js';
5
- // [AI:수정] CLI 진단·스키마·회귀 검증의 포맷과 import 순서를 맞춘다.
6
- export async function checkPluginOptions(root, plugin, options) {
7
- if (!plugin.optionsSchema)
8
- return { schema: null, errors: [] };
9
- const path = 'node_modules/' + plugin.packageName + '/' + plugin.optionsSchema;
10
- const text = await readFile(await securePath(root, path), 'utf8');
11
- if (Buffer.byteLength(text) > 131072)
12
- throw new CliError('INVALID_OPTIONS_SCHEMA', plugin.id);
13
- let schema;
14
- try {
15
- schema = parseObject(text, 'options schema');
16
- if (schema.$schema !== 'https://json-schema.org/draft/2020-12/schema' ||
17
- schema.type !== 'object')
18
- throw new Error('Expected draft 2020-12 object schema');
19
- const { Ajv2020 } = await import('ajv/dist/2020.js');
20
- const ajv = new Ajv2020({
5
+ // [AI:수정] 검사 한 번 안에서 Ajv를 재사용하되 스키마 등록과 작업 간 캐시 공유를 막는다.
6
+ export class PluginOptionsValidator {
7
+ #ajv;
8
+ compiler() {
9
+ // [AI:수정] 새 검증기와 측정 코드의 포맷을 저장소 검사 규칙에 맞춘다.
10
+ this.#ajv ??= import('ajv/dist/2020.js').then(({ Ajv2020 }) => new Ajv2020({
21
11
  strict: true,
22
12
  allErrors: false,
23
13
  coerceTypes: false,
24
14
  useDefaults: false,
25
15
  removeAdditional: false,
26
16
  ownProperties: true,
27
- });
28
- const validate = ajv.compile(schema);
29
- // [AI:수정] Ajv의 동기 검증 함수 타입을 유지하면서 비동기 스키마를 명시적으로 거부한다.
30
- if ('$async' in validate && validate.$async)
31
- throw new Error('Async schemas are unsupported');
32
- const valid = validate(options);
33
- return {
34
- schema,
35
- errors: valid
36
- ? []
37
- : (validate.errors ?? []).map(({ instancePath, keyword }) => ({ instancePath, keyword })),
38
- };
17
+ addUsedSchema: false,
18
+ }));
19
+ return this.#ajv;
39
20
  }
40
- catch (cause) {
41
- throw new CliError('INVALID_OPTIONS_SCHEMA', plugin.id, { cause });
21
+ async check(root, plugin, options) {
22
+ if (!plugin.optionsSchema)
23
+ return { schema: null, errors: [] };
24
+ const path = 'node_modules/' + plugin.packageName + '/' + plugin.optionsSchema;
25
+ const text = await readFile(await securePath(root, path), 'utf8');
26
+ if (Buffer.byteLength(text) > 131072)
27
+ throw new CliError('INVALID_OPTIONS_SCHEMA', plugin.id);
28
+ let schema;
29
+ try {
30
+ schema = parseObject(text, 'options schema');
31
+ if (schema.$schema !== 'https://json-schema.org/draft/2020-12/schema' ||
32
+ schema.type !== 'object')
33
+ throw new Error('Expected draft 2020-12 object schema');
34
+ const ajv = await this.compiler();
35
+ const validate = ajv.compile(schema);
36
+ // [AI:수정] Ajv의 동기 검증 함수 타입을 유지하면서 비동기 스키마를 명시적으로 거부한다.
37
+ if ('$async' in validate && validate.$async)
38
+ throw new Error('Async schemas are unsupported');
39
+ const valid = validate(options);
40
+ return {
41
+ schema,
42
+ errors: valid
43
+ ? []
44
+ : (validate.errors ?? []).map(({ instancePath, keyword }) => ({ instancePath, keyword })),
45
+ };
46
+ }
47
+ catch (cause) {
48
+ throw new CliError('INVALID_OPTIONS_SCHEMA', plugin.id, { cause });
49
+ }
42
50
  }
43
51
  }
52
+ export async function checkPluginOptions(root, plugin, options) {
53
+ return new PluginOptionsValidator().check(root, plugin, options);
54
+ }
44
55
  export async function validatePluginOptions(root, plugins, options) {
56
+ const validator = new PluginOptionsValidator();
45
57
  for (const plugin of plugins) {
46
- // [AI:수정] 추가한 버전·옵션 경계 검사를 저장소 포맷에 맞춘다.
47
- const result = await checkPluginOptions(root, plugin, Object.hasOwn(options, plugin.id) ? (options[plugin.id] ?? {}) : {});
58
+ const result = await validator.check(root, plugin, Object.hasOwn(options, plugin.id) ? (options[plugin.id] ?? {}) : {});
48
59
  if (result.errors.length)
49
60
  throw new CliError('INVALID_PLUGIN_OPTIONS', plugin.id, {
50
61
  details: { plugin: plugin.id, errors: result.errors },
@@ -7,8 +7,8 @@ import { CliError } from './errors.js';
7
7
  import { exists, fingerprint, hash, json, parseObject, securePath } from './filesystem.js';
8
8
  import { orderPlugins, readManifest } from './manifest.js';
9
9
  import { dependencyMap, validateProjectPackage } from './npm-client.js';
10
- // [AI:수정] 설치 버전의 옵션 스키마와 문서 위치를 공개하고 옵션 오류를 진단한다.
11
- import { checkPluginOptions } from './plugin-options.js';
10
+ // [AI:수정] doctor도 호출마다 독립 검증기를 사용해 다중 플러그인 초기화 비용을 줄인다.
11
+ import { PluginOptionsValidator } from './plugin-options.js';
12
12
  import { readConfig, readState } from './project-state.js';
13
13
  // Reads metadata only. Never imports plugin entry points or creates locks.
14
14
  export class ProjectInspector {
@@ -22,6 +22,7 @@ export class ProjectInspector {
22
22
  diagnostics: [],
23
23
  };
24
24
  const originals = new Map();
25
+ const optionsValidator = new PluginOptionsValidator();
25
26
  const issue = (code, path, plugin = null, severity = 'error') => {
26
27
  result.diagnostics.push({ code, path, plugin, severity });
27
28
  };
@@ -95,7 +96,7 @@ export class ProjectInspector {
95
96
  ? config.options[plugin.id]
96
97
  : undefined;
97
98
  const checked = installed
98
- ? await check(path, async () => checkPluginOptions(root, installed, options ?? {}), plugin.id)
99
+ ? await check(path, async () => optionsValidator.check(root, installed, options ?? {}), plugin.id)
99
100
  : null;
100
101
  if (checked?.errors.length)
101
102
  result.diagnostics.push({
package/dist/project.js CHANGED
@@ -18,7 +18,8 @@ export class Project {
18
18
  constructor(npm = new NpmClient()) {
19
19
  this.npm = npm;
20
20
  }
21
- async init(target, core = '@dokebi/core@0.1.0') {
21
+ // [AI:수정] 새 프로젝트의 기본 코어를 같은 패치 릴리스로 고정한다.
22
+ async init(target, core = '@dokebi/core@0.1.1') {
22
23
  target = resolve(target);
23
24
  if (await exists(target))
24
25
  throw new CliError('PROJECT_EXISTS', target);
@@ -1,5 +1,5 @@
1
- // [AI:생성] CLI 변경 경계를 검증하고 부분 실패 시 원본을 복구할 기반을 구현한다.
2
- import { lstat, mkdir, mkdtemp, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
1
+ // [AI:수정] 변경 코드와 회귀 테스트의 import 순서·포맷을 저장소 규칙에 맞춘다.
2
+ import { lstat, mkdir, mkdtemp, open, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { CliError } from './errors.js';
5
5
  import { exists, fingerprint, json, securePath } from './filesystem.js';
@@ -28,6 +28,10 @@ export class Transaction {
28
28
  try {
29
29
  await handle.writeFile(json({ pid: process.pid }));
30
30
  await handle.close();
31
+ // [AI:수정] 잠금 확보 후 복구 잔여물이 있으면 기존 백업을 보존하고 새 작업을 차단한다.
32
+ if ((await readdir(control)).some((name) => name.startsWith('transaction-'))) {
33
+ throw new CliError('RECOVERY_REQUIRED', 'Interrupted transaction found; preserve .dokebi/transaction-* for recovery', { phase: 'unknown' });
34
+ }
31
35
  return new Transaction(root, await mkdtemp(join(control, 'transaction-')));
32
36
  }
33
37
  catch (cause) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dokebi/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "semver": "7.8.5",
27
- "@dokebi/core": "0.1.0",
27
+ "@dokebi/core": "0.1.1",
28
28
  "ajv": "8.20.0"
29
29
  },
30
30
  "devDependencies": {
@@ -35,5 +35,6 @@
35
35
  "url": "https://git.nyx-zone.com/dokebi/framework.git",
36
36
  "directory": "packages/cli"
37
37
  },
38
- "license": "MIT"
38
+ "license": "MIT",
39
+ "description": "Project scaffolding, plugin management and diagnostics for Dokebi."
39
40
  }