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.
- package/CHANGELOG.md +516 -0
- package/README.md +8 -10
- package/dist/chunk-ZIUW7UEA.js +112 -0
- package/dist/created-summary.d.ts +48 -0
- package/dist/created-summary.js +16 -0
- package/dist/index.js +248 -157
- package/dist/templates/AGENTS.md +1 -1
- package/dist/templates/blank/Dockerfile +7 -4
- package/dist/templates/blank/README.md +32 -8
- package/dist/templates/blank/docker-compose.yml +1 -1
- package/dist/templates/blank/objectstack.config.ts +19 -15
- package/dist/templates/blank/objectstack.manifest.json +1 -1
- package/dist/templates/blank/package.json +3 -0
- package/dist/templates/blank/pnpm-workspace.yaml +61 -2
- package/dist/templates/blank/src/objects/note.object.ts +6 -2
- package/package.json +9 -4
|
@@ -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
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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/
|
|
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 =
|
|
167
|
-
const pkg = JSON.parse(
|
|
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(
|
|
248
|
+
console.log(chalk2.bold(`
|
|
175
249
|
\u25C6 ${title}`));
|
|
176
|
-
console.log(
|
|
250
|
+
console.log(chalk2.dim("\u2500".repeat(40)));
|
|
177
251
|
}
|
|
178
252
|
function printKV(key, value) {
|
|
179
|
-
console.log(` ${
|
|
253
|
+
console.log(` ${chalk2.dim(key + ":")} ${chalk2.white(value)}`);
|
|
180
254
|
}
|
|
181
255
|
function printSuccess(msg) {
|
|
182
|
-
console.log(
|
|
256
|
+
console.log(chalk2.green(` \u2713 ${msg}`));
|
|
183
257
|
}
|
|
184
258
|
function printError(msg) {
|
|
185
|
-
console.log(
|
|
259
|
+
console.log(chalk2.red(` \u2717 ${msg}`));
|
|
186
260
|
}
|
|
187
261
|
function printStep(msg) {
|
|
188
|
-
console.log(
|
|
262
|
+
console.log(chalk2.yellow(` \u2192 ${msg}`));
|
|
189
263
|
}
|
|
190
264
|
function printWarning(msg) {
|
|
191
|
-
console.log(
|
|
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 =
|
|
203
|
-
if (!
|
|
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 =
|
|
264
|
-
if (
|
|
287
|
+
const pkgPath = path4.join(targetDir, "package.json");
|
|
288
|
+
if (fs4.existsSync(pkgPath)) {
|
|
265
289
|
try {
|
|
266
|
-
const pkg = JSON.parse(
|
|
290
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
|
|
267
291
|
pkg.name = projectName;
|
|
268
292
|
syncObjectStackDeps(pkg, readCliVersion());
|
|
269
|
-
|
|
293
|
+
fs4.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
270
294
|
} catch {
|
|
271
295
|
}
|
|
272
296
|
}
|
|
273
|
-
const manifestPath =
|
|
274
|
-
if (
|
|
297
|
+
const manifestPath = path4.join(targetDir, "objectstack.manifest.json");
|
|
298
|
+
if (fs4.existsSync(manifestPath)) {
|
|
275
299
|
try {
|
|
276
|
-
const m = JSON.parse(
|
|
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
|
-
|
|
304
|
+
delete m.description;
|
|
305
|
+
fs4.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
|
|
281
306
|
} catch {
|
|
282
307
|
}
|
|
283
308
|
}
|
|
284
|
-
const configPath =
|
|
285
|
-
if (
|
|
286
|
-
let cfg =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
308
|
-
if (
|
|
309
|
-
let md =
|
|
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
|
-
|
|
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 =
|
|
342
|
+
const templatePath = path4.join(BUNDLED_TEMPLATES_DIR, "AGENTS.md");
|
|
317
343
|
let template;
|
|
318
344
|
try {
|
|
319
|
-
template =
|
|
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(
|
|
326
|
-
const copilotPath =
|
|
327
|
-
|
|
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
|
-
|
|
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: ${
|
|
408
|
+
`Template: ${templateNames().join(", ")}`,
|
|
340
409
|
"blank"
|
|
341
|
-
).option("--skip-install", "Skip dependency installation").option("--skip-skills", "Skip installing ObjectStack AI skills").action(
|
|
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(
|
|
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
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
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 ||
|
|
431
|
+
const projectName = name || path4.basename(cwd);
|
|
355
432
|
const namespace = sanitizeNamespace(projectName);
|
|
356
|
-
const targetDir = name ?
|
|
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 &&
|
|
364
|
-
const existing =
|
|
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
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
503
|
+
console.log(chalk2.bold(" Next steps:"));
|
|
416
504
|
if (!isCurrentDir) {
|
|
417
|
-
console.log(
|
|
505
|
+
console.log(chalk2.dim(` cd ${name}`));
|
|
418
506
|
}
|
|
419
507
|
if (options.skipInstall) {
|
|
420
|
-
console.log(
|
|
508
|
+
console.log(chalk2.dim(` ${pm} install`));
|
|
421
509
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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(
|
|
428
|
-
console.log(
|
|
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) {
|
package/dist/templates/AGENTS.md
CHANGED
|
@@ -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.
|
|
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://
|
|
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
|
|
24
|
-
#
|
|
25
|
-
#
|
|
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
|
|