create-objectstack 17.0.0 → 17.2.0

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.
@@ -0,0 +1,48 @@
1
+ /** A directory holding more than this many entries is collapsed to one line. */
2
+ declare const COLLAPSE_AT = 10;
3
+ /**
4
+ * Entries this module is willing to `lstat` per top-level entry before it
5
+ * stops counting and reports a lower bound.
6
+ *
7
+ * The budget is PER TOP-LEVEL ENTRY, not global, and that is load-bearing:
8
+ * with one shared budget, `node_modules/` (17,920 paths in the measurement
9
+ * above) exhausts it before the walk reaches the project's own files, and the
10
+ * summary silently truncates the very content it exists to disclose. Whether
11
+ * that happened would depend on `readdir` order.
12
+ */
13
+ declare const MEASURE_BUDGET = 2000;
14
+ interface SummaryEntry {
15
+ /** Project-relative path. Directories carry a trailing `/`. */
16
+ path: string;
17
+ kind: 'file' | 'dir';
18
+ /** Files and symlinks in the subtree (always 1 for a file). */
19
+ entries: number;
20
+ /** Total size in bytes. Meaningless when `truncated`. */
21
+ bytes: number;
22
+ /** Measurement stopped at the budget — `entries` and `bytes` are lower bounds. */
23
+ truncated: boolean;
24
+ }
25
+ /**
26
+ * Summarize everything under `root`, collapsing large directories.
27
+ *
28
+ * Returns files first (alphabetical), then collapsed directories
29
+ * (alphabetical), so the enumerated content reads as a list and the bulk
30
+ * trees read as a block with their sizes.
31
+ */
32
+ declare function summarizeTree(root: string): SummaryEntry[];
33
+ /** Human-readable byte count. */
34
+ declare function formatBytes(bytes: number): string;
35
+ /** The measurement note that follows a collapsed directory's path. */
36
+ declare function describeEntry(entry: SummaryEntry): string;
37
+ /**
38
+ * The property this module exists to hold: every path in `written` is either
39
+ * named outright by a summary entry, or lies beneath a directory entry that
40
+ * is. Returns the paths that are NOT reachable — empty means the summary is
41
+ * complete.
42
+ *
43
+ * Exported because it is the assertion, and an assertion that lives only in a
44
+ * test file cannot be run against a real scaffold from anywhere else.
45
+ */
46
+ declare function unreachablePaths(entries: SummaryEntry[], written: string[]): string[];
47
+
48
+ export { COLLAPSE_AT, MEASURE_BUDGET, type SummaryEntry, describeEntry, formatBytes, summarizeTree, unreachablePaths };
@@ -0,0 +1,16 @@
1
+ import {
2
+ COLLAPSE_AT,
3
+ MEASURE_BUDGET,
4
+ describeEntry,
5
+ formatBytes,
6
+ summarizeTree,
7
+ unreachablePaths
8
+ } from "./chunk-ZIUW7UEA.js";
9
+ export {
10
+ COLLAPSE_AT,
11
+ MEASURE_BUDGET,
12
+ describeEntry,
13
+ formatBytes,
14
+ summarizeTree,
15
+ unreachablePaths
16
+ };
package/dist/index.js CHANGED
@@ -1,16 +1,15 @@
1
+ import {
2
+ describeEntry,
3
+ summarizeTree
4
+ } from "./chunk-ZIUW7UEA.js";
5
+
1
6
  // src/index.ts
2
7
  import { Command } from "commander";
3
- import chalk from "chalk";
4
- import fs3 from "fs";
5
- import path3 from "path";
6
- import os from "os";
8
+ import chalk2 from "chalk";
9
+ import fs4 from "fs";
10
+ import path4 from "path";
7
11
  import { execSync } from "child_process";
8
12
  import { fileURLToPath } from "url";
9
- import { pipeline } from "stream/promises";
10
- import { createGunzip } from "zlib";
11
- import { createWriteStream, createReadStream } from "fs";
12
- import { mkdtemp, rm } from "fs/promises";
13
- import * as tar from "tar";
14
13
 
15
14
  // src/pkg-utils.ts
16
15
  function syncObjectStackDeps(pkg, version) {
@@ -114,39 +113,114 @@ function findStaleNamespacePrefixes(dir, oldNs) {
114
113
  return stale;
115
114
  }
116
115
 
117
- // src/index.ts
118
- var __filename2 = fileURLToPath(import.meta.url);
119
- var __dirname2 = path3.dirname(__filename2);
120
- var BUNDLED_TEMPLATES_DIR = path3.resolve(__dirname2, "templates");
121
- var REMOTE_REPO = "objectstack-ai/templates";
122
- var REMOTE_BRANCH = "main";
123
- var REMOTE_TARBALL_URL = `https://codeload.github.com/${REMOTE_REPO}/tar.gz/refs/heads/${REMOTE_BRANCH}`;
116
+ // src/template-registry.ts
124
117
  var TEMPLATES = {
125
118
  blank: {
126
119
  description: "Minimal starter \u2014 one object, REST API, ready to extend",
127
120
  source: { kind: "bundled", dir: "blank" }
128
- },
129
- todo: {
130
- description: "Universal task & project management starter",
131
- source: { kind: "remote", pkg: "todo" }
132
- },
133
- compliance: {
134
- description: "Compliance posture & evidence management (SOC2 / ISO27001)",
135
- source: { kind: "remote", pkg: "compliance" }
136
- },
137
- content: {
138
- description: "Content marketing pipeline \u2014 editorial calendar & channel ROI",
139
- source: { kind: "remote", pkg: "content" }
140
- },
141
- contracts: {
142
- description: "Post-signature CLM \u2014 approvals, obligations, renewals",
143
- source: { kind: "remote", pkg: "contracts" }
144
- },
145
- procurement: {
146
- description: "Source-to-pay \u2014 vendors, POs, receipts, invoice matching",
147
- source: { kind: "remote", pkg: "procurement" }
148
121
  }
149
122
  };
123
+ var RETIRED_TEMPLATES = [
124
+ "todo",
125
+ "compliance",
126
+ "content",
127
+ "contracts",
128
+ "procurement"
129
+ ];
130
+ function lookupTemplate(name) {
131
+ const template = TEMPLATES[name];
132
+ if (template) return { kind: "found", name, template };
133
+ if (RETIRED_TEMPLATES.includes(name)) return { kind: "retired", name };
134
+ return { kind: "unknown", name };
135
+ }
136
+ function templateNames() {
137
+ return Object.keys(TEMPLATES);
138
+ }
139
+
140
+ // src/runtime-image.ts
141
+ import fs3 from "fs";
142
+ import path3 from "path";
143
+ var RUNTIME_FROM_RE = /^FROM ghcr\.io\/objectstack-ai\/objectstack:([A-Za-z0-9_][A-Za-z0-9_.+-]*)[ \t]*$/;
144
+ var PINNABLE_VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
145
+ var PROSE_COMMENT_RE = /^#[ \t]+\S/;
146
+ function readResolvedCliVersion(targetDir) {
147
+ const pkgPath = path3.join(
148
+ targetDir,
149
+ "node_modules",
150
+ "@objectstack",
151
+ "cli",
152
+ "package.json"
153
+ );
154
+ try {
155
+ const version = JSON.parse(fs3.readFileSync(pkgPath, "utf8")).version;
156
+ return typeof version === "string" && PINNABLE_VERSION_RE.test(version) ? version : void 0;
157
+ } catch {
158
+ return void 0;
159
+ }
160
+ }
161
+ function pinnedComment(version) {
162
+ return [
163
+ "# Pinned at scaffold time to the @objectstack/cli version this project",
164
+ "# resolved, so the runtime runs the same CLI that built your artifact.",
165
+ "# Move both together when you upgrade \u2014 see docker/README.md tag table.",
166
+ `FROM ghcr.io/objectstack-ai/objectstack:${version}`
167
+ ];
168
+ }
169
+ function pinRuntimeImage(targetDir, version) {
170
+ if (!PINNABLE_VERSION_RE.test(version)) {
171
+ return { pinned: false, reason: `'${version}' is not a pinnable version` };
172
+ }
173
+ const dockerfile = path3.join(targetDir, "Dockerfile");
174
+ let text;
175
+ try {
176
+ text = fs3.readFileSync(dockerfile, "utf8");
177
+ } catch {
178
+ return { pinned: false, reason: "no Dockerfile in the scaffolded project" };
179
+ }
180
+ const lines = text.split("\n");
181
+ const fromIdx = lines.findIndex((line) => RUNTIME_FROM_RE.test(line));
182
+ if (fromIdx === -1) {
183
+ return {
184
+ pinned: false,
185
+ reason: "no `FROM ghcr.io/objectstack-ai/objectstack:<tag>` line found"
186
+ };
187
+ }
188
+ let start = fromIdx;
189
+ while (start > 0 && PROSE_COMMENT_RE.test(lines[start - 1])) start--;
190
+ lines.splice(start, fromIdx - start + 1, ...pinnedComment(version));
191
+ const pinnedText = lines.join("\n");
192
+ const check = pinnedText.split("\n").map((line) => RUNTIME_FROM_RE.exec(line)).find((match) => match !== null);
193
+ if (!check || check[1] !== version) {
194
+ return { pinned: false, reason: "rewrite did not produce the pinned tag" };
195
+ }
196
+ fs3.writeFileSync(dockerfile, pinnedText);
197
+ return { pinned: true, tag: version };
198
+ }
199
+
200
+ // src/banner.ts
201
+ import chalk from "chalk";
202
+ var PREFIX = " \u25C6 Create ObjectStack ";
203
+ var MIN_INNER_WIDTH = 35;
204
+ var MIN_TRAILING_PAD = 3;
205
+ function renderVersionBanner(version) {
206
+ const versionLabel = `v${version}`;
207
+ const innerWidth = Math.max(
208
+ MIN_INNER_WIDTH,
209
+ PREFIX.length + versionLabel.length + MIN_TRAILING_PAD
210
+ );
211
+ const trailingPad = innerWidth - PREFIX.length - versionLabel.length;
212
+ const border = "\u2550".repeat(innerWidth);
213
+ return [
214
+ chalk.bold.cyan(` \u2554${border}\u2557`),
215
+ chalk.bold.cyan(" \u2551") + chalk.bold(PREFIX) + chalk.dim(versionLabel) + chalk.bold.cyan(`${" ".repeat(trailingPad)}\u2551`),
216
+ chalk.bold.cyan(` \u255A${border}\u255D`)
217
+ ];
218
+ }
219
+
220
+ // src/index.ts
221
+ var __filename2 = fileURLToPath(import.meta.url);
222
+ var __dirname2 = path4.dirname(__filename2);
223
+ var BUNDLED_TEMPLATES_DIR = path4.resolve(__dirname2, "templates");
150
224
  function toTitleCase(str) {
151
225
  return str.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
152
226
  }
@@ -163,32 +237,32 @@ function sanitizeNamespace(name) {
163
237
  }
164
238
  function readCliVersion() {
165
239
  try {
166
- const pkgPath = path3.resolve(__dirname2, "..", "package.json");
167
- const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
240
+ const pkgPath = path4.resolve(__dirname2, "..", "package.json");
241
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
168
242
  return String(pkg.version || "0.0.0");
169
243
  } catch {
170
244
  return "0.0.0";
171
245
  }
172
246
  }
173
247
  function printHeader(title) {
174
- console.log(chalk.bold(`
248
+ console.log(chalk2.bold(`
175
249
  \u25C6 ${title}`));
176
- console.log(chalk.dim("\u2500".repeat(40)));
250
+ console.log(chalk2.dim("\u2500".repeat(40)));
177
251
  }
178
252
  function printKV(key, value) {
179
- console.log(` ${chalk.dim(key + ":")} ${chalk.white(value)}`);
253
+ console.log(` ${chalk2.dim(key + ":")} ${chalk2.white(value)}`);
180
254
  }
181
255
  function printSuccess(msg) {
182
- console.log(chalk.green(` \u2713 ${msg}`));
256
+ console.log(chalk2.green(` \u2713 ${msg}`));
183
257
  }
184
258
  function printError(msg) {
185
- console.log(chalk.red(` \u2717 ${msg}`));
259
+ console.log(chalk2.red(` \u2717 ${msg}`));
186
260
  }
187
261
  function printStep(msg) {
188
- console.log(chalk.yellow(` \u2192 ${msg}`));
262
+ console.log(chalk2.yellow(` \u2192 ${msg}`));
189
263
  }
190
264
  function printWarning(msg) {
191
- console.log(chalk.yellow(` \u26A0 ${msg}`));
265
+ console.log(chalk2.yellow(` \u26A0 ${msg}`));
192
266
  }
193
267
  function detectPackageManager() {
194
268
  try {
@@ -199,98 +273,50 @@ function detectPackageManager() {
199
273
  }
200
274
  }
201
275
  function loadBundled(templateDir, targetDir) {
202
- const src = path3.join(BUNDLED_TEMPLATES_DIR, templateDir);
203
- if (!fs3.existsSync(src)) {
276
+ const src = path4.join(BUNDLED_TEMPLATES_DIR, templateDir);
277
+ if (!fs4.existsSync(src)) {
204
278
  throw new Error(`Bundled template missing on disk: ${src}`);
205
279
  }
206
280
  const collected = [];
207
281
  copyDir(src, targetDir, collected);
208
282
  return collected;
209
283
  }
210
- async function downloadTarball(url, destFile) {
211
- const res = await fetch(url, { redirect: "follow" });
212
- if (!res.ok || !res.body) {
213
- throw new Error(`Download failed: ${url} (${res.status})`);
214
- }
215
- const out = createWriteStream(destFile);
216
- for await (const chunk of res.body) {
217
- out.write(chunk);
218
- }
219
- await new Promise((resolve, reject) => {
220
- out.end((err) => err ? reject(err) : resolve());
221
- });
222
- }
223
- async function loadRemote(pkgName, targetDir) {
224
- const tmp = await mkdtemp(path3.join(os.tmpdir(), "create-objectstack-"));
225
- try {
226
- const tarball = path3.join(tmp, "templates.tar.gz");
227
- printStep(`Fetching template "${pkgName}" from ${REMOTE_REPO}@${REMOTE_BRANCH}\u2026`);
228
- await downloadTarball(REMOTE_TARBALL_URL, tarball);
229
- fs3.mkdirSync(targetDir, { recursive: true });
230
- const collected = [];
231
- await pipeline(
232
- createReadStream(tarball),
233
- createGunzip(),
234
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
235
- tar.extract({
236
- cwd: targetDir,
237
- strip: 3,
238
- filter: (p) => {
239
- const parts = p.split("/");
240
- return parts[1] === "packages" && parts[2] === pkgName && parts.length > 3;
241
- },
242
- onentry: (entry) => {
243
- if (entry.type === "File") {
244
- const parts = entry.path.split("/").slice(3);
245
- if (parts.length > 0) collected.push(parts.join("/"));
246
- }
247
- }
248
- })
249
- );
250
- if (collected.length === 0) {
251
- throw new Error(
252
- `Template "${pkgName}" not found in ${REMOTE_REPO}@${REMOTE_BRANCH} (expected packages/${pkgName}/).`
253
- );
254
- }
255
- return collected;
256
- } finally {
257
- await rm(tmp, { recursive: true, force: true });
258
- }
259
- }
260
284
  function rewriteProjectIdentity(targetDir, projectName, namespace) {
261
285
  const title = toTitleCase(projectName);
262
286
  const templateNamespace = readTemplateNamespace(targetDir);
263
- const pkgPath = path3.join(targetDir, "package.json");
264
- if (fs3.existsSync(pkgPath)) {
287
+ const pkgPath = path4.join(targetDir, "package.json");
288
+ if (fs4.existsSync(pkgPath)) {
265
289
  try {
266
- const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf8"));
290
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
267
291
  pkg.name = projectName;
268
292
  syncObjectStackDeps(pkg, readCliVersion());
269
- fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
293
+ fs4.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
270
294
  } catch {
271
295
  }
272
296
  }
273
- const manifestPath = path3.join(targetDir, "objectstack.manifest.json");
274
- if (fs3.existsSync(manifestPath)) {
297
+ const manifestPath = path4.join(targetDir, "objectstack.manifest.json");
298
+ if (fs4.existsSync(manifestPath)) {
275
299
  try {
276
- const m = JSON.parse(fs3.readFileSync(manifestPath, "utf8"));
300
+ const m = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
277
301
  m.name = projectName;
278
302
  m.displayName = title;
279
303
  if ("namespace" in m) m.namespace = namespace;
280
- fs3.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
304
+ delete m.description;
305
+ fs4.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
281
306
  } catch {
282
307
  }
283
308
  }
284
- const configPath = path3.join(targetDir, "objectstack.config.ts");
285
- if (fs3.existsSync(configPath)) {
286
- let cfg = fs3.readFileSync(configPath, "utf8");
309
+ const configPath = path4.join(targetDir, "objectstack.config.ts");
310
+ if (fs4.existsSync(configPath)) {
311
+ let cfg = fs4.readFileSync(configPath, "utf8");
287
312
  cfg = cfg.replace(/(\bid:\s*)(['"`])[^'"`]*\2/, `$1$2${projectName}$2`);
288
313
  cfg = cfg.replace(/(\bnamespace:\s*)(['"`])[^'"`]*\2/, `$1$2${namespace}$2`);
289
314
  cfg = cfg.replace(/(\bname:\s*)(['"`])[^'"`]*\2/, `$1$2${title}$2`);
290
- fs3.writeFileSync(configPath, cfg);
315
+ cfg = cfg.replace(/^[ \t]*description:\s*(['"`])[^'"`]*\1,?\r?\n/m, "");
316
+ fs4.writeFileSync(configPath, cfg);
291
317
  }
292
318
  if (templateNamespace && namespace !== templateNamespace) {
293
- const srcDir = path3.join(targetDir, "src");
319
+ const srcDir = path4.join(targetDir, "src");
294
320
  rewriteObjectNamePrefix(srcDir, templateNamespace, namespace);
295
321
  const stale = findStaleNamespacePrefixes(srcDir, templateNamespace);
296
322
  if (stale.length > 0) {
@@ -304,97 +330,155 @@ The generated project would fail 'objectstack build' on the \${namespace}_\${sho
304
330
  );
305
331
  }
306
332
  }
307
- const readmePath = path3.join(targetDir, "README.md");
308
- if (fs3.existsSync(readmePath)) {
309
- let md = fs3.readFileSync(readmePath, "utf8");
333
+ const readmePath = path4.join(targetDir, "README.md");
334
+ if (fs4.existsSync(readmePath)) {
335
+ let md = fs4.readFileSync(readmePath, "utf8");
310
336
  md = md.replace(/^#\s+.*$/m, `# ${title}`);
311
- fs3.writeFileSync(readmePath, md);
337
+ fs4.writeFileSync(readmePath, md);
312
338
  }
313
339
  writeAgentGuides(targetDir, title, projectName);
314
340
  }
315
341
  function writeAgentGuides(targetDir, title, projectName) {
316
- const templatePath = path3.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
342
+ const templatePath = path4.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
317
343
  let template;
318
344
  try {
319
- template = fs3.readFileSync(templatePath, "utf8");
345
+ template = fs4.readFileSync(templatePath, "utf8");
320
346
  } catch (err) {
321
347
  if (err?.code === "ENOENT") return;
322
348
  throw err;
323
349
  }
324
350
  const rendered = template.replace(/\{\{PROJECT_TITLE\}\}/g, title).replace(/\{\{PROJECT_NAME\}\}/g, projectName);
325
- writeIfAbsent(path3.join(targetDir, "AGENTS.md"), rendered);
326
- const copilotPath = path3.join(targetDir, ".github", "copilot-instructions.md");
327
- fs3.mkdirSync(path3.dirname(copilotPath), { recursive: true });
351
+ writeIfAbsent(path4.join(targetDir, "AGENTS.md"), rendered);
352
+ const copilotPath = path4.join(targetDir, ".github", "copilot-instructions.md");
353
+ fs4.mkdirSync(path4.dirname(copilotPath), { recursive: true });
328
354
  writeIfAbsent(copilotPath, rendered);
329
355
  }
356
+ function topLevelNames(dir) {
357
+ try {
358
+ return new Set(fs4.readdirSync(dir));
359
+ } catch {
360
+ return /* @__PURE__ */ new Set();
361
+ }
362
+ }
363
+ function printCreatedSummary(targetDir, opts) {
364
+ const entries = summarizeTree(targetDir);
365
+ if (entries.length === 0) return;
366
+ console.log(
367
+ chalk2.bold(opts.wasEmpty ? " Created files:" : " Project contents:")
368
+ );
369
+ if (!opts.wasEmpty) {
370
+ console.log(
371
+ chalk2.dim(" (the directory already had contents; this lists all of it)")
372
+ );
373
+ }
374
+ const isSkillPath = (p) => opts.skillPaths.has(p.split("/")[0]);
375
+ const width = Math.min(
376
+ 44,
377
+ Math.max(...entries.map((e) => e.path.length)) + 2
378
+ );
379
+ let flagged = false;
380
+ for (const entry of entries) {
381
+ const note = describeEntry(entry);
382
+ const flag = isSkillPath(entry.path);
383
+ if (flag) flagged = true;
384
+ const pad = note || flag ? entry.path.padEnd(width) : entry.path;
385
+ const line = ` + ${pad}${note ? chalk2.dim(note) : ""}`;
386
+ console.log(chalk2.green(line) + (flag ? chalk2.yellow(" \u26A0 skills") : ""));
387
+ }
388
+ if (flagged) {
389
+ console.log("");
390
+ console.log(
391
+ chalk2.yellow(" \u26A0 Skill files run with your coding agent's full permissions.")
392
+ );
393
+ console.log(
394
+ chalk2.dim(" Review the paths marked \u26A0 above before letting an agent use them.")
395
+ );
396
+ }
397
+ console.log("");
398
+ }
330
399
  function writeIfAbsent(filePath, contents) {
331
400
  try {
332
- fs3.writeFileSync(filePath, contents, { flag: "wx" });
401
+ fs4.writeFileSync(filePath, contents, { flag: "wx" });
333
402
  } catch (err) {
334
403
  if (err?.code !== "EEXIST") throw err;
335
404
  }
336
405
  }
337
406
  var program = new Command().name("create-objectstack").description("Create a new ObjectStack environment").version(readCliVersion()).argument("[name]", "Environment name (defaults to current directory name)").option(
338
407
  "-t, --template <template>",
339
- `Template: ${Object.keys(TEMPLATES).join(", ")}`,
408
+ `Template: ${templateNames().join(", ")}`,
340
409
  "blank"
341
- ).option("--skip-install", "Skip dependency installation").option("--skip-skills", "Skip installing ObjectStack AI skills").action(async (name, options) => {
410
+ ).option("--skip-install", "Skip dependency installation").option("--skip-skills", "Skip installing ObjectStack AI skills").action((name, options) => {
342
411
  console.log("");
343
- console.log(chalk.bold.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
344
- console.log(chalk.bold.cyan(" \u2551") + chalk.bold(" \u25C6 Create ObjectStack ") + chalk.dim("v6.x") + chalk.bold.cyan(" \u2551"));
345
- console.log(chalk.bold.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
412
+ for (const line of renderVersionBanner(readCliVersion())) console.log(line);
346
413
  printHeader("New Environment");
347
- const template = TEMPLATES[options.template];
348
- if (!template) {
349
- printError(`Unknown template: ${options.template}`);
350
- console.log(chalk.dim(` Available: ${Object.keys(TEMPLATES).join(", ")}`));
414
+ const lookup = lookupTemplate(options.template);
415
+ if (lookup.kind !== "found") {
416
+ if (lookup.kind === "retired") {
417
+ printError(`Template "${lookup.name}" has been retired and is no longer available.`);
418
+ console.log(
419
+ chalk2.dim(
420
+ " It was delisted from the ObjectStack template marketplace and is no longer maintained."
421
+ )
422
+ );
423
+ } else {
424
+ printError(`Unknown template: ${lookup.name}`);
425
+ }
426
+ console.log(chalk2.dim(` Available: ${templateNames().join(", ")}`));
351
427
  process.exit(1);
352
428
  }
429
+ const template = lookup.template;
353
430
  const cwd = process.cwd();
354
- const projectName = name || path3.basename(cwd);
431
+ const projectName = name || path4.basename(cwd);
355
432
  const namespace = sanitizeNamespace(projectName);
356
- const targetDir = name ? path3.resolve(cwd, name) : cwd;
433
+ const targetDir = name ? path4.resolve(cwd, name) : cwd;
357
434
  const isCurrentDir = targetDir === cwd;
435
+ const pm = detectPackageManager();
358
436
  printKV("Environment", projectName);
359
437
  printKV("Namespace", namespace);
360
438
  printKV("Template", `${options.template} \u2014 ${template.description}`);
361
439
  printKV("Directory", targetDir);
362
440
  console.log("");
363
- if (!isCurrentDir && fs3.existsSync(targetDir)) {
364
- const existing = fs3.readdirSync(targetDir);
441
+ if (!isCurrentDir && fs4.existsSync(targetDir)) {
442
+ const existing = fs4.readdirSync(targetDir);
365
443
  if (existing.length > 0) {
366
444
  printError(`Directory already exists and is not empty: ${targetDir}`);
367
445
  process.exit(1);
368
446
  }
369
447
  }
448
+ const targetWasEmpty = topLevelNames(targetDir).size === 0;
370
449
  try {
371
- fs3.mkdirSync(targetDir, { recursive: true });
372
- let createdFiles;
373
- if (template.source.kind === "bundled") {
374
- createdFiles = loadBundled(template.source.dir, targetDir);
375
- } else {
376
- createdFiles = await loadRemote(template.source.pkg, targetDir);
377
- }
450
+ fs4.mkdirSync(targetDir, { recursive: true });
451
+ const createdFiles = loadBundled(template.source.dir, targetDir);
378
452
  rewriteProjectIdentity(targetDir, projectName, namespace);
379
- console.log(chalk.bold(" Created files:"));
380
- for (const f of createdFiles.slice(0, 20)) {
381
- console.log(chalk.green(` + ${f}`));
382
- }
383
- if (createdFiles.length > 20) {
384
- console.log(chalk.dim(` \u2026 and ${createdFiles.length - 20} more`));
385
- }
453
+ printSuccess(`Template files written (${createdFiles.length})`);
386
454
  console.log("");
387
455
  if (!options.skipInstall) {
388
456
  printStep("Installing dependencies...");
457
+ let installed = false;
389
458
  try {
390
- const pm = detectPackageManager();
391
459
  execSync(`${pm} install`, { stdio: "inherit", cwd: targetDir });
460
+ installed = true;
392
461
  console.log("");
393
462
  } catch {
394
- printWarning("Dependency installation failed. Run `npm install` manually.");
463
+ printWarning(`Dependency installation failed. Run \`${pm} install\` manually.`);
395
464
  console.log("");
396
465
  }
466
+ if (installed) {
467
+ const resolved = readResolvedCliVersion(targetDir);
468
+ if (resolved) {
469
+ const result = pinRuntimeImage(targetDir, resolved);
470
+ if (result.pinned) {
471
+ printSuccess(`Dockerfile runtime image pinned to ${result.tag}`);
472
+ } else {
473
+ printWarning(
474
+ `Could not pin the Dockerfile runtime image (${result.reason}); it still reads \`latest\` \u2014 pin it to ${resolved} before deploying.`
475
+ );
476
+ }
477
+ console.log("");
478
+ }
479
+ }
397
480
  }
481
+ const beforeSkills = topLevelNames(targetDir);
398
482
  if (!options.skipInstall && !options.skipSkills) {
399
483
  printStep("Installing AI skills for your coding agent...");
400
484
  try {
@@ -410,22 +494,29 @@ var program = new Command().name("create-objectstack").description("Create a new
410
494
  console.log("");
411
495
  }
412
496
  }
497
+ const skillPaths = new Set(
498
+ [...topLevelNames(targetDir)].filter((p) => !beforeSkills.has(p))
499
+ );
500
+ printCreatedSummary(targetDir, { wasEmpty: targetWasEmpty, skillPaths });
413
501
  printSuccess("Environment created!");
414
502
  console.log("");
415
- console.log(chalk.bold(" Next steps:"));
503
+ console.log(chalk2.bold(" Next steps:"));
416
504
  if (!isCurrentDir) {
417
- console.log(chalk.dim(` cd ${name}`));
505
+ console.log(chalk2.dim(` cd ${name}`));
418
506
  }
419
507
  if (options.skipInstall) {
420
- console.log(chalk.dim(" npm install"));
508
+ console.log(chalk2.dim(` ${pm} install`));
421
509
  }
422
- console.log(chalk.dim(" npm run dev # Start development server"));
423
- console.log(chalk.dim(" npm run validate # Verify metadata: schema + predicates + bindings"));
424
- console.log(chalk.dim(" # (run after every metadata edit \u2014 see AGENTS.md)"));
510
+ const devLabel = `${pm} run dev`;
511
+ const validateLabel = `${pm} run validate`;
512
+ const labelWidth = Math.max(devLabel.length, validateLabel.length) + 3;
513
+ console.log(chalk2.dim(` ${devLabel.padEnd(labelWidth)}# Start development server`));
514
+ console.log(chalk2.dim(` ${validateLabel.padEnd(labelWidth)}# Verify metadata: schema + predicates + bindings`));
515
+ console.log(chalk2.dim(` ${" ".repeat(labelWidth)}# (run after every metadata edit \u2014 see AGENTS.md)`));
425
516
  if (options.skipInstall || options.skipSkills) {
426
517
  console.log("");
427
- console.log(chalk.bold(" AI Skills (recommended):"));
428
- console.log(chalk.dim(" npx skills add objectstack-ai/objectstack/skills"));
518
+ console.log(chalk2.bold(" AI Skills (recommended):"));
519
+ console.log(chalk2.dim(" npx skills add objectstack-ai/objectstack/skills"));
429
520
  }
430
521
  console.log("");
431
522
  } catch (error) {
@@ -96,6 +96,6 @@ Skills are triggered automatically based on task context:
96
96
 
97
97
  ## Learn More
98
98
 
99
- - [ObjectStack Documentation](https://objectstack.com/docs)
99
+ - [ObjectStack Documentation](https://objectstack.ai/docs)
100
100
  - [GitHub: objectstack-ai/objectstack](https://github.com/objectstack-ai/objectstack)
101
101
  - [Skills CLI](https://skills.sh/) — Manage AI skills across agents
@@ -7,7 +7,7 @@
7
7
  # my-app
8
8
  #
9
9
  # Or run the full app + Postgres stack: see docker-compose.yml.
10
- # Docs: https://docs.objectstack.ai/docs/deployment/self-hosting
10
+ # Docs: https://objectstack.ai/docs/deployment/self-hosting
11
11
 
12
12
  # ── Build stage: compile TypeScript metadata to the artifact ─────────
13
13
  FROM node:22-slim AS build
@@ -20,9 +20,12 @@ RUN npx os build # → dist/objectstack.json
20
20
  # ── Runtime: the official ObjectStack runtime image ──────────────────
21
21
  # Ships Node + @objectstack/cli with `os start`, a non-root user, the
22
22
  # /api/v1/health HEALTHCHECK, and OS_ARTIFACT_PATH/OS_PORT preset (port 8080)
23
- # — see docker/README.md in the framework repo. Pin the tag to the
24
- # @objectstack/cli version in your package.json so the runtime matches the
25
- # CLI that built the artifact.
23
+ # — see the self-hosting guide linked above.
24
+ #
25
+ # Dependencies were not installed while scaffolding, so the tag below could
26
+ # not be resolved for you. `latest` floats to whatever release is newest,
27
+ # which is unrelated to the @objectstack/cli your app resolves — pin it to
28
+ # that version, the CLI that builds your artifact, before you deploy.
26
29
  FROM ghcr.io/objectstack-ai/objectstack:latest
27
30
  COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json
28
31