@uns-kit/cli 2.0.17 → 2.0.19
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 +18 -0
- package/dist/index.js +238 -61
- package/dist/service-bundle.d.ts +55 -0
- package/dist/service-bundle.js +239 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -43,9 +43,27 @@ pnpm run dev
|
|
|
43
43
|
|
|
44
44
|
When `git` is available on your PATH the scaffold also initializes a fresh repository so you can commit immediately.
|
|
45
45
|
|
|
46
|
+
### Create from a service bundle
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uns-kit create --bundle ./service.bundle.json
|
|
50
|
+
uns-kit create --bundle ./service.bundle.json --dest ./my-dir
|
|
51
|
+
uns-kit create --bundle ./service.bundle.json --dest . --allow-existing
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Bundle-driven create uses `service.bundle.json` as the source of truth. The CLI:
|
|
55
|
+
|
|
56
|
+
- scaffolds the base TypeScript app from the existing default template
|
|
57
|
+
- copies the original bundle into the project root as `service.bundle.json`
|
|
58
|
+
- generates `SERVICE_SPEC.md` and `AGENTS.md`
|
|
59
|
+
- applies supported bundle features such as `vscode` and `devops`
|
|
60
|
+
|
|
61
|
+
When `--bundle` is used, the default destination is `./<metadata.name>`. The TypeScript CLI only accepts bundles with `scaffold.stack = "ts"` and currently supports `scaffold.template = "default"` for this MVP. If the bundle targets Python instead, use `uns-kit-py create --bundle ...`.
|
|
62
|
+
|
|
46
63
|
## Commands
|
|
47
64
|
|
|
48
65
|
- `uns-kit create <name>` – create a new UNS project in the specified directory.
|
|
66
|
+
- `uns-kit create --bundle <path> [--dest <dir>] [--allow-existing]` – create a new TypeScript UNS project from `service.bundle.json`.
|
|
49
67
|
- `uns-kit configure [path] [features...]` – run multiple configure templates in sequence (`--all`, `--overwrite`).
|
|
50
68
|
- `uns-kit configure-templates [path] [templates...]` – copy any template directory (`--all`, `--overwrite`).
|
|
51
69
|
- `uns-kit configure-devops [path]` – add Azure DevOps tooling (dependencies, script, config) to an existing project.
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import process from "node:process";
|
|
|
9
9
|
import readline from "node:readline/promises";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import * as azdev from "azure-devops-node-api";
|
|
12
|
+
import { generateAgentsMarkdown, generateServiceSpecMarkdown, readAndValidateServiceBundle, } from "./service-bundle.js";
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
@@ -145,14 +146,17 @@ async function main() {
|
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
147
148
|
if (command === "create") {
|
|
148
|
-
const projectName = args[1];
|
|
149
|
-
if (!projectName) {
|
|
150
|
-
console.error("Missing project name. Example: uns-kit create my-app");
|
|
151
|
-
process.exitCode = 1;
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
149
|
try {
|
|
155
|
-
|
|
150
|
+
const createOptions = parseCreateArgs(args.slice(1));
|
|
151
|
+
if (createOptions.bundlePath) {
|
|
152
|
+
await createProjectFromBundle(createOptions);
|
|
153
|
+
}
|
|
154
|
+
else if (createOptions.projectName) {
|
|
155
|
+
await createProject(createOptions.projectName);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
throw new Error("Missing project name. Example: uns-kit create my-app");
|
|
159
|
+
}
|
|
156
160
|
}
|
|
157
161
|
catch (error) {
|
|
158
162
|
console.error(error.message);
|
|
@@ -169,6 +173,7 @@ function printHelp() {
|
|
|
169
173
|
"\nUsage: uns-kit <command> [options]\n" +
|
|
170
174
|
"\nCommands:\n" +
|
|
171
175
|
" create <name> Scaffold a new UNS application\n" +
|
|
176
|
+
" create --bundle <path> Scaffold a new UNS application from service.bundle.json\n" +
|
|
172
177
|
" configure [dir] [features...] Configure multiple templates (--all, --overwrite)\n" +
|
|
173
178
|
" configure-templates [dir] [templates...] Copy any template directory (--all, --overwrite)\n" +
|
|
174
179
|
" configure-devops [dir] Configure Azure DevOps tooling in an existing project\n" +
|
|
@@ -181,25 +186,146 @@ function printHelp() {
|
|
|
181
186
|
" configure-uns-reference [dir] Copy UNS dictionaries (objects/attributes/measurements)\n" +
|
|
182
187
|
" help Show this message\n");
|
|
183
188
|
}
|
|
189
|
+
function parseCreateArgs(args) {
|
|
190
|
+
let projectName;
|
|
191
|
+
let bundlePath;
|
|
192
|
+
let destPath;
|
|
193
|
+
let allowExisting = false;
|
|
194
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
195
|
+
const arg = args[index];
|
|
196
|
+
if (arg === "--bundle") {
|
|
197
|
+
const next = args[index + 1];
|
|
198
|
+
if (!next || next.startsWith("--")) {
|
|
199
|
+
throw new Error("Missing value for --bundle.");
|
|
200
|
+
}
|
|
201
|
+
bundlePath = next;
|
|
202
|
+
index += 1;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (arg === "--dest") {
|
|
206
|
+
const next = args[index + 1];
|
|
207
|
+
if (!next || next.startsWith("--")) {
|
|
208
|
+
throw new Error("Missing value for --dest.");
|
|
209
|
+
}
|
|
210
|
+
destPath = next;
|
|
211
|
+
index += 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (arg === "--allow-existing") {
|
|
215
|
+
allowExisting = true;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (arg.startsWith("--")) {
|
|
219
|
+
throw new Error(`Unknown option ${arg}.`);
|
|
220
|
+
}
|
|
221
|
+
if (!projectName) {
|
|
222
|
+
projectName = arg;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`Unexpected argument ${arg}.`);
|
|
226
|
+
}
|
|
227
|
+
if (bundlePath && projectName) {
|
|
228
|
+
throw new Error("Do not pass a positional project name with --bundle. Use --dest to override the target directory.");
|
|
229
|
+
}
|
|
230
|
+
if (!bundlePath && destPath) {
|
|
231
|
+
throw new Error("--dest can only be used with --bundle.");
|
|
232
|
+
}
|
|
233
|
+
if (!bundlePath && allowExisting) {
|
|
234
|
+
throw new Error("--allow-existing can only be used with --bundle.");
|
|
235
|
+
}
|
|
236
|
+
return { projectName, bundlePath, destPath, allowExisting };
|
|
237
|
+
}
|
|
184
238
|
async function createProject(projectName) {
|
|
185
239
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
186
|
-
await
|
|
187
|
-
|
|
240
|
+
const result = await scaffoldTsProject(projectName, targetDir);
|
|
241
|
+
printTsCreateSuccess(targetDir, result.packageName, initializedGitNextSteps(result.initializedGit, "pnpm run dev"));
|
|
242
|
+
}
|
|
243
|
+
async function createProjectFromBundle(options) {
|
|
244
|
+
if (!options.bundlePath) {
|
|
245
|
+
throw new Error("Missing --bundle path.");
|
|
246
|
+
}
|
|
247
|
+
const { bundle, raw } = await readAndValidateServiceBundle(options.bundlePath, {
|
|
248
|
+
expectedStack: "ts",
|
|
249
|
+
cliName: "uns-kit",
|
|
250
|
+
counterpartCliName: "uns-kit-py",
|
|
251
|
+
});
|
|
252
|
+
const targetDir = path.resolve(process.cwd(), options.destPath ?? bundle.metadata.name);
|
|
253
|
+
const result = await scaffoldTsProject(bundle.metadata.name, targetDir, {
|
|
254
|
+
allowExisting: options.allowExisting,
|
|
255
|
+
templateName: bundle.scaffold.template,
|
|
256
|
+
});
|
|
257
|
+
await applyTsBundleFeatures(targetDir, bundle);
|
|
258
|
+
await writeServiceBundleArtifacts(targetDir, bundle, raw);
|
|
259
|
+
printTsCreateSuccess(targetDir, result.packageName, initializedGitNextSteps(result.initializedGit, "pnpm run dev"));
|
|
260
|
+
}
|
|
261
|
+
async function scaffoldTsProject(projectName, targetDir, options = {}) {
|
|
262
|
+
await ensureTargetDir(targetDir, { allowExisting: options.allowExisting });
|
|
263
|
+
const templateDir = path.resolve(__dirname, `../templates/${options.templateName ?? DEFAULT_TEMPLATE_NAME}`);
|
|
264
|
+
try {
|
|
265
|
+
await access(templateDir);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
throw new Error(`Template directory is missing: ${templateDir}`);
|
|
269
|
+
}
|
|
188
270
|
await copyTemplateDirectory(templateDir, targetDir, targetDir);
|
|
189
|
-
// Seed config examples automatically (previously required configure-config).
|
|
190
271
|
await configureConfigFiles(targetDir, { overwrite: false });
|
|
191
|
-
const
|
|
192
|
-
await patchPackageJson(targetDir,
|
|
193
|
-
await patchConfigJson(targetDir,
|
|
194
|
-
await replacePlaceholders(targetDir,
|
|
272
|
+
const packageName = normalizePackageName(projectName);
|
|
273
|
+
await patchPackageJson(targetDir, packageName);
|
|
274
|
+
await patchConfigJson(targetDir, packageName);
|
|
275
|
+
await replacePlaceholders(targetDir, packageName);
|
|
195
276
|
const initializedGit = await initGitRepository(targetDir);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
277
|
+
return { packageName, initializedGit };
|
|
278
|
+
}
|
|
279
|
+
async function applyTsBundleFeatures(targetDir, bundle) {
|
|
280
|
+
for (const featureName of bundle.scaffold.features) {
|
|
281
|
+
const resolvedFeature = resolveConfigureFeatureName(featureName);
|
|
282
|
+
if (resolvedFeature === "devops") {
|
|
283
|
+
await configureDevopsFromBundle(targetDir, bundle);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const handler = configureFeatureHandlers[resolvedFeature];
|
|
287
|
+
await handler(targetDir, { overwrite: false });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function configureDevopsFromBundle(targetDir, bundle) {
|
|
291
|
+
const provider = bundle.repository?.provider?.trim() || AZURE_DEVOPS_PROVIDER;
|
|
292
|
+
if (provider !== AZURE_DEVOPS_PROVIDER) {
|
|
293
|
+
throw new Error(`Bundle feature "devops" only supports repository.provider="${AZURE_DEVOPS_PROVIDER}" in this MVP. Received "${provider}".`);
|
|
294
|
+
}
|
|
295
|
+
const organization = bundle.repository?.organization?.trim();
|
|
296
|
+
const project = bundle.repository?.project?.trim();
|
|
297
|
+
if (!organization || !project) {
|
|
298
|
+
throw new Error('Bundle feature "devops" requires repository.organization and repository.project for non-interactive scaffolding.');
|
|
299
|
+
}
|
|
300
|
+
const result = await applyAzureDevopsConfig(targetDir, {
|
|
301
|
+
organization,
|
|
302
|
+
project,
|
|
303
|
+
overwrite: false,
|
|
304
|
+
ensureRemote: false,
|
|
305
|
+
});
|
|
306
|
+
logDevopsResult(result, { includeRemoteDetails: false });
|
|
307
|
+
}
|
|
308
|
+
async function writeServiceBundleArtifacts(targetDir, bundle, rawBundle) {
|
|
309
|
+
await writeFile(path.join(targetDir, "service.bundle.json"), rawBundle, "utf8");
|
|
310
|
+
await writeFile(path.join(targetDir, "SERVICE_SPEC.md"), generateServiceSpecMarkdown(bundle), "utf8");
|
|
311
|
+
await writeFile(path.join(targetDir, "AGENTS.md"), generateAgentsMarkdown(bundle), "utf8");
|
|
312
|
+
}
|
|
313
|
+
function initializedGitNextSteps(initializedGit, runCommand) {
|
|
314
|
+
const steps = ["pnpm install", runCommand];
|
|
201
315
|
if (initializedGit) {
|
|
202
|
-
|
|
316
|
+
steps.push("git status # verify the new repository");
|
|
317
|
+
}
|
|
318
|
+
return steps;
|
|
319
|
+
}
|
|
320
|
+
function printTsCreateSuccess(targetDir, packageName, nextSteps) {
|
|
321
|
+
const relativeTarget = path.relative(process.cwd(), targetDir) || ".";
|
|
322
|
+
console.log(`\nCreated ${packageName} in ${relativeTarget}`);
|
|
323
|
+
console.log("Next steps:");
|
|
324
|
+
if (relativeTarget !== ".") {
|
|
325
|
+
console.log(` cd ${relativeTarget}`);
|
|
326
|
+
}
|
|
327
|
+
for (const step of nextSteps) {
|
|
328
|
+
console.log(` ${step}`);
|
|
203
329
|
}
|
|
204
330
|
}
|
|
205
331
|
async function initGitRepository(targetDir) {
|
|
@@ -250,8 +376,8 @@ async function initGitRepository(targetDir) {
|
|
|
250
376
|
}
|
|
251
377
|
async function configureDevops(targetPath, options) {
|
|
252
378
|
const targetDir = path.resolve(process.cwd(), targetPath ?? ".");
|
|
379
|
+
await ensureGitRepository(targetDir);
|
|
253
380
|
const packagePath = path.join(targetDir, "package.json");
|
|
254
|
-
const configPath = path.join(targetDir, "config.json");
|
|
255
381
|
let pkgRaw;
|
|
256
382
|
try {
|
|
257
383
|
pkgRaw = await readFile(packagePath, "utf8");
|
|
@@ -262,6 +388,8 @@ async function configureDevops(targetPath, options) {
|
|
|
262
388
|
}
|
|
263
389
|
throw error;
|
|
264
390
|
}
|
|
391
|
+
const pkg = JSON.parse(pkgRaw);
|
|
392
|
+
const configPath = path.join(targetDir, "config.json");
|
|
265
393
|
let configRaw;
|
|
266
394
|
try {
|
|
267
395
|
configRaw = await readFile(configPath, "utf8");
|
|
@@ -272,9 +400,7 @@ async function configureDevops(targetPath, options) {
|
|
|
272
400
|
}
|
|
273
401
|
throw error;
|
|
274
402
|
}
|
|
275
|
-
const pkg = JSON.parse(pkgRaw);
|
|
276
403
|
const config = JSON.parse(configRaw);
|
|
277
|
-
await ensureGitRepository(targetDir);
|
|
278
404
|
const remoteUrl = await getGitRemoteUrl(targetDir, "origin");
|
|
279
405
|
const remoteInfo = remoteUrl ? parseAzureRemote(remoteUrl) : undefined;
|
|
280
406
|
const repositoryName = inferRepositoryNameFromPackage(pkg.name) || inferRepositoryNameFromPackage(path.basename(targetDir));
|
|
@@ -282,44 +408,85 @@ async function configureDevops(targetPath, options) {
|
|
|
282
408
|
const organization = await promptWithDefault(defaultOrganization ? `Azure DevOps organization [${defaultOrganization}]: ` : "Azure DevOps organization: ", defaultOrganization, "Azure DevOps organization is required.");
|
|
283
409
|
const defaultProject = config.devops?.project?.trim() || remoteInfo?.project || "";
|
|
284
410
|
const project = await promptWithDefault(defaultProject ? `Azure DevOps project [${defaultProject}]: ` : "Azure DevOps project: ", defaultProject, "Azure DevOps project is required.");
|
|
411
|
+
const result = await applyAzureDevopsConfig(targetDir, {
|
|
412
|
+
organization,
|
|
413
|
+
project,
|
|
414
|
+
overwrite: options?.overwrite,
|
|
415
|
+
ensureRemote: true,
|
|
416
|
+
repositoryName: remoteInfo?.repository ?? repositoryName,
|
|
417
|
+
});
|
|
418
|
+
logDevopsResult(result, { includeRemoteDetails: true });
|
|
419
|
+
}
|
|
420
|
+
async function applyAzureDevopsConfig(targetDir, options) {
|
|
421
|
+
const packagePath = path.join(targetDir, "package.json");
|
|
422
|
+
const configPath = path.join(targetDir, "config.json");
|
|
423
|
+
let pkgRaw;
|
|
424
|
+
try {
|
|
425
|
+
pkgRaw = await readFile(packagePath, "utf8");
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
if (error.code === "ENOENT") {
|
|
429
|
+
throw new Error(`Could not find package.json in ${targetDir}`);
|
|
430
|
+
}
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
let configRaw;
|
|
434
|
+
try {
|
|
435
|
+
configRaw = await readFile(configPath, "utf8");
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
if (error.code === "ENOENT") {
|
|
439
|
+
throw new Error(`Could not find config.json in ${targetDir}`);
|
|
440
|
+
}
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
const pkg = JSON.parse(pkgRaw);
|
|
444
|
+
const config = JSON.parse(configRaw);
|
|
285
445
|
if (!config.devops || typeof config.devops !== "object") {
|
|
286
446
|
config.devops = {};
|
|
287
447
|
}
|
|
288
448
|
const devopsConfig = config.devops;
|
|
289
449
|
devopsConfig.provider = AZURE_DEVOPS_PROVIDER;
|
|
290
|
-
devopsConfig.organization = organization;
|
|
291
|
-
devopsConfig.project = project;
|
|
450
|
+
devopsConfig.organization = options.organization;
|
|
451
|
+
devopsConfig.project = options.project;
|
|
292
452
|
let gitRemoteMessage;
|
|
293
453
|
let repositoryUrlMessage;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
454
|
+
if (options.ensureRemote) {
|
|
455
|
+
const repositoryName = options.repositoryName
|
|
456
|
+
?? inferRepositoryNameFromPackage(pkg.name)
|
|
457
|
+
?? inferRepositoryNameFromPackage(path.basename(targetDir));
|
|
458
|
+
const remoteUrl = await getGitRemoteUrl(targetDir, "origin");
|
|
459
|
+
const remoteInfo = remoteUrl ? parseAzureRemote(remoteUrl) : undefined;
|
|
460
|
+
if (!remoteUrl) {
|
|
461
|
+
const { gitApi } = await resolveAzureGitApi(options.organization);
|
|
462
|
+
const repositoryDetails = await ensureAzureRepositoryExists(gitApi, {
|
|
463
|
+
organization: options.organization,
|
|
464
|
+
project: options.project,
|
|
465
|
+
repository: repositoryName,
|
|
466
|
+
});
|
|
467
|
+
const ensuredRemoteUrl = repositoryDetails.remoteUrl
|
|
468
|
+
?? buildAzureGitRemoteUrl(options.organization, options.project, repositoryName);
|
|
469
|
+
await addGitRemote(targetDir, "origin", ensuredRemoteUrl);
|
|
470
|
+
gitRemoteMessage = ` Added git remote origin -> ${ensuredRemoteUrl}`;
|
|
471
|
+
const friendlyUrl = repositoryDetails.webUrl
|
|
472
|
+
?? buildAzureRepositoryUrl(options.organization, options.project, repositoryName);
|
|
473
|
+
if (friendlyUrl) {
|
|
474
|
+
repositoryUrlMessage = ` Repository URL: ${friendlyUrl}`;
|
|
475
|
+
}
|
|
308
476
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
repositoryUrlMessage = ` Repository URL: ${friendlyUrl}`;
|
|
477
|
+
else {
|
|
478
|
+
gitRemoteMessage = ` Git remote origin detected -> ${remoteUrl}`;
|
|
479
|
+
const friendlyUrl = buildAzureRepositoryUrl(remoteInfo?.organization ?? options.organization, remoteInfo?.project ?? options.project, remoteInfo?.repository ?? repositoryName);
|
|
480
|
+
if (friendlyUrl) {
|
|
481
|
+
repositoryUrlMessage = ` Repository URL: ${friendlyUrl}`;
|
|
482
|
+
}
|
|
316
483
|
}
|
|
317
484
|
}
|
|
318
485
|
const requiredDevDeps = {
|
|
319
486
|
"azure-devops-node-api": "^15.1.0",
|
|
320
487
|
"simple-git": "^3.27.0",
|
|
321
488
|
"chalk": "^5.4.1",
|
|
322
|
-
"prettier": "^3.5.3"
|
|
489
|
+
"prettier": "^3.5.3",
|
|
323
490
|
};
|
|
324
491
|
let pkgChanged = false;
|
|
325
492
|
const devDeps = (pkg.devDependencies ??= {});
|
|
@@ -343,10 +510,9 @@ async function configureDevops(targetPath, options) {
|
|
|
343
510
|
throw new Error("Azure Pipelines template is missing. Please ensure templates/azure-pipelines.yml exists.");
|
|
344
511
|
}
|
|
345
512
|
const pipelineTargetPath = path.join(targetDir, "azure-pipelines.yml");
|
|
346
|
-
const overwrite = !!options?.overwrite;
|
|
347
513
|
let pipelineMessage = "";
|
|
348
514
|
if (await fileExists(pipelineTargetPath)) {
|
|
349
|
-
if (overwrite) {
|
|
515
|
+
if (options.overwrite) {
|
|
350
516
|
await copyFile(azurePipelineTemplatePath, pipelineTargetPath);
|
|
351
517
|
pipelineMessage = " Overwrote azure-pipelines.yml pipeline definition.";
|
|
352
518
|
}
|
|
@@ -362,20 +528,31 @@ async function configureDevops(targetPath, options) {
|
|
|
362
528
|
if (pkgChanged) {
|
|
363
529
|
await writeFile(packagePath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
364
530
|
}
|
|
531
|
+
return {
|
|
532
|
+
provider: AZURE_DEVOPS_PROVIDER,
|
|
533
|
+
organization: options.organization,
|
|
534
|
+
project: options.project,
|
|
535
|
+
pkgChanged,
|
|
536
|
+
pipelineMessage,
|
|
537
|
+
gitRemoteMessage,
|
|
538
|
+
repositoryUrlMessage,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function logDevopsResult(result, options) {
|
|
365
542
|
console.log(`\nDevOps tooling configured.`);
|
|
366
|
-
console.log(` DevOps provider: ${
|
|
367
|
-
console.log(` Azure organization: ${organization}`);
|
|
368
|
-
console.log(` Azure project: ${project}`);
|
|
369
|
-
if (repositoryUrlMessage) {
|
|
370
|
-
console.log(repositoryUrlMessage);
|
|
543
|
+
console.log(` DevOps provider: ${result.provider}`);
|
|
544
|
+
console.log(` Azure organization: ${result.organization}`);
|
|
545
|
+
console.log(` Azure project: ${result.project}`);
|
|
546
|
+
if (options.includeRemoteDetails && result.repositoryUrlMessage) {
|
|
547
|
+
console.log(result.repositoryUrlMessage);
|
|
371
548
|
}
|
|
372
|
-
if (gitRemoteMessage) {
|
|
373
|
-
console.log(gitRemoteMessage);
|
|
549
|
+
if (options.includeRemoteDetails && result.gitRemoteMessage) {
|
|
550
|
+
console.log(result.gitRemoteMessage);
|
|
374
551
|
}
|
|
375
|
-
if (pipelineMessage) {
|
|
376
|
-
console.log(pipelineMessage);
|
|
552
|
+
if (result.pipelineMessage) {
|
|
553
|
+
console.log(result.pipelineMessage);
|
|
377
554
|
}
|
|
378
|
-
if (pkgChanged) {
|
|
555
|
+
if (result.pkgChanged) {
|
|
379
556
|
console.log(" Updated package.json scripts/devDependencies. Run pnpm install to fetch new packages.");
|
|
380
557
|
}
|
|
381
558
|
else {
|
|
@@ -1204,14 +1381,14 @@ async function fileExists(filePath) {
|
|
|
1204
1381
|
throw error;
|
|
1205
1382
|
}
|
|
1206
1383
|
}
|
|
1207
|
-
async function ensureTargetDir(dir) {
|
|
1384
|
+
async function ensureTargetDir(dir, options = {}) {
|
|
1208
1385
|
try {
|
|
1209
1386
|
const stats = await stat(dir);
|
|
1210
1387
|
if (!stats.isDirectory()) {
|
|
1211
1388
|
throw new Error(`Path ${dir} exists and is not a directory.`);
|
|
1212
1389
|
}
|
|
1213
1390
|
const entries = await readdir(dir);
|
|
1214
|
-
if (entries.length > 0) {
|
|
1391
|
+
if (entries.length > 0 && !options.allowExisting) {
|
|
1215
1392
|
throw new Error(`Directory ${dir} is not empty.`);
|
|
1216
1393
|
}
|
|
1217
1394
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type SupportedBundleStack = "ts" | "python";
|
|
2
|
+
export type ServiceBundle = {
|
|
3
|
+
schemaVersion: 1;
|
|
4
|
+
kind: "uns-service-bundle";
|
|
5
|
+
metadata: {
|
|
6
|
+
name: string;
|
|
7
|
+
displayName?: string;
|
|
8
|
+
serviceType?: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
owner?: string;
|
|
12
|
+
tags: string[];
|
|
13
|
+
};
|
|
14
|
+
scaffold: {
|
|
15
|
+
stack: SupportedBundleStack | string;
|
|
16
|
+
template: string;
|
|
17
|
+
features: string[];
|
|
18
|
+
};
|
|
19
|
+
repository?: {
|
|
20
|
+
provider?: string;
|
|
21
|
+
organization?: string;
|
|
22
|
+
project?: string;
|
|
23
|
+
repository?: string;
|
|
24
|
+
defaultBranch?: string;
|
|
25
|
+
};
|
|
26
|
+
domain?: {
|
|
27
|
+
inputs: unknown[];
|
|
28
|
+
outputs: unknown[];
|
|
29
|
+
};
|
|
30
|
+
docs: {
|
|
31
|
+
serviceSpec: {
|
|
32
|
+
goals: string[];
|
|
33
|
+
nonGoals: string[];
|
|
34
|
+
acceptanceCriteria: string[];
|
|
35
|
+
};
|
|
36
|
+
agents: {
|
|
37
|
+
projectContext: string[];
|
|
38
|
+
guardrails: string[];
|
|
39
|
+
firstTasks: string[];
|
|
40
|
+
verification: string[];
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
type ReadBundleOptions = {
|
|
45
|
+
expectedStack: SupportedBundleStack;
|
|
46
|
+
cliName: string;
|
|
47
|
+
counterpartCliName: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function readAndValidateServiceBundle(bundlePath: string, options: ReadBundleOptions): Promise<{
|
|
50
|
+
bundle: ServiceBundle;
|
|
51
|
+
raw: string;
|
|
52
|
+
}>;
|
|
53
|
+
export declare function generateServiceSpecMarkdown(bundle: ServiceBundle): string;
|
|
54
|
+
export declare function generateAgentsMarkdown(bundle: ServiceBundle): string;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export async function readAndValidateServiceBundle(bundlePath, options) {
|
|
3
|
+
const raw = await readFile(bundlePath, "utf8");
|
|
4
|
+
const parsed = parseJsonObject(raw, bundlePath);
|
|
5
|
+
const bundle = validateServiceBundle(parsed, options);
|
|
6
|
+
return { bundle, raw };
|
|
7
|
+
}
|
|
8
|
+
export function generateServiceSpecMarkdown(bundle) {
|
|
9
|
+
const lines = [
|
|
10
|
+
"# SERVICE_SPEC",
|
|
11
|
+
"",
|
|
12
|
+
"> Generated from `service.bundle.json`. Update the bundle as the source of truth.",
|
|
13
|
+
"",
|
|
14
|
+
"## Service Identity",
|
|
15
|
+
...renderKeyValueList([
|
|
16
|
+
["Name", bundle.metadata.name],
|
|
17
|
+
["Display Name", bundle.metadata.displayName],
|
|
18
|
+
["Service Type", bundle.metadata.serviceType],
|
|
19
|
+
["Owner", bundle.metadata.owner],
|
|
20
|
+
["Tags", bundle.metadata.tags.length ? bundle.metadata.tags.join(", ") : "None"],
|
|
21
|
+
]),
|
|
22
|
+
"",
|
|
23
|
+
"## Summary",
|
|
24
|
+
"",
|
|
25
|
+
bundle.metadata.summary?.trim() || "Not provided.",
|
|
26
|
+
"",
|
|
27
|
+
"## Description",
|
|
28
|
+
"",
|
|
29
|
+
bundle.metadata.description?.trim() || "Not provided.",
|
|
30
|
+
"",
|
|
31
|
+
"## Scaffold",
|
|
32
|
+
...renderKeyValueList([
|
|
33
|
+
["Stack", bundle.scaffold.stack],
|
|
34
|
+
["Template", bundle.scaffold.template],
|
|
35
|
+
["Features", bundle.scaffold.features.length ? bundle.scaffold.features.join(", ") : "None"],
|
|
36
|
+
]),
|
|
37
|
+
];
|
|
38
|
+
if (bundle.repository) {
|
|
39
|
+
lines.push("", "## Repository", ...renderKeyValueList([
|
|
40
|
+
["Provider", bundle.repository.provider],
|
|
41
|
+
["Organization", bundle.repository.organization],
|
|
42
|
+
["Project", bundle.repository.project],
|
|
43
|
+
["Repository", bundle.repository.repository],
|
|
44
|
+
["Default Branch", bundle.repository.defaultBranch],
|
|
45
|
+
]));
|
|
46
|
+
}
|
|
47
|
+
if (bundle.domain) {
|
|
48
|
+
lines.push("", "## Domain Inputs", ...renderUnknownList(bundle.domain.inputs), "", "## Domain Outputs", ...renderUnknownList(bundle.domain.outputs));
|
|
49
|
+
}
|
|
50
|
+
lines.push("", "## Goals", ...renderStringList(bundle.docs.serviceSpec.goals), "", "## Non-Goals", ...renderStringList(bundle.docs.serviceSpec.nonGoals), "", "## Acceptance Criteria", ...renderStringList(bundle.docs.serviceSpec.acceptanceCriteria), "");
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
export function generateAgentsMarkdown(bundle) {
|
|
54
|
+
const lines = [
|
|
55
|
+
"# AGENTS",
|
|
56
|
+
"",
|
|
57
|
+
"> This repository was bootstrapped from `service.bundle.json`. Regenerate derived docs from the bundle instead of treating this file as the source of truth.",
|
|
58
|
+
"",
|
|
59
|
+
"## Service",
|
|
60
|
+
...renderKeyValueList([
|
|
61
|
+
["Name", bundle.metadata.name],
|
|
62
|
+
["Display Name", bundle.metadata.displayName],
|
|
63
|
+
["Stack", bundle.scaffold.stack],
|
|
64
|
+
["Template", bundle.scaffold.template],
|
|
65
|
+
]),
|
|
66
|
+
"",
|
|
67
|
+
"## Project Context",
|
|
68
|
+
...renderStringList(bundle.docs.agents.projectContext),
|
|
69
|
+
"",
|
|
70
|
+
"## Guardrails",
|
|
71
|
+
...renderStringList(bundle.docs.agents.guardrails),
|
|
72
|
+
"",
|
|
73
|
+
"## First Tasks",
|
|
74
|
+
...renderStringList(bundle.docs.agents.firstTasks),
|
|
75
|
+
"",
|
|
76
|
+
"## Verification",
|
|
77
|
+
...renderStringList(bundle.docs.agents.verification),
|
|
78
|
+
"",
|
|
79
|
+
];
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
function validateServiceBundle(value, options) {
|
|
83
|
+
const schemaVersion = value.schemaVersion;
|
|
84
|
+
if (schemaVersion !== 1) {
|
|
85
|
+
throw new Error(`service bundle schemaVersion must be 1. Received ${JSON.stringify(schemaVersion)}.`);
|
|
86
|
+
}
|
|
87
|
+
const kind = value.kind;
|
|
88
|
+
if (kind !== "uns-service-bundle") {
|
|
89
|
+
throw new Error(`service bundle kind must be "uns-service-bundle". Received ${JSON.stringify(kind)}.`);
|
|
90
|
+
}
|
|
91
|
+
const metadata = requireObject(value.metadata, "metadata");
|
|
92
|
+
const scaffold = requireObject(value.scaffold, "scaffold");
|
|
93
|
+
const repository = optionalObject(value.repository, "repository");
|
|
94
|
+
const domain = optionalObject(value.domain, "domain");
|
|
95
|
+
const docs = optionalObject(value.docs, "docs");
|
|
96
|
+
optionalObject(value.provenance, "provenance");
|
|
97
|
+
const normalized = {
|
|
98
|
+
schemaVersion: 1,
|
|
99
|
+
kind: "uns-service-bundle",
|
|
100
|
+
metadata: {
|
|
101
|
+
name: requireNonEmptyString(metadata.name, "metadata.name"),
|
|
102
|
+
displayName: optionalNonEmptyString(metadata.displayName, "metadata.displayName"),
|
|
103
|
+
serviceType: optionalNonEmptyString(metadata.serviceType, "metadata.serviceType"),
|
|
104
|
+
summary: optionalNonEmptyString(metadata.summary, "metadata.summary"),
|
|
105
|
+
description: optionalNonEmptyString(metadata.description, "metadata.description"),
|
|
106
|
+
owner: optionalNonEmptyString(metadata.owner, "metadata.owner"),
|
|
107
|
+
tags: optionalStringArray(metadata.tags, "metadata.tags"),
|
|
108
|
+
},
|
|
109
|
+
scaffold: {
|
|
110
|
+
stack: requireNonEmptyString(scaffold.stack, "scaffold.stack"),
|
|
111
|
+
template: requireNonEmptyString(scaffold.template, "scaffold.template"),
|
|
112
|
+
features: dedupeStrings(optionalStringArray(scaffold.features, "scaffold.features")),
|
|
113
|
+
},
|
|
114
|
+
repository: repository
|
|
115
|
+
? {
|
|
116
|
+
provider: optionalNonEmptyString(repository.provider, "repository.provider"),
|
|
117
|
+
organization: optionalNonEmptyString(repository.organization, "repository.organization"),
|
|
118
|
+
project: optionalNonEmptyString(repository.project, "repository.project"),
|
|
119
|
+
repository: optionalNonEmptyString(repository.repository, "repository.repository"),
|
|
120
|
+
defaultBranch: optionalNonEmptyString(repository.defaultBranch, "repository.defaultBranch"),
|
|
121
|
+
}
|
|
122
|
+
: undefined,
|
|
123
|
+
domain: domain
|
|
124
|
+
? {
|
|
125
|
+
inputs: optionalUnknownArray(domain.inputs, "domain.inputs"),
|
|
126
|
+
outputs: optionalUnknownArray(domain.outputs, "domain.outputs"),
|
|
127
|
+
}
|
|
128
|
+
: undefined,
|
|
129
|
+
docs: {
|
|
130
|
+
serviceSpec: normalizeServiceSpecDocs(optionalObject(docs?.serviceSpec, "docs.serviceSpec")),
|
|
131
|
+
agents: normalizeAgentsDocs(optionalObject(docs?.agents, "docs.agents")),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
if (normalized.scaffold.stack !== options.expectedStack) {
|
|
135
|
+
throw new Error(`Bundle scaffold.stack is "${normalized.scaffold.stack}". Use ${options.counterpartCliName} create --bundle <path> instead of ${options.cliName}.`);
|
|
136
|
+
}
|
|
137
|
+
if (normalized.scaffold.template !== "default") {
|
|
138
|
+
throw new Error(`Bundle scaffold.template must be "default" for this MVP. Received "${normalized.scaffold.template}".`);
|
|
139
|
+
}
|
|
140
|
+
return normalized;
|
|
141
|
+
}
|
|
142
|
+
function normalizeServiceSpecDocs(value) {
|
|
143
|
+
return {
|
|
144
|
+
goals: optionalStringArray(value?.goals, "docs.serviceSpec.goals"),
|
|
145
|
+
nonGoals: optionalStringArray(value?.nonGoals, "docs.serviceSpec.nonGoals"),
|
|
146
|
+
acceptanceCriteria: optionalStringArray(value?.acceptanceCriteria, "docs.serviceSpec.acceptanceCriteria"),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function normalizeAgentsDocs(value) {
|
|
150
|
+
return {
|
|
151
|
+
projectContext: optionalStringArray(value?.projectContext, "docs.agents.projectContext"),
|
|
152
|
+
guardrails: optionalStringArray(value?.guardrails, "docs.agents.guardrails"),
|
|
153
|
+
firstTasks: optionalStringArray(value?.firstTasks, "docs.agents.firstTasks"),
|
|
154
|
+
verification: optionalStringArray(value?.verification, "docs.agents.verification"),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function parseJsonObject(raw, bundlePath) {
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(raw);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
throw new Error(`Failed to parse bundle JSON at ${bundlePath}: ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
return requireObject(parsed, "bundle");
|
|
166
|
+
}
|
|
167
|
+
function requireObject(value, path) {
|
|
168
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
169
|
+
throw new Error(`${path} must be an object.`);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function optionalObject(value, path) {
|
|
174
|
+
if (value === undefined || value === null) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return requireObject(value, path);
|
|
178
|
+
}
|
|
179
|
+
function requireNonEmptyString(value, path) {
|
|
180
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
181
|
+
throw new Error(`${path} is required and must be a non-empty string.`);
|
|
182
|
+
}
|
|
183
|
+
return value.trim();
|
|
184
|
+
}
|
|
185
|
+
function optionalNonEmptyString(value, path) {
|
|
186
|
+
if (value === undefined || value === null || value === "") {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
return requireNonEmptyString(value, path);
|
|
190
|
+
}
|
|
191
|
+
function optionalStringArray(value, path) {
|
|
192
|
+
if (value === undefined || value === null) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
if (!Array.isArray(value)) {
|
|
196
|
+
throw new Error(`${path} must be an array of strings.`);
|
|
197
|
+
}
|
|
198
|
+
return value.map((item, index) => requireNonEmptyString(item, `${path}[${index}]`));
|
|
199
|
+
}
|
|
200
|
+
function optionalUnknownArray(value, path) {
|
|
201
|
+
if (value === undefined || value === null) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
if (!Array.isArray(value)) {
|
|
205
|
+
throw new Error(`${path} must be an array.`);
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
function dedupeStrings(values) {
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
const deduped = [];
|
|
212
|
+
for (const value of values) {
|
|
213
|
+
if (!seen.has(value)) {
|
|
214
|
+
seen.add(value);
|
|
215
|
+
deduped.push(value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return deduped;
|
|
219
|
+
}
|
|
220
|
+
function renderKeyValueList(entries) {
|
|
221
|
+
return entries.map(([label, value]) => `- ${label}: ${value && value.trim() ? value : "Not specified"}`);
|
|
222
|
+
}
|
|
223
|
+
function renderStringList(items) {
|
|
224
|
+
if (!items.length) {
|
|
225
|
+
return ["- None specified."];
|
|
226
|
+
}
|
|
227
|
+
return items.map((item) => `- ${item}`);
|
|
228
|
+
}
|
|
229
|
+
function renderUnknownList(items) {
|
|
230
|
+
if (!items.length) {
|
|
231
|
+
return ["- None specified."];
|
|
232
|
+
}
|
|
233
|
+
return items.map((item) => {
|
|
234
|
+
if (typeof item === "string" && item.trim()) {
|
|
235
|
+
return `- ${item}`;
|
|
236
|
+
}
|
|
237
|
+
return `- \`${JSON.stringify(item)}\``;
|
|
238
|
+
});
|
|
239
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uns-kit/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.19",
|
|
4
4
|
"description": "Command line scaffolding tool for UNS applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"azure-devops-node-api": "^15.1.1",
|
|
29
|
-
"@uns-kit/core": "2.0.
|
|
29
|
+
"@uns-kit/core": "2.0.19"
|
|
30
30
|
},
|
|
31
31
|
"unsKitPackages": {
|
|
32
|
-
"@uns-kit/core": "2.0.
|
|
33
|
-
"@uns-kit/api": "2.0.
|
|
34
|
-
"@uns-kit/cron": "2.0.
|
|
35
|
-
"@uns-kit/temporal": "2.0.
|
|
32
|
+
"@uns-kit/core": "2.0.19",
|
|
33
|
+
"@uns-kit/api": "2.0.19",
|
|
34
|
+
"@uns-kit/cron": "2.0.19",
|
|
35
|
+
"@uns-kit/temporal": "2.0.18"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -p tsconfig.build.json",
|